1use rff_core::{AudioFrame, Error, Result, SampleFormat};
19
20enum AFilter {
21 Volume(f32),
22 ATrim { start: Option<f64>, end: Option<f64> },
23}
24
25#[derive(Default)]
27pub struct AudioFilterChain {
28 filters: Vec<AFilter>,
29 resample_to: Option<u32>,
30}
31
32fn parse_atrim(args: &[&str]) -> Result<AFilter> {
34 let (mut start, mut end, mut duration) = (None, None, None);
35 let mut positional = 0usize;
36 for a in args {
37 let a = a.trim();
38 if a.is_empty() {
39 continue;
40 }
41 let (key, value) = match a.split_once('=') {
42 Some((k, v)) => (k.trim(), v.trim()),
43 None => {
44 let key = match positional {
45 0 => "start",
46 _ => "end",
47 };
48 positional += 1;
49 (key, a)
50 }
51 };
52 let v: f64 = value
53 .parse()
54 .map_err(|_| Error::Option(format!("atrim: bad number `{value}`")))?;
55 match key {
56 "start" => start = Some(v),
57 "end" => end = Some(v),
58 "duration" => duration = Some(v),
59 other => return Err(Error::Option(format!("atrim: unknown key `{other}`"))),
60 }
61 }
62 if let (None, Some(d)) = (end, duration) {
63 end = Some(start.unwrap_or(0.0) + d);
64 }
65 Ok(AFilter::ATrim { start, end })
66}
67
68fn parse_volume(value: &str) -> Result<f32> {
70 let value = value.trim();
71 if let Some(db) = value
72 .strip_suffix("dB")
73 .or_else(|| value.strip_suffix("db"))
74 {
75 let db: f32 = db
76 .trim()
77 .parse()
78 .map_err(|_| Error::Option(format!("volume: bad dB value `{value}`")))?;
79 return Ok(10f32.powf(db / 20.0));
80 }
81 value
82 .parse()
83 .map_err(|_| Error::Option(format!("volume: bad value `{value}`")))
84}
85
86impl AudioFilterChain {
87 pub fn parse(spec: &str) -> Result<AudioFilterChain> {
89 let mut chain = AudioFilterChain::default();
90 for token in spec.split(',') {
91 let token = token.trim();
92 if token.is_empty() {
93 continue;
94 }
95 let (name, args) = token.split_once('=').unwrap_or((token, ""));
96 let parts: Vec<&str> = if args.is_empty() {
97 Vec::new()
98 } else {
99 args.split(':').collect()
100 };
101 match name {
102 "volume" => {
103 let v = parts
104 .first()
105 .ok_or_else(|| Error::Option("volume: missing value".into()))?;
106 chain.filters.push(AFilter::Volume(parse_volume(v)?));
107 }
108 "atrim" => chain.filters.push(parse_atrim(&parts)?),
109 "aresample" => {
110 let v = parts
112 .first()
113 .map(|p| p.trim_start_matches("osr=").trim())
114 .ok_or_else(|| Error::Option("aresample: missing rate".into()))?;
115 let rate: u32 = v
116 .parse()
117 .map_err(|_| Error::Option(format!("aresample: bad rate `{v}`")))?;
118 chain.resample_to = Some(rate);
119 }
120 "anull" => {}
121 other => {
122 return Err(Error::unsupported(format!(
123 "unknown audio filter `{other}` (have: volume, atrim, aresample, anull)"
124 )))
125 }
126 }
127 }
128 Ok(chain)
129 }
130
131 pub fn is_empty(&self) -> bool {
134 self.filters.is_empty()
135 }
136
137 pub fn resample_target(&self) -> Option<u32> {
139 self.resample_to
140 }
141
142 pub fn apply(&mut self, mut frame: AudioFrame, t: Option<f64>) -> Result<Option<AudioFrame>> {
145 for f in &self.filters {
146 frame = match f {
147 AFilter::Volume(gain) => {
148 apply_volume(&mut frame, *gain)?;
149 frame
150 }
151 AFilter::ATrim { start, end } => match apply_atrim(frame, t, *start, *end)? {
152 Some(f) => f,
153 None => return Ok(None),
154 },
155 };
156 }
157 Ok(Some(frame))
158 }
159}
160
161fn apply_volume(frame: &mut AudioFrame, gain: f32) -> Result<()> {
162 match frame.format {
163 SampleFormat::F32 => {
164 for chunk in frame.planes[0].chunks_exact_mut(4) {
165 let v = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) * gain;
166 chunk.copy_from_slice(&v.to_le_bytes());
167 }
168 Ok(())
169 }
170 SampleFormat::S16 => {
171 for chunk in frame.planes[0].chunks_exact_mut(2) {
172 let v = i16::from_le_bytes([chunk[0], chunk[1]]) as f32 * gain;
173 let v = v.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
174 chunk.copy_from_slice(&v.to_le_bytes());
175 }
176 Ok(())
177 }
178 other => Err(Error::unsupported(format!(
179 "volume: sample format `{}` (interleaved s16/f32 only)",
180 other.name()
181 ))),
182 }
183}
184
185pub fn trim_audio_frame(
191 frame: AudioFrame,
192 t: Option<f64>,
193 start: Option<f64>,
194 end: Option<f64>,
195) -> Result<Option<AudioFrame>> {
196 apply_atrim(frame, t, start, end)
197}
198
199fn apply_atrim(
200 frame: AudioFrame,
201 t: Option<f64>,
202 start: Option<f64>,
203 end: Option<f64>,
204) -> Result<Option<AudioFrame>> {
205 let t = t.ok_or_else(|| Error::unsupported("atrim: input has no timestamps"))?;
206 let rate = frame.sample_rate.max(1) as f64;
207 let n = frame.samples;
208 let s0 = match start {
209 Some(s) if s > t => (((s - t) * rate).round() as usize).min(n),
210 _ => 0,
211 };
212 let s1 = match end {
213 Some(e) => {
214 let upto = ((e - t) * rate).round();
215 if upto <= 0.0 {
216 0
217 } else {
218 (upto as usize).min(n)
219 }
220 }
221 None => n,
222 };
223 if s1 <= s0 {
224 return Ok(None);
225 }
226 if s0 == 0 && s1 == n {
227 return Ok(Some(frame));
228 }
229 let bytes_per = frame.format.bytes_per_sample() * frame.channels.max(1) as usize;
230 if frame.format == SampleFormat::F32Planar {
231 return Err(Error::unsupported("atrim: planar audio not supported"));
232 }
233 let plane = &frame.planes[0][s0 * bytes_per..s1 * bytes_per];
234 Ok(Some(AudioFrame {
235 sample_rate: frame.sample_rate,
236 channels: frame.channels,
237 format: frame.format,
238 samples: s1 - s0,
239 planes: vec![plane.to_vec()],
240 pts: frame.pts, }))
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 fn s16_frame(samples: &[i16], rate: u32, channels: u16) -> AudioFrame {
249 AudioFrame {
250 sample_rate: rate,
251 channels,
252 format: SampleFormat::S16,
253 samples: samples.len() / channels.max(1) as usize,
254 planes: vec![samples.iter().flat_map(|s| s.to_le_bytes()).collect()],
255 pts: Some(0),
256 }
257 }
258
259 #[test]
260 fn volume_scales_and_clamps_s16() {
261 let mut chain = AudioFilterChain::parse("volume=2.0").unwrap();
262 let f = s16_frame(&[100, -100, 30000], 48_000, 1);
263 let out = chain.apply(f, Some(0.0)).unwrap().unwrap();
264 let got: Vec<i16> = out.planes[0]
265 .chunks_exact(2)
266 .map(|b| i16::from_le_bytes([b[0], b[1]]))
267 .collect();
268 assert_eq!(got, vec![200, -200, 32767]); }
270
271 #[test]
272 fn volume_parses_decibels() {
273 let g = parse_volume("-6dB").unwrap();
274 assert!((g - 0.501).abs() < 0.01, "got {g}");
275 assert_eq!(parse_volume("0dB").unwrap(), 1.0);
276 }
277
278 #[test]
279 fn atrim_slices_sample_accurately() {
280 let samples: Vec<i16> = (0..1000).map(|i| i as i16).collect();
282 let mut chain = AudioFilterChain::parse("atrim=start=1.25:end=1.5").unwrap();
283 let out = chain
284 .apply(s16_frame(&samples, 1000, 1), Some(1.0))
285 .unwrap()
286 .unwrap();
287 assert_eq!(out.samples, 250);
288 let first = i16::from_le_bytes([out.planes[0][0], out.planes[0][1]]);
289 assert_eq!(first, 250);
290 }
291
292 #[test]
293 fn atrim_consumes_frames_outside_the_window() {
294 let mut chain = AudioFilterChain::parse("atrim=end=0.5").unwrap();
295 let out = chain
296 .apply(s16_frame(&[1, 2, 3, 4], 8000, 1), Some(2.0))
297 .unwrap();
298 assert!(out.is_none());
299 }
300
301 #[test]
302 fn aresample_is_surfaced_not_applied() {
303 let chain = AudioFilterChain::parse("aresample=44100").unwrap();
304 assert_eq!(chain.resample_target(), Some(44_100));
305 assert!(chain.is_empty()); }
307
308 #[test]
309 fn unknown_filters_error() {
310 assert!(AudioFilterChain::parse("acompressor").is_err());
311 }
312}