167 lines
3.2 KiB
JavaScript
167 lines
3.2 KiB
JavaScript
class MidiTrack {
|
|
constructor(player, chunk) {
|
|
this.player = player;
|
|
this.events = chunk.events;
|
|
|
|
this.length = 0;
|
|
for (let i=0; i< this.events.length; i++) {
|
|
this.length += this.events[i].deltaTime;
|
|
}
|
|
|
|
this.index = 0;
|
|
this.lastTime = 0;
|
|
}
|
|
|
|
parseEvent(event) {
|
|
if (event instanceof MidiVoiceEvent) {
|
|
this.player.parseEvent(event);
|
|
} else if (event instanceof MidiSetTempoEvent) {
|
|
this.player.bpm = 60000000 / event.tempo;
|
|
}
|
|
}
|
|
|
|
tick(delta) {
|
|
while(true) {
|
|
if (this.index >= this.events.length) {
|
|
break;
|
|
}
|
|
|
|
let event = this.events[this.index];
|
|
|
|
if (this.lastTime + event.deltaTime - this.player.time <= 0) {
|
|
this.lastTime += event.deltaTime;
|
|
this.parseEvent(event);
|
|
this.index++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class MidiPlayer {
|
|
constructor() {
|
|
this.tracks = [];
|
|
|
|
this.playing = false;
|
|
this.speed = 1;
|
|
this.bpm = 120;
|
|
this.time = 0;
|
|
}
|
|
|
|
loadMidi(midi) {
|
|
this.tracks = [];
|
|
this.time = 0;
|
|
this.length = 0;
|
|
this.bpm = 120;
|
|
this.midi = midi;
|
|
|
|
if (!(this.midi.chunks[0] instanceof MidiHeaderChunk)) {
|
|
throw "No header chunk";
|
|
}
|
|
|
|
let header = this.midi.chunks[0];
|
|
|
|
this.format = header.format;
|
|
|
|
if ((header.division & 0x0800) === 1) {
|
|
throw "SMPTA time format not yet supported";
|
|
} else {
|
|
this.ticksPerQuarterNote = header.division;
|
|
}
|
|
|
|
for (let i=1; i<this.midi.chunks.length; i++) {
|
|
let chunk = this.midi.chunks[i];
|
|
if (chunk instanceof MidiTrackChunk) {
|
|
let track = new MidiTrack(this, chunk);
|
|
this.length = Math.max(track.length, this.length);
|
|
this.tracks.push(track);
|
|
}
|
|
}
|
|
}
|
|
|
|
parseEvent(event) {
|
|
if (event instanceof MidiNoteOnEvent) {
|
|
if (event.channel === 9) {
|
|
return;
|
|
}
|
|
|
|
if (event.velocity === 0) {
|
|
piano.localKeyUp(event.key, false);
|
|
} else {
|
|
piano.localKeyDown(event.key, event.velocity / 127);
|
|
}
|
|
} else if (event instanceof MidiNoteOffEvent) {
|
|
if (event.channel === 9) {
|
|
return;
|
|
}
|
|
piano.localKeyUp(event.key, false);
|
|
}
|
|
}
|
|
|
|
tick(delta) {
|
|
let deltaTicks = delta / 1000 * (this.bpm / 60) * this.ticksPerQuarterNote;
|
|
this.time += deltaTicks;
|
|
|
|
switch(this.format) {
|
|
case 0:
|
|
this.tracks[0].tick(deltaTicks);
|
|
break;
|
|
case 1:
|
|
for (let i=0; i<this.tracks.length; i++) {
|
|
this.tracks[i].tick(deltaTicks);
|
|
}
|
|
break;
|
|
case 2:
|
|
// Todo
|
|
break;
|
|
}
|
|
|
|
if (this.time >= this.length) {
|
|
this.time = this.length;
|
|
this.pause();
|
|
}
|
|
}
|
|
|
|
setTime(time) {
|
|
// TODO skip notes
|
|
this.time = time;
|
|
}
|
|
|
|
play() {
|
|
if (this.playing) return;
|
|
|
|
this.playing = true;
|
|
|
|
let lastTime = Date.now();
|
|
this.interval = setInterval(() => {
|
|
let delta = Date.now() - lastTime;
|
|
lastTime = Date.now();
|
|
this.tick(delta * this.speed);
|
|
}, 1000 / 60);
|
|
}
|
|
|
|
pause() {
|
|
this.playing = false;
|
|
clearInterval(this.interval);
|
|
}
|
|
}
|
|
|
|
let midiPlayer = new MidiPlayer();
|
|
|
|
function uploadMidi() {
|
|
let input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = "audio/midi";
|
|
input.addEventListener("change", function() {
|
|
let reader = new FileReader();
|
|
reader.addEventListener("load", function() {
|
|
let midiFile = new MidiFile(reader.result);
|
|
midiPlayer.loadMidi(midiFile);
|
|
console.log(midiFile);
|
|
});
|
|
reader.readAsArrayBuffer(input.files[0]);
|
|
});
|
|
input.click();
|
|
}
|