firewheel-nodes 0.12.0

Official factory nodes for the Firewheel audio engine
Documentation
//! A simple node that generates pink noise.
//!
//! Base on the algorithm from <https://www.musicdsp.org/en/latest/Synthesis/244-direct-pink-noise-synthesis-with-auto-correlated-generator.html>

use firewheel_core::node::NodeError;
use firewheel_core::param::smoother::DEFAULT_GAIN_SPAN;
use firewheel_core::{
    channel_config::{ChannelConfig, ChannelCount},
    diff::{Diff, Patch},
    dsp::{
        filter::smoothing_filter::DEFAULT_SMOOTH_SECONDS,
        volume::{DEFAULT_MIN_AMP, Volume},
    },
    event::ProcEvents,
    node::{
        AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, ProcBuffers,
        ProcExtra, ProcInfo, ProcessStatus,
    },
    param::smoother::{SmoothedParam, SmootherConfig},
};

const COEFF_A: [i32; 5] = [14055, 12759, 10733, 12273, 15716];
const COEFF_SUM: [i16; 5] = [22347, 27917, 29523, 29942, 30007];

/// A simple node that generates pink noise (Mono output only)
#[derive(Diff, Patch, Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PinkNoiseGenNode {
    /// The overall volume.
    ///
    /// Note, pink noise is really loud, so prefer to use a value like
    /// `Volume::Linear(0.4)` or `Volume::Decibels(-18.0)`.
    pub volume: Volume,
    /// The time in seconds of the internal smoothing filter.
    ///
    /// By default this is set to `0.023` (23ms). This value is chosen to be
    /// roughly equal to a typical block size of 1024 samples (23 ms) to
    /// eliminate stair-stepping for most games.
    pub smooth_seconds: f32,
}

impl Default for PinkNoiseGenNode {
    fn default() -> Self {
        Self {
            volume: Volume::Linear(0.4),
            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
        }
    }
}

/// The configuration for a [`PinkNoiseGenNode`]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PinkNoiseGenConfig {
    /// The starting seed. This cannot be zero.
    pub seed: i32,
}

impl Default for PinkNoiseGenConfig {
    fn default() -> Self {
        Self { seed: 17 }
    }
}

impl AudioNode for PinkNoiseGenNode {
    type Configuration = PinkNoiseGenConfig;

    fn info(&self, _config: &Self::Configuration) -> Result<AudioNodeInfo, NodeError> {
        Ok(AudioNodeInfo::new()
            .debug_name("pink_noise_gen")
            .channel_config(ChannelConfig {
                num_inputs: ChannelCount::ZERO,
                num_outputs: ChannelCount::MONO,
            }))
    }

    fn construct_processor(
        &self,
        config: &Self::Configuration,
        cx: ConstructProcessorContext,
    ) -> Result<impl AudioNodeProcessor, NodeError> {
        // Seed cannot be zero.
        let seed = if config.seed == 0 { 17 } else { config.seed };

        Ok(Processor {
            gain: SmoothedParam::new(
                self.volume.amp_clamped(DEFAULT_MIN_AMP),
                DEFAULT_GAIN_SPAN,
                SmootherConfig {
                    smooth_seconds: self.smooth_seconds,
                    ..Default::default()
                },
                cx.stream_info.sample_rate,
            ),
            params: *self,
            fpd: seed,
            contrib: [0; 5],
            accum: 0,
        })
    }
}

// The realtime processor counterpart to your node.
struct Processor {
    params: PinkNoiseGenNode,
    gain: SmoothedParam,

    // white noise generator state
    fpd: i32,

    // filter stage contributions
    contrib: [i32; 5],
    accum: i32,
}

impl AudioNodeProcessor for Processor {
    fn events(&mut self, info: &ProcInfo, events: &mut ProcEvents, _extra: &mut ProcExtra) {
        for patch in events.drain_patches::<PinkNoiseGenNode>() {
            match patch {
                PinkNoiseGenNodePatch::Volume(vol) => {
                    self.gain.set_value(vol.amp_clamped(DEFAULT_MIN_AMP));
                }
                PinkNoiseGenNodePatch::SmoothSeconds(seconds) => {
                    self.gain.set_smooth_seconds(seconds, info.sample_rate);
                }
            }

            self.params.apply(patch);
        }
    }

    fn process(
        &mut self,
        _info: &ProcInfo,
        buffers: ProcBuffers,
        _extra: &mut ProcExtra,
    ) -> ProcessStatus {
        if self.gain.has_settled_at_or_below(DEFAULT_MIN_AMP) {
            self.gain.reset_to_target();
            return ProcessStatus::ClearAllOutputs;
        }

        for s in buffers.outputs[0].iter_mut() {
            // i16[0,32767]
            let randu: i16 = (rng(&mut self.fpd) & 0x7fff) as i16;

            // i32[-32768,32767]
            let r_bytes = rng(&mut self.fpd).to_ne_bytes();
            let randv: i32 = i16::from_ne_bytes([r_bytes[0], r_bytes[1]]) as i32;

            if randu < COEFF_SUM[0] {
                update_contrib::<0>(&mut self.accum, &mut self.contrib, randv);
            } else if randu < COEFF_SUM[1] {
                update_contrib::<1>(&mut self.accum, &mut self.contrib, randv);
            } else if randu < COEFF_SUM[2] {
                update_contrib::<2>(&mut self.accum, &mut self.contrib, randv);
            } else if randu < COEFF_SUM[3] {
                update_contrib::<3>(&mut self.accum, &mut self.contrib, randv);
            } else if randu < COEFF_SUM[4] {
                update_contrib::<4>(&mut self.accum, &mut self.contrib, randv);
            }

            // Get a random normalized value in the range `[-1.0, 1.0]`.
            let r = self.accum as f32 * (1.0 / 2_147_483_648.0);

            *s = r * self.gain.next_smoothed();
        }

        ProcessStatus::OutputsModified
    }
}

#[inline(always)]
fn rng(fpd: &mut i32) -> i32 {
    *fpd ^= *fpd << 13;
    *fpd ^= *fpd >> 17;
    *fpd ^= *fpd << 5;

    *fpd
}

#[inline(always)]
fn update_contrib<const I: usize>(accum: &mut i32, contrib: &mut [i32; 5], randv: i32) {
    *accum = accum.wrapping_sub(contrib[I]);
    contrib[I] = randv * COEFF_A[I];
    *accum = accum.wrapping_add(contrib[I]);
}