1//! Sega CD's physical drive, which documentation refers to as the CDD 2 3use crate::api::SegaCdLoadResult; 4use crate::cddrive::cdc::{Rchip, RchipDmaArgs}; 5use bincode::{Decode, Encode}; 6use cdrom::cdtime::CdTime; 7use cdrom::cue::{Track, TrackType}; 8use cdrom::reader::CdRom; 9use genesis_config::{GenesisRegion, SegaCdEmulatorConfig}; 10use jgenesis_proc_macros::PartialClone; 11use regex::Regex; 12use std::cmp::Ordering; 13use std::sync::LazyLock; 14use std::{array, cmp}; 15 16const INITIAL_STATUS: [u8; 10] = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F]; 17 18// The BIOS freezes if this delay is too short or too long 19// Time Gal freezes at boot if this delay is too short 20const PLAY_DELAY_CLOCKS: u8 = 6; 21 22// 2x signed 16-bit PCM samples, one per stereo channel 23const BYTES_PER_AUDIO_SAMPLE: u16 = 4; 24 25const MAX_FADER_VOLUME: u16 = 1 << 10; 26 27// Fast-forward / rewind should skip at roughly 100x playback speed 28const FAST_FORWARD_SECONDS: u8 = 1; 29const FAST_FORWARD_FRAMES: u8 = 25; 30 31const DIVIDER_75HZ: u16 = 44100 / 75; 32 33#[repr(u8)] 34#[derive(Debug, Clone, Copy, PartialEq, Eq)] 35enum Status { 36 Stopped = 0x00, 37 Playing = 0x01, 38 Seeking = 0x02, 39 Scanning = 0x03, 40 Paused = 0x04, 41 TrayOpen = 0x05, 42 InvalidCommand = 0x07, 43 ReadingToc = 0x09, 44 TrackSkipping = 0x0A, 45 NoDisc = 0x0B, 46 DiscEnd = 0x0C, 47 DiscStart = 0x0D, 48 TrayMoving = 0x0E, 49} 50 51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Encode, Decode)] 52enum ReportType { 53 #[default] 54 AbsoluteTime, 55 RelativeTime, 56 CurrentTrack, 57 DiscLength, 58 StartAndEndTracks, 59 TrackNStartTime(u8), 60} 61 62impl ReportType { 63 fn to_byte(self) -> u8 { 64 match self { 65 Self::AbsoluteTime => 0x00, 66 Self::RelativeTime => 0x01, 67 Self::CurrentTrack => 0x02, 68 Self::DiscLength => 0x03, 69 Self::StartAndEndTracks => 0x04, 70 Self::TrackNStartTime(..) => 0x05, 71 } 72 } 73 74 fn from_command(command: [u8; 10]) -> Self { 75 // Report type is always stored in Command 3 for Read TOC commands 76 match command[3] { 77 0x00 => Self::AbsoluteTime, 78 0x01 => Self::RelativeTime, 79 0x02 => Self::CurrentTrack, 80 0x03 => Self::DiscLength, 81 0x04 => Self::StartAndEndTracks, 82 0x05 => { 83 // Track number (BCD) is at Command 4-5 84 let track_number = 10 * command[4] + command[5]; 85 Self::TrackNStartTime(track_number) 86 } 87 _ => { 88 log::warn!("Invalid CDD report type byte: {}; defaulting to absolute", command[3]); 89 Self::AbsoluteTime 90 } 91 } 92 } 93} 94 95#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 96enum ReaderStatus { 97 Playing, 98 Paused, 99} 100 101#[derive(Debug, Clone, Copy, Default, Encode, Decode)] 102enum State { 103 #[default] 104 MotorStopped, 105 NoDisc, 106 PreparingToPlay { 107 time: CdTime, 108 clocks_remaining: u8, 109 }, 110 Playing(CdTime), 111 Paused(CdTime), 112 Seeking { 113 current_time: CdTime, 114 seek_time: CdTime, 115 next_status: ReaderStatus, 116 clocks_remaining: u8, 117 }, 118 TrackSkipping { 119 current_time: CdTime, 120 skip_time: CdTime, 121 clocks_remaining: u8, 122 }, 123 FastForwarding(CdTime), 124 Rewinding(CdTime), 125 DiscStart, 126 DiscEnd(CdTime), 127 TrayOpening { 128 auto_close: bool, 129 }, 130 TrayOpen { 131 auto_close: bool, 132 }, 133 TrayClosing, 134 InvalidCommand(CdTime), 135 ReadingToc, 136} 137 138impl State { 139 fn current_time(self) -> CdTime { 140 match self { 141 Self::MotorStopped 142 | Self::NoDisc 143 | Self::DiscStart 144 | Self::ReadingToc 145 | Self::TrayOpening { .. } 146 | Self::TrayOpen { .. } 147 | Self::TrayClosing => CdTime::ZERO, 148 Self::PreparingToPlay { time, .. } 149 | Self::Playing(time) 150 | Self::Paused(time) 151 | Self::FastForwarding(time) 152 | Self::Rewinding(time) 153 | Self::DiscEnd(time) 154 | Self::InvalidCommand(time) => time, 155 Self::Seeking { current_time, .. } | Self::TrackSkipping { current_time, .. } => { 156 current_time 157 } 158 } 159 } 160} 161 162#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)] 163pub enum CdModel { 164 One, 165 Two, 166} 167 168#[derive(Debug, Encode, Decode, PartialClone)] 169pub struct CdDrive { 170 #[partial_clone(default)] 171 disc: Option<CdRom>, 172 model: CdModel, 173 sector_buffer: Box<[u8; cdrom::BYTES_PER_SECTOR as usize]>, 174 state: State, 175 report_type: ReportType, 176 // If a command is sent while the drive is playing, it needs to read at least 1 more sector 177 // before it responds to the new command. Radical Rex depends on this or it will crash shortly 178 // into the intro 179 next_clock_play: Option<CdTime>, 180 interrupt_pending: bool, 181 status: [u8; 10], 182 audio_sample_idx: u16, 183 loaded_audio_sector: bool, 184 fader_volume: u16, 185 current_volume: u16, 186 divider_75hz: u16, 187 data_speed: u16, 188} 189 190impl CdDrive { 191 pub(super) fn new(disc: Option<CdRom>, model: CdModel, config: &SegaCdEmulatorConfig) -> Self { 192 Self { 193 disc, 194 model, 195 sector_buffer: Box::new(array::from_fn(|_| 0)), 196 state: State::default(), 197 report_type: ReportType::default(), 198 interrupt_pending: false, 199 next_clock_play: None, 200 status: INITIAL_STATUS, 201 audio_sample_idx: 0, 202 loaded_audio_sector: false, 203 fader_volume: 0, 204 current_volume: 0, 205 divider_75hz: DIVIDER_75HZ, 206 data_speed: config.disc_drive_speed.get(), 207 } 208 } 209 210 #[allow(clippy::match_same_arms)] 211 pub fn send_command(&mut self, command: [u8; 10]) { 212 log::trace!("CDD command: {command:02X?}"); 213 214 let prev_playing_time = match self.state { 215 State::Playing(time) => Some(time), 216 _ => None, 217 }; 218 219 match command[0] { 220 0x00 => { 221 // No-op; return current status 222 log::trace!(" Command: No-op"); 223 } 224 0x01 => { 225 // Stop motor 226 log::trace!(" Command: Stop motor"); 227 228 self.state = State::MotorStopped; 229 self.report_type = ReportType::AbsoluteTime; 230 } 231 0x02 => { 232 // Read TOC 233 log::trace!(" Command: Read TOC"); 234 235 self.execute_read_toc(command); 236 } 237 0x03 => { 238 // Seek and play 239 log::trace!(" Command: Seek and play"); 240 241 self.execute_seek(command, ReaderStatus::Playing); 242 } 243 0x04 => { 244 // Seek 245 log::trace!(" Command: Seek"); 246 247 self.execute_seek(command, ReaderStatus::Paused); 248 } 249 0x06 => { 250 // Pause 251 log::trace!(" Command: Pause"); 252 253 match &self.disc { 254 Some(_) => { 255 self.state = State::Paused(self.state.current_time()); 256 } 257 None => { 258 self.state = State::NoDisc; 259 } 260 } 261 } 262 0x07 => { 263 // Play 264 log::trace!(" Command: Play"); 265 266 match self.state { 267 State::Paused(time) | State::FastForwarding(time) | State::Rewinding(time) => { 268 self.state = 269 State::PreparingToPlay { time, clocks_remaining: PLAY_DELAY_CLOCKS }; 270 } 271 _ => {} 272 } 273 } 274 0x08 => { 275 // Fast-forward 276 log::trace!(" Command: Fast-forward"); 277 278 if self.disc.is_some() { 279 self.state = State::FastForwarding(self.state.current_time()); 280 } 281 } 282 0x09 => { 283 // Rewind 284 log::trace!(" Command: Rewind"); 285 286 if self.disc.is_some() { 287 self.state = State::Rewinding(self.state.current_time()); 288 } 289 } 290 0x0A => { 291 // Start track skipping 292 log::trace!(" Command: Track Skip"); 293 294 self.execute_track_skip(command); 295 } 296 0x0B => { 297 // Start track cueing 298 // This command does not appear to be used by any official BIOS version 299 log::error!("Ignoring unimplemented CDD command: Cue Track"); 300 } 301 0x0C => { 302 // Close tray 303 log::trace!(" Command: Close Tray"); 304 if matches!(self.state, State::TrayOpening { .. } | State::TrayOpen { .. }) { 305 self.state = State::TrayClosing; 306 } 307 } 308 0x0D => { 309 // Open tray 310 log::trace!(" Command: Open Tray"); 311 if !matches!(self.state, State::TrayOpening { .. }) { 312 self.state = State::TrayOpening { auto_close: false }; 313 } 314 } 315 _ => {} 316 } 317 318 // Executing any command other than No-op or Read TOC cancels the TOCN report; default to absolute time 319 if command[0] != 0x00 320 && command[0] != 0x02 321 && matches!(self.report_type, ReportType::TrackNStartTime(..)) 322 { 323 self.report_type = ReportType::AbsoluteTime; 324 } 325 326 self.update_status(); 327 328 if prev_playing_time.is_some() && !matches!(self.state, State::Playing(..)) { 329 // If the drive was playing and a command just changed the state, have it read one more 330 // sector on the next clock before it handles the new state (e.g. seeking). 331 // Per tests by krikzz, the CDD status changes immediately in response to a command 332 // despite this behavior, so changing the state immediately is correct 333 self.next_clock_play = prev_playing_time; 334 } 335 336 log::trace!("CDD status: {:02X?}", self.status); 337 } 338 339 fn update_status(&mut self) { 340 self.status.fill(0); 341 342 // Status 0 is always drive status 343 self.status[0] = self.current_cdd_status() as u8; 344 345 // If drive is stopped, leave status as all 0s 346 if matches!(self.state, State::MotorStopped) { 347 update_cdd_checksum(&mut self.status); 348 return; 349 } 350 351 // If seeking/skipping, return "not ready" status; not doing this causes Lunar to randomly freeze 352 if matches!( 353 self.state, 354 State::Seeking { .. } 355 | State::TrackSkipping { .. } 356 | State::TrayOpening { .. } 357 | State::TrayOpen { .. } 358 | State::TrayClosing 359 | State::NoDisc 360 ) { 361 self.status[1] = 0x0F; 362 update_cdd_checksum(&mut self.status); 363 return; 364 } 365 366 // Status 1 is always report type 367 self.status[1] = self.report_type.to_byte(); 368 369 // Only update other status bytes if there is a disc 370 if let Some(disc) = &self.disc { 371 // Status 2-8 depend on report type 372 match self.report_type { 373 ReportType::AbsoluteTime => { 374 // Write current absolute time in minutes/seconds/frames (BCD) to Status 2-7 375 let current_time = self.state.current_time(); 376 write_time_to_status(current_time, &mut self.status); 377 self.status[8] = self.status_flags(); 378 } 379 ReportType::RelativeTime => { 380 // Write current relative time in minutes/seconds/frames (BCD) to Status 2-7 381 let current_time = self.state.current_time(); 382 let track_start_time = disc 383 .cue() 384 .find_track_by_time(current_time) 385 .map_or(CdTime::ZERO, Track::effective_start_time); 386 write_time_to_status( 387 current_time.saturating_sub(track_start_time), 388 &mut self.status, 389 ); 390 self.status[8] = self.status_flags(); 391 } 392 ReportType::CurrentTrack => { 393 // Write current track number (BCD) to Status 2-3 394 let current_time = self.state.current_time(); 395 let track_number = 396 disc.cue().find_track_by_time(current_time).map_or(0, |track| track.number); 397 self.status[2] = track_number / 10; 398 self.status[3] = track_number % 10; 399 400 self.status[8] = self.status_flags(); 401 } 402 ReportType::DiscLength => { 403 // Write disc length in minutes/seconds/frames (BCD) to Status 2-7 404 let disc_end_time = disc.cue().last_track().end_time; 405 write_time_to_status(disc_end_time, &mut self.status); 406 self.status[8] = self.status_flags(); 407 } 408 ReportType::StartAndEndTracks => { 409 // Write start track number to Status 2-3 and end track number to Status 4-5, both in BCD 410 // Assume start track number is always 1 411 self.status[2] = 0x00; 412 self.status[3] = 0x01; 413 414 let end_track_number = disc.cue().last_track().number; 415 self.status[4] = end_track_number / 10; 416 self.status[5] = end_track_number % 10; 417 418 self.status[8] = self.status_flags(); 419 } 420 ReportType::TrackNStartTime(track_number) => { 421 let track = if track_number <= disc.cue().last_track().number { 422 disc.cue().track(track_number) 423 } else { 424 // ??? Invalid track number 425 disc.cue().last_track() 426 }; 427 428 let track_start_time = track.effective_start_time(); 429 let track_type = track.track_type; 430 431 // Write track start time in minutes/seconds/frames (BCD) to Status 2-7 432 write_time_to_status(track_start_time, &mut self.status); 433 434 // If this is a data track, OR Status 6 with $08 435 if track_type == TrackType::Data { 436 self.status[6] |= 0x08; 437 } 438 439 // Write the lower digit of track number to Status 8 440 self.status[8] = track_number % 10; 441 } 442 } 443 } 444 445 // Update checksum in Status 9 446 update_cdd_checksum(&mut self.status); 447 } 448 449 fn status_flags(&self) -> u8 { 450 // $04 if playing a data track, $00 otherwise 451 let playing_data_track = match self.state { 452 State::Playing(time) | State::PreparingToPlay { time, .. } => { 453 self.disc.as_ref().is_some_and(|disc| { 454 disc.cue() 455 .find_track_by_time(time) 456 .is_some_and(|track| track.track_type == TrackType::Data) 457 }) 458 } 459 _ => false, 460 }; 461 462 if playing_data_track { 0x04 } else { 0x00 } 463 } 464 465 fn current_cdd_status(&self) -> Status { 466 match self.state { 467 State::MotorStopped => Status::Stopped, 468 State::NoDisc => Status::NoDisc, 469 State::Paused(..) => Status::Paused, 470 State::PreparingToPlay { .. } | State::Playing(..) => Status::Playing, 471 State::Seeking { .. } => Status::Seeking, 472 State::TrackSkipping { .. } => Status::TrackSkipping, 473 State::FastForwarding(..) | State::Rewinding(..) => Status::Scanning, 474 State::DiscStart => Status::DiscStart, 475 State::DiscEnd(..) => Status::DiscEnd, 476 State::TrayOpening { .. } | State::TrayClosing => Status::TrayMoving, 477 State::TrayOpen { .. } => Status::TrayOpen, 478 State::InvalidCommand(..) => Status::InvalidCommand, 479 State::ReadingToc => Status::ReadingToc, 480 } 481 } 482 483 fn execute_read_toc(&mut self, command: [u8; 10]) { 484 let report_type = ReportType::from_command(command); 485 self.report_type = report_type; 486 487 log::trace!(" Report type changed to {report_type:?}"); 488 489 self.state = match (self.state, &self.disc, report_type) { 490 (State::MotorStopped, None, _) => State::NoDisc, 491 (State::MotorStopped, Some(_), _) => State::Paused(CdTime::ZERO), 492 (_, Some(_), ReportType::TrackNStartTime(..)) => { 493 // TOCN reports require reading the TOC; move back to start of disc 494 State::ReadingToc 495 } 496 _ => self.state, 497 }; 498 } 499 500 fn execute_seek(&mut self, command: [u8; 10], next_status: ReaderStatus) { 501 if self.disc.is_none() { 502 self.state = State::NoDisc; 503 return; 504 } 505 506 let Some(seek_time) = read_time_from_command(command) else { 507 self.state = State::InvalidCommand(self.state.current_time()); 508 return; 509 }; 510 511 let current_time = self.state.current_time(); 512 513 if seek_time == current_time { 514 log::trace!( 515 "Already at desired seek time {seek_time}; changing status to {next_status:?}" 516 ); 517 self.state = match next_status { 518 ReaderStatus::Paused => State::Paused(seek_time), 519 ReaderStatus::Playing => { 520 State::PreparingToPlay { time: seek_time, clocks_remaining: PLAY_DELAY_CLOCKS } 521 } 522 }; 523 return; 524 } 525 526 // Enforce a minimum seek time of 7 cycles at 75 Hz 527 // At least one game depends on this, the Japanese version of Cobra Command / Thunder Storm FX 528 let seek_clocks = cmp::max(7, estimate_seek_clocks(current_time, seek_time)); 529 530 log::trace!( 531 "Seeking from {current_time} to {seek_time}; estimated time {seek_clocks} 75Hz clocks" 532 ); 533 534 self.state = 535 State::Seeking { current_time, seek_time, next_status, clocks_remaining: seek_clocks }; 536 } 537 538 fn execute_track_skip(&mut self, command: [u8; 10]) { 539 let Some(disc) = &self.disc else { 540 self.state = State::NoDisc; 541 return; 542 }; 543 544 // Number of "tracks" to skip is in Command 4-7, as a 16-bit value stored across 4 nibbles 545 let skip_tracks = (u32::from(command[4] & 0x0F) << 12) 546 | (u32::from(command[5] & 0x0F) << 8) 547 | (u32::from(command[6] & 0x0F) << 4) 548 | u32::from(command[7] & 0x0F); 549 550 // Treat a "track" as 15 blocks. This isn't completely accurate but it doesn't need to be. 551 // The BIOS will often issue a Track Skip command before a Seek or Read command. 552 let skip_blocks = 15 * skip_tracks; 553 554 let current_time = self.state.current_time(); 555 556 // Command 3 holds direction; treat 0 as positive, non-0 as negative 557 let skip_time = if command[3] == 0 { 558 // Skip forwards 559 let skip_sector = current_time.to_sector_number() + skip_blocks; 560 let disc_end_time = disc.cue().last_track().end_time; 561 562 if skip_sector >= disc_end_time.to_sector_number() { 563 disc_end_time 564 } else { 565 CdTime::from_sector_number(skip_sector) 566 } 567 } else { 568 // Skip backwards 569 let skip_sector = current_time.to_sector_number().saturating_sub(skip_blocks); 570 CdTime::from_sector_number(skip_sector) 571 }; 572 573 let clocks_required = estimate_seek_clocks(current_time, skip_time); 574 575 log::trace!( 576 "Skipping from {current_time} to {skip_time}; estimated {clocks_required} 75Hz cycles" 577 ); 578 579 self.state = 580 State::TrackSkipping { current_time, skip_time, clocks_remaining: clocks_required }; 581 } 582 583 pub fn status(&self) -> [u8; 10] { 584 self.status 585 } 586 587 fn reading_data_track(&self) -> bool { 588 match self.state { 589 State::Playing(current_time) => self.disc.as_ref().is_some_and(|disc| { 590 disc.cue() 591 .find_track_by_time(current_time) 592 .is_some_and(|track| track.track_type == TrackType::Data) 593 }), 594 _ => false, 595 } 596 } 597 598 pub fn playing_audio(&self) -> bool { 599 match self.state { 600 State::Playing(current_time) => { 601 let is_audio_track = self.disc.as_ref().is_some_and(|disc| { 602 disc.cue() 603 .find_track_by_time(current_time) 604 .is_some_and(|track| track.track_type == TrackType::Audio) 605 }); 606 is_audio_track && self.loaded_audio_sector 607 } 608 _ => false, 609 } 610 } 611 612 pub fn set_fader_volume(&mut self, fader_volume: u16) { 613 self.fader_volume = cmp::min(MAX_FADER_VOLUME, fader_volume); 614 615 log::trace!("Fader volume set to {:03X}", self.fader_volume); 616 } 617 618 pub fn clock_44100hz( 619 &mut self, 620 rchip: &mut Rchip, 621 mut rchip_dma_args: RchipDmaArgs<'_>, 622 ) -> SegaCdLoadResult<(f64, f64)> { 623 self.adjust_fader_volume(); 624 625 let audio_sample = self.sample_audio().unwrap_or((0.0, 0.0)); 626 627 let divider_decrement = if self.reading_data_track() { self.data_speed } else { 1 }; 628 for _ in 0..divider_decrement { 629 self.divider_75hz -= 1; 630 if self.divider_75hz == 0 { 631 self.divider_75hz = DIVIDER_75HZ; 632 self.clock_75hz(rchip)?; 633 rchip.clock_75hz(); 634 } else { 635 rchip.clock_44100hz(rchip_dma_args.reborrow()); 636 } 637 } 638 639 Ok(audio_sample) 640 } 641 642 fn adjust_fader_volume(&mut self) { 643 match self.current_volume.cmp(&self.fader_volume) { 644 Ordering::Less => self.current_volume += 1, 645 Ordering::Greater => self.current_volume -= 1, 646 Ordering::Equal => {} 647 } 648 } 649 650 fn sample_audio(&mut self) -> Option<(f64, f64)> { 651 if !self.playing_audio() { 652 self.audio_sample_idx = 0; 653 return None; 654 } 655 656 let idx = self.audio_sample_idx as usize; 657 658 let sample_l = i16::from_le_bytes([self.sector_buffer[idx], self.sector_buffer[idx + 1]]); 659 let sample_r = 660 i16::from_le_bytes([self.sector_buffer[idx + 2], self.sector_buffer[idx + 3]]); 661 662 let fader_multiplier = fader_volume_multiplier(self.current_volume); 663 664 let sample_l = fader_multiplier * f64::from(sample_l) / -f64::from(i16::MIN); 665 let sample_r = fader_multiplier * f64::from(sample_r) / -f64::from(i16::MIN); 666 667 self.audio_sample_idx = 668 (self.audio_sample_idx + BYTES_PER_AUDIO_SAMPLE) % cdrom::BYTES_PER_SECTOR as u16; 669 670 Some((sample_l, sample_r)) 671 } 672 673 fn clock_75hz(&mut self, rchip: &mut Rchip) -> SegaCdLoadResult<()> { 674 // It is a bug if clock_75hz() is called when audio index is not 0; sample_audio() must 675 // be called before clock_75hz() on the cycle when both are called 676 assert_eq!(self.audio_sample_idx, 0); 677 678 // CDD interrupt fires once every 1/75 of a second 679 self.interrupt_pending = true; 680 681 if let Some(time) = self.next_clock_play.take() { 682 // If the state was just changed from playing, read one more sector before handling the 683 // new state 684 log::trace!("Reading sector at {time} before handling new state of {:?}", self.state); 685 return self.handle_playing(time, false, rchip); 686 } 687 688 match self.state { 689 State::Seeking { current_time, seek_time, next_status, clocks_remaining } => { 690 if clocks_remaining == 1 { 691 log::trace!("Seek to {seek_time} complete"); 692 693 self.state = match next_status { 694 ReaderStatus::Paused => State::Paused(seek_time), 695 ReaderStatus::Playing => State::PreparingToPlay { 696 time: seek_time, 697 clocks_remaining: PLAY_DELAY_CLOCKS, 698 }, 699 }; 700 } else { 701 // 113 clocks to seek across the entire disc 702 let new_time = estimate_intermediate_seek_time( 703 current_time, 704 seek_time, 705 clocks_remaining - 1, 706 ); 707 708 log::trace!( 709 "Current seek status: prev_time={current_time}, new_time={new_time}, seek_time={seek_time}, clocks_remaining={}", 710 clocks_remaining - 1 711 ); 712 713 self.state = State::Seeking { 714 current_time: new_time, 715 seek_time, 716 next_status, 717 clocks_remaining: clocks_remaining - 1, 718 }; 719 } 720 } 721 State::TrackSkipping { current_time, skip_time, clocks_remaining } => { 722 if clocks_remaining == 1 { 723 log::trace!("Skip to {skip_time} complete"); 724 725 self.state = State::Paused(skip_time); 726 } else { 727 // 56 clocks to skip across the entire desc 728 let new_time = estimate_intermediate_seek_time( 729 current_time, 730 skip_time, 731 clocks_remaining - 1, 732 ); 733 734 log::trace!( 735 "Current skip status: prev_time={current_time}, new_time={new_time}, skip_time={skip_time}, clocks_remaining={}", 736 clocks_remaining - 1 737 ); 738 739 self.state = State::TrackSkipping { 740 current_time: new_time, 741 skip_time, 742 clocks_remaining: clocks_remaining - 1, 743 }; 744 } 745 } 746 State::FastForwarding(time) => { 747 let new_time = time + CdTime::new(0, FAST_FORWARD_SECONDS, FAST_FORWARD_FRAMES); 748 self.state = if let Some(disc) = &self.disc { 749 let disc_end_time = disc.cue().last_track().end_time; 750 if new_time >= disc_end_time { 751 log::trace!("Fast-forwarded to end of disc"); 752 State::DiscEnd(disc_end_time) 753 } else { 754 log::trace!("Fast-forwarded to {new_time}"); 755 State::FastForwarding(new_time) 756 } 757 } else { 758 log::trace!("Fast-forwarded to {new_time}"); 759 State::FastForwarding(new_time) 760 }; 761 } 762 State::Rewinding(time) => { 763 let new_time = 764 time.saturating_sub(CdTime::new(0, FAST_FORWARD_SECONDS, FAST_FORWARD_FRAMES)); 765 if new_time == CdTime::ZERO { 766 log::trace!("Rewound to beginning of disc"); 767 self.state = State::DiscStart; 768 } else { 769 log::trace!("Rewound to {new_time}"); 770 self.state = State::Rewinding(new_time); 771 } 772 } 773 State::PreparingToPlay { time, clocks_remaining } => { 774 if clocks_remaining == 1 { 775 log::trace!("Beginning to play at {time}"); 776 777 self.state = State::Playing(time); 778 779 // Ensure that leftover data in the buffer doesn't get played until the buffer is refilled 780 self.loaded_audio_sector = false; 781 } else { 782 self.state = 783 State::PreparingToPlay { time, clocks_remaining: clocks_remaining - 1 }; 784 } 785 } 786 State::Playing(time) => { 787 log::trace!("Playing at {time}"); 788 self.handle_playing(time, true, rchip)?; 789 } 790 State::MotorStopped => { 791 match &self.disc { 792 Some(_) => { 793 // Always transition to Reading TOC one clock after the motor is stopped; this fixes 794 // the EU BIOS freezing after leaving the options menu 795 self.state = State::ReadingToc; 796 } 797 None => { 798 self.state = State::NoDisc; 799 } 800 } 801 } 802 State::TrayOpening { auto_close } => { 803 self.state = State::TrayOpen { auto_close }; 804 } 805 State::TrayClosing => { 806 self.state = State::MotorStopped; 807 self.report_type = ReportType::AbsoluteTime; 808 } 809 State::TrayOpen { auto_close: true } => { 810 self.state = State::TrayClosing; 811 } 812 _ => {} 813 } 814 815 Ok(()) 816 } 817 818 fn handle_playing( 819 &mut self, 820 time: CdTime, 821 change_state: bool, 822 rchip: &mut Rchip, 823 ) -> SegaCdLoadResult<()> { 824 let Some(disc) = &mut self.disc else { 825 if change_state { 826 self.state = State::NoDisc; 827 } 828 return Ok(()); 829 }; 830 831 let Some(track) = disc.cue().find_track_by_time(time) else { 832 if change_state { 833 self.state = State::DiscEnd(disc.cue().last_track().end_time); 834 } 835 return Ok(()); 836 }; 837 838 let relative_time = time - track.start_time; 839 let track_type = track.track_type; 840 disc.read_sector(track.number, relative_time, self.sector_buffer.as_mut())?; 841 842 self.loaded_audio_sector = track_type == TrackType::Audio; 843 844 rchip.decode_block(&self.sector_buffer); 845 846 if change_state { 847 self.state = State::Playing(time + CdTime::new(0, 0, 1)); 848 } 849 850 Ok(()) 851 } 852 853 pub fn interrupt_pending(&self) -> bool { 854 self.interrupt_pending 855 } 856 857 pub fn acknowledge_interrupt(&mut self) { 858 self.interrupt_pending = false; 859 } 860 861 #[allow(clippy::items_after_statements)] 862 pub fn disc_title(&mut self, region: GenesisRegion) -> SegaCdLoadResult<Option<String>> { 863 let Some(disc) = &mut self.disc else { return Ok(None) }; 864 865 // Title information is always stored in the first sector of track 1 866 disc.read_sector(1, CdTime::SECTOR_0_START, self.sector_buffer.as_mut())?; 867 868 let title_bytes = match region { 869 GenesisRegion::Japan => &self.sector_buffer[0x130..0x160], 870 GenesisRegion::Americas | GenesisRegion::Europe => &self.sector_buffer[0x160..0x190], 871 }; 872 let title: String = title_bytes 873 .iter() 874 .copied() 875 .filter_map(|b| { 876 let c = b as char; 877 (c.is_ascii_alphanumeric() || c.is_ascii_whitespace() || c.is_ascii_punctuation()) 878 .then_some(c) 879 }) 880 .collect(); 881 882 static WHITESPACE_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r" +").unwrap()); 883 884 Ok(Some(WHITESPACE_RE.replace_all(title.trim(), " ").to_string())) 885 } 886 887 pub fn has_six_button_incompatible_game(&mut self) -> SegaCdLoadResult<bool> { 888 let Some(disc) = &mut self.disc else { return Ok(false) }; 889 890 disc.read_sector(1, CdTime::SECTOR_0_START, self.sector_buffer.as_mut())?; 891 892 // Skip sync bytes and sector header 893 let first_data_sector = &self.sector_buffer[16..]; 894 Ok(genesis_components::cartridge::is_six_button_incompatible(first_data_sector)) 895 } 896 897 pub fn take_disc(&mut self) -> Option<CdRom> { 898 self.disc.take() 899 } 900 901 pub fn take_disc_from(&mut self, other: &mut Self) { 902 self.disc = other.disc.take(); 903 } 904 905 pub fn reset(&mut self) { 906 self.state = State::default(); 907 self.report_type = ReportType::default(); 908 self.status = INITIAL_STATUS; 909 self.interrupt_pending = false; 910 } 911 912 pub fn remove_disc(&mut self) { 913 log::info!("Removing disc"); 914 915 self.disc = None; 916 self.state = State::TrayOpening { auto_close: self.model == CdModel::Two }; 917 } 918 919 pub fn change_disc(&mut self, disc: CdRom) { 920 log::info!("Changing disc"); 921 922 self.disc = Some(disc); 923 924 // Only open the tray if running a Model 2 BIOS version. 925 // Model 1 BIOS versions will usually crash if the tray opens without the BIOS first sending 926 // an "open tray" command 927 if self.model == CdModel::Two { 928 self.state = State::TrayOpening { auto_close: true }; 929 } 930 } 931 932 pub fn reload_config(&mut self, config: &SegaCdEmulatorConfig) { 933 self.data_speed = config.disc_drive_speed.get(); 934 } 935} 936 937fn fader_volume_multiplier(volume: u16) -> f64 { 938 // Yes, 1025; fader volumes range from 0 to 1024 inclusive 939 static LOOKUP_TABLE: LazyLock<[f64; 1025]> = LazyLock::new(|| { 940 let mut lookup_table = [0.0; 1025]; 941 942 // The fader appears to use a linear scale, not logarithmic. 943 // 944 // Based on LC7883 documentation: 945 // For every volume >3, multiplier is (Bits 9-2)/256 946 // For volumes 1-3, multiplier is (Bit 1)/512 + (Bit 0)/1024. This matches the dB values in LC7883 docs 947 // For volume 0, multiplier is 0 (i.e. attenuation -inf) 948 for (i, value) in lookup_table.iter_mut().enumerate() { 949 *value = match i { 950 0..=3 => i as f64 / 1024.0, 951 4..=1024 => (i >> 2) as f64 / 256.0, 952 _ => unreachable!("array len is 1025"), 953 }; 954 } 955 956 lookup_table 957 }); 958 959 LOOKUP_TABLE[volume as usize] 960} 961 962// Checksum is the first 9 nibbles summed and then inverted 963fn update_cdd_checksum(cdd_status: &mut [u8; 10]) { 964 let sum = cdd_status[0..9].iter().copied().sum::<u8>(); 965 cdd_status[9] = !sum & 0x0F; 966} 967 968fn read_time_from_command(command: [u8; 10]) -> Option<CdTime> { 969 // Minutes stored in Command 2-3 970 let minutes = 10 * command[2] + command[3]; 971 972 // Seconds stored in Command 4-5 973 let seconds = 10 * command[4] + command[5]; 974 975 // Frames stored in Command 6-7 976 let frames = 10 * command[6] + command[7]; 977 978 CdTime::new_checked(minutes, seconds, frames) 979} 980 981fn write_time_to_status(time: CdTime, status: &mut [u8; 10]) { 982 // Minutes stored in Status 2-3 983 status[2] = time.minutes / 10; 984 status[3] = time.minutes % 10; 985 986 // Seconds stored in Status 4-5 987 status[4] = time.seconds / 10; 988 status[5] = time.seconds % 10; 989 990 // Frames stored in Status 6-7 991 status[6] = time.frames / 10; 992 status[7] = time.frames % 10; 993} 994 995fn estimate_seek_clocks(current_time: CdTime, seek_time: CdTime) -> u8 { 996 let diff = 997 if current_time >= seek_time { current_time - seek_time } else { seek_time - current_time }; 998 999 // It supposedly takes roughly 1.5 seconds / 113 frames to seek from one end of the disc to the 1000 // other, so scale based on that 1001 let seek_cycles = (113.0 * f64::from(diff.to_frames()) 1002 / f64::from(CdTime::DISC_END.to_frames())) 1003 .round() as u8; 1004 1005 // Require seek to always take at least 1 cycle 1006 cmp::max(1, seek_cycles) 1007} 1008 1009fn estimate_intermediate_seek_time( 1010 current_time: CdTime, 1011 seek_time: CdTime, 1012 clocks_remaining: u8, 1013) -> CdTime { 1014 let diff_frames = f64::from(clocks_remaining) / 113.0 * f64::from(CdTime::DISC_END.to_frames()); 1015 let diff = CdTime::from_frames(diff_frames.round() as u32); 1016 1017 if current_time < seek_time { seek_time - diff } else { seek_time + diff } 1018}