Skip to content

Commit 2c9e6b9

Browse files
authored
feat: add audio peak limiting with configurable settings (#751)
Provides a new LimitSettings API and limiter filter to prevent audio peaks from exceeding a threshold. Supports soft-knee limiting, per-channel detection, configurable attack/release, and efficient processing for mono, stereo, and multi-channel audio. Includes comprehensive tests and usage examples.
1 parent 9900e6e commit 2c9e6b9

File tree

10 files changed

+1816
-64
lines changed

10 files changed

+1816
-64
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3131
- Added `OutputStreamConfig::channel_count()`, `OutputStreamConfig::sample_rate()`,
3232
`OutputStreamConfig::buffer_size()` and `OutputStreamConfig::sample_format()` getters to access
3333
an `OutputStreamConfig`'s channel count, sample rate, buffer size and sample format values.
34+
- Added `Source::limit()` method for limiting the maximum amplitude of a source.
3435

3536
### Changed
3637
- Breaking: `OutputStreamBuilder` should now be used to initialize an audio output stream.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ required-features = ["playback"]
132132
name = "into_file"
133133
required-features = ["mp3", "wav"]
134134

135+
[[example]]
136+
name = "limit_wav"
137+
required-features = ["playback", "wav"]
138+
135139
[[example]]
136140
name = "low_pass"
137141
required-features = ["playback", "wav"]

examples/limit_settings.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//! Example demonstrating the new LimitSettings API for audio limiting.
2+
//!
3+
//! This example shows how to use the LimitSettings struct with the builder
4+
//! to configure audio limiting parameters.
5+
6+
use rodio::source::{LimitSettings, SineWave, Source};
7+
use std::time::Duration;
8+
9+
fn main() {
10+
println!("Example 1: Default LimitSettings");
11+
let default_limiting = LimitSettings::default();
12+
println!(" Threshold: {} dB", default_limiting.threshold);
13+
println!(" Knee width: {} dB", default_limiting.knee_width);
14+
println!(" Attack: {:?}", default_limiting.attack);
15+
println!(" Release: {:?}", default_limiting.release);
16+
println!();
17+
18+
println!("Example 2: Custom LimitSettings with builder pattern");
19+
let custom_limiting = LimitSettings::new()
20+
.with_threshold(-3.0)
21+
.with_knee_width(2.0)
22+
.with_attack(Duration::from_millis(10))
23+
.with_release(Duration::from_millis(50));
24+
25+
println!(" Threshold: {} dB", custom_limiting.threshold);
26+
println!(" Knee width: {} dB", custom_limiting.knee_width);
27+
println!(" Attack: {:?}", custom_limiting.attack);
28+
println!(" Release: {:?}", custom_limiting.release);
29+
println!();
30+
31+
println!("Example 3: Applying limiter to a sine wave with default settings");
32+
33+
// Create a sine wave at 440 Hz
34+
let sine_wave = SineWave::new(440.0)
35+
.amplify(2.0) // Amplify to cause limiting
36+
.take_duration(Duration::from_millis(100));
37+
38+
// Apply limiting with default settings (simplest usage)
39+
let limited_wave = sine_wave.limit(LimitSettings::default());
40+
41+
// Collect some samples to demonstrate
42+
let samples: Vec<f32> = limited_wave.take(100).collect();
43+
println!(" Generated {} limited samples", samples.len());
44+
45+
// Show peak reduction
46+
let max_sample = samples.iter().fold(0.0f32, |acc, &x| acc.max(x.abs()));
47+
println!(" Peak amplitude after limiting: {:.3}", max_sample);
48+
println!();
49+
50+
println!("Example 4: Custom settings with builder pattern");
51+
52+
// Create another sine wave for custom limiting
53+
let sine_wave2 = SineWave::new(880.0)
54+
.amplify(1.8)
55+
.take_duration(Duration::from_millis(50));
56+
57+
// Apply the custom settings from Example 2
58+
let custom_limited = sine_wave2.limit(custom_limiting);
59+
let custom_samples: Vec<f32> = custom_limited.take(50).collect();
60+
println!(
61+
" Generated {} samples with custom settings",
62+
custom_samples.len()
63+
);
64+
println!();
65+
66+
println!("Example 5: Comparing different limiting scenarios");
67+
68+
let gentle_limiting = LimitSettings::default()
69+
.with_threshold(-6.0) // Higher threshold (less limiting)
70+
.with_knee_width(8.0) // Wide knee (softer)
71+
.with_attack(Duration::from_millis(20)) // Slower attack
72+
.with_release(Duration::from_millis(200)); // Slower release
73+
74+
let aggressive_limiting = LimitSettings::default()
75+
.with_threshold(-1.0) // Lower threshold (more limiting)
76+
.with_knee_width(1.0) // Narrow knee (harder)
77+
.with_attack(Duration::from_millis(2)) // Fast attack
78+
.with_release(Duration::from_millis(20)); // Fast release
79+
80+
println!(" Gentle limiting:");
81+
println!(
82+
" Threshold: {} dB, Knee: {} dB",
83+
gentle_limiting.threshold, gentle_limiting.knee_width
84+
);
85+
println!(
86+
" Attack: {:?}, Release: {:?}",
87+
gentle_limiting.attack, gentle_limiting.release
88+
);
89+
90+
println!(" Aggressive limiting:");
91+
println!(
92+
" Threshold: {} dB, Knee: {} dB",
93+
aggressive_limiting.threshold, aggressive_limiting.knee_width
94+
);
95+
println!(
96+
" Attack: {:?}, Release: {:?}",
97+
aggressive_limiting.attack, aggressive_limiting.release
98+
);
99+
println!();
100+
101+
println!("Example 6: Limiting with -6dB threshold");
102+
103+
// Create a sine wave that will definitely trigger limiting
104+
const AMPLITUDE: f32 = 2.5; // High amplitude to ensure limiting occurs
105+
let test_sine = SineWave::new(440.0)
106+
.amplify(AMPLITUDE)
107+
.take_duration(Duration::from_millis(100)); // 100ms = ~4410 samples
108+
109+
// Apply limiting with -6dB threshold (should limit to ~0.5)
110+
let strict_limiting = LimitSettings::default()
111+
.with_threshold(-6.0)
112+
.with_knee_width(0.5) // Narrow knee for precise limiting
113+
.with_attack(Duration::from_millis(3)) // Fast attack
114+
.with_release(Duration::from_millis(12)); // Moderate release
115+
116+
let limited_sine = test_sine.limit(strict_limiting.clone());
117+
let test_samples: Vec<f32> = limited_sine.take(4410).collect();
118+
119+
// Analyze peaks at different time periods
120+
let early_peak = test_samples[0..500]
121+
.iter()
122+
.fold(0.0f32, |acc, &x| acc.max(x.abs()));
123+
let mid_peak = test_samples[1000..1500]
124+
.iter()
125+
.fold(0.0f32, |acc, &x| acc.max(x.abs()));
126+
let settled_peak = test_samples[2000..]
127+
.iter()
128+
.fold(0.0f32, |acc, &x| acc.max(x.abs()));
129+
130+
// With -6dB threshold, ALL samples are well below 1.0!
131+
let target_linear = 10.0_f32.powf(strict_limiting.threshold / 20.0);
132+
let max_settled = test_samples[2000..]
133+
.iter()
134+
.fold(0.0f32, |acc, &x| acc.max(x.abs()));
135+
136+
println!(
137+
" {}dB threshold limiting results:",
138+
strict_limiting.threshold
139+
);
140+
println!(" Original max amplitude: {AMPLITUDE}");
141+
println!(" Target threshold: {:.3}", target_linear);
142+
println!(" Early peak (0-500 samples): {:.3}", early_peak);
143+
println!(" Mid peak (1000-1500 samples): {:.3}", mid_peak);
144+
println!(" Settled peak (2000+ samples): {:.3}", settled_peak);
145+
println!(
146+
" ALL samples now well below 1.0: max = {:.3}",
147+
max_settled
148+
);
149+
}

examples/limit_wav.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use rodio::{source::LimitSettings, Source};
2+
use std::error::Error;
3+
4+
fn main() -> Result<(), Box<dyn Error>> {
5+
let stream_handle = rodio::OutputStreamBuilder::open_default_stream()?;
6+
let sink = rodio::Sink::connect_new(stream_handle.mixer());
7+
8+
let file = std::fs::File::open("assets/music.wav")?;
9+
let source = rodio::Decoder::try_from(file)?
10+
.amplify(3.0)
11+
.limit(LimitSettings::default());
12+
13+
sink.append(source);
14+
15+
println!("Playing music.wav with limiting until finished...");
16+
sink.sleep_until_end();
17+
println!("Done.");
18+
19+
Ok(())
20+
}

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,6 @@ pub use cpal::{
167167
};
168168

169169
mod common;
170-
mod math;
171170
mod sink;
172171
mod spatial_sink;
173172
#[cfg(feature = "playback")]
@@ -178,6 +177,7 @@ mod wav_output;
178177
pub mod buffer;
179178
pub mod conversions;
180179
pub mod decoder;
180+
pub mod math;
181181
pub mod mixer;
182182
pub mod queue;
183183
pub mod source;

0 commit comments

Comments
 (0)