将 PCM 原始字节保存到 DataView 对象中
Save PCM raw bytes into a DataView object
我正在使用 new AudioContext({ sampleRate: 16000 })
获取 PCM 原始字节(注意,我正在使用 Chrome,它支持 sampleRate
选项),我想将将结果数组转换为 DataView
对象。
我当前的代码如下,在 stop
方法中我读取左声道并将其存储为 Float32Array
s 的数组。
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>
为了将此录音发送到我的后端,我需要将数组转换为 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 原始音频。
另请注意,您正在创建长度等于 PCM32fSamples
中样本数的 ArrayBuffer,应该是以字节为单位的大小,而且对 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;
}
}
我正在使用 new AudioContext({ sampleRate: 16000 })
获取 PCM 原始字节(注意,我正在使用 Chrome,它支持 sampleRate
选项),我想将将结果数组转换为 DataView
对象。
我当前的代码如下,在 stop
方法中我读取左声道并将其存储为 Float32Array
s 的数组。
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>
为了将此录音发送到我的后端,我需要将数组转换为 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 原始音频。
另请注意,您正在创建长度等于 PCM32fSamples
中样本数的 ArrayBuffer,应该是以字节为单位的大小,而且对 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;
}
}