const { Notation } = require("./MusicNotation.js"); //const msDelay = ms => new Promise(resolve => setTimeout(resolve, ms)); const animationDelay = () => new Promise(resolve => requestAnimationFrame(resolve)); class MidiPlayer { constructor (midiData, {cacheSpan = 600, onMidi, onPlayFinish, onTurnCursor} = {}) { this.cacheSpan = cacheSpan; this.onMidi = onMidi; this.onPlayFinish = onPlayFinish; this.onTurnCursor = onTurnCursor; let notation; if (midiData.notes && Number.isFinite(midiData.endTime)) notation = midiData; else notation = Notation.parseMidi(midiData); this.notation = notation; this.events = notation.events; //console.log("events:", this.events); this.isPlaying = false; this.progressTime = 0; this.startTime = performance.now(); this.duration = notation.endTime; this.cursorTurnDelta = 0; console.assert(notation.tempos && notation.tempos.length, "[MidiPlayer] invalid notation, tempos is empty."); } dispose () { this.isPlaying = false; this.progressTime = 0; } get progressTicks () { return this.notation.timeToTicks(this.progressTime); } set progressTicks (value) { this.progressTime = this.notation.ticksToTime(value); if (this.onTurnCursor) this.onTurnCursor(this.progressTime); } async play ({nextFrame = animationDelay} = {}) { if (this.progressTime >= this.duration) this.progressTime = 0; let now = performance.now(); this.startTime = now - this.progressTime; this.isPlaying = true; let currentEventIndex = this.events.findIndex(event => event.time >= now - this.startTime); while (this.isPlaying) { for (; currentEventIndex < this.events.length; ++currentEventIndex) { const event = this.events[currentEventIndex]; //console.log("play event:", currentEventIndex, event.time, this.progressTime + this.cacheSpan); if (!event || event.time > this.progressTime + this.cacheSpan) break; if (event.data.type === "channel" && this.startTime + event.time >= now) if (this.onMidi) this.onMidi(event.data, this.startTime + event.time); } await nextFrame(); if (!this.isPlaying) break; if (this.cursorTurnDelta !== 0) { const backturn = this.cursorTurnDelta < 0; this.startTime -= this.cursorTurnDelta; this.cursorTurnDelta = 0; if (backturn) { for (; currentEventIndex > 0; --currentEventIndex) { const eventTime = this.events[currentEventIndex].time; if (this.startTime + eventTime < now) break; } } } now = performance.now(); this.progressTime = now - this.startTime; if (this.progressTime > this.duration) { this.isPlaying = false; if (this.onPlayFinish) this.onPlayFinish(); } } } pause () { this.isPlaying = false; } turnCursor (time) { //console.log("onTurnCursor:", time, oldTime); if (this.isPlaying) this.cursorTurnDelta += time - this.progressTime; else this.progressTime = time; if (this.onTurnCursor) this.onTurnCursor(time); } }; module.exports = MidiPlayer;