bbx_dsp/
waveform.rs

1//! Waveform types and generation.
2//!
3//! This module defines standard waveform shapes used by oscillators and LFOs.
4
5#[cfg(feature = "simd")]
6use std::simd::StdFloat;
7
8use bbx_core::random::XorShiftRng;
9
10#[cfg(feature = "simd")]
11use crate::polyblep::{apply_polyblamp_triangle, apply_polyblep_pulse, apply_polyblep_saw, apply_polyblep_square};
12#[cfg(feature = "simd")]
13use crate::sample::SIMD_LANES;
14use crate::{
15    polyblep::{polyblamp_triangle, polyblep_pulse, polyblep_saw, polyblep_square},
16    sample::Sample,
17};
18
19/// Standard waveform shapes for oscillators and LFOs.
20#[derive(Debug, Clone, Copy)]
21pub enum Waveform {
22    /// Pure tone with no harmonics.
23    Sine,
24    /// Rich in odd harmonics, bright and buzzy.
25    Square,
26    /// Contains all harmonics, bright and cutting.
27    Sawtooth,
28    /// Soft, flute-like tone with odd harmonics.
29    Triangle,
30    /// Variable duty cycle square wave.
31    Pulse,
32    /// Random values for each sample.
33    Noise,
34}
35
36/// Default value for the duty cycle or the inflection point
37/// at which the values of a waveform change sign. This effectively
38/// determines the width of the waveform within its periodic cycle.
39pub(crate) const DEFAULT_DUTY_CYCLE: f64 = 0.5;
40
41/// Generate 4 naive samples of a waveform using SIMD (internal helper).
42///
43/// Returns `None` for Noise waveform (requires sequential RNG).
44#[cfg(feature = "simd")]
45fn generate_naive_samples_simd<S: Sample>(
46    waveform: Waveform,
47    phases: S::Simd,
48    duty_cycle: S,
49    two_pi: S::Simd,
50    inv_two_pi: S::Simd,
51) -> Option<[S; SIMD_LANES]> {
52    match waveform {
53        Waveform::Sine => Some(S::simd_to_array(phases.sin())),
54
55        Waveform::Square => {
56            let half = S::simd_splat(S::from_f64(0.5));
57            let one = S::simd_splat(S::ONE);
58            let neg_one = S::simd_splat(-S::ONE);
59            let normalized = (phases % two_pi) * inv_two_pi;
60            Some(S::simd_to_array(S::simd_select_lt(normalized, half, one, neg_one)))
61        }
62
63        Waveform::Sawtooth => {
64            let two = S::simd_splat(S::from_f64(2.0));
65            let one = S::simd_splat(S::ONE);
66            let normalized = (phases % two_pi) * inv_two_pi;
67            Some(S::simd_to_array(two * normalized - one))
68        }
69
70        Waveform::Triangle => {
71            let half = S::simd_splat(S::from_f64(0.5));
72            let four = S::simd_splat(S::from_f64(4.0));
73            let one = S::simd_splat(S::ONE);
74            let three = S::simd_splat(S::from_f64(3.0));
75
76            let normalized = (phases % two_pi) * inv_two_pi;
77            let rising = four * normalized - one;
78            let falling = three - four * normalized;
79            Some(S::simd_to_array(S::simd_select_lt(normalized, half, rising, falling)))
80        }
81
82        Waveform::Pulse => {
83            let duty = S::simd_splat(duty_cycle);
84            let one = S::simd_splat(S::ONE);
85            let neg_one = S::simd_splat(-S::ONE);
86
87            let normalized = (phases % two_pi) * inv_two_pi;
88            Some(S::simd_to_array(S::simd_select_lt(normalized, duty, one, neg_one)))
89        }
90
91        Waveform::Noise => None,
92    }
93}
94
95/// Generate a band-limited waveform sample using PolyBLEP/PolyBLAMP.
96///
97/// Uses polynomial corrections near discontinuities to reduce aliasing.
98/// Sine and noise waveforms pass through without correction.
99pub(crate) fn generate_waveform_sample(
100    waveform: Waveform,
101    phase: f64,
102    phase_increment: f64,
103    duty_cycle: f64,
104    rng: &mut XorShiftRng,
105) -> f64 {
106    let normalized_phase = (phase % <f64 as Sample>::TAU) * <f64 as Sample>::INV_TAU;
107    let normalized_inc = phase_increment * <f64 as Sample>::INV_TAU;
108
109    match waveform {
110        Waveform::Sine => phase.sin(),
111        Waveform::Sawtooth => polyblep_saw(normalized_phase, normalized_inc),
112        Waveform::Square => polyblep_square(normalized_phase, normalized_inc),
113        Waveform::Pulse => polyblep_pulse(normalized_phase, normalized_inc, duty_cycle),
114        Waveform::Triangle => polyblamp_triangle(normalized_phase, normalized_inc),
115        Waveform::Noise => rng.next_noise_sample(),
116    }
117}
118
119/// Process waveform samples using scalar operations with band-limiting.
120///
121/// Writes band-limited samples to `output`, advances `phase` by `phase_increment`
122/// per sample, and applies PolyBLEP/PolyBLAMP corrections.
123pub(crate) fn process_waveform_scalar<S: Sample>(
124    output: &mut [S],
125    waveform: Waveform,
126    phase: &mut f64,
127    phase_increment: f64,
128    rng: &mut XorShiftRng,
129    scale: f64,
130) {
131    for sample in output.iter_mut() {
132        let value = generate_waveform_sample(waveform, *phase, phase_increment, DEFAULT_DUTY_CYCLE, rng);
133        *sample = S::from_f64(value * scale);
134        *phase += phase_increment;
135    }
136    *phase = phase.rem_euclid(<f64 as Sample>::TAU);
137}
138
139/// Generate 4 band-limited waveform samples using SIMD with PolyBLEP corrections.
140///
141/// Generates naive samples via SIMD, then applies PolyBLEP/PolyBLAMP corrections.
142#[cfg(feature = "simd")]
143pub(crate) fn generate_waveform_samples_simd<S: Sample>(
144    waveform: Waveform,
145    phases: S::Simd,
146    phases_normalized: [S; SIMD_LANES],
147    phase_inc_normalized: S,
148    duty_cycle: S,
149    two_pi: S::Simd,
150    inv_two_pi: S::Simd,
151) -> Option<[S; SIMD_LANES]> {
152    let mut samples = generate_naive_samples_simd(waveform, phases, duty_cycle, two_pi, inv_two_pi)?;
153
154    match waveform {
155        Waveform::Sine | Waveform::Noise => {}
156        Waveform::Sawtooth => {
157            apply_polyblep_saw(&mut samples, phases_normalized, phase_inc_normalized);
158        }
159        Waveform::Square => {
160            apply_polyblep_square(&mut samples, phases_normalized, phase_inc_normalized);
161        }
162        Waveform::Pulse => {
163            apply_polyblep_pulse(&mut samples, phases_normalized, phase_inc_normalized, duty_cycle);
164        }
165        Waveform::Triangle => {
166            apply_polyblamp_triangle(&mut samples, phases_normalized, phase_inc_normalized);
167        }
168    }
169
170    Some(samples)
171}