Skip to content

Instantly share code, notes, and snippets.

@flpvsk
Last active March 22, 2024 06:28
  • Star 30 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save flpvsk/047140b31c968001dc563998f7440cc1 to your computer and use it in GitHub Desktop.
An example of a recorder based on AudioWorklet API.
/*
A worklet for recording in sync with AudioContext.currentTime.
More info about the API:
https://developers.google.com/web/updates/2017/12/audio-worklet
How to use:
1. Serve this file from your server (e.g. put it in the "public" folder) as is.
2. Register the worklet:
const audioContext = new AudioContext();
audioContext.audioWorklet.addModule('path/to/recorderWorkletProcessor.js')
.then(() => {
// your code here
})
3. Whenever you need to record anything, create a WorkletNode, route the
audio into it, and schedule the values for 'isRecording' parameter:
const recorderNode = new window.AudioWorkletNode(
audioContext,
'recorder-worklet'
);
yourSourceNode.connect(recorderNode);
recorderNode.connect(audioContext.destination);
recorderNode.port.onmessage = (e) => {
if (e.data.eventType === 'data') {
const audioData = e.data.audioBuffer;
// process pcm data
}
if (e.data.eventType === 'stop') {
// recording has stopped
}
};
recorderNode.parameters.get('isRecording').setValueAtTime(1, time);
recorderNode.parameters.get('isRecording').setValueAtTime(
0,
time + duration
);
yourSourceNode.start(time);
*/
class RecorderWorkletProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [{
name: 'isRecording',
defaultValue: 0
}];
}
constructor() {
super();
this._bufferSize = 2048;
this._buffer = new Float32Array(this._bufferSize);
this._initBuffer();
}
_initBuffer() {
this._bytesWritten = 0;
}
_isBufferEmpty() {
return this._bytesWritten === 0;
}
_isBufferFull() {
return this._bytesWritten === this._bufferSize;
}
_appendToBuffer(value) {
if (this._isBufferFull()) {
this._flush();
}
this._buffer[this._bytesWritten] = value;
this._bytesWritten += 1;
}
_flush() {
let buffer = this._buffer;
if (this._bytesWritten < this._bufferSize) {
buffer = buffer.slice(0, this._bytesWritten);
}
this.port.postMessage({
eventType: 'data',
audioBuffer: buffer
});
this._initBuffer();
}
_recordingStopped() {
this.port.postMessage({
eventType: 'stop'
});
}
process(inputs, outputs, parameters) {
const isRecordingValues = parameters.isRecording;
for (
let dataIndex = 0;
dataIndex < isRecordingValues.length;
dataIndex++
) {
const shouldRecord = isRecordingValues[dataIndex] === 1;
if (!shouldRecord && !this._isBufferEmpty()) {
this._flush();
this._recordingStopped();
}
if (shouldRecord) {
this._appendToBuffer(inputs[0][0][dataIndex]);
}
}
return true;
}
}
registerProcessor('recorder-worklet', RecorderWorkletProcessor);
@rpivo
Copy link

rpivo commented Nov 18, 2021

This is great -- although I'm confused by the yourSourceNode.start(time); call in the comment example.

@rpivo
Copy link

rpivo commented Nov 20, 2021

@flpvsk ahh thanks!

@BlueRui
Copy link

BlueRui commented Feb 24, 2022

Thanks for sharing. What is the performance of this approach from your side? I tried your code and had some performance issues. I am guessing this is because it is streaming raw data through the message port. What are your thoughts?

@flpvsk
Copy link
Author

flpvsk commented Feb 24, 2022

@BlueRui

Thanks for sharing. What is the performance of this approach from your side?

I haven't noticed any glaring performance issues, but I've also haven't done any extensive testing. This worklet works fine in production on a very specific usecase.

I think as long as there's no other way to share memory between worklets & main thread, sending data through the message port is the only option.

@MadTomT
Copy link

MadTomT commented Aug 12, 2022

Hi, Do you have an example page showing this working in a similar manner to recorder.js ?

Thanks

@badpaybad
Copy link

Hi, Do you have an example page showing this working in a similar manner to recorder.js ?

Thanks

https://gist.github.com/tatsuyasusukida/b6daa0cd09bba2fbbf6289c58777eeca#file-encode-audio-js I found this one

@badpaybad
Copy link

mic-audio-recorder.js

`

class MicAudioProcessor extends AudioWorkletProcessor {
constructor() {
super();
this._lastUpdate = (new Date()).getTime();
this.waveformData = [];
}

  process(inputs, outputs) {

      this.port.postMessage(inputs[0]);

      //// Copy inputs to outputs.
      //output[0].set(input[0]);

      return true;
  }

}

registerProcessor("micAudioworklet", MicAudioProcessor);

`

from https://github.com/addpipe/simple-recorderjs-demo/blob/master/js/recorder.js

change line 57 -> 74

`

                this.context = source.context;//AudioContext
                var micAudioWorklet;
                //window.AudioWorkletNode=null;
                if (window.AudioWorkletNode) {
                    this.context.audioWorklet.addModule('./assets/mic-audio-recorder.js')
                        .then(async () => {
                            micAudioWorklet = new AudioWorkletNode(this.context, "micAudioworklet");
                            micAudioWorklet.connect(this.context.destination);
                            var bufferWithChannel = [];
                            for (var channel = 0; channel < _this.config.numChannels; channel++) {
                                bufferWithChannel.push([]);
                            }
                            micAudioWorklet.port.onmessage = ({ data }) => {
                                if (!_this.recording) return;
                                var buffer = [];
                                var canPost = true;
                                for (var channel = 0; channel < _this.config.numChannels; channel++) {
                                    bufferWithChannel[channel].push(...data[channel]);
                                    if (bufferWithChannel[channel].length >= this.config.bufferLen) {
                                        var temp = bufferWithChannel[channel].slice(0, this.config.bufferLen);
                                        //console.log(temp.length)
                                        buffer[channel]=temp;
                                        bufferWithChannel[channel] = bufferWithChannel[channel].slice(this.config.bufferLen);
                                    } else {
                                        canPost = false;
                                    }
                                }
                                if (canPost) {
                                    _this.worker.postMessage({
                                        command: 'record',
                                        buffer: buffer
                                    });
                                    buffer = [];
                                }
                                //// draw wave form: https://codesandbox.io/p/devbox/audioworket-port-1-y7ctsn?file=%2Findex.js%3A24%2C69
                            };
                            source.connect(micAudioWorklet).connect(this.context.destination);
                        })
                        .catch(error => {
                            console.error('Error registering audio worklet:', error);
                        });
                } else {
                    //// this is old one 
                    this.node = (this.context.createScriptProcessor || this.context.createJavaScriptNode).call(this.context, this.config.bufferLen, this.config.numChannels, this.config.numChannels);
                    this.node.onaudioprocess = function (e) {
                        if (!_this.recording) return;
                        var buffer = [];
                        for (var channel = 0; channel < _this.config.numChannels; channel++) {
                            //console.log(e.inputBuffer.getChannelData(channel).length)
                            buffer.push(e.inputBuffer.getChannelData(channel));
                        }
                        _this.worker.postMessage({
                            command: 'record',
                            buffer: buffer
                        });
                    };
                    source.connect(this.node);
                    this.node.connect(this.context.destination); //this should not be necessary
                }

`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment