1use anyhow::{bail, Context};
2use int_conv::Truncate;
3use metaflac::{
4 block::{Picture, PictureType, StreamInfo, VorbisComment},
5 Block,
6};
7use more_asserts as ma;
8use std::{
9 borrow::Cow,
10 fmt::Debug,
11 fs::{create_dir_all, File},
12 io::Write,
13 num::NonZeroU32,
14 path::{Path, PathBuf},
15 str::FromStr,
16};
17use symphonia_bundle_flac::FlacReader;
18use symphonia_core::{
19 checksum::{Crc16Ansi, Crc8Ccitt},
20 formats::{Cue, FormatReader, Packet},
21 io::{MediaSourceStream, Monitor, ReadBytes},
22 meta::{StandardVisualKey, Tag, Value, Visual},
23};
24use tracing::{debug, info, instrument};
25
26#[instrument(skip(base_path, metadata_padding), err)]
27pub fn split_one_file<P: AsRef<Path> + Debug, B: AsRef<Path> + Debug>(
28 input_path: P,
29 base_path: B,
30 metadata_padding: u32,
31) -> anyhow::Result<Vec<PathBuf>> {
32 let file = File::open(&input_path).with_context(|| format!("opening {:?}", input_path))?;
33 let file_length = file.metadata().context("file metadata")?.len();
34 let mss = MediaSourceStream::new(Box::new(file), Default::default());
35 let mut reader =
36 FlacReader::try_new(mss, &Default::default()).context("could not create flac reader")?;
37 debug!("tracks: {:?}", reader.tracks());
38 let track = reader.default_track().context("no default track")?;
39 let data = match &track.codec_params.extra_data {
40 Some(it) => it,
41 _ => bail!("Unclear track codec params - Not a flac file?"),
42 };
43 let info = StreamInfo::from_bytes(data);
44 let cues = reader.cues().to_vec();
45 let time_base = track.codec_params.time_base.context("track time base")?;
46 if time_base.numer != 1 {
47 bail!(
48 "track time_base numerator should be a fraction like 1/44000, instead {:?}",
49 time_base
50 );
51 }
52 if time_base.denom != info.sample_rate {
53 bail!("track time_base denominator ({:?}) should be the same as the overall streaminfo ({:?})", time_base, info.sample_rate);
54 }
55 let last_ts: u64 = info.total_samples;
59
60 let mut track_paths = vec![];
61 let mut cue_iter = cues.iter().peekable();
62 let mut audio_buffer = Vec::with_capacity(file_length.try_into().unwrap());
63 while let Some(cue) = cue_iter.next() {
64 audio_buffer.clear();
65 let next = cue_iter.peek();
66 let end_ts = match next {
67 None => last_ts, Some(track) if track.index == LEAD_OUT_TRACK_NUMBER => {
69 let end_ts = track.start_ts;
71 cue_iter.next();
72 end_ts
73 }
74 Some(track) => track.start_ts,
75 };
76 let track = {
77 let metadata = reader.metadata();
78 let current_metadata = metadata.current().context("track tags")?;
79 let tags = current_metadata.tags();
80 let visuals = current_metadata.visuals();
81 Track::from_tags(&info, cue, end_ts, tags, visuals)
82 };
83 debug!(number = track.number, output = ?track.pathname(), "Track");
84 let pathbuf = base_path.as_ref().join(track.pathname());
85 let path = &pathbuf;
86 if let Some(parent) = path.parent() {
87 create_dir_all(parent).context("creating album dir")?;
88 }
89 let mut f = File::create(path).unwrap();
90 let sample_count = track
91 .write_audio(&mut reader, &mut audio_buffer)
92 .with_context(|| format!("buffering track {:?} audio", path))?;
93
94 track
95 .write_metadata(sample_count, metadata_padding, &mut f)
96 .with_context(|| format!("writing track {:?}", path))?;
97 f.write_all(&audio_buffer)
98 .with_context(|| format!("writing track {:?} audio", path))?;
99 track_paths.push(pathbuf);
100 }
101 info!("Done with disc image");
102 Ok(track_paths)
103}
104
105pub const LEAD_OUT_TRACK_NUMBER: u32 = 170;
107
108#[derive(Clone)]
110pub struct Track {
111 streaminfo: StreamInfo,
112 pub number: u32,
113 pub start_ts: u64,
114 pub end_ts: u64,
115 pub tags: Vec<Tag>,
116 pub visuals: Vec<Visual>,
117}
118
119impl std::fmt::Debug for Track {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 f.debug_struct("Track")
122 .field("number", &self.number)
123 .field("start_ts", &self.start_ts)
124 .field("end_ts", &self.end_ts)
125 .field("tags", &self.tags)
126 .finish()
127 }
128}
129
130impl Track {
131 fn interesting_tag(name: &str) -> bool {
132 !name.ends_with(']') && name != "CUESHEET" && name != "LOG"
133 }
134
135 pub fn from_tags(
137 streaminfo: &StreamInfo,
138 cue: &Cue,
139 end_ts: u64,
140 tags: &[Tag],
141 visuals: &[Visual],
142 ) -> Self {
143 let suffix = format!("[{}]", cue.index);
144 let tags = tags
145 .iter()
146 .filter_map(|tag| {
147 let tag_name = if tag.key.ends_with(&suffix) {
148 Some(&tag.key[0..(tag.key.len() - suffix.len())])
149 } else if Self::interesting_tag(&tag.key) {
150 Some(tag.key.as_str())
151 } else {
152 None
153 };
154 tag_name.map(|key| Tag::new(tag.std_key, key, tag.value.clone()))
155 })
156 .collect();
157 let visuals = visuals.to_vec();
158 Self {
159 streaminfo: StreamInfo {
160 md5: [0u8; 16].to_vec(),
161 total_samples: (end_ts - cue.start_ts),
162 ..streaminfo.clone()
163 },
164 number: cue.index,
165 start_ts: cue.start_ts,
166 end_ts,
167 tags,
168 visuals,
169 }
170 }
171
172 pub fn tag_value(&self, name: &str) -> Option<&Value> {
174 self.tags
175 .iter()
176 .find(|tag| tag.key == name)
177 .map(|found| &found.value)
178 }
179
180 fn is_risky_char(c: char) -> bool {
181 match c {
182 ' ' | '_' | '-' | ',' | '.' | '!' | '&' | '(' | ')' | '[' | ']' | '{' | '}' | '<'
186 | '>' => false,
187 _ => !c.is_alphanumeric(),
188 }
189 }
190
191 fn sanitize_pathname(name: &str) -> Cow<str> {
192 if name.contains(Self::is_risky_char) {
193 Cow::Owned(name.replace(Self::is_risky_char, "_"))
194 } else {
195 Cow::Borrowed(name)
196 }
197 }
198
199 pub fn pathname(&self) -> PathBuf {
201 let mut buf = PathBuf::new();
202 if let Some(Value::String(artist)) = self.tag_value("ALBUMARTIST") {
203 buf.push(Self::sanitize_pathname(artist).as_ref());
204 } else if let Some(Value::String(artist)) = self.tag_value("ARTIST") {
205 buf.push(Self::sanitize_pathname(artist).as_ref());
206 } else {
207 buf.push("Unknown Artist");
208 }
209
210 if let Some(Value::String(album)) = self.tag_value("ALBUM") {
211 if let Some(Value::String(year)) = self.tag_value("DATE") {
212 buf.push(format!("{} - {}", year, Self::sanitize_pathname(album)));
213 } else {
214 buf.push(Self::sanitize_pathname(album).as_ref());
215 }
216 } else {
217 buf.push("Unknown Album");
218 }
219
220 if let (Some(Value::String(track)), Some(Value::String(title))) =
221 (self.tag_value("TRACKNUMBER"), self.tag_value("TITLE"))
222 {
223 if let Ok(trackno) = <usize as FromStr>::from_str(track) {
224 buf.push(format!(
225 "{:02}.{}.flac",
226 trackno,
227 Self::sanitize_pathname(title)
228 ));
229 } else {
230 buf.push(format!("99.{}.flac", Self::sanitize_pathname(title)));
231 }
232 }
233 buf
234 }
235
236 #[instrument(skip(self, to), fields(number = self.number, path = ?self.pathname()), err)]
241 pub fn write_metadata<S: Write>(
242 &self,
243 total_samples: u64,
244 metadata_padding: u32,
245 mut to: S,
246 ) -> anyhow::Result<()> {
247 to.write_all(b"fLaC")?;
248 let comment = VorbisComment {
249 vendor_string: "flac-tracksplit".to_string(),
250 comments: self
251 .tags
252 .iter()
253 .map(|tag| (tag.key.to_string(), vec![tag.value.to_string()]))
254 .collect(),
255 };
256 let pictures: Vec<Block> = self
257 .visuals
258 .iter()
259 .map(|visual| {
260 Block::Picture(Picture {
261 picture_type: translate_visual_key(
262 visual.usage.unwrap_or(StandardVisualKey::OtherIcon),
263 ),
264 mime_type: visual.media_type.to_string(),
265 description: "".to_string(),
266 width: visual.dimensions.map(|s| s.width).unwrap_or(0),
267 height: visual.dimensions.map(|s| s.height).unwrap_or(0),
268 depth: visual.bits_per_pixel.map(NonZeroU32::get).unwrap_or(0),
269 num_colors: match visual.color_mode {
270 Some(symphonia_core::meta::ColorMode::Discrete) => 0,
271 Some(symphonia_core::meta::ColorMode::Indexed(n)) => n.get(),
272 None => 0,
273 },
274 data: visual.data.to_vec(),
275 })
276 })
277 .collect();
278 let mut streaminfo = self.streaminfo.clone();
279 if total_samples != streaminfo.total_samples {
280 debug!(
285 inferred = streaminfo.total_samples,
286 actual = total_samples,
287 duration_diff_s = (streaminfo.total_samples as f32 - total_samples as f32)
288 / (streaminfo.sample_rate as f32),
289 "inferred and actual total samples differ."
290 );
291 }
292 streaminfo.total_samples = total_samples;
293 let headers = vec![Block::StreamInfo(streaminfo), Block::VorbisComment(comment)];
294 for block in headers.into_iter().chain(pictures.into_iter()) {
295 block
296 .write_to(false, &mut to)
297 .with_context(|| format!("writing block {:?}", block))?;
298 }
299 Block::Padding(metadata_padding)
300 .write_to(true, &mut to)
301 .context("writing padding")?;
302 Ok(())
303 }
304
305 #[instrument(skip(self, from, to), fields(number = self.number, path = ?self.pathname()), err)]
309 pub fn write_audio<S: Write>(&self, from: &mut FlacReader, mut to: S) -> anyhow::Result<u64> {
310 let mut last_end: u64 = 0;
315 let mut frame = OffsetFrame::default();
316 loop {
317 let packet = from
318 .next_packet()
319 .with_context(|| format!("last end: {:?} vs {:?}", last_end, self.end_ts))?;
320
321 let ts = packet.ts;
322 let dur = packet.dur;
323 ma::assert_ge!(
324 ts,
325 self.start_ts,
326 "Packet timestamp is not >= this track's start ts. Potential bug exposed by the previous track.",
327 );
328
329 let updated_buf = frame
335 .process(packet)
336 .with_context(|| format!("processing frame at ts {}", ts))?;
337 to.write_all(&updated_buf)?;
338
339 last_end = ts + dur;
340 if last_end >= self.end_ts {
341 return Ok(frame.samples_processed);
342 }
343 }
344 }
345}
346
347fn translate_visual_key(key: StandardVisualKey) -> PictureType {
348 use PictureType::*;
349 match key {
350 StandardVisualKey::FileIcon => Icon,
351 StandardVisualKey::OtherIcon => Other,
352 StandardVisualKey::FrontCover => CoverFront,
353 StandardVisualKey::BackCover => CoverBack,
354 StandardVisualKey::Leaflet => Leaflet,
355 StandardVisualKey::Media => Media,
356 StandardVisualKey::LeadArtistPerformerSoloist => LeadArtist,
357 StandardVisualKey::ArtistPerformer => Artist,
358 StandardVisualKey::Conductor => Conductor,
359 StandardVisualKey::BandOrchestra => Band,
360 StandardVisualKey::Composer => Composer,
361 StandardVisualKey::Lyricist => Lyricist,
362 StandardVisualKey::RecordingLocation => RecordingLocation,
363 StandardVisualKey::RecordingSession => DuringRecording,
364 StandardVisualKey::Performance => DuringPerformance,
365 StandardVisualKey::ScreenCapture => ScreenCapture,
366 StandardVisualKey::Illustration => Illustration,
367 StandardVisualKey::BandArtistLogo => BandLogo,
368 StandardVisualKey::PublisherStudioLogo => PublisherLogo,
369 }
370}
371
372#[derive(Default)]
379pub struct OffsetFrame {
380 initial_offset: Option<u64>,
381 samples_processed: u64,
382}
383
384impl OffsetFrame {
385 pub fn process(&mut self, packet: Packet) -> anyhow::Result<Vec<u8>> {
391 let mut frame_reader = packet.as_buf_reader();
392 let mut header_crc = Crc8Ccitt::new(0);
393 let mut footer_crc = Crc16Ansi::new(0);
394 let mut frame_out = Vec::with_capacity(packet.buf().len());
395
396 let sync = frame_reader.read_be_u16().context("reading frame sync")?;
398 let sync_u8 = sync.to_be_bytes();
399 header_crc.process_double_bytes(sync_u8);
400 footer_crc.process_double_bytes(sync_u8);
401 frame_out.write_all(&sync_u8)?;
402
403 let desc = frame_reader.read_be_u16().context("reading frame desc")?;
405 let desc_u8 = desc.to_be_bytes();
406 header_crc.process_double_bytes(desc_u8);
407 footer_crc.process_double_bytes(desc_u8);
408 frame_out.write_all(&desc_u8)?;
409 let block_size_enc = u32::from((desc & 0xf000) >> 12);
410 let sample_rate_enc = u32::from((desc & 0x0f00) >> 8);
411
412 let (orig_sample_offset, _sample_n_bytes) =
414 utf8_decode_be_u64(&mut frame_reader).context("decoding the sample offset")?;
415 if self.initial_offset.is_none() {
416 debug!(orig_sample_offset, "first sample offset");
417 self.initial_offset.replace(orig_sample_offset);
418 }
419 let sample_offset = orig_sample_offset - self.initial_offset.unwrap();
420 let offset_u8 = utf8_encode_be_u64(sample_offset).context("encoding the new offset")?;
421 header_crc.process_buf_bytes(&offset_u8);
422 footer_crc.process_buf_bytes(&offset_u8);
423 frame_out.write_all(&offset_u8)?;
424
425 let block_samples: u64 = match block_size_enc & 0b1111 {
429 0b0110 => {
430 let bs_u8 = frame_reader.read_u8().context("8bit block size")?;
432 header_crc.process_byte(bs_u8);
433 footer_crc.process_byte(bs_u8);
434 frame_out.write_all(&[bs_u8])?;
435 bs_u8.into()
436 }
437 0b0111 => {
438 let bs = frame_reader.read_be_u16().context("8bit block size")?;
440 let bs_u8 = bs.to_be_bytes();
441 header_crc.process_double_bytes(bs_u8);
442 footer_crc.process_double_bytes(bs_u8);
443 frame_out.write_all(&bs_u8)?;
444 bs.into()
445 }
446 0b0001 => 192,
447 0b0000 => bail!("reserved sample count"),
448 b if b & 0b1000 != 0 => 256 * 2u64.pow((b & 0b1111) - 8),
449 b => 576 * 2u64.pow((b & 0b111) - 2),
450 };
451 match sample_rate_enc & 0b1111 {
452 0b1100 => {
453 let sr_u8 = frame_reader.read_u8().context("8bit block size")?;
455 header_crc.process_byte(sr_u8);
456 footer_crc.process_byte(sr_u8);
457 frame_out.write_all(&[sr_u8])?;
458 }
459 0b1101 | 0b1110 => {
460 let bs = frame_reader.read_be_u16().context("8bit block size")?;
462 let sr_u8 = bs.to_be_bytes();
463 header_crc.process_double_bytes(sr_u8);
464 footer_crc.process_double_bytes(sr_u8);
465 frame_out.write_all(&sr_u8)?;
466 }
467 0b1111 => anyhow::bail!("invalid sample rate: sync-fooling string of 1s"),
468 _ => {
469 }
471 }
472
473 let _original_header_crc = frame_reader.read_u8().context("reading header CRC")?;
475 let my_header_crc = header_crc.crc();
476 footer_crc.process_byte(my_header_crc);
477 frame_out.write_all(&[my_header_crc])?;
478
479 let remainder = frame_reader.read_buf_bytes_available_ref();
481 let subframes = &remainder[..remainder.len() - 2];
482 footer_crc.process_buf_bytes(subframes);
483 frame_out.write_all(subframes)?;
484
485 let my_footer_crc = footer_crc.crc();
486 let my_footer_crc_u8 = my_footer_crc.to_be_bytes();
487 frame_out.write_all(&my_footer_crc_u8)?;
488 self.samples_processed += block_samples;
489 Ok(frame_out)
490 }
491}
492
493fn utf8_decode_be_u64<B: symphonia_core::io::ReadBytes>(src: &mut B) -> anyhow::Result<(u64, u32)> {
499 let mut state = u64::from(src.read_u8()?);
501
502 let mask: u8 = match state {
508 0x00..=0x7f => return Ok((state, 1)),
509 0xc0..=0xdf => 0x1f,
510 0xe0..=0xef => 0x0f,
511 0xf0..=0xf7 => 0x07,
512 0xf8..=0xfb => 0x03,
513 0xfc..=0xfd => 0x01,
514 0xfe => 0x00,
515 _ => anyhow::bail!("invalid utf-8 encoded sample/frame number"),
516 };
517
518 state &= u64::from(mask);
520
521 for _i in 2..mask.leading_zeros() {
526 state = (state << 6) | u64::from(src.read_u8()? & 0x3f);
531
532 }
534
535 Ok((state, mask.leading_zeros() - 1))
536}
537
538fn utf8_encode_be_u64(input: u64) -> anyhow::Result<Vec<u8>> {
539 let mut number = input;
540 let start_mask: u8 = match number.leading_zeros() {
541 57..=64 => return Ok(vec![number.truncate()]),
542 53..=56 => 0b1100_0000,
543 48..=52 => 0b1110_0000,
544 44..=47 => 0b1111_0000,
545 39..=43 => 0b1111_1000,
546 34..=38 => 0b1111_1100,
547 29..=33 => 0b1111_1110,
548 00..=28 | 65..=u32::MAX => unreachable!("can't encode more than 7 leading bits"),
549 };
550 debug_assert!(
551 64 - input.leading_zeros()
552 <= (start_mask.count_ones() - 1) * 6 + (start_mask.count_zeros() - 1),
553 "Number with {} set bits ({:b}) to be represented in {} bits",
554 64 - input.leading_zeros(),
555 input,
556 (start_mask.count_ones() - 1) * 6 + (start_mask.count_zeros() - 1)
557 );
558 let len: usize = start_mask.count_ones() as usize;
559 debug!(
560 len,
561 start_mask,
562 leading_zeros = input.leading_zeros(),
563 "allocating a vec for mask",
564 );
565 let mut val = vec![0; len];
566 let mut posn = len - 1;
567 while posn > 0 {
568 let byte: u8 = number.truncate();
569 val[posn] = (byte & 0x3f) | 0b10000000;
570 number >>= 6;
571 posn -= 1;
572 }
573 let byte: u8 = number.truncate();
574 val[0] = byte | start_mask;
575 debug_assert_eq!(
576 0,
577 byte >> (start_mask.count_zeros() - 1) as usize,
578 "Should have 0 set bits left"
579 );
580 Ok(val)
581}
582
583#[cfg(test)]
584mod test {
585 use super::*;
586 use proptest::{prop_assert_eq, proptest};
587 use std::fmt;
588 use symphonia_core::io::BufReader;
589
590 struct V<'a>(&'a [u8]);
591
592 impl<'a> fmt::Binary for V<'a> {
593 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
594 let vec = &self.0;
597
598 for (count, n) in vec.iter().enumerate() {
601 if count != 0 {
602 write!(f, " ")?;
603 }
604
605 write!(f, "{:b}", n)?;
606 }
607
608 Ok(())
609 }
610 }
611
612 proptest! {
613 #[test]
614 fn test_encoding(input in 0..(2u64.pow(35))) {
615 let encoded = utf8_encode_be_u64(input).expect("encoding");
616 let mut buf = BufReader::new(&encoded);
617 let decoded = utf8_decode_be_u64(&mut buf).unwrap_or_else(|_| panic!("decoding {:b}", V(&encoded)));
618 prop_assert_eq!((input, encoded.len() as u32), decoded,
619 "received:\n{:#064b} but wanted:\n{:#064b}",
620 decoded.0, input);
621 }
622 }
623}