k-l-lambda's picture
updated node_modules
4cadbaf
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,
};