jpemartins / speex.js

Speex codec in Javascript. Mirror from https://code.ua.pt/projects/speex-js
214 stars 63 forks source link

encodeWAV example producing output of all zeros #1

Closed akumpf closed 10 years ago

akumpf commented 11 years ago

I found that there was an implicit conversion from Float32Array to Int16Array that didn't actually convert the data. This meant that all of the (-1.0,1.0) float data I passed in was essentially integer zeros, and was converted as such.

Nothing big, but took a while to debug. Hopefully this will help others along the way.

Just needed to do the conversion to Int16Array before passing in the data (so it wouldn't need to do any data conversion within the library) and the output sprang to life :)

For reference, this is what I did at the top of encodeWAV()

  var shorts = data; // the input data (array of Int16 or Float32)
  if(data.constructor.prototype == Float32Array.prototype){
    // we need to convert the Floats to Integers...
    // could just multiply, but nice to make sure data is in bounds as well.
    var datalen = data.length;
    shorts = new Int16Array(datalen);
    for(var i=0; i<datalen; i++){
      shorts[i] = Math.floor(Math.min(1.0, Math.max(-1.0, data[i]))*32767);
    }
  }

Awesome library. Thanks for putting it together!

jpemartins commented 11 years ago

Cool, many thanks for the fix. I will add it to the demo soon.

Btw, sorry for the (big) delay in the response.

tiagobt commented 11 years ago

akumpf,

Could you please post the entire encodeWAV function?

In my case, data.constructor.prototype seems to be neither a Float32Array.prototype nor an Int16Array.prototype. It seems to be an ArrayBuffer.prototype.

I was able to encode and play the demo file (female.wav), but most other Wave files I tested output a very distorted audio. I'm not sure the problem I'm having is related to this specific issue or if I'm facing a codec limitation.

Thanks a lot!

akumpf commented 11 years ago

To save WAV files, try something like this. It takes in an audio sample, interleaves the channels, converts to 16BitPCM, and then prompts you to save with a download dialog in the browser. You may only need part of it, but hopefully it'll help you connect the dots. :)

  function interleave(chanData){
    var chans  = chanData.length;
    if(chans === 1) return chanData[0];
    var l0     = chanData[0].length;
    var length = l0 * chans;
    var result = new Float32Array(length);
    // --
    var index = 0;
    var inputIndex = 0;
    for(var i=0; i<l0; i++){
      for(var c=0; c<chans; c++){
        result[i*chans+c] = chanData[c][i];
      }
    }
    console.log(result);
    return result;
  }
  function floatTo16BitPCM(output, offset, input){
    try{
      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);
      }
    }catch(ex){
      console.warn("float to 16BitPM Err: i", i, ", offset", offset);
      console.log(ex);
    }
  }
  function wr(view, offset, string){
    for (var i = 0; i < string.length; i++){
      view.setUint8(offset + i, string.charCodeAt(i));
    }
  } 
  function getAudioSamplesAsWavBlob(samples, interleavedChannels, sampleRate){
    // see: https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
    interleavedChannels = interleavedChannels||1;
    console.log("encoding wav: samples:"+samples.length+", chans:"+interleavedChannels+", rate:"+sampleRate);
    var buffer  = new ArrayBuffer(44 + samples.length * 2); // 44 + PCM points * 2
    var dv      = new DataView(buffer);
    // -- header
    wr(dv, 0, 'RIFF');   // RIFF
    dv.setUint32(4, 32 + samples.length * interleavedChannels, true); // 32 + length
    wr(dv, 8, 'WAVE');   // RIFF type
    // -- chunk 1
    wr(dv, 12, 'fmt ');  // chunk id
    dv.setUint32(16, 16, true);   // subchunk1size (16 for PCM)
    dv.setUint16(20, 1, true);    // 1=PCM
    dv.setUint16(22, interleavedChannels, true); // num channels
    dv.setUint32(24, sampleRate, true);          // samplerate
    dv.setUint32(28, sampleRate * interleavedChannels * 2, true); // byterate
    dv.setUint16(32, 2 * interleavedChannels, true);  // block align
    dv.setUint16(34, 16, true); // bits per sample (16 = 2 bytes)
    // -- chunk 2
    wr(dv, 36, 'data');         // data chunk id
    dv.setUint32(40, samples.length * interleavedChannels, true); // chunk len
    floatTo16BitPCM(dv, 44, samples);
    var wavBlob = new Blob([dv], {type: "audio/wav"});
    return wavBlob;
  };
  function exportWAVSampleAndSave(sample, cb){
    if(!window.saveAs){
      console.warn("Cannot export, no window.saveAs();");
      return cb("No browser saveAs().");
    }
    var chanData = [];
    var chans    = sample.numberOfChannels;
    console.log("Sample channels:", chans);
    for(var c=0; c<chans; c++){
      chanData.push(sample.getChannelData(c));
    }
    var sample_chandata = interleave(chanData);
    console.log("interleaved chandata length:", sample_chandata.length);
    // --
    var wavBlob = getAudioSamplesAsWavBlob(sample_chandata, chans, sample.sampleRate);
    window.saveAs(wavBlob, sampleID+".wav"); 
    return cb(null);
  };

