Skip to main content

firewheel_nodes/noise_generator/
pink.rs

1//! A simple node that generates pink noise.
2//!
3//! Base on the algorithm from <https://www.musicdsp.org/en/latest/Synthesis/244-direct-pink-noise-synthesis-with-auto-correlated-generator.html>
4
5use firewheel_core::{
6    channel_config::{ChannelConfig, ChannelCount},
7    diff::{Diff, Patch},
8    dsp::{
9        filter::smoothing_filter::DEFAULT_SMOOTH_SECONDS,
10        volume::{Volume, DEFAULT_AMP_EPSILON},
11    },
12    event::ProcEvents,
13    node::{
14        AudioNode, AudioNodeInfo, AudioNodeProcessor, ConstructProcessorContext, ProcBuffers,
15        ProcExtra, ProcInfo, ProcessStatus,
16    },
17    param::smoother::{SmoothedParam, SmootherConfig},
18};
19
20const COEFF_A: [i32; 5] = [14055, 12759, 10733, 12273, 15716];
21const COEFF_SUM: [i16; 5] = [22347, 27917, 29523, 29942, 30007];
22
23/// A simple node that generates pink noise (Mono output only)
24#[derive(Diff, Patch, Debug, Clone, Copy, PartialEq)]
25#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
26#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub struct PinkNoiseGenNode {
29    /// The overall volume.
30    ///
31    /// Note, pink noise is really loud, so prefer to use a value like
32    /// `Volume::Linear(0.4)` or `Volume::Decibels(-18.0)`.
33    pub volume: Volume,
34    /// Whether or not this node is enabled.
35    pub enabled: bool,
36    /// The time in seconds of the internal smoothing filter.
37    ///
38    /// By default this is set to `0.015` (15ms).
39    pub smooth_seconds: f32,
40}
41
42impl Default for PinkNoiseGenNode {
43    fn default() -> Self {
44        Self {
45            volume: Volume::Linear(0.4),
46            enabled: true,
47            smooth_seconds: DEFAULT_SMOOTH_SECONDS,
48        }
49    }
50}
51
52/// The configuration for a [`PinkNoiseGenNode`]
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54#[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Component))]
55#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57pub struct PinkNoiseGenConfig {
58    /// The starting seed. This cannot be zero.
59    pub seed: i32,
60}
61
62impl Default for PinkNoiseGenConfig {
63    fn default() -> Self {
64        Self { seed: 17 }
65    }
66}
67
68impl AudioNode for PinkNoiseGenNode {
69    type Configuration = PinkNoiseGenConfig;
70
71    fn info(&self, _config: &Self::Configuration) -> AudioNodeInfo {
72        AudioNodeInfo::new()
73            .debug_name("pink_noise_gen")
74            .channel_config(ChannelConfig {
75                num_inputs: ChannelCount::ZERO,
76                num_outputs: ChannelCount::MONO,
77            })
78    }
79
80    fn construct_processor(
81        &self,
82        config: &Self::Configuration,
83        cx: ConstructProcessorContext,
84    ) -> impl AudioNodeProcessor {
85        // Seed cannot be zero.
86        let seed = if config.seed == 0 { 17 } else { config.seed };
87
88        Processor {
89            gain: SmoothedParam::new(
90                self.volume.amp_clamped(DEFAULT_AMP_EPSILON),
91                SmootherConfig {
92                    smooth_seconds: self.smooth_seconds,
93                    ..Default::default()
94                },
95                cx.stream_info.sample_rate,
96            ),
97            params: *self,
98            fpd: seed,
99            contrib: [0; 5],
100            accum: 0,
101        }
102    }
103}
104
105// The realtime processor counterpart to your node.
106struct Processor {
107    params: PinkNoiseGenNode,
108    gain: SmoothedParam,
109
110    // white noise generator state
111    fpd: i32,
112
113    // filter stage contributions
114    contrib: [i32; 5],
115    accum: i32,
116}
117
118impl AudioNodeProcessor for Processor {
119    fn process(
120        &mut self,
121        info: &ProcInfo,
122        buffers: ProcBuffers,
123        events: &mut ProcEvents,
124        _extra: &mut ProcExtra,
125    ) -> ProcessStatus {
126        for patch in events.drain_patches::<PinkNoiseGenNode>() {
127            match patch {
128                PinkNoiseGenNodePatch::Volume(vol) => {
129                    self.gain.set_value(vol.amp_clamped(DEFAULT_AMP_EPSILON));
130                }
131                PinkNoiseGenNodePatch::SmoothSeconds(seconds) => {
132                    self.gain.set_smooth_seconds(seconds, info.sample_rate);
133                }
134                _ => {}
135            }
136
137            self.params.apply(patch);
138        }
139
140        if !self.params.enabled || self.gain.has_settled_at_or_below(DEFAULT_AMP_EPSILON) {
141            self.gain.reset_to_target();
142            return ProcessStatus::ClearAllOutputs;
143        }
144
145        for s in buffers.outputs[0].iter_mut() {
146            // i16[0,32767]
147            let randu: i16 = (rng(&mut self.fpd) & 0x7fff) as i16;
148
149            // i32[-32768,32767]
150            let r_bytes = rng(&mut self.fpd).to_ne_bytes();
151            let randv: i32 = i16::from_ne_bytes([r_bytes[0], r_bytes[1]]) as i32;
152
153            if randu < COEFF_SUM[0] {
154                update_contrib::<0>(&mut self.accum, &mut self.contrib, randv);
155            } else if randu < COEFF_SUM[1] {
156                update_contrib::<1>(&mut self.accum, &mut self.contrib, randv);
157            } else if randu < COEFF_SUM[2] {
158                update_contrib::<2>(&mut self.accum, &mut self.contrib, randv);
159            } else if randu < COEFF_SUM[3] {
160                update_contrib::<3>(&mut self.accum, &mut self.contrib, randv);
161            } else if randu < COEFF_SUM[4] {
162                update_contrib::<4>(&mut self.accum, &mut self.contrib, randv);
163            }
164
165            // Get a random normalized value in the range `[-1.0, 1.0]`.
166            let r = self.accum as f32 * (1.0 / 2_147_483_648.0);
167
168            *s = r * self.gain.next_smoothed();
169        }
170
171        ProcessStatus::OutputsModified
172    }
173}
174
175#[inline(always)]
176fn rng(fpd: &mut i32) -> i32 {
177    *fpd ^= *fpd << 13;
178    *fpd ^= *fpd >> 17;
179    *fpd ^= *fpd << 5;
180
181    *fpd
182}
183
184#[inline(always)]
185fn update_contrib<const I: usize>(accum: &mut i32, contrib: &mut [i32; 5], randv: i32) {
186    *accum = accum.wrapping_sub(contrib[I]);
187    contrib[I] = randv * COEFF_A[I];
188    *accum = accum.wrapping_add(contrib[I]);
189}