Make LilyPond template dynamic - title and staves generated from parameters

This commit is contained in:
Michael Winter 2026-03-23 19:54:50 +01:00
parent 306de8b5c2
commit 6bf97e5abe
2 changed files with 146 additions and 17 deletions

112
lilypond/score_template.ly Normal file
View file

@ -0,0 +1,112 @@
\version "2.24.1"
\paper {
#(set-paper-size "a4" 'portrait)
top-margin = 1 \cm
bottom-margin = 1 \cm
left-margin = 2 \cm
ragged-bottom = ##t
top-system-spacing =
#'((basic-distance . 15 )
(minimum-distance . 15 )
(padding . 0 )
(stretchability . 0))
system-system-spacing =
#'((basic-distance . 30 )
(minimum-distance . 30 )
(padding . 0 )
(stretchability . 0))
last-bottom-spacing =
#'((basic-distance . 10 )
(minimum-distance . 10 )
(padding . 0 )
(stretchability . 0))
%systems-per-page = 4
first-page-number = 1
print-first-page-number = ##t
print-page-number = ##t
oddHeaderMarkup = \markup { \fill-line { \line { \unless \on-first-page {\pad-markup #2 { \concat {\italic {"{NAME}"}}}}}}}
evenHeaderMarkup = \markup { \fill-line { \line { \unless \on-first-page {\pad-markup #2 { \concat {\italic {"{NAME}"}}}}}}}
oddFooterMarkup = \markup { \fill-line {
\concat {
"-"
\fontsize #1.5
\fromproperty #'page:page-number-string
"-"}}}
evenFooterMarkup = \markup { \fill-line {
\concat {
"-"
\fontsize #1.5
\fromproperty #'page:page-number-string
"-"}}}
}
\header {
title = \markup { \italic {"{NAME}"}}
composer = \markup \right-column {"michael winter" "(cdmx and schloss solitude; 2024)"}
poet = ""
tagline = ""
}
#(set-global-staff-size 11)
\layout {
indent = 0.0\cm
line-width = 17.5\cm
ragged-last = ##f
ragged-right = ##f
\context {
\Score
\override BarNumber.stencil = #(make-stencil-circler 0.1 0.25 ly:text-interface::print)
\override Stem.stemlet-length = #0.75
%proportionalNotationDuration = #(ly:make-moment 1/16)
\remove "Separating_line_group_engraver"
\override RehearsalMark.self-alignment-X = #-1
\override RehearsalMark.Y-offset = #10
\override RehearsalMark.X-offset = #-8
%\override RehearsalMark.outside-staff-priority = #0
\override SpacingSpanner.base-shortest-duration = #(ly:make-moment 1/32)
%\override Stem.stencil = ##f
%\override BarLine.stencil = ##f
}
\context {
\Staff
\override VerticalAxisGroup.staff-staff-spacing =
#'((basic-distance . 20 )
(minimum-distance . 20 )
(padding . 0 )
(stretchability . 0))
\override VerticalAxisGroup.default-staff-staff-spacing =
#'((basic-distance . 20 )
(minimum-distance . 20 )
(padding . 0 )
(stretchability . 0))
\override TextScript.staff-padding = #2
%\override TextScript.self-alignment-X = #0
}
\context {
\StaffGroup
\name "SemiStaffGroup"
\consists "Span_bar_engraver"
\override SpanBar.stencil =
#(lambda (grob)
(if (string=? (ly:grob-property grob 'glyph-name) "|")
(set! (ly:grob-property grob 'glyph-name) ""))
(ly:span-bar::print grob))
}
\context {
\Score
\accepts SemiStaffGroup
}
}
{STAVES}

View file

@ -469,24 +469,40 @@ def output_chords_to_music_data(chords, fundamental=55, chord_duration=4):
return music_data
def generate_score(name, template_path, output_dir="lilypond"):
def generate_score(name, num_voices, output_dir="lilypond"):
"""Generate full score .ly file from template.
Args:
name: Name for the output
template_path: Path to score template
name: Name for the output (used as title)
num_voices: Number of voices/staves to generate
output_dir: Base output directory
"""
template = Path(template_path)
if not template.exists():
template_path = Path(output_dir) / name / "score_template.ly"
if not template_path.exists():
print(f"Error: Template not found: {template_path}")
return False
score_path = Path(output_dir) / name / f"{name}.ly"
score_text = template.read_text()
score_text = template_path.read_text()
score_text = score_text.replace("{NAME}", name)
score_text = score_text.replace("{COMPOSER}", "Michael Winter")
voice_names = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII"]
staves = ""
for i in range(num_voices):
v_name = voice_names[i]
staves += f'''
\\new Staff = "{v_name}" \\with {{
instrumentName = "{v_name}"
shortInstrumentName = "{v_name}"
midiInstrument = #"clarinet"
}}
{{
\\include "includes/part_{v_name}.ly"
}}
'''
score_text = score_text.replace("{STAVES}", staves)
with open(score_path, "w") as f:
f.write(score_text)
@ -550,7 +566,6 @@ def transcribe(
chords,
name,
fundamental=55,
template_path="lilypond/score_template.ly",
output_dir="lilypond",
generate_pdf_flag=True,
):
@ -560,22 +575,30 @@ def transcribe(
chords: Chord data (list from output_chords.json or music_data format)
name: Name for the output
fundamental: Fundamental frequency in Hz
template_path: Path to score template
output_dir: Base output directory
generate_pdf_flag: Whether to generate PDF
Returns:
Dictionary with paths to generated files
"""
import shutil
if isinstance(chords[0][0], dict):
music_data = output_chords_to_music_data(chords, fundamental)
else:
music_data = chords
output_path = Path(output_dir) / name
output_path.mkdir(parents=True, exist_ok=True)
template_source = Path(__file__).parent.parent / "lilypond" / "score_template.ly"
if template_source.exists():
shutil.copy(template_source, output_path / "score_template.ly")
generate_parts(music_data, name, output_dir)
if template_path and Path(template_path).exists():
generate_score(name, template_path, output_dir)
num_voices = len(music_data)
generate_score(name, num_voices, output_dir)
result = {
"parts_dir": str(Path(output_dir) / name / "includes"),
@ -606,11 +629,6 @@ def main():
parser.add_argument(
"--fundamental", type=float, default=55, help="Fundamental frequency in Hz"
)
parser.add_argument(
"--template",
default="lilypond/score_template.ly",
help="LilyPond template path",
)
parser.add_argument(
"--lilypond-dir", default="lilypond", help="Base LilyPond output directory"
)
@ -636,7 +654,6 @@ def main():
chords,
args.name,
fundamental=args.fundamental,
template_path=args.template,
output_dir=args.lilypond_dir,
generate_pdf_flag=not args.no_pdf,
)