And if you haven't already, you may need the window.saveAs polyfill to allow simple downloading of blobs here:

http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js 
akumpf commented 11 years ago

Alternatively, it may be as simple as getting your ArrayBuffer into a Float32Array first (assuming the data is stored as such).

var dataAsFloatArray = new Float32Array(arrayBuffer);

This essentially creates a view on top of the generic ArrayBuffer so that it can be read as an array of floats if the data is using that format behind the scenes.

tiagobt commented 11 years ago

Thanks for the help, akumpf! Saving the Wave file is not my goal right now, but the functions you posted will probably be useful soon.

I'm just trying to use the demo page ("Encoding WAV") with a Wave file I saved in Audacity (WAV signed 16 bit PCM, sampling rate of 44.100, 2 channels). Following your suggestion, I tried to modify the encodeWAV function as follows:

function encodeWAV (data) {
    var isFloatArray = data.constructor.prototype == Float32Array.prototype;
    var frames, bytes, begin, end, times, ret
      , buffer = data;

    //var shorts = !isFloatArray ? new Int16Array(buffer) : data;
    var shorts = new Float32Array(data);

    var codec = new Speex({
        benchmark: false
      , quality: 2
      , complexity: 2
      , bits_size: 15         
    })

    var spxdata = codec.encode(shorts, true);
    Speex.util.play(codec.decode(spxdata));
    codec.close();
}

But the sound played by the function is not correct. I've just realized my audio file uses 16 bits per sample, just like the demo WAV provided (female.wav). Maybe the Int16Array was right to start with. What else could be causing the problem? A higher sampling frequency? 2 channels instead of 1?

Thanks again!

akumpf commented 11 years ago

Just to verify, this is what the encodeWAV function might look like with the Float32 check inline:

  function encodeWAV (data) {
    var isFloatArray = data.constructor.prototype == Float32Array.prototype;
    var frames, bytes, begin, end, times, ret, buffer = data;

    var shorts = data;
    if(data.constructor.prototype == Float32Array.prototype){
      var datalen = data.length;
      shorts = new Int16Array(datalen);
      for(var i=0; i<datalen; i++){
        shorts[i] = Math.floor(Math.min(1.0, Math.max(-1.0, data[i]))*32767);
      }
    }
    var codec = new Speex({
        benchmark: false
      , quality: 6
      , complexity: 2        
    })

    var spxdata = codec.encode(shorts, true);
    codec.close();
    return spxdata;
  }

Note that I also don't specify the bits_size and have the quality up to 6. Just seemed to work better for me.

I'm guessing the problem is occurring on the step before you call encodeWAV. Where are you getting the audio data input to encode?

I do something like this (may have typos, but it's the core of the speex code I've been playing with so it should be very close):

  function addSampleFromURL(url, cb){
    function bufferSound(event) {
      var xhr = event.target; 
      var audiobuffer = xhr.response;
      return cb(audiobuffer);
    }
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);
    xhr.responseType = 'arraybuffer';
    xhr.addEventListener('load', bufferSound, false);
    xhr.send();  
  };
  // --
  var audioContext = new AudioContext();
  addSampleFromURL("http://example.com/mysound.mp3", function(audioArrayBuffer){
    var sample = audioContext.createBuffer(audioArrayBuffer, false);
    var data = sample.getChannelData(0); // using mono here...
    var spxdata = encodeWAV(data);
    Speex.util.play(codec.decode(spxdata));
  }
tiagobt commented 11 years ago

I'm getting the file from an <input type="file">, just like it happens in http://jpemartins.github.io/speex.js/. The file itself was previously recorded in Audacity.

I tried to replace the original encodeWAV with your version, but now there's no sound output. A quick debug showed that, in my case, isFloatArray is false, so the new if block is skipped and the original ArrayBuffer is passed to the codec.encode function.

I'll try to change the way I get the audio data and see if it helps.

jpemartins commented 10 years ago

Guys, I no longer do this awkward conversion from float to shorts. Instead I take directly as shorts from the wav data stream. Since it is fixed, I will close this issue. There is also WAV->OGG conversion in the demo, so check it out if you're interested :) Thanks a lot for your feedback!