const MidiSequence = require("./MidiSequence.js"); const PedalControllerTypes = { 64: "Sustain", 65: "Portamento", 66: "Sostenuto", 67: "Soft", }; class Notation { static parseMidi (data, {fixOverlap = true} = {}) { const channelStatus = []; const pedalStatus = {}; const pedals = {}; const channels = []; const bars = []; let time = 0; let millisecondsPerBeat = 600000 / 120; let beats = 0; let numerator = 4; let barIndex = 0; const keyRange = {}; let rawTicks = 0; let ticks = 0; let correspondences; const tempos = []; const ticksPerBeat = data.header.ticksPerBeat; let rawEvents = MidiSequence.midiToSequence(data); if (fixOverlap) rawEvents = MidiSequence.trimSequence(MidiSequence.fixOverlapNotes(rawEvents)); const events = rawEvents.map(d => ({ data: d[0].event, track: d[0].track, deltaTime: d[1], deltaTicks: d[0].ticksToEvent, })); let index = 0; const ticksNormal = 1; for (const ev of events) { rawTicks += ev.deltaTicks; ticks = Math.round(rawTicks * ticksNormal); if (ev.deltaTicks > 0) { // append bars const deltaBeats = ev.deltaTicks / ticksPerBeat; for (let b = Math.ceil(beats); b < beats + deltaBeats; ++b) { const t = time + (b - beats) * millisecondsPerBeat; bars.push({time: t, index: barIndex % numerator}); ++barIndex; } beats += deltaBeats; } time += ev.deltaTime; //const ticksTime = beats * millisecondsPerBeat; //console.log("time:", time, ticksTime, ticksTime - time); ev.time = time; ev.ticks = ticks; const event = ev.data; switch (event.type) { case "channel": //channelStatus[event.channel] = channelStatus[event.channel] || []; switch (event.subtype) { case "noteOn": { const pitch = event.noteNumber; //channelStatus[event.channel][pitch] = { channelStatus.push({ channel: event.channel, pitch, startTick: ticks, start: time, velocity: event.velocity, beats: beats, track: ev.track, }); keyRange.low = Math.min(keyRange.low || pitch, pitch); ev.index = index; ++index; } break; case "noteOff": { const pitch = event.noteNumber; channels[event.channel] = channels[event.channel] || []; const statusIndex = channelStatus.findIndex(status => status.channel == event.channel && status.pitch == pitch); if (statusIndex >= 0) { const status = channelStatus.splice(statusIndex, 1)[0]; channels[event.channel].push({ channel: event.channel, startTick: status.startTick, endTick: ticks, pitch, start: status.start, duration: time - status.start, velocity: status.velocity, beats: status.beats, track: status.track, finger: status.finger, }); } else console.debug("unexpected noteOff: ", time, event); keyRange.high = Math.max(keyRange.high || pitch, pitch); } break; case "controller": switch (event.controllerType) { // pedal controllers case 64: case 65: case 66: case 67: const pedalType = PedalControllerTypes[event.controllerType]; pedalStatus[event.channel] = pedalStatus[event.channel] || {}; pedals[event.channel] = pedals[event.channel] || []; const status = pedalStatus[event.channel][pedalType]; if (status) pedals[event.channel].push({type: pedalType, start: status.start, duration: time - status.start, value: status.value}); pedalStatus[event.channel][pedalType] = {start: time, value: event.value}; break; } break; } break; case "meta": switch (event.subtype) { case "setTempo": millisecondsPerBeat = event.microsecondsPerBeat / 1000; //beats = Math.round(beats); //console.assert(Number.isFinite(time), "invalid time:", time); tempos.push({tempo: event.microsecondsPerBeat, tick: ticks, time}); break; case "timeSignature": numerator = event.numerator; barIndex = 0; break; case "text": if (!correspondences && /^find-corres:/.test(event.text)) { const captures = event.text.match(/:([\d\,-]+)/); const str = captures && captures[1] || ""; correspondences = str.split(",").map(s => Number(s)); } else if (/fingering\(.*\)/.test(event.text)) { const [_, fingers] = event.text.match(/\((.+)\)/); const finger = Number(fingers); if (!Number.isNaN(finger)) { const status = channelStatus[channelStatus.length - 1]; if (status) status.finger = finger; const event = events.find(e => e.index == index - 1); if (event) event.data.finger = finger; } } break; case "copyrightNotice": console.log("MIDI copyright:", event.text); break; } break; } } channelStatus.forEach(status => { console.debug("unclosed noteOn event at", status.startTick, status); channels[status.channel].push({ startTick: status.startTick, endTick: ticks, pitch: status.pitch, start: status.start, duration: time - status.start, velocity: status.velocity, beats: status.beats, track: status.track, finger: status.finger, }); }); return new Notation({ channels, keyRange, pedals, bars, endTime: time, endTick: ticks, correspondences, events, tempos, ticksPerBeat, meta: {}, }); } constructor (fields) { Object.assign(this, fields); // channels to notes this.notes = []; for (const channel of this.channels) { if (channel) { for (const note of channel) this.notes.push(note); } } this.notes.sort(function (n1, n2) { return n1.start - n2.start; }); for (const i in this.notes) this.notes[i].index = Number(i); // duration this.duration = this.notes.length > 0 ? (this.endTime - this.notes[0].start) : 0, //this.endSoftIndex = this.notes.length ? this.notes[this.notes.length - 1].softIndex : 0; // pitch map this.pitchMap = []; for (const c in this.channels) { for (const n in this.channels[c]) { const pitch = this.channels[c][n].pitch; this.pitchMap[pitch] = this.pitchMap[pitch] || []; this.pitchMap[pitch].push(this.channels[c][n]); } } this.pitchMap.forEach(notes => notes.sort((n1, n2) => n1.start - n2.start)); /*// setup measure notes index if (this.measures) { const measure_list = []; let last_measure = null; const measure_entries = Object.entries(this.measures).sort((e1, e2) => Number(e1[0]) - Number(e2[0])); for (const [t, measure] of measure_entries) { //console.log("measure time:", Number(t)); measure.startTick = Number(t); measure.notes = []; if (last_measure) last_measure.endTick = measure.startTick; const m = measure.measure; measure_list[m] = measure_list[m] || []; measure_list[m].push(measure); last_measure = measure; } if (last_measure) last_measure.endTick = this.notes[this.notes.length - 1].endTick; for (const i in this.notes) { const note = this.notes[i]; for (const t in this.measures) { const measure = this.measures[t]; if (note.startTick >= measure.startTick && note.startTick < measure.endTick || note.endTick > measure.startTick && note.endTick <= measure.endTick) measure.notes.push(note); } } this.measure_list = measure_list; }*/ // prepare beats info if (this.meta.beatInfos) { for (let i = 0; i < this.meta.beatInfos.length; ++i) { const info = this.meta.beatInfos[i]; if (i > 0) { const lastInfo = this.meta.beatInfos[i - 1]; info.beatIndex = lastInfo.beatIndex + Math.ceil((info.tick - lastInfo.tick) / this.ticksPerBeat); } else info.beatIndex = 0; } } // compute tempos tick -> time { let time = 0; let ticks = 0; let tempo = 500000; for (const entry of this.tempos) { const deltaTicks = entry.tick - ticks; time += (tempo / 1000) * deltaTicks / this.ticksPerBeat; ticks = entry.tick; tempo = entry.tempo; entry.time = time; } } } findChordBySoftindex (softIndex, radius = 0.8) { return this.notes.filter(note => Math.abs(note.softIndex - softIndex) < radius); } averageTempo (tickRange) { tickRange = tickRange || {from: 0, to: this.endtick}; console.assert(this.tempos, "no tempos."); console.assert(tickRange.to > tickRange.from, "range is invalid:", tickRange); const span = index => { const from = Math.max(tickRange.from, this.tempos[index].tick); const to = (index < this.tempos.length - 1) ? Math.min(this.tempos[index + 1].tick, tickRange.to) : tickRange.to; return Math.max(0, to - from); }; const tempo_sum = this.tempos.reduce((sum, tempo, index) => sum + tempo.tempo * span(index), 0); const average = tempo_sum / (tickRange.to - tickRange.from); // convert microseconds per beat to beats per minute return 60e+6 / average; } ticksToTime (tick) { console.assert(Number.isFinite(tick), "invalid tick value:", tick); console.assert(this.tempos && this.tempos.length, "no tempos."); const next_tempo_index = this.tempos.findIndex(tempo => tempo.tick > tick); const tempo_index = next_tempo_index < 0 ? this.tempos.length - 1 : Math.max(next_tempo_index - 1, 0); const tempo = this.tempos[tempo_index]; return tempo.time + (tick - tempo.tick) * tempo.tempo * 1e-3 / this.ticksPerBeat; } timeToTicks (time) { console.assert(Number.isFinite(time), "invalid time value:", time); console.assert(this.tempos && this.tempos.length, "no tempos."); const next_tempo_index = this.tempos.findIndex(tempo => tempo.time > time); const tempo_index = next_tempo_index < 0 ? this.tempos.length - 1 : Math.max(next_tempo_index - 1, 0); const tempo = this.tempos[tempo_index]; return tempo.tick + (time - tempo.time) * this.ticksPerBeat / (tempo.tempo * 1e-3); } tickRangeToTimeRange (tickRange) { console.assert(tickRange.to >= tickRange.from, "invalid tick range:", tickRange); return { from: this.ticksToTime(tickRange.from), to: this.ticksToTime(tickRange.to), }; } /*getMeasureRange (measureRange) { console.assert(Number.isInteger(measureRange.start) && Number.isInteger(measureRange.end), "invalid measure range:", measureRange); console.assert(this.measure_list && this.measure_list[measureRange.start] && this.measure_list[measureRange.end], "no measure data for specific index:", this.measure_list, measureRange); const startMeasure = this.measure_list[measureRange.start][0]; let endMeasure = null; for (const measure of this.measure_list[measureRange.end]) { if (measure.endTick > startMeasure.startTick) { endMeasure = measure; break; } } // there no path between start measure and end measure. if (!endMeasure) return null; const tickRange = {from: startMeasure.startTick, to: endMeasure.endTick, duration: endMeasure.endTick - startMeasure.startTick}; const timeRange = this.tickRangeToTimeRange(tickRange); timeRange.duration = timeRange.to - timeRange.from; return { tickRange, timeRange, }; }*/ scaleTempo ({factor, headTempo}) { console.assert(this.tempos && this.tempos.length, "[Notation.scaleTempo] tempos is empty."); if (headTempo) factor = headTempo / this.tempos[0].tempo; console.assert(Number.isFinite(factor) && factor > 0, "[Notation.scaleTempo] invalid factor:", factor); this.tempos.forEach(tempo => { tempo.tempo *= factor; tempo.time *= factor; }); this.events.forEach(event => { event.deltaTime *= factor; event.time *= factor; }); this.notes.forEach(note => { note.start *= factor; note.duration *= factor; }); this.endTime *= factor; } }; module.exports = { Notation, };