网页中用户语音的 API

什么是 PCM

PCM 全称是脉冲调制编码(Pulse Code Modulation),是一种用数字表示采样模拟信号的方法。
PCM 数据格式包含三个重要步骤:

  • 采样: 采样是将连续的模拟信号转换为离散的样本,采样率是每秒钟采样的次数,单位是赫兹。
  • 量化: 量化是将每个样本的振幅用一个有限的数字来表示,位深是用来描述每个数字信号值的比特数。
  • 编码: 编码是将每个量化后的样本转换为二进制数据,这些数据就是 PCM 数据。

常用的量化指标有:采样率、位深、声道数、采样数据是否有符号、字节序。 PCM 数据流是一种未经压缩的音频采样数据裸流,它可以用一些专业的音频播放器或者转换器来播放或转换。数据流的存储结构根据声道数和字节序的不同而不同,通常单声道的数据按时间顺序存储,双声道的数据按左右声道交叉存储.

常用的模拟信号位深有:
8-bit: 2^8 = 256 ,有 256 个等级可以用于衡量真实的模拟信号.
16-bit:2^16 = 65,536 ,有 65,536 个等级可以用于衡量真实的模拟信号.
32-bit: 2^32 = 4294967296 ,有 4294967296 个等级可以衡量真实的模拟信号.

其中 16-bit 的最常见。显而易见位深越大对模拟信号的描述将越真实,对声音的描述更加准确.

在 Web 中如何获取用户的语音

MediaDevices.getUserMedia():这个方法可以让你访问用户的媒体设备,比如麦克风,并且返回一个包含媒体流的 Promise 对象。
Web Audio API:这个 API 可以让你使用 Javascript 来控制和处理网页上的音频,比如从媒体流中读取 PCM 数据,或者对 PCM 数据进行分析和变换。
MediaStream Recording API:这个 API 可以让你使用 Javascript 来捕获和记录媒体流,比如从麦克风中获取的音频。

这些 API 分别对音频视频进行采集,处理,记录。

请求麦克风权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
console.log("getUserMedia supported.");
navigator.mediaDevices
.getUserMedia(
// constraints - only request audio
{
audio: true,
}
)

// Success callback
.then((stream) => {})

// Error callback
.catch((err) => {
console.error(`The following getUserMedia error occurred: ${err}`);
});
} else {
console.log("getUserMedia not supported on your browser!");
}

对音频数据直接进行录制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 创建媒体记录对象
const mediaRecorder = new MediaRecorder(stream);


// store the voice data
let chunks = [];
// when voice data come in
mediaRecorder.ondataavailable = (e) => {
chunks.push(e.data);
};

mediaRecorder.onstop = (e) => {
console.log("recorder stopped");
const blob = new Blob(chunks, { type: "audio/ogg; codecs=opus" });
chunks = [];
const audioURL = window.URL.createObjectURL(blob);

// 或者展示到 audio 标签中
const audio = document.createElement("audio");
audio.src = audioURL;
// 自动下载
const a = document.createElement("a");
// 设置它的href属性为音频的blob URL
a.href = audioURL;
// 设置它的download属性为音频的文件名
a.download = "audio.ogg";
// 触发它的click事件
a.click();
};

// start to record
function start_record() {
mediaRecorder.start();
console.log(mediaRecorder.state);
console.log("recorder started");
}

// stop record
function stop_record() {
mediaRecorder.stop();
console.log(mediaRecorder.state);
console.log("recorder stopped");
}

处理音频

除了直接录制, 还可以使用 Web Audio API 对音频进行可视化和处理

