lotus / inc /lilyNotation /matcher.ts
k-l-lambda's picture
commit lotus dist.
d605f27
import {MusicNotation, Matcher} from "@k-l-lambda/music-widgets";
import {MIDI} from "@k-l-lambda/music-widgets";
import {WHOLE_DURATION_MAGNITUDE} from "../lilyParser/utils";
import {Notation} from "./notation";
import ImplicitType from "./implicitType";
export interface MatcherResult {
criterion: MusicNotation.NotationData,
sample: MusicNotation.NotationData,
path: number[], // S to C
trackMap?: number[], // S to C
};
const matchWithMIDI = async (lilyNotation: Notation, target: MIDI.MidiData): Promise<MatcherResult> => {
const midiTickFactor = (WHOLE_DURATION_MAGNITUDE / 4) / target.header.ticksPerBeat;
const midiNotation = MusicNotation.Notation.parseMidi(target);
const criterion = lilyNotation.toPerformingNotation();
Matcher.genNotationContext(criterion, {softIndexFactor: 1e3});
Matcher.genNotationContext(midiNotation, {softIndexFactor: midiTickFactor * 1e3});
//console.debug("notations:", criterion, midiNotation);
for (const note of midiNotation.notes)
Matcher.makeMatchNodes(note, criterion);
const navigator = await Matcher.runNavigation(criterion, midiNotation);
const path = navigator.path();
const matcher = {criterion, sample: midiNotation, path};
lilyNotation.assignMatcher(matcher);
return matcher;
};
const IMPLICIT_PITCH_BIAS = {
[ImplicitType.Mordent]: [0, -1, -2],
[ImplicitType.Prall]: [0, 1, 2],
[ImplicitType.Turn]: [-2, -1, 0, 1, 2],
[ImplicitType.Trill]: [0, 1, 2],
};
const alignNotationTicks = (source: MusicNotation.Notation, criterion: MusicNotation.Notation, {midiTrackTickBias}: {midiTrackTickBias?: number[]}) => {
const midiTickFactor = criterion.ticksPerBeat / source.ticksPerBeat;
source.ticksPerBeat = criterion.ticksPerBeat;
source.notes.forEach(note => {
note.startTick *= midiTickFactor;
note.endTick *= midiTickFactor;
const bias = midiTrackTickBias && midiTrackTickBias[note.track];
if (bias) {
note.startTick += bias;
note.endTick += bias;
}
});
source.events.forEach(event => {
event.ticks *= midiTickFactor;
const bias = midiTrackTickBias && midiTrackTickBias[event.track];
if (bias)
event.ticks += bias;
});
source.events.sort((e1, e2) => e1.ticks - e2.ticks);
};
const matchWithExactMIDI = async (lilyNotation: Notation, target: MIDI.MidiData): Promise<MatcherResult> => {
const criterion = lilyNotation.toPerformingNotation();
const trackTickBiasMap = lilyNotation.trackTickBias;
const targetTrackNames = target.tracks.map(events => {
const nameEvent = events.find(event => event.subtype === "trackName");
return nameEvent ? nameEvent.text : null;
});
const trackIndexC2S = lilyNotation.trackNames.map(name => targetTrackNames.indexOf(name));
//if (trackIndexC2S.includes(-1))
// console.debug("mismatched track found:", trackIndexC2S, lilyNotation.trackNames, targetTrackNames);
const midiTrackTickBias = targetTrackNames.map(name => name ? (trackTickBiasMap[name] || 0) : 0);
//console.log("midiTrackTickBias:", midiTrackTickBias);
const midiNotation = MusicNotation.Notation.parseMidi(target, {fixOverlap: false});
alignNotationTicks(midiNotation, criterion, {midiTrackTickBias});
// 1st pass: ordinary notes exact matching
const noteKey = note => `${note.track}|${Math.round(note.startTick)}|${note.pitch}`;
const snoteMap: {[key: string]: MusicNotation.Note} = {};
midiNotation.notes.forEach(note => {
snoteMap[noteKey(note)] = note;
});
const path = Array(midiNotation.notes.length).fill(-1);
const restCNotes = [];
criterion.notes.forEach(note => {
const implicit = !!(note as any).implicitType;
const key = noteKey({...note, track: trackIndexC2S[note.track - 1]});
const sn = snoteMap[key];
if (sn) {
path[sn.index] = note.index;
delete snoteMap[key];
if (!implicit)
return;
}
restCNotes.push(note);
});
// 2nd pass: implicit notes matching
const restCNotes2 = [];
const restSNotes = Object.values(snoteMap);
restCNotes.forEach(note => {
if (note.implicitType) {
const pbs = IMPLICIT_PITCH_BIAS[note.implicitType];
if (pbs) {
const matches = restSNotes.filter(sn => {
const tick = Math.round(sn.startTick);
const pb = sn.pitch - note.pitch;
return tick >= note.startTick && tick <= note.endTick && pbs.includes(pb);
});
if (matches.length) {
matches.forEach(sn => {
path[sn.index] = note.index;
delete snoteMap[noteKey(sn)];
});
return;
}
}
}
restCNotes2.push(note);
});
// 3rd pass: rest fuzzy matching
const restCNotes3 = [];
restCNotes2.forEach(note => {
const sn = restSNotes.find(sn => sn.pitch === note.pitch && Math.abs(sn.startTick - note.startTick) < 4);
if (sn) {
path[sn.index] = note.index;
delete snoteMap[noteKey(sn)];
}
else
restCNotes3.push(note);
});
// 4th pass: nearest matching
const restSNotes3 = Object.values(snoteMap);
//console.log("restCNotes3:", restCNotes3.map(n => n.index));
//console.log("restSNotes3:", restSNotes3.map(n => n.index));
restSNotes3.forEach(note => {
const cn = restCNotes3.reduce((best, cn) => {
if (cn.pitch === note.pitch) {
if (!best || Math.abs(cn.startTick - note.startTick) < Math.abs(best.startTick - note.startTick))
return cn;
}
return best;
}, null);
if (cn)
path[note.index] = cn.index;
});
const trackMap = Object.entries(trackIndexC2S).reduce((map, [ct, st]) => ((st >= 0 && (map[st] = Number(ct) + 1)), map), []);
const matcher = {criterion, sample: midiNotation, path, trackMap};
lilyNotation.assignMatcher(matcher);
return matcher;
};
export {
matchWithMIDI,
matchWithExactMIDI,
};