// Copyright (C) 2013 Massachusetts Institute of Technology // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License version 2, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. // NotePlayer.js // Tim Mickel, 2013 // Based entirely on the AS version by John Maloney // // Subclass of SoundDecoder to play notes on a sampled instrument or drum. // // A sampled instrument outputs interpolated sound samples from an array of signed, // 16-bit integers with an original sampling rate of 22050 samples/sec. The pitch is // shifted by change the step size while iterating through this array. An instrument // may also be looped so that it can be sustained and it may have a volume envelope // to control the attack and decay of the note. var NotePlayer = function(wavFileData, originalPitch, loopStart, loopEnd, env) { this.originalPitch = originalPitch || null; this.index = 0; this.samplesRemaining = 0; // determines note duration // Looping this.isLooped = false; this.loopPoint = 0; // final sample in loop this.loopLength = 0; // Volume Envelope this.envelopeValue = 1; this.samplesSinceStart = 0; this.attackEnd = 0; this.attackRate = 0; this.holdEnd = 0; this.decayRate = 1; if (wavFileData == null) wavFileData = new ArrayBuffer(); var stepSize = 0.5; // default - no pitch shift var startOffset = 0; this.endOffset = wavFileData.byteLength / 2; // end of sample data var getSample = function() { return 0; } // called once at startup time this.soundData = new Uint8Array(wavFileData); if ((loopStart >= 0) && (loopStart < this.endOffset)) { this.isLooped = true; this.loopPoint = loopStart; if ((loopEnd > 0) && (loopEnd <= this.endOffset)) this.endOffset = loopEnd; this.loopLength = this.endOffset - this.loopPoint; // Compute the original pitch more exactly from the loop length: var oneCycle = 22050 / this.originalPitch; var cycles = Math.round(this.loopLength / oneCycle); this.originalPitch = 22050 / (this.loopLength / cycles); } if (env) { this.attackEnd = env[0] * 44.100; if (this.attackEnd > 0) this.attackRate = Math.pow(33000, 1 / this.attackEnd); this.holdEnd = this.attackEnd + (env[1] * 44.100); var decayCount = env[2] * 44100; this.decayRate = (decayCount == 0) ? 1 : Math.pow(33000, -1 / decayCount); } } NotePlayer.prototype = Object.create(SoundDecoder.prototype); NotePlayer.prototype.constructor = NotePlayer; NotePlayer.prototype.setNoteAndDuration = function(midiKey, secs) { midiKey = Math.max(0, Math.min(midiKey, 127)); var pitch = 440 * Math.pow(2, (midiKey - 69) / 12); // midi key 69 is A (440 Hz) this.stepSize = pitch / (2 * this.originalPitch); // adjust for original sampling rate of 22050 this.setDuration(secs); } NotePlayer.prototype.setDuration = function(secs) { this.samplesSinceStart = 0; this.samplesRemaining = 44100 * secs; if (!this.isLooped) this.samplesRemaining = Math.min(this.samplesRemaining, this.endOffset / this.stepSize); this.envelopeValue = (this.attackEnd > 0) ? 1 / 33000 : 1; } NotePlayer.prototype.interpolatedSample = function() { if (this.samplesRemaining-- <= 0) { this.noteFinished(); return 0; } this.index += this.stepSize; while (this.index >= this.endOffset) { if (!this.isLooped) return 0; this.index -= this.loopLength; } var i = Math.floor(this.index); var frac = this.index - i; var curr = this.rawSample(i); var next = this.rawSample(i + 1); var sample = (curr + (frac * (next - curr))) / 100000; // xxx 32000; attenuate... if (this.samplesRemaining < 1000) sample *= (this.samplesRemaining / 1000.0); // relaase phease this.updateEnvelope(); return this.envelopeValue * sample; } NotePlayer.prototype.rawSample = function(sampleIndex) { if (sampleIndex >= this.endOffset) { if (this.isLooped) sampleIndex = this.loopPoint; else return 0; } var byteIndex = 2 * sampleIndex; var result = (this.soundData[byteIndex + 1] << 8) + this.soundData[byteIndex]; return (result <= 32767) ? result : result - 65536; } NotePlayer.prototype.updateEnvelope = function() { // Compute envelopeValue for the current sample. this.samplesSinceStart++; if (this.samplesSinceStart < this.attackEnd) { this.envelopeValue *= this.attackRate; } else if (this.samplesSinceStart == this.attackEnd) { this.envelopeValue = 1; } else if (this.samplesSinceStart > this.holdEnd) { if (this.decayRate < 1) this.envelopeValue *= this.decayRate; } }