//JavaScript Audio Resampler (c) 2011 - Grant Galitz function Resampler(fromSampleRate, toSampleRate, channels, outputBufferSize, noReturn) { this.fromSampleRate = fromSampleRate; this.toSampleRate = toSampleRate; this.channels = channels | 0; this.outputBufferSize = outputBufferSize; this.noReturn = !!noReturn; this.initialize(); } Resampler.prototype.initialize = function () { //Perform some checks: if (this.fromSampleRate > 0 && this.toSampleRate > 0 && this.channels > 0) { if (this.fromSampleRate == this.toSampleRate) { //Setup a resampler bypass: this.resampler = this.bypassResampler; //Resampler just returns what was passed through. this.ratioWeight = 1; } else { this.ratioWeight = this.fromSampleRate / this.toSampleRate; if (this.fromSampleRate < this.toSampleRate) { /* Use generic linear interpolation if upsampling, as linear interpolation produces a gradient that we want and works fine with two input sample points per output in this case. */ this.compileLinearInterpolationFunction(); this.lastWeight = 1; } else { /* Custom resampler I wrote that doesn't skip samples like standard linear interpolation in high downsampling. This is more accurate than linear interpolation on downsampling. */ this.compileMultiTapFunction(); this.tailExists = false; this.lastWeight = 0; } this.initializeBuffers(); } } else { throw(new Error("Invalid settings specified for the resampler.")); } } Resampler.prototype.compileLinearInterpolationFunction = function () { var toCompile = "var bufferLength = buffer.length;\ var outLength = this.outputBufferSize;\ if ((bufferLength % " + this.channels + ") == 0) {\ if (bufferLength > 0) {\ var weight = this.lastWeight;\ var firstWeight = 0;\ var secondWeight = 0;\ var sourceOffset = 0;\ var outputOffset = 0;\ var outputBuffer = this.outputBuffer;\ for (; weight < 1; weight += " + this.ratioWeight + ") {\ secondWeight = weight % 1;\ firstWeight = 1 - secondWeight;"; for (var channel = 0; channel < this.channels; ++channel) { toCompile += "outputBuffer[outputOffset++] = (this.lastOutput[" + channel + "] * firstWeight) + (buffer[" + channel + "] * secondWeight);"; } toCompile += "}\ weight -= 1;\ for (bufferLength -= " + this.channels + ", sourceOffset = Math.floor(weight) * " + this.channels + "; outputOffset < outLength && sourceOffset < bufferLength;) {\ secondWeight = weight % 1;\ firstWeight = 1 - secondWeight;"; for (var channel = 0; channel < this.channels; ++channel) { toCompile += "outputBuffer[outputOffset++] = (buffer[sourceOffset" + ((channel > 0) ? (" + " + channel) : "") + "] * firstWeight) + (buffer[sourceOffset + " + (this.channels + channel) + "] * secondWeight);"; } toCompile += "weight += " + this.ratioWeight + ";\ sourceOffset = Math.floor(weight) * " + this.channels + ";\ }"; for (var channel = 0; channel < this.channels; ++channel) { toCompile += "this.lastOutput[" + channel + "] = buffer[sourceOffset++];"; } toCompile += "this.lastWeight = weight % 1;\ return this.bufferSlice(outputOffset);\ }\ else {\ return (this.noReturn) ? 0 : [];\ }\ }\ else {\ throw(new Error(\"Buffer was of incorrect sample length.\"));\ }"; this.resampler = Function("buffer", toCompile); } Resampler.prototype.compileMultiTapFunction = function () { var toCompile = "var bufferLength = buffer.length;\ var outLength = this.outputBufferSize;\ if ((bufferLength % " + this.channels + ") == 0) {\ if (bufferLength > 0) {\ var weight = 0;"; for (var channel = 0; channel < this.channels; ++channel) { toCompile += "var output" + channel + " = 0;" } toCompile += "var actualPosition = 0;\ var amountToNext = 0;\ var alreadyProcessedTail = !this.tailExists;\ this.tailExists = false;\ var outputBuffer = this.outputBuffer;\ var outputOffset = 0;\ var currentPosition = 0;\ do {\ if (alreadyProcessedTail) {\ weight = " + this.ratioWeight + ";"; for (channel = 0; channel < this.channels; ++channel) { toCompile += "output" + channel + " = 0;" } toCompile += "}\ else {\ weight = this.lastWeight;"; for (channel = 0; channel < this.channels; ++channel) { toCompile += "output" + channel + " = this.lastOutput[" + channel + "];" } toCompile += "alreadyProcessedTail = true;\ }\ while (weight > 0 && actualPosition < bufferLength) {\ amountToNext = 1 + actualPosition - currentPosition;\ if (weight >= amountToNext) {"; for (channel = 0; channel < this.channels; ++channel) { toCompile += "output" + channel + " += buffer[actualPosition++] * amountToNext;" } toCompile += "currentPosition = actualPosition;\ weight -= amountToNext;\ }\ else {"; for (channel = 0; channel < this.channels; ++channel) { toCompile += "output" + channel + " += buffer[actualPosition" + ((channel > 0) ? (" + " + channel) : "") + "] * weight;" } toCompile += "currentPosition += weight;\ weight = 0;\ break;\ }\ }\ if (weight <= 0) {"; for (channel = 0; channel < this.channels; ++channel) { toCompile += "outputBuffer[outputOffset++] = output" + channel + " / " + this.ratioWeight + ";" } toCompile += "}\ else {\ this.lastWeight = weight;"; for (channel = 0; channel < this.channels; ++channel) { toCompile += "this.lastOutput[" + channel + "] = output" + channel + ";" } toCompile += "this.tailExists = true;\ break;\ }\ } while (actualPosition < bufferLength && outputOffset < outLength);\ return this.bufferSlice(outputOffset);\ }\ else {\ return (this.noReturn) ? 0 : [];\ }\ }\ else {\ throw(new Error(\"Buffer was of incorrect sample length.\"));\ }"; this.resampler = Function("buffer", toCompile); } Resampler.prototype.bypassResampler = function (buffer) { if (this.noReturn) { //Set the buffer passed as our own, as we don't need to resample it: this.outputBuffer = buffer; return buffer.length; } else { //Just return the buffer passsed: return buffer; } } Resampler.prototype.bufferSlice = function (sliceAmount) { if (this.noReturn) { //If we're going to access the properties directly from this object: return sliceAmount; } else { //Typed array and normal array buffer section referencing: try { return this.outputBuffer.subarray(0, sliceAmount); } catch (error) { try { //Regular array pass: this.outputBuffer.length = sliceAmount; return this.outputBuffer; } catch (error) { //Nightly Firefox 4 used to have the subarray function named as slice: return this.outputBuffer.slice(0, sliceAmount); } } } } Resampler.prototype.initializeBuffers = function () { //Initialize the internal buffer: try { this.outputBuffer = new Float32Array(this.outputBufferSize); this.lastOutput = new Float32Array(this.channels); } catch (error) { this.outputBuffer = []; this.lastOutput = []; } }