use crate::cue::Msf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Control(pub u8);
impl Control {
#[must_use]
pub fn is_data(self) -> bool {
self.0 & 0b0100 != 0
}
#[must_use]
pub fn copy_permitted(self) -> bool {
self.0 & 0b0010 != 0
}
#[must_use]
pub fn four_channel(self) -> bool {
self.0 & 0b1000 != 0
}
#[must_use]
pub fn pre_emphasis(self) -> bool {
self.0 & 0b0001 != 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrackNo {
Track(u8),
LeadOut,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QData {
Position { track: TrackNo, index: u8, relative: Msf, absolute: Msf },
Catalog(String),
Isrc(String),
Other(u8),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QFrame {
pub control: Control,
pub adr: u8,
pub data: QData,
}
#[must_use]
pub fn extract_q(subchannel: &[u8]) -> Option<[u8; 12]> {
if subchannel.len() < 96 {
return None;
}
let mut q = [0u8; 12];
for (bit, &byte) in subchannel[..96].iter().enumerate() {
if byte & 0b0100_0000 != 0 {
q[bit / 8] |= 1 << (7 - (bit % 8));
}
}
Some(q)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct QSummary {
pub catalog: Option<String>,
pub isrcs: std::collections::BTreeMap<u8, String>,
}
#[must_use]
pub fn summarize_sub(sub: &[u8]) -> QSummary {
let frames = sub
.chunks_exact(96)
.filter_map(extract_q)
.filter(|raw| q_crc_valid(raw))
.filter_map(|raw| decode_q(&raw));
summarize_q(frames)
}
#[must_use]
pub fn summarize_q<I: IntoIterator<Item = QFrame>>(frames: I) -> QSummary {
let mut summary = QSummary::default();
let mut current_track: Option<u8> = None;
for frame in frames {
match frame.data {
QData::Position { track: TrackNo::Track(n), .. } => current_track = Some(n),
QData::Position { track: TrackNo::LeadOut, .. } => current_track = None,
QData::Catalog(mcn) => {
summary.catalog.get_or_insert(mcn);
}
QData::Isrc(code) => {
if let Some(n) = current_track {
summary.isrcs.entry(n).or_insert(code);
}
}
QData::Other(_) => {}
}
}
summary
}
#[must_use]
pub fn q_crc_valid(frame: &[u8]) -> bool {
if frame.len() < 12 {
return false;
}
let computed = crate::cdtext::crc16_ccitt(&frame[0..10]) ^ 0xFFFF;
let stored = u16::from_be_bytes([frame[10], frame[11]]);
computed == stored
}
#[must_use]
pub fn decode_q(frame: &[u8]) -> Option<QFrame> {
if frame.len() < 10 {
return None;
}
let control = Control(frame[0] >> 4);
let adr = frame[0] & 0x0F;
let q = &frame[1..10];
let data = match adr {
1 => {
let track = if q[0] == 0xAA { TrackNo::LeadOut } else { TrackNo::Track(bcd(q[0])) };
QData::Position {
track,
index: bcd(q[1]),
relative: Msf { minutes: bcd(q[2]), seconds: bcd(q[3]), frames: bcd(q[4]) },
absolute: Msf { minutes: bcd(q[6]), seconds: bcd(q[7]), frames: bcd(q[8]) },
}
}
2 => {
let mut s = String::with_capacity(13);
for i in 0..13 {
let byte = q[i / 2];
let nib = if i % 2 == 0 { byte >> 4 } else { byte & 0x0F };
s.push((b'0' + (nib % 10)) as char);
}
QData::Catalog(s)
}
3 => QData::Isrc(decode_isrc(q)),
other => QData::Other(other),
};
Some(QFrame { control, adr, data })
}
fn decode_isrc(q: &[u8]) -> String {
let cells = [
q[0] >> 2,
((q[0] & 0x03) << 4) | (q[1] >> 4),
((q[1] & 0x0F) << 2) | (q[2] >> 6),
q[2] & 0x3F,
q[3] >> 2,
];
let mut s = String::with_capacity(12);
for &c in &cells {
s.push(isrc_char(c));
}
for &(byte, high) in
&[(4, true), (4, false), (5, true), (5, false), (6, true), (6, false), (7, true)]
{
let nib = if high { q[byte] >> 4 } else { q[byte] & 0x0F };
s.push((b'0' + (nib % 10)) as char);
}
s
}
fn isrc_char(code: u8) -> char {
match code {
0x00..=0x09 => (b'0' + code) as char,
0x11..=0x2A => (b'A' + (code - 0x11)) as char,
_ => '?',
}
}
fn bcd(b: u8) -> u8 {
(b >> 4) * 10 + (b & 0x0F)
}