An Audio Recorder Using ScriptProcessorNode

An Audio Recorder Using ScriptProcessorNode

·

3 min read

We know the MediaStream Recording API is created for recording audio/video on the Web. In this article, instead of using it, we will explore how to use ScriptProcessorNode to record audio.

Let's go through the basic process.

First, we need to capture audio stream from microphone.

const stream = await navigator.mediaDevices.getUserMedia({ audio: true });

Then, we need to create a source node, a scriptProcessorNode and connect them to destination.

const audioCtx = new AudioContext();
const source = audioCtx.createMediaStreamSource(stream);
const scriptNode = audioCtx.createScriptProcessor();
source.connect(scriptNode);
scriptNode.connect(audioCtx.destination);

Then, we could get raw audio data from onaudioprocess callback from scriptProcessorNode.

scriptNode.onaudioprocess = (e) => {
  console.log(e.inputBuffer.getChannelData(0));
}

We want to record audio, so everytime onaudioprocess callback be triggered, we save the input data into an array. One thing need to note here, we must copy the data first, or we will end up with the same array buffer.

let inputDatas = [];
scriptNode.onaudioprocess = (e) => {
  const inputDataArray = new Float32Array(e.inputBuffer.getChannelData(0));
  inputDatas.push(inputDataArray);
}

After we get all the audioBuffers, we need to merge this into one.

const length = inputDatas.reduce((pre, cur) => pre + cur.length, 0);
const outputBuffer = audioCtx.createBuffer(channelNum, length, sampleRate)

// write data from input into output
const outputData = outputBuffer.getChannelData(0)
let offset = 0;
for (let i = 0; i < inputDatas.length; i++) {
  const inputData = inputDatas[i];
  for (let j = 0; j < inputData.length; j++) {
    outputData[offset] = inputData[j];
    offset++;
  }
}

Then comes the last step, convert the audioBuffer into a wav format arrayBuffer. We do this by a small tool audiobuffer-to-wav. Just pass the audioBuffer as argument, and we will get the wav format result.

Now, we can download the arrayBuffer and play. Here is the complete code example.

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
</head>

<body>
  <h1>ScriptProcessorNode example</h1>
  <button id="playBtn">Play song</button>
  <button id="downloadBtn">Download</button>

  <!-- this is the convert tool: https://github.com/Jam3/audiobuffer-to-wav -->
  <script src="convert.js"></script>

  <script>
    const playButton = document.querySelector('#playBtn');
    const downloadButton = document.querySelector("#downloadBtn")

    const sampleRate = 16000;
    const channelNum = 1;
    const bufferSize = 4096;

    let audioCtx;
    let source;

    // for save input data
    let inputDatas = [];

    playButton.onclick = async () => {
      audioCtx = new AudioContext({ sampleRate });

      const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
      source = audioCtx.createMediaStreamSource(stream)

      const scriptNode = audioCtx.createScriptProcessor(bufferSize, channelNum, channelNum);
      scriptNode.onaudioprocess = (e) => {
        const inputDataArray = new Float32Array(e.inputBuffer.getChannelData(0));
        inputDatas.push(inputDataArray)
      }

      source.connect(scriptNode);
      scriptNode.connect(audioCtx.destination);

      source.onended = () => {
        source.disconnect(scriptNode);
        scriptNode.disconnect(audioCtx.destination);
      }
    }

    downloadButton.onclick = () => {
      // create a new audio buffer
      const length = inputDatas.reduce((pre, cur) => pre + cur.length, 0);
      const outputBuffer = audioCtx.createBuffer(channelNum, length, sampleRate)

      // write data from input into output
      const outputData = outputBuffer.getChannelData(0)
      let offset = 0;
      for (let i = 0; i < inputDatas.length; i++) {
        const inputData = inputDatas[i];
        for (let j = 0; j < inputData.length; j++) {
          outputData[offset] = inputData[j];
          offset++;
        }
      }

      // convert audioBuffer into wav arrayBuffer
      const wav = audioBufferToWav(outputBuffer)
      const blob = new window.Blob([new DataView(wav)], {
        type: 'audio/wav'
      })
      const url = window.URL.createObjectURL(blob)

      const anchor = document.createElement('a')
      document.body.appendChild(anchor)
      anchor.style = 'display: none'
      anchor.href = url
      anchor.download = 'audio.wav'
      anchor.click()
      window.URL.revokeObjectURL(url)
      document.body.removeChild(anchor)
    }

  </script>
</body>

</html>

Lastly, one thing needed to be noted that the ScriptProcessorNode api is deprecated and be replaced by AudioWorkletNode api. But the key idea is the same.