Spaces:
Sleeping
Sleeping
const MIDI = require("./MIDI"); | |
const trackDeltaToAbs = events => { | |
let tick = 0; | |
events.forEach(event => { | |
tick += event.deltaTime; | |
event.tick = tick; | |
}); | |
}; | |
const trackAbsToDelta = events => { | |
let lastTick = 0; | |
events.sort((e1, e2) => e1.tick - e2.tick).forEach(event => { | |
event.deltaTime = event.tick - lastTick; | |
lastTick = event.tick; | |
}); | |
}; | |
const sliceTrack = (track, startTick, endTick) => { | |
trackDeltaToAbs(track); | |
const events = []; | |
const status = {}; | |
track.forEach(event => { | |
if (event.tick >= startTick && event.tick <= endTick && event.subtype !== "endOfTrack") | |
events.push({ | |
...event, | |
tick: event.tick - startTick, | |
}); | |
else if (event.tick < startTick) { | |
switch (event.type) { | |
case "meta": | |
status[event.subtype] = event; | |
break; | |
} | |
} | |
}); | |
Object.values(status).forEach(event => events.push({ | |
...event, | |
tick: 0, | |
})); | |
events.push({ | |
tick: endTick - startTick, | |
type: "meta", | |
subtype: "endOfTrack", | |
}); | |
trackAbsToDelta(events); | |
return events; | |
}; | |
const sliceMidi = (midi, startTick, endTick) => ({ | |
header: midi.header, | |
tracks: midi.tracks.map(track => sliceTrack(track, startTick, endTick)), | |
}); | |
const TICKS_PER_BEATS = 480; | |
const EXCLUDE_MIDI_EVENT_SUBTYPES = [ | |
"endOfTrack", "trackName", | |
"noteOn", "noteOff", | |
]; | |
function encodeToMIDIData(notation, {startTime, unclosedNoteDuration = 30e+3} = {}) { | |
notation.microsecondsPerBeat = notation.microsecondsPerBeat || 500000; | |
const ticksPerBeat = TICKS_PER_BEATS; | |
const msToTicks = ticksPerBeat * 1000 / notation.microsecondsPerBeat; | |
const header = { formatType: 0, ticksPerBeat }; | |
const track = []; | |
if (!Number.isFinite(startTime)) { | |
if (!notation.notes || !notation.notes[0]) | |
throw new Error("encodeToMidiData: no start time specificed"); | |
startTime = notation.notes[0].start; | |
} | |
track.push({ time: startTime, type: "meta", subtype: "copyrightNotice", text: `Composed by MusicWdigets. BUILT on ${new Date(Number(process.env.VUE_APP_BUILD_TIME)).toDateString()}` }); | |
const containsTempo = notation.events && notation.events.find(event => event.subtype == "setTempo"); | |
if (!containsTempo) { | |
track.push({ time: startTime, type: "meta", subtype: "timeSignature", numerator: 4, denominator: 4, thirtyseconds: 8 }); | |
track.push({ time: startTime, type: "meta", subtype: "setTempo", microsecondsPerBeat: notation.microsecondsPerBeat }); | |
} | |
//if (notation.correspondences) | |
// track.push({ time: startTime, type: "meta", subtype: "text", text: "find-corres:" + notation.correspondences.join(",") }); | |
let endTime = startTime || 0; | |
if (notation.notes) { | |
for (const note of notation.notes) { | |
track.push({ | |
time: note.start, | |
type: "channel", | |
subtype: "noteOn", | |
channel: note.channel || 0, | |
noteNumber: note.pitch, | |
velocity: note.velocity, | |
finger: note.finger, | |
}); | |
endTime = Math.max(endTime, note.start); | |
if (Number.isFinite(unclosedNoteDuration)) | |
note.duration = note.duration || unclosedNoteDuration; | |
if (note.duration) { | |
track.push({ | |
time: note.start + note.duration, | |
type: "channel", | |
subtype: "noteOff", | |
channel: note.channel || 0, | |
noteNumber: note.pitch, | |
velocity: 0, | |
}); | |
endTime = Math.max(endTime, note.start + note.duration); | |
} | |
} | |
} | |
if (notation.events) { | |
const events = notation.events.filter(event => !EXCLUDE_MIDI_EVENT_SUBTYPES.includes(event.data.subtype)); | |
for (const event of events) { | |
track.push({ | |
time: event.time, | |
...event.data, | |
}); | |
endTime = Math.max(endTime, event.time); | |
} | |
} | |
track.push({ time: endTime + 100, type: "meta", subtype: "endOfTrack" }); | |
track.sort(function (e1, e2) { return e1.time - e2.time; }); | |
// append finger event after every noteOn event | |
track.map((event, index) => ({event, index})) | |
.filter(({event}) => event.subtype == "noteOn" && event.finger != null) | |
.reverse() | |
.forEach(({event, index}) => track.splice(index + 1, 0, { | |
time: event.time, | |
type: "meta", | |
subtype: "text", | |
text: `fingering(${event.finger})`, | |
})); | |
track.forEach(event => event.ticks = Math.round((event.time - startTime) * msToTicks)); | |
track.forEach((event, i) => event.deltaTime = (event.ticks - (i > 0 ? track[i - 1].ticks : 0))); | |
return {header, tracks: [track]}; | |
}; | |
function encodeToMIDI(notation, options) { | |
const data = encodeToMIDIData(notation, options); | |
return MIDI.encodeMidiFile(data); | |
}; | |
module.exports = { | |
sliceMidi, | |
encodeToMIDIData, | |
encodeToMIDI, | |
}; | |