piano/frontend/midiPlayer.js

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();
}