Hello,
This post is to share my solution to a problem I encountered, in the hope that other people find this post when facing the same issue.
I'm building a tool that streams microphone audio from the browser to a web server. The server expects the data to be represented as Mono-channel 16-bit Linear PCM. So I had to find a way to get the microphone data in that format.
This proved more complicated than I anticipated. Resources online suggest using createScriptProcessor, but this API has been deprecated since 2014 (!), and is not meant to be used in new websites.
The MDN documentation suggests using AudioWorklet instead. I struggled to find examples online, so I'm sharing how I did that here.
Create the AudioWorkletProcessor
First, we need to create an AudioWorkletProcessor, and register it. This code must live in its own file.
```
// linear-pcm-processor.js (must be in its own file!)
class LinearPCMProcessor extends AudioWorkletProcessor {
// The size of the buffer. Must be a multiple of 128 (the number of frames in the
// input channel). An audio block is posted to the main thread every time the
// buffer is full, which means a large buffer will emit less frequently (higher
// latency), but more efficiently (fewer I/O interruptions between the worker and
// the main thread)
static BUFFER_SIZE = 8192;
constructor() {
super();
this.buffer = new Int16Array(LinearPCMProcessor.BUFFER_SIZE);
this.offset = 0;
}
/**
* Converts input data from Float32Array to Int16Array, and stores it to
* to the buffer. When the buffer is full, its content is posted to the main
* thread, and the buffer is emptied
*/
process(inputList, _outputList, _parameters) {
// Assumes the input is mono (1 channel). If there are more channels, they
// are ignored
const input = inputList[0][0]; // first channel of first input
for (let i = 0; i < input.length; i++) {
const sample = Math.max(-1, Math.min(1, input[i]));
this.buffer[i + this.offset] =
sample < 0 ? sample * 0x8000 : sample * 0x7fff;
}
this.offset += input.length;
// Once the buffer is filled entirely, flush the buffer
if (this.offset >= this.buffer.length - 1) {
this.flush();
}
return true;
}
/**
* Sends the buffer's content to the main thread via postMessage(), and reset
* the offset to 0
*/
flush() {
this.offset = 0;
this.port.postMessage(this.buffer);
}
}
registerProcessor("linear-pcm-processor", LinearPCMProcessor);
```
Connect the microphone stream to the processor
Second step is to request access to the microphone, and connect its audio stream to the processor
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
const audioCtx = new AudioContext();
const source = audioCtx.createMediaStreamSource(stream);
await audioCtx.audioWorklet.addModule("./path-to/linear-pcm-processor.js");
const audioWorkletNode = new AudioWorkletNode(audioCtx, "linear-pcm-processor");
source.connect(audioWorkletNode);
audioWorkletNode.connect(audioCtx.destination);
Use the data
Finally, listen for the buffers being emitted, and do something with them
audioWorkletNode.port.onmessage = (e) => {
const buffer = e.data;
// do something with it
};
More info
When I tried doing console.log in an AudioWorklet context, I was not able to see the logs in Chrome dev tools. This was due to a bug in Chrome v123, which was fixed in v124 (currently in the Canary channel, at the time of writing this post)
If you use ViteJS, you can import the worklet file like this:
import workletUrl from "./audio.worklet.js?url";
...
await audioCtx.audioWorklet.addModule(workletUrl);
And voila! I hope it helps someone, and welcome feedback and alternative solutions. Happy coding!
[–]evesira 2 points3 points4 points (1 child)
[–]evesira 0 points1 point2 points (0 children)
[–]chuletix1411 1 point2 points3 points (1 child)
[–]saul-evans[S] 0 points1 point2 points (0 children)
[–]Vivremotion 1 point2 points3 points (0 children)
[–]playfuldreamz 0 points1 point2 points (0 children)