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