Spaces:
Sleeping
Sleeping
| 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, | |
| }; | |