diff --git a/examples/bandlimited/css/main.css b/examples/bandlimited/css/main.css new file mode 100644 index 0000000..e69de29 diff --git a/examples/bandlimited/index.htm b/examples/bandlimited/index.htm new file mode 100644 index 0000000..3b3f691 --- /dev/null +++ b/examples/bandlimited/index.htm @@ -0,0 +1,187 @@ + + + + + Bandlimited Synthesis + + + + + + + + + + + + + + + + + + + + This example demonstrates how to interact with audiolet. For now, it emulates + a musical keyboard on your computer keyboard. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 1 + + 2 + + 3 + + 4 + + 5 + + 6 + + 7 + + 8 + + 9 + + 0 +
higher octave + Q + + W + + E + + R + + T + + Z + + U + + I + + O + + P +
  + A + + S + + D + + F + + G + + H + + J + + K + + L +
lower octave + Y + + X + + C + + V + + B + + N + + M + + ; + + : +
+ + + + + + + + diff --git a/examples/bandlimited/js/audiolet_app.js b/examples/bandlimited/js/audiolet_app.js new file mode 100644 index 0000000..73ef8a1 --- /dev/null +++ b/examples/bandlimited/js/audiolet_app.js @@ -0,0 +1,76 @@ +window.onload = function() { + + this.audiolet = new Audiolet(); + + var play=function(f,canvas) { + var X=new Object(); + X.square = new BandlimitedSquare(this.audiolet,f); + X.saw = new BandlimitedSaw(this.audiolet,f); + X.tri = new BandlimitedTriangle(this.audiolet,f); + + X.amplitude = new Multiply(this.audiolet, 1); + // X.square.connect(X.amplitude); + // X.saw.connect(X.amplitude); + X.tri.connect(X.amplitude); + + X.envelope = new InteractiveEnvelope(this.audiolet,0.5,1e-10,function () { X.amplitude.remove(); delete X; }); // Don't have a clue if this is sufficient to prevent mem leaks + + X.envelope.connect(X.amplitude,0,1); + + if (canvas) { + X.osci = new Oscilloscope(this.audiolet,undefined,canvas); + X.amplitude.connect(X.osci); + X.osci.connect(this.audiolet.output); + X.osci.raf=function () { window.mozRequestAnimationFrame(function () {X.osci.paint();},canvas); }; + // window.setTimeout(X.osci.raf,300); + X.osci.paint(); + + } else { + X.amplitude.connect(this.audiolet.output); + } + X.envelope.newTarget(0.002,1e-5); + + return X; + } + + // x=play(220,document.getElementById('scope')); + // window.setTimeout(function () { x.envelope.newTarget(0,2e-2); },200); + + function keyboard() { + var keyboard_scale=[89,83,88,68,67,86,71,66,72,78,74,77, + 81,50,87,51,69,82,53,84,54,90,55,85]; + + var f=220; + var keyboard=Array(); + for (k in keyboard_scale) { + keyboard[keyboard_scale[k]]=f; + f*=Math.pow(2,1/12); + } + return keyboard; + } + + this.keyboard=keyboard(); + + this.playing_notes=Array(); + + window.onkeydown=function(e) { + if ((f=this.keyboard[e.keyCode])!=null) { + if (playing_notes[e.keyCode]==null) { + f*=Math.pow(2,document.getElementById('octave').value); + var p=new Object(); + p.play=play(f); + playing_notes[e.keyCode]=p; + var kc=e.keyCode; + p.marker=kc; + p.switchOff=function() { p.play.envelope.newTarget(0,2e-4); playing_notes[kc]=null;} + } + } + } + window.onkeyup=function(e) { + var kc=e.keyCode; + if ((f=this.playing_notes[kc])!=null) { + this.playing_notes[kc].switchOff(); + } + } + +}; diff --git a/examples/interaction/css/main.css b/examples/interaction/css/main.css new file mode 100644 index 0000000..e69de29 diff --git a/examples/interaction/index.htm b/examples/interaction/index.htm new file mode 100644 index 0000000..3ddb6a6 --- /dev/null +++ b/examples/interaction/index.htm @@ -0,0 +1,181 @@ + + + + + Audiolet Template + + + + + + + + + + + + + + + + + This example demonstrates how to interact with audiolet. For now, it emulates + a musical keyboard on your computer keyboard. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 1 + + 2 + + 3 + + 4 + + 5 + + 6 + + 7 + + 8 + + 9 + + 0 +
higher octave + Q + + W + + E + + R + + T + + Z + + U + + I + + O + + P +
  + A + + S + + D + + F + + G + + H + + J + + K + + L +
lower octave + Y + + X + + C + + V + + B + + N + + M + + ; + + : +
+ + + + + diff --git a/examples/interaction/js/audiolet_app.js b/examples/interaction/js/audiolet_app.js new file mode 100644 index 0000000..8070eaf --- /dev/null +++ b/examples/interaction/js/audiolet_app.js @@ -0,0 +1,67 @@ +window.onload = function() { + + this.audiolet = new Audiolet(); + + var play=function(f) { + var X=new Object(); + X.sine = new Sine(this.audiolet,f); + + X.modulator = new Sine(this.audiolet, f); + + X.modulator_op = new MulAdd(this.audiolet,f/4,f); + X.modulator.connect(X.modulator_op,0,0); + X.modulator_op.connect(X.sine); + + X.amplitude = new Multiply(this.audiolet, 1); + X.sine.connect(X.amplitude); + + X.envelope = new InteractiveEnvelope(this.audiolet,0.1,1e-10,function () { X.amplitude.remove(); delete X; }); + + X.envelope.connect(X.amplitude,0,1); + X.amplitude.connect(this.audiolet.output); + + X.envelope.newTarget(0.02,1e-5); + return X; + } + + x=play(440); + window.setTimeout(function () { x.envelope.newTarget(0,2e-2); },200); + + function keyboard() { + var keyboard_scale=[89,83,88,68,67,86,71,66,72,78,74,77, + 81,50,87,51,69,82,53,84,54,90,55,85]; + + var f=220; + var keyboard=Array(); + for (k in keyboard_scale) { + keyboard[keyboard_scale[k]]=f; + f*=Math.pow(2,1/12); + } + return keyboard; + } + + this.keyboard=keyboard(); + + this.playing_notes=Array(); + + window.onkeydown=function(e) { + if ((f=this.keyboard[e.keyCode])!=null) { + if (playing_notes[e.keyCode]==null) { + f*=Math.pow(2,document.getElementById('octave').value); + var p=new Object(); + p.play=play(f); + playing_notes[e.keyCode]=p; + var kc=e.keyCode; + p.marker=kc; + p.switchOff=function() { p.play.envelope.newTarget(0,2e-2); playing_notes[kc]=null;} + } + } + } + window.onkeyup=function(e) { + var kc=e.keyCode; + if ((f=this.playing_notes[kc])!=null) { + this.playing_notes[kc].switchOff(); + } + } + +}; diff --git a/src/audiolet/Audiolet.js b/src/audiolet/Audiolet.js index d0fbb02..3c6ad17 100644 --- a/src/audiolet/Audiolet.js +++ b/src/audiolet/Audiolet.js @@ -3439,7 +3439,7 @@ TableLookupOscillator.prototype.generate = function(inputBuffers, var step = frequency * tableSize / sampleRate; phase += step; if (phase >= tableSize) { - phase %= tableSize; + phase = ((phase%tableSize)+tableSize)%tableSize; // javascript % doesn't behave consistently } channel[i] = table[Math.floor(phase)]; } diff --git a/src/core/Oscilloscope.js b/src/core/Oscilloscope.js new file mode 100644 index 0000000..ec01103 --- /dev/null +++ b/src/core/Oscilloscope.js @@ -0,0 +1,67 @@ + +/** + * An oscilloscope + * + * **Inputs** + * + * - Signal + * + * **Outputs** + * + * - Unmodified Signal (necessary, since Audiolet uses a "pull" architecture) + * + * **Parameters** + * + * @constructor + * @param {Audiolet} audiolet The audiolet object. + * @param {signal} signal Signal input + * @param {canvas} canvas A canvas object to display the oscilloscope + * @extends AudioletNode + */ + +var Oscilloscope = function(audiolet,signal,canvas) { + AudioletNode.call(this, audiolet, 1,1); + this.linkNumberOfOutputChannels(0, 0); + // this.value = new AudioletParameter(this, 0, signal); + this.canvas=canvas; + this.gc = canvas.getContext('2d'); + this.width = canvas.width; + this.height = canvas.height; + this.displayeddata = Array(); + this.displayedpointer = 0; + this.count=0; +}; + +extend(Oscilloscope, AudioletNode); + +Oscilloscope.prototype.generate = function(inputBuffers,outputBuffers) { + var c=inputBuffers[0].getChannelData(0); + var o=outputBuffers[0].getChannelData(0); + for (var i=0;ithis.width) { + this.canvas.width=this.width; + gc.fillStyle='#ffffff'; + gc.strokeStyle='#000000'; + gc.beginPath(); + gc.moveTo(0,this.displayeddata[0]*h2*factor+h2); + for (var i=0;iP) { + throw "Too many Partials in BandlimitedImpulseTrain."; + } + return function(n) { + var x=Math.PI*M*n/P; + var t=x/M; + if (Math.abs(Math.sin(t)) > 1e-5) + return Math.sin(x)/(P*Math.sin(t)); + else + return M/P*Math.cos(x)/(Math.cos(t)); + } +}; + +/** + * Bandlimited Square Wave Generator + * + * **Inputs** + * + * none (frequency modulation is hard to get right with the closed formula above) + * + * **Outputs** + * + * - Bandlimited Square Wave + * + * **Parameters** + * + * @constructor + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [frequency=440] Frequency. + */ + +BandlimitedSquare = function(audiolet,frequency) { + AudioletNode.call(this, audiolet, 0, 1); + this.frequency = frequency || 440; + this.phase = 0; + P=(44100.0/frequency)/2; // double "frequency" for square wave - see source mentioned in header + + M=Math.floor(P/2)*2; // even number of harmonics, lower than the original P + + // bipolar BLIT + + this.BandlimitedImpulseTrain=BandlimitedImpulseTrain(P,M,0); + + // initialize integrator storage + + this.store=0; +}; + +extend(BandlimitedSquare, AudioletNode); + +BandlimitedSquare.prototype.generate = function(inputBuffers, + outputBuffers) { + var buffer = outputBuffers[0]; + var channel = buffer.getChannelData(0); + + var bufferLength = buffer.length; + for (var i = 0; i < bufferLength; i++) { + var blit=this.BandlimitedImpulseTrain(this.phase++); + channel[i] = blit+this.store; + this.store += blit; + this.store *= 0.9999; + } +}; + + +/** + * toString + * + * @return {String} String representation. + */ +BandlimitedSquare.prototype.toString = function() { + return 'BandlimitedSquare'; +}; + + + +/** + * Bandlimited Saw Wave Generator + * + * **Inputs** + * + * none (frequency modulation is hard to get right with the closed formula above) + * + * **Outputs** + * + * - Bandlimited Saw Wave + * + * **Parameters** + * + * @constructor + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [frequency=440] Frequency. + */ + +BandlimitedSaw = function(audiolet,frequency) { + AudioletNode.call(this, audiolet, 0, 1); + this.frequency = frequency || 440; + this.phase = 0; + P=(44100.0/frequency); + + M=Math.floor(P/2-1)*2+1; // odd number of harmonics, lower than the original P + + // unipolar BLIT + + this.BandlimitedImpulseTrain=BandlimitedImpulseTrain(P,M,1); + this.P=P; + + // offset 1/2 - integral (DC part) of signal will be 0 + + this.offset=-1/P; + this.store=0; +}; + +extend(BandlimitedSaw, AudioletNode); + +BandlimitedSaw.prototype.generate = function(inputBuffers, + outputBuffers) { + var buffer = outputBuffers[0]; + var channel = buffer.getChannelData(0); + + var bufferLength = buffer.length; + for (var i = 0; i < bufferLength; i++) { + var blit= this.BandlimitedImpulseTrain(this.phase++)+this.offset; + channel[i] = blit+this.store; + this.store += blit; + this.store *= 0.9999; + } +}; + + +/** + * toString + * + * @return {String} String representation. + */ +BandlimitedSaw.prototype.toString = function() { + return 'BandlimitedSaw'; +}; + + +/** + * Bandlimited Triangle Wave Generator + * + * **Inputs** + * + * none (frequency modulation is hard to get right with the closed formula above) + * + * **Outputs** + * + * - Bandlimited Triangle Wave + * + * **Parameters** + * + * @constructor + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [frequency=440] Frequency. + */ + + +BandlimitedTriangle = function(audiolet,frequency) { + AudioletNode.call(this, audiolet, 0, 1); + this.frequency = frequency || 440; + P=(44100.0/frequency)/2; // double "frequency" for square wave - see source mentioned in header + + this.phase = P/2; + + M=Math.floor(P/2)*2; // even number of harmonics, lower than the original P + + // bipolar BLIT + this.Period=P; + this.BandlimitedImpulseTrain=BandlimitedImpulseTrain(P,M,0); + + // initialize integrator storage + + this.store=Array(); + this.store[0]=0; + this.store[1]=0; + this.store[2]=0; + this.overflow=false; + this.count=0; + this.offset=0.5/P; + console.log("Tri!"); +}; + +extend(BandlimitedTriangle, AudioletNode); + +BandlimitedTriangle.prototype.generate = function(inputBuffers, + outputBuffers) { + var buffer = outputBuffers[0]; + var channel = buffer.getChannelData(0); + + var bufferLength = buffer.length; + + var frequency=1/this.Period; + var frequency2=0.5/this.Period; + for (var i = 0; i < bufferLength; i++) { + var blit=this.BandlimitedImpulseTrain(1+this.phase++); + this.store[0] += blit*frequency; + this.store[0] *= 0.9999; + this.offset *= 0.9999; + this.store[1] += (this.store[0]+this.offset); + this.store[1] *= 0.999; + channel[i] = this.store[1]; + } + if (!this.overflow) { + if (Math.abs(channel[0])>2) { + this.overflow=true; + console.log("0 "+this.store[0]); + console.log("1 "+this.store[1]); + } + } +}; + + +/** + * toString + * + * @return {String} String representation. + */ +BandlimitedTriangle.prototype.toString = function() { + return 'BandlimitedTriangle'; +}; + diff --git a/src/dsp/Blit.js b/src/dsp/Blit.js new file mode 100644 index 0000000..2646b90 --- /dev/null +++ b/src/dsp/Blit.js @@ -0,0 +1,84 @@ + +/** + * BLIT closed formula, cf. + * http://www.music.mcgill.ca/~gary/307/week5/bandlimited.html + * + * It is possible to synthesize a sign-alternating impulse train + * by supplying an even number of Partials. However, then the + * Interpretation of P is different. + * + * @param {P} If odd is true, period in (fractional) samples. If odd + * is false, half period. + * @param {M} number of synthesized Partials + * @param {odd} assert that M is odd/even. + * @returns a function that will calculate a Bandlimited impulse train + * depending on a time parameter + */ +function Blit(P,M,odd) { + if ((odd && (M%2!=1)) || + (!odd && (M%2!=0))) { + throw "Erroneous Number of Partials in Blit."; + } + if (M>P) { + throw "Too many Partials in Blit."; + } + return function(t) { + var t=Math.PI*n/P; + var x=t*M; + n++; + if (Math.abs(Math.sin(t) > 1e-5)) + return Math.sin(x)/(P*Math.sin(t)); + else + return M*Math.cos(x)/(P*Math.cos(t)); + } +}; + +/** + * Bandlimited Square Wave Generator + * + * **Inputs** + * + * none (frequency modulation is hard to get right with the closed formula above) + * + * **Outputs** + * + * - Bandlimited Square Wave + * + * **Parameters** + * + * @constructor + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} [frequency=440] Frequency. + */ + +BlitSquare = function(audiolet,frequency) { + AudioletNode.call(this, audiolet, 0, 1); + this.frequency = frequency || 440; + this.phase = 0; + M=(44100/frequency); // XXX fixed sampling frequency + M=(Math.floor(M/2)*2); // even number, lower than the original M + P=(44100/frequency); + this.Blit=Blit(M,P,0); +}; + +BlitSquare.prototype.generate = function(inputBuffers, + outputBuffers) { + var buffer = outputBuffers[0]; + var channel = buffer.getChannelData(0); + + var bufferLength = buffer.length; + for (var i = 0; i < bufferLength; i++) { + channel[i] = this.Blit(this.phase++); + } +}; + +/** + * toString + * + * @return {String} String representation. + */ +BlitSquare.prototype.toString = function() { + return 'BlitSquare'; +}; + +extend(BlitSquare, AudioletNode); diff --git a/src/dsp/FixedBiquadFilter.js b/src/dsp/FixedBiquadFilter.js new file mode 100644 index 0000000..279edc3 --- /dev/null +++ b/src/dsp/FixedBiquadFilter.js @@ -0,0 +1,120 @@ +/*! + * @depends ../core/AudioletNode.js + */ + +/** + * Generic biquad filter. The coefficients (a0, a1, a2, b0, b1 and b2) are + * set at initalization. This simplifies bandlimited synthesis + * + * **Inputs** + * + * - Audio + * + * **Outputs** + * + * - Filtered audio + * + * **Parameters** + * + * @constructor + * @extends AudioletNode + * @param {Audiolet} audiolet The audiolet object. + * @param {b} Nominator of the transfer function + * @param {a} Denominator of the transfer function + */ +var FixedBiquadFilter = function(audiolet, b, a) { + AudioletNode.call(this, audiolet, 2, 1); + + // Same number of output channels as input channels + this.linkNumberOfOutputChannels(0, 0); + + // Delayed values + this.xValues = []; + this.yValues = []; + + // Coefficients + this.b0 = b[0]; + this.b1 = b[1]; + this.b2 = b[2]; + this.a0 = a[0]; + this.a1 = a[1]; + this.a2 = a[2]; +}; +extend(FixedBiquadFilter, AudioletNode); + +/** + * Process a block of samples + * + * @param {AudioletBuffer[]} inputBuffers Samples received from the inputs. + * @param {AudioletBuffer[]} outputBuffers Samples to be sent to the outputs. + */ + +FixedBiquadFilter.prototype.generate = function(inputBuffers, outputBuffers) { + var inputBuffer = inputBuffers[0]; + var outputBuffer = outputBuffers[0]; + + if (inputBuffer.isEmpty) { + outputBuffer.isEmpty = true; + return; + } + + var xValueArray = this.xValues; + var yValueArray = this.yValues; + + var inputChannels = []; + var outputChannels = []; + var numberOfChannels = inputBuffer.numberOfChannels; + for (var i = 0; i < numberOfChannels; i++) { + inputChannels.push(inputBuffer.getChannelData(i)); + outputChannels.push(outputBuffer.getChannelData(i)); + if (i >= xValueArray.length) { + xValueArray.push([0, 0]); + yValueArray.push([0, 0]); + } + } + + var a0 = this.a0; + var a1 = this.a1; + var a2 = this.a2; + var b0 = this.b0; + var b1 = this.b1; + var b2 = this.b2; + + var bufferLength = outputBuffer.length; + for (var i = 0; i < bufferLength; i++) { + for (var j = 0; j < numberOfChannels; j++) { + var inputChannel = inputChannels[j]; + var outputChannel = outputChannels[j]; + + var xValues = xValueArray[j]; + var x1 = xValues[0]; + var x2 = xValues[1]; + var yValues = yValueArray[j]; + var y1 = yValues[0]; + var y2 = yValues[1]; + + var x0 = inputChannel[i]; + var y0 = (b0 / a0) * x0 + + (b1 / a0) * x1 + + (b2 / a0) * x2 - + (a1 / a0) * y1 - + (a2 / a0) * y2; + + outputChannel[i] = y0; + + xValues[0] = x0; + xValues[1] = x1; + yValues[0] = y0; + yValues[1] = y1; + } + } +}; + +/** + * toString + * + * @return {String} String representation. + */ +FixedBiquadFilter.prototype.toString = function() { + return 'Fixed Biquad Filter'; +}; diff --git a/src/dsp/InteractiveEnvelope.js b/src/dsp/InteractiveEnvelope.js new file mode 100644 index 0000000..5322066 --- /dev/null +++ b/src/dsp/InteractiveEnvelope.js @@ -0,0 +1,76 @@ +/*! + * @depends Envelope.js + */ + +/** + * Exponential Level Changes + * + * **Inputs** + * + * - Asynchronous: Target Level, Decay Time + * + * **Outputs** + * + * - Envelope + * + * + * @constructor + * @extends Envelope + * @param {Audiolet} audiolet The audiolet object. + * @param {Number} initial initial level + * @param {Number} [trigger] trigger level + * @param {Function} [onComplete] A function called when the level sinks + * below the trigger level + */ +var InteractiveEnvelope = function(audiolet, initial, trigger, + onComplete) { + this.level = initial; + this.targetLevel = initial; + this.decay = 1; + this.onComplete=onComplete; + this.trigger=trigger; + AudioletNode.call(this, audiolet, 0, 1); +}; + +extend(InteractiveEnvelope, AudioletNode); + +/** + * toString + * + * @return {String} String representation. + */ +InteractiveEnvelope.prototype.toString = function() { + return 'Interactive Envelope'; +}; + +InteractiveEnvelope.prototype.newTarget = function (target,decay) { + this.targetLevel=target; + this.decay=decay; +} + +/** + * Process a block of samples + * + * @param {AudioletBuffer[]} inputBuffers Samples received from the inputs. + * @param {AudioletBuffer[]} outputBuffers Samples to be sent to the outputs. + */ +InteractiveEnvelope.prototype.generate = function(inputBuffers, outputBuffers) { + + var level = this.level; + var targetLevel = this.targetLevel; + var decay = this.decay; + var buffer = outputBuffers[0].getChannelData(0); + var bufferLength = buffer.length; + + + for (var i = 0; i < bufferLength; i++) { + level -= (level-targetLevel)*decay; + buffer[i]=level; + } + // console.log(level+ " " + this.trigger); + if (level= tableSize) { - phase %= tableSize; + phase = ((phase%tableSize)+tableSize)%tableSize; // javascript % doesn't behave consistently + } channel[i] = table[Math.floor(phase)]; }