bpm_analyzer/
dsp.rs

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