0
votes

Problem
Javascript-created WAV file incorrect length and silent.

Details
I've been using the JavaScript Web Audio API to create a web application that can take multiple sound files, grab a random chunk of each, and then "mix" them together into a sampler (serially, i.e. file1 + file2 + ... + fileN), like a mashup of sorts. Sounds can be successfully loaded into my custom SoundPlayer object, and they can be played. However, when you go to actually mix them together, the resulting WAV file is the wrong length and is completely silent.

The interface allows for up to 10 sounds to be loaded and played at their own volume. You click a button to make a sampler WAV of them together, and it dynamically creates a link to download it. I also have a way to see a hex dump of the resulting file, and it shows a bunch of 00s and even some NaNs at the bottom, so obviously my algorithm is flawed, but I just can't figure out how.

When you click the "Make Sampler!" button, it runs the following function (with an array of custom SoundPlayer objects, which contain Audio Buffers, as its argument):

function createSampler(sndArr) {
  var numberOfChannels = _getSoundChannelsMin(sndArr);
  var sndLengthSum = (function() {
    var lng = 0;
    for (var i = 0; i < sndArr.length; i++) {
      lng += sndArr[i].audioBuffer.length;
    }
    return lng;
  })();

  var samplerBuffer = getAudioContext().createBuffer(
    numberOfChannels,
    sndLengthSum,
    sndArr[0].audioBuffer.sampleRate
  );

  for (var i = 0; i < numberOfChannels; i++) {
    var channel = samplerBuffer.getChannelData(i);
    channel.set(sndArr[0].audioBuffer.getChannelData(i), 0);
    for (var j = 1; j < sndArr.length; j++) {
       channel.set(sndArr[j].audioBuffer.getChannelData(i), sndArr[j-1].audioBuffer.length);
    }
  }

  // encode our newly made audio blob into a wav file
  var dataView = _encodeWavFile(samplerBuffer, samplerBuffer.sampleRate);
  var audioBlob = new Blob([dataView], { type : 'audio/wav' });

 // post new wav file to download link
 _enableDownload(audioBlob);
}

The number of channels (mono/stereo/etc.) is acquired with this function:

function _getSoundChannelsMin(sndArr) {
  var sndChannelsArr = [];
  sndArr.forEach(function(snd) {
    sndChannelsArr.push(snd.audioBuffer.numberOfChannels);
  });
  return Math.min.apply(Math, sndChannelsArr);
}

The WAV is encoded with this function:

function _encodeWavFile(samples, sampleRate) {
  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);
  // stereo (2 channels)
  view.setUint16(22, 2, true);
  // sample rate
  view.setUint32(24, sampleRate, true);
  // byte rate (sample rate * block align)
  view.setUint32(28, sampleRate * 4, true);
  // block align (channels * bytes/sample)
  view.setUint16(32, 4, true);
  // bits/sample
  view.setUint16(34, 16, true);
  // data chunk identifier
  _writeString(view, 36, 'data');
  // data chunk length
  view.setUint32(40, samples.length * 2, true);
  // write the PCM samples
  _writePCMSamples(view, 44, samples);

  return view;
}

Strings in the WAV file are handled with this:

function _writeString(view, offset, string) {
  for (var i = 0; i < string.length; i++){
    view.setUint8(offset + i, string.charCodeAt(i));
  }
}

PCM samples are handed with this:

function _writePCMSamples(output, offset, input) {
  for (var i = 0; i < input.length; i++, offset+=2){
    var s = Math.max(-1, Math.min(1, input[i]));
    output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
  }
}

Finally, the WAV file is turned into a link with this:

function _enableDownload(blob, givenFilename) {
  var url = (window.URL || window.webkitURL).createObjectURL(blob);
  var link = document.getElementById("linkDownloadSampler");
  var d = new Date();
  var defaultFilename = "sampler" + d.curDateTime() + ".wav";
  link.style.display = "inline";
  link.href = url;
  link.download = givenFilename || defaultFilename;
}

Here's a snippet of the hex dump I get:

52 49 46 46 FFFD FFFD 02 00  57 41 56 45 66 6D 74 20  RIFF....WAVEfmt 
10 00 00 00 01 00 02 00  FFFD FFFD 00 00 00 FFFD 02 00  ................
04 00 10 00 64 61 74 61  FFFD FFFD 02 00 00 00 00 00  ....data........
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................

If anyone can help me see the error of my ways, I'd appreciate it. Sorry for the long post, but I wanted to post all the relevant details and code up front.

Thanks!

1

1 Answers

1
votes

You're neglecting to increment the offset as you write out the wave data.

Try this:

output.setInt16(offset + i, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
                      ^^^^

Without moving the write location you'll just keep overwriting the same offset in the file until finally it will be overwritten by the last sample's value.

Also, I see you have posted the _writePCMSamples code but the caller is commented out and you are apparently just trying to do the same thing inline. The inlined code has the same bug.