1use std::fmt;
4
5use crate::{Error, IntegerType, Pitch, Result};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum AbcClef {
10 Treble,
12 Bass,
14}
15
16impl fmt::Display for AbcClef {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Treble => f.write_str("treble"),
20 Self::Bass => f.write_str("bass"),
21 }
22 }
23}
24
25pub fn abc_note(pitch: &Pitch) -> Result<String> {
30 let name = pitch.name();
31 let mut chars = name.chars();
32 let step = chars
33 .next()
34 .ok_or_else(|| Error::Pitch("cannot write empty pitch name as ABC".to_string()))?;
35 if !matches!(step, 'A'..='G') {
36 return Err(Error::Pitch(format!(
37 "cannot write pitch step {step:?} as ABC"
38 )));
39 }
40
41 let accidental = chars
42 .map(|modifier| match modifier {
43 '#' => Ok('^'),
44 '-' => Ok('_'),
45 _ => Err(Error::Pitch(format!(
46 "cannot write accidental modifier {modifier:?} as ABC"
47 ))),
48 })
49 .collect::<Result<String>>()?;
50 let octave = pitch.octave().unwrap_or(4);
51
52 if octave >= 5 {
53 Ok(format!(
54 "{accidental}{}{}",
55 step.to_ascii_lowercase(),
56 "'".repeat((octave - 5) as usize)
57 ))
58 } else {
59 Ok(format!(
60 "{accidental}{step}{}",
61 ",".repeat((4 - octave).max(0) as usize)
62 ))
63 }
64}
65
66pub fn abc_clef_for_pitches(pitches: &[Pitch]) -> AbcClef {
71 if pitches.is_empty() {
72 return AbcClef::Treble;
73 }
74
75 let midi_values = pitches.iter().map(Pitch::midi).collect::<Vec<_>>();
76 let total = midi_values.iter().sum::<IntegerType>();
77 let average = total as f64 / midi_values.len() as f64;
78 let lowest = midi_values.iter().min().copied().unwrap_or(60);
79
80 if average < 60.0 || lowest < 48 {
81 AbcClef::Bass
82 } else {
83 AbcClef::Treble
84 }
85}
86
87pub fn abc_chord(pitches: &[Pitch]) -> Result<String> {
89 let notes = pitches.iter().map(abc_note).collect::<Result<Vec<_>>>()?;
90 if notes.is_empty() {
91 Ok("z4".to_string())
92 } else {
93 Ok(format!("[{}]4", notes.join("")))
94 }
95}
96
97pub fn abc_chord_document(pitches: &[Pitch]) -> Result<String> {
99 Ok(format!(
100 "X:1\nL:1/4\nM:4/4\nK:C clef={}\n{} |]\n",
101 abc_clef_for_pitches(pitches),
102 abc_chord(pitches)?
103 ))
104}
105
106pub fn abc_chord_resolution_document(source: &[Pitch], target: &[Pitch]) -> Result<String> {
108 let mut combined = Vec::with_capacity(source.len() + target.len());
109 combined.extend_from_slice(source);
110 combined.extend_from_slice(target);
111
112 Ok(format!(
113 "X:1\nL:1/4\nM:4/4\nK:C clef={}\n{} | {} |]\n",
114 abc_clef_for_pitches(&combined),
115 abc_chord(source)?,
116 abc_chord(target)?
117 ))
118}
119
120pub fn abc_duration(numerator: u32, denominator: u32) -> Result<String> {
122 if denominator == 0 {
123 return Err(Error::Polyrhythm(
124 "ABC duration denominator cannot be zero".to_string(),
125 ));
126 }
127
128 let divisor = gcd(numerator, denominator);
129 let top = numerator / divisor;
130 let bottom = denominator / divisor;
131
132 if bottom == 1 {
133 if top == 1 {
134 Ok(String::new())
135 } else {
136 Ok(top.to_string())
137 }
138 } else if top == 1 {
139 Ok(format!("/{bottom}"))
140 } else {
141 Ok(format!("{top}/{bottom}"))
142 }
143}
144
145pub fn abc_polyrhythm_voice(component: u32, base: u32) -> Result<String> {
147 if component == 0 || base == 0 {
148 return Err(Error::Polyrhythm(
149 "ABC polyrhythm components must be positive".to_string(),
150 ));
151 }
152
153 if component == base {
154 return Ok(std::iter::repeat_n("B", component as usize)
155 .collect::<Vec<_>>()
156 .join(" "));
157 }
158
159 if component == 1 {
160 return Ok(format!("B{base}"));
161 }
162
163 if component <= 9 {
164 let notes = std::iter::repeat_n("B", component as usize)
165 .collect::<Vec<_>>()
166 .join(" ");
167 return Ok(format!("({component}:{base}:{component}{notes}"));
168 }
169
170 let duration = abc_duration(base, component)?;
171 Ok((0..component)
172 .map(|index| {
173 let label = if index == 0 {
174 format!("\"^{component}:{base}\"")
175 } else {
176 String::new()
177 };
178 format!("{label}B{duration}")
179 })
180 .collect::<Vec<_>>()
181 .join(" "))
182}
183
184pub fn abc_polyrhythm_document(components: &[u32], base: u32) -> Result<String> {
186 if components.is_empty() {
187 return Err(Error::Polyrhythm(
188 "ABC polyrhythm document requires at least one component".to_string(),
189 ));
190 }
191
192 let mut lines = vec![
193 "X:1".to_string(),
194 "L:1/4".to_string(),
195 format!("M:{base}/4"),
196 "K:C clef=perc style=x".to_string(),
197 ];
198
199 for (index, component) in components.iter().enumerate() {
200 lines.push(format!(
201 "V:{} name=\"{}\" clef=perc style=x",
202 index + 1,
203 component
204 ));
205 lines.push(format!("{} |]", abc_polyrhythm_voice(*component, base)?));
206 }
207
208 Ok(format!("{}\n", lines.join("\n")))
209}
210
211fn gcd(mut left: u32, mut right: u32) -> u32 {
212 while right != 0 {
213 (left, right) = (right, left % right);
214 }
215 left
216}
217
218#[cfg(test)]
219mod tests {
220 use super::{
221 AbcClef, abc_chord, abc_chord_document, abc_chord_resolution_document, abc_duration,
222 abc_note, abc_polyrhythm_document, abc_polyrhythm_voice,
223 };
224 use crate::{Pitch, Result};
225
226 fn pitches(names: &[&str]) -> Result<Vec<Pitch>> {
227 names.iter().map(|name| Pitch::from_name(*name)).collect()
228 }
229
230 #[test]
231 fn writes_pitch_tokens_with_accidentals_and_octaves() -> Result<()> {
232 assert_eq!(abc_note(&Pitch::from_name("C4")?)?, "C");
233 assert_eq!(abc_note(&Pitch::from_name("C#4")?)?, "^C");
234 assert_eq!(abc_note(&Pitch::from_name("E-4")?)?, "_E");
235 assert_eq!(abc_note(&Pitch::from_name("C5")?)?, "c");
236 assert_eq!(abc_note(&Pitch::from_name("B3")?)?, "B,");
237 Ok(())
238 }
239
240 #[test]
241 fn builds_chord_tokens_and_documents() -> Result<()> {
242 let chord = pitches(&["C4", "E4", "G4"])?;
243
244 assert_eq!(abc_chord(&chord)?, "[CEG]4");
245 assert_eq!(
246 abc_chord_document(&chord)?,
247 "X:1\nL:1/4\nM:4/4\nK:C clef=treble\n[CEG]4 |]\n"
248 );
249 Ok(())
250 }
251
252 #[test]
253 fn chooses_bass_clef_for_low_material() -> Result<()> {
254 let source = pitches(&["G2", "B2", "D3", "F3"])?;
255 let target = pitches(&["C3", "E3", "G3"])?;
256
257 assert_eq!(super::abc_clef_for_pitches(&source), AbcClef::Bass);
258 assert_eq!(
259 abc_chord_resolution_document(&source, &target)?,
260 "X:1\nL:1/4\nM:4/4\nK:C clef=bass\n[G,,B,,D,F,]4 | [C,E,G,]4 |]\n"
261 );
262 Ok(())
263 }
264
265 #[test]
266 fn writes_polyrhythm_abc_documents() -> Result<()> {
267 assert_eq!(abc_duration(4, 10)?, "2/5");
268 assert_eq!(abc_polyrhythm_voice(3, 4)?, "(3:4:3B B B");
269 assert_eq!(
270 abc_polyrhythm_voice(11, 4)?,
271 "\"^11:4\"B4/11 B4/11 B4/11 B4/11 B4/11 B4/11 B4/11 B4/11 B4/11 B4/11 B4/11"
272 );
273 assert_eq!(
274 abc_polyrhythm_document(&[2, 3], 4)?,
275 "X:1\nL:1/4\nM:4/4\nK:C clef=perc style=x\nV:1 name=\"2\" clef=perc style=x\n(2:4:2B B |]\nV:2 name=\"3\" clef=perc style=x\n(3:4:3B B B |]\n"
276 );
277 Ok(())
278 }
279}