Search code examples
javascriptweb-audio-apiarraybuffertyped-arrays

Convert AudioBuffer to ArrayBuffer / Blob for WAV Download


I'd like to convert an AudioBuffer to a Blob so that I can create an ObjectURL from it and then download the audio file.

let rec = new Recorder(async(chunks) => {
 var blob = new Blob(chunks, {
        type: 'audio/mp3'
      });
var arrayBuffer = await blob.arrayBuffer();
 const audioContext = new AudioContext()
 await audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => {

// How to I now convert the AudioBuffer into an ArrayBuffer => Blob ?

}

Solution

  • An AudioBuffer contains non-interleaved Float32Array PCM samples for each decoded audio channel. For a stereo AudioBuffer, it will contain 2 channels. Those channels need to be interleaved (combined) first, and then the interleaved PCM must have a WAV header appended to it so you can download and play as a WAV.

    // Float32Array samples
    const [left, right] =  [audioBuffer.getChannelData(0), audioBuffer.getChannelData(1)]
    
    // interleaved
    const interleaved = new Float32Array(left.length + right.length)
    for (let src=0, dst=0; src < left.length; src++, dst+=2) {
      interleaved[dst] =   left[src]
      interleaved[dst+1] = right[src]
    }
    
    // get WAV file bytes and audio params of your audio source
    const wavBytes = getWavBytes(interleaved.buffer, {
      isFloat: true,       // floating point or 16-bit integer
      numChannels: 2,
      sampleRate: 48000,
    })
    const wav = new Blob([wavBytes], { type: 'audio/wav' })
    
    // create download link and append to Dom
    const downloadLink = document.createElement('a')
    downloadLink.href = URL.createObjectURL(wav)
    downloadLink.setAttribute('download', 'my-audio.wav') // name file
    

    supporting functions below:

    // Returns Uint8Array of WAV bytes
    function getWavBytes(buffer, options) {
      const type = options.isFloat ? Float32Array : Uint16Array
      const numFrames = buffer.byteLength / type.BYTES_PER_ELEMENT
    
      const headerBytes = getWavHeader(Object.assign({}, options, { numFrames }))
      const wavBytes = new Uint8Array(headerBytes.length + buffer.byteLength);
    
      // prepend header, then add pcmBytes
      wavBytes.set(headerBytes, 0)
      wavBytes.set(new Uint8Array(buffer), headerBytes.length)
    
      return wavBytes
    }
    
    // adapted from https://gist.github.com/also/900023
    // returns Uint8Array of WAV header bytes
    function getWavHeader(options) {
      const numFrames =      options.numFrames
      const numChannels =    options.numChannels || 2
      const sampleRate =     options.sampleRate || 44100
      const bytesPerSample = options.isFloat? 4 : 2
      const format =         options.isFloat? 3 : 1
    
      const blockAlign = numChannels * bytesPerSample
      const byteRate = sampleRate * blockAlign
      const dataSize = numFrames * blockAlign
    
      const buffer = new ArrayBuffer(44)
      const dv = new DataView(buffer)
    
      let p = 0
    
      function writeString(s) {
        for (let i = 0; i < s.length; i++) {
          dv.setUint8(p + i, s.charCodeAt(i))
        }
        p += s.length
      }
    
      function writeUint32(d) {
        dv.setUint32(p, d, true)
        p += 4
      }
    
      function writeUint16(d) {
        dv.setUint16(p, d, true)
        p += 2
      }
    
      writeString('RIFF')              // ChunkID
      writeUint32(dataSize + 36)       // ChunkSize
      writeString('WAVE')              // Format
      writeString('fmt ')              // Subchunk1ID
      writeUint32(16)                  // Subchunk1Size
      writeUint16(format)              // AudioFormat https://i.sstatic.net/BuSmb.png
      writeUint16(numChannels)         // NumChannels
      writeUint32(sampleRate)          // SampleRate
      writeUint32(byteRate)            // ByteRate
      writeUint16(blockAlign)          // BlockAlign
      writeUint16(bytesPerSample * 8)  // BitsPerSample
      writeString('data')              // Subchunk2ID
      writeUint32(dataSize)            // Subchunk2Size
    
      return new Uint8Array(buffer)
    }