年初在公司做了一个需求:传感器采集到了机器的振动数据,要在网页中利用这个数据播放出振动的声音。
之前只了解过HTML中的<audio>
元素可以通过src
属性指定音频文件路径来播放音频,现在我是要把一个振动数据的数组转化为可供浏览器播放的音频数据,再通过<audio>
元素在页面中展示一个音频播放器把音频数据播放出来,其中把振动数据数组转化为音频数据就要用到Web Audio API了。
Web Audio API 提供了在Web上控制音频的一个非常有效通用的系统,允许开发者来自选音频源,音频源可以提供一个片段一个片段的音频采样数据(以数组的方式),也可以是音频或视频的文件读出来的,又或者是音频流。
要把振动数据数组播放出来,首先要把振动数据转化为Web Audio API支持的音频源,这里要用到AudioBuffer这一音频源。
AudioBuffer接口表示存在内存里的一段短小的音频资源,可利用 AudioContext.createBuffer()从原始数据构建。
这里又引出了AudioContext:
Web Audio API使用户可以在音频上下文(AudioContext)中进行音频操作。
AudioContext接口表示由链接在一起的音频模块构建的音频处理图。音频上下文控制它包含的节点的创建和音频处理或解码的执行。在做任何其他操作之前,您需要创建一个AudioContext对象,因为所有事情都是在上下文中发生的。
要把振动数据数组转化为AudioBuffer,就要用到AudioContext.createBuffer()
,先上一份根据振动数据生成AudioBuffer的代码:
// 根据振动时域波形生成audioBuffer
generateAudioBuffer = (PCMData, sampleRate) => {
var audioCtx = new AudioContext(); // AudioContext接口表示由链接在一起的音频模块构建的音频处理图
let frameCount = PCMData.length; // 帧数
// AudioBuffer接口表示存在内存里的一段短小的音频资源,利用 AudioContext.createBuffer()从原始数据构建,
// createBuffer() 方法用于新建一个空白的 AudioBuffer 对象,以便用于填充数据
let myArrayBuffer = audioCtx.createBuffer(1, frameCount, sampleRate); // sampleRate,线性音频样本的采样率,即每一秒包含的关键帧的个数。
// 返回一个 Float32Array,包含了带有频道的PCM数据,由频道参数定义(有0代表第一个频道)
let nowBuffering = myArrayBuffer.getChannelData(0);
// let max = Math.max(...newX_Input), min = Math.min(...newX_Input) // 参数量过大时会报Maximum call stack size exceeded
let max = this.getArrayMax(PCMData), min = this.getArrayMin(PCMData)
for (let i = 0; i < frameCount; i++) {
// audio needs to be in [-1.0; 1.0]
nowBuffering[i] = 2 * (PCMData[i] + Math.abs(min)) / (max + Math.abs(min)) - 1;
}
return myArrayBuffer
}
getArrayMax = (arr) => {
let max = arr[0]
arr.forEach((item, index) => {
max = max > item ? max : item
})
return max
}
getArrayMin = (arr) => {
let min = arr[0]
arr.forEach((item, index) => {
min = min < item ? min : item
})
return min
}
上述代码中generateAudioBuffer
的PCMData
参数就是振动数据数组,sampleRate
是采样率,即每一秒包含的关键帧的个数,PCMData
的长度就是帧数。
先来看一下createBuffer
方法的介绍:
createBuffer() 方法用于新建一个空白的 AudioBuffer 对象,以便用于填充数据。
基本语法:> AudioContext.createBuffer(Number numOfChannels, Number length, Number sampleRate);numOfChannels:一个定义了 buffer 中包含的声频通道数量的整数(比如单声道、双声道和立体声等)。
length:一个代表 buffer 中的样本帧数的整数。
sampleRate:线性音频样本的采样率,即每一秒包含的关键帧的个数。
上面的代码中我用createBuffer
方法创建了一个单声道、以PCMData
的长度为帧数、以sampleRate
为采样率的AudioBuffer,接着对其填充数据,注意AudioBuffer中的数据必须是-1到1之间,因此要对PCMData进行归一化处理后才能填充到AudioBuffer中。由于PCMData是传感器连续采集的数据,数据量可能会比较大,归一化的时候如果直接用Math.max()
和Math.min()
方法取数组的最大、最小值可能会报Maximum call stack size exceeded这个错,因此要用上面代码中通过循环封装的方法来取最大、最小值。
有了这个AudioBuffer,下一步就是将其转化为wav格式的音频文件:
// Convert AudioBuffer to a Blob using WAVE representation
bufferToWave = (abuffer, frameCount) => {
var numOfChan = abuffer.numberOfChannels,
length = frameCount * numOfChan * 2 + 44,
buffer = new ArrayBuffer(length),
view = new DataView(buffer),
channels = [], i, sample,
offset = 0,
pos = 0;
// write WAVE header
// "RIFF"
setUint32(0x46464952);
// file length - 8
setUint32(length - 8);
// "WAVE"
setUint32(0x45564157);
// "fmt " chunk
setUint32(0x20746d66);
// length = 16
setUint32(16);
// PCM (uncompressed)
setUint16(1);
setUint16(numOfChan);
setUint32(abuffer.sampleRate);
// avg. bytes/sec
setUint32(abuffer.sampleRate * 2 * numOfChan);
// block-align
setUint16(numOfChan * 2);
// 16-bit (hardcoded in this demo)
setUint16(16);
// "data" - chunk
setUint32(0x61746164);
// chunk length
setUint32(length - pos - 4);
// write interleaved data
for (i = 0; i < abuffer.numberOfChannels; i++)
channels.push(abuffer.getChannelData(i));
while (pos < length) {
// interleave channels
for (i = 0; i < numOfChan; i++) {
// clamp
sample = Math.max(-1, Math.min(1, channels[i][offset]));
// scale to 16-bit signed int
sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0;
// write 16-bit sample
view.setInt16(pos, sample, true);
pos += 2;
}
// next source sample
offset++
}
// create Blob
return new Blob([buffer], { type: "audio/wav" });
function setUint16(data) {
view.setUint16(pos, data, true);
pos += 2;
}
function setUint32(data) {
view.setUint32(pos, data, true);
pos += 4;
}
}
上面的方法可以得到WAV音频的Blob数据格式,接着再通过src = URL.createObjectURL(blob)
得到可以传给<audio>
元素的src
属性的url,即可播放出振动音频。