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}