Skip to main content

music21_rs/
abc.rs

1//! Small ABC notation helpers.
2
3use std::fmt;
4
5use crate::{Error, IntegerType, Pitch, Result};
6
7/// A clef choice for a compact ABC excerpt.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum AbcClef {
10    /// Treble clef.
11    Treble,
12    /// Bass clef.
13    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
25/// Returns an ABC note token for a pitch.
26///
27/// Octave-less pitches are written in octave 4, matching the crate's default
28/// pitch-space behavior.
29pub 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
66/// Chooses a compact treble or bass clef for the supplied pitches.
67///
68/// The heuristic favors bass clef when the average MIDI number is below middle
69/// C or any pitch is below C3.
70pub 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
87/// Returns an ABC chord token for the supplied pitches.
88pub 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
97/// Returns a complete one-bar ABC document for a chord.
98pub 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
106/// Returns a complete two-bar ABC document showing one chord resolving to another.
107pub 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
120/// Returns an ABC duration suffix for a rational note length.
121pub 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
145/// Returns an ABC voice body for one polyrhythm component.
146pub 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
184/// Returns a complete percussion ABC document for a polyrhythm.
185pub 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}