Skip to main content

oxstack_audio/
tone.rs

1//! Sine-tone generator using a phase accumulator and a sine table built at
2//! compile time (256 entries, 512 bytes of flash, no runtime trig).
3
4/// One period of a sine, 256 steps, scaled to ±32767.
5static SINE: [i16; 256] = sine_table();
6
7const fn sine_table() -> [i16; 256] {
8    let mut t = [0i16; 256];
9    let mut i = 0;
10    while i < 256 {
11        // Taylor series of sin(x) on x in [-π, π], evaluated in f64 at
12        // compile time; 11 terms keep the error far below one LSB.
13        let mut x = (i as f64) * (2.0 * core::f64::consts::PI / 256.0);
14        if x > core::f64::consts::PI {
15            x -= 2.0 * core::f64::consts::PI;
16        }
17        let (mut term, mut sum, mut k) = (x, 0.0, 1);
18        while k < 23 {
19            sum += term;
20            term *= -x * x / (((k + 1) * (k + 2)) as f64);
21            k += 2;
22        }
23        let v = sum * 32767.0;
24        t[i] = (if v >= 0.0 { v + 0.5 } else { v - 0.5 }) as i16;
25        i += 1;
26    }
27    t
28}
29
30/// A continuous sine oscillator.
31#[derive(Clone, Copy, Debug)]
32#[cfg_attr(feature = "defmt", derive(defmt::Format))]
33pub struct Sine {
34    phase: u32,
35    step: u32,
36    amplitude: i32,
37}
38
39impl Sine {
40    /// A tone of `freq_hz` at `sample_rate`, with `amplitude` in
41    /// `0.0..=1.0` of full scale.
42    pub fn new(freq_hz: f32, sample_rate: u32, amplitude: f32) -> Self {
43        // The phase is a 32-bit fraction of a full turn; the table index is
44        // its top 8 bits.
45        let step = (freq_hz as f64 / f64::from(sample_rate) * 4_294_967_296.0) as u32;
46        let amplitude = (amplitude.clamp(0.0, 1.0) * 32768.0) as i32;
47        Self {
48            phase: 0,
49            step,
50            amplitude,
51        }
52    }
53
54    /// Changes the frequency without resetting the phase, so the waveform
55    /// stays continuous (no click).
56    pub fn set_frequency(&mut self, freq_hz: f32, sample_rate: u32) {
57        self.step = Self::new(freq_hz, sample_rate, 0.0).step;
58    }
59
60    /// Next sample.
61    pub fn next_sample(&mut self) -> i16 {
62        let s = i32::from(SINE[(self.phase >> 24) as usize]);
63        self.phase = self.phase.wrapping_add(self.step);
64        ((s * self.amplitude) >> 15) as i16
65    }
66
67    /// Fills interleaved frames, writing the same sample to every channel.
68    pub fn fill(&mut self, frames: &mut [i16], channels: usize) {
69        for frame in frames.chunks_exact_mut(channels.max(1)) {
70            frame.fill(self.next_sample());
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    extern crate std;
78    use super::*;
79    use crate::level::Level;
80
81    #[test]
82    fn table_hits_the_cardinal_points() {
83        assert_eq!(SINE[0], 0);
84        assert_eq!(SINE[64], 32767);
85        assert_eq!(SINE[128], 0);
86        assert_eq!(SINE[192], -32767);
87    }
88
89    #[test]
90    fn full_scale_1khz_stereo() {
91        let mut osc = Sine::new(1000.0, 48_000, 1.0);
92        let mut buf = [0i16; 2 * 4800];
93        osc.fill(&mut buf, 2);
94        assert!(buf.chunks(2).all(|f| f[0] == f[1]), "channels differ");
95
96        let l = Level::measure(&buf, 0, 2);
97        assert!((l.rms_dbfs() + 3.01).abs() < 0.05, "rms {}", l.rms_dbfs());
98        assert!(l.dc.abs() < 1e-3, "dc {}", l.dc);
99
100        // 48 samples per period: 100 periods, 200 zero crossings.
101        let left: std::vec::Vec<i16> = buf.iter().step_by(2).copied().collect();
102        let crossings = left.windows(2).filter(|w| (w[0] < 0) != (w[1] < 0)).count();
103        assert!((199..=201).contains(&crossings), "{crossings} crossings");
104    }
105
106    #[test]
107    fn frequency_changes_are_continuous() {
108        // A 1 kHz -> 2 kHz switch must not jump more than one 2 kHz step.
109        let mut osc = Sine::new(1000.0, 48_000, 1.0);
110        let mut prev = 0i32;
111        for i in 0..2000 {
112            if i == 1000 {
113                osc.set_frequency(2000.0, 48_000);
114            }
115            let s = i32::from(osc.next_sample());
116            // Max slope of a full-scale 2 kHz sine at 48 kHz is 2π·2000/48000 ≈ 0.26 FS/sample.
117            assert!(
118                i == 0 || (s - prev).abs() < 9000,
119                "jump at {i}: {prev} -> {s}"
120            );
121            prev = s;
122        }
123    }
124}