使用Web Audio API将双轨音频变单轨音频

562 阅读4分钟

需求背景

系统通话录音为了能够进行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生成,种类繁多,但可以被简单明确的分为三类:

  1. 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)

大概流程:

具体实现

实现如下:

  1. 调用 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(); // 在音频准备好之后播放
			};
		}
	});
};

遇到的问题

  1. MediaElementAudioSource outputs zeroes due to CORS access restrictions for 音频文件

解决: audio.crossOrigin = 'anonymous';

浏览器会遵循同源策略,限制对跨域资源的访问,导致在尝试使用 MediaElementAudioSourceNode 时只收到零值。

  1. 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)

参考链接

Web Audio API-MDN

Web Audio 入门之读取左右声道数据

阮一峰 Web API 教程