Skip to main content

badterm_core/
music.rs

1//! ANSI music: the BASIC `PLAY` language that BBSes send between `ESC [ M`
2//! (or `ESC [ N`, `ESC [ |`) and Ctrl-N, turned into tones to play.
3//!
4//! Supported, as in GW-BASIC: notes `A`-`G` with `#`/`+` (sharp) or `-`
5//! (flat), a length and dots; `O` octave, `<` and `>`; `L` default length;
6//! `T` tempo; `P` pause; `N` note number; `MN`/`ML`/`MS` normal, legato and
7//! staccato. `MF`/`MB` (foreground/background) don't apply to a terminal
8//! and are ignored.
9
10/// One sound: a square wave at `hz` (0 is silence) for `ms` milliseconds.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct Tone {
13    pub hz: u32,
14    pub ms: u32,
15}
16
17/// Longest piece played from one string, so a runaway string can't keep
18/// the speaker going for long.
19const MAX_TOTAL_MS: u32 = 5 * 60 * 1000;
20
21#[derive(Clone, Copy)]
22enum Style {
23    Normal,
24    Legato,
25    Staccato,
26}
27
28/// Parse a music string. Anything not understood is skipped.
29pub fn parse(s: &[u8]) -> Vec<Tone> {
30    let mut p = Parser { s, i: 0 };
31    let (mut octave, mut length, mut tempo, mut style) = (4u32, 4u32, 120u32, Style::Normal);
32    let mut out = Vec::new();
33    let mut total = 0u32;
34    while let Some(c) = p.next() {
35        let tone_of = |key: Option<u32>, len: u32, dots: u32, style: Style, tempo: u32| {
36            // A whole note is four beats.
37            let mut ms = 240_000 / (tempo * len.max(1)) as u64;
38            let mut extra = ms;
39            for _ in 0..dots {
40                extra /= 2;
41                ms += extra;
42            }
43            let ms = ms as u32;
44            let Some(key) = key else { return vec![Tone { hz: 0, ms }] };
45            let on = match style {
46                Style::Normal => ms * 7 / 8,
47                Style::Legato => ms,
48                Style::Staccato => ms * 3 / 4,
49            };
50            let mut v = vec![Tone { hz: frequency(key), ms: on }];
51            if on < ms {
52                v.push(Tone { hz: 0, ms: ms - on });
53            }
54            v
55        };
56        let tones = match c.to_ascii_uppercase() {
57            n @ b'A'..=b'G' => {
58                let mut key = octave as i32 * 12 + semitone(n);
59                match p.peek() {
60                    Some(b'#' | b'+') => {
61                        p.i += 1;
62                        key += 1;
63                    }
64                    Some(b'-') => {
65                        p.i += 1;
66                        key -= 1;
67                    }
68                    _ => {}
69                }
70                let len = p.number().filter(|&n| (1..=64).contains(&n)).unwrap_or(length);
71                let dots = p.dots();
72                tone_of(Some(key.clamp(0, 83) as u32), len, dots, style, tempo)
73            }
74            b'N' => match p.number() {
75                Some(0) => tone_of(None, length, p.dots(), style, tempo),
76                Some(n @ 1..=84) => tone_of(Some(n - 1), length, p.dots(), style, tempo),
77                _ => Vec::new(),
78            },
79            b'P' => {
80                let len = p.number().filter(|&n| (1..=64).contains(&n)).unwrap_or(length);
81                tone_of(None, len, p.dots(), style, tempo)
82            }
83            b'O' => {
84                if let Some(n) = p.number().filter(|&n| n <= 6) {
85                    octave = n;
86                }
87                Vec::new()
88            }
89            b'>' => {
90                octave = (octave + 1).min(6);
91                Vec::new()
92            }
93            b'<' => {
94                octave = octave.saturating_sub(1);
95                Vec::new()
96            }
97            b'L' => {
98                if let Some(n) = p.number().filter(|&n| (1..=64).contains(&n)) {
99                    length = n;
100                }
101                Vec::new()
102            }
103            b'T' => {
104                if let Some(n) = p.number().filter(|&n| (32..=255).contains(&n)) {
105                    tempo = n;
106                }
107                Vec::new()
108            }
109            b'M' => {
110                match p.next().map(|c| c.to_ascii_uppercase()) {
111                    Some(b'N') => style = Style::Normal,
112                    Some(b'L') => style = Style::Legato,
113                    Some(b'S') => style = Style::Staccato,
114                    _ => {}
115                }
116                Vec::new()
117            }
118            _ => Vec::new(),
119        };
120        for t in tones {
121            total = total.saturating_add(t.ms);
122            if total > MAX_TOTAL_MS {
123                return out;
124            }
125            // Join consecutive rests.
126            match out.last_mut() {
127                Some(Tone { hz: 0, ms }) if t.hz == 0 => *ms += t.ms,
128                _ => out.push(t),
129            }
130        }
131    }
132    out
133}
134
135/// Semitones above C.
136fn semitone(note: u8) -> i32 {
137    match note {
138        b'C' => 0,
139        b'D' => 2,
140        b'E' => 4,
141        b'F' => 5,
142        b'G' => 7,
143        b'A' => 9,
144        _ => 11,
145    }
146}
147
148/// Frequency of key `n` (octave * 12 + semitone). In BASIC, octave 3 starts
149/// at middle C, so A in octave 3 (key 45) is 440 Hz.
150fn frequency(key: u32) -> u32 {
151    (440.0 * 2f64.powf((key as f64 - 45.0) / 12.0)).round() as u32
152}
153
154struct Parser<'a> {
155    s: &'a [u8],
156    i: usize,
157}
158
159impl Parser<'_> {
160    fn peek(&self) -> Option<u8> {
161        self.s.get(self.i).copied()
162    }
163
164    /// The next byte that isn't a space.
165    fn next(&mut self) -> Option<u8> {
166        while let Some(c) = self.peek() {
167            self.i += 1;
168            if !c.is_ascii_whitespace() {
169                return Some(c);
170            }
171        }
172        None
173    }
174
175    fn number(&mut self) -> Option<u32> {
176        let start = self.i;
177        while self.peek().is_some_and(|c| c.is_ascii_digit()) {
178            self.i += 1;
179        }
180        std::str::from_utf8(&self.s[start..self.i]).ok()?.parse().ok()
181    }
182
183    fn dots(&mut self) -> u32 {
184        let mut n = 0;
185        while self.peek() == Some(b'.') {
186            self.i += 1;
187            n += 1;
188        }
189        n
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    fn t(hz: u32, ms: u32) -> Tone {
198        Tone { hz, ms }
199    }
200
201    #[test]
202    fn notes_lengths_and_tempo() {
203        // At 120 bpm a quarter note is 500 ms; normal style sounds 7/8.
204        assert_eq!(parse(b"O3A"), [t(440, 437), t(0, 63)]);
205        assert_eq!(parse(b"ML O3 A8"), [t(440, 250)]);
206        assert_eq!(parse(b"MS T60 O3 A"), [t(440, 750), t(0, 250)]);
207        // Dotted: 1.5 times.
208        assert_eq!(parse(b"MLO3A4."), [t(440, 750)]);
209        // Middle C, sharps and flats, octave changes.
210        assert_eq!(parse(b"MLO3C"), [t(262, 500)]);
211        assert_eq!(parse(b"MLO3C#D-"), [t(277, 500), t(277, 500)]);
212        assert_eq!(parse(b"MLO3A>A<<A"), [t(440, 500), t(880, 500), t(220, 500)]);
213    }
214
215    #[test]
216    fn rests_numbers_and_junk() {
217        assert_eq!(parse(b"L8P4"), [t(0, 500)]);
218        // N46 is A in octave 3; N0 is a rest. Rests in a row merge.
219        assert_eq!(parse(b"MLN46N0P8"), [t(440, 500), t(0, 750)]);
220        // Lower case works; unknown letters and out-of-range values are ignored.
221        assert_eq!(parse(b"ml o3 a xyz O9 L99 T5"), [t(440, 500)]);
222        assert!(parse(b"MFMB").is_empty());
223    }
224
225    #[test]
226    fn long_strings_are_cut_off() {
227        let s = format!("T32L1{}", "C".repeat(1000));
228        let total: u32 = parse(s.as_bytes()).iter().map(|t| t.ms).sum();
229        assert!(total <= MAX_TOTAL_MS);
230    }
231}