title: Use JavaScript to record audio as .wav in Chrome
body: <p>I am building a webpage that records audio from the user's device and will be sending it to Microsoft's Cognitive Speech Services for a Speech-to-Text conversion. So far, I have been able to create and play back .ogg files made in JavaScript, but I need to get the files in .wav format.</p>
  
  <p>Blob type <code>audio/wav</code> can't be relied on since not all browsers support it (mine doesn't, at least). The blobs are sent to and stored by a Django server. When I try to open these files with PySoundFile, I get an error that says <code>File contains data in an unknown format</code>. The blobs are being created with <code>new Blob(chunks, { type: 'audio/ogg; codecs=opus' })</code> and saved using <a href="https://docs.djangoproject.com/en/2.2/ref/models/fields/#django.db.models.FileField" rel="noreferrer"><code>django.db.FileField</code></a>. The blob chunks come from <code>MediaRecorder.ondataavailable</code>.</p>
  
  <p>UPDATE:
  I gave up on using <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder" rel="noreferrer">MediaRecorder</a> and opted for <a href="https://developer.mozilla.org/en-US/docs/Web/API/ScriptProcessorNode" rel="noreferrer">ScriptProcessorNode</a> instead. Again, Firefox works, but Chrome does not. It seems that Chrome is getting a small portion at the end of the audio and repeating that for the length of the audio. Here is the code that I used, which was based on Matt Diamond's work at <a href="https://github.com/mattdiamond/Recorderjs" rel="noreferrer">github.com/mattdiamond/Recorderjs</a>. A demo using his work can be seen at <a href="https://webaudiodemos.appspot.com/AudioRecorder/index.html" rel="noreferrer">webaudiodemos.appspot.com/AudioRecorder/index.html</a>, which works on both Firefox and Chrome for me. Also, my original code was in a class, but I did not want to include the entire class. I apologize if I made any syntactic errors in translation.</p>
  
  <pre><code>let recBuffers = [[], []];
  let recLength = 0;
  let numChannels = 2;
  let listening = false;
  let timeout = null;
  let constraints = {
      audio: true
  };
  let failedToGetUserMedia = false;
  
  if (navigator.getUserMedia) {
      navigator.getUserMedia(constraints, (stream) =&gt; {
          init(stream);
      }, (err) =&gt; {
          alert('Unable to access audio.\n\n' + err);
          console.log('The following error occurred: ' + err);
          failedToGetUserMedia = true;
      });
  }
  else if (navigator.mediaDevices.getUserMedia) {
      navigator.mediaDevices.getUserMedia(constraints).then((stream) =&gt; {
          init(stream);
      }).catch((err) =&gt; {
          alert('Unable to access audio.\n\n' + err);
          console.log('The following error occurred: ' + err);
          failedToGetUserMedia = true;
      });
  }
  else failedToGetUserMedia = true;
  
  function beginRecording() {
      recBuffers = [[], []];
      recLength = 0;
      listening = true;
      timeout = setTimeout(() =&gt; {
          endRecording();
      }, maxTime);
  }
  
  function endRecording() {
      clearTimeout(timeout);
      timeout = null;
      exportWAV();
  }
  
  function init(stream) {
      let audioContext = new AudioContext();
      let source = audioContext.createMediaStreamSource(stream);
      let context = source.context;
      let node = (context.createScriptProcessor || context.createJavaScriptNode).call(context, 4096, numChannels, numChannels);
      node.onaudioprocess = (e) =&gt; {
          if (!listening) return;
  
          for (var i = 0; i &lt; numChannels; i++) {
              recBuffers[i].push(e.inputBuffer.getChannelData(i));
          }
  
          recLength += recBuffers[0][0].length;
      }
      source.connect(node);
      node.connect(context.destination);
  }
  
  function mergeBuffers(buffers, len) {
      let result = new Float32Array(len);
      let offset = 0;
      for (var i = 0; i &lt; buffers.length; i++) {
          result.set(buffers[i], offset);
          offset += buffers[i].length;
      }
      return result;
  }
  
  function interleave(inputL, inputR) {
      let len = inputL.length + inputR.length;
      let result = new Float32Array(len);
  
      let index = 0;
      let inputIndex = 0;
  
      while (index &lt; len) {
          result[index++] = inputL[inputIndex];
          result[index++] = inputR[inputIndex];
          inputIndex++;
      }
  
      return result;
  }
  
  function exportWAV() {
      let buffers = [];
      for (var i = 0; i &lt; numChannels; i++) {
          buffers.push(mergeBuffers(recBuffers[i], recLength));
      }
  
      let interleaved = numChannels == 2 ? interleave(buffers[0], buffers[1]) : buffers[0];
      let dataView = encodeWAV(interleaved);
      let blob = new Blob([ dataView ], { type: 'audio/wav' });
      blob.name = Math.floor((new Date()).getTime() / 1000) + '.wav';
  
      listening = false;
  
      return blob;
  }
  
  function floatTo16BitPCM(output, offset, input){
      for (var i = 0; i &lt; input.length; i++, offset+=2){
          var s = Math.max(-1, Math.min(1, input[i]));
          output.setInt16(offset, s &lt; 0 ? s * 0x8000 : s * 0x7FFF, true);
      }
  }
  
  function writeString(view, offset, string){
      for (var i = 0; i &lt; string.length; i++) {
          view.setUint8(offset + i, string.charCodeAt(i));
      }
  }
  
  function encodeWAV(samples){
      var buffer = new ArrayBuffer(44 + samples.length * 2);
      var view = new DataView(buffer);
  
      /* RIFF identifier */
      writeString(view, 0, 'RIFF');
      /* file length */
      view.setUint32(4, 36 + samples.length * 2, true);
      /* RIFF type */
      writeString(view, 8, 'WAVE');
      /* format chunk identifier */
      writeString(view, 12, 'fmt ');
      /* format chunk length */
      view.setUint32(16, 16, true);
      /* sample format (raw) */
      view.setUint16(20, 1, true);
      /* channel count */
      view.setUint16(22, numChannels, true);
      /* sample rate */
      view.setUint32(24, context.sampleRate, true);
      /* byte rate (sample rate * block align) */
      view.setUint32(28, context.sampleRate * 4, true);
      /* block align (channel count * bytes per sample) */
      view.setUint16(32, numChannels * 2, true);
      /* bits per sample */
      view.setUint16(34, 16, true);
      /* data chunk identifier */
      writeString(view, 36, 'data');
      /* data chunk length */
      view.setUint32(40, samples.length * 2, true);
  
      floatTo16BitPCM(view, 44, samples);
  
      return view;
  }
  
  if (!failedToGetUserMedia) beginRecording();
  </code></pre>
  
  <p>UPDATE:
  I have confirmed that when the value of Chrome's buffers is provided as input to the interleave on Firefox, the output is the same as the output of Chrome's. This suggests that Chrome is not filling recBuffers with the correct values. Indeed, when I look at recBuffers on Chrome, each channel is full of alternating lists. For example:</p>
  
  <pre><code>recBuffers = [[
      [2, 3],
      [7, 1],
      [2, 3],
      [7, 1],
      [2, 3],
      [7, 1],
      [2, 3],
      [7, 1],
      [2, 3],
      [7, 1]
  ], [
      [5, 4],
      [6, 8],
      [5, 4],
      [6, 8],
      [5, 4],
      [6, 8],
      [5, 4],
      [6, 8],
      [5, 4],
      [6, 8]
  ]]
  </code></pre>
  
  <p>Of course, the actual values are different. This is just an example to illustrate the point.</p>