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

psx_io/cd/
xa.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2//! XA-ADPCM music: stream one channel of an interleaved XA file through the
3//! drive's own decoder to the SPU's CD input.
4//!
5//! The encoder side is `psx-audio-cook xa-encode`; `docs/XA-MUSIC.md` has the
6//! workflow. A song file holds several songs as interleaved channels (every
7//! fourth sector for four 37.8 kHz stereo songs at single speed). With
8//! XA-ADPCM and the file/channel filter switched on, the drive hands the
9//! sectors of the chosen channel to its decoder and drops the rest; the CPU
10//! never sees the audio.
11//!
12//! # The drive is busy while music plays
13//!
14//! Music is a read: the drive streams the file continuously, so the CPU
15//! cannot read data from the disc meanwhile, not even at the same time at the
16//! same speed (the laser follows the file, not the data). Load first, then
17//! start the song, or stop it for the load and start it again afterwards (it
18//! restarts from the top). Streaming data and music together needs the data
19//! interleaved into the filler slots of the song file, which this player does
20//! not do.
21//!
22//! # Polling
23//!
24//! The player is poll-driven like [`super::audio`]: call [`Player::poll`]
25//! about once per frame. It asks the drive where the head is, and when the
26//! head reaches the end of the song file it either restarts the file
27//! (looping) or pauses the drive. A restart is a seek, so a loop has an
28//! audible gap whose length is the drive's seek time; authoring the loop
29//! point into a quiet passage hides it.
30
31use super::PlayPosition;
32use crate::disc_base::shift_lba;
33use crate::periph::Cd;
34use psx_hw::cd::xa::{END_GUARD_SECTORS, SECTORS_PER_SECOND};
35use psx_hw::cd::{CMD_READS, CMD_SETFILTER, MODE_DOUBLE_SPEED, MODE_XA_ADPCM, MODE_XA_FILTER};
36
37/// Per-command response spin budget: generous, the drive answers far sooner.
38pub const COMMAND_SPINS: u32 = 131_072;
39/// Spin budget for a pause to finish (the drive spends up to about a tenth
40/// of a second stopping a read).
41const PAUSE_SPINS: u32 = 2_000_000;
42
43/// Speed the song file was interleaved for (`psx-audio-cook xa-encode --speed`).
44#[derive(Copy, Clone, Debug, PartialEq, Eq)]
45pub enum DriveSpeed {
46    /// 75 sectors per second.
47    Single,
48    /// 150 sectors per second.
49    Double,
50}
51
52impl DriveSpeed {
53    const fn sectors_per_second(self) -> u32 {
54        match self {
55            DriveSpeed::Single => SECTORS_PER_SECOND,
56            DriveSpeed::Double => 2 * SECTORS_PER_SECOND,
57        }
58    }
59
60    const fn mode(self) -> u8 {
61        let speed = match self {
62            DriveSpeed::Single => 0,
63            DriveSpeed::Double => MODE_DOUBLE_SPEED,
64        };
65        MODE_XA_ADPCM | MODE_XA_FILTER | speed
66    }
67}
68
69/// An interleaved XA file on the disc.
70#[derive(Copy, Clone, Debug, PartialEq, Eq)]
71pub struct File {
72    lba: u32,
73    sector_count: u32,
74    number: u8,
75    speed: DriveSpeed,
76}
77
78impl File {
79    /// A file at `lba` (relative to this program's disc image, see
80    /// [`crate::disc_base`]) of `sector_count` sectors, including the end
81    /// guard `mkisopsx` keeps. `number` and `speed` come from the encoder's
82    /// manifest.
83    pub const fn new(lba: u32, sector_count: u32, number: u8, speed: DriveSpeed) -> Self {
84        Self {
85            lba,
86            sector_count,
87            number,
88            speed,
89        }
90    }
91
92    /// A file from its ISO 9660 directory record: first LBA and size in
93    /// bytes (a whole number of 2048-byte sectors for an XA file).
94    pub const fn from_directory_entry(
95        lba: u32,
96        size_bytes: u32,
97        number: u8,
98        speed: DriveSpeed,
99    ) -> Self {
100        Self::new(lba, size_bytes / 2048, number, speed)
101    }
102
103    /// Channel `channel` of this file as a song.
104    pub const fn song(self, channel: u8) -> Song {
105        Song {
106            file: self,
107            channel,
108        }
109    }
110
111    /// Sectors the songs span, without the end guard.
112    pub const fn span_sector_count(self) -> u32 {
113        self.sector_count.saturating_sub(END_GUARD_SECTORS)
114    }
115}
116
117/// One channel of an [`File`].
118#[derive(Copy, Clone, Debug, PartialEq, Eq)]
119pub struct Song {
120    file: File,
121    channel: u8,
122}
123
124/// The step of starting a song that the drive refused.
125#[derive(Copy, Clone, Debug, PartialEq, Eq)]
126pub enum Error {
127    /// Demute did not answer.
128    Unmute,
129    /// Setmode did not answer.
130    SetMode,
131    /// Setfilter did not answer.
132    SetFilter,
133    /// Setloc did not answer.
134    Seek,
135    /// ReadS did not answer.
136    Read,
137}
138
139/// What [`Player::poll`] saw.
140#[derive(Copy, Clone, Debug, PartialEq, Eq)]
141pub enum Event {
142    /// No song is playing.
143    Idle,
144    /// A song is playing (or its start is still seeking).
145    Playing,
146    /// The song reached its end and restarted from the top.
147    Looped,
148    /// The song reached its end and the drive was paused.
149    Finished,
150}
151
152/// The drive commands the player needs, so the state machine can run against
153/// a model on the host.
154trait Drive {
155    fn unmute(&mut self) -> bool;
156    fn set_mode(&mut self, mode: u8) -> bool;
157    fn set_filter(&mut self, file: u8, channel: u8) -> bool;
158    fn set_target(&mut self, lba: u32) -> bool;
159    fn start_streaming(&mut self) -> bool;
160    /// Pause the drive and wait for it; `false` if the drive refused (it
161    /// does during the seek at the start of a read).
162    fn pause(&mut self) -> bool;
163    /// Absolute LBA the head is reading, if the drive answers.
164    fn head_lba(&mut self) -> Option<u32>;
165}
166
167impl Drive for Cd {
168    fn unmute(&mut self) -> bool {
169        self.try_unmute(COMMAND_SPINS).is_some()
170    }
171    fn set_mode(&mut self, mode: u8) -> bool {
172        self.try_set_mode(mode, COMMAND_SPINS).is_some()
173    }
174    fn set_filter(&mut self, file: u8, channel: u8) -> bool {
175        self.try_command(CMD_SETFILTER, &[file, channel], COMMAND_SPINS)
176            .is_some()
177    }
178    fn set_target(&mut self, lba: u32) -> bool {
179        self.try_set_target_lba(lba, COMMAND_SPINS).is_some()
180    }
181    fn start_streaming(&mut self) -> bool {
182        self.try_command(CMD_READS, &[], COMMAND_SPINS).is_some()
183    }
184    fn pause(&mut self) -> bool {
185        self.try_pause_until_complete(PAUSE_SPINS)
186    }
187    fn head_lba(&mut self) -> Option<u32> {
188        let position = PlayPosition::parse(&self.try_play_position(COMMAND_SPINS)?)?;
189        let frames = (position.absolute_min as u32 * 60 + position.absolute_sec as u32) * 75
190            + position.absolute_frame as u32;
191        // The drive counts from the start of the lead-in, two seconds before LBA 0.
192        frames.checked_sub(150)
193    }
194}
195
196#[derive(Copy, Clone, Debug, PartialEq, Eq)]
197enum State {
198    Idle,
199    /// Started; the head has not been seen inside the file yet. The drive
200    /// reports its old position until the seek lands, so an end test now
201    /// would fire on the previous run's position.
202    Seeking,
203    Streaming,
204}
205
206struct Engine {
207    state: State,
208    song: Option<Song>,
209    looping: bool,
210    /// A pause was refused; try again at the next poll.
211    pause_pending: bool,
212    start_lba: u32,
213    head_lba: u32,
214}
215
216impl Engine {
217    const fn new() -> Self {
218        Self {
219            state: State::Idle,
220            song: None,
221            looping: false,
222            pause_pending: false,
223            start_lba: 0,
224            head_lba: 0,
225        }
226    }
227
228    fn start(&mut self, drive: &mut impl Drive, song: Song, looping: bool) -> Result<(), Error> {
229        let file = song.file;
230        let result = (|| {
231            if !drive.unmute() {
232                return Err(Error::Unmute);
233            }
234            if !drive.set_mode(file.speed.mode()) {
235                return Err(Error::SetMode);
236            }
237            if !drive.set_filter(file.number, song.channel) {
238                return Err(Error::SetFilter);
239            }
240            self.begin_read(drive, song)
241        })();
242        if result.is_ok() {
243            self.song = Some(song);
244            self.looping = looping;
245        } else {
246            self.state = State::Idle;
247            self.song = None;
248        }
249        result
250    }
251
252    /// Seek to the top of the file and start streaming.
253    fn begin_read(&mut self, drive: &mut impl Drive, song: Song) -> Result<(), Error> {
254        let start = shift_lba(song.file.lba);
255        if !drive.set_target(start) {
256            return Err(Error::Seek);
257        }
258        if !drive.start_streaming() {
259            return Err(Error::Read);
260        }
261        self.start_lba = start;
262        self.head_lba = start;
263        self.pause_pending = false;
264        self.state = State::Seeking;
265        Ok(())
266    }
267
268    /// Pause a running drive. The drive refuses a pause during the seek that
269    /// starts a read, so a refused pause is retried by [`Self::poll`].
270    fn stop(&mut self, drive: &mut impl Drive) {
271        if self.state != State::Idle {
272            self.pause_pending = !drive.pause();
273        }
274        self.state = State::Idle;
275        self.song = None;
276    }
277
278    fn poll(&mut self, drive: &mut impl Drive) -> Event {
279        let Some(song) = self.song else {
280            if self.pause_pending {
281                self.pause_pending = !drive.pause();
282            }
283            return Event::Idle;
284        };
285        let end = self.start_lba + song.file.span_sector_count();
286        if let Some(head) = drive.head_lba() {
287            if self.state == State::Seeking && (self.start_lba..end).contains(&head) {
288                self.state = State::Streaming;
289            }
290            if self.state == State::Streaming {
291                self.head_lba = head;
292            }
293            if self.state == State::Streaming && head >= end {
294                return self.reach_end(drive, song);
295            }
296        }
297        Event::Playing
298    }
299
300    fn is_streaming(&self) -> bool {
301        self.state == State::Streaming
302    }
303
304    fn elapsed_millis(&self) -> u32 {
305        let Some(song) = self.song else {
306            return 0;
307        };
308        let sectors = self.head_lba.saturating_sub(self.start_lba);
309        sectors * 1000 / song.file.speed.sectors_per_second()
310    }
311
312    fn reach_end(&mut self, drive: &mut impl Drive, song: Song) -> Event {
313        if self.looping && self.begin_read(drive, song).is_ok() {
314            return Event::Looped;
315        }
316        self.stop(drive);
317        Event::Finished
318    }
319}
320
321/// Plays XA-ADPCM songs. Owns the CD token: nothing else drives the
322/// controller while a player exists.
323pub struct Player {
324    cd: Cd,
325    engine: Engine,
326}
327
328impl Player {
329    /// A player with nothing playing. The SPU's CD input must be on and have
330    /// a volume (`psx_spu::enable_cd_audio`, `psx_spu::set_cd_volume`), and
331    /// the drive's own mixer must pass audio ([`set_volume`](Self::set_volume)).
332    pub const fn new(cd: Cd) -> Self {
333        Self {
334            cd,
335            engine: Engine::new(),
336        }
337    }
338
339    /// Stop and give the token back.
340    pub fn release(mut self) -> Cd {
341        self.stop();
342        self.cd
343    }
344
345    /// Start `song` from the top (or restart it). With `looping` it
346    /// restarts by itself when [`poll`](Self::poll) sees it end. Blocks for
347    /// the five drive commands, a few milliseconds on a warm drive.
348    pub fn play(&mut self, song: Song, looping: bool) -> Result<(), Error> {
349        self.engine.start(&mut self.cd, song, looping)
350    }
351
352    /// Pause the drive and forget the song. Waits for the drive to settle.
353    pub fn stop(&mut self) {
354        self.engine.stop(&mut self.cd);
355    }
356
357    /// Call about once per frame: notices the end of the song and loops or
358    /// stops. Costs one drive command.
359    pub fn poll(&mut self) -> Event {
360        self.engine.poll(&mut self.cd)
361    }
362
363    /// Whether a song is started and has not finished.
364    pub fn is_playing(&self) -> bool {
365        self.engine.state != State::Idle
366    }
367
368    /// Whether the head has been seen inside the song file since the last
369    /// start, so the audio is flowing and not still seeking.
370    pub fn is_streaming(&self) -> bool {
371        self.engine.is_streaming()
372    }
373
374    /// The song being played.
375    pub fn song(&self) -> Option<Song> {
376        self.engine.song
377    }
378
379    /// Time since the start of the song file at the last poll, in
380    /// milliseconds (the head runs a sector or two ahead of the sound).
381    pub fn elapsed_millis(&self) -> u32 {
382        self.engine.elapsed_millis()
383    }
384
385    /// Set the level of the drive's audio output (CD-DA too). `0x80` is
386    /// unity, `0xFF` about twice that; `(left, right)` feed the left and
387    /// right outputs of a stereo song.
388    pub fn set_volume(&mut self, left: u8, right: u8) {
389        self.cd.set_audio_mixer(left, 0, right, 0);
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    extern crate std;
396    use super::*;
397    use std::string::{String, ToString};
398    use std::vec::Vec;
399    use std::{format, vec};
400
401    #[test]
402    fn an_idle_player_owns_the_token_and_hands_it_back() {
403        // SAFETY: a test-local token on the host; an idle player sends the
404        // drive nothing.
405        let player = Player::new(unsafe { Cd::steal() });
406        assert!(!player.is_playing());
407        let _token: Cd = player.release();
408    }
409
410    /// Records commands; `heads` is what GetlocP answers, one per poll.
411    #[derive(Default)]
412    struct Fake {
413        log: Vec<String>,
414        fail_at: Option<&'static str>,
415        heads: Vec<Option<u32>>,
416        pause_refusals: u32,
417    }
418
419    impl Fake {
420        fn step(&mut self, name: &str) -> bool {
421            self.log.push(name.to_string());
422            self.fail_at
423                != Some(match name {
424                    n if n.starts_with("unmute") => "unmute",
425                    n if n.starts_with("mode") => "mode",
426                    n if n.starts_with("filter") => "filter",
427                    n if n.starts_with("target") => "target",
428                    _ => "read",
429                })
430        }
431    }
432
433    impl Drive for Fake {
434        fn unmute(&mut self) -> bool {
435            self.step("unmute")
436        }
437        fn set_mode(&mut self, mode: u8) -> bool {
438            self.step(&format!("mode {mode:#04x}"))
439        }
440        fn set_filter(&mut self, file: u8, channel: u8) -> bool {
441            self.step(&format!("filter {file} {channel}"))
442        }
443        fn set_target(&mut self, lba: u32) -> bool {
444            self.step(&format!("target {lba}"))
445        }
446        fn start_streaming(&mut self) -> bool {
447            self.step("read")
448        }
449        fn pause(&mut self) -> bool {
450            self.log.push("pause".into());
451            if self.pause_refusals > 0 {
452                self.pause_refusals -= 1;
453                return false;
454            }
455            true
456        }
457        fn head_lba(&mut self) -> Option<u32> {
458            if self.heads.is_empty() {
459                None
460            } else {
461                self.heads.remove(0)
462            }
463        }
464    }
465
466    /// 100 sectors of songs plus the guard, at LBA 1000.
467    fn song(speed: DriveSpeed) -> Song {
468        File::new(1000, 100 + END_GUARD_SECTORS, 3, speed).song(2)
469    }
470
471    #[test]
472    fn start_sets_up_the_filter_before_the_read() {
473        let mut drive = Fake::default();
474        let mut engine = Engine::new();
475        engine
476            .start(&mut drive, song(DriveSpeed::Single), true)
477            .unwrap();
478        assert_eq!(
479            drive.log,
480            ["unmute", "mode 0x48", "filter 3 2", "target 1000", "read"]
481        );
482        let mut drive = Fake::default();
483        engine
484            .start(&mut drive, song(DriveSpeed::Double), true)
485            .unwrap();
486        assert_eq!(drive.log[1], "mode 0xc8");
487    }
488
489    #[test]
490    fn each_refused_step_is_reported_and_leaves_the_player_idle() {
491        for (at, error) in [
492            ("unmute", Error::Unmute),
493            ("mode", Error::SetMode),
494            ("filter", Error::SetFilter),
495            ("target", Error::Seek),
496            ("read", Error::Read),
497        ] {
498            let mut drive = Fake {
499                fail_at: Some(at),
500                ..Fake::default()
501            };
502            let mut engine = Engine::new();
503            let result = engine.start(&mut drive, song(DriveSpeed::Single), false);
504            assert_eq!(result, Err(error));
505            assert_eq!(engine.poll(&mut drive), Event::Idle);
506        }
507    }
508
509    #[test]
510    fn an_old_head_position_does_not_end_a_fresh_start() {
511        let mut drive = Fake::default();
512        let mut engine = Engine::new();
513        engine
514            .start(&mut drive, song(DriveSpeed::Double), true)
515            .unwrap();
516        drive.log.clear();
517        // Past the end (where the previous run stopped), then inside the file.
518        drive.heads = vec![Some(1105), None, Some(1003), Some(1050)];
519        assert_eq!(engine.poll(&mut drive), Event::Playing);
520        assert_eq!(engine.poll(&mut drive), Event::Playing);
521        assert!(!engine.is_streaming());
522        assert_eq!(engine.poll(&mut drive), Event::Playing);
523        assert!(engine.is_streaming());
524        assert_eq!(engine.elapsed_millis(), 3 * 1000 / 150);
525        assert_eq!(engine.poll(&mut drive), Event::Playing);
526        assert_eq!(engine.elapsed_millis(), 50 * 1000 / 150);
527        assert!(
528            drive.log.is_empty(),
529            "no command beyond GetlocP: {:?}",
530            drive.log
531        );
532    }
533
534    #[test]
535    fn a_loop_restarts_at_the_top_and_waits_for_the_head_again() {
536        let mut drive = Fake::default();
537        let mut engine = Engine::new();
538        engine
539            .start(&mut drive, song(DriveSpeed::Single), true)
540            .unwrap();
541        drive.log.clear();
542        drive.heads = vec![Some(1010), Some(1100), Some(1103), Some(1020)];
543        assert_eq!(engine.poll(&mut drive), Event::Playing);
544        assert_eq!(engine.poll(&mut drive), Event::Looped);
545        assert_eq!(drive.log, ["target 1000", "read"]);
546        // The drive still reports the guard region until the seek lands.
547        assert_eq!(engine.poll(&mut drive), Event::Playing);
548        assert_eq!(drive.log.len(), 2);
549        assert_eq!(engine.poll(&mut drive), Event::Playing);
550        assert!(engine.is_streaming());
551    }
552
553    #[test]
554    fn a_one_shot_pauses_at_the_end_and_goes_idle() {
555        let mut drive = Fake::default();
556        let mut engine = Engine::new();
557        engine
558            .start(&mut drive, song(DriveSpeed::Single), false)
559            .unwrap();
560        drive.log.clear();
561        drive.heads = vec![Some(1099), Some(1101)];
562        assert_eq!(engine.poll(&mut drive), Event::Playing);
563        assert_eq!(engine.poll(&mut drive), Event::Finished);
564        assert_eq!(drive.log, ["pause"]);
565        assert_eq!(engine.poll(&mut drive), Event::Idle);
566    }
567
568    #[test]
569    fn stop_pauses_only_a_running_player() {
570        let mut drive = Fake::default();
571        let mut engine = Engine::new();
572        engine.stop(&mut drive);
573        assert!(drive.log.is_empty());
574        engine
575            .start(&mut drive, song(DriveSpeed::Single), false)
576            .unwrap();
577        engine.stop(&mut drive);
578        assert_eq!(drive.log.last().unwrap(), "pause");
579        assert_eq!(engine.poll(&mut drive), Event::Idle);
580    }
581
582    #[test]
583    fn a_refused_pause_is_retried_at_the_next_polls() {
584        let mut drive = Fake {
585            pause_refusals: 2,
586            ..Fake::default()
587        };
588        let mut engine = Engine::new();
589        engine
590            .start(&mut drive, song(DriveSpeed::Single), false)
591            .unwrap();
592        drive.log.clear();
593        engine.stop(&mut drive);
594        assert_eq!(engine.poll(&mut drive), Event::Idle);
595        assert_eq!(engine.poll(&mut drive), Event::Idle);
596        assert_eq!(drive.log, ["pause", "pause", "pause"]);
597        // Accepted: no further commands.
598        assert_eq!(engine.poll(&mut drive), Event::Idle);
599        assert_eq!(drive.log.len(), 3);
600    }
601
602    #[test]
603    fn starting_a_song_cancels_a_pending_pause() {
604        let mut drive = Fake {
605            pause_refusals: 5,
606            ..Fake::default()
607        };
608        let mut engine = Engine::new();
609        engine
610            .start(&mut drive, song(DriveSpeed::Single), false)
611            .unwrap();
612        engine.stop(&mut drive);
613        engine
614            .start(&mut drive, song(DriveSpeed::Single), false)
615            .unwrap();
616        drive.log.clear();
617        assert_eq!(engine.poll(&mut drive), Event::Playing);
618        assert!(drive.log.is_empty());
619    }
620
621    #[test]
622    fn a_failed_loop_restart_finishes_the_song() {
623        let mut drive = Fake::default();
624        let mut engine = Engine::new();
625        engine
626            .start(&mut drive, song(DriveSpeed::Single), true)
627            .unwrap();
628        drive.heads = vec![Some(1010), Some(1100)];
629        drive.fail_at = Some("target");
630        assert_eq!(engine.poll(&mut drive), Event::Playing);
631        assert_eq!(engine.poll(&mut drive), Event::Finished);
632        assert_eq!(drive.log.last().unwrap(), "pause");
633    }
634
635    #[test]
636    fn the_file_span_excludes_the_guard() {
637        let file = File::from_directory_entry(7, 2048 * 116, 1, DriveSpeed::Single);
638        assert_eq!(file.span_sector_count(), 100);
639        assert_eq!(
640            File::new(0, 4, 0, DriveSpeed::Double).span_sector_count(),
641            0
642        );
643    }
644}