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