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