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];
#[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 {
pub volume: Volume,
pub smooth_seconds: f32,
}
impl Default for PinkNoiseGenNode {
fn default() -> Self {
Self {
volume: Volume::Linear(0.4),
smooth_seconds: DEFAULT_SMOOTH_SECONDS,
}
}
}
#[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 {
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> {
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,
})
}
}
struct Processor {
params: PinkNoiseGenNode,
gain: SmoothedParam,
fpd: i32,
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() {
let randu: i16 = (rng(&mut self.fpd) & 0x7fff) as i16;
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);
}
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]);
}