概念

  • AudioContext:
    AudioContext是一个Web API接口,它表示一个由音频模块连接而成的音频处理图,每个音频模块都由一个 AudioNode 表示。AudioContext控制着它包含的节点的创建和音频处理或解码的执行.例如:
    1
    2
    3
    4
    5
    6
    7
    8
    // 创建一个新的AudioContext对象
    const audioCtx = new AudioContext();

    // 创建一个新的AudioContext对象,并设置延迟提示为交互式
    const audioCtx = new AudioContext({latencyHint: "interactive"});

    // 创建一个新的AudioContext对象,并设置采样率为48000 Hz
    const audioCtx = new AudioContext({sampleRate: 48000});
  • AudioNode:
    AudioNode是一个Web API接口, 它表示一个音频处理模块. 每个AudioNode都有输入和输出,可以和其他AudioNode连接起来,形成一个音频路由图。例如:
    有些AudioNode是音频源,比如 <audio> 元素,OscillatorNode等,它们没有输入,但有一个或多个输出,可以用来生成声音。
    有些AudioNode是音频目标,比如AudioDestinationNode,它们没有输出,而是将所有的输入直接播放到扬声器或者其他音频输出设备上。
    还有些AudioNode是音频效果,比如BiquadFilterNode,ConvolverNode,GainNode等,它们有输入和输出,可以用来修改音频,比如添加滤波,混响,声像,压缩等效果。
    1
    2
    3
    4
    5
    6
    7
    var audioContext = new AudioContext(); 
    // HTML5标签获取的音频源
    var source1 = audioContext.createMediaElementSource(audio);
    // navigator.getUserMedia 获取的外部(如麦克风)stream音频源
    var source2 = audioContext.createMediaStreamAudioSourceNode(stream);
    // 通过xhr获取服务器音频源
    var source3 = audioContext.createBufferSource(buffer);
  • audio routing graph: 是将所有的 AudioNode 链接起来形成的. 例如
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 创建一个新的AudioContext对象
    const audioCtx = new AudioContext();

    // 创建一个振荡器作为音频源
    const oscillator = audioCtx.createOscillator();

    // 创建一个增益节点作为音频效果
    const gainNode = audioCtx.createGain();

    // 获取系统扬声器作为音频目标
    const destination = audioCtx.destination;

    // 将振荡器连接到增益节点
    oscillator.connect(gainNode);

    // 将增益节点连接到扬声器
    gainNode.connect(destination);

    // 开始播放振荡器
    oscillator.start();

常用方法

获取用户输入输出设备列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 获取音频上下文
const audioCtx = new AudioContext();
try {
// 获取扬声器列表
const devices = await navigator.mediaDevices.enumerateDevices();
// 过滤出音频输出设备
const speakers = devices.filter(device => device.kind === 'audiooutput');
// 打印出设备信息
speakers.forEach(speaker => console.log(speaker.label, speaker.deviceId));
// 如果支持 setSinkId 方法,就尝试设置第一个扬声器为输出设备
// 此处设置后调用 audioCtx.destination 就会将输出指向设置的设备
if (audioCtx.sinkId) {
audioCtx.setSinkId(speakers[0].deviceId)
.then(() => console.log('Success'))
.catch(error => console.error(error));
} else {
console.warn('setSinkId is not supported');
}

// 过滤出音频输入设备
const inputs = devices.filter(device => device.kind === 'audioinput');
// 打印出设备信息
inputs.forEach(input => console.log(input.label, input.deviceId));
// 获取第一个输入设备的媒体流
navigator.mediaDevices.getUserMedia({audio: {deviceId: inputs[0].deviceId}})
.then(stream => {
// 创建一个音频源节点
const source = audioCtx.createMediaStreamSource(stream);
// 连接到输出节点
source.connect(audioCtx.destination);
})
.catch(error => console.error(error));
} catch(error) {
console.error(error)
}

其中会看到 deviceID 为 ‘default’ 和 ‘communications’ 的设备, default 表示的是默认设备,而 communications 表示的是首选语音通信设备。默认设备是指操作系统中设置的用于一般音频输出或输入的设备,而首选语音通信设备是指操作系统中设置的用于语音通话或视频会议等场景的设备。这两个设备可能是同一个,也可能是不同的,取决于用户的配置。

音频可视化

想实现可视化可以利用analyser节点实现音频可视化。
analyser 提供了傅立叶时域变换和频域变换后的数据方便展示

1
2
3
4
var analyser = audioContext.createAnalyser();//音频分析节点
analyser.fftSize = 512;
var dataArray = new Float32Array(analyser.fftSize);
analyser.getFloatTimeDomainData(dataArray);//获取音频数据

声道分离合并

WebAudio 提供了 ChannelSplitterNode 和 ChannelMergerNode 两种节点,用于对声道的处理。ChannelSplitterNode 可以将一个多声道的音频信号分离成多个单声道的信号,每个输出端口对应一个声道。ChannelMergerNode 可以将多个单声道的音频信号合并成一个多声道的信号,每个输入端口对应一个声道。可以使用 AudioContext.createChannelSplitter() 和 AudioContext.createChannelMerger() 方法来创建这两种节点,并指定要分离或合并的声道数。您还可以使用 AudioNode.connect() 方法来连接这些节点,注意要指定输入或输出的声道索引。

