1.项目方案
前端采用webrtc创建音频上下文,后创建音频源输入和音频处理器,连接音频输入与处理器,处理器再连接到音频输出(扬声器),再通过事件获取音频数据,把音频数据转换成字节数据通过webscoket发送给后端。
注意
1.前端使用的创建音频源api createScriptProcessor onaudioprocess 已经开始废弃使用,但是浏览器依然适配。
2.因为前端使用websocket实时传输录音数据,后端开发需要多线程接受处理数据,给每个数据包提供index坐标,然后处理后保存,再通过单线程发送,可以降低延迟
2.前端代码
// AudioManager.js
export default class AudioManager {
/**
* 构造函数
* @param {string} url - WebSocket服务器的地址
* @param {function} onMessageCallback - 当WebSocket接收到消息时的回调函数
*/
constructor(url, onMessageCallback) {
this.url = url; // WebSocket服务器的完整URL
this.websocket = null; // WebSocket连接实例
this.audioContext = null; // 音频上下文
this.audioStream = null; // 流媒体对象
this.audioProcessor = null; // 音频处理器
this.onMessageCallback = onMessageCallback; // WebSocket消息的回调函数
}
/**
* 初始化WebSocket连接并设置消息监听器
*/
initWs() {
console.log(this.url,';this.url');
// 创建WebSocket实例
this.websocket = new WebSocket(this.url);
// 设置WebSocket接收消息时的回调函数
this.websocket.onmessage = (e) => {
if (this.onMessageCallback) {
// 调用通过构造函数传入的回调函数处理接收到的消息
this.onMessageCallback(e.data);
}
};
// 请求用户的麦克风权限并开始处理音频流
this.queryHttp();
}
/**
* 停止录音并关闭所有资源
*/
stopRecording() {
// 关闭所有音频轨道
if (this.audioStream) {
this.audioStream.getTracks().forEach((track) => track.stop());
}
// 断开音频处理器的连接
if (this.audioProcessor) {
this.audioProcessor.disconnect();
}
// 关闭音频上下文
if (this.audioContext) {
this.audioContext.close();
}
// 关闭WebSocket连接
if (this.websocket) {
this.websocket.close();
}
}
/**
* 请求麦克风资源,并在成功后处理音频流
*/
queryHttp() {
navigator.mediaDevices
.getUserMedia({
audio: {
echoCancellation: true, // 开启回声消除
noiseSuppression: true, // 开启噪声抑制
autoGainControl: true, // 开启自动增益控制
},
})
.then((stream) => {
// 处理成功获取的音频流
this.handleStream(stream);
})
.catch((error) => {
// 处理获取音频流失败的情况
console.error("Error accessing microphone:", error);
});
}
/**
* 处理音频流,连接音频输入和处理器,并设置音频处理事件
* @param {MediaStream} stream - 从麦克风获取的音频流
*/
handleStream(stream) {
this.audioContext = new AudioContext({
sampleRate: 16000, // 设置采样率
latencyHint: "interactive", // 延迟模式为交互式
channels: 1, // 单声道
frameRate: 60, // 帧率
sampleType: "int16", // 采样类型
numberOfOutputs: 1, // 输出数量
});
// 创建音频源输入
let audioInput = this.audioContext.createMediaStreamSource(stream);
// 创建音频处理器
this.audioProcessor = this.audioContext.createScriptProcessor(4096, 1, 1);
// 设置音频处理事件
this.audioProcessor.onaudioprocess = (event) => {
// 获取音频数据
const inputData = event.inputBuffer.getChannelData(0);
// 转换音频数据为字节数据
const byteData = this.convertToByteData(inputData);
// 通过WebSocket发送字节数据
this.websocket.send(byteData);
};
// 连接音频输入与处理器,处理器再连接到音频输出(扬声器)
audioInput.connect(this.audioProcessor);
this.audioProcessor.connect(this.audioContext.destination);
// 保存音频流引用
this.audioStream = stream;
}
/**
* 将浮点数组的音频数据转换为字节数据
* @param {Float32Array} inputData - 浮点数组格式的原始音频数据
* @return {Uint8Array} 字节数据
*/
convertToByteData(inputData) {
// 创建Int16Array,由于原始的音频数据是Float32Array类型,需要转换
const intData = new Int16Array(inputData.map((item) => item * 32767));
// 创建Uint8Array来保存字节数据
const byteData = new Uint8Array(intData.length * 2);
// 将Int16Array的数据转换为字节数据并填充到Uint8Array
intData.forEach((value, index) => {
byteData[index * 2] = value & 0xff; // 存储低位字节
byteData[index * 2 + 1] = (value >> 8) & 0xff; // 存储高位字节
});
return byteData;
}
}
/*使用方法
<template>
<audio style="display: none" ref="audio" controls="controls" autoplay>
<source :src="audioUrl" type="audio/wav" />
</audio>
</template>
<script>
import AudioManager from './AudioManager.js';
export default {
data() {
return {
audioManager: null,
};
},
methods: {
handleWsMessage(data) {
let message = JSON.parse(data);
this.$refs.audio.src = message.url;
},
startRecording() {
this.audioManager = new AudioManager(
'your-server-url',
this.handleWsMessage // 回调函数用于接受通话后获取的信息这里我用于播放接受的音频
);
this.audioManager.initWs();
},
},
};
</script> */