Closed akumpf closed 10 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.
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!
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
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.
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!
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));
}
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.
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!
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()
Awesome library. Thanks for putting it together!