use crate::bitwriter::BitWriter;
pub struct StreamInfo {
pub min_blocksize: u32,
pub max_blocksize: u32,
pub min_framesize: u32,
pub max_framesize: u32,
pub sample_rate: u32,
pub channels: u32,
pub bits_per_sample: u32,
pub total_samples: u64,
pub md5: [u8; 16],
}
const METADATA_TYPE_STREAMINFO: u32 = 0;
const METADATA_TYPE_PADDING: u32 = 1;
const METADATA_TYPE_APPLICATION: u32 = 2;
const METADATA_TYPE_SEEKTABLE: u32 = 3;
const METADATA_TYPE_VORBIS_COMMENT: u32 = 4;
const METADATA_TYPE_CUESHEET: u32 = 5;
const METADATA_TYPE_PICTURE: u32 = 6;
const STREAMINFO_LENGTH: u32 = 34;
const SEEKPOINT_LENGTH: u32 = 18;
pub const SEEKPOINT_PLACEHOLDER: u64 = 0xffff_ffff_ffff_ffff;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SeekPoint {
pub sample_number: u64,
pub stream_offset: u64,
pub frame_samples: u32,
}
const CUESHEET_RESERVED_BITS: u32 = 7 + 258 * 8;
const CUESHEET_TRACK_RESERVED_BITS: u32 = 6 + 13 * 8;
const CUESHEET_INDEX_RESERVED_BITS: u32 = 3 * 8;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CueSheetIndex {
pub offset: u64,
pub number: u8,
}
pub struct CueSheetTrack<'a> {
pub offset: u64,
pub number: u8,
pub isrc: [u8; 12],
pub non_audio: bool,
pub pre_emphasis: bool,
pub indices: &'a [CueSheetIndex],
}
pub enum MetadataBlock<'a> {
VorbisComment(&'a str),
Padding(u32),
Application { id: [u8; 4], data: &'a [u8] },
Seektable(&'a [SeekPoint]),
CueSheet {
media_catalog_number: &'a [u8; 128],
lead_in: u64,
is_cd: bool,
tracks: &'a [CueSheetTrack<'a>],
},
Picture {
picture_type: u32,
mime_type: &'a str,
description: &'a str,
width: u32,
height: u32,
depth: u32,
colors: u32,
data: &'a [u8],
},
}
pub fn write_block(bw: &mut BitWriter, block: &MetadataBlock, is_last: bool) {
match block {
MetadataBlock::VorbisComment(vendor) => write_vorbis_comment(bw, vendor, is_last),
MetadataBlock::Padding(len) => write_padding(bw, *len, is_last),
MetadataBlock::Application { id, data } => write_application(bw, id, data, is_last),
MetadataBlock::Seektable(points) => write_seektable(bw, points, is_last),
MetadataBlock::CueSheet {
media_catalog_number,
lead_in,
is_cd,
tracks,
} => write_cuesheet(bw, media_catalog_number, *lead_in, *is_cd, tracks, is_last),
MetadataBlock::Picture {
picture_type,
mime_type,
description,
width,
height,
depth,
colors,
data,
} => {
let mime = mime_type.as_bytes();
let desc = description.as_bytes();
let length = 4 + 4 + mime.len() + 4 + desc.len() + 16 + 4 + data.len();
write_block_header(bw, is_last, METADATA_TYPE_PICTURE, length as u32);
bw.write_raw_u32(*picture_type, 32);
bw.write_raw_u32(mime.len() as u32, 32);
bw.write_byte_block(mime);
bw.write_raw_u32(desc.len() as u32, 32);
bw.write_byte_block(desc);
bw.write_raw_u32(*width, 32);
bw.write_raw_u32(*height, 32);
bw.write_raw_u32(*depth, 32);
bw.write_raw_u32(*colors, 32);
bw.write_raw_u32(data.len() as u32, 32);
bw.write_byte_block(data);
}
}
}
pub fn write_application(bw: &mut BitWriter, id: &[u8; 4], data: &[u8], is_last: bool) {
write_block_header(
bw,
is_last,
METADATA_TYPE_APPLICATION,
4 + data.len() as u32,
);
bw.write_byte_block(id);
bw.write_byte_block(data);
}
pub fn write_seektable(bw: &mut BitWriter, points: &[SeekPoint], is_last: bool) {
write_block_header(
bw,
is_last,
METADATA_TYPE_SEEKTABLE,
points.len() as u32 * SEEKPOINT_LENGTH,
);
for p in points {
bw.write_raw_u64(p.sample_number, 64);
bw.write_raw_u64(p.stream_offset, 64);
bw.write_raw_u32(p.frame_samples, 16);
}
}
pub fn spaced_seek_points(num: u32, total_samples: u64) -> Vec<SeekPoint> {
if num == 0 || total_samples == 0 {
return Vec::new();
}
(0..num as u64)
.map(|i| SeekPoint {
sample_number: total_samples * i / num as u64,
stream_offset: 0,
frame_samples: 0,
})
.collect()
}
pub fn seektable_sort(points: &mut [SeekPoint]) {
if points.is_empty() {
return;
}
points.sort_by_key(|p| p.sample_number);
let mut j = 0usize;
let mut first = true;
for i in 0..points.len() {
let sn = points[i].sample_number;
if !first && sn != SEEKPOINT_PLACEHOLDER && sn == points[j - 1].sample_number {
continue; }
first = false;
points[j] = points[i];
j += 1;
}
for p in &mut points[j..] {
*p = SeekPoint {
sample_number: SEEKPOINT_PLACEHOLDER,
stream_offset: 0,
frame_samples: 0,
};
}
}
fn cuesheet_length(tracks: &[CueSheetTrack]) -> u32 {
let mut len = 396u32;
for t in tracks {
len += 36 + t.indices.len() as u32 * 12;
}
len
}
pub fn write_cuesheet(
bw: &mut BitWriter,
media_catalog_number: &[u8; 128],
lead_in: u64,
is_cd: bool,
tracks: &[CueSheetTrack],
is_last: bool,
) {
write_block_header(bw, is_last, METADATA_TYPE_CUESHEET, cuesheet_length(tracks));
bw.write_byte_block(media_catalog_number);
bw.write_raw_u64(lead_in, 64);
bw.write_raw_u32(is_cd as u32, 1);
bw.write_zeroes(CUESHEET_RESERVED_BITS);
bw.write_raw_u32(tracks.len() as u32, 8);
for t in tracks {
bw.write_raw_u64(t.offset, 64);
bw.write_raw_u32(t.number as u32, 8);
bw.write_byte_block(&t.isrc);
bw.write_raw_u32(t.non_audio as u32, 1);
bw.write_raw_u32(t.pre_emphasis as u32, 1);
bw.write_zeroes(CUESHEET_TRACK_RESERVED_BITS);
bw.write_raw_u32(t.indices.len() as u32, 8);
for idx in t.indices {
bw.write_raw_u64(idx.offset, 64);
bw.write_raw_u32(idx.number as u32, 8);
bw.write_zeroes(CUESHEET_INDEX_RESERVED_BITS);
}
}
}
pub const LIBFLAC_VENDOR_STRING: &str = "reference libFLAC 1.4.3 20230623";
fn write_block_header(bw: &mut BitWriter, is_last: bool, block_type: u32, length: u32) {
bw.write_raw_u32(is_last as u32, 1);
bw.write_raw_u32(block_type, 7);
bw.write_raw_u32(length, 24);
}
pub fn write_streaminfo(bw: &mut BitWriter, si: &StreamInfo, is_last: bool) {
write_block_header(bw, is_last, METADATA_TYPE_STREAMINFO, STREAMINFO_LENGTH);
bw.write_raw_u32(si.min_blocksize, 16);
bw.write_raw_u32(si.max_blocksize, 16);
bw.write_raw_u32(si.min_framesize, 24);
bw.write_raw_u32(si.max_framesize, 24);
bw.write_raw_u32(si.sample_rate, 20);
bw.write_raw_u32(si.channels - 1, 3);
bw.write_raw_u32(si.bits_per_sample - 1, 5);
bw.write_raw_u64(si.total_samples, 36);
bw.write_byte_block(&si.md5);
}
pub fn write_vorbis_comment(bw: &mut BitWriter, vendor: &str, is_last: bool) {
let vendor = vendor.as_bytes();
let length = 4 + vendor.len() as u32 + 4; write_block_header(bw, is_last, METADATA_TYPE_VORBIS_COMMENT, length);
bw.write_raw_u32_little_endian(vendor.len() as u32);
bw.write_byte_block(vendor);
bw.write_raw_u32_little_endian(0); }
pub fn write_padding(bw: &mut BitWriter, length: u32, is_last: bool) {
write_block_header(bw, is_last, METADATA_TYPE_PADDING, length);
bw.write_zeroes(length * 8);
}
#[cfg(test)]
mod tests {
use super::*;
fn pt(sample_number: u64, stream_offset: u64, frame_samples: u32) -> SeekPoint {
SeekPoint {
sample_number,
stream_offset,
frame_samples,
}
}
fn placeholder() -> SeekPoint {
pt(SEEKPOINT_PLACEHOLDER, 0, 0)
}
#[test]
fn spaced_points_formula() {
assert_eq!(
spaced_seek_points(4, 8000),
vec![pt(0, 0, 0), pt(2000, 0, 0), pt(4000, 0, 0), pt(6000, 0, 0)],
);
assert_eq!(
spaced_seek_points(3, 10),
vec![pt(0, 0, 0), pt(3, 0, 0), pt(6, 0, 0)], );
assert!(spaced_seek_points(0, 8000).is_empty());
assert!(spaced_seek_points(4, 0).is_empty());
}
#[test]
fn sort_dedups_and_pads_with_placeholders() {
let mut points = vec![
pt(30, 300, 2048),
pt(10, 100, 2048),
pt(10, 100, 2048), placeholder(),
pt(20, 200, 2048),
];
seektable_sort(&mut points);
assert_eq!(
points,
vec![
pt(10, 100, 2048),
pt(20, 200, 2048),
pt(30, 300, 2048),
placeholder(),
placeholder(),
],
);
}
#[test]
fn sort_keeps_existing_placeholders_at_tail() {
let mut points = vec![placeholder(), pt(5, 50, 2048), placeholder()];
seektable_sort(&mut points);
assert_eq!(points, vec![pt(5, 50, 2048), placeholder(), placeholder()]);
}
#[test]
fn seektable_byte_layout() {
let mut bw = BitWriter::new();
write_seektable(
&mut bw,
&[pt(0x0102_0304_0506_0708, 0x1112_1314_1516_1718, 2048)],
true,
);
assert_eq!(
bw.as_bytes(),
&[
0x83, 0x00, 0x00, 0x12, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x08, 0x00, ],
);
}
#[test]
fn cuesheet_byte_layout() {
let mcn = [0u8; 128];
let indices = [CueSheetIndex {
offset: 0x1122_3344_5566_7788,
number: 1,
}];
let tracks = [CueSheetTrack {
offset: 0x0102_0304_0506_0708,
number: 7,
isrc: *b"ABCDEFGHIJKL",
non_audio: true,
pre_emphasis: false,
indices: &indices,
}];
let mut bw = BitWriter::new();
write_cuesheet(&mut bw, &mcn, 0x00FF_00FF_00FF_00FF, true, &tracks, true);
let out = bw.as_bytes();
assert_eq!(out.len(), 448);
assert_eq!(&out[0..4], &[0x85, 0x00, 0x01, 0xBC]);
assert_eq!(&out[4..132], &[0u8; 128]); assert_eq!(&out[132..140], &0x00FF_00FF_00FF_00FFu64.to_be_bytes()); assert_eq!(out[140], 0x80); assert_eq!(&out[141..399], &[0u8; 258]); assert_eq!(out[399], 1); assert_eq!(&out[400..408], &0x0102_0304_0506_0708u64.to_be_bytes()); assert_eq!(out[408], 7); assert_eq!(&out[409..421], b"ABCDEFGHIJKL"); assert_eq!(out[421], 0b1000_0000); assert_eq!(&out[422..435], &[0u8; 13]); assert_eq!(out[435], 1); assert_eq!(&out[436..444], &0x1122_3344_5566_7788u64.to_be_bytes()); assert_eq!(out[444], 1); assert_eq!(&out[445..448], &[0u8; 3]); }
}