1use crate::cd::PlayPosition;
26use crate::periph::Cd;
27
28pub const COLD_DRIVE_DELAY_TICKS: u32 = 45;
31pub const DEFAULT_COMMAND_SPINS: u32 = crate::cd::DEFAULT_COMMAND_SPINS;
33const RETRY_AFTER_OK_TICKS: u32 = 2;
34const RETRY_AFTER_NAK_TICKS: u32 = 4;
35
36#[derive(Copy, Clone, Debug, PartialEq, Eq)]
37enum StartStep {
38 SetMode,
39 Demute,
40 Play,
41 Done,
42}
43
44#[derive(Copy, Clone, Debug)]
55pub struct PlaybackStarter {
56 step: StartStep,
57 next_try_tick: u32,
58 spins: u32,
59 retry_ticks: u32,
60}
61
62impl PlaybackStarter {
63 pub const fn new() -> Self {
65 Self {
66 step: StartStep::Done,
67 next_try_tick: 0,
68 spins: DEFAULT_COMMAND_SPINS,
69 retry_ticks: RETRY_AFTER_NAK_TICKS,
70 }
71 }
72
73 pub const fn with_spins(mut self, spins: u32) -> Self {
76 self.spins = spins;
77 self
78 }
79
80 pub const fn with_retry_ticks(mut self, ticks: u32) -> Self {
83 self.retry_ticks = ticks;
84 self
85 }
86
87 pub fn begin_after(&mut self, now_tick: u32, delay: u32) {
89 self.step = StartStep::SetMode;
90 self.next_try_tick = now_tick.wrapping_add(delay);
91 }
92
93 pub fn begin(&mut self, now_tick: u32) {
96 self.begin_after(now_tick, COLD_DRIVE_DELAY_TICKS);
97 }
98
99 pub fn has_started(&self) -> bool {
101 self.step == StartStep::Done
102 }
103
104 pub fn step_code(&self) -> u8 {
109 match self.step {
110 StartStep::SetMode => 0,
111 StartStep::Demute => 1,
112 StartStep::Play => 2,
113 StartStep::Done => 3,
114 }
115 }
116
117 pub fn tick_on(&mut self, cd: &mut Cd, now_tick: u32, track: u8) -> bool {
121 if self.step == StartStep::Done || now_tick.wrapping_sub(self.next_try_tick) > u32::MAX / 2
122 {
123 return false;
124 }
125 self.tick_with(now_tick, track, |step, track, spins| {
126 let ok = match step {
127 StartStep::SetMode => cd.try_set_mode(psx_hw::cd::MODE_CDDA, spins).is_some(),
128 StartStep::Demute => cd.try_unmute(spins).is_some(),
129 StartStep::Play => cd.try_play_track(track, spins).is_some(),
130 StartStep::Done => unreachable!(),
131 };
132 let _ = cd.try_status(spins);
133 ok
134 })
135 }
136
137 fn tick_with(
138 &mut self,
139 now_tick: u32,
140 track: u8,
141 mut command: impl FnMut(StartStep, u8, u32) -> bool,
142 ) -> bool {
143 if self.step == StartStep::Done || now_tick.wrapping_sub(self.next_try_tick) > u32::MAX / 2
144 {
145 return false;
146 }
147 let ok = command(self.step, track, self.spins);
148 if ok {
153 self.step = match self.step {
154 StartStep::SetMode => StartStep::Demute,
155 StartStep::Demute => StartStep::Play,
156 StartStep::Play | StartStep::Done => StartStep::Done,
157 };
158 self.next_try_tick = now_tick.wrapping_add(RETRY_AFTER_OK_TICKS);
159 self.step == StartStep::Done
160 } else {
161 self.next_try_tick = now_tick.wrapping_add(self.retry_ticks);
162 false
163 }
164 }
165}
166
167impl Default for PlaybackStarter {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173#[derive(Copy, Clone, Debug)]
187pub struct EndDetector {
188 armed: bool,
189 quiet_polls: u8,
190 threshold: u8,
191}
192
193impl EndDetector {
194 pub const fn new(threshold: u8) -> Self {
196 Self {
197 armed: false,
198 quiet_polls: 0,
199 threshold,
200 }
201 }
202
203 pub fn rearm(&mut self) {
206 self.armed = false;
207 self.quiet_polls = 0;
208 }
209
210 pub fn poll(&mut self, status: Option<u8>) -> bool {
214 let Some(status) = status else {
215 self.quiet_polls = 0;
216 return false;
217 };
218 if status & psx_hw::cd::STAT_PLAYING != 0 {
219 self.armed = true;
220 self.quiet_polls = 0;
221 return false;
222 }
223 let quiet = status & (psx_hw::cd::STAT_PLAYING | psx_hw::cd::STAT_SEEKING) == 0;
224 if !(self.armed && quiet) {
225 self.quiet_polls = 0;
226 return false;
227 }
228 self.quiet_polls += 1;
229 if self.quiet_polls >= self.threshold {
230 self.rearm();
231 return true;
232 }
233 false
234 }
235
236 pub fn is_armed(&self) -> bool {
238 self.armed
239 }
240
241 pub fn quiet_polls(&self) -> u8 {
243 self.quiet_polls
244 }
245}
246
247const DEADBAND_MS: i32 = 5;
249const SPIKE_MS: i32 = 150;
252const RESYNC_TICKS: u32 = 30;
255
256pub struct PlaybackClock {
264 ticks_hz: u32,
265 anchor_ms: u32,
266 anchor_tick: u32,
267 last_now_ms: u32,
268 last_poll_tick: u32,
269 pending_spike_ms: Option<u32>,
270 playing: bool,
271}
272
273impl PlaybackClock {
274 pub const fn new(ticks_hz: u32) -> Self {
276 Self {
277 ticks_hz,
278 anchor_ms: 0,
279 anchor_tick: 0,
280 last_now_ms: 0,
281 last_poll_tick: 0,
282 pending_spike_ms: None,
283 playing: false,
284 }
285 }
286
287 pub fn start(&mut self, now_tick: u32) {
290 self.anchor_ms = 0;
291 self.anchor_tick = now_tick;
292 self.last_now_ms = 0;
293 self.last_poll_tick = now_tick;
294 self.pending_spike_ms = None;
295 self.playing = true;
296 }
297
298 pub fn is_playing(&self) -> bool {
300 self.playing
301 }
302
303 pub fn tick_on(&mut self, cd: &mut Cd, now_tick: u32) -> u32 {
306 if !self.playing {
307 return 0;
308 }
309 if now_tick.wrapping_sub(self.last_poll_tick) >= RESYNC_TICKS {
310 self.last_poll_tick = now_tick;
311 if let Some(drive_ms) = poll_drive_ms(cd) {
312 self.consider_anchor(now_tick, drive_ms);
313 }
314 }
315 let now = self.predict(now_tick);
316 if now > self.last_now_ms {
317 self.last_now_ms = now;
318 }
319 self.last_now_ms
320 }
321
322 fn predict(&self, now_tick: u32) -> u32 {
323 let elapsed = now_tick.wrapping_sub(self.anchor_tick);
324 self.anchor_ms + elapsed * 1000 / self.ticks_hz
325 }
326
327 fn consider_anchor(&mut self, now_tick: u32, drive_ms: u32) {
330 let predicted = self.predict(now_tick);
331 let delta = drive_ms as i32 - predicted as i32;
332 let mag = delta.abs();
333
334 if mag <= DEADBAND_MS {
335 self.pending_spike_ms = None; return;
337 }
338 if mag > SPIKE_MS {
339 match self.pending_spike_ms {
342 Some(prev) if (drive_ms as i32 - prev as i32).abs() <= SPIKE_MS => {
343 self.set_anchor(now_tick, drive_ms);
344 self.last_now_ms = drive_ms;
348 self.pending_spike_ms = None;
349 }
350 _ => self.pending_spike_ms = Some(drive_ms),
351 }
352 return;
353 }
354 self.pending_spike_ms = None;
357 if drive_ms >= self.last_now_ms {
358 self.set_anchor(now_tick, drive_ms);
359 }
360 }
361
362 fn set_anchor(&mut self, now_tick: u32, drive_ms: u32) {
363 self.anchor_ms = drive_ms;
364 self.anchor_tick = now_tick;
365 }
366}
367
368fn poll_drive_ms(cd: &mut Cd) -> Option<u32> {
371 let resp = cd.try_play_position(16_384)?;
372 PlayPosition::parse(&resp).map(|p| p.relative_millis())
373}
374
375impl PlaybackStarter {
376 #[deprecated(note = "use `tick_on` with the `Cd` token")]
378 pub fn tick(&mut self, now_tick: u32, track: u8) -> bool {
379 self.tick_on(&mut steal(), now_tick, track)
380 }
381
382 #[deprecated(note = "renamed to `has_started`")]
384 pub fn started(&self) -> bool {
385 self.has_started()
386 }
387}
388
389impl EndDetector {}
390
391impl PlaybackClock {}
392
393fn steal() -> Cd {
395 unsafe { Cd::steal() }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn configurable_start_and_retry_keep_success_gap_and_wrap() {
406 let mut starter = PlaybackStarter::new()
407 .with_spins(16384)
408 .with_retry_ticks(60);
409 let start = u32::MAX - 10;
410 starter.begin_after(start, 30);
411 let due = start.wrapping_add(30);
412 assert!(!starter.tick_with(due - 1, 2, |_, _, _| panic!("early command")));
413 assert!(!starter.tick_with(due, 2, |step, track, spins| {
414 assert_eq!(step, StartStep::SetMode);
415 assert_eq!((track, spins), (2, 16384));
416 false
417 }));
418 assert!(!starter.tick_with(due + 59, 2, |_, _, _| panic!("early retry")));
419 for (t, step, result) in [
420 (due + 60, StartStep::SetMode, false),
421 (due + 62, StartStep::Demute, false),
422 (due + 64, StartStep::Play, true),
423 ] {
424 assert_eq!(
425 starter.tick_with(t, 2, |s, _, _| {
426 assert_eq!(s, step);
427 true
428 }),
429 result
430 );
431 }
432 assert!(!starter.tick_with(due + 66, 2, |_, _, _| panic!("repeated Play")));
433 starter.begin_after(100, 0);
434 assert!(!starter.tick_with(100, 2, |s, _, _| {
435 assert_eq!(s, StartStep::SetMode);
436 true
437 }));
438 }
439
440 fn started() -> PlaybackClock {
441 let mut c = PlaybackClock::new(60);
442 c.start(0);
443 c
444 }
445
446 #[test]
447 fn interpolates_between_anchors() {
448 let mut c = started();
449 c.set_anchor(0, 1000);
450 assert_eq!(c.predict(30), 1500); assert_eq!(c.predict(60), 2000);
452 }
453
454 #[test]
455 fn deadband_readings_are_ignored() {
456 let mut c = started();
457 c.set_anchor(0, 1000);
458 c.consider_anchor(30, 1503); assert_eq!(c.anchor_ms, 1000); }
461
462 #[test]
463 fn moderate_drift_reanchors_forward_only() {
464 let mut c = started();
465 c.set_anchor(0, 1000);
466 c.last_now_ms = 1500;
467 c.consider_anchor(30, 1540); assert_eq!((c.anchor_ms, c.anchor_tick), (1540, 30));
469 c.last_now_ms = 1600;
471 c.consider_anchor(33, 1560);
472 assert_eq!(c.anchor_ms, 1540); }
474
475 #[test]
476 fn spike_needs_confirmation() {
477 let mut c = started();
478 c.set_anchor(0, 1000);
479 c.consider_anchor(30, 9000); assert_eq!(c.anchor_ms, 1000);
481 c.consider_anchor(60, 9010); assert_eq!(c.anchor_ms, 9010);
483 }
484
485 #[test]
486 fn confirmed_loop_resets_the_monotonic_floor() {
487 let mut c = started();
488 c.set_anchor(0, 60_000);
489 c.last_now_ms = 60_000;
490 c.consider_anchor(30, 100); c.consider_anchor(60, 120); assert_eq!(c.anchor_ms, 120);
493 assert_eq!(c.last_now_ms, 120); }
495
496 #[test]
497 fn stopped_clock_reads_zero() {
498 let mut c = PlaybackClock::new(60);
499 assert_eq!(c.tick_on(&mut steal(), 100), 0);
502 }
503
504 #[test]
508 fn spin_up_window_is_not_an_end_of_track() {
509 let mut d = EndDetector::new(3);
510 d.rearm();
511 for s in [0x00, 0x00, 0x00, 0x00, 0x42, 0x42, 0x82] {
512 assert!(!d.poll(Some(s)), "advanced on status {s:#04x}");
513 }
514 assert!(d.is_armed());
515 }
516
517 #[test]
518 fn a_real_end_fires_once_after_the_threshold() {
519 let mut d = EndDetector::new(3);
520 d.rearm();
521 assert!(!d.poll(Some(0x82)));
522 assert!(!d.poll(Some(0x02)));
523 assert!(!d.poll(Some(0x02)));
524 assert!(d.poll(Some(0x02)), "third quiet poll after playing");
525 assert!(!d.poll(Some(0x02)));
527 assert!(!d.poll(Some(0x02)));
528 assert!(!d.poll(Some(0x02)));
529 }
530
531 #[test]
532 fn timeouts_and_seeks_reset_the_quiet_streak() {
533 let mut d = EndDetector::new(2);
534 d.rearm();
535 assert!(!d.poll(Some(0x82)));
536 assert!(!d.poll(Some(0x02)));
537 assert!(!d.poll(None), "timeout must not count as quiet");
538 assert!(!d.poll(Some(0x02)));
539 assert!(!d.poll(Some(0x42)), "a seek is activity, not quiet");
540 assert!(!d.poll(Some(0x02)));
541 assert!(d.poll(Some(0x02)));
542 }
543}