bpm_analyzer/
dsp.rs

1use crossbeam_channel::Receiver;
2use fundsp::prelude32::*;
3
4/// Base ID for custom audio nodes
5const BASE_ID: u64 = 0x1337;
6
7/// Audio input node that receives samples from a crossbeam channel.
8///
9/// This node pulls stereo audio data from a channel receiver and outputs it
10/// as a 2-channel audio stream. It's useful for integrating external audio sources
11/// into a fundsp processing graph.
12#[derive(Clone)]
13pub struct Input<F: Real> {
14    receiver: Receiver<(F, F)>,
15}
16
17impl<F: Real> AudioNode for Input<F> {
18    const ID: u64 = BASE_ID;
19
20    type Inputs = U0;
21
22    type Outputs = U2;
23
24    /// Receives one stereo frame from the channel.
25    /// Returns zeros if the channel is disconnected or empty.
26    #[inline]
27    fn tick(&mut self, _input: &Frame<f32, Self::Inputs>) -> Frame<f32, Self::Outputs> {
28        let (left, right) = self
29            .receiver
30            .recv()
31            .inspect_err(|e| {
32                tracing::error!(error =? e);
33            })
34            .unwrap_or_else(|_| (F::zero(), F::zero()));
35        [convert(left), convert(right)].into()
36    }
37}
38
39/// Creates an audio input node from a channel receiver.
40///
41/// # Arguments
42///
43/// * `receiver` - A channel receiver providing stereo `(left, right)` audio samples
44///
45/// # Returns
46///
47/// A 2-channel audio node that outputs the received samples
48pub fn input<F: Real>(receiver: Receiver<(F, F)>) -> An<Input<F>> {
49    An(Input { receiver })
50}
51
52/// Single-pole low-pass filter with configurable smoothing coefficient.
53///
54/// This filter implements a simple recursive smoothing formula:
55/// `y[n] = (1 - α) * x[n] + α * x[n-1]`
56///
57/// where α (alpha) controls the smoothing:
58/// - α close to 0: minimal smoothing (faster response)
59/// - α close to 1: heavy smoothing (slower response)
60///
61/// Commonly used for envelope following and smoothing onset detection signals.
62#[derive(Clone)]
63pub struct AlphaLpf<F: Real> {
64    /// Smoothing coefficient (0.0 to 1.0)
65    alpha: F,
66    /// Previous input sample
67    previous: F,
68}
69
70impl<F: Real> AudioNode for AlphaLpf<F> {
71    const ID: u64 = BASE_ID + 1;
72
73    type Inputs = U1;
74
75    type Outputs = U1;
76
77    /// Processes one sample through the low-pass filter.
78    #[inline]
79    fn tick(&mut self, input: &Frame<f32, Self::Inputs>) -> Frame<f32, Self::Outputs> {
80        let x = convert(input[0]);
81        let value = (F::one() - self.alpha) * x + self.alpha * self.previous;
82        self.previous = x;
83
84        [convert(value)].into()
85    }
86
87    /// Resets the filter state to zero.
88    fn reset(&mut self) {
89        self.previous = F::zero();
90    }
91}
92
93/// Creates a single-pole low-pass filter.
94///
95/// # Arguments
96///
97/// * `alpha` - Smoothing coefficient (0.0 to 1.0). Higher values = more smoothing.
98///
99/// # Example
100///
101/// ```rust,ignore
102/// // Create a heavily smoothing filter for envelope extraction
103/// let envelope_filter = alpha_lpf(0.99);
104/// ```
105pub fn alpha_lpf<F: Real>(alpha: F) -> An<AlphaLpf<F>> {
106    An(AlphaLpf {
107        alpha,
108        previous: F::zero(),
109    })
110}
111
112/// Full-wave rectifier that outputs the absolute value of the input signal.
113///
114/// This operation is essential for onset detection, as it converts bipolar audio
115/// signals into unipolar envelopes that can be used to detect amplitude changes.
116///
117/// The rectified signal is typically followed by a low-pass filter to extract
118/// the amplitude envelope.
119#[derive(Clone)]
120pub struct FullWaveRectification<F: Real> {
121    _marker: std::marker::PhantomData<F>,
122}
123
124impl<F: Real> AudioNode for FullWaveRectification<F> {
125    const ID: u64 = BASE_ID + 2;
126
127    type Inputs = U1;
128
129    type Outputs = U1;
130
131    /// Computes the absolute value of the input sample.
132    #[inline]
133    fn tick(&mut self, input: &Frame<f32, Self::Inputs>) -> Frame<f32, Self::Outputs> {
134        [convert(input[0].abs())].into()
135    }
136
137    /// SIMD-optimized batch processing of multiple samples.
138    fn process(&mut self, size: usize, input: &BufferRef, output: &mut BufferMut) {
139        (0..simd_items(size)).for_each(|i| {
140            let input = input.channel(0)[i];
141
142            let output = &mut output.channel_mut(0)[i];
143
144            *output = input.abs();
145        });
146    }
147}
148
149/// Creates a full-wave rectifier node.
150///
151/// # Returns
152///
153/// A 1-input, 1-output node that outputs |x| for input x.
154///
155/// # Example
156///
157/// ```rust,ignore
158/// // Create an onset detector: rectify, then smooth
159/// let onset_detector = fwr::<f32>() >> alpha_lpf(0.95);
160/// ```
161pub fn fwr<F: Real>() -> An<FullWaveRectification<F>> {
162    An(FullWaveRectification {
163        _marker: std::marker::PhantomData,
164    })
165}