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