需求背景
系统通话录音为了能够进行AI语音识别,音频由原来的单轨变成双轨的,方便识别主叫和被叫。但是鉴于有些用户只能听一个声道的音频,所以前端需要把双轨音频转换成单轨的。
未实现之前代码:
<audio ref="myAudioRef" class="audio" @pause="onChange" @play="onChange" @ended="onEnded" controls
:src="url"></audio>
const playRecording = (options: PlayRecordingOptions) => {
playOptions?.onChange?.(true) //把上一个播放暂停
visible.value = true
url.value = options.url
playOptions = options
nextTick(() => {
myAudioRef.value?.play()
})
}
Web Audio API
这里我们采用Web Audio API。
介绍
Web Audio API是web处理与合成音频的高级javascript api。Web Audio API提供了非常丰富的接口让开发者在web平台上实现对web音频进行处理。利用Web Audio,能做到的不是单纯的播放声音,还可以制造声音,获取声音数据,修改声音数据。
与audio标签完全不同,Web Audio的功能更为强大。Audio 和 Web Audio的关系,就像img和canvas的关系一样。
运行过程
最初的声音数据从源节点(sourceNode)出发,经过一个个声音处理器(audioNode),到达目标节点(destinationNode)
基本概念
AudioContext:音频上下文
浏览器原生提供AudioContext对象,该对象用于生成一个声音的上下文,管理和播放所有的声音。
const audioContext = new AudioContext();
AudioNode:音频处理器
AudioNode由AudioContext生成,种类繁多,但可以被简单明确的分为三类:
- sourceNode,声音的源头处理器,它可以读取声音数据流;
sourceNode = audioContext.createBufferSource();
2. destinationNode,声音的的终点处理器,它可以传递声音数据流到播放设备上;
destinationNode = audioContext.destination;
3. 其它audioNode,语法一般如下
xxxNode = audioContext.createXxx
connect:连接音频处理器(节点)的方法
这个方法允许将一个节点的输出连接到另一个节点的输入,从而实现音频信号的传递和处理。
connect() 方法的一般语法:
audioNode1.connect(audioNode2, outputIndex, inputIndex);
audioNode1: 要连接的源节点,即输出节点。
audioNode2: 要连接的目标节点,即输入节点。
outputIndex: 指定源节点的哪个输出端口(output)要连接。
inputIndex: 指定目标节点的哪个输入端口(input)要连接。
一些要点:
outputIndex 和 inputIndex 参数通常是可选的,如果未提供,它们将默认为 0。
一个节点可以连接到多个节点,但一个节点的输出只能连接到另一个节点的输入。
(一个节点可以有多个输出,但每个输出只能连接到另一个节点的输入)
连接节点会创建音频流的路径,使音频信号能够在节点之间流动,从而实现音频处理和效果。
sourceNode.connect(xxxNode1)
xxxNode2.connect(xxxNode3)
xxxNode3.connect(destinationNode)
大概流程:
具体实现
实现如下:
- 调用
getAudioContext()函数来获取AudioContext对象,很多操作都是基于这个对象。
const getAudioContext = (() => {
let audioContext:AudioContext
return () => {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
return audioContext;
};
})();
const audioContext = getAudioContext();
2. 使用给定的 HTMLMediaElement 对象(比如 <audio> 或 <video> 元素)作为参数,创建一个 MediaElementSourceNode 对象。
const sourceNode = audioContext.createMediaElementSource(audio);
3. 创建一个具有两个输入的 ChannelMergerNode 对象mergerNode, 该节点可以将多个音频信号合并成一个单一的音频信号。
const mergerNode = audioContext.createChannelMerger(2); // 合并为单声道
4. 将 sourceNode 节点的音频信号分别输入到 mergerNode 节点的两个输入端口,从而实现将两个音频信号合并成一个单一的输出信号的操作。
sourceNode.connect(mergerNode, 0, 0);
sourceNode.connect(mergerNode, 0, 1);
5. 将 mergerNode 节点的输出连接到 audioContext 的目的地(destination)
mergerNode.connect(audioContext.destination);
再把这张图搬过来看看,整个过程是非常清晰明了的。
整体代码:
const getAudioContext = (() => {
let audioContext:AudioContext
return () => {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
return audioContext;
};
})();
let sourceNode:MediaElementAudioSourceNode
const playRecording = (options: PlayRecordingOptions) => {
playOptions?.onChange?.(true); // 暂停上一个播放
visible.value = true;
url.value = options.url;
playOptions = options;
nextTick(() => {
const audio = myAudioRef.value;
if (audio) {
audio.crossOrigin = 'anonymous';
const audioContext = getAudioContext();
// audio的类型HTMLMediaElement
// 报错 将一个HTMLMediaElement连接到MediaElementSourceNode,但该元素已经连接到其他的MediaElementSourceNode(sourceNode)
// 解决: 已经绑定之后就无需重新连接
if (!sourceNode) {
sourceNode = audioContext.createMediaElementSource(audio); // 创建新的 MediaElementSourceNode
const mergerNode = audioContext.createChannelMerger(2); // 合并为单声道
sourceNode.connect(mergerNode, 0, 0);
sourceNode.connect(mergerNode, 0, 1);
mergerNode.connect(audioContext.destination);
}
audio.oncanplay = () => {
audio.play(); // 在音频准备好之后播放
};
}
});
};
遇到的问题
- MediaElementAudioSource outputs zeroes due to CORS access restrictions for 音频文件
解决: audio.crossOrigin = 'anonymous';
浏览器会遵循同源策略,限制对跨域资源的访问,导致在尝试使用 MediaElementAudioSourceNode 时只收到零值。
- Uncaught (in promise) DOMException: Failed to execute 'createMediaElementSource' on 'AudioContext': HTMLMediaElement already connected previously to a different MediaElementSourceNode.
出现原因是:
const sourceNode = audioContext.createMediaElementSource(audio)
const mergerNode = audioContext.createChannelMerger(2); // 合并为单声道
sourceNode.connect(mergerNode, 0, 0);
sourceNode.connect(mergerNode, 0, 1);
mergerNode.connect(audioContext.destination);
一开始每播放一次,就进行一次连接。会报错:将一个HTMLMediaElement连接到MediaElementSourceNode,但该元素已经连接到其他的MediaElementSourceNode(sourceNode)