一个实际应用场景是通过对声道的分离,经过一系列处理后再合并,实现在歌曲中人声的消除。这个效果的原理是利用了人声通常是在立体声音频中居中的特点,也就是说左右声道中的人声信号是相同的,而其他乐器信号是不同的。因此,如果我们将左右声道相减,就可以消除人声,保留其他乐器。下面是一个使用 WebAudio API 实现这个效果的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 获取音频上下文
const audioCtx = new AudioContext();
// 获取音频源
const audioSource = audioCtx.createMediaElementSource(audioElement); // audioElement 是一个 <audio> 元素
// 创建分离器和合并器
const splitter = audioCtx.createChannelSplitter(2); // 分离两个声道
const merger = audioCtx.createChannelMerger(2); // 合并两个声道
// 创建增益节点
const gainL = audioCtx.createGain(); // 用于控制左声道的增益
const gainR = audioCtx.createGain(); // 用于控制右声道的增益
// 设置增益值为 -1,相当于取反
gainL.gain.value = -1;
gainR.gain.value = -1;
// 连接节点
audioSource.connect(splitter); // 将音频源分离成两个单声道
splitter.connect(gainL, 0); // 将左声道连接到左增益节点
splitter.connect(gainR, 1); // 将右声道连接到右增益节点
gainL.connect(merger, 0, 0); // 将左增益节点连接到合并器的左输入端口
gainR.connect(merger, 0, 1); // 将右增益节点连接到合并器的右输入端口
merger.connect(audioCtx.destination); // 将合并后的音频输出到目标设备

// 播放音频
audioElement.play();

空间音效

Audio spatialization 是指在 Web Audio API 中模拟音频在三维空间中的位置和移动的能力。它可以让您控制音频的方向、距离、速度和其他参数,从而创建出逼真的空间音效。这对于 WebXR 和游戏等场景非常有用,可以增强用户的沉浸感和交互感。

一个简单的例子是使用 PannerNode 来创建一个立体声的音频源,然后使用 AudioListener 来表示用户在空间中的位置和朝向。您可以通过改变 PannerNode 和 AudioListener 的属性来模拟音频源和用户之间的相对位置和移动,从而产生不同的空间音效。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 获取音频上下文
const audioCtx = new AudioContext();
// 获取音频源
const audioSource = audioCtx.createMediaElementSource(audioElement); // audioElement 是一个 <audio> 元素
// 创建一个立体声的音频源节点
const panner = audioCtx.createPanner();
// 设置音频源节点的位置、速度和方向
panner.positionX.value = 0; // x 轴坐标
panner.positionY.value = 0; // y 轴坐标
panner.positionZ.value = 0; // z 轴坐标
panner.orientationX.value = 1; // x 轴方向
panner.orientationY.value = 0; // y 轴方向
panner.orientationZ.value = 0; // z 轴方向
panner.velocityX.value = 0; // x 轴速度
panner.velocityY.value = 0; // y 轴速度
panner.velocityZ.value = 0; // z 轴速度
// 获取用户的监听器对象
const listener = audioCtx.listener;
// 设置监听器对象的位置、速度和方向
listener.positionX.value = 0; // x 轴坐标
listener.positionY.value = 0; // y 轴坐标
listener.positionZ.value = 1; // z 轴坐标
listener.forwardX.value = 0; // x 轴前方向量
listener.forwardY.value = 0; // y 轴前方向量
listener.forwardZ.value = -1; // z 轴前方向量
listener.upX.value = 0; // x 轴上方向量
listener.upY.value = 1; // y 轴上方向量
listener.upZ.value = 0; // z 轴上方向量

// 连接节点
audioSource.connect(panner); // 将音频源连接到立体声节点
panner.connect(audioCtx.destination); // 将立体声节点连接到目标设备

// 播放音频
audioElement.play();

Audio processing

但是有时候我们需要自定义一些特性,这就需要用到 AudioWorkletNode 进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义 processor.js 文件
// 获取音频工作集全局作用域
var globalScope = globalThis;

class MyProcess extends AudioWorkletProcessor {

constructor(options) {
super(options);
}
process(inputs, outputs, parameters) {

// 处理音频数
// false 表示停止处理
return true;
}

}

globalScope.registerProcessor("processor", MyProcess);

使用
1
2
3
4
5
await audioContext.audioWorklet.addModule('/processor.js');
const activeProcess = new AudioWorkletNode(
audioContext,
"processor"
);

参考链接

  1. Using the MediaStream Recording API
  2. MediaStream Recording API
  3. Basic concepts behind Web Audio API
  4. Using ChannelSplitter and MergeSplitter nodes in Web Audio API
  5. When connecting to a merger node is there any reason to use a number other than 0 as the second argument if the input is not channel splitter
  6. How do I use the WebAudio API channel splitter for adjusting the Left or Right gain on an audio track?
  7. Spatialized Audio
  8. Web audio spatialization basics
  9. Spatial sound best practices
  10. Spatialize Monaural Audio into 5.1 Channel Surround Sound Using Raspberry Pi
  11. Web Audio API介绍与web音频应用案例分析
Author: Sean
Link: https://blog.whileaway.io/posts/551a0c99/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.