Skip to main content

sim_lib_audio_dsp/
oscillator.rs

1use std::f32::consts::TAU;
2
3use sim_lib_audio_graph_core::{PrepareConfig, ProcessBlock, Processor};
4
5use crate::common::prepared_output_channels;
6
7/// Periodic waveform generated by [`BandlimitedOscillator`].
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub enum BandlimitedWaveform {
10    /// A sinusoid, which is already bandlimited.
11    Sine,
12    /// A rising sawtooth with its discontinuity corrected by the selected policy.
13    Saw,
14    /// A pulse wave with a duty cycle in the open interval `(0, 1)`.
15    Pulse {
16        /// Fraction of one period spent at the positive level.
17        duty: f32,
18    },
19    /// A triangle obtained by integrating a corrected square wave.
20    Triangle,
21}
22
23/// Anti-aliasing policy applied at oscillator discontinuities.
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub enum BandlimitPolicy {
26    /// Correct discontinuities with a two-sample polynomial bandlimited step.
27    PolyBlep,
28    /// Generate only a sinusoid; discontinuous waveforms become silent.
29    ///
30    /// This is useful when a caller would rather reject alias-prone content at
31    /// the signal boundary than substitute a different timbre.
32    SineOnly,
33}
34
35/// Explicit oscillator frequency, waveform, level, and bandlimit policy.
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct OscillatorPolicy {
38    /// Oscillator frequency in hertz.
39    pub frequency_hz: f32,
40    /// Peak linear output level.
41    pub amplitude: f32,
42    /// Periodic waveform to generate.
43    pub waveform: BandlimitedWaveform,
44    /// Anti-aliasing behavior for discontinuous waveforms.
45    pub bandlimit: BandlimitPolicy,
46}
47
48impl OscillatorPolicy {
49    /// Creates a policy, sanitizing non-finite values and clamping pulse duty.
50    pub fn new(frequency_hz: f32, waveform: BandlimitedWaveform) -> Self {
51        let waveform = match waveform {
52            BandlimitedWaveform::Pulse { duty } => BandlimitedWaveform::Pulse {
53                duty: finite_or(duty, 0.5).clamp(0.01, 0.99),
54            },
55            other => other,
56        };
57        Self {
58            frequency_hz: finite_or(frequency_hz, 0.0).max(0.0),
59            amplitude: 1.0,
60            waveform,
61            bandlimit: BandlimitPolicy::PolyBlep,
62        }
63    }
64
65    /// Returns this policy with a finite linear amplitude.
66    pub fn with_amplitude(mut self, amplitude: f32) -> Self {
67        self.amplitude = finite_or(amplitude, 0.0);
68        self
69    }
70
71    /// Returns this policy with an explicit anti-aliasing choice.
72    pub fn with_bandlimit(mut self, bandlimit: BandlimitPolicy) -> Self {
73        self.bandlimit = bandlimit;
74        self
75    }
76}
77
78/// Realtime oscillator with one preallocated phase and integrator per channel.
79///
80/// [`prepare`](Processor::prepare) fixes all channel state. The steady-state
81/// [`process`](Processor::process) path performs no allocation, locking, or I/O.
82#[derive(Clone, Debug, PartialEq)]
83pub struct BandlimitedOscillator {
84    policy: OscillatorPolicy,
85    sample_rate_hz: f32,
86    phases: Vec<f32>,
87    triangle_state: Vec<f32>,
88}
89
90impl BandlimitedOscillator {
91    /// Creates an unprepared oscillator from an explicit policy.
92    pub fn new(policy: OscillatorPolicy) -> Self {
93        Self {
94            policy,
95            sample_rate_hz: 48_000.0,
96            phases: Vec::new(),
97            triangle_state: Vec::new(),
98        }
99    }
100
101    /// Returns the retained oscillator policy.
102    pub fn policy(&self) -> OscillatorPolicy {
103        self.policy
104    }
105
106    /// Replaces the frequency without disturbing oscillator phase.
107    pub fn set_frequency_hz(&mut self, frequency_hz: f32) {
108        self.policy.frequency_hz = finite_or(frequency_hz, 0.0).max(0.0);
109    }
110
111    fn phase_increment(&self) -> f32 {
112        if self.sample_rate_hz <= 0.0 {
113            0.0
114        } else {
115            (self.policy.frequency_hz / self.sample_rate_hz).clamp(0.0, 0.499)
116        }
117    }
118
119    fn sample(&mut self, channel: usize, increment: f32) -> f32 {
120        let phase = self.phases[channel];
121        let value = match (self.policy.waveform, self.policy.bandlimit) {
122            (BandlimitedWaveform::Sine, _) => (TAU * phase).sin(),
123            (_, BandlimitPolicy::SineOnly) => 0.0,
124            (BandlimitedWaveform::Saw, BandlimitPolicy::PolyBlep) => {
125                2.0 * phase - 1.0 - poly_blep(phase, increment)
126            }
127            (BandlimitedWaveform::Pulse { duty }, BandlimitPolicy::PolyBlep) => {
128                let naive = if phase < duty { 1.0 } else { -1.0 };
129                naive + poly_blep(phase, increment)
130                    - poly_blep((phase - duty).rem_euclid(1.0), increment)
131            }
132            (BandlimitedWaveform::Triangle, BandlimitPolicy::PolyBlep) => {
133                let naive = if phase < 0.5 { 1.0 } else { -1.0 };
134                let square = naive + poly_blep(phase, increment)
135                    - poly_blep((phase - 0.5).rem_euclid(1.0), increment);
136                let leak = (1.0 - increment).clamp(0.0, 0.999_99);
137                let integrated = leak * self.triangle_state[channel] + square * increment * 4.0;
138                self.triangle_state[channel] = integrated.clamp(-1.2, 1.2);
139                self.triangle_state[channel]
140            }
141        };
142        self.phases[channel] = (phase + increment).rem_euclid(1.0);
143        value * self.policy.amplitude
144    }
145
146    #[cfg(test)]
147    pub(crate) fn realtime_state_snapshot(&self) -> [usize; 2] {
148        [self.phases.capacity(), self.triangle_state.capacity()]
149    }
150}
151
152impl Processor for BandlimitedOscillator {
153    fn prepare(&mut self, cfg: PrepareConfig) {
154        self.sample_rate_hz = cfg.sample_rate_hz.max(1) as f32;
155        self.phases.clear();
156        self.phases.resize(usize::from(cfg.out_channels), 0.0);
157        self.triangle_state.clear();
158        self.triangle_state
159            .resize(usize::from(cfg.out_channels), 0.0);
160    }
161
162    fn reset(&mut self) {
163        self.phases.fill(0.0);
164        self.triangle_state.fill(0.0);
165    }
166
167    fn process(&mut self, block: &mut ProcessBlock<'_>) {
168        let channels = prepared_output_channels(block, self.phases.len(), "BandlimitedOscillator");
169        let increment = self.phase_increment();
170        for frame in 0..block.frames as usize {
171            for channel in 0..channels {
172                block.out_audio[channel][frame] = self.sample(channel, increment);
173            }
174        }
175    }
176}
177
178fn poly_blep(phase: f32, increment: f32) -> f32 {
179    if increment <= f32::EPSILON {
180        return 0.0;
181    }
182    if phase < increment {
183        let x = phase / increment;
184        x + x - x * x - 1.0
185    } else if phase > 1.0 - increment {
186        let x = (phase - 1.0) / increment;
187        x * x + x + x + 1.0
188    } else {
189        0.0
190    }
191}
192
193fn finite_or(value: f32, fallback: f32) -> f32 {
194    if value.is_finite() { value } else { fallback }
195}