Skip to main content

badterm_core/
audio.rs

1//! SyncTERM's audio APCs (`ESC _ SyncTERM:A;... ESC \`), as CTerm
2//! documents them: 256 slots of sound (16-bit stereo at 44.1 kHz) filled
3//! from files in the cache or sent inline (WAV, AIFF, VOC and Ogg Opus are
4//! decoded here), or synthesized; and 16 channels the BBS queues them on,
5//! with fades, loops, crossfades and volume. Synchronet's doors play their
6//! sound effects and music this way.
7//!
8//! The mixing is the front end's: queued sound goes out as [`AudioOp`]s.
9//! What the BBS asks about (is a channel playing?) is answered from the
10//! lengths of what was queued.
11
12use std::path::PathBuf;
13use std::sync::Arc;
14use std::time::{Duration, Instant};
15
16/// The sample rate of slots and channels.
17pub const RATE: u32 = 44_100;
18pub const SLOTS: usize = 256;
19pub const CHANNELS: usize = 16;
20/// The first channel the BBS may queue sound on; 0 is ANSI music, 1
21/// sound effects (RIP), which are the terminal's own.
22pub const FIRST_APC_CHANNEL: usize = 2;
23/// APC channels start at -12 dB.
24pub const BASE_DB: f32 = -12.0;
25/// Longer sounds are cut off (ten minutes).
26const MAX_FRAMES: usize = RATE as usize * 600;
27
28/// Interleaved stereo frames (left, right) at [`RATE`].
29pub type Pcm = Arc<[i16]>;
30
31/// What the front end's mixer is to do.
32#[derive(Clone, Debug, PartialEq)]
33pub enum AudioOp {
34    /// Play `pcm` on `channel` after what is queued there (or, with
35    /// `crossfade`, fade out what is playing while this fades in over
36    /// `fade_in` frames). `volume` is in dB, added to the channel's.
37    Queue {
38        channel: usize,
39        pcm: Pcm,
40        fade_in: usize,
41        fade_out: usize,
42        crossfade: bool,
43        looped: bool,
44        volume: (f32, f32),
45    },
46    /// Stop a channel: at once, or fading out over `fade_out` frames.
47    Flush { channel: usize, fade_out: usize },
48    /// Set a channel's level (dB) now, or over `ramp` frames.
49    Volume { channel: usize, left: Option<f32>, right: Option<f32>, ramp: usize },
50}
51
52/// When a channel will have played all that is queued.
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
54enum Busy {
55    #[default]
56    Idle,
57    Until(Instant),
58    Looping,
59}
60
61pub struct Audio {
62    slots: Vec<Option<Pcm>>,
63    busy: [Busy; CHANNELS],
64    /// Channels to report on when they stop (`Update`).
65    notify: [bool; CHANNELS],
66}
67
68impl Default for Audio {
69    fn default() -> Audio {
70        Audio { slots: vec![None; SLOTS], busy: [Busy::Idle; CHANNELS], notify: [false; CHANNELS] }
71    }
72}
73
74/// A duration: milliseconds (`500`, `500ms`), frames (`22050f`), or with
75/// `hz`, whole periods of it (`3p`). Anything else is 0.
76fn duration(s: &str, hz: Option<f64>) -> usize {
77    let digits = s.bytes().take_while(u8::is_ascii_digit).count();
78    let Ok(n) = s[..digits].parse::<u64>() else { return 0 };
79    match &s[digits..] {
80        "" | "ms" => (n * RATE as u64 / 1000) as usize,
81        "f" => n as usize,
82        "p" => hz.filter(|&f| f > 0.0).map_or(0, |f| (n as f64 * RATE as f64 / f + 0.5) as usize),
83        _ => 0,
84    }
85}
86
87/// A volume in dB: `-6dB`, or a percentage (0 is -60 dB).
88fn volume(s: &str) -> f32 {
89    let lower = s.to_ascii_lowercase();
90    if let Some(db) = lower.strip_suffix("db") {
91        return db.trim().parse().unwrap_or(0.0);
92    }
93    match s.trim().parse::<f32>() {
94        Ok(v) if v <= 0.0 => -60.0,
95        Ok(v) => 20.0 * (v.min(100.0) / 100.0).log10(),
96        Err(_) => 0.0,
97    }
98}
99
100/// `KEY=value` among an APC's fields.
101fn field<'a>(fields: &[&'a str], key: &str) -> Option<&'a str> {
102    fields.iter().find_map(|f| f.strip_prefix(key)?.strip_prefix('='))
103}
104
105fn number(fields: &[&str], key: &str) -> Option<usize> {
106    field(fields, key)?.trim().parse().ok()
107}
108
109impl Audio {
110    /// An `A;...` APC: `fields` after `SyncTERM:A;`. Sound to play goes
111    /// in `ops`; `hold` is set for `Wait` to how long to stop reading.
112    pub fn apc(&mut self, fields: &[&str], cache: Option<&PathBuf>, ops: &mut Vec<AudioOp>) -> Option<Instant> {
113        let (&verb, rest) = fields.split_first()?;
114        let slot = number(rest, "S").filter(|&s| s < SLOTS);
115        let channel = number(rest, "C").filter(|&c| c < CHANNELS);
116        match verb {
117            "Load" | "LoadBlob" => {
118                // The fields after the S=: a file name, or the data.
119                let data = rest.iter().rfind(|f| !f.starts_with("S="))?;
120                let bytes = if verb == "Load" {
121                    let name = crate::syncterm::safe_name(data)?;
122                    std::fs::read(cache?.join(name)).ok()?
123                } else {
124                    crate::syncterm::decode_b64(data)?
125                };
126                match decode(&bytes) {
127                    Some(pcm) => self.slots[slot?] = Some(pcm),
128                    None => log::info!("audio {verb}: can't decode it"),
129                }
130            }
131            "Synth" => {
132                let shape = field(rest, "W").and_then(Shape::parse)?;
133                let hz: f64 = field(rest, "F").and_then(|f| f.trim().parse().ok()).unwrap_or(0.0);
134                let frames = duration(field(rest, "T")?, Some(hz)).min(MAX_FRAMES);
135                if frames > 0 {
136                    self.slots[slot?] = Some(synth(shape, hz, frames).into());
137                }
138            }
139            "Copy" => {
140                let to = number(rest, "D").filter(|&d| d < SLOTS)?;
141                self.slots[to] = self.slots[slot?].clone();
142            }
143            "Queue" => {
144                let channel = channel.filter(|&c| c >= FIRST_APC_CHANNEL)?;
145                let pcm = self.slots[slot?].take().filter(|p| !p.is_empty())?;
146                let both = field(rest, "V").map(volume).unwrap_or(0.0);
147                let left = field(rest, "VL").map(volume).unwrap_or(both);
148                let right = field(rest, "VR").map(volume).unwrap_or(both);
149                let fade_in = field(rest, "I").map_or(0, |d| duration(d, None));
150                let crossfade = rest.contains(&"X");
151                let looped = rest.contains(&"L");
152                let now = Instant::now();
153                let length = Duration::from_secs_f64((pcm.len() / 2) as f64 / RATE as f64);
154                self.busy[channel] = match (looped, self.busy[channel]) {
155                    (true, _) => Busy::Looping,
156                    (false, Busy::Until(t)) if !crossfade && t > now => Busy::Until(t + length),
157                    _ => Busy::Until(now + length),
158                };
159                ops.push(AudioOp::Queue {
160                    channel,
161                    pcm,
162                    fade_in,
163                    fade_out: field(rest, "O").map_or(0, |d| duration(d, None)),
164                    crossfade,
165                    looped,
166                    volume: (left, right),
167                });
168            }
169            "Flush" => {
170                let channel = channel?;
171                let fade_out = field(rest, "O").map_or(0, |d| duration(d, None));
172                self.busy[channel] = match fade_out {
173                    0 => Busy::Idle,
174                    f => Busy::Until(Instant::now() + Duration::from_secs_f64(f as f64 / RATE as f64)),
175                };
176                ops.push(AudioOp::Flush { channel, fade_out });
177            }
178            "Volume" => {
179                let channel = channel?;
180                let both = field(rest, "V").map(volume);
181                let left = field(rest, "VL").map(volume).or(both);
182                let right = field(rest, "VR").map(volume).or(both);
183                if left.is_some() || right.is_some() {
184                    let ramp = field(rest, "T").map_or(0, |d| duration(d, None));
185                    ops.push(AudioOp::Volume { channel, left, right, ramp });
186                }
187            }
188            "Wait" => {
189                if let Busy::Until(t) = self.busy[channel?] {
190                    return Some(t);
191                }
192            }
193            "Update" => self.notify[channel?] = true,
194            _ => log::debug!("unhandled audio APC {verb}"),
195        }
196        None
197    }
198
199    /// ANSI music started on channel 0, playing for `length`.
200    pub fn music(&mut self, length: Duration) {
201        let now = Instant::now();
202        self.busy[0] = match self.busy[0] {
203            Busy::Until(t) if t > now => Busy::Until(t + length),
204            _ => Busy::Until(now + length),
205        };
206    }
207
208    fn playing(&self, channel: usize, now: Instant) -> bool {
209        match self.busy[channel] {
210            Busy::Idle => false,
211            Busy::Until(t) => t > now,
212            Busy::Looping => true,
213        }
214    }
215
216    /// The audio state report (`CSI = 7 [; channel] n`).
217    pub fn report(&self, channel: Option<usize>) -> String {
218        let now = Instant::now();
219        match channel {
220            Some(c) => format!("\x1b[=7;{c};{}n", (c < CHANNELS && self.playing(c, now)) as u8),
221            None => {
222                let pairs: String =
223                    (0..CHANNELS).filter(|&c| self.playing(c, now)).map(|c| format!(";{c};1")).collect();
224                format!("\x1b[=7{pairs}n")
225            }
226        }
227    }
228
229    /// Reports for channels that were asked about (`Update`) and have
230    /// stopped since.
231    pub fn poll(&mut self, now: Instant, replies: &mut Vec<u8>) {
232        for c in 0..CHANNELS {
233            if self.notify[c] && !self.playing(c, now) {
234                self.notify[c] = false;
235                replies.extend_from_slice(format!("\x1b[=7;{c};0n").as_bytes());
236            }
237        }
238    }
239
240    /// When the next report may be due.
241    pub fn next_due(&self) -> Option<Instant> {
242        (0..CHANNELS)
243            .filter(|&c| self.notify[c])
244            .filter_map(|c| match self.busy[c] {
245                Busy::Until(t) => Some(t),
246                _ => None,
247            })
248            .min()
249    }
250}
251
252/// Whether a libsndfile format (major `pm`, subtype `ps`) is one BadTerm
253/// decodes.
254pub fn format_supported(pm: u32, ps: u32) -> bool {
255    const WAV: u32 = 1;
256    const AIFF: u32 = 2;
257    const VOC: u32 = 8;
258    const WAVEX: u32 = 19;
259    const OGG: u32 = 32;
260    const OPUS: u32 = 100;
261    match pm {
262        WAV | WAVEX => matches!(ps, 2..=7),
263        AIFF => matches!(ps, 1..=4 | 6),
264        VOC => matches!(ps, 2 | 5),
265        OGG => ps == OPUS,
266        _ => false,
267    }
268}
269
270/// Parse a number as the doc allows: decimal or `0x` hex.
271pub fn parse_id(s: &str) -> u32 {
272    let s = s.trim();
273    match s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
274        Some(h) => u32::from_str_radix(h, 16).unwrap_or(0),
275        None => s.parse().unwrap_or(0),
276    }
277}
278
279/// SyncTERM's tone shapes.
280#[derive(Clone, Copy, Debug, PartialEq, Eq)]
281enum Shape {
282    Sine,
283    Saw,
284    Square,
285    SineHarm,
286    SineSaw,
287    SineSawChord,
288    SineSawHarm,
289    Silence,
290}
291
292impl Shape {
293    fn parse(s: &str) -> Option<Shape> {
294        Some(match s.trim() {
295            "SIN" => Shape::Sine,
296            "SAW" => Shape::Saw,
297            "SQ" => Shape::Square,
298            "SINE_HARM" => Shape::SineHarm,
299            "SINE_SAW" => Shape::SineSaw,
300            "SINE_SAW_CHORD" => Shape::SineSawChord,
301            "SINE_SAW_HARM" => Shape::SineSawHarm,
302            "SILENCE" => Shape::Silence,
303            _ => return None,
304        })
305    }
306}
307
308/// A tone, as SyncTERM's `xptone_makewave` makes it: the "noisy" shapes
309/// keep that code's 8-bit arithmetic (wrap-arounds and all), so they
310/// sound the same; the ends are softened over a few zero crossings so
311/// they don't click.
312fn synth(shape: Shape, hz: f64, frames: usize) -> Vec<i16> {
313    if hz <= 0.0 || shape == Shape::Silence {
314        return vec![0; frames * 2];
315    }
316    use std::f64::consts::{PI, TAU};
317    let inc = TAU * hz / RATE as f64;
318    // C's double to unsigned char, as compilers do it: through an int.
319    let u8c = |v: f64| (v as i64) as u8;
320    let mut mono: Vec<i16> = (0..frames)
321        .map(|i| {
322            let i = i as f64;
323            let pos = (inc * i) % TAU;
324            let noisy = |u: u8| (((u as i32) - 128) << 8) as i16;
325            match shape {
326                Shape::Sine => (pos.sin() * 32767.0) as i16,
327                Shape::Saw => (32767.0 - (pos / TAU) * 65535.0) as i16,
328                Shape::Square => {
329                    if pos < PI {
330                        32767
331                    } else {
332                        -32768
333                    }
334                }
335                Shape::SineHarm => noisy(u8c((inc * 3.0 * i).sin() * 16.0)),
336                Shape::SineSaw => noisy(u8c(((pos.sin() * 127.0 + 128.0) + ((TAU - pos) * 40.5)) / 2.0)),
337                Shape::SineSawChord | Shape::SineSawHarm => {
338                    let (a, b) = if shape == Shape::SineSawChord { (0.5, 1.0 / 3.0) } else { (2.0, 3.0) };
339                    let mut u = u8c(((pos.sin() * 64.0 + 128.0) + ((TAU - pos) * 6.2)) / 2.0);
340                    u = u8c(u as f64 + (inc * a * i).sin() * 24.0);
341                    u = u8c(u as f64 + (inc * b * i).sin() * 16.0);
342                    noisy(u)
343                }
344                Shape::Silence => 0,
345            }
346        })
347        .collect();
348    soften(&mut mono, hz);
349    mono.iter().flat_map(|&v| [v, v]).collect()
350}
351
352/// `xptone_makewave`'s click prevention: drop the last partial wave and
353/// ramp the first and last few zero crossings.
354fn soften(mono: &mut [i16], hz: f64) {
355    let n = mono.len();
356    if n < 4 {
357        return;
358    }
359    let mid = n / 2;
360    let end_high = mono[n - 1] > 0;
361    if mono[n - 1] != 0 {
362        let mut i = n - 1;
363        while i > mid {
364            if end_high && mono[i] < 0 || !end_high && mono[i] > 0 {
365                break;
366            }
367            mono[i] = 0;
368            i -= 1;
369        }
370    }
371    let crossings_wanted = (hz / 100.0) as i32;
372    if crossings_wanted == 0 {
373        return;
374    }
375    let mut crossings = 0;
376    for i in 0..n - 1 {
377        if ((mono[i] < 0 && mono[i + 1] >= 0) || (mono[i] > 0 && mono[i + 1] <= 0)) && i > 2 {
378            crossings += 1;
379            if crossings >= crossings_wanted {
380                break;
381            }
382        }
383        let d = crossings_wanted - crossings;
384        mono[i] = (mono[i] as i32 / (d * d)) as i16;
385    }
386    crossings = 0;
387    for i in (1..n).rev() {
388        if ((mono[i] < 0 && mono[i - 1] >= 0) || (mono[i] > 0 && mono[i - 1] <= 0)) && i > 2 {
389            crossings += 1;
390            if crossings >= crossings_wanted {
391                break;
392            }
393        }
394        let d = crossings_wanted - crossings;
395        mono[i] = (mono[i] as i32 / (d * d)) as i16;
396    }
397}
398
399/// A sound file, as stereo frames at [`RATE`].
400pub fn decode(data: &[u8]) -> Option<Pcm> {
401    let (rate, channels, samples) = if data.starts_with(b"RIFF") && data.get(8..12) == Some(b"WAVE") {
402        decode_wav(data)?
403    } else if data.starts_with(b"FORM") && matches!(data.get(8..12), Some(b"AIFF" | b"AIFC")) {
404        decode_aiff(data)?
405    } else if data.starts_with(b"Creative Voice File\x1a") {
406        decode_voc(data)?
407    } else if data.starts_with(b"OggS") {
408        decode_opus(data)?
409    } else {
410        return None;
411    };
412    Some(to_stereo(rate, channels, &samples).into())
413}
414
415/// Samples in -1..1 (interleaved, `channels` of them) at `rate`, as
416/// stereo 16-bit at [`RATE`] (linear interpolation).
417fn to_stereo(rate: u32, channels: usize, samples: &[f32]) -> Vec<i16> {
418    let channels = channels.max(1);
419    let frames = samples.len() / channels;
420    let frame = |i: usize| -> (f32, f32) {
421        let f = &samples[i.min(frames - 1) * channels..][..channels];
422        (f[0], if channels > 1 { f[1] } else { f[0] })
423    };
424    if frames == 0 || rate == 0 {
425        return Vec::new();
426    }
427    let out_frames = ((frames as u64 * RATE as u64 / rate as u64) as usize).min(MAX_FRAMES);
428    let step = rate as f64 / RATE as f64;
429    let mut out = Vec::with_capacity(out_frames * 2);
430    for i in 0..out_frames {
431        let pos = i as f64 * step;
432        let (j, t) = (pos as usize, (pos.fract()) as f32);
433        let ((l0, r0), (l1, r1)) = (frame(j), frame(j + 1));
434        let s = |a: f32, b: f32| ((a + (b - a) * t) * 32768.0).round().clamp(-32768.0, 32767.0) as i16;
435        out.push(s(l0, l1));
436        out.push(s(r0, r1));
437    }
438    out
439}
440
441fn u16le(d: &[u8], at: usize) -> Option<u16> {
442    Some(u16::from_le_bytes(d.get(at..at + 2)?.try_into().ok()?))
443}
444
445fn u32le(d: &[u8], at: usize) -> Option<u32> {
446    Some(u32::from_le_bytes(d.get(at..at + 4)?.try_into().ok()?))
447}
448
449fn u16be(d: &[u8], at: usize) -> Option<u16> {
450    Some(u16::from_be_bytes(d.get(at..at + 2)?.try_into().ok()?))
451}
452
453fn u32be(d: &[u8], at: usize) -> Option<u32> {
454    Some(u32::from_be_bytes(d.get(at..at + 4)?.try_into().ok()?))
455}
456
457/// Integer or float PCM samples to -1..1. `bits` 8 is unsigned (WAV,
458/// VOC) when `unsigned8`.
459fn pcm_samples(data: &[u8], bits: u16, float: bool, big_endian: bool, unsigned8: bool) -> Option<Vec<f32>> {
460    let bytes = bits as usize / 8;
461    if bytes == 0 || bytes > 8 {
462        return None;
463    }
464    let limit = MAX_FRAMES * 2 * bytes;
465    let data = &data[..data.len().min(limit) / bytes * bytes];
466    Some(
467        data.chunks_exact(bytes)
468            .map(|c| {
469                let mut b = [0u8; 8];
470                if big_endian {
471                    for (i, &v) in c.iter().rev().enumerate() {
472                        b[i] = v;
473                    }
474                } else {
475                    b[..bytes].copy_from_slice(c);
476                }
477                match (float, bytes) {
478                    (true, 4) => f32::from_le_bytes([b[0], b[1], b[2], b[3]]),
479                    (true, 8) => f64::from_le_bytes(b) as f32,
480                    (_, 1) if unsigned8 => (b[0] as f32 - 128.0) / 128.0,
481                    (_, 1) => b[0] as i8 as f32 / 128.0,
482                    (_, 2) => i16::from_le_bytes([b[0], b[1]]) as f32 / 32768.0,
483                    (_, 3) => (i32::from_le_bytes([0, b[0], b[1], b[2]]) >> 8) as f32 / 8_388_608.0,
484                    (_, 4) => i32::from_le_bytes([b[0], b[1], b[2], b[3]]) as f32 / 2_147_483_648.0,
485                    _ => 0.0,
486                }
487            })
488            .collect(),
489    )
490}
491
492fn decode_wav(d: &[u8]) -> Option<(u32, usize, Vec<f32>)> {
493    let (mut at, mut fmt, mut data) = (12, None, None);
494    while at + 8 <= d.len() {
495        let size = u32le(d, at + 4)? as usize;
496        let body = d.get(at + 8..(at + 8 + size).min(d.len()))?;
497        match &d[at..at + 4] {
498            b"fmt " => fmt = Some(body),
499            b"data" => data = Some(body),
500            _ => {}
501        }
502        at += 8 + size + size % 2;
503    }
504    let (fmt, data) = (fmt?, data?);
505    let mut tag = u16le(fmt, 0)?;
506    let channels = u16le(fmt, 2)? as usize;
507    let rate = u32le(fmt, 4)?;
508    let bits = u16le(fmt, 14)?;
509    if tag == 0xFFFE {
510        // WAVE_FORMAT_EXTENSIBLE: the real format starts the sub-format GUID.
511        tag = u16le(fmt, 24)?;
512    }
513    let float = match tag {
514        1 => false,
515        3 => true,
516        _ => return None,
517    };
518    Some((rate, channels, pcm_samples(data, bits, float, false, true)?))
519}
520
521/// An 80-bit IEEE extended float (AIFF's sample rate).
522fn extended(b: &[u8]) -> Option<f64> {
523    let exp = (u16be(b, 0)? & 0x7FFF) as i32;
524    let mant = u64::from_be_bytes(b.get(2..10)?.try_into().ok()?);
525    Some(mant as f64 * 2f64.powi(exp - 16383 - 63))
526}
527
528fn decode_aiff(d: &[u8]) -> Option<(u32, usize, Vec<f32>)> {
529    let aifc = &d[8..12] == b"AIFC";
530    let (mut at, mut comm, mut ssnd) = (12, None, None);
531    while at + 8 <= d.len() {
532        let size = u32be(d, at + 4)? as usize;
533        let body = d.get(at + 8..(at + 8 + size).min(d.len()))?;
534        match &d[at..at + 4] {
535            b"COMM" => comm = Some(body),
536            b"SSND" => ssnd = Some(body),
537            _ => {}
538        }
539        at += 8 + size + size % 2;
540    }
541    let (comm, ssnd) = (comm?, ssnd?);
542    let channels = u16be(comm, 0)? as usize;
543    let bits = u16be(comm, 6)?;
544    let rate = extended(comm.get(8..18)?)? as u32;
545    let (float, big) = match comm.get(18..22).filter(|_| aifc) {
546        None | Some(b"NONE") | Some(b"twos") => (false, true),
547        Some(b"sowt") => (false, false),
548        Some(b"fl32") | Some(b"FL32") => (true, true),
549        _ => return None,
550    };
551    let offset = u32be(ssnd, 0)? as usize;
552    let data = ssnd.get(8 + offset..)?;
553    let bits = if float { 32 } else { bits.div_ceil(8) * 8 };
554    Some((rate, channels, pcm_samples(data, bits, float, big, false)?))
555}
556
557fn decode_voc(d: &[u8]) -> Option<(u32, usize, Vec<f32>)> {
558    let mut at = u16le(d, 20)? as usize;
559    let (mut rate, mut channels, mut bits) = (0u32, 1usize, 8u16);
560    let mut samples = Vec::new();
561    while let Some(&kind) = d.get(at) {
562        if kind == 0 {
563            break;
564        }
565        let size = u32le(d, at)? as usize >> 8;
566        let body = d.get(at + 4..(at + 4 + size).min(d.len()))?;
567        at += 4 + size;
568        let pcm = match kind {
569            // Sound data: a time constant and a codec (0: 8-bit unsigned).
570            1 => {
571                if rate == 0 {
572                    rate = 1_000_000 / (256 - *body.first()? as u32);
573                }
574                (*body.get(1)? == 0).then(|| body.get(2..)).flatten()
575            }
576            // More of the same.
577            2 => Some(body),
578            // The newer header: rate, bits, channels, codec (0 or 4).
579            9 => {
580                rate = u32le(body, 0)?;
581                bits = *body.get(4)? as u16;
582                channels = *body.get(5)? as usize;
583                matches!(u16le(body, 6)?, 0 | 4).then(|| body.get(12..)).flatten()
584            }
585            _ => None,
586        };
587        if let Some(p) = pcm {
588            samples.extend(pcm_samples(p, bits, false, false, true)?);
589            if samples.len() > MAX_FRAMES * 2 {
590                break;
591            }
592        }
593    }
594    (rate > 0).then_some((rate, channels, samples))
595}
596
597/// Ogg Opus: decoded at 48 kHz, stereo.
598fn decode_opus(d: &[u8]) -> Option<(u32, usize, Vec<f32>)> {
599    let mut reader = ogg::PacketReader::new(std::io::Cursor::new(d));
600    let head = reader.read_packet().ok()??;
601    if !head.data.starts_with(b"OpusHead") || head.data.get(18).copied().unwrap_or(0) != 0 {
602        // Not Opus, or a surround mapping.
603        return None;
604    }
605    let pre_skip = u16le(&head.data, 10)? as usize;
606    let gain = i16::from_le_bytes(head.data.get(16..18)?.try_into().ok()?);
607    let gain = 10f32.powf(gain as f32 / 256.0 / 20.0);
608    let mut decoder = rusty_opus::OpusDecoder::new(48_000, 2).ok()?;
609    let mut buf = vec![0f32; 5760 * 2];
610    let mut samples = Vec::new();
611    let mut skipped = 0;
612    while let Ok(Some(packet)) = reader.read_packet() {
613        if packet.data.starts_with(b"OpusTags") {
614            continue;
615        }
616        let Ok(n) = decoder.decode(&packet.data, 5760, &mut buf) else { continue };
617        let mut frames = &buf[..n * 2];
618        if skipped < pre_skip {
619            let skip = (pre_skip - skipped).min(n);
620            skipped += skip;
621            frames = &frames[skip * 2..];
622        }
623        samples.extend(frames.iter().map(|s| s * gain));
624        if samples.len() > MAX_FRAMES * 2 {
625            break;
626        }
627    }
628    Some((48_000, 2, samples))
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634
635    fn wav(rate: u32, channels: u16, bits: u16, data: &[u8]) -> Vec<u8> {
636        let mut f = b"RIFF\0\0\0\0WAVEfmt ".to_vec();
637        f.extend(16u32.to_le_bytes());
638        f.extend(1u16.to_le_bytes());
639        f.extend(channels.to_le_bytes());
640        f.extend(rate.to_le_bytes());
641        f.extend((rate * channels as u32 * bits as u32 / 8).to_le_bytes());
642        f.extend((channels * bits / 8).to_le_bytes());
643        f.extend(bits.to_le_bytes());
644        f.extend(b"data");
645        f.extend((data.len() as u32).to_le_bytes());
646        f.extend(data);
647        f
648    }
649
650    #[test]
651    fn durations_and_volumes() {
652        assert_eq!(duration("500", None), 22050);
653        assert_eq!(duration("500ms", None), 22050);
654        assert_eq!(duration("100f", None), 100);
655        assert_eq!(duration("2p", Some(441.0)), 200);
656        assert_eq!(duration("2p", None), 0);
657        assert_eq!(duration("x", None), 0);
658        assert_eq!(volume("-6.5dB"), -6.5);
659        assert_eq!(volume("100"), 0.0);
660        assert_eq!(volume("0"), -60.0);
661        assert!((volume("50") + 6.02).abs() < 0.01);
662    }
663
664    #[test]
665    fn files_decode_to_stereo_at_44k() {
666        // 8-bit unsigned mono WAV at 22050 Hz: twice as many frames out.
667        let pcm = decode(&wav(22050, 1, 8, &[128, 255, 0, 128])).unwrap();
668        assert_eq!(pcm.len(), 8 * 2);
669        assert_eq!(pcm[0], 0);
670        assert_eq!(pcm[2], pcm[3], "mono goes to both sides");
671        // Frame 1 is halfway between samples 0 and 1; frame 2 is sample 1.
672        assert!((16000..16500).contains(&pcm[2]), "{}", pcm[2]);
673        assert!(pcm[4] > 32000);
674        // 16-bit stereo at the same rate is kept as it is.
675        let pcm = decode(&wav(44100, 2, 16, &[0, 0x40, 0, 0xC0])).unwrap();
676        assert_eq!(&pcm[..], &[16384, -16384]);
677        // A VOC: an 8-bit block at 1 MHz / (256 - 233) = 43478 Hz.
678        let mut voc = b"Creative Voice File\x1a\x1a\x00\x0a\x01\x29\x11".to_vec();
679        voc.extend([1, 6, 0, 0, 233, 0, 128, 255, 128, 0, 0]);
680        let pcm = decode(&voc).unwrap();
681        assert!(pcm.len() >= 8 && pcm.iter().any(|&s| s > 30000));
682        // AIFF: 16-bit big-endian mono at 44100 Hz.
683        let mut comm = 1u16.to_be_bytes().to_vec();
684        comm.extend(2u32.to_be_bytes());
685        comm.extend(16u16.to_be_bytes());
686        comm.extend([0x40, 0x0E, 0xAC, 0x44, 0, 0, 0, 0, 0, 0]);
687        let mut aiff = b"FORM\0\0\0\0AIFFCOMM".to_vec();
688        aiff.extend((comm.len() as u32).to_be_bytes());
689        aiff.extend(&comm);
690        aiff.extend(b"SSND");
691        aiff.extend(12u32.to_be_bytes());
692        aiff.extend([0u8; 8]);
693        aiff.extend([0x40, 0x00, 0xC0, 0x00]);
694        assert_eq!(&decode(&aiff).unwrap()[..], &[16384, 16384, -16384, -16384]);
695        assert!(decode(b"not a sound").is_none());
696    }
697
698    #[test]
699    fn slots_channels_and_reports() {
700        let mut a = Audio::default();
701        let mut ops = Vec::new();
702        let apc = |a: &mut Audio, ops: &mut Vec<AudioOp>, s: &str| {
703            let fields: Vec<&str> = s.split(';').collect();
704            a.apc(&fields, None, ops)
705        };
706        apc(&mut a, &mut ops, "Synth;S=3;W=SQ;F=441;T=10p");
707        apc(&mut a, &mut ops, "Copy;S=3;D=4");
708        // Channels 0 and 1 are the terminal's own.
709        apc(&mut a, &mut ops, "Queue;C=1;S=3");
710        assert!(ops.is_empty());
711        apc(&mut a, &mut ops, "Queue;C=2;S=3;V=50;I=5;L");
712        let Some(AudioOp::Queue { channel: 2, pcm, looped: true, fade_in: 220, volume, .. }) = ops.first() else {
713            panic!("{ops:?}")
714        };
715        assert_eq!(pcm.len(), 1000 * 2);
716        assert!((volume.0 + 6.02).abs() < 0.01);
717        // The slot is emptied; its copy isn't.
718        apc(&mut a, &mut ops, "Queue;C=3;S=3");
719        assert_eq!(ops.len(), 1);
720        apc(&mut a, &mut ops, "Queue;C=3;S=4");
721        assert_eq!(a.report(None), "\x1b[=7;2;1;3;1n");
722        assert_eq!(a.report(Some(5)), "\x1b[=7;5;0n");
723        // Waiting on the 1000-frame tone holds about 23 ms.
724        let until = apc(&mut a, &mut ops, "Wait;C=3").expect("a hold");
725        assert!(until > Instant::now() && until < Instant::now() + Duration::from_millis(100));
726        // Told when it stops.
727        apc(&mut a, &mut ops, "Update;C=3");
728        let mut replies = Vec::new();
729        a.poll(Instant::now(), &mut replies);
730        assert!(replies.is_empty());
731        a.poll(Instant::now() + Duration::from_secs(1), &mut replies);
732        assert_eq!(replies, b"\x1b[=7;3;0n");
733        apc(&mut a, &mut ops, "Flush;C=2");
734        assert_eq!(a.report(Some(2)), "\x1b[=7;2;0n");
735        assert_eq!(ops.last(), Some(&AudioOp::Flush { channel: 2, fade_out: 0 }));
736    }
737
738    #[test]
739    fn tones_match_synctermss_shapes() {
740        let sq = synth(Shape::Square, 441.0, 1000);
741        // Full scale in the middle, softened at the ends.
742        assert_eq!(sq[425 * 2], 32767);
743        assert_eq!(sq[475 * 2], -32768);
744        assert!(sq[0].abs() < 32767 / 4);
745        assert_eq!(synth(Shape::Silence, 441.0, 10), vec![0; 20]);
746        let noisy = synth(Shape::SineSawHarm, 441.0, 1000);
747        assert!(noisy.iter().any(|&v| v != 0));
748        assert!(format_supported(32, 100) && format_supported(1, 2) && !format_supported(23, 2));
749        assert_eq!(parse_id("0x20"), 32);
750    }
751
752    /// An Ogg Opus file: `BADTERM_OPUS=a.opus cargo test -p badterm-core --release opus -- --ignored --nocapture`
753    #[test]
754    #[ignore]
755    fn opus_decodes() {
756        let data = std::fs::read(std::env::var("BADTERM_OPUS").expect("set BADTERM_OPUS")).unwrap();
757        let t = Instant::now();
758        let pcm = decode(&data).expect("decodes");
759        let secs = pcm.len() as f64 / 2.0 / RATE as f64;
760        let peak = pcm.iter().map(|s| s.unsigned_abs()).max().unwrap_or(0);
761        let rms = (pcm.iter().map(|&s| (s as f64).powi(2)).sum::<f64>() / pcm.len() as f64).sqrt();
762        // Zero crossings per second, left channel: music, not noise.
763        let zc = pcm.chunks(2).collect::<Vec<_>>().windows(2).filter(|w| (w[0][0] < 0) != (w[1][0] < 0)).count();
764        eprintln!(
765            "{secs:.2} s, peak {peak}, rms {rms:.0}, {:.0} crossings/s, decoded in {:?}",
766            zc as f64 / secs,
767            t.elapsed()
768        );
769        // The length from the last page's granule position.
770        let mut r = ogg::PacketReader::new(std::io::Cursor::new(&data[..]));
771        let mut last = 0;
772        while let Ok(Some(p)) = r.read_packet() {
773            last = p.absgp_page();
774        }
775        let pre_skip = u16le(&data, 28 + 10).unwrap() as u64;
776        eprintln!("granule length {:.2} s", (last - pre_skip) as f64 / 48_000.0);
777        if let Ok(out) = std::env::var("BADTERM_WAV_OUT") {
778            let bytes: Vec<u8> = pcm.iter().flat_map(|s| s.to_le_bytes()).collect();
779            let mut f = b"RIFF\0\0\0\0WAVEfmt ".to_vec();
780            f.extend(16u32.to_le_bytes());
781            f.extend([1, 0, 2, 0]);
782            f.extend(RATE.to_le_bytes());
783            f.extend((RATE * 4).to_le_bytes());
784            f.extend([4, 0, 16, 0]);
785            f.extend(b"data");
786            f.extend((bytes.len() as u32).to_le_bytes());
787            f.extend(bytes);
788            std::fs::write(out, f).unwrap();
789        }
790    }
791}