1#![forbid(unsafe_code)]
14
15use rff_codec::{Codec, CodecRegistry, Decoder, Encoder};
16use rff_core::{
17 AudioFrame, CodecId, Dictionary, Error, Frame, MediaType, Packet, Result, SampleFormat,
18};
19
20pub use rusty_mp3;
22
23#[cfg(feature = "prometheus-telemetry")]
34pub use rusty_mp3::prometheus_telemetry;
35
36pub fn register(registry: &mut CodecRegistry) {
38 registry.register(Codec {
39 id: CodecId::Mp3,
40 name: "mp3",
41 long_name: "MP3 (MPEG-1/2 Audio Layer III)",
42 media_type: MediaType::Audio,
43 decoder: Some(|| Box::new(Mp3Decoder::default())),
44 encoder: Some(|| Box::new(Mp3Encoder::default())),
45 });
46}
47
48fn map_err(e: rusty_mp3::Error) -> Error {
52 match e {
53 rusty_mp3::Error::Unimplemented(what) => Error::Unimplemented(what),
54 rusty_mp3::Error::Eof => Error::Eof,
55 rusty_mp3::Error::Again => Error::Again,
56 rusty_mp3::Error::InvalidData(msg) => Error::InvalidData(msg),
57 rusty_mp3::Error::Unsupported(msg) => Error::Unsupported(msg),
58 other => Error::InvalidData(format!("rusty_mp3: {other}")),
61 }
62}
63
64#[derive(Default)]
65struct Mp3Decoder {
66 inner: rusty_mp3::Mp3Decoder,
67}
68
69impl Decoder for Mp3Decoder {
70 fn send_packet(&mut self, packet: &Packet) -> Result<()> {
71 self.inner.push(&packet.data);
72 Ok(())
73 }
74
75 fn receive_frame(&mut self) -> Result<Frame> {
76 let audio = self.inner.next_frame().map_err(map_err)?;
77 let channels = audio.channels.max(1);
78 let mut bytes = vec![0u8; audio.samples.len() * 4];
83 for (d, s) in bytes.chunks_exact_mut(4).zip(&audio.samples) {
84 d.copy_from_slice(&s.to_le_bytes());
85 }
86 Ok(Frame::Audio(AudioFrame {
87 sample_rate: audio.sample_rate,
88 channels,
89 format: SampleFormat::F32,
90 samples: audio.samples.len() / channels as usize,
91 planes: vec![bytes],
92 pts: None,
93 }))
94 }
95
96 fn flush(&mut self) {
97 self.inner.flush();
98 }
99}
100
101#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
108fn parse_bitrate(s: &str) -> Option<u32> {
109 let s = s.trim();
110 if let Some(n) = s.strip_suffix(['k', 'K']) {
111 n.trim().parse::<f32>().ok().map(|x| x as u32)
112 } else if let Some(n) = s.strip_suffix(['m', 'M']) {
113 n.trim().parse::<f32>().ok().map(|x| (x * 1000.0) as u32)
114 } else {
115 s.parse::<u32>()
116 .ok()
117 .map(|x| if x >= 1000 { x / 1000 } else { x })
118 }
119}
120
121#[derive(Default)]
125struct Mp3Encoder {
126 inner: Option<rusty_mp3::Mp3Encoder>,
127 cbr_kbps: u32,
129 quality: Option<f32>,
131 eof: bool,
132 streamed: bool,
135 tag_sent: bool,
137}
138
139impl Mp3Encoder {
140 fn inner(&mut self) -> &mut rusty_mp3::Mp3Encoder {
141 if self.inner.is_none() {
142 self.inner = Some(rusty_mp3::Mp3Encoder::new(rusty_mp3::Mp3EncoderConfig {
143 bitrate_kbps: self.cbr_kbps,
144 vbr_quality: self.quality,
145 }));
146 }
147 self.inner.as_mut().unwrap()
149 }
150}
151
152impl Encoder for Mp3Encoder {
153 fn configure(&mut self, options: &Dictionary) -> Result<()> {
154 if let Some(b) = options.get("b").and_then(parse_bitrate) {
155 self.cbr_kbps = b;
156 }
157 for key in ["q", "qp", "crf", "qscale"] {
160 if let Some(q) = options.get(key).and_then(|v| v.trim().parse::<f32>().ok()) {
161 self.quality = Some(rusty_mp3::vbr_quality_index(q));
162 break;
163 }
164 }
165 Ok(())
166 }
167
168 fn send_frame(&mut self, frame: &Frame) -> Result<()> {
169 let Frame::Audio(af) = frame else {
170 return Ok(());
171 };
172 let in_ch = af.channels.max(1);
173 let bps = match af.format {
179 SampleFormat::F32 => 4,
180 SampleFormat::S16 => 2,
181 other => {
182 return Err(Error::unsupported(format!(
183 "mp3 encode: sample format `{}` (need interleaved s16/f32)",
184 other.name()
185 )))
186 }
187 };
188 let Some(data) = af.planes.first() else {
200 return Err(Error::invalid(
201 "mp3 encode: audio frame has no sample plane",
202 ));
203 };
204 let frame_bytes = usize::from(in_ch) * bps;
205 let frames = af.samples.min(data.len() / frame_bytes);
206 let plane = &data[..frames * frame_bytes];
207 let sr = af.sample_rate;
208 let inner = self.inner();
209 match bps {
210 4 => inner.push_pcm_f32le(plane, in_ch, sr),
211 _ => inner.push_pcm_s16le(plane, in_ch, sr),
212 }
213 .map_err(map_err)
214 }
215
216 fn receive_packet(&mut self) -> Result<Packet> {
217 match self.inner.as_mut() {
227 Some(inner) => match inner.next_packet() {
228 Ok(bytes) => {
229 self.streamed |= !self.eof;
230 Ok(Packet::from_data(0, bytes))
231 }
232 Err(rusty_mp3::Error::Eof) if self.streamed && !self.tag_sent => {
233 self.tag_sent = true;
234 match inner.lame_tag_frame() {
235 Some(tag) => Ok(Packet::from_data(0, tag.to_vec())),
236 None => Err(Error::Eof),
237 }
238 }
239 Err(e) => Err(map_err(e)),
240 },
241 None if self.eof => Err(Error::Eof),
242 None => Err(Error::Again),
243 }
244 }
245
246 fn flush(&mut self) {
247 if let Some(inner) = self.inner.as_mut() {
248 inner.finish();
249 }
250 self.eof = true;
251 }
252}
253
254#[cfg(test)]
255#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] mod tests {
257 use super::*;
258
259 fn encode_mono_s16(input: &[i16], sample_rate: u32) -> Vec<u8> {
262 let bytes: Vec<u8> = input.iter().flat_map(|s| s.to_le_bytes()).collect();
263 let mut enc = Mp3Encoder::default();
264 enc.send_frame(&Frame::Audio(AudioFrame {
265 sample_rate,
266 channels: 1,
267 format: SampleFormat::S16,
268 planes: vec![bytes],
269 samples: input.len(),
270 pts: None,
271 }))
272 .unwrap();
273 enc.flush();
274 let mut mp3 = Vec::new();
275 while let Ok(p) = enc.receive_packet() {
276 mp3.extend_from_slice(&p.data);
277 }
278 mp3
279 }
280
281 fn decode_mono(mp3: Vec<u8>) -> Vec<f32> {
282 let mut dec = Mp3Decoder::default();
283 dec.send_packet(&Packet::from_data(0, mp3)).unwrap();
284 dec.flush();
285 let mut out = Vec::new();
286 while let Ok(Frame::Audio(af)) = dec.receive_frame() {
287 for c in af.planes[0].chunks_exact(4) {
288 out.push(f32::from_le_bytes([c[0], c[1], c[2], c[3]]));
289 }
290 }
291 out
292 }
293
294 #[test]
300 #[cfg_attr(miri, ignore = "runs real encodes: too slow to interpret under Miri")]
301 fn s16_input_encodes_to_audible_output() {
302 let sr = 44100u32;
303 let n = sr as usize; let s16: Vec<i16> = (0..n)
305 .map(|i| {
306 let t = i as f32 / sr as f32;
307 (0.5 * (2.0 * std::f32::consts::PI * 440.0 * t).sin() * 32767.0) as i16
308 })
309 .collect();
310 let out = decode_mono(encode_mono_s16(&s16, sr));
311 let rms = (out.iter().map(|x| x * x).sum::<f32>() / out.len().max(1) as f32).sqrt();
312 assert!(
313 rms > 0.1,
314 "S16 input produced near-silent output (rms={rms}); encoder ignored af.format"
315 );
316 }
317
318 #[test]
322 #[cfg_attr(miri, ignore = "runs real encodes: too slow to interpret under Miri")]
323 fn hostile_frame_shapes_are_errors_or_bounded() {
324 let mut enc = Mp3Encoder::default();
325 let empty = Frame::Audio(AudioFrame {
326 sample_rate: 44_100,
327 channels: 2,
328 format: SampleFormat::S16,
329 planes: vec![],
330 samples: 1152,
331 pts: None,
332 });
333 assert!(enc.send_frame(&empty).is_err());
334
335 let claim = |samples: usize| {
336 let mut enc = Mp3Encoder::default();
337 enc.send_frame(&Frame::Audio(AudioFrame {
338 sample_rate: 44_100,
339 channels: 1,
340 format: SampleFormat::S16,
341 planes: vec![vec![0x11; 2 * 4608]],
342 samples,
343 pts: None,
344 }))
345 .unwrap();
346 enc.flush();
347 let mut n = 0;
348 while let Ok(p) = enc.receive_packet() {
349 n += p.data.len();
350 }
351 n
352 };
353 assert_eq!(claim(1 << 40), claim(4608));
356 }
357
358 #[test]
362 #[cfg_attr(miri, ignore = "runs real encodes: too slow to interpret under Miri")]
363 fn random_frame_shapes_never_panic() {
364 let mut s = 0x9E37_79B9_7F4A_7C15u64;
365 let mut rnd = move || {
366 s ^= s << 13;
367 s ^= s >> 7;
368 s ^= s << 17;
369 s
370 };
371 let formats = [
372 SampleFormat::S16,
373 SampleFormat::F32,
374 SampleFormat::F32Planar,
375 ];
376 let rates = [44_100u32, 22_050, 8_000, 48_000, 0, 7, 96_000];
377 for _ in 0..300 {
378 let mut enc = Mp3Encoder::default();
379 let plane: Vec<u8> = (0..(rnd() % 20_000)).map(|_| rnd() as u8).collect();
380 let frame = Frame::Audio(AudioFrame {
381 sample_rate: rates[(rnd() % 7) as usize],
382 channels: (rnd() % 9) as u16,
383 format: formats[(rnd() % 3) as usize],
384 planes: if rnd() % 10 == 0 { vec![] } else { vec![plane] },
385 samples: if rnd() % 3 == 0 {
386 (rnd() % (1 << 40)) as usize
387 } else {
388 (rnd() % 6000) as usize
389 },
390 pts: None,
391 });
392 let _ = enc.send_frame(&frame);
393 enc.flush();
394 while enc.receive_packet().is_ok() {}
395 }
396 }
397
398 #[test]
401 fn bitrate_options_parse_or_saturate() {
402 assert_eq!(parse_bitrate("128k"), Some(128));
403 assert_eq!(parse_bitrate("192000"), Some(192));
404 assert_eq!(parse_bitrate("0.32M"), Some(320));
405 assert_eq!(parse_bitrate("-5k"), Some(0));
406 assert_eq!(parse_bitrate("nank"), Some(0));
407 assert_eq!(parse_bitrate("1e30k"), Some(u32::MAX));
408 for s in ["", "k", "M", "abc", " 12 K ", "99999999999999999999", "∞k"] {
409 let _ = parse_bitrate(s);
410 }
411 }
412
413 #[test]
416 #[cfg_attr(miri, ignore = "runs real encodes: too slow to interpret under Miri")]
417 fn adapter_decode_matches_rusty_mp3() {
418 let sr = 44_100u32;
419 let s16: Vec<i16> = (0..sr as usize)
420 .map(|i| ((i as f32 * 0.07).sin() * 9000.0) as i16)
421 .collect();
422 let base = encode_mono_s16(&s16, sr);
423 let mut seed = 0x2545_F491_4F6C_DD1Du64;
424 for case in 0..20 {
425 let mut mp3 = base.clone();
426 for _ in 0..case {
427 seed ^= seed << 13;
428 seed ^= seed >> 7;
429 seed ^= seed << 17;
430 let i = (seed % mp3.len() as u64) as usize;
431 mp3[i] ^= 1 << (seed % 8);
432 }
433 let ours = decode_mono(mp3.clone());
434 let mut reference = rusty_mp3::Mp3Decoder::new();
435 reference.push(&mp3);
436 reference.flush();
437 let mut want = Vec::new();
438 while let Ok(f) = reference.next_frame() {
439 want.extend_from_slice(&f.samples);
440 }
441 assert_eq!(ours.len(), want.len(), "case {case}");
442 assert!(
443 ours.iter()
444 .zip(&want)
445 .all(|(a, b)| a.to_bits() == b.to_bits()),
446 "case {case}"
447 );
448 }
449 }
450
451 fn streamed_packets(options: &[(&str, &str)], frames: usize) -> Vec<Vec<u8>> {
454 let mut enc = Mp3Encoder::default();
455 let mut dict = Dictionary::new();
456 for (k, v) in options {
457 dict.set(*k, *v);
458 }
459 enc.configure(&dict).unwrap();
460 let s16: Vec<u8> = (0..1152 * 2)
461 .flat_map(|i| (((i as f32 * 0.05).sin() * 8000.0) as i16).to_le_bytes())
462 .collect();
463 let mut out = Vec::new();
464 for _ in 0..frames {
465 enc.send_frame(&Frame::Audio(AudioFrame {
466 sample_rate: 44_100,
467 channels: 1,
468 format: SampleFormat::S16,
469 planes: vec![s16.clone()],
470 samples: 1152 * 2,
471 pts: None,
472 }))
473 .unwrap();
474 while let Ok(p) = enc.receive_packet() {
475 out.push(p.data);
476 }
477 }
478 enc.flush();
479 while let Ok(p) = enc.receive_packet() {
480 out.push(p.data);
481 }
482 out
483 }
484
485 fn is_tag(frame: &[u8]) -> bool {
486 frame.len() > 25 && matches!(&frame[21..25], b"Info" | b"Xing")
488 }
489
490 #[test]
496 #[cfg_attr(miri, ignore = "runs real encodes: too slow to interpret under Miri")]
497 fn streamed_encode_ends_with_the_completed_tag() {
498 let vbr = streamed_packets(&[("q", "2")], 4);
499 let (first, last) = (&vbr[0], &vbr[vbr.len() - 1]);
500 assert!(is_tag(first) && is_tag(last));
501 assert_eq!(first.len(), last.len());
502 assert_ne!(first, last, "the placeholder's counts are filled in");
503 assert_eq!(vbr.iter().filter(|p| is_tag(p)).count(), 2);
504
505 let cbr = streamed_packets(&[("b", "128k")], 4);
506 assert!(is_tag(&cbr[0]));
507 assert_eq!(cbr.iter().filter(|p| is_tag(p)).count(), 1);
508 }
509
510 #[test]
511 fn registers_as_audio_codec() {
512 let mut reg = CodecRegistry::new();
513 register(&mut reg);
514 let codec = reg.by_name("mp3").expect("mp3 registered");
515 assert_eq!(codec.id, CodecId::Mp3);
516 assert_eq!(codec.media_type, MediaType::Audio);
517 assert!(codec.can_decode() && codec.can_encode());
518 }
519}