javascript - 将 PCM 原始字节保存到 DataView 对象中
问题描述
我正在获取 PCM 原始字节new AudioContext({ sampleRate: 16000 })
(注意,我使用的是支持该sampleRate
选项的 Chrome),并且我想将结果数组转换为DataView
对象。
我当前的代码如下,在stop
我读取左声道并将其存储为 s 数组的方法中Float32Array
。
function mergeBuffers(channelBuffer, recordingLength) {
let result = new Float32Array(recordingLength);
let offset = 0;
for (let i = 0; i < channelBuffer.length; i++) {
result.set(channelBuffer[i], offset);
offset += channelBuffer[i].length;
}
return Array.prototype.slice.call(result);
}
class AudioRecorder {
constructor(audioStream, config) {
this.audioStream = audioStream;
// creates the an instance of audioContext
this.audioContext = new AudioContext({ sampleRate: 16000 });
// retrieve the current sample rate of microphone the browser is using
this.sampleRate = this.audioContext.sampleRate;
// creates a gain node
this.volume = this.audioContext.createGain();
// creates an audio node from the microphone incoming stream
this.audioInput = this.audioContext.createMediaStreamSource(audioStream);
this.leftChannel = [];
this.recordingLength = 0;
/*
* From the spec: This value controls how frequently the audioprocess event is
* dispatched and how many sample-frames need to be processed each call.
* Lower values for buffer size will result in a lower (better) latency.
* Higher values will be necessary to avoid audio breakup and glitches
*/
const bufferSize = config?.bufferSize ?? 2048;
this.recorder = (
this.audioContext.createScriptProcessor ||
this.audioContext.createJavaScriptNode
).call(this.audioContext, bufferSize, 1, 1);
// connect the stream to the gain node
this.audioInput.connect(this.volume);
this.recorder.onaudioprocess = (event) => {
const samples = event.inputBuffer.getChannelData(0);
// we clone the samples
this.leftChannel.push(new Float32Array(samples));
this.recordingLength += bufferSize;
};
this.ondataavailable = config?.ondataavailable;
}
start() {
// we connect the recorder
this.volume.connect(this.recorder);
// start recording
this.recorder.connect(this.audioContext.destination);
}
stop() {
this.recorder.disconnect();
const PCM32fSamples = mergeBuffers(this.leftChannel, this.recordingLength);
const PCM16iSamples = [];
for (let i = 0; i < PCM32fSamples.length; i++) {
let val = Math.floor(32767 * PCM32fSamples[i]);
val = Math.min(32767, val);
val = Math.max(-32768, val);
PCM16iSamples.push(val);
}
return PCM16iSamples;
}
}
(async () => {
if (!navigator.getUserMedia) {
alert("getUserMedia not supported in this browser.");
}
let audioStream;
try {
audioStream = await navigator.mediaDevices.getUserMedia({
audio: true
});
} catch (err) {
alert("Error capturing audio.");
}
const recorder = new AudioRecorder(audioStream);
document.querySelector('#start').addEventListener('click', () => recorder.start());
document.querySelector('#stop').addEventListener('click', () => console.log(recorder.stop()));
})();
<button id="start">Start</button>
<button id="stop">Stop</button>
为了将此记录发送到我的后端,我需要将数组转换为 a DataView
,因此我尝试执行以下操作:
stop() {
this.recorder.disconnect();
const PCM32fSamples = mergeBuffers(this.leftChannel, this.recordingLength);
const buffer = new ArrayBuffer(PCM32fSamples.length + 1);
const PCM16iSamples = new DataView(buffer);
for (let i = 0; i < PCM32fSamples.length; i++) {
let val = Math.floor(32767 * PCM32fSamples[i]);
val = Math.min(32767, val);
val = Math.max(-32768, val);
PCM16iSamples.setInt16(i, val);
}
return PCM16iSamples;
}
但问题是生成的音频是听不见的。
据我了解,无论我将其设置为使用什么,它AudioContext
都会返回一个列表,所以我不明白我应该做什么来转换这些值以使其适合缓冲区...Float32Array
sampleRate
Int16
解决方案
根据您的评论,我假设您知道如何处理 s16le 原始音频。
另请注意,您创建的 ArrayBuffer 的长度等于 中的样本数PCM32fSamples
,应该是以字节为单位的大小,调用也setInt16
应该以字节为单位传递偏移量。
设置数组缓冲区的另一种方法是构造一个Int16Array。使用 DataView 的动机是能够编写混合类型的数据。这将使您的代码更具可读性。
const buffer = new ArrayBuffer(this.recordingLength * 2);
const PCM16iSamples = new Int16Array(buffer);
let offset = 0;
for(const chunk of this.leftChannel){
for(const sample of chunk){
let val = Math.floor(32767 * sample);
val = Math.min(32767, val);
val = Math.max(-32768, val);
PCM16iSamples[offset++] = val;
}
}
最后,数据将在PCM16iSamples
和 中buffer
,您可以像在示例中那样从缓冲区构造数据视图
PS我没有测试,剪断在这里不起作用。
填充使用DataView
const buffer = new ArrayBuffer(this.recordingLength * 2);
const data = new DataView(buffer);
let offset = 0;
for(const chunk of this.leftChannel){
for(const sample of chunk){
let val = Math.floor(32767 * sample);
val = Math.min(32767, val);
val = Math.max(-32768, val);
data.setInt16(offset, val) = val;
offset += 2;
}
}