Spaces:
Sleeping
Sleeping
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, | |
}; | |