我的服务端是对接的阿里云的一个AI语音服务(不是百炼),使用的技术栈是websocket通信传输音频的blob数据,每个blob数据是一小段音频,大概半个字到一个字的量,数据大小4032Byte,也就是差不多4k,有些会大小不一;
现在遇到的问题是这样的,
我用Vue3起了一个项目,先是使用了Web Audio API来对音频进行播放,这一切都正常,我先是用电脑的浏览器播放测试,然后我用安卓手机浏览器打开测试,都正常,我后来使用苹果手机的浏览器打开测试的时候,无法播放出声音,查了一下资料才知道,苹果手机的限制问题和兼容性问题,没有直接支持播放这样的接口函数可以用的,需要自行对每段音频缓存来播放每一段音频,后来改了一下,根据每段进行播放,也做了环境判断,IOS环境和非IOS环境的播放,为了查出具体问题,我把每段音频每隔3秒进行了播放,发现在每段音频最后会出现一个小爆音,上一段和下一段衔接也不是很好,音频听起来,就有点很大的顿挫感;下面我会提供测试服务器的测试地址,和我的音频处理的核心代码:
websocket接收到音频后,会调用AudioServicer的 addAudioToBuffer函数,将接收到的音频块传入,如果有对这个技术熟悉的朋友可以付费解决,有意者直接报价
ios环境打开:safari浏览器或者微信、支付宝直接打开都可以,如果测试差别,也可以用电脑打开和安卓手机打开听效果
测试服务器地址:
https://aiu-test.szwalkplay.com/?code=test00000001
// 音频处理服务类
import { convertBitDepth } from '../utils/AudioUtil';
import config from '@/config';
import IOSAudioStreamer from './IOSAudioStreamer';
class AudioService {
constructor() {
this.mediaRecorder = null;
this.audioContext = null;
this.isRecording = false;
this.isPlaying = false;
this.targetSampleRate = config.audio.sampleRate;
this.bitDepth = config.audio.bitDepth;
this.channels = config.audio.channels;
this.mediaSource = null;
this.sourceBuffer = null;
this.audioElement = null;
this.isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
this.iosPlayer = new IOSAudioStreamer();
this.iosPlayer.initByUserGesture();
}
// 初始化音频上下文
async initAudioContext() {
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: this.targetSampleRate,
latencyHint: 'interactive'
});
if (this.audioContext.state === 'suspended') {
await this.audioContext.resume();
}
}
}
// 初始化MediaSource
async initMediaSource() {
if (!this.mediaSource) {
this.mediaSource = new MediaSource();
this.audioElement = new Audio();
this.audioElement.src = URL.createObjectURL(this.mediaSource);
this.mediaSource.addEventListener('sourceopen', () => {
try {
this.sourceBuffer = this.mediaSource.addSourceBuffer('audio/mpeg');
} catch (error) {
console.error('创建 SourceBuffer 失败:', error);
}
});
}
}
// 开始录音
async startRecording(onDataAvailable) {
try {
await this.initAudioContext();
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: this.targetSampleRate,
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
}
});
const sourceNode = this.audioContext.createMediaStreamSource(stream);
const processorNode = this.audioContext.createScriptProcessor(4096, 1, 1);
processorNode.onaudioprocess = (e) => {
if (!this.isRecording) return;
const inputBuffer = e.inputBuffer;
const channelData = inputBuffer.getChannelData(0);
const inputArray = new Array(channelData.length);
const outputArray = new Int16Array(channelData.length);
for (let i = 0; i < channelData.length; i++) {
inputArray[i] = channelData[i];
}
convertBitDepth(
inputArray,
"32f",
outputArray,
"16"
);
onDataAvailable(outputArray.buffer);
};
sourceNode.connect(processorNode);
processorNode.connect(this.audioContext.destination);
this.isRecording = true;
console.log('录音已开始');
} catch (error) {
console.error('录音启动失败:', error);
throw error;
}
}
// 停止录音
stopRecording() {
if (this.isRecording) {
this.isRecording = false;
if (this.processorNode) {
this.processorNode.disconnect();
}
if (this.sourceNode) {
this.sourceNode.disconnect();
}
if (this.sourceNode?.mediaStream) {
this.sourceNode.mediaStream.getTracks().forEach(track => track.stop());
}
console.log('录音已停止');
}
}
// 添加音频数据到缓冲区
async addAudioToBuffer(audioData) {
try {
if (this.isIOS) {
const blob = new Blob([audioData], {type: 'audio/mpeg'});
await this.iosPlayer.feedData(blob);
} else {
await this.initAudioContext();
await this.initMediaSource();
const arrayBuffer = await audioData.arrayBuffer();
if (this.mediaSource.readyState === 'closed') {
this.mediaSource = new MediaSource();
this.audioElement.src = URL.createObjectURL(this.mediaSource);
await new Promise(resolve => {
this.mediaSource.addEventListener('sourceopen', resolve, { once: true });
});
this.sourceBuffer = this.mediaSource.addSourceBuffer('audio/mpeg');
}
if (this.sourceBuffer && this.sourceBuffer.updating) {
await new Promise(resolve => {
this.sourceBuffer.addEventListener('updateend', resolve, { once: true });
});
}
if (this.sourceBuffer) {
this.sourceBuffer.appendBuffer(arrayBuffer);
if (this.audioElement && this.audioElement.paused) {
try {
await this.audioElement.play();
} catch (error) {
console.warn('播放失败,尝试恢复:', error);
setTimeout(async () => {
try {
await this.audioElement.play();
} catch (retryError) {
console.error('重试播放失败:', retryError);
}
}, 100);
}
}
}
}
} catch (error) {
console.error('添加音频到缓冲区失败:', error);
throw error;
}
}
// 停止当前播放
stopCurrentAudio() {
if (this.isIOS) {
this.iosPlayer.stop();
} else {
if (this.audioElement) {
this.audioElement.pause();
this.audioElement.currentTime = 0;
}
if (this.mediaSource) {
this.mediaSource.endOfStream();
}
this.mediaSource = null;
this.sourceBuffer = null;
}
this.isPlaying = false;
}
// 获取当前状态
getStatus() {
return {
isPlaying: this.isPlaying,
mediaSourceState: this.mediaSource?.readyState,
sampleRate: this.audioContext?.sampleRate || this.targetSampleRate,
bitDepth: this.bitDepth,
channels: this.channels
};
}
}
export default new AudioService();
//IOS音频播放工具
class IOSAudioStreamer {
constructor() {
this.audioContext = null;
this.audioBufferQueue = [];
this.isPlayingBuffer = false;
this.currentSource = null;
this.lastEndTime = 0;
this.fadeDuration = 0.05;
this.overlapDuration = 0.02;
this.targetSampleRate = 16000;
this.isFirstPlay = true;
this.debugMode = true;
this.amplitudeLimit = 0.98;
}
// 调试日志
log(...args) {
if (this.debugMode) {
console.log('[IOSAudioStreamer]', ...args);
}
}
// 初始化音频上下文
async initAudioContext() {
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({
sampleRate: this.targetSampleRate,
latencyHint: 'interactive'
});
if (this.audioContext.state === 'suspended') {
await this.audioContext.resume();
}
this.log('AudioContext 初始化成功,采样率:', this.audioContext.sampleRate);
}
}
// 预处理音频数据以避免爆音
preprocessAudioBuffer(audioBuffer) {
const numberOfChannels = audioBuffer.numberOfChannels;
const length = audioBuffer.length;
// 创建新的音频缓冲区
const processedBuffer = this.audioContext.createBuffer(
numberOfChannels,
length,
audioBuffer.sampleRate
);
// 处理每个通道
for (let channel = 0; channel < numberOfChannels; channel++) {
const inputData = audioBuffer.getChannelData(channel);
const outputData = processedBuffer.getChannelData(channel);
// 应用淡入淡出效果到整个音频段
const fadeInLength = Math.min(length, Math.floor(this.fadeDuration * audioBuffer.sampleRate));
const fadeOutLength = Math.min(length, Math.floor(this.fadeDuration * audioBuffer.sampleRate));
// 复制并处理数据
for (let i = 0; i < length; i++) {
let sample = inputData[i];
// 应用平滑的淡入效果
if (i < fadeInLength) {
// 使用余弦函数实现更平滑的淡入
const fadeInFactor = 0.5 * (1 - Math.cos(Math.PI * i / fadeInLength));
sample *= fadeInFactor;
}
// 应用平滑的淡出效果
if (i > length - fadeOutLength) {
// 使用余弦函数实现更平滑的淡出
const fadeOutFactor = 0.5 * (1 - Math.cos(Math.PI * (length - i) / fadeOutLength));
sample *= fadeOutFactor;
}
// 使用更平滑的振幅限制
if (Math.abs(sample) > this.amplitudeLimit) {
const sign = Math.sign(sample);
const excess = Math.abs(sample) - this.amplitudeLimit;
// 使用平滑的压缩曲线
sample = sign * (this.amplitudeLimit + (1 - Math.exp(-excess * 2)));
}
outputData[i] = sample;
}
}
return processedBuffer;
}
// 解码音频数据
async decodeAudioData(arrayBuffer) {
try {
this.log('开始解码音频数据,大小:', arrayBuffer.byteLength);
// 使用 Web Audio API 解码
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
this.log('音频解码成功,时长:', audioBuffer.duration);
// 预处理音频数据
const processedBuffer = this.preprocessAudioBuffer(audioBuffer);
// 检查音频数据
let maxSample = 0;
for (let channel = 0; channel < processedBuffer.numberOfChannels; channel++) {
const channelData = processedBuffer.getChannelData(channel);
for (let i = 0; i < channelData.length; i++) {
maxSample = Math.max(maxSample, Math.abs(channelData[i]));
}
}
this.log('音频缓冲区创建成功,最大样本值:', maxSample);
return processedBuffer;
} catch (error) {
this.log('音频解码错误:', error);
throw error;
}
}
// 创建淡入淡出效果
createFadeEffect(source, startTime, duration, isFadeIn = true) {
const gainNode = this.audioContext.createGain();
source.connect(gainNode);
gainNode.connect(this.audioContext.destination);
if (isFadeIn) {
gainNode.gain.setValueAtTime(0, startTime);
gainNode.gain.linearRampToValueAtTime(1, startTime + duration);
} else {
gainNode.gain.setValueAtTime(1, startTime);
gainNode.gain.linearRampToValueAtTime(0, startTime + duration);
}
return gainNode;
}
// 核心数据接收入口
async feedData(audioData) {
try {
await this.initAudioContext();
const arrayBuffer = await audioData.arrayBuffer();
this.log('接收到音频数据,大小:', arrayBuffer.byteLength);
const audioBuffer = await this.decodeAudioData(arrayBuffer);
this.log('音频解码成功,时长:', audioBuffer.duration);
this.audioBufferQueue.push(audioBuffer);
if (!this.isPlayingBuffer) {
this.playNextBuffer();
}
} catch (error) {
this.log('处理音频数据失败:', error);
throw error;
}
}
// 播放下一个音频缓冲区
async playNextBuffer() {
if (this.audioBufferQueue.length === 0) {
this.isPlayingBuffer = false;
return;
}
this.isPlayingBuffer = true;
const audioBuffer = this.audioBufferQueue.shift();
const source = this.audioContext.createBufferSource();
source.buffer = audioBuffer;
const currentTime = this.audioContext.currentTime;
let startTime;
if (this.isFirstPlay) {
startTime = currentTime + 0.1;
this.isFirstPlay = false;
this.log('首次播放,延迟:', 0.1);
} else if (this.lastEndTime > 0) {
startTime = Math.max(currentTime, this.lastEndTime - this.overlapDuration);
this.log('连续播放,开始时间:', startTime);
} else {
startTime = currentTime;
this.log('开始播放,时间:', startTime);
}
const fadeInGain = this.createFadeEffect(source, startTime, this.fadeDuration, true);
source.gainNode = fadeInGain;
const endTime = startTime + audioBuffer.duration;
const fadeOutGain = this.createFadeEffect(source, endTime - this.fadeDuration, this.fadeDuration, false);
source.start(startTime);
this.log('音频开始播放,时长:', audioBuffer.duration);
this.lastEndTime = endTime;
this.currentSource = source;
source.onended = () => {
this.log('音频播放结束');
source.disconnect();
if (source.gainNode) {
source.gainNode.disconnect();
}
fadeOutGain.disconnect();
if (this.currentSource === source) {
this.currentSource = null;
}
this.playNextBuffer();
};
}
// 停止当前播放
stop() {
this.log('停止播放');
if (this.currentSource) {
try {
this.currentSource.stop();
} catch (error) {
this.log('停止音频源失败:', error);
}
this.currentSource.disconnect();
if (this.currentSource.gainNode) {
this.currentSource.gainNode.disconnect();
}
this.currentSource = null;
}
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
this.isPlayingBuffer = false;
this.audioBufferQueue = [];
this.lastEndTime = 0;
this.isFirstPlay = true;
}
// 用户必须通过点击事件触发初始化
initByUserGesture() {
const btn = document.createElement('button');
btn.style.position = 'absolute';
btn.style.opacity = '0';
btn.innerHTML = '激活播放';
btn.onclick = async () => {
this.log('用户点击激活按钮');
await this.initAudioContext();
document.body.removeChild(btn);
};
document.body.appendChild(btn);
}
}
export default IOSAudioStreamer;