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
10pub 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
34pub fn export_lilypond(score: &Score) -> Result<String, NotationError> {
36 Ok(export_lilypond_report(score)?.value)
37}
38
39pub fn export_melody_lilypond(melody: &Melody, key: Option<&str>) -> Result<String, NotationError> {
41 render_melody(melody, key)
42}
43
44pub fn export_counterpoint_lilypond(
46 counterpoint: &Counterpoint,
47 key: Option<&str>,
48) -> Result<String, NotationError> {
49 render_counterpoint(counterpoint, key)
50}
51
52pub 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}