Search code examples
javascriptwavweb-audio-api

Downloading audio from web that has been modified with wavesurfer.js


I have created a multitrack web player using wavesurfer.js which can adjust the levels and panning of the different tracks.

What I want to do is export the mixed tracks with new levels and panning as a single .wav file.

I've done a bit of research into this and a lot of people are pointing to https://github.com/mattdiamond/Recorderjs but development stopped on this over 4 years ago and from what I've found it seems to have a load of issues.

Just initializing it like so var rec = new Recorder(spectrum); I get an error saying Cannot read property 'createScriptProcessor' of undefined at new Recorder And a quick search shows that is deprecated, see https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/createScriptProcessor.

Although I have a multitrack player if I can figure out how to export a single track with the levels and panning I could go from there. Is there any other way of exporting web audio with just the web audio API, or are there any other JS libraries like this that might work?


Solution

  • Assuming you have PCM audio, you can add a RIFF/WAV header to it, create a Blob from that, and then set blob as an Object URL on the a.href attribute. StackOverflow blocks the download here, but you run it locally to test. Hope this helps! 🇮🇪

    // fetch stereo PCM Float 32 little-endian file 
    const url = 'https://batman.dev/static/61881209/triangle-stereo-float.pcm'
    
    const ctx = new AudioContext()
    const elStatus = document.querySelector('#status')
    const elButton = document.querySelector('#download')
    
    init().catch(showError)
    
    async function init() {
      // get raw/PCM buffer (you will presumably already have your own)
      const buffer = await (await fetch(url)).arrayBuffer()
      
      // get WAV file bytes and audio params of your audio source
      const wavBytes = getWavBytes(buffer, {
        isFloat: true,       // floating point or 16-bit integer (WebAudio API decodes to Float32Array)
        numChannels: 2,
        sampleRate: 44100,
      })
    
      // add the button
      elButton.href = URL.createObjectURL(
        new Blob([wavBytes], { type: 'audio/wav' })
      )
      elButton.setAttribute('download', 'my-audio.wav') // name file
      status('')
      elButton.hidden = false
    }
    
    
    function status(msg) {
      elStatus.innerText = msg
    }
    
    
    function showError(e) {
      console.error(e)
      status(`ERROR: ${e}`)
    }
    
    
    // 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
      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)
    }
    body {
      padding: 2rem;
      font-family: sans-serif;
      text-align: center;
    }
    #download {
      padding: 1em 2em;
      color: #fff;
      background: #4c8bf5;
      text-decoration: none;
    }
    <div id="status">Loading...</div>
    <a hidden id="download">⬇ Download</a>