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