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, | |
}; | |