Spaces:
Sleeping
Sleeping
| 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<number>, | |
| 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<number>[], | |
| 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<number>[] = 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", | |
| ]); | |
| }; | |