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", | |
]); | |
}; | |