import {Matcher} from "@k-l-lambda/music-widgets"; // eslint-disable-next-line import {MusicNotation} from "@k-l-lambda/music-widgets"; import LogRecorder from "../logRecorder"; import {roundNumber, constants} from "./utils"; import {fuzzyMatchNotations, assignNotationEventsIds} from "../lilyNotation"; import {StaffContext} from "../pitchContext"; // eslint-disable-next-line import {PitchContext} from "../pitchContext"; import pick from "../pick"; const TICKS_PER_BEAT = 480; const parseNotationInMeasure = (context: StaffContext, measure) => { //console.log("parseNotationInMeasure:", measure); context.resetAlters(); const notes = []; //const xs = {}; const pitchNotes: {[key: number]: any[]} = {}; let keyAltered = false; for (const token of measure.tokens) { if (!token.symbols.size) continue; if (token.is("ALTER")) { // ignore invalid alters if (Number.isInteger(token.ry * 2)) { if (token.is("KEY") /*|| token.logicX < measure.headX*/) { if (!keyAltered) { context.resetKeyAlters(); keyAltered = true; } context.setKeyAlter(token.ry, token.alterValue); } // alter with href may be chordmode element else if (!token.href) context.setAlter(token.ry, token.alterValue); } } else if (token.is("CLEF")) context.setClef(token.ry, token.clefValue); else if (token.is("OCTAVE")) context.setOctaveShift(token.octaveShiftValue); else if (token.is("TIME_SIG")) { if (token.ry === 0) context.setBeatsPerMeasure(token.timeSignatureValue); } else if (token.is("NOTEHEAD")) { /*// ignore tempo note heads if (token.source.substr(0, 6) === "\\tempo") continue;*/ const contextIndex = context.snapshot(); const note = { x: roundNumber(token.logicX, 1e-4) - measure.noteRange.begin, rx: token.rx - measure.noteRange.begin, y: token.ry, pitch: context.yToPitch(token.ry), id: token.href, tied: token.tied, contextIndex, type: token.noteType, stemUp: token.stemUp, }; notes.push(note); //xs[note.rx] = xs[note.rx] || new Set(); //xs[note.rx].add(token.ry); pitchNotes[note.pitch] = pitchNotes[note.pitch] || []; pitchNotes[note.pitch].push(note); } } // merge first degree side by side notes Object.values(pitchNotes).forEach(notes => { //notes.length > 1 && console.log("notes:", notes); for (let i = 1; i < notes.length; ++i) { const note = notes[i]; const lastNote = notes[i - 1]; if (note.rx - lastNote.rx <= 1.5 && note.stemUp !== lastNote.stemUp) note.tied = true; } }); const duration = context.beatsPerMeasure * TICKS_PER_BEAT; //console.log("notes:", notes); notes.forEach(note => { /*// merge first degree side by side notes if (xs[note.rx - 1.5] && xs[note.rx - 1.5].has(note.y)) note.x -= constants.CLOSED_NOTEHEAD_INTERVAL_FIRST_DEG; else if (xs[note.rx - 1.25] && xs[note.rx - 1.25].has(note.y)) note.x -= constants.CLOSED_NOTEHEAD_INTERVAL_FIRST_DEG;*/ context.track.appendNote(note.x, { pitch: note.pitch, id: note.id, tied: note.tied, contextIndex: note.contextIndex, type: note.type, }); }); context.track.endTime += duration; }; const parseNotationInStaff = (context : StaffContext, staff) => { //console.log("parseNotationInStaff:", staff); context.resetKeyAlters(); if (staff) { for (const measure of staff.measures) parseNotationInMeasure(context, measure); } }; interface SheetNotation extends MusicNotation.NotationData { pitchContexts: PitchContext[][]; }; const parseNotationFromSheetDocument = (document, {logger = new LogRecorder()} = {}): SheetNotation => { if (!document.trackCount) return null; const contexts = Array(document.trackCount).fill(null).map(() => new StaffContext({logger})); for (const page of document.pages) { logger.append("parsePage", document.pages.indexOf(page)); for (const system of page.systems) { logger.append("parseSystem", page.systems.indexOf(system)); console.assert(system.staves.length === contexts.length, "staves size mismatched:", contexts.length, system.staves.length); if (system.staves.length !== contexts.length) logger.append("mismatchedStaves", {contextLen: contexts.length, stavesLen: system.staves.length, system}); system.staves.forEach((staff, i) => { logger.append("parseStaff", i); if (contexts[i]) parseNotationInStaff(contexts[i], staff); }); } } //console.log("result:", contexts); // merge tracks contexts.forEach((context, t) => context.track.notes.forEach(note => note.track = t)); const notes = [].concat(...contexts.map(context => context.track.notes)).sort((n1, n2) => (n1.time - n2.time) + (n1.pitch - n2.pitch) * -1e-3); logger.append("notesBeforeClusterize", notes.map(note => pick(note, ["time", "pitch"]))); clusterizeNotes(notes); return { endTime: contexts[0].track.endTime, notes, pitchContexts: contexts.map(context => context.track.contexts), }; }; const assignTickByLocationTable = (notation: SheetNotation, locationTickTable: {[key: string]: number}) => { notation.notes.forEach((note: any) => { const location = note.id && note.id.match(/^\d+:\d+/)[0]; if (locationTickTable[location] === undefined) { if (note.id) { const [line, column] = note.id.match(/\d+/g).map(Number); for (let c = column - 1; c >= 0; --c) { const loc = `${line}:${c}`; if (locationTickTable[loc]) { note.time = locationTickTable[loc]; return; } } } console.warn("[assignTickByLocationTable] location not found in table:", location); return; } note.time = locationTickTable[location]; }); }; const xClusterize = x => Math.tanh((x / 1.2) ** 12); const CLUSTERIZE_WIDTH_FACTORS = [1, 1, .5, .5]; // get time closed for notes in a chord const clusterizeNotes = notes => { notes.forEach((note, i) => { if (i < 1) note.deltaTime = 0; else { const delta = note.time - notes[i - 1].time; const noteType = Math.min(note.type, notes[i - 1].type); note.deltaTime = xClusterize(delta / (constants.NOTE_TYPE_WIDTHS[noteType] * CLUSTERIZE_WIDTH_FACTORS[noteType])); } }); notes.forEach((note, i) => i > 0 && (note.time = notes[i - 1].time + note.deltaTime * 480)); }; const matchNotations = async (midiNotation, svgNotation, {enableFuzzy = true} = {}) => { console.assert(midiNotation, "midiNotation is null."); console.assert(svgNotation, "svgNotation is null."); const TIME_FACTOR = 4; // map svgNotation without duplicated ones const noteMap = {}; const notePMap = {}; const svgNotes = svgNotation.notes.reduce((notes, note) => { if (note.tied) { if (notePMap[note.pitch]) { const tieNote = notePMap[note.pitch]; tieNote.ids = tieNote.ids || [tieNote.id]; tieNote.ids.push(note.id); } } else { const index = `${note.time}-${note.pitch}`; if (noteMap[index]) { noteMap[index].ids = noteMap[index].ids || [noteMap[index].id]; noteMap[index].ids.push(note.id); } else { const sn = {start: note.time * TIME_FACTOR, pitch: note.pitch, id: note.id, track: note.track, contextIndex: note.contextIndex}; noteMap[index] = sn; notePMap[sn.pitch] = sn; notes.push(sn); } } return notes; }, []).map((note, index) => ({index, ...note})); const criterion = { notes: svgNotes, pitchMap: null, }; criterion.pitchMap = criterion.notes.reduce((map, note) => { map[note.pitch] = map[note.pitch] || []; map[note.pitch].push(note); return map; }, []); const sample = { notes: midiNotation.notes.map(({startTick, pitch}, index) => ({index, start: startTick * TIME_FACTOR, pitch})), }; Matcher.genNotationContext(criterion); Matcher.genNotationContext(sample); //console.log("criterion:", criterion, sample); for (const note of sample.notes) Matcher.makeMatchNodes(note, criterion); //console.log("before.runNavigation:", performance.now()); const navigator = await Matcher.runNavigation(criterion, sample); //console.log("navigator:", navigator); //console.log("after.runNavigation:", performance.now()); const path = navigator.path(); //const path = navigator.sample.notes.map(note => note.matches[0] ? note.matches[0].ci : -1); if (enableFuzzy) fuzzyMatchNotations(path, criterion, sample); //console.log("path:", path); //console.log("after.path:", performance.now()); path.forEach((ci, si) => { if (ci >= 0) { const cn = criterion.notes[ci]; const ids = cn.ids || [cn.id]; midiNotation.notes[si].ids = ids; midiNotation.notes[si].staffTrack = cn.track; midiNotation.notes[si].contextIndex = cn.contextIndex; } }); //console.log("after.path.forEach:", performance.now()); // assign ids onto MIDI events assignNotationEventsIds(midiNotation); //console.log("after.ids:", performance.now()); //console.log("midiNotation:", midiNotation.events); return {criterion, sample, path}; }; const assignIds = (midiNotation: MusicNotation.NotationData, noteIds: string[][]) => { noteIds.forEach((ids, i) => { const note = midiNotation.notes[i]; if (note && ids) note.ids = ids; }); assignNotationEventsIds(midiNotation); }; export { parseNotationFromSheetDocument, assignTickByLocationTable, matchNotations, assignIds, SheetNotation, };