Skip to main content

libflac_rs/
metadata.rs

1//! FLAC metadata blocks. Only STREAMINFO (the mandatory first block) is written;
2//! the field layout is from `FLAC/format.h` and the metadata framing in
3//! `stream_encoder.c` (`streaminfo` setup at `:1200`, framesize tracking at
4//! `:2703`). All fields are big-endian, MSB-first, and the body is exactly 34
5//! bytes (byte-aligned throughout).
6
7use crate::bitwriter::BitWriter;
8
9/// The STREAMINFO fields. `min_blocksize`/`max_blocksize` are the configured
10/// block size (libFLAC reports the same value for both even with a short final
11/// frame); `min_framesize`/`max_framesize` are the min/max frame size in bytes.
12pub struct StreamInfo {
13    pub min_blocksize: u32,
14    pub max_blocksize: u32,
15    pub min_framesize: u32,
16    pub max_framesize: u32,
17    pub sample_rate: u32,
18    pub channels: u32,
19    pub bits_per_sample: u32,
20    pub total_samples: u64,
21    pub md5: [u8; 16],
22}
23
24/// Metadata block type codes.
25const METADATA_TYPE_STREAMINFO: u32 = 0;
26const METADATA_TYPE_PADDING: u32 = 1;
27const METADATA_TYPE_APPLICATION: u32 = 2;
28const METADATA_TYPE_SEEKTABLE: u32 = 3;
29const METADATA_TYPE_VORBIS_COMMENT: u32 = 4;
30const METADATA_TYPE_CUESHEET: u32 = 5;
31const METADATA_TYPE_PICTURE: u32 = 6;
32/// STREAMINFO body length in bytes.
33const STREAMINFO_LENGTH: u32 = 34;
34/// One serialized seek point: `sample_number` (u64) + `stream_offset` (u64) +
35/// `frame_samples` (u16) = 18 bytes (`FLAC__STREAM_METADATA_SEEKPOINT_LENGTH`).
36const SEEKPOINT_LENGTH: u32 = 18;
37/// Sample number marking an unused seek point
38/// (`FLAC__STREAM_METADATA_SEEKPOINT_PLACEHOLDER`, `format.c:81`).
39pub const SEEKPOINT_PLACEHOLDER: u64 = 0xffff_ffff_ffff_ffff;
40
41/// One SEEKTABLE seek point (`FLAC__StreamMetadata_SeekPoint`). In a *template*
42/// (before encoding) `sample_number` is the target sample to make seekable and the
43/// other two fields are 0; the encoder rewrites all three for the frame that holds
44/// each target (see [`Encoder`](crate::Encoder)).
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub struct SeekPoint {
47    pub sample_number: u64,
48    pub stream_offset: u64,
49    pub frame_samples: u32,
50}
51
52/// CUESHEET bit-lengths (`format.h`) for the non-byte-aligned reserved runs.
53/// Cuesheet-level reserved after `is_cd` is `7 + 258*8` bits; per-track reserved
54/// after the two flag bits is `6 + 13*8`; per-index reserved is `3*8`.
55const CUESHEET_RESERVED_BITS: u32 = 7 + 258 * 8;
56const CUESHEET_TRACK_RESERVED_BITS: u32 = 6 + 13 * 8;
57const CUESHEET_INDEX_RESERVED_BITS: u32 = 3 * 8;
58
59/// One CUESHEET track index point (`FLAC__StreamMetadata_CueSheet_Index`).
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub struct CueSheetIndex {
62    /// Offset in samples relative to the track offset.
63    pub offset: u64,
64    pub number: u8,
65}
66
67/// One CUESHEET track (`FLAC__StreamMetadata_CueSheet_Track`).
68pub struct CueSheetTrack<'a> {
69    /// Offset in samples from the start of the stream.
70    pub offset: u64,
71    pub number: u8,
72    /// 12-byte ISRC (zero-filled when unset).
73    pub isrc: [u8; 12],
74    /// The track type bit: `false` = audio, `true` = non-audio.
75    pub non_audio: bool,
76    pub pre_emphasis: bool,
77    pub indices: &'a [CueSheetIndex],
78}
79
80/// A metadata block the caller can place after STREAMINFO (which the encoder
81/// always writes first). libFLAC writes blocks in the order given (the OGG
82/// reorder is compiled out for native FLAC), so this list maps 1:1 to the output.
83pub enum MetadataBlock<'a> {
84    /// A VORBIS_COMMENT with the given vendor string and no user comments.
85    VorbisComment(&'a str),
86    /// A PADDING block of N zero bytes.
87    Padding(u32),
88    /// An APPLICATION block: a 4-byte registered application id + opaque data.
89    Application { id: [u8; 4], data: &'a [u8] },
90    /// A SEEKTABLE block: the seek points to serialize, in order. The encoder fills
91    /// a *template* (each point's `sample_number` a target, offsets 0) during
92    /// encoding and writes the filled+sorted result; `write_seektable` serializes
93    /// whatever points it is given verbatim.
94    Seektable(&'a [SeekPoint]),
95    /// A CUESHEET block: the 128-byte media catalog number, lead-in samples, the
96    /// CD-DA flag, and the track list (each with its index points). Fully
97    /// caller-supplied — unlike SEEKTABLE, nothing is generated during encoding.
98    CueSheet {
99        media_catalog_number: &'a [u8; 128],
100        lead_in: u64,
101        is_cd: bool,
102        tracks: &'a [CueSheetTrack<'a>],
103    },
104    /// A PICTURE block (e.g. cover art). `mime_type`/`description` are stored with
105    /// 32-bit length prefixes; `picture_type` is the FLAC picture-type code.
106    Picture {
107        picture_type: u32,
108        mime_type: &'a str,
109        description: &'a str,
110        width: u32,
111        height: u32,
112        depth: u32,
113        colors: u32,
114        data: &'a [u8],
115    },
116}
117
118/// Write one [`MetadataBlock`] with its `is_last` flag.
119pub fn write_block(bw: &mut BitWriter, block: &MetadataBlock, is_last: bool) {
120    match block {
121        MetadataBlock::VorbisComment(vendor) => write_vorbis_comment(bw, vendor, is_last),
122        MetadataBlock::Padding(len) => write_padding(bw, *len, is_last),
123        MetadataBlock::Application { id, data } => write_application(bw, id, data, is_last),
124        MetadataBlock::Seektable(points) => write_seektable(bw, points, is_last),
125        MetadataBlock::CueSheet {
126            media_catalog_number,
127            lead_in,
128            is_cd,
129            tracks,
130        } => write_cuesheet(bw, media_catalog_number, *lead_in, *is_cd, tracks, is_last),
131        MetadataBlock::Picture {
132            picture_type,
133            mime_type,
134            description,
135            width,
136            height,
137            depth,
138            colors,
139            data,
140        } => {
141            let mime = mime_type.as_bytes();
142            let desc = description.as_bytes();
143            // body = type + mime_len + mime + desc_len + desc + w/h/d/colors + data_len + data
144            let length = 4 + 4 + mime.len() + 4 + desc.len() + 16 + 4 + data.len();
145            write_block_header(bw, is_last, METADATA_TYPE_PICTURE, length as u32);
146            bw.write_raw_u32(*picture_type, 32);
147            bw.write_raw_u32(mime.len() as u32, 32);
148            bw.write_byte_block(mime);
149            bw.write_raw_u32(desc.len() as u32, 32);
150            bw.write_byte_block(desc);
151            bw.write_raw_u32(*width, 32);
152            bw.write_raw_u32(*height, 32);
153            bw.write_raw_u32(*depth, 32);
154            bw.write_raw_u32(*colors, 32);
155            bw.write_raw_u32(data.len() as u32, 32);
156            bw.write_byte_block(data);
157        }
158    }
159}
160
161/// Write an APPLICATION block: 4-byte id then the application data
162/// (`FLAC__metadata_object_application`; body length = 4 + data length).
163pub fn write_application(bw: &mut BitWriter, id: &[u8; 4], data: &[u8], is_last: bool) {
164    write_block_header(
165        bw,
166        is_last,
167        METADATA_TYPE_APPLICATION,
168        4 + data.len() as u32,
169    );
170    bw.write_byte_block(id);
171    bw.write_byte_block(data);
172}
173
174/// Write a SEEKTABLE block: N seek points × 18 bytes
175/// (`stream_encoder_framing.c:122`; the finish-time rewrite at
176/// `stream_encoder.c:2928` produces the same layout). `sample_number` and
177/// `stream_offset` are 64-bit, `frame_samples` 16-bit, all big-endian. The body
178/// length is `num_points * 18` even after sorting (unused trailing points are
179/// placeholders), matching the header written once at metadata time.
180pub fn write_seektable(bw: &mut BitWriter, points: &[SeekPoint], is_last: bool) {
181    write_block_header(
182        bw,
183        is_last,
184        METADATA_TYPE_SEEKTABLE,
185        points.len() as u32 * SEEKPOINT_LENGTH,
186    );
187    for p in points {
188        bw.write_raw_u64(p.sample_number, 64);
189        bw.write_raw_u64(p.stream_offset, 64);
190        bw.write_raw_u32(p.frame_samples, 16);
191    }
192}
193
194/// Build a SEEKTABLE *template* of `num` evenly-spaced placeholder points for a
195/// stream of `total_samples`
196/// (`FLAC__metadata_object_seektable_template_append_spaced_points`): point `i`
197/// targets sample `total_samples * i / num`, with zero offset/frame_samples, to be
198/// filled during encoding. Returns empty if `num` or `total_samples` is 0. (For a
199/// legal table — at most ~932k points so the block fits the 24-bit length field —
200/// the `total_samples * i` product cannot overflow `u64`.)
201pub fn spaced_seek_points(num: u32, total_samples: u64) -> Vec<SeekPoint> {
202    if num == 0 || total_samples == 0 {
203        return Vec::new();
204    }
205    (0..num as u64)
206        .map(|i| SeekPoint {
207            sample_number: total_samples * i / num as u64,
208            stream_offset: 0,
209            frame_samples: 0,
210        })
211        .collect()
212}
213
214/// Sort + uniquify a (filled) seektable in place, exactly as the encoder does at
215/// finish (`FLAC__format_seektable_sort`, `format.c:281`): sort by `sample_number`
216/// (placeholders, `u64::MAX`, sort last), drop any non-placeholder point whose
217/// `sample_number` duplicates the previous kept point, and overwrite the freed tail
218/// slots with placeholders. The point count is preserved, so the serialized length
219/// is unchanged.
220pub fn seektable_sort(points: &mut [SeekPoint]) {
221    if points.is_empty() {
222        return;
223    }
224    // qsort in C is unstable, but post-fill duplicates share all three fields, so
225    // the kept representative is identical regardless of order.
226    points.sort_by_key(|p| p.sample_number);
227    let mut j = 0usize;
228    let mut first = true;
229    for i in 0..points.len() {
230        let sn = points[i].sample_number;
231        if !first && sn != SEEKPOINT_PLACEHOLDER && sn == points[j - 1].sample_number {
232            continue; // duplicate of the previous kept point
233        }
234        first = false;
235        points[j] = points[i];
236        j += 1;
237    }
238    for p in &mut points[j..] {
239        *p = SeekPoint {
240            sample_number: SEEKPOINT_PLACEHOLDER,
241            stream_offset: 0,
242            frame_samples: 0,
243        };
244    }
245}
246
247/// The serialized body length of a CUESHEET (`stream_encoder_framing.c:154`): a
248/// 396-byte fixed header (128-byte catalog + 8-byte lead-in + 259 bytes of
249/// `is_cd`+reserved + 1-byte track count), then 36 bytes per track plus 12 bytes
250/// per index. Every field run lands on a byte boundary, so this is exact.
251fn cuesheet_length(tracks: &[CueSheetTrack]) -> u32 {
252    let mut len = 396u32;
253    for t in tracks {
254        len += 36 + t.indices.len() as u32 * 12;
255    }
256    len
257}
258
259/// Write a CUESHEET block (`stream_encoder_framing.c:154`). All fields are
260/// big-endian/MSB-first; the reserved runs (`7+258*8`, `6+13*8`, `3*8` bits) are
261/// **not** byte-aligned individually but each track/index boundary is. `is_cd` is a
262/// single bit; the track `type` bit is `non_audio`.
263pub fn write_cuesheet(
264    bw: &mut BitWriter,
265    media_catalog_number: &[u8; 128],
266    lead_in: u64,
267    is_cd: bool,
268    tracks: &[CueSheetTrack],
269    is_last: bool,
270) {
271    write_block_header(bw, is_last, METADATA_TYPE_CUESHEET, cuesheet_length(tracks));
272    bw.write_byte_block(media_catalog_number);
273    bw.write_raw_u64(lead_in, 64);
274    bw.write_raw_u32(is_cd as u32, 1);
275    bw.write_zeroes(CUESHEET_RESERVED_BITS);
276    bw.write_raw_u32(tracks.len() as u32, 8);
277    for t in tracks {
278        bw.write_raw_u64(t.offset, 64);
279        bw.write_raw_u32(t.number as u32, 8);
280        bw.write_byte_block(&t.isrc);
281        bw.write_raw_u32(t.non_audio as u32, 1);
282        bw.write_raw_u32(t.pre_emphasis as u32, 1);
283        bw.write_zeroes(CUESHEET_TRACK_RESERVED_BITS);
284        bw.write_raw_u32(t.indices.len() as u32, 8);
285        for idx in t.indices {
286            bw.write_raw_u64(idx.offset, 64);
287            bw.write_raw_u32(idx.number as u32, 8);
288            bw.write_zeroes(CUESHEET_INDEX_RESERVED_BITS);
289        }
290    }
291}
292
293/// The vendor string libFLAC 1.4.3 writes into its auto VORBIS_COMMENT
294/// (`FLAC__VENDOR_STRING` = `"reference libFLAC " PACKAGE_VERSION " 20230623"`
295/// with no git tag/hash defined). Used to byte-match libFLAC's default output.
296pub const LIBFLAC_VENDOR_STRING: &str = "reference libFLAC 1.4.3 20230623";
297
298/// Write a metadata block header (1-bit last flag, 7-bit type, 24-bit length).
299fn write_block_header(bw: &mut BitWriter, is_last: bool, block_type: u32, length: u32) {
300    bw.write_raw_u32(is_last as u32, 1);
301    bw.write_raw_u32(block_type, 7);
302    bw.write_raw_u32(length, 24);
303}
304
305/// Write the STREAMINFO metadata block (4-byte block header + 34-byte body).
306/// `is_last` sets the last-metadata-block flag.
307pub fn write_streaminfo(bw: &mut BitWriter, si: &StreamInfo, is_last: bool) {
308    write_block_header(bw, is_last, METADATA_TYPE_STREAMINFO, STREAMINFO_LENGTH);
309
310    bw.write_raw_u32(si.min_blocksize, 16);
311    bw.write_raw_u32(si.max_blocksize, 16);
312    bw.write_raw_u32(si.min_framesize, 24);
313    bw.write_raw_u32(si.max_framesize, 24);
314    bw.write_raw_u32(si.sample_rate, 20);
315    bw.write_raw_u32(si.channels - 1, 3);
316    bw.write_raw_u32(si.bits_per_sample - 1, 5);
317    bw.write_raw_u64(si.total_samples, 36);
318    bw.write_byte_block(&si.md5);
319}
320
321/// Write a VORBIS_COMMENT block with the given vendor string and no comments
322/// (the empty block libFLAC auto-writes; `add_metadata_block`,
323/// `stream_encoder_framing.c:132`). The vendor length and comment count are
324/// little-endian per the Vorbis comment spec.
325pub fn write_vorbis_comment(bw: &mut BitWriter, vendor: &str, is_last: bool) {
326    let vendor = vendor.as_bytes();
327    let length = 4 + vendor.len() as u32 + 4; // vendor-length + vendor + num-comments
328    write_block_header(bw, is_last, METADATA_TYPE_VORBIS_COMMENT, length);
329    bw.write_raw_u32_little_endian(vendor.len() as u32);
330    bw.write_byte_block(vendor);
331    bw.write_raw_u32_little_endian(0); // num_comments
332}
333
334/// Write a PADDING block of `length` zero bytes (`add_metadata_block`,
335/// `stream_encoder_framing.c:112`).
336pub fn write_padding(bw: &mut BitWriter, length: u32, is_last: bool) {
337    write_block_header(bw, is_last, METADATA_TYPE_PADDING, length);
338    bw.write_zeroes(length * 8);
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    fn pt(sample_number: u64, stream_offset: u64, frame_samples: u32) -> SeekPoint {
346        SeekPoint {
347            sample_number,
348            stream_offset,
349            frame_samples,
350        }
351    }
352
353    fn placeholder() -> SeekPoint {
354        pt(SEEKPOINT_PLACEHOLDER, 0, 0)
355    }
356
357    #[test]
358    fn spaced_points_formula() {
359        // sample_number = total * i / num (matches libFLAC's append_spaced_points).
360        assert_eq!(
361            spaced_seek_points(4, 8000),
362            vec![pt(0, 0, 0), pt(2000, 0, 0), pt(4000, 0, 0), pt(6000, 0, 0)],
363        );
364        assert_eq!(
365            spaced_seek_points(3, 10),
366            vec![pt(0, 0, 0), pt(3, 0, 0), pt(6, 0, 0)], // 10*1/3=3, 10*2/3=6
367        );
368        assert!(spaced_seek_points(0, 8000).is_empty());
369        assert!(spaced_seek_points(4, 0).is_empty());
370    }
371
372    #[test]
373    fn sort_dedups_and_pads_with_placeholders() {
374        // Multiple targets that resolved to the same frame become identical points;
375        // the sort keeps one and pushes the freed slots to the tail as placeholders,
376        // preserving the count. (Mirrors FLAC__format_seektable_sort.)
377        let mut points = vec![
378            pt(30, 300, 2048),
379            pt(10, 100, 2048),
380            pt(10, 100, 2048), // duplicate of the previous (same frame)
381            placeholder(),
382            pt(20, 200, 2048),
383        ];
384        seektable_sort(&mut points);
385        assert_eq!(
386            points,
387            vec![
388                pt(10, 100, 2048),
389                pt(20, 200, 2048),
390                pt(30, 300, 2048),
391                placeholder(),
392                placeholder(),
393            ],
394        );
395    }
396
397    #[test]
398    fn sort_keeps_existing_placeholders_at_tail() {
399        let mut points = vec![placeholder(), pt(5, 50, 2048), placeholder()];
400        seektable_sort(&mut points);
401        assert_eq!(points, vec![pt(5, 50, 2048), placeholder(), placeholder()]);
402    }
403
404    #[test]
405    fn seektable_byte_layout() {
406        let mut bw = BitWriter::new();
407        write_seektable(
408            &mut bw,
409            &[pt(0x0102_0304_0506_0708, 0x1112_1314_1516_1718, 2048)],
410            true,
411        );
412        assert_eq!(
413            bw.as_bytes(),
414            &[
415                0x83, 0x00, 0x00, 0x12, // is_last=1 | type=3, length=18
416                0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sample_number
417                0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, // stream_offset
418                0x08, 0x00, // frame_samples = 2048
419            ],
420        );
421    }
422
423    #[test]
424    fn cuesheet_byte_layout() {
425        let mcn = [0u8; 128];
426        let indices = [CueSheetIndex {
427            offset: 0x1122_3344_5566_7788,
428            number: 1,
429        }];
430        let tracks = [CueSheetTrack {
431            offset: 0x0102_0304_0506_0708,
432            number: 7,
433            isrc: *b"ABCDEFGHIJKL",
434            non_audio: true,
435            pre_emphasis: false,
436            indices: &indices,
437        }];
438        let mut bw = BitWriter::new();
439        write_cuesheet(&mut bw, &mcn, 0x00FF_00FF_00FF_00FF, true, &tracks, true);
440        let out = bw.as_bytes();
441
442        // 4-byte block header + 396 fixed + 36 (one track) + 12 (one index) = 448.
443        assert_eq!(out.len(), 448);
444        // is_last=1 | type=5 (0x85); body length = 444 = 0x0001BC.
445        assert_eq!(&out[0..4], &[0x85, 0x00, 0x01, 0xBC]);
446        assert_eq!(&out[4..132], &[0u8; 128]); // media catalog number
447        assert_eq!(&out[132..140], &0x00FF_00FF_00FF_00FFu64.to_be_bytes()); // lead_in
448        assert_eq!(out[140], 0x80); // is_cd bit set, then reserved zeros
449        assert_eq!(&out[141..399], &[0u8; 258]); // rest of the cuesheet reserved
450        assert_eq!(out[399], 1); // num_tracks
451        assert_eq!(&out[400..408], &0x0102_0304_0506_0708u64.to_be_bytes()); // track offset
452        assert_eq!(out[408], 7); // track number
453        assert_eq!(&out[409..421], b"ABCDEFGHIJKL"); // isrc
454        assert_eq!(out[421], 0b1000_0000); // type=1 (non_audio), pre_emphasis=0
455        assert_eq!(&out[422..435], &[0u8; 13]); // rest of the track reserved
456        assert_eq!(out[435], 1); // num_indices
457        assert_eq!(&out[436..444], &0x1122_3344_5566_7788u64.to_be_bytes()); // index offset
458        assert_eq!(out[444], 1); // index number
459        assert_eq!(&out[445..448], &[0u8; 3]); // index reserved
460    }
461}