Files
MusicFS/src/music/flac.rs
T
2026-06-29 12:03:31 +02:00

445 lines
15 KiB
Rust

use std::{
fs,
io::{Cursor, Read, Seek, SeekFrom},
path::Path,
};
use symphonia::core::{
formats::FormatOptions, io::MediaSourceStream, meta::MetadataOptions, probe::Hint,
};
use crate::music::encoder::MusicMetadataEncoder;
use crate::music::metadata::{MusicMetadata, extract_standard_tags};
use crate::music::parser::MusicMetadataParser;
const BLOCK_PADDING: u8 = 1;
const BLOCK_VORBIS_COMMENT: u8 = 4;
const BLOCK_PICTURE: u8 = 6;
const BLOCK_LAST_FLAG: u8 = 0x80;
const BLOCK_TYPE_MASK: u8 = 0x7f;
const PADDING_SIZE: usize = 8192;
/// FLAC parser. Owns all FLAC container parsing; returns `None` (never panics)
/// on a file it can't read — a corrupt or mid-copy file is logged and served
/// as plain passthrough rather than taking down the snapshot/watcher.
pub struct FlacMusicMetadataParser;
impl MusicMetadataParser for FlacMusicMetadataParser {
fn parse(&self, path: &Path) -> Option<MusicMetadata> {
match parse_flac_metadata(path) {
Some(mm) => Some(mm),
None => {
eprintln!(
"musicfs: failed to parse FLAC metadata from {}",
path.display()
);
None
}
}
}
}
/// FLAC encoder. Rebuilds the FLAC metadata header from the current tag fields,
/// re-injecting the (possibly overridden) Vorbis comment.
pub struct FlacMusicMetadataEncoder;
impl MusicMetadataEncoder for FlacMusicMetadataEncoder {
fn encode(&self, metadata: &mut MusicMetadata) {
if metadata.header.is_empty() {
return;
}
let blocks = extract_non_vorbis_blocks(&metadata.header);
let (header, vorbis_comment_offset, vorbis_comment_length) =
build_flac_header(blocks, metadata);
metadata.header = header;
metadata.vorbis_comment_offset = vorbis_comment_offset;
metadata.vorbis_comment_length = vorbis_comment_length;
}
}
fn parse_flac_metadata(path: &Path) -> Option<MusicMetadata> {
let src = fs::File::open(path).ok()?;
let mss = MediaSourceStream::new(Box::new(src), Default::default());
let mut hint = Hint::new();
hint.with_extension("flac");
let meta_opts: MetadataOptions = Default::default();
let fmt_opts: FormatOptions = Default::default();
let mut format = symphonia::default::get_probe()
.format(&hint, mss, &fmt_opts, &meta_opts)
.ok()?;
let metadata = format.format.metadata();
let mut music_metadata = MusicMetadata::default();
if let Some(revision) = metadata.current() {
extract_standard_tags(revision, &mut music_metadata);
}
if let Some(parsed) = parse_flac(path) {
music_metadata.real_audio_start = parsed.audio_start;
music_metadata.picture_block_headers = parsed.picture_block_headers;
music_metadata.picture_data_ranges = parsed.picture_data_ranges;
let (header, vorbis_comment_offset, vorbis_comment_length) =
build_flac_header(parsed.other_blocks, &music_metadata);
music_metadata.header = header;
music_metadata.vorbis_comment_offset = vorbis_comment_offset;
music_metadata.vorbis_comment_length = vorbis_comment_length;
}
Some(music_metadata)
}
impl MusicMetadata {
/// Locate the Vorbis comment block within the rebuilt FLAC header so writes
/// to it can be intercepted. No-op for non-FLAC (e.g. ID3) headers.
pub fn find_vorbis_offsets(&mut self) {
let mut cursor = Cursor::new(&self.header);
let mut magic = [0u8; 4];
if cursor.read_exact(&mut magic).is_err() {
return;
}
if &magic != b"fLaC" {
return;
}
loop {
let mut hdr = [0u8; 4];
if cursor.read_exact(&mut hdr).is_err() {
break;
}
let (is_last, block_type, length) = read_block_header(&hdr);
if block_type == BLOCK_VORBIS_COMMENT {
self.vorbis_comment_offset = cursor.position();
self.vorbis_comment_length = length;
return;
}
if cursor.seek(SeekFrom::Current(length as i64)).is_err() {
break;
}
if is_last {
break;
}
}
}
/// Apply a freshly written Vorbis comment block and rebuild the header.
pub fn update_from_vorbis_comment_data(&mut self, data: &[u8]) {
parse_vorbis_comment_block(data, self);
let other_blocks = extract_non_vorbis_blocks(&self.header);
let (header, vorbis_comment_offset, vorbis_comment_length) =
build_flac_header(other_blocks, self);
self.header = header;
self.vorbis_comment_offset = vorbis_comment_offset;
self.vorbis_comment_length = vorbis_comment_length;
}
}
/// Parse a 4-byte FLAC metadata block header into (is_last, block_type, data_length).
fn read_block_header(hdr: &[u8; 4]) -> (bool, u8, u64) {
let is_last = (hdr[0] & BLOCK_LAST_FLAG) != 0;
let block_type = hdr[0] & BLOCK_TYPE_MASK;
let length = u32::from_be_bytes([0, hdr[1], hdr[2], hdr[3]]) as u64;
(is_last, block_type, length)
}
struct FlacParsed {
other_blocks: Vec<(u8, Vec<u8>)>,
picture_block_headers: Vec<Vec<u8>>,
picture_data_ranges: Vec<(u64, u64)>,
audio_start: u64,
}
fn parse_flac(path: &Path) -> Option<FlacParsed> {
let mut f = fs::File::open(path).ok()?;
let mut magic = [0u8; 4];
f.read_exact(&mut magic).ok()?;
if &magic != b"fLaC" {
return None;
}
let mut other_blocks: Vec<(u8, Vec<u8>)> = vec![];
let mut picture_block_headers: Vec<Vec<u8>> = vec![];
let mut picture_data_ranges: Vec<(u64, u64)> = vec![];
let mut pos = 4u64;
loop {
let mut hdr = [0u8; 4];
f.read_exact(&mut hdr).ok()?;
let (is_last, block_type, length) = read_block_header(&hdr);
pos += 4;
if block_type == BLOCK_PICTURE {
// PICTURE: keep block header (we'll fix is_last later), record data range
picture_block_headers.push(hdr.to_vec());
picture_data_ranges.push((pos, length));
f.seek(SeekFrom::Current(length as i64)).ok()?;
} else {
let mut data = vec![0u8; length as usize];
f.read_exact(&mut data).ok()?;
// Skip VORBIS_COMMENT — we rebuild it
if block_type != BLOCK_VORBIS_COMMENT {
other_blocks.push((block_type, data));
}
}
pos += length;
if is_last {
break;
}
}
// Fix is_last on the last picture block header: it must be 1 when audio follows
if let Some(last_hdr) = picture_block_headers.last_mut() {
last_hdr[0] = BLOCK_LAST_FLAG | (last_hdr[0] & BLOCK_TYPE_MASK);
}
Some(FlacParsed {
other_blocks,
picture_block_headers,
picture_data_ranges,
audio_start: pos,
})
}
fn build_flac_header(blocks: Vec<(u8, Vec<u8>)>, metadata: &MusicMetadata) -> (Vec<u8>, u64, u64) {
let vorbis = build_vorbis_comment(metadata);
let vorbis_len = vorbis.len() as u64;
let mut patched: Vec<(u8, Vec<u8>)> = blocks;
patched.push((BLOCK_VORBIS_COMMENT, vorbis));
patched.push((BLOCK_PADDING, vec![0u8; PADDING_SIZE])); // allows metaflac in-place writes
let vorbis_idx = patched.len() - 2;
let has_pictures = !metadata.picture_data_ranges.is_empty();
let mut out = Vec::new();
out.extend_from_slice(b"fLaC");
let last = patched.len() - 1;
let mut vorbis_offset = 0u64;
for (i, (block_type, data)) in patched.iter().enumerate() {
let is_last_block = i == last && !has_pictures;
let flag: u8 = if is_last_block { BLOCK_LAST_FLAG } else { 0x00 };
let length = data.len() as u32;
if i == vorbis_idx {
vorbis_offset = out.len() as u64 + 4; // data starts after 4-byte block header
}
out.push(flag | block_type);
out.push((length >> 16) as u8);
out.push((length >> 8) as u8);
out.push(length as u8);
out.extend_from_slice(data);
}
(out, vorbis_offset, vorbis_len)
}
fn parse_vorbis_comment_block(data: &[u8], out: &mut MusicMetadata) {
let mut cursor = Cursor::new(data);
let mut len_bytes = [0u8; 4];
if cursor.read_exact(&mut len_bytes).is_err() {
return;
}
let vendor_len = u32::from_le_bytes(len_bytes) as i64;
if cursor.seek(SeekFrom::Current(vendor_len)).is_err() {
return;
}
if cursor.read_exact(&mut len_bytes).is_err() {
return;
}
let count = u32::from_le_bytes(len_bytes);
out.artist.clear();
out.other_tags.clear();
for _ in 0..count {
if cursor.read_exact(&mut len_bytes).is_err() {
break;
}
let comment_len = u32::from_le_bytes(len_bytes) as usize;
let mut comment_bytes = vec![0u8; comment_len];
if cursor.read_exact(&mut comment_bytes).is_err() {
break;
}
let comment = String::from_utf8_lossy(&comment_bytes).into_owned();
if let Some((key, value)) = comment.split_once('=') {
match key.to_ascii_uppercase().as_str() {
"TITLE" => out.track_title = value.to_string(),
"ALBUM" => out.album = value.to_string(),
"TRACKNUMBER" => out.track_number = value.parse().unwrap_or(0),
"ARTIST" => out.artist.push(value.to_string()),
_ => out.other_tags.push(comment),
}
}
}
}
fn extract_non_vorbis_blocks(header: &[u8]) -> Vec<(u8, Vec<u8>)> {
let mut cursor = Cursor::new(header);
let mut blocks = vec![];
let mut magic = [0u8; 4];
if cursor.read_exact(&mut magic).is_err() {
return blocks;
}
loop {
let mut hdr = [0u8; 4];
if cursor.read_exact(&mut hdr).is_err() {
break;
}
let (is_last, block_type, length) = read_block_header(&hdr);
let mut data = vec![0u8; length as usize];
if cursor.read_exact(&mut data).is_err() {
break;
}
if block_type != BLOCK_VORBIS_COMMENT && block_type != BLOCK_PADDING {
blocks.push((block_type, data));
}
if is_last {
break;
}
}
blocks
}
fn build_vorbis_comment(metadata: &MusicMetadata) -> Vec<u8> {
let vendor = b"musicfs";
let mut out = Vec::new();
out.extend_from_slice(&(vendor.len() as u32).to_le_bytes());
out.extend_from_slice(vendor);
let mut comments: Vec<String> = vec![
format!("TITLE={}", metadata.track_title),
format!("ALBUM={}", metadata.album),
format!("TRACKNUMBER={}", metadata.track_number),
];
for artist in &metadata.artist {
comments.push(format!("ARTIST={}", artist));
}
comments.extend(metadata.other_tags.iter().cloned());
out.extend_from_slice(&(comments.len() as u32).to_le_bytes());
for comment in &comments {
let bytes = comment.as_bytes();
out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
out.extend_from_slice(bytes);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_block_header_normal() {
let hdr = [0x04, 0x00, 0x01, 0x00];
let (is_last, block_type, length) = read_block_header(&hdr);
assert!(!is_last);
assert_eq!(block_type, 4);
assert_eq!(length, 256);
}
#[test]
fn read_block_header_last() {
let hdr = [0x84, 0x00, 0x00, 0x10];
let (is_last, block_type, length) = read_block_header(&hdr);
assert!(is_last);
assert_eq!(block_type, 4);
assert_eq!(length, 16);
}
#[test]
fn build_vorbis_comment_roundtrip() {
let mut metadata = MusicMetadata::default();
metadata.artist = vec!["Artist1".to_string(), "Artist2".to_string()];
metadata.album = "Album".to_string();
metadata.track_number = 3;
metadata.track_title = "Title".to_string();
metadata.other_tags = vec!["GENRE=Rock".to_string()];
let vorbis_bytes = build_vorbis_comment(&metadata);
let mut parsed = MusicMetadata::default();
parse_vorbis_comment_block(&vorbis_bytes, &mut parsed);
assert_eq!(parsed.artist, vec!["Artist1", "Artist2"]);
assert_eq!(parsed.album, "Album");
assert_eq!(parsed.track_number, 3);
assert_eq!(parsed.track_title, "Title");
assert_eq!(parsed.other_tags, vec!["GENRE=Rock"]);
}
#[test]
fn parse_vorbis_comment_multi_artist() {
let mut vorbis_bytes = Vec::new();
let vendor = b"test";
vorbis_bytes.extend_from_slice(&(vendor.len() as u32).to_le_bytes());
vorbis_bytes.extend_from_slice(vendor);
let comments = vec!["ARTIST=Artist1", "ARTIST=Artist2"];
vorbis_bytes.extend_from_slice(&(comments.len() as u32).to_le_bytes());
for comment in &comments {
let bytes = comment.as_bytes();
vorbis_bytes.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
vorbis_bytes.extend_from_slice(bytes);
}
let mut metadata = MusicMetadata::default();
parse_vorbis_comment_block(&vorbis_bytes, &mut metadata);
assert_eq!(metadata.artist.len(), 2);
assert_eq!(metadata.artist[0], "Artist1");
assert_eq!(metadata.artist[1], "Artist2");
}
#[test]
fn extract_non_vorbis_blocks_strips_vc_and_padding() {
let mut header = Vec::new();
header.extend_from_slice(b"fLaC");
let streaminfo_data = vec![0u8; 34];
header.push(0x00);
header.extend_from_slice(&[0x00, 0x00, 0x22]);
header.extend_from_slice(&streaminfo_data);
let vorbis_data = vec![0u8; 50];
header.push(0x04);
header.extend_from_slice(&[0x00, 0x00, 0x32]);
header.extend_from_slice(&vorbis_data);
let padding_data = vec![0u8; 100];
header.push(0x81);
header.extend_from_slice(&[0x00, 0x00, 0x64]);
header.extend_from_slice(&padding_data);
let blocks = extract_non_vorbis_blocks(&header);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].0, 0);
assert_eq!(blocks[0].1.len(), 34);
}
#[test]
fn find_vorbis_offsets_correct() {
let mut metadata = MusicMetadata::default();
metadata.artist = vec!["TestArtist".to_string()];
metadata.album = "TestAlbum".to_string();
metadata.track_number = 1;
metadata.track_title = "TestTitle".to_string();
let other_blocks = vec![(0, vec![0u8; 34])];
let (header, expected_offset, expected_length) = build_flac_header(other_blocks, &metadata);
metadata.header = header;
metadata.find_vorbis_offsets();
assert_eq!(metadata.vorbis_comment_offset, expected_offset);
assert_eq!(metadata.vorbis_comment_length, expected_length);
}
}