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