Skip to main content

libcdio_rs/mmc/
read_subchannel.rs

1// Copyright (C) 2026 Shiva Kiran Koninty <shiva@skran.xyz>
2//
3// This file is part of libcdio-rs.
4//
5// libcdio-rs is free software: you can redistribute it and/or
6// modify it under the terms of the GNU General Public License as
7// published by the Free Software Foundation, either version 3 of the
8// License, or (at your option) any later version.
9//
10// libcdio-rs is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13// General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with libcdio-rs. If not, see <https://www.gnu.org/licenses/>.
17
18//! Routines based on MMC `READ SUB-CHANNEL`.
19
20use displaydoc::Display;
21use num_enum::{TryFromPrimitive, TryFromPrimitiveError};
22use thiserror::Error;
23use tracing::debug;
24use winnow::{
25    Parser,
26    binary::{
27        be_u16,
28        bits::{bits, bool, take as bits_take},
29        length_take, u8,
30    },
31    error::{ContextError, StrContext},
32    token::take,
33};
34
35use crate::{
36    Mmc,
37    mmc::{Cdb, MmcDirection, MmcError},
38};
39
40/// Routines based on MMC `READ SUB-CHANNEL`.
41impl Mmc {
42    /// Get the status of audio play operations.
43    pub fn audio_status(&self) -> Result<MmcAudioStatus, MmcAudioStatusError> {
44        let data =
45            self.read_subchannel(AddressFormat::Lba, SubchannelParameter::CdCurrentPosition)?;
46        let status = parse_header(&mut data.as_slice())?;
47
48        status.ok_or(MmcAudioStatusError::NotSupported)
49    }
50
51    /// Get the Media Catalog Number (UPC/bar code), if found.
52    pub fn media_catalog_number(&self) -> Result<Option<String>, MmcSubchannelError> {
53        let data = self.read_subchannel(AddressFormat::Lba, SubchannelParameter::Mcn)?;
54        let input = &mut data.as_slice();
55        parse_header(input)?;
56
57        let format_code =
58            u8::<_, ContextError>.verify(|code| *code == SubchannelParameter::Mcn.discriminant());
59        let mcval = bits(bool::<_, ContextError>);
60        let (_format_code, _, mcval, mcn, _zero, _aframe) = (
61            format_code,
62            take(RESERVED1_SIZE),
63            mcval,
64            take(MCN_SIZE),
65            u8.verify(|zero| *zero == 0),
66            u8, // aframe
67        )
68            .context(StrContext::Label("media catalog number descriptor"))
69            .parse_next(input)?;
70
71        if !mcval {
72            return Ok(None);
73        }
74        let mcn =
75            String::from_utf8(mcn.to_vec()).expect("mcn should not contain non utf8 characters");
76
77        return Ok(Some(mcn));
78
79        const RESERVED1_SIZE: usize = 3; // the field starts at index 1
80        const MCN_SIZE: usize = 13;
81    }
82
83    /// Get the International Standard Recording Code (ISRC) of given track number.
84    pub fn isrc(&self, track_number: TrackNumber) -> Result<Option<String>, MmcSubchannelError> {
85        let param = SubchannelParameter::Isrc {
86            track_number: track_number.0,
87        };
88        let data = self.read_subchannel(AddressFormat::Lba, param)?;
89        let input = &mut data.as_slice();
90        parse_header(input)?;
91
92        let format_code = u8::<_, ContextError>.verify(|code| *code == param.discriminant());
93        let adr_and_control = bits((
94            bits_take::<_, u8, _, ContextError>(ADR_BITS),
95            bits_take::<_, u8, _, _>(CONTROL_BITS),
96        ));
97        let tcval = bits(bool::<_, ContextError>);
98        let (_format_code, _adr_and_control, _track, _, tcval, isrc, _zero, _aframe) = (
99            format_code,
100            adr_and_control,
101            u8.verify(|track| *track == track_number.0),
102            take(RESERVED3_SIZE),
103            tcval,
104            take(ISRC_SIZE),
105            u8.verify(|zero| *zero == 0),
106            u8, // aframe
107        )
108            .context(StrContext::Label("isrc descriptor"))
109            .parse_next(input)?;
110
111        if !tcval {
112            return Ok(None);
113        }
114        let isrc =
115            String::from_utf8(isrc.to_vec()).expect("isrc should not contain non utf8 characters");
116
117        return Ok(Some(isrc));
118
119        const ADR_BITS: usize = 4;
120        const CONTROL_BITS: usize = 4;
121        const RESERVED3_SIZE: usize = 1;
122        const ISRC_SIZE: usize = 12;
123    }
124
125    /// Get the current position of the disc in time units.
126    pub fn cd_current_position(&self) -> Result<CdCurrentPosition, MmcSubchannelError> {
127        let param = SubchannelParameter::CdCurrentPosition;
128        let data = self.read_subchannel(AddressFormat::Time, param)?;
129        let input = &mut data.as_slice();
130        parse_header(input)?;
131
132        let format_code = u8::<_, ContextError>.verify(|code| *code == param.discriminant());
133        let adr_and_control = bits((
134            bits_take::<_, u8, _, ContextError>(ADR_BITS),
135            bits_take::<_, u8, _, _>(CONTROL_BITS),
136        ));
137        let (_, _adr_and_control, track, index, absolute_address, relative_address) = (
138            format_code,
139            adr_and_control,
140            u8, // track
141            u8, // index
142            take(ABSOLUTE_ADDR_SIZE),
143            take(RELATIVE_ADDR_SIZE),
144        )
145            .context(StrContext::Label("cd current position descriptor"))
146            .parse_next(input)?;
147
148        let absolute_position = TimePosition {
149            hour: absolute_address[0],
150            minute: absolute_address[1],
151            second: absolute_address[2],
152            frame: absolute_address[3],
153        };
154        let relative_position = TimePosition {
155            hour: relative_address[0],
156            minute: relative_address[1],
157            second: relative_address[2],
158            frame: relative_address[3],
159        };
160
161        return Ok(CdCurrentPosition {
162            track,
163            index,
164            absolute_position,
165            relative_position,
166        });
167
168        const ADR_BITS: usize = 4;
169        const CONTROL_BITS: usize = 4;
170        const ABSOLUTE_ADDR_SIZE: usize = 4;
171        const RELATIVE_ADDR_SIZE: usize = 4;
172    }
173
174    /// Perform an MMC `READ SUB-CHANNEL`.
175    fn read_subchannel(
176        &self,
177        address_format: AddressFormat,
178        param: SubchannelParameter,
179    ) -> Result<SubchannelData, MmcError> {
180        let mut data = SubchannelData::default();
181        let mut cdb = Cdb::default();
182        cdb[0] = OPCODE;
183        if let AddressFormat::Time = address_format {
184            cdb[1] |= 1 << TIME_BITPOS;
185        }
186        cdb[2] |= 1 << SUBQ_BITPOS; // always include Q sub-channel data
187        cdb[3] = param.discriminant(); // parameter list
188        if let SubchannelParameter::Isrc { track_number } = param {
189            cdb[6] = track_number;
190        }
191        cdb[7..9].copy_from_slice(&(data.len() as u16).to_be_bytes());
192
193        self.run_command(Some(MmcDirection::Read), &mut data, cdb)?;
194
195        return Ok(data);
196
197        const OPCODE: u8 = 0x42;
198        const SUBQ_BITPOS: usize = 6;
199        const TIME_BITPOS: usize = 1;
200    }
201}
202
203/// The status of audio play operations
204#[repr(u8)]
205#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, TryFromPrimitive)]
206pub enum MmcAudioStatus {
207    PlayInProgress = 0x11,
208    PlayPaused = 0x12,
209    PlayCompleted = 0x13,
210    PlayStopped = 0x14,
211
212    #[default]
213    NoStatus = 0x15,
214}
215
216/// error getting audio status via `READ SUB-CHANNEL`
217#[derive(Debug, Display, Error)]
218pub enum MmcAudioStatusError {
219    /// The device not support reporting audio status
220    NotSupported,
221
222    /// operating system returned an error: {0}
223    Cmd(#[from] MmcError),
224
225    /// invalid response from command
226    InvalidResponse(String),
227}
228impl From<MmcSubchannelError> for MmcAudioStatusError {
229    fn from(value: MmcSubchannelError) -> Self {
230        match value {
231            MmcSubchannelError::Cmd(error) => Self::Cmd(error),
232            MmcSubchannelError::InvalidResponse(error) => Self::InvalidResponse(error),
233        }
234    }
235}
236
237/// Format to use for address fields in the response of `READ SUB-CHANNEL`
238#[allow(unused)]
239enum AddressFormat {
240    Lba,
241    Time,
242}
243
244#[repr(u8)]
245#[allow(unused)]
246#[derive(Clone, Copy, Debug)]
247enum SubchannelParameter {
248    CdCurrentPosition = 0x1,
249    Mcn = 0x2,
250    Isrc { track_number: u8 } = 0x3,
251}
252impl SubchannelParameter {
253    fn discriminant(&self) -> u8 {
254        // SAFETY: Because `Self` is marked `repr(u8)`, its layout is a `repr(C)` `union`
255        // between `repr(C)` structs, each of which has the `u8` discriminant as its first
256        // field, so we can read the discriminant without offsetting the pointer.
257        // Source:
258        // https://doc.rust-lang.org/stable/std/mem/fn.discriminant.html#accessing-the-numeric-value-of-the-discriminant
259        unsafe { *(&raw const *self).cast::<u8>() }
260    }
261}
262
263type SubchannelData = [u8; 24];
264
265fn parse_header(input: &mut &[u8]) -> Result<Option<MmcAudioStatus>, MmcSubchannelError> {
266    debug!(header = ?input, "parse_header()");
267    let (_, audio_status, remainder) = (u8::<_, ContextError>, u8, length_take(be_u16))
268        .context(StrContext::Label("READ SUB-CHANNEL response header"))
269        .parse_next(input)?;
270    *input = remainder;
271
272    (audio_status != 0)
273        .then(|| MmcAudioStatus::try_from(audio_status))
274        .transpose()
275        .map_err(MmcSubchannelError::from)
276}
277
278/// error from a `READ SUB-CHANNEL` command
279#[non_exhaustive]
280#[derive(Debug, Display, Error)]
281pub enum MmcSubchannelError {
282    /// operating system returned an error
283    Cmd(#[from] MmcError),
284
285    /// invalid response from mmc command: {0}
286    InvalidResponse(String),
287}
288impl From<ContextError> for MmcSubchannelError {
289    fn from(err: ContextError) -> Self {
290        Self::InvalidResponse(err.to_string())
291    }
292}
293impl<T: TryFromPrimitive> From<TryFromPrimitiveError<T>> for MmcSubchannelError {
294    fn from(err: TryFromPrimitiveError<T>) -> Self {
295        Self::InvalidResponse(err.to_string())
296    }
297}
298
299/// Track number.
300///
301/// Values must be between 1 and 99.
302/// Use [`TrackNumber::try_from`] to construct a new value.
303#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
304pub struct TrackNumber(u8);
305impl TryFrom<u8> for TrackNumber {
306    type Error = InvalidTrackNumber;
307
308    /// Construct `Self` from given track number.
309    ///
310    /// # Errors
311    /// If the track number not within the range of 1 to 99, inclusive.
312    fn try_from(track_number: u8) -> Result<Self, Self::Error> {
313        if (1..=99).contains(&track_number) {
314            Ok(Self(track_number))
315        } else {
316            Err(InvalidTrackNumber(track_number))
317        }
318    }
319}
320impl Default for TrackNumber {
321    fn default() -> Self {
322        Self(1)
323    }
324}
325/// invalid track number '{0}'; value must be between 1 and 99
326#[derive(Debug, Display, Error)]
327pub struct InvalidTrackNumber(u8);
328
329/// CD current position information.
330#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
331pub struct CdCurrentPosition {
332    /// track number
333    pub track: u8,
334
335    /// index number
336    pub index: u8,
337
338    /// position relative to the logical beginning of the media
339    pub absolute_position: TimePosition,
340
341    /// position relative to the logical beginning of the current track
342    pub relative_position: TimePosition,
343}
344/// A CD position, expressed in time units
345#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
346pub struct TimePosition {
347    pub hour: u8,
348    pub minute: u8,
349    pub second: u8,
350    pub frame: u8,
351}
352
353#[cfg(test)]
354mod tests {
355    use tracing::info;
356
357    use super::*;
358
359    #[test_log::test(test)]
360    #[ignore = "requires a disc drive with mmc"]
361    fn audio_status() {
362        let audio_status = Mmc::new().unwrap().audio_status();
363        info!(?audio_status);
364        assert!(matches!(
365            audio_status,
366            Ok(_) | Err(MmcAudioStatusError::NotSupported)
367        ));
368    }
369
370    #[test_log::test(test)]
371    #[ignore = "requires a disc drive with mmc"]
372    fn media_catalog_number() {
373        let mcn = Mmc::new().unwrap().media_catalog_number().unwrap();
374        info!(?mcn);
375    }
376
377    #[test_log::test(test)]
378    #[ignore = "requires a disc drive with mmc"]
379    fn isrc() {
380        let isrc = Mmc::new().unwrap().isrc(TrackNumber::default()).unwrap();
381        info!(?isrc);
382    }
383
384    #[test_log::test(test)]
385    #[ignore = "requires a disc drive with mmc"]
386    fn cd_current_position() {
387        let cd_current_position = Mmc::new().unwrap().cd_current_position().unwrap();
388        info!(?cd_current_position);
389    }
390}