lotus / inc /lilyNotation /fuzzyMatch.ts
k-l-lambda's picture
commit lotus dist.
d605f27
// eslint-disable-next-line
import {MusicNotation} from "@k-l-lambda/music-widgets";
interface Note {
index: number;
pitch: number;
softIndex: number;
baseCsi?: number;
};
const notePitchDistance = (n1: Note, n2: Note): number => {
let differ = Math.abs(n1.pitch - n2.pitch);
if (differ > 7)
differ -= 11.5;
return differ * 2;
};
const fuzzyMatchNotes = (path: number[], cnotes: Note[], snotes: Note[], pitchTolerance: number, offsetTolerance = 2): {fixed: number, matched: number} => {
const candidates: {[key: number]: {si: number, distance: number}} = {};
const matched = snotes.reduce((count, snote) => {
const pcan = cnotes.filter(cnote => notePitchDistance(cnote, snote) <= pitchTolerance
&& Math.abs(snote.baseCsi - cnote.softIndex) < offsetTolerance)
.sort((n1, n2) => Math.abs(snote.baseCsi - n1.softIndex) - Math.abs(snote.baseCsi - n2.softIndex));
if (pcan.length) {
const bestDistance = notePitchDistance(pcan[0], snote);
const last = candidates[pcan[0].index];
if (!last || bestDistance < last.distance)
candidates[pcan[0].index] = {si: snote.index, distance: bestDistance};
++count;
}
return count;
}, 0);
const fixed = Object.entries(candidates).map(([ci, {si}]) => {
path[si] = Number(ci);
return [si, Number(ci)];
});
//console.debug("fuzzyMatch.fixed:", fixed.map(pair => pair.join()));
return {fixed: fixed.length, matched};
};
const fuzzyMatchNotations = (path: number[], criterion: MusicNotation.NotationData, sample: MusicNotation.NotationData, {pitchToleranceMax = 4} = {}) => {
// unmatch overlapped indices
const overlapped = new Set();
path.reduce((set, ci) => {
if (ci >= 0) {
if (set.has(ci))
overlapped.add(ci);
else
set.add(ci);
}
return set;
}, new Set());
//console.debug("overlapped:", overlapped, [...path]);
path.forEach((ci, si) => {
if (overlapped.has(ci))
path[si] = -1;
});
// assign base offset on sample notes
let offset = null;
for (let i = 0; i < sample.notes.length; ++i) {
const note = sample.notes[i];
const ci = path[i];
if (ci < 0)
(note as any).baseOffset = offset;
else
offset = note.softIndex - criterion.notes[ci].softIndex;
}
for (let i = sample.notes.length - 1; i >= 0; --i) {
const note = sample.notes[i];
const ci = path[i];
if (ci < 0) {
const lastOffset = (note as any).baseOffset;
(note as any).baseOffset = Number.isFinite(lastOffset) ? (lastOffset + offset) / 2 : offset;
}
else
offset = note.softIndex - criterion.notes[ci].softIndex;
}
let pitchTolerance = 0;
while (true) {
const cnotes = criterion.notes.filter(note => !path.some(ci => ci === note.index)).map(note => ({index: note.index, pitch: note.pitch, softIndex: note.softIndex}));
const snotes = sample.notes.filter(note => path[note.index] < 0 && Number.isFinite((note as any).baseOffset))
.map(note => ({index: note.index, pitch: note.pitch, softIndex: note.softIndex, baseCsi: note.softIndex - (note as any).baseOffset}));
//console.debug("fuzzyMatch.notes:", cnotes.map(note => note.index), snotes.map(note => note.index));
if (!cnotes.length || !snotes.length)
break;
const {fixed, matched} = fuzzyMatchNotes(path, cnotes, snotes, pitchTolerance);
if (matched)
console.debug("fuzzyMatch.pass:", `c:${cnotes.length}, s:${snotes.length},`, pitchTolerance, `${fixed}/${matched}`);
if (fixed >= matched) {
++pitchTolerance;
if (pitchTolerance > pitchToleranceMax)
break;
}
}
//console.debug("fuzzyMatch.path:", path);
};
export {
fuzzyMatchNotations,
};