Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_io/
cd.rs

1//! CD-ROM drive: commands, responses, sector polling, CD-DA playback in
2//! [`audio`], XA-ADPCM music in [`xa`] and polled data-sector reads in
3//! [`reader`].
4//!
5//! One driver owns the controller: the [`Cd`] token. Every command and
6//! register step is a method taking `&mut Cd`, so the borrow checker sees a
7//! second driver that tries to program the controller while one is mid-command.
8//! [`xa::Player`] and [`reader::SectorReader`] hold the token while they exist
9//! and give it back with `release`.
10//!
11//! The controller exposes four byte registers selected by the low two
12//! bits of the index register at [`psx_hw::cd::reg::INDEX`]. Register addresses, command bytes
13//! and status bits live in [`psx_hw::cd`].
14
15pub mod audio;
16pub mod reader;
17pub mod xa;
18
19use crate::periph::Cd;
20use crate::{irq, read_u8, write_u8};
21
22use psx_hw::cd::index_status as status;
23use psx_hw::cd::irq as code;
24use psx_hw::cd::{reg, request, volume_apply};
25use psx_hw::cd::{
26    CMD_DEMUTE, CMD_GETLOCP, CMD_GETSTAT, CMD_MUTE, CMD_PAUSE, CMD_PLAY, CMD_READN, CMD_SETLOC,
27    CMD_SETMODE, CMD_STOP, STAT_PLAYING, STAT_SEEKING,
28};
29
30/// Response bytes a sector probe discards at most. The FIFO holds 16, but a
31/// controller wedged "not empty" must not be able to spin the drain forever.
32const SECTOR_POLL_DRAIN_LIMIT: u32 = 256;
33
34/// Fixed-size command response.
35#[derive(Copy, Clone, Debug, PartialEq, Eq)]
36pub struct Response {
37    bytes: [u8; 16],
38    len: usize,
39}
40
41impl Response {
42    /// A response with no bytes, what a deprecated forwarder hands back where
43    /// the command it wraps now reports a [`CdError`].
44    pub const fn empty() -> Self {
45        Response {
46            bytes: [0; 16],
47            len: 0,
48        }
49    }
50
51    /// Number of response bytes captured.
52    pub const fn len(&self) -> usize {
53        self.len
54    }
55
56    /// Whether no response bytes were captured.
57    pub const fn is_empty(&self) -> bool {
58        self.len == 0
59    }
60
61    /// Response bytes in FIFO order.
62    pub fn bytes(&self) -> &[u8] {
63        &self.bytes[..self.len]
64    }
65}
66
67/// Why a CD command produced no response.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum CdError {
70    /// The controller did not free a parameter slot or answer within the poll
71    /// budget (no drive, no disc, or a wedged controller).
72    Timeout,
73    /// The drive answered with an error (INT5).
74    DriveError,
75}
76
77/// Poll budget the blocking commands give the controller for each wait (a
78/// parameter slot, then the acknowledge). Generous: the controller frees a
79/// slot and acknowledges within microseconds to milliseconds, so this is a
80/// hang guard, not a latency.
81pub const DEFAULT_COMMAND_SPINS: u32 = 131_072;
82
83impl Cd {
84    /// Send a command and return its first response packet, waiting at most
85    /// [`DEFAULT_COMMAND_SPINS`] polls for each step.
86    ///
87    /// Returns [`CdError::Timeout`] when the controller never answers and
88    /// [`CdError::DriveError`] when the drive reports an error. A timed-out
89    /// command leaves CD-ROM IRQ output masked, as [`Self::try_command`] does.
90    pub fn command(&mut self, command: u8, params: &[u8]) -> Result<Response, CdError> {
91        self.command_within(command, params, DEFAULT_COMMAND_SPINS)
92    }
93
94    /// [`Self::command`] with an explicit poll budget per step.
95    pub fn command_within(
96        &mut self,
97        command: u8,
98        params: &[u8],
99        spin_limit: u32,
100    ) -> Result<Response, CdError> {
101        run_command(&mut Mmio, command, params, spin_limit)
102    }
103
104    /// Try to send a command and capture its first response packet.
105    ///
106    /// Returns `None` if the controller does not expose parameter room or
107    /// a response within `spin_limit` polls, or if the drive reports an error
108    /// ([`Self::command_within`] tells the two apart). Use this for gameplay paths
109    /// where a not-ready drive should not stall rendering forever. If a
110    /// dispatched command times out, CD-ROM IRQ output remains masked so a
111    /// late ACK cannot interrupt a polling caller.
112    pub fn try_command(&mut self, command: u8, params: &[u8], spin_limit: u32) -> Option<Response> {
113        self.command_within(command, params, spin_limit).ok()
114    }
115
116    /// Current CD-ROM IRQ flag value (0 = none, 1 = data ready, 2 = complete,
117    /// 3 = ack, 5 = error).
118    ///
119    /// Exposed so callers can build their own wait loops bounded by a hardware
120    /// timer rather than by a poll count. A poll budget is only a proxy for time
121    /// and drifts with CPU and bus speed, which matters when the thing being
122    /// measured is mechanical.
123    pub fn irq_flag_value(&mut self) -> u8 {
124        irq_flag()
125    }
126
127    /// Acknowledge the given CD-ROM IRQ bits.
128    pub fn acknowledge_irq(&mut self, bits: u8) {
129        ack_irq(bits);
130    }
131
132    /// Drain any pending response bytes, discarding them.
133    pub fn discard_response(&mut self) {
134        drain_response_fifo();
135    }
136
137    /// Send a command without waiting for any response. Pairs with
138    /// [`Self::irq_flag_value`] for caller-timed waits.
139    ///
140    /// Returns the CD-ROM IRQ enable to give back to
141    /// [`restore_irq_output`](Self::restore_irq_output), or `None` if the
142    /// parameter FIFO never made room. Leaves CD-ROM IRQ
143    /// output masked exactly as the polled helpers do, so a late ACK cannot
144    /// interrupt the caller mid-measurement; call [`Self::restore_irq_output`] when done.
145    pub fn dispatch_command(&mut self, command: u8, params: &[u8], spin_limit: u32) -> Option<u8> {
146        let irq_enable = begin_polled_command();
147        select_index(0);
148        for &param in params {
149            if !wait_param_room_bounded(spin_limit) {
150                finish_failed_polled_command(irq_enable);
151                return None;
152            }
153            write_byte(reg::PARAMETER, param);
154        }
155        write_byte(reg::COMMAND_RESPONSE, command);
156        Some(irq_enable)
157    }
158
159    /// Restore the CD-ROM IRQ enable saved by [`Self::dispatch_command`].
160    pub fn restore_irq_output(&mut self, saved: u8) {
161        drain_response_fifo();
162        ack_irq(code::ACK_ALL);
163        restore_irq_enable(saved);
164        select_index(0);
165    }
166
167    /// Nonblocking readiness probe for a sector reader that owns the INT1 ACK.
168    /// Data-ready is never acknowledged here. Other completion responses are
169    /// drained (at most 256 bytes) and acknowledged; drive errors also reset the
170    /// parameter FIFO. A ready data FIFO is accepted if no classified IRQ remains.
171    pub fn poll_data_sector(&mut self) -> Result<bool, SectorPollError> {
172        poll_sector(&mut SectorPollMmio)
173    }
174
175    /// Wait for the next streamed data sector (INT1) and acknowledge it.
176    ///
177    /// Use between [`Self::try_start_reading`] and [`Self::try_pause_until_complete`] to step through
178    /// a sector stream. Unrelated pending IRQs are drained and acknowledged so a
179    /// stale response cannot be mistaken for a sector arrival. Returns `false` on
180    /// a drive error or if no sector arrives within `spin_limit` polls.
181    pub fn try_wait_data_sector(&mut self, spin_limit: u32) -> bool {
182        let mut spins = spin_limit;
183        loop {
184            let flag = irq_flag();
185            if flag == code::DATA_READY {
186                ack_irq(flag);
187                return true;
188            }
189            if flag == code::ERROR {
190                let _ = read_response_fifo();
191                ack_irq(flag);
192                return false;
193            }
194            if flag != 0 {
195                let _ = read_response_fifo();
196                ack_irq(flag);
197            }
198            if spins == 0 {
199                return false;
200            }
201            spins -= 1;
202            core::hint::spin_loop();
203        }
204    }
205
206    /// Get the CD-ROM drive status byte.
207    #[doc(alias = "Getstat")]
208    #[doc(alias = "CdlNop")]
209    pub fn status(&mut self) -> Result<Response, CdError> {
210        self.command(CMD_GETSTAT, &[])
211    }
212
213    /// Try to get the CD-ROM drive status byte.
214    #[doc(alias = "Getstat")]
215    pub fn try_status(&mut self, spin_limit: u32) -> Option<Response> {
216        self.try_command(CMD_GETSTAT, &[], spin_limit)
217    }
218
219    /// Set the CD-ROM controller mode byte.
220    pub fn set_mode(&mut self, mode: u8) -> Result<Response, CdError> {
221        self.command(CMD_SETMODE, &[mode])
222    }
223
224    /// Try to set the CD-ROM controller mode byte.
225    pub fn try_set_mode(&mut self, mode: u8, spin_limit: u32) -> Option<Response> {
226        self.try_command(CMD_SETMODE, &[mode], spin_limit)
227    }
228
229    /// Seek to a logical data-sector LBA (the command receives absolute BCD MSF).
230    #[doc(alias = "Setloc")]
231    #[doc(alias = "CdlSetloc")]
232    pub fn try_set_target_lba(&mut self, lba: u32, spin_limit: u32) -> Option<Response> {
233        let [minute, second, frame] = lba_to_bcd_msf(lba);
234        self.try_command(CMD_SETLOC, &[minute, second, frame], spin_limit)
235    }
236
237    /// Begin a normal data-sector stream at the most recently selected location.
238    #[doc(alias = "ReadN")]
239    #[doc(alias = "CdlReadN")]
240    pub fn try_start_reading(&mut self, spin_limit: u32) -> Option<Response> {
241        self.try_command(CMD_READN, &[], spin_limit)
242    }
243
244    /// Route CD-DA/XA output out of the CD-ROM controller.
245    #[doc(alias = "Demute")]
246    #[doc(alias = "CdlDemute")]
247    pub fn unmute(&mut self) -> Result<Response, CdError> {
248        self.command(CMD_DEMUTE, &[])
249    }
250
251    /// Try to route CD-DA/XA output out of the CD-ROM controller.
252    #[doc(alias = "Demute")]
253    pub fn try_unmute(&mut self, spin_limit: u32) -> Option<Response> {
254        self.try_command(CMD_DEMUTE, &[], spin_limit)
255    }
256
257    /// Mute CD-DA/XA output at the CD-ROM controller.
258    pub fn mute(&mut self) -> Result<Response, CdError> {
259        self.command(CMD_MUTE, &[])
260    }
261
262    /// Try to mute CD-DA/XA output at the CD-ROM controller.
263    pub fn try_mute(&mut self, spin_limit: u32) -> Option<Response> {
264        self.try_command(CMD_MUTE, &[], spin_limit)
265    }
266
267    /// Start CD-DA playback at a 1-based track number.
268    ///
269    /// The number is relative to this program's own tracks; on a multi-program
270    /// disc [`crate::disc_base`] shifts it past whatever came before.
271    pub fn play_track(&mut self, track: u8) -> Result<Response, CdError> {
272        self.command(
273            CMD_PLAY,
274            &[bin_to_bcd(crate::disc_base::shift_track(track))],
275        )
276    }
277
278    /// Try to start CD-DA playback at a 1-based track number. Shifted like
279    /// [`Self::play_track`].
280    pub fn try_play_track(&mut self, track: u8, spin_limit: u32) -> Option<Response> {
281        self.try_command(
282            CMD_PLAY,
283            &[bin_to_bcd(crate::disc_base::shift_track(track))],
284            spin_limit,
285        )
286    }
287
288    /// Pause CD-DA/read playback.
289    pub fn pause(&mut self) -> Result<Response, CdError> {
290        self.command(CMD_PAUSE, &[])
291    }
292
293    /// Try to pause CD-DA/read playback.
294    pub fn try_pause(&mut self, spin_limit: u32) -> Option<Response> {
295        self.try_command(CMD_PAUSE, &[], spin_limit)
296    }
297
298    /// Try to pause CD-DA/read playback and wait for the completion IRQ.
299    ///
300    /// Unlike [`Self::try_stop`], this leaves the drive spun up, which makes it the
301    /// right handoff before gameplay code starts issuing data-read commands.
302    pub fn try_pause_until_complete(&mut self, spin_limit: u32) -> bool {
303        try_command_until_complete_inner(CMD_PAUSE, &[], spin_limit)
304    }
305
306    /// Stop the CD-ROM motor/playback.
307    pub fn stop(&mut self) -> Result<Response, CdError> {
308        self.command(CMD_STOP, &[])
309    }
310
311    /// Try to stop the CD-ROM motor/playback.
312    ///
313    /// **This returns when the command is ACCEPTED, not when the drive is
314    /// done.** On real hardware the motor then winds down for one to two
315    /// seconds, during which GetStat reports no activity bits and data-read
316    /// commands fail; an emulator answers instantly and hides the window
317    /// (proven by a demo-disc burn whose every chain-load died this way). If
318    /// data reads follow, either call [`Self::stop_and_settle`], or better, keep the
319    /// drive spun up with [`Self::try_pause_until_complete`].
320    pub fn try_stop(&mut self, spin_limit: u32) -> Option<Response> {
321        self.try_command(CMD_STOP, &[], spin_limit)
322    }
323
324    /// Stop playback and wait until the drive has genuinely gone quiet:
325    /// GetStat reporting neither playing nor seeking on two consecutive polls,
326    /// with a short pause between polls. Bounded by `max_polls` so a wedged
327    /// drive cannot hang the caller; returns whether the drive settled.
328    ///
329    /// This is the safe prelude to issuing data-read commands after CD-DA.
330    /// [`Self::try_pause_until_complete`] is the faster choice when the next reads
331    /// are imminent, since it keeps the motor spinning.
332    pub fn stop_and_settle(&mut self, spin_limit: u32, max_polls: u32) -> bool {
333        let _ = self.try_stop(spin_limit);
334        let mut settled = 0u8;
335        for _ in 0..max_polls {
336            // Pace the polls: on silicon each takes real time anyway, but an
337            // emulator answers instantly and would burn the poll budget in
338            // microseconds. Volatile MMIO reads cannot be optimized out.
339            for _ in 0..20_000u32 {
340                // SAFETY: `reg::INDEX` is the CD-ROM controller's index/status register, byte-wide
341                // MMIO on every PS1. Reading it has no side effects, so this is a pure delay.
342                unsafe { core::ptr::read_volatile(reg::INDEX as *const u8) };
343            }
344            let quiet = match self.try_status(spin_limit) {
345                Some(r) => r
346                    .bytes()
347                    .first()
348                    .is_some_and(|s| s & (STAT_PLAYING | STAT_SEEKING) == 0),
349                None => false,
350            };
351            settled = if quiet { settled + 1 } else { 0 };
352            if settled >= 2 {
353                return true;
354            }
355        }
356        false
357    }
358
359    /// Get the current physical play position (CdlGetlocP). The 8-byte reply is
360    /// `[Track(bcd), Index(raw), RMM, RSS, RSECT, AMM, ASS, ASECT]`; parse it with
361    /// [`PlayPosition::parse`].
362    #[doc(alias = "GetlocP")]
363    #[doc(alias = "CdlGetlocP")]
364    pub fn play_position(&mut self) -> Result<Response, CdError> {
365        self.command(CMD_GETLOCP, &[])
366    }
367
368    /// Try to get the current physical play position, giving up after `spin_limit`.
369    #[doc(alias = "GetlocP")]
370    pub fn try_play_position(&mut self, spin_limit: u32) -> Option<Response> {
371        self.try_command(CMD_GETLOCP, &[], spin_limit)
372    }
373
374    /// Send a command and wait for its SECOND response (the completion IRQ),
375    /// not just the initial acknowledgement.
376    ///
377    /// Seek, read and init all acknowledge immediately and finish much later, so
378    /// timing them against the ack measures command dispatch rather than the
379    /// mechanical operation. Returns `false` if either response fails to arrive
380    /// within `spin_limit` polls.
381    pub fn try_command_until_complete(
382        &mut self,
383        command: u8,
384        params: &[u8],
385        spin_limit: u32,
386    ) -> bool {
387        try_command_until_complete_inner(command, params, spin_limit)
388    }
389
390    /// Program the drive's audio mixer (CD-DA and XA-ADPCM on their way to
391    /// the SPU's CD input) and apply it. Volumes are 0..=0xFF with 0x80 as
392    /// unity; `(0x80, 0, 0x80, 0)` is plain stereo. The SPU side still needs
393    /// its CD input enabled and a CD volume (`psx-spu`).
394    pub fn set_audio_mixer(
395        &mut self,
396        left_to_left: u8,
397        left_to_right: u8,
398        right_to_right: u8,
399        right_to_left: u8,
400    ) {
401        select_index(2);
402        write_byte(reg::PARAMETER, left_to_left);
403        write_byte(reg::REQUEST_IRQ, left_to_right);
404        select_index(3);
405        write_byte(reg::COMMAND_RESPONSE, right_to_right);
406        write_byte(reg::PARAMETER, right_to_left);
407        // Apply the new volumes (index 3, register 3, bit 5), un-muting ADPCM.
408        write_byte(reg::REQUEST_IRQ, volume_apply::APPLY);
409        select_index(0);
410    }
411}
412
413/// Register-level steps, for a driver that sequences commands itself, such as
414/// the polled [`reader::SectorReader`].
415///
416/// Each is one controller access or a short fixed run of them, and each
417/// leaves register index 0 selected, so the byte steps that follow need not
418/// select it again.
419impl Cd {
420    /// Acknowledge every controller IRQ (writing `0x5F`, which also resets
421    /// the parameter FIFO) and the CD-ROM bit of `I_STAT`.
422    pub fn acknowledge_all_and_reset_parameters(&mut self) {
423        ack_all_and_reset_parameters();
424    }
425
426    /// The controller's IRQ enable bits.
427    pub fn irq_enable_mask(&mut self) -> u8 {
428        irq_enable()
429    }
430
431    /// Set the controller's IRQ enable bits (five are defined).
432    pub fn set_irq_enable_mask(&mut self, mask: u8) {
433        set_irq_enable(mask);
434    }
435
436    /// Pop and discard up to `limit` response bytes. The FIFO is 16 bytes
437    /// deep; the bound is for a controller wedged "not empty".
438    pub fn drain_response_limited(&mut self, limit: u32) {
439        drain_response_limited(limit);
440    }
441
442    /// Whether the data FIFO holds sector bytes to pop.
443    pub fn is_data_fifo_ready(&mut self) -> bool {
444        data_fifo_ready()
445    }
446
447    /// Wait for the parameter FIFO to have room, for at most `spins` more
448    /// status reads. `false` on timeout.
449    pub fn wait_parameter_room(&mut self, spins: u32) -> bool {
450        wait_param_room_bounded(spins)
451    }
452
453    /// Empty the parameter FIFO.
454    pub fn reset_parameter_fifo(&mut self) {
455        clear_parameter_fifo();
456    }
457
458    /// Push one parameter byte. Wait for room first.
459    pub fn send_parameter_byte(&mut self, byte: u8) {
460        write_byte(reg::PARAMETER, byte);
461    }
462
463    /// Write the command byte, which starts the command.
464    pub fn send_command_byte(&mut self, byte: u8) {
465        write_byte(reg::COMMAND_RESPONSE, byte);
466    }
467
468    /// Pop one response byte.
469    pub fn read_response_byte(&mut self) -> u8 {
470        read_byte(reg::COMMAND_RESPONSE)
471    }
472
473    /// The status register: FIFO flags and the busy bit.
474    pub fn status_register(&mut self) -> u8 {
475        read_status()
476    }
477
478    /// Arm the data FIFO so the drive's buffered sector can be popped with
479    /// [`read_data_byte`](Self::read_data_byte) once
480    /// [`is_data_fifo_ready`](Self::is_data_fifo_ready) says it filled.
481    #[doc(alias = "BFRD")]
482    pub fn request_data(&mut self) {
483        write_byte(reg::REQUEST_IRQ, request::WANT_DATA);
484    }
485
486    /// Drop the data request, which resets the data FIFO.
487    pub fn clear_data_request(&mut self) {
488        write_byte(reg::REQUEST_IRQ, 0);
489    }
490
491    /// Pop one byte of sector data.
492    pub fn read_data_byte(&mut self) -> u8 {
493        read_byte(reg::PARAMETER)
494    }
495}
496
497/// The controller steps one polled command takes, so the order and the
498/// timeouts can be tested against a fake.
499trait CommandIo {
500    fn begin(&mut self) -> u8;
501    fn wait_param_room(&mut self, spins: u32) -> bool;
502    fn write_param(&mut self, value: u8);
503    fn write_command(&mut self, command: u8);
504    fn wait_irq(&mut self, expected: u8, spins: u32) -> Result<u8, CdError>;
505    fn finish(&mut self, irq_enable: u8, irq: u8) -> Response;
506    /// Leave the controller clean after a drive error: drain, acknowledge
507    /// everything and restore the IRQ enable.
508    fn recover(&mut self, irq_enable: u8);
509    /// Give the IRQ enable back without touching the response side.
510    fn restore_enable(&mut self, irq_enable: u8);
511}
512
513struct Mmio;
514
515impl CommandIo for Mmio {
516    fn begin(&mut self) -> u8 {
517        begin_polled_command()
518    }
519    fn wait_param_room(&mut self, spins: u32) -> bool {
520        wait_param_room_bounded(spins)
521    }
522    fn write_param(&mut self, value: u8) {
523        write_byte(reg::PARAMETER, value);
524    }
525    fn write_command(&mut self, command: u8) {
526        write_byte(reg::COMMAND_RESPONSE, command);
527    }
528    fn wait_irq(&mut self, expected: u8, spins: u32) -> Result<u8, CdError> {
529        wait_irq_flag(expected, spins, irq_flag, |flag| {
530            let _ = read_response_fifo();
531            ack_irq(flag);
532        })
533    }
534    fn finish(&mut self, irq_enable: u8, irq: u8) -> Response {
535        finish_polled_command(irq_enable, irq)
536    }
537    fn recover(&mut self, irq_enable: u8) {
538        finish_failed_polled_command(irq_enable);
539    }
540    fn restore_enable(&mut self, irq_enable: u8) {
541        restore_irq_enable(irq_enable);
542        select_index(0);
543    }
544}
545
546fn run_command(
547    io: &mut impl CommandIo,
548    command: u8,
549    params: &[u8],
550    spin_limit: u32,
551) -> Result<Response, CdError> {
552    let irq_enable = io.begin();
553    for &param in params {
554        if !io.wait_param_room(spin_limit) {
555            // Nothing was dispatched, so the IRQ output can be given back.
556            io.restore_enable(irq_enable);
557            return Err(CdError::Timeout);
558        }
559        io.write_param(param);
560    }
561    io.write_command(command);
562    match io.wait_irq(code::ACKNOWLEDGE, spin_limit) {
563        Ok(irq) => Ok(io.finish(irq_enable, irq)),
564        Err(CdError::DriveError) => {
565            io.recover(irq_enable);
566            Err(CdError::DriveError)
567        }
568        // A command is in flight: leave IRQ output masked so its late ACK
569        // cannot interrupt a polling caller.
570        Err(CdError::Timeout) => Err(CdError::Timeout),
571    }
572}
573
574/// A drive error reported while polling for the next streamed sector.
575#[derive(Clone, Copy, Debug, PartialEq, Eq)]
576pub struct SectorPollError;
577
578trait SectorPollIo {
579    fn flag(&mut self) -> u8;
580    fn drain(&mut self);
581    fn acknowledge(&mut self, flag: u8, reset: bool);
582    fn data_ready(&mut self) -> bool;
583}
584struct SectorPollMmio;
585impl SectorPollIo for SectorPollMmio {
586    #[inline]
587    fn flag(&mut self) -> u8 {
588        irq_flag()
589    }
590    #[inline]
591    fn drain(&mut self) {
592        drain_response_limited(SECTOR_POLL_DRAIN_LIMIT);
593    }
594    #[inline]
595    fn acknowledge(&mut self, flag: u8, reset: bool) {
596        if reset {
597            ack_all_and_reset_parameters();
598        } else {
599            ack_irq(flag);
600        }
601    }
602    #[inline]
603    fn data_ready(&mut self) -> bool {
604        data_fifo_ready()
605    }
606}
607#[inline]
608fn poll_sector(io: &mut impl SectorPollIo) -> Result<bool, SectorPollError> {
609    match io.flag() {
610        code::DATA_READY => Ok(true),
611        code::ERROR => {
612            io.drain();
613            io.acknowledge(code::ACK_ALL, true);
614            Err(SectorPollError)
615        }
616        flag @ (code::COMPLETE | code::ACKNOWLEDGE | code::DATA_END) => {
617            io.drain();
618            io.acknowledge(flag, false);
619            Ok(false)
620        }
621        _ => Ok(io.data_ready()),
622    }
623}
624
625/// Data-track LBA to the absolute BCD `[minute, second, frame]` Setloc takes.
626/// LBA 0 is 00:02:00: the 150-sector lead-in puts it two seconds in. Minutes
627/// clamp at 99.
628pub const fn lba_to_bcd_msf(lba: u32) -> [u8; 3] {
629    let absolute = lba.saturating_add(150);
630    let raw_minute = absolute / (60 * 75);
631    let minute = if raw_minute > 99 {
632        99
633    } else {
634        raw_minute as u8
635    };
636    let second = ((absolute / 75) % 60) as u8;
637    let frame = (absolute % 75) as u8;
638    [bin_to_bcd(minute), bin_to_bcd(second), bin_to_bcd(frame)]
639}
640
641/// Convert binary `0..=99` to BCD for CD-ROM command parameters.
642pub const fn bin_to_bcd(v: u8) -> u8 {
643    let v = if v > 99 { 99 } else { v };
644    ((v / 10) << 4) | (v % 10)
645}
646
647/// Convert a BCD byte back to binary. Inverse of [`bin_to_bcd`] for well-formed
648/// input; nibbles above 9 are not normalised (the drive never emits them).
649pub const fn bcd_to_bin(v: u8) -> u8 {
650    (v >> 4) * 10 + (v & 0x0F)
651}
652
653/// Decoded CdlGetlocP reply. The relative MSF (`relative_*`) is elapsed time
654/// into the current track, independent of the pregap, which makes it the source
655/// of truth for a music/song clock. The absolute MSF (`absolute_*`) is the raw
656/// disc position.
657#[derive(Clone, Copy, PartialEq, Eq, Debug)]
658pub struct PlayPosition {
659    /// 1-based track number the drive is currently in.
660    pub track: u8,
661    /// Track index (raw): 0 = pregap, 1 = program area.
662    pub index: u8,
663    /// Minutes elapsed into the current track.
664    pub relative_min: u8,
665    /// Seconds elapsed into the current track (0..=59).
666    pub relative_sec: u8,
667    /// MSF frames elapsed into the current track (0..=74, 75 per second).
668    pub relative_frame: u8,
669    /// Absolute disc position, minutes.
670    pub absolute_min: u8,
671    /// Absolute disc position, seconds (0..=59).
672    pub absolute_sec: u8,
673    /// Absolute disc position, MSF frames (0..=74).
674    pub absolute_frame: u8,
675}
676
677impl PlayPosition {
678    /// Parse a GetlocP [`Response`]. Returns `None` if fewer than 8 bytes came
679    /// back (drive not ready / no disc). Track and MSF fields are BCD; index is
680    /// raw.
681    pub fn parse(resp: &Response) -> Option<Self> {
682        let b = resp.bytes();
683        if b.len() < 8 {
684            return None;
685        }
686        Some(PlayPosition {
687            track: bcd_to_bin(b[0]),
688            index: b[1],
689            relative_min: bcd_to_bin(b[2]),
690            relative_sec: bcd_to_bin(b[3]),
691            relative_frame: bcd_to_bin(b[4]),
692            absolute_min: bcd_to_bin(b[5]),
693            absolute_sec: bcd_to_bin(b[6]),
694            absolute_frame: bcd_to_bin(b[7]),
695        })
696    }
697
698    /// Elapsed sectors into the current track (75 sectors per second).
699    pub fn relative_sectors(&self) -> u32 {
700        (self.relative_min as u32 * 60 + self.relative_sec as u32) * 75 + self.relative_frame as u32
701    }
702
703    /// Elapsed milliseconds into the current track. This is the song clock feed.
704    pub fn relative_millis(&self) -> u32 {
705        self.relative_sectors() * 1000 / 75
706    }
707}
708
709fn begin_polled_command() -> u8 {
710    let irq_enable = irq_enable();
711    set_irq_enable(0);
712    ack_irq(code::ACK_ALL);
713    select_index(0);
714    drain_response_fifo();
715    clear_parameter_fifo();
716    irq_enable
717}
718
719fn finish_polled_command(irq_enable: u8, irq: u8) -> Response {
720    let response = read_response_fifo();
721    ack_irq(irq);
722    restore_irq_enable(irq_enable);
723    select_index(0);
724    response
725}
726
727/// Poll `flag` until it reads `expected`, at most `spins` more times after the
728/// first read. A drive error (INT5) ends the wait at once; any other nonzero
729/// flag is a stale response, handed to `discard` and polled past.
730fn wait_irq_flag(
731    expected: u8,
732    mut spins: u32,
733    mut flag: impl FnMut() -> u8,
734    mut discard: impl FnMut(u8),
735) -> Result<u8, CdError> {
736    loop {
737        let irq = flag();
738        if irq == expected {
739            return Ok(irq);
740        }
741        if irq == code::ERROR {
742            return Err(CdError::DriveError);
743        }
744        if irq != 0 {
745            discard(irq);
746        }
747        if spins == 0 {
748            return Err(CdError::Timeout);
749        }
750        spins -= 1;
751        core::hint::spin_loop();
752    }
753}
754
755fn wait_irq_bounded(expected: u8, spins: u32) -> Option<u8> {
756    Mmio.wait_irq(expected, spins).ok()
757}
758
759fn try_command_until_complete_inner(command: u8, params: &[u8], spin_limit: u32) -> bool {
760    let irq_enable = begin_polled_command();
761    select_index(0);
762    for &param in params {
763        if !wait_param_room_bounded(spin_limit) {
764            finish_failed_polled_command(irq_enable);
765            return false;
766        }
767        write_byte(reg::PARAMETER, param);
768    }
769    write_byte(reg::COMMAND_RESPONSE, command);
770
771    let ok = wait_ack_then_complete(spin_limit);
772    drain_response_fifo();
773    ack_irq(code::ACK_ALL);
774    restore_irq_enable(irq_enable);
775    select_index(0);
776    ok
777}
778
779fn wait_ack_then_complete(spin_limit: u32) -> bool {
780    if wait_irq_bounded(code::ACKNOWLEDGE, spin_limit).is_none() {
781        return false;
782    }
783    drain_response_fifo();
784    ack_irq(code::ACKNOWLEDGE);
785
786    if wait_irq_bounded(code::COMPLETE, spin_limit).is_none() {
787        return false;
788    }
789    drain_response_fifo();
790    ack_irq(code::COMPLETE);
791    true
792}
793
794fn finish_failed_polled_command(irq_enable: u8) {
795    drain_response_fifo();
796    ack_irq(code::ACK_ALL);
797    restore_irq_enable(irq_enable);
798    select_index(0);
799}
800
801fn read_response_fifo() -> Response {
802    select_index(0);
803
804    let mut bytes = [0u8; 16];
805    let mut len = 0;
806    while read_status() & status::RESPONSE_NOT_EMPTY != 0 && len < bytes.len() {
807        bytes[len] = read_byte(reg::COMMAND_RESPONSE);
808        len += 1;
809    }
810    Response { bytes, len }
811}
812
813fn drain_response_fifo() {
814    let _ = read_response_fifo();
815}
816
817fn wait_param_room_bounded(mut spins: u32) -> bool {
818    while read_status() & status::PARAM_NOT_FULL == 0 {
819        if spins == 0 {
820            return false;
821        }
822        spins -= 1;
823        core::hint::spin_loop();
824    }
825    true
826}
827
828/// Pop and discard up to `limit` response bytes.
829fn drain_response_limited(limit: u32) {
830    select_index(0);
831    let mut drained = 0;
832    while read_status() & status::RESPONSE_NOT_EMPTY != 0 && drained < limit {
833        let _ = read_byte(reg::COMMAND_RESPONSE);
834        drained += 1;
835    }
836}
837
838/// Acknowledge every controller IRQ and reset the parameter FIFO, then the
839/// CD-ROM bit of I_STAT.
840fn ack_all_and_reset_parameters() {
841    select_index(1);
842    write_byte(reg::REQUEST_IRQ, code::ACK_ALL | code::CLEAR_PARAMETER_FIFO);
843    irq::acknowledge(1 << psx_hw::irq::source::CDROM);
844    select_index(0);
845}
846
847fn data_fifo_ready() -> bool {
848    select_index(0);
849    read_status() & status::DATA_FIFO_NOT_EMPTY != 0
850}
851
852fn clear_parameter_fifo() {
853    select_index(1);
854    write_byte(reg::REQUEST_IRQ, code::CLEAR_PARAMETER_FIFO);
855    select_index(0);
856}
857
858fn ack_irq(bits: u8) {
859    select_index(1);
860    write_byte(reg::REQUEST_IRQ, bits & code::ACK_ALL);
861    irq::acknowledge(1 << psx_hw::irq::source::CDROM);
862    select_index(0);
863}
864
865fn irq_flag() -> u8 {
866    select_index(1);
867    let flag = read_byte(reg::REQUEST_IRQ) & code::ACK_ALL;
868    select_index(0);
869    flag
870}
871
872fn irq_enable() -> u8 {
873    select_index(0);
874    let enable = read_byte(reg::REQUEST_IRQ) & code::ACK_ALL;
875    select_index(0);
876    enable
877}
878
879fn restore_irq_enable(enable: u8) {
880    set_irq_enable(enable);
881}
882
883fn set_irq_enable(enable: u8) {
884    select_index(1);
885    write_byte(reg::PARAMETER, enable & code::ACK_ALL);
886    select_index(0);
887}
888
889fn read_status() -> u8 {
890    read_byte(reg::INDEX)
891}
892
893fn select_index(index: u8) {
894    write_byte(reg::INDEX, index & 0x03);
895}
896
897fn read_byte(addr: u32) -> u8 {
898    // SAFETY: fixed CD-ROM MMIO register read.
899    unsafe { read_u8(addr) }
900}
901
902fn write_byte(addr: u32, value: u8) {
903    // SAFETY: fixed CD-ROM MMIO register write.
904    unsafe { write_u8(addr, value) }
905}
906
907/// A token for a deprecated forwarder that never took one.
908fn steal() -> Cd {
909    // SAFETY: a token is a logic guard, not a memory-safety one (see
910    // `crate::periph`), and the old free functions never took one.
911    unsafe { Cd::steal() }
912}
913
914/// Moved to [`Cd::try_command`].
915#[deprecated(note = "use `Cd::try_command` with the `Cd` token")]
916#[inline(always)]
917pub fn try_command(command: u8, params: &[u8], spin_limit: u32) -> Option<Response> {
918    steal().try_command(command, params, spin_limit)
919}
920
921/// Moved to [`Cd::irq_flag_value`].
922#[deprecated(note = "use `Cd::irq_flag_value` with the `Cd` token")]
923#[inline(always)]
924pub fn irq_flag_value() -> u8 {
925    steal().irq_flag_value()
926}
927
928/// Moved to [`Cd::acknowledge_irq`].
929#[deprecated(note = "use `Cd::acknowledge_irq` with the `Cd` token")]
930#[inline(always)]
931pub fn acknowledge_irq(bits: u8) {
932    steal().acknowledge_irq(bits)
933}
934
935/// Moved to [`Cd::discard_response`].
936#[deprecated(note = "use `Cd::discard_response` with the `Cd` token")]
937#[inline(always)]
938pub fn discard_response() {
939    steal().discard_response()
940}
941
942/// Moved to [`Cd::dispatch_command`].
943#[deprecated(note = "use `Cd::dispatch_command` with the `Cd` token")]
944#[inline(always)]
945pub fn dispatch_command(command: u8, params: &[u8], spin_limit: u32) -> Option<u8> {
946    steal().dispatch_command(command, params, spin_limit)
947}
948
949/// Moved to [`Cd::restore_irq_output`].
950#[deprecated(note = "use `Cd::restore_irq_output` with the `Cd` token")]
951#[inline(always)]
952pub fn restore_irq_output(saved: u8) {
953    steal().restore_irq_output(saved)
954}
955
956/// Moved to [`Cd::poll_data_sector`].
957#[deprecated(note = "use `Cd::poll_data_sector` with the `Cd` token")]
958#[inline(always)]
959pub fn poll_data_sector() -> Result<bool, SectorPollError> {
960    steal().poll_data_sector()
961}
962
963/// Moved to [`Cd::try_status`].
964#[deprecated(note = "use `Cd::try_status` with the `Cd` token")]
965#[inline(always)]
966pub fn try_status(spin_limit: u32) -> Option<Response> {
967    steal().try_status(spin_limit)
968}
969
970/// Moved to [`Cd::try_set_mode`].
971#[deprecated(note = "use `Cd::try_set_mode` with the `Cd` token")]
972#[inline(always)]
973pub fn try_set_mode(mode: u8, spin_limit: u32) -> Option<Response> {
974    steal().try_set_mode(mode, spin_limit)
975}
976
977/// Moved to [`Cd::try_set_target_lba`].
978#[deprecated(note = "use `Cd::try_set_target_lba` with the `Cd` token")]
979#[inline(always)]
980pub fn try_set_target_lba(lba: u32, spin_limit: u32) -> Option<Response> {
981    steal().try_set_target_lba(lba, spin_limit)
982}
983
984/// Moved to [`Cd::try_start_reading`].
985#[deprecated(note = "use `Cd::try_start_reading` with the `Cd` token")]
986#[inline(always)]
987pub fn try_start_reading(spin_limit: u32) -> Option<Response> {
988    steal().try_start_reading(spin_limit)
989}
990
991/// Moved to [`Cd::try_unmute`].
992#[deprecated(note = "use `Cd::try_unmute` with the `Cd` token")]
993#[inline(always)]
994pub fn try_unmute(spin_limit: u32) -> Option<Response> {
995    steal().try_unmute(spin_limit)
996}
997
998/// Moved to [`Cd::try_mute`].
999#[deprecated(note = "use `Cd::try_mute` with the `Cd` token")]
1000#[inline(always)]
1001pub fn try_mute(spin_limit: u32) -> Option<Response> {
1002    steal().try_mute(spin_limit)
1003}
1004
1005/// Moved to [`Cd::try_play_track`].
1006#[deprecated(note = "use `Cd::try_play_track` with the `Cd` token")]
1007#[inline(always)]
1008pub fn try_play_track(track: u8, spin_limit: u32) -> Option<Response> {
1009    steal().try_play_track(track, spin_limit)
1010}
1011
1012/// Moved to [`Cd::try_pause`].
1013#[deprecated(note = "use `Cd::try_pause` with the `Cd` token")]
1014#[inline(always)]
1015pub fn try_pause(spin_limit: u32) -> Option<Response> {
1016    steal().try_pause(spin_limit)
1017}
1018
1019/// Moved to [`Cd::try_pause_until_complete`].
1020#[deprecated(note = "use `Cd::try_pause_until_complete` with the `Cd` token")]
1021#[inline(always)]
1022pub fn try_pause_until_complete(spin_limit: u32) -> bool {
1023    steal().try_pause_until_complete(spin_limit)
1024}
1025
1026/// Moved to [`Cd::try_play_position`].
1027#[deprecated(note = "use `Cd::try_play_position` with the `Cd` token")]
1028#[inline(always)]
1029pub fn try_play_position(spin_limit: u32) -> Option<Response> {
1030    steal().try_play_position(spin_limit)
1031}
1032
1033#[cfg(test)]
1034mod tests {
1035    use super::*;
1036
1037    #[test]
1038    fn sector_probe_leaves_int1_and_orders_other_acknowledgements() {
1039        struct Fake {
1040            flag: u8,
1041            ready: bool,
1042            events: [u8; 4],
1043            len: usize,
1044        }
1045        impl Fake {
1046            fn event(&mut self, e: u8) {
1047                self.events[self.len] = e;
1048                self.len += 1;
1049            }
1050        }
1051        impl SectorPollIo for Fake {
1052            fn flag(&mut self) -> u8 {
1053                self.event(1);
1054                self.flag
1055            }
1056            fn drain(&mut self) {
1057                self.event(2);
1058            }
1059            fn acknowledge(&mut self, flag: u8, reset: bool) {
1060                assert_eq!(flag, if reset { 31 } else { self.flag });
1061                self.event(if reset { 4 } else { 3 });
1062            }
1063            fn data_ready(&mut self) -> bool {
1064                self.event(5);
1065                self.ready
1066            }
1067        }
1068        for flag in 0..=5 {
1069            for ready in [false, true] {
1070                let mut io = Fake {
1071                    flag,
1072                    ready,
1073                    events: [0; 4],
1074                    len: 0,
1075                };
1076                let result = poll_sector(&mut io);
1077                let expected = match flag {
1078                    1 => Ok(true),
1079                    5 => Err(SectorPollError),
1080                    2..=4 => Ok(false),
1081                    _ => Ok(ready),
1082                };
1083                assert_eq!(result, expected);
1084                let events: &[u8] = match flag {
1085                    1 => &[1],
1086                    5 => &[1, 2, 4],
1087                    2..=4 => &[1, 2, 3],
1088                    _ => &[1, 5],
1089                };
1090                assert_eq!(&io.events[..io.len], events);
1091            }
1092        }
1093    }
1094
1095    /// Records the controller steps a command takes and answers each wait from
1096    /// a script.
1097    struct FakeIo {
1098        room: bool,
1099        irq: Result<u8, CdError>,
1100        events: [u8; 12],
1101        len: usize,
1102        spins_seen: [u32; 4],
1103        spins_len: usize,
1104    }
1105
1106    impl FakeIo {
1107        fn new(room: bool, irq: Result<u8, CdError>) -> Self {
1108            FakeIo {
1109                room,
1110                irq,
1111                events: [0; 12],
1112                len: 0,
1113                spins_seen: [0; 4],
1114                spins_len: 0,
1115            }
1116        }
1117        fn event(&mut self, e: u8) {
1118            self.events[self.len] = e;
1119            self.len += 1;
1120        }
1121        fn events(&self) -> &[u8] {
1122            &self.events[..self.len]
1123        }
1124        fn spins(&mut self, spins: u32) {
1125            self.spins_seen[self.spins_len] = spins;
1126            self.spins_len += 1;
1127        }
1128    }
1129
1130    // Event codes: begin, param room, param byte, command byte, wait irq,
1131    // finish, recover, restore enable.
1132    const BEGIN: u8 = 1;
1133    const ROOM: u8 = 2;
1134    const PARAM: u8 = 3;
1135    const COMMAND: u8 = 4;
1136    const WAIT: u8 = 5;
1137    const FINISH: u8 = 6;
1138    const RECOVER: u8 = 7;
1139    const RESTORE: u8 = 8;
1140
1141    impl CommandIo for FakeIo {
1142        fn begin(&mut self) -> u8 {
1143            self.event(BEGIN);
1144            0x1F
1145        }
1146        fn wait_param_room(&mut self, spins: u32) -> bool {
1147            self.event(ROOM);
1148            self.spins(spins);
1149            self.room
1150        }
1151        fn write_param(&mut self, _value: u8) {
1152            self.event(PARAM);
1153        }
1154        fn write_command(&mut self, _command: u8) {
1155            self.event(COMMAND);
1156        }
1157        fn wait_irq(&mut self, _expected: u8, spins: u32) -> Result<u8, CdError> {
1158            self.event(WAIT);
1159            self.spins(spins);
1160            self.irq
1161        }
1162        fn finish(&mut self, _irq_enable: u8, _irq: u8) -> Response {
1163            self.event(FINISH);
1164            response_from(&[0x02])
1165        }
1166        fn recover(&mut self, _irq_enable: u8) {
1167            self.event(RECOVER);
1168        }
1169        fn restore_enable(&mut self, _irq_enable: u8) {
1170            self.event(RESTORE);
1171        }
1172    }
1173
1174    #[test]
1175    fn a_full_parameter_fifo_times_out_without_writing_the_parameter() {
1176        // The wait used to ignore its own timeout and write the byte anyway.
1177        let mut io = FakeIo::new(false, Ok(code::ACKNOWLEDGE));
1178        let result = run_command(&mut io, CMD_SETLOC, &[0x00, 0x02, 0x00], 77);
1179        assert_eq!(result, Err(CdError::Timeout));
1180        assert_eq!(io.events(), &[BEGIN, ROOM, RESTORE]);
1181    }
1182
1183    #[test]
1184    fn a_command_nobody_acknowledges_times_out_and_leaves_irq_output_masked() {
1185        let mut io = FakeIo::new(true, Err(CdError::Timeout));
1186        let result = run_command(&mut io, CMD_GETSTAT, &[], DEFAULT_COMMAND_SPINS);
1187        assert_eq!(result, Err(CdError::Timeout));
1188        // No finish, recover or restore: the late ACK stays masked.
1189        assert_eq!(io.events(), &[BEGIN, COMMAND, WAIT]);
1190        assert_eq!(&io.spins_seen[..io.spins_len], &[DEFAULT_COMMAND_SPINS]);
1191    }
1192
1193    #[test]
1194    fn a_drive_error_is_reported_and_the_controller_cleaned_up() {
1195        let mut io = FakeIo::new(true, Err(CdError::DriveError));
1196        let result = run_command(&mut io, CMD_PLAY, &[0x01], 10);
1197        assert_eq!(result, Err(CdError::DriveError));
1198        assert_eq!(io.events(), &[BEGIN, ROOM, PARAM, COMMAND, WAIT, RECOVER]);
1199    }
1200
1201    #[test]
1202    fn an_acknowledged_command_returns_its_response() {
1203        let mut io = FakeIo::new(true, Ok(code::ACKNOWLEDGE));
1204        let result = run_command(&mut io, CMD_SETLOC, &[1, 2, 3], 10).unwrap();
1205        assert_eq!(result.bytes(), &[0x02]);
1206        assert_eq!(
1207            io.events(),
1208            &[BEGIN, ROOM, PARAM, ROOM, PARAM, ROOM, PARAM, COMMAND, WAIT, FINISH]
1209        );
1210    }
1211
1212    #[test]
1213    fn the_flag_wait_discards_stale_responses_and_stops_at_its_budget() {
1214        // Stale INT2 then INT3: the stale one is discarded, INT3 ends the wait.
1215        let script = [0u8, 2, 3];
1216        let mut next = 0;
1217        let mut discarded = [0u8; 4];
1218        let mut dropped = 0;
1219        let got = wait_irq_flag(
1220            code::ACKNOWLEDGE,
1221            10,
1222            || {
1223                let flag = script[next];
1224                next += 1;
1225                flag
1226            },
1227            |flag| {
1228                discarded[dropped] = flag;
1229                dropped += 1;
1230            },
1231        );
1232        assert_eq!(got, Ok(code::ACKNOWLEDGE));
1233        assert_eq!(&discarded[..dropped], &[2]);
1234
1235        // Silence: one read plus `spins` more, then Timeout.
1236        let mut reads = 0u32;
1237        let got = wait_irq_flag(
1238            code::ACKNOWLEDGE,
1239            5,
1240            || {
1241                reads += 1;
1242                0
1243            },
1244            |_| {},
1245        );
1246        assert_eq!(got, Err(CdError::Timeout));
1247        assert_eq!(reads, 6);
1248
1249        // INT5 ends it at once, whatever budget is left.
1250        let got = wait_irq_flag(code::ACKNOWLEDGE, 1000, || code::ERROR, |_| {});
1251        assert_eq!(got, Err(CdError::DriveError));
1252    }
1253
1254    #[test]
1255    fn bcd_clamps_to_two_digits() {
1256        assert_eq!(bin_to_bcd(2), 0x02);
1257        assert_eq!(bin_to_bcd(42), 0x42);
1258        assert_eq!(bin_to_bcd(100), 0x99);
1259    }
1260
1261    #[test]
1262    fn bcd_to_bin_roundtrips() {
1263        for v in 0u8..=99 {
1264            assert_eq!(bcd_to_bin(bin_to_bcd(v)), v);
1265        }
1266    }
1267
1268    #[test]
1269    fn lba_to_msf_includes_the_lead_in() {
1270        assert_eq!(lba_to_bcd_msf(0), [0x00, 0x02, 0x00]);
1271        assert_eq!(lba_to_bcd_msf(424), [0x00, 0x07, 0x49]);
1272        assert_eq!(lba_to_bcd_msf(992), [0x00, 0x15, 0x17]);
1273    }
1274
1275    fn response_from(bytes: &[u8]) -> Response {
1276        let mut buf = [0u8; 16];
1277        buf[..bytes.len()].copy_from_slice(bytes);
1278        Response {
1279            bytes: buf,
1280            len: bytes.len(),
1281        }
1282    }
1283
1284    #[test]
1285    fn play_position_parses_and_converts() {
1286        // Track 2, index 1, relative 01:23:45 (min:sec:frame), absolute 04:56:00.
1287        let resp = response_from(&[
1288            bin_to_bcd(2),
1289            0x01,
1290            bin_to_bcd(1),
1291            bin_to_bcd(23),
1292            bin_to_bcd(45),
1293            bin_to_bcd(4),
1294            bin_to_bcd(56),
1295            bin_to_bcd(0),
1296        ]);
1297        let p = PlayPosition::parse(&resp).unwrap();
1298        assert_eq!(p.track, 2);
1299        assert_eq!(p.index, 1);
1300        assert_eq!(
1301            (p.relative_min, p.relative_sec, p.relative_frame),
1302            (1, 23, 45)
1303        );
1304        // (1*60 + 23) * 75 + 45 = 6270 sectors.
1305        assert_eq!(p.relative_sectors(), 6270);
1306        // 6270 * 1000 / 75 = 83600 ms.
1307        assert_eq!(p.relative_millis(), 83_600);
1308    }
1309
1310    #[test]
1311    fn play_position_rejects_short_response() {
1312        assert!(PlayPosition::parse(&response_from(&[0x02, 0x01, 0x00])).is_none());
1313    }
1314}