sim-lib-audio-dsp 0.1.3

Reusable pure Rust DSP processors for the SIM audio graph.
Documentation
use std::f32::consts::TAU;

use sim_lib_audio_graph_core::{PrepareConfig, ProcessBlock, Processor};

use crate::common::prepared_output_channels;

/// Periodic waveform generated by [`BandlimitedOscillator`].
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BandlimitedWaveform {
    /// A sinusoid, which is already bandlimited.
    Sine,
    /// A rising sawtooth with its discontinuity corrected by the selected policy.
    Saw,
    /// A pulse wave with a duty cycle in the open interval `(0, 1)`.
    Pulse {
        /// Fraction of one period spent at the positive level.
        duty: f32,
    },
    /// A triangle obtained by integrating a corrected square wave.
    Triangle,
}

/// Anti-aliasing policy applied at oscillator discontinuities.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BandlimitPolicy {
    /// Correct discontinuities with a two-sample polynomial bandlimited step.
    PolyBlep,
    /// Generate only a sinusoid; discontinuous waveforms become silent.
    ///
    /// This is useful when a caller would rather reject alias-prone content at
    /// the signal boundary than substitute a different timbre.
    SineOnly,
}

/// Explicit oscillator frequency, waveform, level, and bandlimit policy.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct OscillatorPolicy {
    /// Oscillator frequency in hertz.
    pub frequency_hz: f32,
    /// Peak linear output level.
    pub amplitude: f32,
    /// Periodic waveform to generate.
    pub waveform: BandlimitedWaveform,
    /// Anti-aliasing behavior for discontinuous waveforms.
    pub bandlimit: BandlimitPolicy,
}

impl OscillatorPolicy {
    /// Creates a policy, sanitizing non-finite values and clamping pulse duty.
    pub fn new(frequency_hz: f32, waveform: BandlimitedWaveform) -> Self {
        let waveform = match waveform {
            BandlimitedWaveform::Pulse { duty } => BandlimitedWaveform::Pulse {
                duty: finite_or(duty, 0.5).clamp(0.01, 0.99),
            },
            other => other,
        };
        Self {
            frequency_hz: finite_or(frequency_hz, 0.0).max(0.0),
            amplitude: 1.0,
            waveform,
            bandlimit: BandlimitPolicy::PolyBlep,
        }
    }

    /// Returns this policy with a finite linear amplitude.
    pub fn with_amplitude(mut self, amplitude: f32) -> Self {
        self.amplitude = finite_or(amplitude, 0.0);
        self
    }

    /// Returns this policy with an explicit anti-aliasing choice.
    pub fn with_bandlimit(mut self, bandlimit: BandlimitPolicy) -> Self {
        self.bandlimit = bandlimit;
        self
    }
}

/// Realtime oscillator with one preallocated phase and integrator per channel.
///
/// [`prepare`](Processor::prepare) fixes all channel state. The steady-state
/// [`process`](Processor::process) path performs no allocation, locking, or I/O.
#[derive(Clone, Debug, PartialEq)]
pub struct BandlimitedOscillator {
    policy: OscillatorPolicy,
    sample_rate_hz: f32,
    phases: Vec<f32>,
    triangle_state: Vec<f32>,
}

impl BandlimitedOscillator {
    /// Creates an unprepared oscillator from an explicit policy.
    pub fn new(policy: OscillatorPolicy) -> Self {
        Self {
            policy,
            sample_rate_hz: 48_000.0,
            phases: Vec::new(),
            triangle_state: Vec::new(),
        }
    }

    /// Returns the retained oscillator policy.
    pub fn policy(&self) -> OscillatorPolicy {
        self.policy
    }

    /// Replaces the frequency without disturbing oscillator phase.
    pub fn set_frequency_hz(&mut self, frequency_hz: f32) {
        self.policy.frequency_hz = finite_or(frequency_hz, 0.0).max(0.0);
    }

    fn phase_increment(&self) -> f32 {
        if self.sample_rate_hz <= 0.0 {
            0.0
        } else {
            (self.policy.frequency_hz / self.sample_rate_hz).clamp(0.0, 0.499)
        }
    }

    fn sample(&mut self, channel: usize, increment: f32) -> f32 {
        let phase = self.phases[channel];
        let value = match (self.policy.waveform, self.policy.bandlimit) {
            (BandlimitedWaveform::Sine, _) => (TAU * phase).sin(),
            (_, BandlimitPolicy::SineOnly) => 0.0,
            (BandlimitedWaveform::Saw, BandlimitPolicy::PolyBlep) => {
                2.0 * phase - 1.0 - poly_blep(phase, increment)
            }
            (BandlimitedWaveform::Pulse { duty }, BandlimitPolicy::PolyBlep) => {
                let naive = if phase < duty { 1.0 } else { -1.0 };
                naive + poly_blep(phase, increment)
                    - poly_blep((phase - duty).rem_euclid(1.0), increment)
            }
            (BandlimitedWaveform::Triangle, BandlimitPolicy::PolyBlep) => {
                let naive = if phase < 0.5 { 1.0 } else { -1.0 };
                let square = naive + poly_blep(phase, increment)
                    - poly_blep((phase - 0.5).rem_euclid(1.0), increment);
                let leak = (1.0 - increment).clamp(0.0, 0.999_99);
                let integrated = leak * self.triangle_state[channel] + square * increment * 4.0;
                self.triangle_state[channel] = integrated.clamp(-1.2, 1.2);
                self.triangle_state[channel]
            }
        };
        self.phases[channel] = (phase + increment).rem_euclid(1.0);
        value * self.policy.amplitude
    }

    #[cfg(test)]
    pub(crate) fn realtime_state_snapshot(&self) -> [usize; 2] {
        [self.phases.capacity(), self.triangle_state.capacity()]
    }
}

impl Processor for BandlimitedOscillator {
    fn prepare(&mut self, cfg: PrepareConfig) {
        self.sample_rate_hz = cfg.sample_rate_hz.max(1) as f32;
        self.phases.clear();
        self.phases.resize(usize::from(cfg.out_channels), 0.0);
        self.triangle_state.clear();
        self.triangle_state
            .resize(usize::from(cfg.out_channels), 0.0);
    }

    fn reset(&mut self) {
        self.phases.fill(0.0);
        self.triangle_state.fill(0.0);
    }

    fn process(&mut self, block: &mut ProcessBlock<'_>) {
        let channels = prepared_output_channels(block, self.phases.len(), "BandlimitedOscillator");
        let increment = self.phase_increment();
        for frame in 0..block.frames as usize {
            for channel in 0..channels {
                block.out_audio[channel][frame] = self.sample(channel, increment);
            }
        }
    }
}

fn poly_blep(phase: f32, increment: f32) -> f32 {
    if increment <= f32::EPSILON {
        return 0.0;
    }
    if phase < increment {
        let x = phase / increment;
        x + x - x * x - 1.0
    } else if phase > 1.0 - increment {
        let x = (phase - 1.0) / increment;
        x * x + x + x + 1.0
    } else {
        0.0
    }
}

fn finite_or(value: f32, fallback: f32) -> f32 {
    if value.is_finite() { value } else { fallback }
}