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