Skip to main content

rff_codec_mp3/
lib.rs

1//! MP3 (MPEG-1/2/2.5 Audio Layer III) codec, backed by the pure-Rust
2//! [`rusty_mp3`] decoder + encoder (formerly in-tree here, now a standalone
3//! publishable crate — the same adapter pattern as `rff-codec-h264` over
4//! `rusty_h264`).
5//!
6//! This crate is the thin rff-facing layer: `register()` wires the codec into
7//! the registry, the wrapper structs implement the rff [`Decoder`]/[`Encoder`]
8//! traits by delegating to [`rusty_mp3::Mp3Decoder`]/[`rusty_mp3::Mp3Encoder`],
9//! and everything ffmpeg-CLI-shaped stays here: `Dictionary` option parsing
10//! (`-b:a`, `-q:a`), `AudioFrame` ↔ PCM conversion honoring `af.format`, and
11//! rusty_mp3 → rff error mapping.
12
13use rff_codec::{Codec, CodecRegistry, Decoder, Encoder};
14use rff_core::{
15    AudioFrame, CodecId, Dictionary, Error, Frame, MediaType, Packet, Result, SampleFormat,
16};
17
18/// The underlying pure-Rust MP3 engine, re-exported for direct use.
19pub use rusty_mp3;
20
21/// Register the MP3 codec (decoder + encoder) into a [`CodecRegistry`].
22pub fn register(registry: &mut CodecRegistry) {
23    registry.register(Codec {
24        id: CodecId::Mp3,
25        name: "mp3",
26        long_name: "MP3 (MPEG-1/2 Audio Layer III)",
27        media_type: MediaType::Audio,
28        decoder: Some(|| Box::new(Mp3Decoder::default())),
29        encoder: Some(|| Box::new(Mp3Encoder::default())),
30    });
31}
32
33/// Map a [`rusty_mp3::Error`] onto the rff error space, variant for variant
34/// (the drain-protocol variants `Again`/`Eof` must survive the mapping — the
35/// pipeline drives codecs by them).
36fn map_err(e: rusty_mp3::Error) -> Error {
37    match e {
38        rusty_mp3::Error::Unimplemented(what) => Error::Unimplemented(what),
39        rusty_mp3::Error::Eof => Error::Eof,
40        rusty_mp3::Error::Again => Error::Again,
41        rusty_mp3::Error::InvalidData(msg) => Error::InvalidData(msg),
42        rusty_mp3::Error::Unsupported(msg) => Error::Unsupported(msg),
43        // `rusty_mp3::Error` is #[non_exhaustive]; anything future-added is a
44        // decode/encode failure from rff's point of view.
45        other => Error::InvalidData(format!("rusty_mp3: {other}")),
46    }
47}
48
49#[derive(Default)]
50struct Mp3Decoder {
51    inner: rusty_mp3::Mp3Decoder,
52}
53
54impl Decoder for Mp3Decoder {
55    fn send_packet(&mut self, packet: &Packet) -> Result<()> {
56        self.inner.push(&packet.data);
57        Ok(())
58    }
59
60    fn receive_frame(&mut self) -> Result<Frame> {
61        let audio = self.inner.next_frame().map_err(map_err)?;
62        let channels = audio.channels.max(1);
63        let mut bytes = Vec::with_capacity(audio.samples.len() * 4);
64        for s in &audio.samples {
65            bytes.extend_from_slice(&s.to_le_bytes());
66        }
67        Ok(Frame::Audio(AudioFrame {
68            sample_rate: audio.sample_rate,
69            channels,
70            format: SampleFormat::F32,
71            samples: audio.samples.len() / channels as usize,
72            planes: vec![bytes],
73            pts: None,
74        }))
75    }
76
77    fn flush(&mut self) {
78        self.inner.flush();
79    }
80}
81
82/// Parse an FFmpeg-style bitrate string ("128k", "192000") into kbps.
83fn parse_bitrate(s: &str) -> Option<u32> {
84    let s = s.trim();
85    if let Some(n) = s.strip_suffix(['k', 'K']) {
86        n.trim().parse::<f32>().ok().map(|x| x as u32)
87    } else if let Some(n) = s.strip_suffix(['m', 'M']) {
88        n.trim().parse::<f32>().ok().map(|x| (x * 1000.0) as u32)
89    } else {
90        s.parse::<u32>()
91            .ok()
92            .map(|x| if x >= 1000 { x / 1000 } else { x })
93    }
94}
95
96/// MP3 encoder adapter. Options are collected in [`Encoder::configure`]; the
97/// inner [`rusty_mp3::Mp3Encoder`] is built lazily on the first audio frame
98/// (its header locks on the first PCM push).
99#[derive(Default)]
100struct Mp3Encoder {
101    inner: Option<rusty_mp3::Mp3Encoder>,
102    /// CBR target (kbps); 0 ⇒ default 128.
103    cbr_kbps: u32,
104    /// VBR quality target (peak NMR). `Some` ⇒ VBR, `None` ⇒ CBR.
105    quality: Option<f32>,
106    eof: bool,
107}
108
109impl Mp3Encoder {
110    fn inner(&mut self) -> &mut rusty_mp3::Mp3Encoder {
111        if self.inner.is_none() {
112            self.inner = Some(rusty_mp3::Mp3Encoder::new(rusty_mp3::Mp3EncoderConfig {
113                bitrate_kbps: self.cbr_kbps,
114                vbr_quality: self.quality,
115            }));
116        }
117        self.inner.as_mut().unwrap()
118    }
119}
120
121impl Encoder for Mp3Encoder {
122    fn configure(&mut self, options: &Dictionary) -> Result<()> {
123        if let Some(b) = options.get("b").and_then(parse_bitrate) {
124            self.cbr_kbps = b;
125        }
126        // VBR quality via -q:a / -qp:a / -crf:a (0 = best … 9 = smallest). Its
127        // presence switches to VBR; map the index to a peak-NMR target.
128        for key in ["q", "qp", "crf", "qscale"] {
129            if let Some(q) = options.get(key).and_then(|v| v.trim().parse::<f32>().ok()) {
130                self.quality = Some(rusty_mp3::vbr_quality_index(q));
131                break;
132            }
133        }
134        Ok(())
135    }
136
137    fn send_frame(&mut self, frame: &Frame) -> Result<()> {
138        let Frame::Audio(af) = frame else {
139            return Ok(());
140        };
141        let in_ch = af.channels.max(1);
142        let data = &af.planes[0];
143        // The PCM/WAV demuxer path delivers S16 (not F32), so we MUST honor
144        // `af.format` here — reading s16 bytes with an f32 stride yields
145        // denormal garbage (a silent encode). The unit tests only ever fed F32,
146        // which is why this once slipped; `s16_input_encodes_to_audible_output`
147        // below guards it.
148        match af.format {
149            SampleFormat::F32 => {
150                let mut pcm = Vec::with_capacity(af.samples * in_ch as usize);
151                for s in 0..af.samples {
152                    for c in 0..in_ch as usize {
153                        let off = (s * in_ch as usize + c) * 4;
154                        pcm.push(if off + 4 <= data.len() {
155                            f32::from_le_bytes([
156                                data[off],
157                                data[off + 1],
158                                data[off + 2],
159                                data[off + 3],
160                            ])
161                        } else {
162                            0.0
163                        });
164                    }
165                }
166                let sr = af.sample_rate;
167                self.inner()
168                    .push_pcm_f32(&pcm, in_ch, sr)
169                    .map_err(map_err)?;
170            }
171            SampleFormat::S16 => {
172                let mut pcm = Vec::with_capacity(af.samples * in_ch as usize);
173                for s in 0..af.samples {
174                    for c in 0..in_ch as usize {
175                        let off = (s * in_ch as usize + c) * 2;
176                        pcm.push(if off + 2 <= data.len() {
177                            i16::from_le_bytes([data[off], data[off + 1]])
178                        } else {
179                            0
180                        });
181                    }
182                }
183                let sr = af.sample_rate;
184                self.inner()
185                    .push_pcm_s16(&pcm, in_ch, sr)
186                    .map_err(map_err)?;
187            }
188            other => {
189                return Err(Error::unsupported(format!(
190                    "mp3 encode: sample format `{}` (need interleaved s16/f32)",
191                    other.name()
192                )))
193            }
194        }
195        Ok(())
196    }
197
198    fn receive_packet(&mut self) -> Result<Packet> {
199        // The inner encoder owns the packet queue (including the Xing/Info
200        // frame push-fronted at finish) — pull straight through, mapping the
201        // Again/Eof drain protocol variant for variant.
202        match self.inner.as_mut() {
203            Some(inner) => inner
204                .next_packet()
205                .map(|bytes| Packet::from_data(0, bytes))
206                .map_err(map_err),
207            None if self.eof => Err(Error::Eof),
208            None => Err(Error::Again),
209        }
210    }
211
212    fn flush(&mut self) {
213        if let Some(inner) = self.inner.as_mut() {
214            inner.finish();
215        }
216        self.eof = true;
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    /// Encode interleaved **S16** mono — the sample format the WAV/PCM demuxer
225    /// actually delivers (the rusty_mp3 native tests otherwise feed f32 slices).
226    fn encode_mono_s16(input: &[i16], sample_rate: u32) -> Vec<u8> {
227        let bytes: Vec<u8> = input.iter().flat_map(|s| s.to_le_bytes()).collect();
228        let mut enc = Mp3Encoder::default();
229        enc.send_frame(&Frame::Audio(AudioFrame {
230            sample_rate,
231            channels: 1,
232            format: SampleFormat::S16,
233            planes: vec![bytes],
234            samples: input.len(),
235            pts: None,
236        }))
237        .unwrap();
238        enc.flush();
239        let mut mp3 = Vec::new();
240        while let Ok(p) = enc.receive_packet() {
241            mp3.extend_from_slice(&p.data);
242        }
243        mp3
244    }
245
246    fn decode_mono(mp3: Vec<u8>) -> Vec<f32> {
247        let mut dec = Mp3Decoder::default();
248        dec.send_packet(&Packet::from_data(0, mp3)).unwrap();
249        dec.flush();
250        let mut out = Vec::new();
251        while let Ok(Frame::Audio(af)) = dec.receive_frame() {
252            for c in af.planes[0].chunks_exact(4) {
253                out.push(f32::from_le_bytes([c[0], c[1], c[2], c[3]]));
254            }
255        }
256        out
257    }
258
259    /// REGRESSION (silent-encode bug): `send_frame` must honor `af.format`. The
260    /// WAV/PCM demuxer delivers S16; reading those bytes with an F32 (4-byte)
261    /// stride over 2-byte samples yields denormal garbage → a full-size but
262    /// SILENT MP3 that every decoder reproduces as silence. Feed S16, require
263    /// audible output. (Brick tests all fed F32, so this class of bug slipped.)
264    #[test]
265    fn s16_input_encodes_to_audible_output() {
266        let sr = 44100u32;
267        let n = sr as usize; // 1 s
268        let s16: Vec<i16> = (0..n)
269            .map(|i| {
270                let t = i as f32 / sr as f32;
271                (0.5 * (2.0 * std::f32::consts::PI * 440.0 * t).sin() * 32767.0) as i16
272            })
273            .collect();
274        let out = decode_mono(encode_mono_s16(&s16, sr));
275        let rms = (out.iter().map(|x| x * x).sum::<f32>() / out.len().max(1) as f32).sqrt();
276        assert!(
277            rms > 0.1,
278            "S16 input produced near-silent output (rms={rms}); encoder ignored af.format"
279        );
280    }
281
282    #[test]
283    fn registers_as_audio_codec() {
284        let mut reg = CodecRegistry::new();
285        register(&mut reg);
286        let codec = reg.by_name("mp3").expect("mp3 registered");
287        assert_eq!(codec.id, CodecId::Mp3);
288        assert_eq!(codec.media_type, MediaType::Audio);
289        assert!(codec.can_decode() && codec.can_encode());
290    }
291}