Skip to main content

sim_lib_music_notation/
export.rs

1use num_rational::Ratio;
2
3use sim_lib_music_core::{Chord, Counterpoint, Melody, MelodyItem, Music, Note, Rest, Score, Time};
4
5use crate::{
6    model::{NotationError, NotationReport},
7    spell::{encode_lily_pitch, lily_key_spec, spell_pitch_in_key},
8};
9
10/// Renders a score as a LilyPond `\score` block, returning the text with diagnostics.
11pub fn export_lilypond_report(score: &Score) -> Result<NotationReport<String>, NotationError> {
12    let mut lines = vec!["\\score {".to_owned()];
13    lines.push(format!("  \\tempo 4 = {}", score.tempo_bpm));
14    if let Some((tonic, mode, _)) = lily_key_spec(score.key.as_deref())? {
15        lines.push(format!("  \\key {tonic} \\{mode}"));
16    }
17    lines.push(format!(
18        "  \\time {}/{}",
19        score.time_signature.0, score.time_signature.1
20    ));
21    lines.push(indent_block(
22        &render_music(&score.body, score.key.as_deref())?,
23        2,
24    ));
25    lines.push("}".to_owned());
26    Ok(NotationReport {
27        value: lines.join("\n"),
28        diagnostics: Vec::new(),
29        identities: Vec::new(),
30        losses: Vec::new(),
31    })
32}
33
34/// Renders a score as a LilyPond `\score` block, discarding diagnostics.
35pub fn export_lilypond(score: &Score) -> Result<String, NotationError> {
36    Ok(export_lilypond_report(score)?.value)
37}
38
39/// Renders a melody as a LilyPond note sequence, spelling pitches in `key`.
40pub fn export_melody_lilypond(melody: &Melody, key: Option<&str>) -> Result<String, NotationError> {
41    render_melody(melody, key)
42}
43
44/// Renders counterpoint as parallel LilyPond voices, spelling pitches in `key`.
45pub fn export_counterpoint_lilypond(
46    counterpoint: &Counterpoint,
47    key: Option<&str>,
48) -> Result<String, NotationError> {
49    render_counterpoint(counterpoint, key)
50}
51
52/// Renders a chord progression as a LilyPond chord sequence, spelling pitches in `key`.
53pub fn export_progression_lilypond(
54    progression: &sim_lib_music_core::Progression,
55    key: Option<&str>,
56) -> Result<String, NotationError> {
57    render_progression(progression, key)
58}
59
60fn render_music(value: &Music, key: Option<&str>) -> Result<String, NotationError> {
61    match value {
62        Music::Note(note) => render_melody(
63            &Melody {
64                items: vec![MelodyItem::Note(note.clone())],
65            },
66            key,
67        ),
68        Music::Rest(rest) => render_melody(
69            &Melody {
70                items: vec![MelodyItem::Rest(rest.clone())],
71            },
72            key,
73        ),
74        Music::Chord(chord) => render_progression(
75            &sim_lib_music_core::Progression {
76                key: key.map(str::to_owned),
77                chords: vec![chord.clone()],
78            },
79            key,
80        ),
81        Music::Melody(melody) => render_melody(melody, key),
82        Music::Progression(progression) => render_progression(progression, key),
83        Music::Counterpoint(counterpoint) => render_counterpoint(counterpoint, key),
84        _ => Err(NotationError::UnsupportedMusicObject(match value {
85            Music::Par(_) => "Par",
86            Music::Seq(_) => "Seq",
87            Music::PianoRoll(_) => "PianoRoll",
88            Music::MidiTrack(_) => "MidiTrack",
89            Music::MidiFile(_) => "MidiFile",
90            _ => "Unknown",
91        })),
92    }
93}
94
95fn render_melody(melody: &Melody, key: Option<&str>) -> Result<String, NotationError> {
96    let mut tokens = Vec::with_capacity(melody.items.len());
97    for item in &melody.items {
98        match item {
99            MelodyItem::Note(note) => tokens.push(render_note(note, key)?),
100            MelodyItem::Rest(rest) => tokens.push(render_rest(rest)?),
101        }
102    }
103    Ok(format!("{{ {} }}", tokens.join(" ")))
104}
105
106fn render_progression(
107    progression: &sim_lib_music_core::Progression,
108    key: Option<&str>,
109) -> Result<String, NotationError> {
110    let key = progression.key.as_deref().or(key);
111    let mut tokens = Vec::with_capacity(progression.chords.len());
112    for chord in &progression.chords {
113        tokens.push(render_chord(chord, key)?);
114    }
115    Ok(format!("{{ {} }}", tokens.join(" ")))
116}
117
118fn render_counterpoint(
119    counterpoint: &Counterpoint,
120    key: Option<&str>,
121) -> Result<String, NotationError> {
122    let mut voices = Vec::with_capacity(counterpoint.voices.len());
123    for (index, melody) in counterpoint.voices.iter().enumerate() {
124        let name = counterpoint
125            .voice_names
126            .get(index)
127            .cloned()
128            .unwrap_or_else(|| format!("Voice {}", index + 1));
129        voices.push(format!(
130            "\\new Voice = \"{}\" {}",
131            name.replace('"', "\\\""),
132            render_melody(melody, key)?,
133        ));
134    }
135    Ok(format!("<< {} >>", voices.join(" ")))
136}
137
138fn render_note(note: &Note, key: Option<&str>) -> Result<String, NotationError> {
139    let pitch = encode_lily_pitch(spell_pitch_in_key(note.pitch, key)?);
140    render_tied_segments(
141        duration_segments(note.duration)?,
142        |denom| format!("{pitch}{denom}"),
143        true,
144    )
145}
146
147fn render_rest(rest: &Rest) -> Result<String, NotationError> {
148    render_tied_segments(
149        duration_segments(rest.duration)?,
150        |denom| format!("r{denom}"),
151        false,
152    )
153}
154
155fn render_chord(chord: &Chord, key: Option<&str>) -> Result<String, NotationError> {
156    let pitches = chord
157        .pitches
158        .iter()
159        .map(|pitch| spell_pitch_in_key(*pitch, key).map(encode_lily_pitch))
160        .collect::<Result<Vec<_>, _>>()?
161        .join(" ");
162    render_tied_segments(
163        duration_segments(chord.duration)?,
164        |denom| format!("<{pitches}>{denom}"),
165        true,
166    )
167}
168
169fn render_tied_segments(
170    segments: Vec<Time>,
171    render: impl Fn(u64) -> String,
172    use_ties: bool,
173) -> Result<String, NotationError> {
174    let mut out = Vec::with_capacity(segments.len());
175    for segment in segments {
176        let denom = lily_duration_number(segment)?;
177        out.push(render(denom));
178    }
179    if use_ties {
180        Ok(out.join(" ~ "))
181    } else {
182        Ok(out.join(" "))
183    }
184}
185
186fn duration_segments(duration: Time) -> Result<Vec<Time>, NotationError> {
187    if duration <= Time::from_integer(0) {
188        return Err(NotationError::UnsupportedDuration(format_ratio(duration)));
189    }
190    let mut reduced = duration.denom().abs();
191    while reduced % 2 == 0 {
192        reduced /= 2;
193    }
194    if reduced != 1 {
195        return Err(NotationError::UnsupportedDuration(format_ratio(duration)));
196    }
197    let mut remaining = duration;
198    let mut segments = Vec::new();
199    while remaining > Time::from_integer(0) {
200        let segment = largest_dyadic_leq(remaining);
201        remaining -= segment;
202        segments.push(segment);
203    }
204    Ok(segments)
205}
206
207fn largest_dyadic_leq(limit: Time) -> Time {
208    let mut candidate = Time::from_integer(1);
209    while candidate > limit {
210        candidate /= Ratio::from_integer(2);
211    }
212    candidate
213}
214
215fn lily_duration_number(segment: Time) -> Result<u64, NotationError> {
216    if *segment.numer() != 1 {
217        return Err(NotationError::UnsupportedDuration(format_ratio(segment)));
218    }
219    u64::try_from(*segment.denom())
220        .map_err(|_| NotationError::UnsupportedDuration(format_ratio(segment)))
221}
222
223fn format_ratio(value: Time) -> String {
224    format!("{}/{}", value.numer(), value.denom())
225}
226
227fn indent_block(value: &str, spaces: usize) -> String {
228    let pad = " ".repeat(spaces);
229    value
230        .lines()
231        .map(|line| format!("{pad}{line}"))
232        .collect::<Vec<_>>()
233        .join("\n")
234}