const midiToSequence = (midiFile, {timeWarp = 1} = {}) => { const trackStates = []; let beatsPerMinute = 120; const ticksPerBeat = midiFile.header.ticksPerBeat; for (let i = 0; i < midiFile.tracks.length; i++) { trackStates[i] = { nextEventIndex: 0, ticksToNextEvent: ( midiFile.tracks[i].length ? midiFile.tracks[i][0].deltaTime : null ), }; } function getNextEvent () { let ticksToNextEvent = null; let nextEventTrack = null; let nextEventIndex = null; for (let 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 */ const 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 (let i = 0; i < trackStates.length; i++) { if (trackStates[i].ticksToNextEvent != null) trackStates[i].ticksToNextEvent -= ticksToNextEvent; } return { ticksToEvent: ticksToNextEvent, event: nextEvent, track: nextEventTrack, }; } else return null; }; // let midiEvent; const events = []; // function processEvents () { function processNext () { let secondsToGenerate = 0; if (midiEvent.ticksToEvent > 0) { const beatsToGenerate = midiEvent.ticksToEvent / ticksPerBeat; secondsToGenerate = beatsToGenerate / (beatsPerMinute / 60); } // beatsPerMinute must be changed after secondsToGenerate calculation if ( midiEvent.event.type == "meta" && midiEvent.event.subtype == "setTempo" ) { // tempo change events can occur anywhere in the middle and affect events that follow beatsPerMinute = 60e+6 / midiEvent.event.microsecondsPerBeat; } const time = (secondsToGenerate * 1000 * timeWarp) || 0; events.push([ midiEvent, time ]); midiEvent = getNextEvent(); }; // if (midiEvent = getNextEvent()) { while (midiEvent) processNext(); } }; processEvents(); return events; }; const trimSequence = seq => { const status = new Map(); return seq.filter(([{event, ticksToEvent}]) => { if (ticksToEvent > 0) status.clear(); if (event.type !== "channel") return true; const key = `${event.subtype}|${event.channel}|${event.noteNumber}`; if (status.get(key)) { //console.debug("event trimmed:", event, ticksToEvent); return false; } status.set(key, event); return true; }); }; const fixOverlapNotes = seq => { const noteMap = new Map(); const overlapMap = new Map(); const swaps = []; let leapIndex = -1; seq.forEach(([{event, ticksToEvent}], index) => { if (ticksToEvent > 0) leapIndex = index; if (event.type !== "channel") return; const key = `${event.channel}|${event.noteNumber}`; switch (event.subtype) { case "noteOn": if (noteMap.get(key)) overlapMap.set(key, leapIndex); else noteMap.set(key, leapIndex); break; case "noteOff": if (overlapMap.get(key)) { swaps.push([overlapMap.get(key), index]); overlapMap.delete(key); } else noteMap.delete(key); break; } }); // shift overlapped swaps swaps.forEach((swap, i) => { for (let ii = i - 1; ii >= 0; --ii) { const pre = swaps[ii]; if (pre[1] < swap[0]) break; if (swap[0] > pre[0]) ++swap[0]; } }); //console.debug("swaps:", swaps); swaps.forEach(([front, back]) => { if (back >= seq.length - 1 || front < 0) return; const offEvent = seq[back]; const nextEvent = seq[back + 1]; const leapEvent = seq[front]; if (!leapEvent[0].ticksToEvent) { console.warn("invalid front index:", front, back, leapEvent); return; } // ms per tick const tempo = leapEvent[1] / leapEvent[0].ticksToEvent; nextEvent[1] += offEvent[1]; nextEvent[0].ticksToEvent += offEvent[0].ticksToEvent; offEvent[0].ticksToEvent = leapEvent[0].ticksToEvent - 1; leapEvent[0].ticksToEvent = 1; offEvent[1] = offEvent[0].ticksToEvent * tempo; leapEvent[1] = leapEvent[0].ticksToEvent * tempo; //console.debug("swap:", [front, back], offEvent, nextEvent, leapEvent); seq.splice(back, 1); seq.splice(front, 0, offEvent); }); return seq; }; module.exports = { midiToSequence, trimSequence, fixOverlapNotes, };