sim_lib_audio_dsp/
oscillator.rs1use std::f32::consts::TAU;
2
3use sim_lib_audio_graph_core::{PrepareConfig, ProcessBlock, Processor};
4
5use crate::common::prepared_output_channels;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
9pub enum BandlimitedWaveform {
10 Sine,
12 Saw,
14 Pulse {
16 duty: f32,
18 },
19 Triangle,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq)]
25pub enum BandlimitPolicy {
26 PolyBlep,
28 SineOnly,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct OscillatorPolicy {
38 pub frequency_hz: f32,
40 pub amplitude: f32,
42 pub waveform: BandlimitedWaveform,
44 pub bandlimit: BandlimitPolicy,
46}
47
48impl OscillatorPolicy {
49 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 pub fn with_amplitude(mut self, amplitude: f32) -> Self {
67 self.amplitude = finite_or(amplitude, 0.0);
68 self
69 }
70
71 pub fn with_bandlimit(mut self, bandlimit: BandlimitPolicy) -> Self {
73 self.bandlimit = bandlimit;
74 self
75 }
76}
77
78#[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 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 pub fn policy(&self) -> OscillatorPolicy {
103 self.policy
104 }
105
106 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}