summaryrefslogtreecommitdiff
path: root/replayer.js
diff options
context:
space:
mode:
Diffstat (limited to 'replayer.js')
-rw-r--r--replayer.js171
1 files changed, 171 insertions, 0 deletions
diff --git a/replayer.js b/replayer.js
new file mode 100644
index 0000000..d1c8b4b
--- /dev/null
+++ b/replayer.js
@@ -0,0 +1,171 @@
+function Replayer(midiFile, synth) {
+ var trackStates = [];
+ var beatsPerMinute = 120;
+ var ticksPerBeat = midiFile.header.ticksPerBeat;
+ var channelCount = 16;
+
+ for (var i = 0; i < midiFile.tracks.length; i++) {
+ trackStates[i] = {
+ 'nextEventIndex': 0,
+ 'ticksToNextEvent': (
+ midiFile.tracks[i].length ?
+ midiFile.tracks[i][0].deltaTime :
+ null
+ )
+ };
+ }
+
+ function Channel() {
+
+ var generatorsByNote = {};
+ var currentProgram = PianoProgram;
+
+ function noteOn(note, velocity) {
+ if (generatorsByNote[note] && !generatorsByNote[note].released) {
+ /* playing same note before releasing the last one. BOO */
+ generatorsByNote[note].noteOff(); /* TODO: check whether we ought to be passing a velocity in */
+ }
+ generator = currentProgram.createNote(note, velocity);
+ synth.addGenerator(generator);
+ generatorsByNote[note] = generator;
+ }
+ function noteOff(note, velocity) {
+ if (generatorsByNote[note] && !generatorsByNote[note].released) {
+ generatorsByNote[note].noteOff(velocity);
+ }
+ }
+ function setProgram(programNumber) {
+ currentProgram = PROGRAMS[programNumber] || PianoProgram;
+ }
+
+ return {
+ 'noteOn': noteOn,
+ 'noteOff': noteOff,
+ 'setProgram': setProgram
+ }
+ }
+
+ var channels = [];
+ for (var i = 0; i < channelCount; i++) {
+ channels[i] = Channel();
+ }
+
+ var nextEventInfo;
+ var samplesToNextEvent = 0;
+
+ function getNextEvent() {
+ var ticksToNextEvent = null;
+ var nextEventTrack = null;
+ var nextEventIndex = null;
+
+ for (var i = 0; i < trackStates.length; i++) {
+ if (
+ trackStates[i].ticksToNextEvent != null
+ && (ticksToNextEvent == null || trackStates[i].ticksToNextEvent < ticksToNextEvent)
+ ) {
+ ticksToNextEvent = trackStates[i].ticksToNextEvent;
+ nextEventTrack = i;
+ nextEventIndex = trackStates[i].nextEventIndex;
+ }
+ }
+ if (nextEventTrack != null) {
+ /* consume event from that track */
+ var nextEvent = midiFile.tracks[nextEventTrack][nextEventIndex];
+ if (midiFile.tracks[nextEventTrack][nextEventIndex + 1]) {
+ trackStates[nextEventTrack].ticksToNextEvent += midiFile.tracks[nextEventTrack][nextEventIndex + 1].deltaTime;
+ } else {
+ trackStates[nextEventTrack].ticksToNextEvent = null;
+ }
+ trackStates[nextEventTrack].nextEventIndex += 1;
+ /* advance timings on all tracks by ticksToNextEvent */
+ for (var i = 0; i < trackStates.length; i++) {
+ if (trackStates[i].ticksToNextEvent != null) {
+ trackStates[i].ticksToNextEvent -= ticksToNextEvent
+ }
+ }
+ nextEventInfo = {
+ 'ticksToEvent': ticksToNextEvent,
+ 'event': nextEvent,
+ 'track': nextEventTrack
+ }
+ var beatsToNextEvent = ticksToNextEvent / ticksPerBeat;
+ var secondsToNextEvent = beatsToNextEvent / (beatsPerMinute / 60);
+ samplesToNextEvent += secondsToNextEvent * synth.sampleRate;
+ } else {
+ nextEventInfo = null;
+ samplesToNextEvent = null;
+ self.finished = true;
+ }
+ }
+
+ getNextEvent();
+
+ function generate(samples) {
+ var data = new Array(samples*2);
+ var samplesRemaining = samples;
+ var dataOffset = 0;
+
+ while (true) {
+ if (samplesToNextEvent != null && samplesToNextEvent <= samplesRemaining) {
+ /* generate samplesToNextEvent samples, process event and repeat */
+ var samplesToGenerate = Math.ceil(samplesToNextEvent);
+ if (samplesToGenerate > 0) {
+ synth.generateIntoBuffer(samplesToGenerate, data, dataOffset);
+ dataOffset += samplesToGenerate * 2;
+ samplesRemaining -= samplesToGenerate;
+ samplesToNextEvent -= samplesToGenerate;
+ }
+
+ handleEvent();
+ getNextEvent();
+ } else {
+ /* generate samples to end of buffer */
+ if (samplesRemaining > 0) {
+ synth.generateIntoBuffer(samplesRemaining, data, dataOffset);
+ samplesToNextEvent -= samplesRemaining;
+ }
+ break;
+ }
+ }
+ return data;
+ }
+
+ function handleEvent() {
+ var event = nextEventInfo.event;
+ switch (event.type) {
+ case 'meta':
+ switch (event.subtype) {
+ case 'setTempo':
+ beatsPerMinute = 60000000 / event.microsecondsPerBeat
+ }
+ break;
+ case 'channel':
+ switch (event.subtype) {
+ case 'noteOn':
+ channels[event.channel].noteOn(event.noteNumber, event.velocity);
+ break;
+ case 'noteOff':
+ channels[event.channel].noteOff(event.noteNumber, event.velocity);
+ break;
+ case 'programChange':
+ //console.log('program change to ' + event.programNumber);
+ channels[event.channel].setProgram(event.programNumber);
+ break;
+ }
+ break;
+ }
+ }
+
+ function replay(audio) {
+ console.log('replay');
+ audio.write(generate(44100));
+ setTimeout(function() {replay(audio)}, 10);
+ }
+
+ var self = {
+ 'replay': replay,
+ 'generate': generate,
+ 'finished': false
+ }
+ return self;
+}