use rff_core::{AudioFrame, Error, Result, SampleFormat};
enum AFilter {
Volume(f32),
ATrim { start: Option<f64>, end: Option<f64> },
}
#[derive(Default)]
pub struct AudioFilterChain {
filters: Vec<AFilter>,
resample_to: Option<u32>,
}
fn parse_atrim(args: &[&str]) -> Result<AFilter> {
let (mut start, mut end, mut duration) = (None, None, None);
let mut positional = 0usize;
for a in args {
let a = a.trim();
if a.is_empty() {
continue;
}
let (key, value) = match a.split_once('=') {
Some((k, v)) => (k.trim(), v.trim()),
None => {
let key = match positional {
0 => "start",
_ => "end",
};
positional += 1;
(key, a)
}
};
let v: f64 = value
.parse()
.map_err(|_| Error::Option(format!("atrim: bad number `{value}`")))?;
match key {
"start" => start = Some(v),
"end" => end = Some(v),
"duration" => duration = Some(v),
other => return Err(Error::Option(format!("atrim: unknown key `{other}`"))),
}
}
if let (None, Some(d)) = (end, duration) {
end = Some(start.unwrap_or(0.0) + d);
}
Ok(AFilter::ATrim { start, end })
}
fn parse_volume(value: &str) -> Result<f32> {
let value = value.trim();
if let Some(db) = value
.strip_suffix("dB")
.or_else(|| value.strip_suffix("db"))
{
let db: f32 = db
.trim()
.parse()
.map_err(|_| Error::Option(format!("volume: bad dB value `{value}`")))?;
return Ok(10f32.powf(db / 20.0));
}
value
.parse()
.map_err(|_| Error::Option(format!("volume: bad value `{value}`")))
}
impl AudioFilterChain {
pub fn parse(spec: &str) -> Result<AudioFilterChain> {
let mut chain = AudioFilterChain::default();
for token in spec.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
let (name, args) = token.split_once('=').unwrap_or((token, ""));
let parts: Vec<&str> = if args.is_empty() {
Vec::new()
} else {
args.split(':').collect()
};
match name {
"volume" => {
let v = parts
.first()
.ok_or_else(|| Error::Option("volume: missing value".into()))?;
chain.filters.push(AFilter::Volume(parse_volume(v)?));
}
"atrim" => chain.filters.push(parse_atrim(&parts)?),
"aresample" => {
let v = parts
.first()
.map(|p| p.trim_start_matches("osr=").trim())
.ok_or_else(|| Error::Option("aresample: missing rate".into()))?;
let rate: u32 = v
.parse()
.map_err(|_| Error::Option(format!("aresample: bad rate `{v}`")))?;
chain.resample_to = Some(rate);
}
"anull" => {}
other => {
return Err(Error::unsupported(format!(
"unknown audio filter `{other}` (have: volume, atrim, aresample, anull)"
)))
}
}
}
Ok(chain)
}
pub fn is_empty(&self) -> bool {
self.filters.is_empty()
}
pub fn resample_target(&self) -> Option<u32> {
self.resample_to
}
pub fn apply(&mut self, mut frame: AudioFrame, t: Option<f64>) -> Result<Option<AudioFrame>> {
for f in &self.filters {
frame = match f {
AFilter::Volume(gain) => {
apply_volume(&mut frame, *gain)?;
frame
}
AFilter::ATrim { start, end } => match apply_atrim(frame, t, *start, *end)? {
Some(f) => f,
None => return Ok(None),
},
};
}
Ok(Some(frame))
}
}
fn apply_volume(frame: &mut AudioFrame, gain: f32) -> Result<()> {
match frame.format {
SampleFormat::F32 => {
for chunk in frame.planes[0].chunks_exact_mut(4) {
let v = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) * gain;
chunk.copy_from_slice(&v.to_le_bytes());
}
Ok(())
}
SampleFormat::S16 => {
for chunk in frame.planes[0].chunks_exact_mut(2) {
let v = i16::from_le_bytes([chunk[0], chunk[1]]) as f32 * gain;
let v = v.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
chunk.copy_from_slice(&v.to_le_bytes());
}
Ok(())
}
other => Err(Error::unsupported(format!(
"volume: sample format `{}` (interleaved s16/f32 only)",
other.name()
))),
}
}
pub fn trim_audio_frame(
frame: AudioFrame,
t: Option<f64>,
start: Option<f64>,
end: Option<f64>,
) -> Result<Option<AudioFrame>> {
apply_atrim(frame, t, start, end)
}
fn apply_atrim(
frame: AudioFrame,
t: Option<f64>,
start: Option<f64>,
end: Option<f64>,
) -> Result<Option<AudioFrame>> {
let t = t.ok_or_else(|| Error::unsupported("atrim: input has no timestamps"))?;
let rate = frame.sample_rate.max(1) as f64;
let n = frame.samples;
let s0 = match start {
Some(s) if s > t => (((s - t) * rate).round() as usize).min(n),
_ => 0,
};
let s1 = match end {
Some(e) => {
let upto = ((e - t) * rate).round();
if upto <= 0.0 {
0
} else {
(upto as usize).min(n)
}
}
None => n,
};
if s1 <= s0 {
return Ok(None);
}
if s0 == 0 && s1 == n {
return Ok(Some(frame));
}
let bytes_per = frame.format.bytes_per_sample() * frame.channels.max(1) as usize;
if frame.format == SampleFormat::F32Planar {
return Err(Error::unsupported("atrim: planar audio not supported"));
}
let plane = &frame.planes[0][s0 * bytes_per..s1 * bytes_per];
Ok(Some(AudioFrame {
sample_rate: frame.sample_rate,
channels: frame.channels,
format: frame.format,
samples: s1 - s0,
planes: vec![plane.to_vec()],
pts: frame.pts, }))
}
#[cfg(test)]
mod tests {
use super::*;
fn s16_frame(samples: &[i16], rate: u32, channels: u16) -> AudioFrame {
AudioFrame {
sample_rate: rate,
channels,
format: SampleFormat::S16,
samples: samples.len() / channels.max(1) as usize,
planes: vec![samples.iter().flat_map(|s| s.to_le_bytes()).collect()],
pts: Some(0),
}
}
#[test]
fn volume_scales_and_clamps_s16() {
let mut chain = AudioFilterChain::parse("volume=2.0").unwrap();
let f = s16_frame(&[100, -100, 30000], 48_000, 1);
let out = chain.apply(f, Some(0.0)).unwrap().unwrap();
let got: Vec<i16> = out.planes[0]
.chunks_exact(2)
.map(|b| i16::from_le_bytes([b[0], b[1]]))
.collect();
assert_eq!(got, vec![200, -200, 32767]); }
#[test]
fn volume_parses_decibels() {
let g = parse_volume("-6dB").unwrap();
assert!((g - 0.501).abs() < 0.01, "got {g}");
assert_eq!(parse_volume("0dB").unwrap(), 1.0);
}
#[test]
fn atrim_slices_sample_accurately() {
let samples: Vec<i16> = (0..1000).map(|i| i as i16).collect();
let mut chain = AudioFilterChain::parse("atrim=start=1.25:end=1.5").unwrap();
let out = chain
.apply(s16_frame(&samples, 1000, 1), Some(1.0))
.unwrap()
.unwrap();
assert_eq!(out.samples, 250);
let first = i16::from_le_bytes([out.planes[0][0], out.planes[0][1]]);
assert_eq!(first, 250);
}
#[test]
fn atrim_consumes_frames_outside_the_window() {
let mut chain = AudioFilterChain::parse("atrim=end=0.5").unwrap();
let out = chain
.apply(s16_frame(&[1, 2, 3, 4], 8000, 1), Some(2.0))
.unwrap();
assert!(out.is_none());
}
#[test]
fn aresample_is_surfaced_not_applied() {
let chain = AudioFilterChain::parse("aresample=44100").unwrap();
assert_eq!(chain.resample_target(), Some(44_100));
assert!(chain.is_empty()); }
#[test]
fn unknown_filters_error() {
assert!(AudioFilterChain::parse("acompressor").is_err());
}
}