I'm acquiring PCM raw bytes with new AudioContext({ sampleRate: 16000 })
(note, I'm using Chrome, which supports the sampleRate
option), and I'd like to convert the resulting array into a DataView
object.
My current code is the following, in the stop
method I read the left channel and store it as an array of 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>
In order to send this recording to my backend I need to convert the array into a DataView
, so I tried to do the following:
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;
}
But the problem is that the resulting audio is inaudible.
From my understanding, the AudioContext
is returning a list of Float32Array
no matter what sampleRate
I set it to use, so I don't understand what am I supposed to do to convert the values so that they fit into a Int16
buffer...
From your comment I will assume you know how to handle s16le raw audio.
Also notice that you are creating your ArrayBuffer with length equals the number of samples in the PCM32fSamples
, should be the size in bytes, also the call to setInt16
should pass the offset in bytes.
An alternative to set an array buffer is to construct an Int16Array. The motivation for using a DataView is to be able to write mixed type data. This will make your code more readable.
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;
}
}
In the end the data will be on the PCM16iSamples
and in the buffer
, you can construct the dataview from the buffer as you did in your example
PS I didn't test, the snipped does not work here.
To populate using 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;
}
}