mirror of
https://github.com/scratchfoundation/scratch-html5.git
synced 2024-11-28 18:16:11 -05:00
128 lines
5.1 KiB
JavaScript
128 lines
5.1 KiB
JavaScript
// 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;
|
|
}
|
|
};
|