#![allow(clippy::unwrap_used)]
use ff_filter::{EqBand, FilterGraph};
use ff_format::{AudioFrame, SampleFormat, Timestamp};
fn make_sine_frame(freq_hz: f64, sample_rate: u32, num_samples: usize) -> AudioFrame {
let channels = 2usize;
let bytes_per_sample = 4usize; let mut buf = vec![0u8; num_samples * channels * bytes_per_sample];
for i in 0..num_samples {
let t = i as f64 / sample_rate as f64;
let v = (0.1_f32 * (2.0 * std::f64::consts::PI * freq_hz * t).sin() as f32).to_le_bytes();
let offset = i * channels * bytes_per_sample;
buf[offset..offset + 4].copy_from_slice(&v); buf[offset + 4..offset + 8].copy_from_slice(&v); }
AudioFrame::new(
vec![buf],
num_samples,
2,
sample_rate,
SampleFormat::F32,
Timestamp::default(),
)
.unwrap()
}
fn rms(samples: &[f32]) -> f64 {
if samples.is_empty() {
return 0.0;
}
let sum_sq: f64 = samples.iter().map(|&s| (s as f64).powi(2)).sum();
(sum_sq / samples.len() as f64).sqrt()
}
fn push_pull_audio(graph: &mut FilterGraph, frame: &AudioFrame) -> Option<AudioFrame> {
match graph.push_audio(0, frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping push_audio: {e}");
return None;
}
}
match graph.pull_audio() {
Ok(Some(f)) => Some(f),
Ok(None) => {
println!("Skipping: no audio output frame produced");
None
}
Err(e) => {
println!("Skipping pull_audio: {e}");
None
}
}
}
#[test]
fn volume_6db_should_double_amplitude() {
let mut graph = match FilterGraph::builder().volume(6.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_sine_frame(440.0, 48000, 4800);
let out = match push_pull_audio(&mut graph, &frame) {
Some(f) => f,
None => return,
};
let in_samples = frame.as_f32().unwrap();
let in_rms = rms(in_samples);
let out_rms = if let Some(s) = out.as_f32() {
rms(s)
} else if let Some(s) = out.channel_as_f32(0) {
rms(s)
} else {
println!("Skipping: unrecognised output format {:?}", out.format());
return;
};
let ratio = out_rms / in_rms;
assert!(
(ratio - 2.0).abs() < 0.30,
"+6 dB should double amplitude: expected ratio≈2.0, got {ratio:.3}"
);
}
#[test]
fn volume_minus6db_should_halve_amplitude() {
let mut graph = match FilterGraph::builder().volume(-6.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_sine_frame(440.0, 48000, 4800);
let out = match push_pull_audio(&mut graph, &frame) {
Some(f) => f,
None => return,
};
let in_rms = rms(frame.as_f32().unwrap());
let out_rms = if let Some(s) = out.as_f32() {
rms(s)
} else if let Some(s) = out.channel_as_f32(0) {
rms(s)
} else {
println!("Skipping: unrecognised output format {:?}", out.format());
return;
};
let ratio = out_rms / in_rms;
assert!(
(ratio - 0.5).abs() < 0.10,
"-6 dB should halve amplitude: expected ratio≈0.5, got {ratio:.3}"
);
}
#[test]
fn afade_in_should_start_at_silence_and_reach_full_volume() {
let mut graph = match FilterGraph::builder().afade_in(0.0, 0.1).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_sine_frame(440.0, 48000, 4800);
let out = match push_pull_audio(&mut graph, &frame) {
Some(f) => f,
None => return,
};
let first_sample = if let Some(s) = out.as_f32() {
s[0].abs()
} else if let Some(s) = out.channel_as_f32(0) {
s[0].abs()
} else {
println!("Skipping: unrecognised output format {:?}", out.format());
return;
};
assert!(
first_sample < 0.02,
"afade_in: first sample should be near silence, got {first_sample:.4}"
);
}
#[test]
fn afade_out_should_reach_silence_at_end() {
let mut graph = match FilterGraph::builder().afade_out(0.0, 0.1).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_sine_frame(440.0, 48000, 4800);
let out = match push_pull_audio(&mut graph, &frame) {
Some(f) => f,
None => return,
};
let last_sample = if let Some(s) = out.as_f32() {
s[s.len() - 1].abs()
} else if let Some(s) = out.channel_as_f32(0) {
s[s.len() - 1].abs()
} else {
println!("Skipping: unrecognised output format {:?}", out.format());
return;
};
assert!(
last_sample < 0.02,
"afade_out: last sample should be near silence, got {last_sample:.4}"
);
}
#[test]
fn equalizer_peak_should_boost_target_frequency() {
let bands = vec![EqBand::Peak {
freq_hz: 1000.0,
gain_db: 6.0,
q: 1.0,
}];
let mut graph = match FilterGraph::builder().equalizer(bands).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_sine_frame(1000.0, 48000, 4800);
match push_pull_audio(&mut graph, &frame) {
Some(out) => {
assert_eq!(out.sample_rate(), 48000, "sample rate must be unchanged");
let in_rms = rms(frame.as_f32().unwrap());
let out_rms = if let Some(s) = out.as_f32() {
rms(s)
} else if let Some(s) = out.channel_as_f32(0) {
rms(s)
} else {
return;
};
assert!(
out_rms >= in_rms * 0.9,
"EQ peak at 1 kHz: output RMS ({out_rms:.4}) should not be less than input ({in_rms:.4})"
);
}
None => {}
}
}
#[test]
fn stereo_to_mono_should_average_both_channels() {
let mut graph = match FilterGraph::builder().stereo_to_mono().build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_sine_frame(440.0, 48000, 4800);
let out = match push_pull_audio(&mut graph, &frame) {
Some(f) => f,
None => return,
};
assert_eq!(
out.channels(),
1,
"stereo_to_mono: output must have exactly 1 channel, got {}",
out.channels()
);
assert_eq!(
out.sample_rate(),
48000,
"sample rate must be unchanged after stereo_to_mono"
);
}
#[test]
fn audio_delay_100ms_should_shift_audio_later() {
let mut graph = match FilterGraph::builder().audio_delay(100.0).build() {
Ok(g) => g,
Err(e) => {
println!("Skipping: {e}");
return;
}
};
let frame = make_sine_frame(440.0, 48000, 9600);
match graph.push_audio(0, &frame) {
Ok(()) => {}
Err(e) => {
println!("Skipping push_audio: {e}");
return;
}
}
match graph.pull_audio() {
Ok(Some(out)) => {
assert_eq!(
out.sample_rate(),
48000,
"sample rate must be preserved after delay"
);
if let Some(s) = out.as_f32() {
if !s.is_empty() {
assert!(
s[0].abs() < 0.01,
"audio_delay: first output sample should be silence, got {:.4}",
s[0]
);
}
}
}
Ok(None) => {
println!("Note: audio_delay produced no immediate output (buffering expected).");
}
Err(e) => {
println!("Skipping pull_audio: {e}");
}
}
}