Skip to main content

audiobox/synth/
wave.rs

1use std::str::FromStr;
2
3use crate::{Error, Result};
4
5/// Pulse duty cycle: the fraction of each period spent high.
6#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
7pub enum Duty {
8    /// 12.5%.
9    Eighth,
10    /// 25%.
11    Quarter,
12    /// 50%, a square wave.
13    #[default]
14    Half,
15    /// 75%. Sounds like 25% inverted.
16    ThreeQuarters,
17}
18impl Duty {
19    pub fn fraction(self) -> f64 {
20        match self {
21            Self::Eighth => 0.125,
22            Self::Quarter => 0.25,
23            Self::Half => 0.5,
24            Self::ThreeQuarters => 0.75,
25        }
26    }
27}
28impl FromStr for Duty {
29    type Err = Error;
30    /// Accepts the percentages `12.5`, `25`, `50` and `75`.
31    fn from_str(s: &str) -> Result<Self> {
32        match s {
33            "12.5" | "12" => Ok(Self::Eighth),
34            "25" => Ok(Self::Quarter),
35            "50" => Ok(Self::Half),
36            "75" => Ok(Self::ThreeQuarters),
37            _ => Err(Error::invalid(format!(
38                "Duty must be 12.5, 25, 50 or 75, not {s}"
39            ))),
40        }
41    }
42}
43
44/// Feedback tap of the noise generator's 15-bit linear-feedback shift register.
45#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
46pub enum NoiseMode {
47    /// Taps bits 0 and 1: a 32,767-step sequence that sounds like hiss.
48    #[default]
49    Long,
50    /// Taps bits 0 and 6: a 93-step sequence with a metallic, pitched tone.
51    Short,
52}
53
54#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
55pub enum Waveform {
56    Pulse(Duty),
57    Triangle,
58    Sawtooth,
59    /// Noise clocked at 93 times the note's frequency, so the short mode's
60    /// 93-step period sounds at the note's pitch.
61    Noise(NoiseMode),
62}
63impl Default for Waveform {
64    fn default() -> Self {
65        Self::Pulse(Duty::Half)
66    }
67}
68
69/// How oscillators treat harmonics above the Nyquist frequency.
70#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
71pub enum Aliasing {
72    /// PolyBLEP steps for pulse and sawtooth, PolyBLAMP corners for triangle, and
73    /// noise averaged over each sample. Aliases are strongly reduced, not removed.
74    #[default]
75    BandLimited,
76    /// Raw waveforms sampled at each frame. Harsh, aliased "period" sound.
77    Naive,
78}
79
80/// Linear-feedback shift register of 15 bits, starting at 1.
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub struct Lfsr {
83    state: u16,
84    mode: NoiseMode,
85}
86impl Lfsr {
87    pub fn new(mode: NoiseMode) -> Self {
88        Self { state: 1, mode }
89    }
90    pub fn mode(&self) -> NoiseMode {
91        self.mode
92    }
93    /// Changes the tap. The register restarts at 1, so the short mode always runs its 93-step cycle.
94    pub fn set_mode(&mut self, mode: NoiseMode) {
95        if mode != self.mode {
96            *self = Self::new(mode);
97        }
98    }
99    pub fn state(&self) -> u16 {
100        self.state
101    }
102    /// +1 while bit 0 is clear, otherwise -1.
103    pub fn output(&self) -> f64 {
104        if self.state & 1 == 0 { 1.0 } else { -1.0 }
105    }
106    pub fn step(&mut self) {
107        let tap = match self.mode {
108            NoiseMode::Long => 1,
109            NoiseMode::Short => 6,
110        };
111        let feedback = (self.state ^ (self.state >> tap)) & 1;
112        self.state = (self.state >> 1) | (feedback << 14);
113    }
114}
115
116const ONE: u64 = 1 << 32;
117const FRACTION: u64 = ONE - 1;
118
119/// A phase-continuous oscillator. Changing the waveform or frequency between
120/// samples keeps the phase, so note changes do not click.
121///
122/// The phase is a 32.32 fixed-point accumulator and every sample is computed with
123/// basic IEEE arithmetic only, so the output is identical on every platform.
124#[derive(Clone, Debug)]
125pub struct Oscillator {
126    phase: u64,
127    lfsr: Lfsr,
128    aliasing: Aliasing,
129}
130impl Oscillator {
131    pub(super) fn phase_offset(&mut self, turns: f64) {
132        self.phase = ((turns - turns.floor()) * ONE as f64).round() as u64 & FRACTION;
133    }
134    pub fn new(aliasing: Aliasing) -> Self {
135        Self {
136            phase: 0,
137            lfsr: Lfsr::new(NoiseMode::Long),
138            aliasing,
139        }
140    }
141    /// Returns the sample at the current phase, in -1..=1 for naive waveforms, then
142    /// advances by one frame. `frequency` is clamped to 0..=sample_rate / 2.
143    pub fn next(&mut self, waveform: Waveform, frequency: f64, sample_rate: u32) -> f64 {
144        match waveform {
145            Waveform::Pulse(duty) => {
146                self.tone(Shape::Pulse(duty.fraction()), frequency, sample_rate)
147            }
148            Waveform::Triangle => self.tone(Shape::Triangle, frequency, sample_rate),
149            Waveform::Sawtooth => self.tone(Shape::Sawtooth, frequency, sample_rate),
150            Waveform::Noise(mode) => {
151                let rate = sample_rate.max(1) as f64;
152                self.lfsr.set_mode(mode);
153                let increment =
154                    (clamp_frequency(frequency, rate) * 93.0 / rate * ONE as f64).round() as u64;
155                self.noise(increment)
156            }
157        }
158    }
159    /// Like [`Oscillator::next`] for a pulse of any duty, clamped to 0.01..=0.99.
160    /// The four [`Duty`] values give the same samples as [`Waveform::Pulse`].
161    pub fn next_pulse(&mut self, duty: f64, frequency: f64, sample_rate: u32) -> f64 {
162        let duty = if duty.is_finite() {
163            duty.clamp(0.01, 0.99)
164        } else {
165            0.5
166        };
167        self.tone(Shape::Pulse(duty), frequency, sample_rate)
168    }
169    /// Separate fixed register clock for opt-in period-table noise.
170    pub(super) fn clocked_noise(&mut self, mode: NoiseMode, clock: f64, sample_rate: u32) -> f64 {
171        self.lfsr.set_mode(mode);
172        let increment = (clock / sample_rate as f64 * ONE as f64).round() as u64;
173        self.noise(increment)
174    }
175    fn tone(&mut self, shape: Shape, frequency: f64, sample_rate: u32) -> f64 {
176        let rate = sample_rate.max(1) as f64;
177        let increment = (clamp_frequency(frequency, rate) / rate * ONE as f64).round() as u64;
178        let t = (self.phase & FRACTION) as f64 / ONE as f64;
179        let dt = increment as f64 / ONE as f64;
180        self.phase = (self.phase & FRACTION) + increment;
181        let band_limited = self.aliasing == Aliasing::BandLimited && dt > 0.0;
182        match shape {
183            Shape::Pulse(d) => {
184                let mut value = if t < d { 1.0 } else { -1.0 };
185                if band_limited {
186                    value += poly_blep(t, dt) - poly_blep(wrap(t - d), dt);
187                }
188                value
189            }
190            Shape::Sawtooth => {
191                let mut value = 2.0 * t - 1.0;
192                if band_limited {
193                    value -= poly_blep(t, dt);
194                }
195                value
196            }
197            Shape::Triangle => {
198                let mut value = if t < 0.5 {
199                    4.0 * t - 1.0
200                } else {
201                    3.0 - 4.0 * t
202                };
203                if band_limited {
204                    value += 8.0 * dt * (poly_blamp(t, dt) - poly_blamp(wrap(t - 0.5), dt));
205                }
206                value
207            }
208        }
209    }
210    fn noise(&mut self, increment: u64) -> f64 {
211        let start = self.phase & FRACTION;
212        let end = start + increment;
213        let steps = end >> 32;
214        let value = if self.aliasing == Aliasing::Naive || increment == 0 {
215            let value = self.lfsr.output();
216            for _ in 0..steps {
217                self.lfsr.step();
218            }
219            value
220        } else {
221            let mut sum = self.lfsr.output() * (end.min(ONE) - start) as f64;
222            for k in 1..=steps {
223                self.lfsr.step();
224                let from = k * ONE;
225                sum += self.lfsr.output() * (end.min(from + ONE) - from) as f64;
226            }
227            sum / increment as f64
228        };
229        self.phase = end & FRACTION;
230        value
231    }
232}
233
234#[derive(Clone, Copy)]
235enum Shape {
236    Pulse(f64),
237    Triangle,
238    Sawtooth,
239}
240
241fn clamp_frequency(frequency: f64, rate: f64) -> f64 {
242    if frequency.is_finite() {
243        frequency.clamp(0.0, rate / 2.0)
244    } else {
245        0.0
246    }
247}
248
249fn wrap(t: f64) -> f64 {
250    if t < 0.0 { t + 1.0 } else { t }
251}
252
253/// Residual of a band-limited step of height 2, for a phase `t` near the step at 0.
254fn poly_blep(t: f64, dt: f64) -> f64 {
255    if t < dt {
256        let x = t / dt;
257        2.0 * x - x * x - 1.0
258    } else if t > 1.0 - dt {
259        let x = (t - 1.0) / dt;
260        x * x + 2.0 * x + 1.0
261    } else {
262        0.0
263    }
264}
265
266/// Residual of a band-limited corner whose slope rises by one unit per sample,
267/// for a phase `t` near the corner at 0: the integral of the PolyBLEP residual.
268fn poly_blamp(t: f64, dt: f64) -> f64 {
269    let x = if t < dt {
270        1.0 - t / dt
271    } else if t > 1.0 - dt {
272        1.0 - (1.0 - t) / dt
273    } else {
274        return 0.0;
275    };
276    x * x * x / 6.0
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    #[test]
283    fn lfsr_periods() {
284        for (mode, period) in [(NoiseMode::Long, 32767), (NoiseMode::Short, 93)] {
285            let mut lfsr = Lfsr::new(mode);
286            let mut n = 0;
287            loop {
288                lfsr.step();
289                n += 1;
290                if lfsr.state() == 1 {
291                    break;
292                }
293            }
294            assert_eq!(n, period);
295        }
296    }
297}