import {WHOLE_DURATION_MAGNITUDE, FractionNumber} from "./utils"; import {MusicBlock, LyricMode, ContextedMusic, Variable, Command, Duration, Lyric, Times, LiteralString} from "./lilyTerms"; // eslint-disable-next-line import LilyInterpreter, {MusicTrack} from "./lilyInterpreter"; // eslint-disable-next-line import {SimultaneousList} from "./lilyTerms"; const COLOR_NAMES = [ "lyrGray", "lyrRed", "lyrGreen", "lyrYellow", ]; const createPianoRhythmTrack = ({ticks, durationMagnitude, subdivider, color = null}: { ticks: Set, durationMagnitude: number, subdivider: number, color?: string, }): LyricMode => { const granularity = WHOLE_DURATION_MAGNITUDE / subdivider; //console.log("ticks:", ticks, granularity); const denominator = 2 ** Math.floor(Math.log2(subdivider)); const duration = new Duration({number: denominator, dots: 0}); let body = []; if (color) body.push(new Variable({name: color})); for (let tick = 0; tick < durationMagnitude; tick += granularity) { const variable = new Variable({name: ticks.has(tick) ? "dotB" : "dotW"}); const lyric = new Lyric({content: variable, duration: tick === 0 ? duration.clone() : null}); body.push(lyric); } if (denominator !== subdivider) { const fraction = new FractionNumber(denominator, subdivider).reduced; const times = new Times({cmd: "times", args: [fraction.toString(), new MusicBlock({body})]}); body = [times]; } return new LyricMode({cmd: "lyricmode", args: [new MusicBlock({body})]}); }; const createPianoNumberTrack = ({durationMagnitude, subdivider, measureTicks, trackTicks, colored}: { durationMagnitude: number, subdivider: number, measureTicks: [number, number][], trackTicks: Set[], colored?: boolean, }): LyricMode => { const granularity = WHOLE_DURATION_MAGNITUDE / subdivider; const denominator = 2 ** Math.floor(Math.log2(subdivider)); const duration = new Duration({number: denominator, dots: 0}); const words = []; let number = 1; for (let tick = 0; tick < durationMagnitude; tick += granularity) { // eslint-disable-next-line if (measureTicks.some(([_, t]) => t === tick)) number = 1; let type = 0; trackTicks.forEach((track, i) => track.has(tick) && (type += 2 ** i)); words.push({number, type}); ++number; } let body = [].concat(...words.map(({number, type}, i) => [ colored ? new Variable({name: COLOR_NAMES[type]}) : null, new Lyric({content: LiteralString.fromString(number.toString()), duration: i === 0 ? duration.clone() : null}), ])).map(term => term); if (denominator !== subdivider) { const fraction = new FractionNumber(denominator, subdivider).reduced; const times = new Times({cmd: "times", args: [fraction.toString(), new MusicBlock({body})]}); body = [times]; } return new LyricMode({cmd: "lyricmode", args: [new MusicBlock({body})]}); }; interface PianoRhythmOptions { colored?: boolean; numberTrack?: boolean; dotTracks?: boolean; }; const isPianoStaff = term => term instanceof ContextedMusic && term.type === "PianoStaff"; export const createPianoRhythm = (interpreter: LilyInterpreter, {dotTracks = true, numberTrack, colored}: PianoRhythmOptions = {}) => { console.assert(!!interpreter.scores.length, "interpreter.scores is empty."); let pianoMusic = interpreter.mainScore && interpreter.mainScore.findFirst(isPianoStaff) as ContextedMusic; if (!pianoMusic) pianoMusic = interpreter.scores[0] && interpreter.scores[0].findFirst(isPianoStaff) as ContextedMusic; //console.log("pianoMusic:", pianoMusic); if (!pianoMusic) throw new Error("[createPianoRhythm] no pianoMusic"); const list = pianoMusic.body as SimultaneousList; // remove lyrics tracks list.list = list.list.filter(music => !(music instanceof ContextedMusic) || music.type !== "Lyrics"); const staves = list.list.filter(music => music instanceof ContextedMusic && music.type === "Staff"); const upStaffPos = list.list.indexOf(staves[0]) + 1; interpreter.updateTrackAssignments(); const layoutMusic = interpreter.layoutMusic; const trackTicks: Set[] = staves.map(staff => { const variables = staff.findAll(Variable).map(variable => variable.name); const voices = layoutMusic.musicTracks.filter(track => variables.includes(track.name)); return new Set([].concat(...voices.map(voice => voice.block.noteTicks))); }); const subdivider = layoutMusic.getNoteDurationSubdivider(); const durationMagnitude = layoutMusic.musicTracks[0].durationMagnitude; //console.log("staves:", staves); if (dotTracks) { trackTicks.forEach((ticks, i) => { const color = COLOR_NAMES[2 ** Math.min(i, 1)]; const lyric = createPianoRhythmTrack({ticks, durationMagnitude, subdivider, color: colored ? color : null}); // TODO: create with clause at pos[2] in \new command: \with { \override VerticalAxisGroup.staff-affinity = #UP } const music = new ContextedMusic({head: new Command({cmd: "new", args: ["Lyrics"]}), body: lyric}); list.list.splice(upStaffPos + i, 0, music); }); } if (numberTrack) { const pos = upStaffPos + (dotTracks ? 1 : 0); const measureTicks = layoutMusic.musicTracks[0].block.measureTicks; const lyric = createPianoNumberTrack({durationMagnitude, subdivider, measureTicks, trackTicks, colored}); const music = new ContextedMusic({head: new Command({cmd: "new", args: ["Lyrics"]}), body: lyric}); list.list.splice(pos, 0, music); } interpreter.addIncludeFile("rhythmSymbols.ly"); interpreter.appendReservedVariables([ "dotB", "dotW", "lyrRed", "lyrGreen", "lyrYellow", "lyrGray", ]); };