Is there a way to convert a blob with MIME type 'audio/wav' into a WAV file?

78 Views Asked by At

I am building an electron app that requires recording audio and saving it in the directory of my main.js. I got the recording as a blob and then turned it into an array buffer in order to send it to my backend.

In the startRecording function I use the mediarecorder to save the audio and then send it to main

const startRecording = async () => {
  console.log('Recording started');
  chunks = [];
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  mediaRecorder = new MediaRecorder(stream);
  mediaRecorder.ondataavailable = event => {
      chunks.push(event.data);
  };
  mediaRecorder.onstop = () => {
      const blob = new Blob(chunks, { type: 'audio/wav' });

      // Convert Blob to buffer
      blob.arrayBuffer().then(buffer => {
          // Sending audio data to the main process using the function exposed in the preload script
          window.sendAudioToMain.send(buffer);
      }).catch(error => {
          console.error('Error converting Blob to buffer:', error);
      });

      const audioUrl = URL.createObjectURL(blob);
      const audioElement = document.getElementById('audioElement');
      audioElement.src = audioUrl;
  };
  mediaRecorder.start();
};
ipcMain.on('save-audio', async (event, buffer) => {
  try {
    // Convert the buffer back into a Blob
    const audioBlob = new Blob([buffer], { type: 'audio/wav' });

    // Print the MIME type of the blob
    console.log('MIME Type:', audioBlob.type);
} catch (error) {
    console.error('Error:', error);
}
});

In my code block I have the array buffer turned back into a blob, but what I would really like to know is how I can convert this into a wav file

1

There are 1 best solutions below

18
guest271314 On

The last time I checked browser implementations of MediaRecorder do not provide an "audio/wav" option.

Changing MIME type alone does not re-encode the media file.

I have written media recorded in the browser to WAV format multiple ways, mainly using and adjusting code I found in the wild for my own purposes.

Here is the most recent WAV encoder code I wrote https://github.com/guest271314/WebCodecsOpusRecorder/blob/main/WebCodecsOpusRecorder.js#L256-L341 based largely on https://github.com/higuma/wav-audio-encoder-js for real-time WAV encoding from a media when necessary for the use case.

Initialize with number of channels and sample rate

const wav = new WavAudioEncoder({
  numberOfChannels: this.config.decoderConfig.numberOfChannels,
  sampleRate: this.config.decoderConfig.sampleRate,
});

Here I'm writing an ArrayBuffer copied from a WebCodecs AudioFrame with "f32-planar" format that we'll have to sort out for the case of 2 channels or more later, here https://github.com/guest271314/WebCodecsOpusRecorder/blob/main/WebCodecsOpusRecorder.js#L274C1-L286C6

write(buffer) {
  const floats = new Float32Array(buffer);
  let channels;
  // Deinterleave
  if (this.numberOfChannels > 1) {
    channels = [
      [],
      []
    ];
    for (let i = 0, j = 0, n = 1; i < floats.length; i++) {
      channels[(n = ++n % 2)][!n ? j++ : j - 1] = floats[i];
    }
    channels = channels.map((f) => new Float32Array(f));
  } else {
    channels = [floats];
  }
  // ...
const size = frame.allocationSize({
  planeIndex: 0,
});
const chunk = new ArrayBuffer(size);
frame.copyTo(chunk, {
  planeIndex: 0,
});
wav.write(chunk);

When we're done

const data = await wav.encode();

which asynchrously returns a Blob with "audio/wav" MIME type, containing an encoded WAV file.

// https://github.com/higuma/wav-audio-encoder-js
class WavAudioEncoder {
  constructor({ sampleRate, numberOfChannels }) {
    let controller;
    let readable = new ReadableStream({
      start(c) {
        return (controller = c);
      },
    });
    Object.assign(this, {
      sampleRate,
      numberOfChannels,
      numberOfSamples: 0,
      dataViews: [],
      controller,
      readable,
    });
  }
  write(buffer) {
    const floats = new Float32Array(buffer);
    let channels;
    // Deinterleave
    if (this.numberOfChannels > 1) {
      channels = [[], []];
      for (let i = 0, j = 0, n = 1; i < floats.length; i++) {
        channels[(n = ++n % 2)][!n ? j++ : j - 1] = floats[i];
      }
      channels = channels.map((f) => new Float32Array(f));
    } else {
      channels = [floats];
    }
    const [{ length }] = channels;
    const ab = new ArrayBuffer(length * this.numberOfChannels * 2);
    const data = new DataView(ab);
    let offset = 0;
    for (let i = 0; i < length; i++) {
      for (let ch = 0; ch < this.numberOfChannels; ch++) {
        let x = channels[ch][i] * 0x7fff;
        data.setInt16(
          offset,
          x < 0 ? Math.max(x, -0x8000) : Math.min(x, 0x7fff),
          true
        );
        offset += 2;
      }
    }
    this.controller.enqueue(new Uint8Array(ab));
    this.numberOfSamples += length;
  }
  setString(view, offset, str) {
    const len = str.length;
    for (let i = 0; i < len; i++) {
      view.setUint8(offset + i, str.charCodeAt(i));
    }
  }
  async encode() {
    const dataSize = this.numberOfChannels * this.numberOfSamples * 2;
    const buffer = new ArrayBuffer(44);
    const view = new DataView(buffer);
    this.setString(view, 0, 'RIFF');
    view.setUint32(4, 36 + dataSize, true);
    this.setString(view, 8, 'WAVE');
    this.setString(view, 12, 'fmt ');
    view.setUint32(16, 16, true);
    view.setUint16(20, 1, true);
    view.setUint16(22, this.numberOfChannels, true);
    view.setUint32(24, this.sampleRate, true);
    view.setUint32(28, this.sampleRate * 4, true);
    view.setUint16(32, this.numberOfChannels * 2, true);
    view.setUint16(34, 16, true);
    this.setString(view, 36, 'data');
    view.setUint32(40, dataSize, true);
    this.controller.close();
    return new Blob(
      [
        buffer,
        await new Response(this.readable, {
          cache: 'no-store',
        }).arrayBuffer(),
      ],
      {
        type: 'audio/wav',
      }
    );
  }
}