1 /***************************************************************************
2 * Copyright (C) 2007 Ryan Schultz, PCSX-df Team, PCSX team *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, write to the *
16 * Free Software Foundation, Inc., *
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *
18 ***************************************************************************/
21 * Handles all CD-ROM registers and functions.
27 #include "arm_features.h"
31 #define CDR_LOG SysPrintf
36 #define CDR_LOG_I SysPrintf
38 #define CDR_LOG_I log_unhandled
41 #define CDR_LOG_IO SysPrintf
43 #define CDR_LOG_IO(...)
45 //#define CDR_LOG_CMD_IRQ
48 // unused members maintain savesate compatibility
49 unsigned char unused0;
50 unsigned char unused1;
52 unsigned char unused2;
58 unsigned char Transfer[DATA_SIZE];
62 unsigned char Relative[3];
63 unsigned char Absolute[3];
65 unsigned char TrackChanged;
66 unsigned char unused3[3];
67 unsigned int freeze_ver;
69 unsigned char Prev[4];
70 unsigned char Param[8];
71 unsigned char Result[16];
75 unsigned char ResultC;
76 unsigned char ResultP;
77 unsigned char ResultReady;
79 unsigned char unused4;
80 unsigned char SetlocPending;
83 unsigned char ResultTN[6];
84 unsigned char ResultTD[4];
85 unsigned char SetSectorPlay[4];
86 unsigned char SetSectorEnd[4];
87 unsigned char SetSector[4];
91 int Mode, File, Channel;
113 u8 AttenuatorLeftToLeft, AttenuatorLeftToRight;
114 u8 AttenuatorRightToRight, AttenuatorRightToLeft;
115 u8 AttenuatorLeftToLeftT, AttenuatorLeftToRightT;
116 u8 AttenuatorRightToRightT, AttenuatorRightToLeftT;
118 static s16 read_buf[CD_FRAMESIZE_RAW/2];
120 /* CD-ROM magic numbers */
121 #define CdlSync 0 /* nocash documentation : "Uh, actually, returns error code 40h = Invalid Command...?" */
126 #define CdlBackward 5
134 #define CdlSetfilter 13
135 #define CdlSetmode 14
136 #define CdlGetparam 15
137 #define CdlGetlocL 16
138 #define CdlGetlocP 17
144 #define CdlSetclock 23
145 #define CdlGetclock 24
151 #define CdlReadToc 30
153 #ifdef CDR_LOG_CMD_IRQ
154 static const char * const CmdName[0x100] = {
155 "CdlSync", "CdlNop", "CdlSetloc", "CdlPlay",
156 "CdlForward", "CdlBackward", "CdlReadN", "CdlStandby",
157 "CdlStop", "CdlPause", "CdlReset", "CdlMute",
158 "CdlDemute", "CdlSetfilter", "CdlSetmode", "CdlGetparam",
159 "CdlGetlocL", "CdlGetlocP", "CdlReadT", "CdlGetTN",
160 "CdlGetTD", "CdlSeekL", "CdlSeekP", "CdlSetclock",
161 "CdlGetclock", "CdlTest", "CdlID", "CdlReadS",
162 "CdlInit", NULL, "CDlReadToc", NULL
166 unsigned char Test04[] = { 0 };
167 unsigned char Test05[] = { 0 };
168 unsigned char Test20[] = { 0x98, 0x06, 0x10, 0xC3 };
169 unsigned char Test22[] = { 0x66, 0x6F, 0x72, 0x20, 0x45, 0x75, 0x72, 0x6F };
170 unsigned char Test23[] = { 0x43, 0x58, 0x44, 0x32, 0x39 ,0x34, 0x30, 0x51 };
176 #define Acknowledge 3
181 #define MODE_SPEED (1<<7) // 0x80
182 #define MODE_STRSND (1<<6) // 0x40 ADPCM on/off
183 #define MODE_SIZE_2340 (1<<5) // 0x20
184 #define MODE_SIZE_2328 (1<<4) // 0x10
185 #define MODE_SIZE_2048 (0<<4) // 0x00
186 #define MODE_SF (1<<3) // 0x08 channel on/off
187 #define MODE_REPORT (1<<2) // 0x04
188 #define MODE_AUTOPAUSE (1<<1) // 0x02
189 #define MODE_CDDA (1<<0) // 0x01
192 #define STATUS_PLAY (1<<7) // 0x80
193 #define STATUS_SEEK (1<<6) // 0x40
194 #define STATUS_READ (1<<5) // 0x20
195 #define STATUS_SHELLOPEN (1<<4) // 0x10
196 #define STATUS_UNKNOWN3 (1<<3) // 0x08
197 #define STATUS_UNKNOWN2 (1<<2) // 0x04
198 #define STATUS_ROTATING (1<<1) // 0x02
199 #define STATUS_ERROR (1<<0) // 0x01
202 #define ERROR_NOTREADY (1<<7) // 0x80
203 #define ERROR_INVALIDCMD (1<<6) // 0x40
204 #define ERROR_INVALIDARG (1<<5) // 0x20
206 // 1x = 75 sectors per second
207 // PSXCLK = 1 sec in the ps
208 // so (PSXCLK / 75) = cdr read time (linuzappz)
209 #define cdReadTime (PSXCLK / 75)
212 DRIVESTATE_STANDBY = 0, // pause, play, read
214 DRIVESTATE_RESCAN_CD,
215 DRIVESTATE_PREPARE_CD,
219 static struct CdrStat stat;
221 static unsigned int msf2sec(const u8 *msf) {
222 return ((msf[0] * 60 + msf[1]) * 75) + msf[2];
225 // for that weird psemu API..
226 static unsigned int fsm2sec(const u8 *msf) {
227 return ((msf[2] * 60 + msf[1]) * 75) + msf[0];
230 static void sec2msf(unsigned int s, u8 *msf) {
231 msf[0] = s / 75 / 60;
232 s = s - msf[0] * 75 * 60;
239 #define CDR_INT(eCycle) { \
240 psxRegs.interrupt |= (1 << PSXINT_CDR); \
241 psxRegs.intCycle[PSXINT_CDR].cycle = eCycle; \
242 psxRegs.intCycle[PSXINT_CDR].sCycle = psxRegs.cycle; \
243 new_dyna_set_event(PSXINT_CDR, eCycle); \
246 // cdrPlaySeekReadInterrupt
247 #define CDRPLAYSEEKREAD_INT(eCycle, isFirst) { \
249 psxRegs.interrupt |= (1 << PSXINT_CDREAD); \
251 psxRegs.intCycle[PSXINT_CDREAD].sCycle = psxRegs.cycle; \
253 psxRegs.intCycle[PSXINT_CDREAD].sCycle += psxRegs.intCycle[PSXINT_CDREAD].cycle; \
254 psxRegs.intCycle[PSXINT_CDREAD].cycle = e_; \
255 new_dyna_set_event_abs(PSXINT_CDREAD, psxRegs.intCycle[PSXINT_CDREAD].sCycle + e_); \
258 // cdrLidSeekInterrupt
259 #define CDRLID_INT(eCycle) { \
260 psxRegs.interrupt |= (1 << PSXINT_CDRLID); \
261 psxRegs.intCycle[PSXINT_CDRLID].cycle = eCycle; \
262 psxRegs.intCycle[PSXINT_CDRLID].sCycle = psxRegs.cycle; \
263 new_dyna_set_event(PSXINT_CDRLID, eCycle); \
266 #define StopReading() { \
268 psxRegs.interrupt &= ~(1 << PSXINT_CDREAD); \
271 #define StopCdda() { \
272 if (cdr.Play && !Config.Cdda) CDR_stop(); \
274 cdr.FastForward = 0; \
275 cdr.FastBackward = 0; \
278 #define SetPlaySeekRead(x, f) { \
279 x &= ~(STATUS_PLAY | STATUS_SEEK | STATUS_READ); \
283 #define SetResultSize(size) { \
285 cdr.ResultC = size; \
286 cdr.ResultReady = 1; \
289 static void setIrq(int log_cmd)
291 if (cdr.Stat & cdr.Reg2)
292 psxHu32ref(0x1070) |= SWAP32((u32)0x4);
294 #ifdef CDR_LOG_CMD_IRQ
298 SysPrintf("%u cdrom: CDR IRQ=%d cmd %02x stat %02x: ",
299 psxRegs.cycle, !!(cdr.Stat & cdr.Reg2), log_cmd, cdr.Stat);
300 for (i = 0; i < cdr.ResultC; i++)
301 SysPrintf("%02x ", cdr.Result[i]);
307 // timing used in this function was taken from tests on real hardware
308 // (yes it's slow, but you probably don't want to modify it)
309 void cdrLidSeekInterrupt(void)
311 CDR_LOG_I("%u %s cdr.DriveState=%d\n", psxRegs.cycle, __func__, cdr.DriveState);
313 switch (cdr.DriveState) {
315 case DRIVESTATE_STANDBY:
318 SetPlaySeekRead(cdr.StatP, 0);
320 if (CDR_getStatus(&stat) == -1)
323 if (stat.Status & STATUS_SHELLOPEN)
325 cdr.DriveState = DRIVESTATE_LID_OPEN;
330 case DRIVESTATE_LID_OPEN:
331 if (CDR_getStatus(&stat) == -1)
332 stat.Status &= ~STATUS_SHELLOPEN;
335 if (!(cdr.StatP & STATUS_SHELLOPEN)) {
336 cdr.StatP |= STATUS_SHELLOPEN;
338 // could generate error irq here, but real hardware
339 // only sometimes does that
340 // (not done when lots of commands are sent?)
342 CDRLID_INT(cdReadTime * 30);
345 else if (cdr.StatP & STATUS_ROTATING) {
346 cdr.StatP &= ~STATUS_ROTATING;
348 else if (!(stat.Status & STATUS_SHELLOPEN)) {
352 // cdr.StatP STATUS_SHELLOPEN is "sticky"
353 // and is only cleared by CdlNop
355 cdr.DriveState = DRIVESTATE_RESCAN_CD;
356 CDRLID_INT(cdReadTime * 105);
361 CDRLID_INT(cdReadTime * 3);
364 case DRIVESTATE_RESCAN_CD:
365 cdr.StatP |= STATUS_ROTATING;
366 cdr.DriveState = DRIVESTATE_PREPARE_CD;
368 // this is very long on real hardware, over 6 seconds
369 // make it a bit faster here...
370 CDRLID_INT(cdReadTime * 150);
373 case DRIVESTATE_PREPARE_CD:
374 if (cdr.StatP & STATUS_SEEK) {
375 SetPlaySeekRead(cdr.StatP, 0);
376 cdr.DriveState = DRIVESTATE_STANDBY;
379 SetPlaySeekRead(cdr.StatP, STATUS_SEEK);
380 CDRLID_INT(cdReadTime * 26);
386 static void Find_CurTrack(const u8 *time)
390 current = msf2sec(time);
392 for (cdr.CurTrack = 1; cdr.CurTrack < cdr.ResultTN[1]; cdr.CurTrack++) {
393 CDR_getTD(cdr.CurTrack + 1, cdr.ResultTD);
394 sect = fsm2sec(cdr.ResultTD);
395 if (sect - current >= 150)
400 static void generate_subq(const u8 *time)
402 unsigned char start[3], next[3];
403 unsigned int this_s, start_s, next_s, pregap;
406 CDR_getTD(cdr.CurTrack, start);
407 if (cdr.CurTrack + 1 <= cdr.ResultTN[1]) {
409 CDR_getTD(cdr.CurTrack + 1, next);
412 // last track - cd size
414 next[0] = cdr.SetSectorEnd[2];
415 next[1] = cdr.SetSectorEnd[1];
416 next[2] = cdr.SetSectorEnd[0];
419 this_s = msf2sec(time);
420 start_s = fsm2sec(start);
421 next_s = fsm2sec(next);
423 cdr.TrackChanged = FALSE;
425 if (next_s - this_s < pregap) {
426 cdr.TrackChanged = TRUE;
433 relative_s = this_s - start_s;
434 if (relative_s < 0) {
436 relative_s = -relative_s;
438 sec2msf(relative_s, cdr.subq.Relative);
440 cdr.subq.Track = itob(cdr.CurTrack);
441 cdr.subq.Relative[0] = itob(cdr.subq.Relative[0]);
442 cdr.subq.Relative[1] = itob(cdr.subq.Relative[1]);
443 cdr.subq.Relative[2] = itob(cdr.subq.Relative[2]);
444 cdr.subq.Absolute[0] = itob(time[0]);
445 cdr.subq.Absolute[1] = itob(time[1]);
446 cdr.subq.Absolute[2] = itob(time[2]);
449 static void ReadTrack(const u8 *time) {
450 unsigned char tmp[3];
454 tmp[0] = itob(time[0]);
455 tmp[1] = itob(time[1]);
456 tmp[2] = itob(time[2]);
458 if (memcmp(cdr.Prev, tmp, 3) == 0)
461 CDR_LOG("ReadTrack *** %02x:%02x:%02x\n", tmp[0], tmp[1], tmp[2]);
463 cdr.NoErr = CDR_readTrack(tmp);
464 memcpy(cdr.Prev, tmp, 3);
469 subq = (struct SubQ *)CDR_getBufferSub();
470 if (subq != NULL && cdr.CurTrack == 1) {
471 crc = calcCrc((u8 *)subq + 12, 10);
472 if (crc == (((u16)subq->CRC[0] << 8) | subq->CRC[1])) {
473 cdr.subq.Track = subq->TrackNumber;
474 cdr.subq.Index = subq->IndexNumber;
475 memcpy(cdr.subq.Relative, subq->TrackRelativeAddress, 3);
476 memcpy(cdr.subq.Absolute, subq->AbsoluteAddress, 3);
479 CDR_LOG_I("subq bad crc @%02x:%02x:%02x\n",
480 tmp[0], tmp[1], tmp[2]);
487 CDR_LOG(" -> %02x,%02x %02x:%02x:%02x %02x:%02x:%02x\n",
488 cdr.subq.Track, cdr.subq.Index,
489 cdr.subq.Relative[0], cdr.subq.Relative[1], cdr.subq.Relative[2],
490 cdr.subq.Absolute[0], cdr.subq.Absolute[1], cdr.subq.Absolute[2]);
493 static void cdrPlayInterrupt_Autopause()
496 boolean abs_lev_chselect;
499 if ((cdr.Mode & MODE_AUTOPAUSE) && cdr.TrackChanged) {
500 CDR_LOG( "CDDA STOP\n" );
502 // Magic the Gathering
503 // - looping territory cdda
506 //cdr.ResultReady = 1;
507 //cdr.Stat = DataReady;
509 setIrq(0x1000); // 0x1000 just for logging purposes
512 SetPlaySeekRead(cdr.StatP, 0);
514 else if (((cdr.Mode & MODE_REPORT) || cdr.FastForward || cdr.FastBackward)) {
515 cdr.Result[0] = cdr.StatP;
516 cdr.Result[1] = cdr.subq.Track;
517 cdr.Result[2] = cdr.subq.Index;
519 abs_lev_chselect = cdr.subq.Absolute[1] & 0x01;
521 /* 8 is a hack. For accuracy, it should be 588. */
522 for (i = 0; i < 8; i++)
524 abs_lev_max = MAX_VALUE(abs_lev_max, abs(read_buf[i * 2 + abs_lev_chselect]));
526 abs_lev_max = MIN_VALUE(abs_lev_max, 32767);
527 abs_lev_max |= abs_lev_chselect << 15;
529 if (cdr.subq.Absolute[2] & 0x10) {
530 cdr.Result[3] = cdr.subq.Relative[0];
531 cdr.Result[4] = cdr.subq.Relative[1] | 0x80;
532 cdr.Result[5] = cdr.subq.Relative[2];
535 cdr.Result[3] = cdr.subq.Absolute[0];
536 cdr.Result[4] = cdr.subq.Absolute[1];
537 cdr.Result[5] = cdr.subq.Absolute[2];
540 cdr.Result[6] = abs_lev_max >> 0;
541 cdr.Result[7] = abs_lev_max >> 8;
543 // Rayman: Logo freeze (resultready + dataready)
545 cdr.Stat = DataReady;
552 static int cdrSeekTime(unsigned char *target)
554 int diff = msf2sec(cdr.SetSectorPlay) - msf2sec(target);
555 int seekTime = abs(diff) * (cdReadTime / 200);
558 * It was originally set to 1000000 for Driver, however it is not high enough for Worms Pinball
559 * and was unreliable for that game.
560 * I also tested it against Mednafen and Driver's titlescreen music starts 25 frames later, not immediatly.
562 * Obviously, this isn't perfect but right now, it should be a bit better.
563 * Games to test this against if you change that setting :
564 * - Driver (titlescreen music delay and retry mission)
565 * - Worms Pinball (Will either not boot or crash in the memory card screen)
566 * - Viewpoint (short pauses if the delay in the ingame music is too long)
568 * It seems that 3386880 * 5 is too much for Driver's titlescreen and it starts skipping.
569 * However, 1000000 is not enough for Worms Pinball to reliably boot.
571 if(seekTime > 3386880 * 2) seekTime = 3386880 * 2;
572 CDR_LOG("seek: %.2f %.2f\n", (float)seekTime / PSXCLK, (float)seekTime / cdReadTime);
576 static void cdrUpdateTransferBuf(const u8 *buf);
577 static void cdrReadInterrupt(void);
578 static void cdrPrepCdda(s16 *buf, int samples);
579 static void cdrAttenuate(s16 *buf, int samples, int stereo);
581 void cdrPlaySeekReadInterrupt(void)
588 if (!cdr.Play && (cdr.StatP & STATUS_SEEK)) {
590 CDR_LOG_I("cdrom: seek stat hack\n");
591 CDRPLAYSEEKREAD_INT(0x1000, 1);
595 cdr.StatP |= STATUS_ROTATING;
596 SetPlaySeekRead(cdr.StatP, 0);
597 cdr.Result[0] = cdr.StatP;
601 Find_CurTrack(cdr.SetSectorPlay);
602 ReadTrack(cdr.SetSectorPlay);
603 cdr.TrackChanged = FALSE;
607 if (!cdr.Play) return;
609 CDR_LOG( "CDDA - %d:%d:%d\n",
610 cdr.SetSectorPlay[0], cdr.SetSectorPlay[1], cdr.SetSectorPlay[2] );
612 SetPlaySeekRead(cdr.StatP, STATUS_PLAY);
613 if (memcmp(cdr.SetSectorPlay, cdr.SetSectorEnd, 3) == 0) {
615 SetPlaySeekRead(cdr.StatP, 0);
616 cdr.TrackChanged = TRUE;
619 CDR_readCDDA(cdr.SetSectorPlay[0], cdr.SetSectorPlay[1], cdr.SetSectorPlay[2], (u8 *)read_buf);
622 if (!cdr.Stat && (cdr.Mode & (MODE_AUTOPAUSE|MODE_REPORT)))
623 cdrPlayInterrupt_Autopause();
625 if (!cdr.Muted && !Config.Cdda) {
626 cdrPrepCdda(read_buf, CD_FRAMESIZE_RAW / 4);
627 cdrAttenuate(read_buf, CD_FRAMESIZE_RAW / 4, 1);
628 SPU_playCDDAchannel(read_buf, CD_FRAMESIZE_RAW, psxRegs.cycle, cdr.FirstSector);
632 cdr.SetSectorPlay[2]++;
633 if (cdr.SetSectorPlay[2] == 75) {
634 cdr.SetSectorPlay[2] = 0;
635 cdr.SetSectorPlay[1]++;
636 if (cdr.SetSectorPlay[1] == 60) {
637 cdr.SetSectorPlay[1] = 0;
638 cdr.SetSectorPlay[0]++;
642 // update for CdlGetlocP/autopause
643 generate_subq(cdr.SetSectorPlay);
645 CDRPLAYSEEKREAD_INT(cdReadTime, 0);
648 #define CMD_PART2 0x100
649 #define CMD_WHILE_NOT_READY 0x200
651 void cdrInterrupt(void) {
652 int start_rotating = 0;
654 unsigned int seekTime = 0;
655 u32 second_resp_time = 0;
663 CDR_LOG_I("%u cdrom: cmd %02x with irqstat %x\n",
664 psxRegs.cycle, cdr.CmdInProgress, cdr.Stat);
667 if (cdr.Irq1Pending) {
668 // hand out the "newest" sector, according to nocash
669 cdrUpdateTransferBuf(CDR_getBuffer());
670 CDR_LOG_I("cdrom: %x:%02x:%02x loaded on ack\n",
671 cdr.Transfer[0], cdr.Transfer[1], cdr.Transfer[2]);
673 cdr.Result[0] = cdr.Irq1Pending;
674 cdr.Stat = (cdr.Irq1Pending & STATUS_ERROR) ? DiskError : DataReady;
682 cdr.Result[0] = cdr.StatP;
683 cdr.Stat = Acknowledge;
685 Cmd = cdr.CmdInProgress;
686 cdr.CmdInProgress = 0;
695 switch (cdr.DriveState) {
696 case DRIVESTATE_PREPARE_CD:
698 // Syphon filter 2 expects commands to work shortly after it sees
699 // STATUS_ROTATING, so give up trying to emulate the startup seq
700 cdr.DriveState = DRIVESTATE_STANDBY;
701 cdr.StatP &= ~STATUS_SEEK;
702 psxRegs.interrupt &= ~(1 << PSXINT_CDRLID);
706 case DRIVESTATE_LID_OPEN:
707 case DRIVESTATE_RESCAN_CD:
708 // no disk or busy with the initial scan, allowed cmds are limited
709 not_ready = CMD_WHILE_NOT_READY;
713 switch (Cmd | not_ready) {
715 case CdlNop + CMD_WHILE_NOT_READY:
716 if (cdr.DriveState != DRIVESTATE_LID_OPEN)
717 cdr.StatP &= ~STATUS_SHELLOPEN;
721 case CdlSetloc + CMD_WHILE_NOT_READY:
722 CDR_LOG("CDROM setloc command (%02X, %02X, %02X)\n", cdr.Param[0], cdr.Param[1], cdr.Param[2]);
724 // MM must be BCD, SS must be BCD and <0x60, FF must be BCD and <0x75
725 if (((cdr.Param[0] & 0x0F) > 0x09) || (cdr.Param[0] > 0x99) || ((cdr.Param[1] & 0x0F) > 0x09) || (cdr.Param[1] >= 0x60) || ((cdr.Param[2] & 0x0F) > 0x09) || (cdr.Param[2] >= 0x75))
727 CDR_LOG("Invalid/out of range seek to %02X:%02X:%02X\n", cdr.Param[0], cdr.Param[1], cdr.Param[2]);
728 error = ERROR_INVALIDARG;
733 for (i = 0; i < 3; i++)
734 set_loc[i] = btoi(cdr.Param[i]);
735 memcpy(cdr.SetSector, set_loc, 3);
736 cdr.SetSector[3] = 0;
737 cdr.SetlocPending = 1;
746 cdr.FastBackward = 0;
750 // - Pause player, hit Track 01/02/../xx (Setloc issued!!)
752 if (ParamC != 0 && cdr.Param[0] != 0) {
753 int track = btoi( cdr.Param[0] );
755 if (track <= cdr.ResultTN[1])
756 cdr.CurTrack = track;
758 CDR_LOG("PLAY track %d\n", cdr.CurTrack);
760 if (CDR_getTD((u8)cdr.CurTrack, cdr.ResultTD) != -1) {
761 for (i = 0; i < 3; i++)
762 set_loc[i] = cdr.ResultTD[2 - i];
763 seekTime = cdrSeekTime(set_loc);
764 memcpy(cdr.SetSectorPlay, set_loc, 3);
767 else if (cdr.SetlocPending) {
768 seekTime = cdrSeekTime(cdr.SetSector);
769 memcpy(cdr.SetSectorPlay, cdr.SetSector, 4);
772 CDR_LOG("PLAY Resume @ %d:%d:%d\n",
773 cdr.SetSectorPlay[0], cdr.SetSectorPlay[1], cdr.SetSectorPlay[2]);
775 cdr.SetlocPending = 0;
778 Rayman: detect track changes
781 Twisted Metal 2: skip PREGAP + starting accurate SubQ
782 - plays tracks without retry play
784 Wild 9: skip PREGAP + starting accurate SubQ
785 - plays tracks without retry play
787 Find_CurTrack(cdr.SetSectorPlay);
788 ReadTrack(cdr.SetSectorPlay);
789 cdr.TrackChanged = FALSE;
793 CDR_play(cdr.SetSectorPlay);
795 SetPlaySeekRead(cdr.StatP, STATUS_SEEK | STATUS_ROTATING);
797 // BIOS player - set flag again
800 CDRPLAYSEEKREAD_INT(cdReadTime + seekTime, 1);
805 // TODO: error 80 if stopped
808 // GameShark CD Player: Calls 2x + Play 2x
810 cdr.FastBackward = 0;
816 // GameShark CD Player: Calls 2x + Play 2x
817 cdr.FastBackward = 1;
822 if (cdr.DriveState != DRIVESTATE_STOPPED) {
823 error = ERROR_INVALIDARG;
826 second_resp_time = cdReadTime * 125 / 2;
830 case CdlStandby + CMD_PART2:
836 // grab time for current track
837 CDR_getTD((u8)(cdr.CurTrack), cdr.ResultTD);
839 cdr.SetSectorPlay[0] = cdr.ResultTD[2];
840 cdr.SetSectorPlay[1] = cdr.ResultTD[1];
841 cdr.SetSectorPlay[2] = cdr.ResultTD[0];
846 SetPlaySeekRead(cdr.StatP, 0);
847 cdr.StatP &= ~STATUS_ROTATING;
849 second_resp_time = 0x800;
850 if (cdr.DriveState == DRIVESTATE_STANDBY)
851 second_resp_time = cdReadTime * 30 / 2;
853 cdr.DriveState = DRIVESTATE_STOPPED;
856 case CdlStop + CMD_PART2:
864 Gundam Battle Assault 2: much slower (*)
865 - Fixes boot, gameplay
867 Hokuto no Ken 2: slower
868 - Fixes intro + subtitles
870 InuYasha - Feudal Fairy Tale: slower
873 /* Gameblabla - Tightening the timings (as taken from Duckstation).
874 * The timings from Duckstation are based upon hardware tests.
875 * Mednafen's timing don't work for Gundam Battle Assault 2 in PAL/50hz mode,
876 * seems to be timing sensitive as it can depend on the CPU's clock speed.
878 if (!(cdr.StatP & (STATUS_PLAY | STATUS_READ)))
880 second_resp_time = 7000;
884 second_resp_time = (((cdr.Mode & MODE_SPEED) ? 2 : 1) * 1000000);
886 SetPlaySeekRead(cdr.StatP, 0);
889 case CdlPause + CMD_PART2:
894 case CdlReset + CMD_WHILE_NOT_READY:
897 SetPlaySeekRead(cdr.StatP, 0);
899 cdr.Mode = 0x20; /* This fixes This is Football 2, Pooh's Party lockups */
900 second_resp_time = not_ready ? 70000 : 4100000;
904 case CdlReset + CMD_PART2:
905 case CdlReset + CMD_PART2 + CMD_WHILE_NOT_READY:
918 cdr.File = cdr.Param[0];
919 cdr.Channel = cdr.Param[1];
923 case CdlSetmode + CMD_WHILE_NOT_READY:
924 CDR_LOG("cdrWrite1() Log: Setmode %x\n", cdr.Param[0]);
925 cdr.Mode = cdr.Param[0];
929 case CdlGetparam + CMD_WHILE_NOT_READY:
930 /* Gameblabla : According to mednafen, Result size should be 5 and done this way. */
932 cdr.Result[1] = cdr.Mode;
934 cdr.Result[3] = cdr.File;
935 cdr.Result[4] = cdr.Channel;
940 memcpy(cdr.Result, cdr.Transfer, 8);
945 memcpy(&cdr.Result, &cdr.subq, 8);
948 case CdlReadT: // SetSession?
950 second_resp_time = cdReadTime * 290 / 4;
954 case CdlReadT + CMD_PART2:
960 if (CDR_getTN(cdr.ResultTN) == -1) {
961 cdr.Stat = DiskError;
962 cdr.Result[0] |= STATUS_ERROR;
964 cdr.Stat = Acknowledge;
965 cdr.Result[1] = itob(cdr.ResultTN[0]);
966 cdr.Result[2] = itob(cdr.ResultTN[1]);
971 cdr.Track = btoi(cdr.Param[0]);
973 if (CDR_getTD(cdr.Track, cdr.ResultTD) == -1) {
974 cdr.Stat = DiskError;
975 cdr.Result[0] |= STATUS_ERROR;
977 cdr.Stat = Acknowledge;
978 cdr.Result[0] = cdr.StatP;
979 cdr.Result[1] = itob(cdr.ResultTD[2]);
980 cdr.Result[2] = itob(cdr.ResultTD[1]);
981 /* According to Nocash's documentation, the function doesn't care about ff.
982 * This can be seen also in Mednafen's implementation. */
983 //cdr.Result[3] = itob(cdr.ResultTD[0]);
991 SetPlaySeekRead(cdr.StatP, STATUS_SEEK);
993 seekTime = cdrSeekTime(cdr.SetSector);
994 memcpy(cdr.SetSectorPlay, cdr.SetSector, 4);
996 Crusaders of Might and Magic = 0.5x-4x
997 - fix cutscene speech start
1003 - fix cutscene speech
1008 CDRPLAYSEEKREAD_INT(cdReadTime + seekTime, 1);
1013 case CdlTest + CMD_WHILE_NOT_READY:
1014 switch (cdr.Param[0]) {
1015 case 0x20: // System Controller ROM Version
1017 memcpy(cdr.Result, Test20, 4);
1021 memcpy(cdr.Result, Test22, 4);
1023 case 0x23: case 0x24:
1025 memcpy(cdr.Result, Test23, 4);
1031 second_resp_time = 20480;
1034 case CdlID + CMD_PART2:
1036 cdr.Result[0] = cdr.StatP;
1041 // 0x10 - audio | 0x40 - disk missing | 0x80 - unlicensed
1042 if (CDR_getStatus(&stat) == -1 || stat.Type == 0 || stat.Type == 0xff) {
1043 cdr.Result[1] = 0xc0;
1047 cdr.Result[1] |= 0x10;
1048 if (CdromId[0] == '\0')
1049 cdr.Result[1] |= 0x80;
1051 cdr.Result[0] |= (cdr.Result[1] >> 4) & 0x08;
1053 /* This adds the string "PCSX" in Playstation bios boot screen */
1054 memcpy((char *)&cdr.Result[4], "PCSX", 4);
1055 cdr.Stat = Complete;
1059 case CdlInit + CMD_WHILE_NOT_READY:
1062 SetPlaySeekRead(cdr.StatP, 0);
1063 // yes, it really sets STATUS_SHELLOPEN
1064 cdr.StatP |= STATUS_SHELLOPEN;
1065 cdr.DriveState = DRIVESTATE_RESCAN_CD;
1071 case CdlGetQ + CMD_WHILE_NOT_READY:
1075 case CdlReadToc + CMD_WHILE_NOT_READY:
1076 second_resp_time = cdReadTime * 180 / 4;
1080 case CdlReadToc + CMD_PART2:
1081 case CdlReadToc + CMD_PART2 + CMD_WHILE_NOT_READY:
1082 cdr.Stat = Complete;
1087 Find_CurTrack(cdr.SetlocPending ? cdr.SetSector : cdr.SetSectorPlay);
1089 if ((cdr.Mode & MODE_CDDA) && cdr.CurTrack > 1)
1090 // Read* acts as play for cdda tracks in cdda mode
1094 if (cdr.SetlocPending) {
1095 seekTime = cdrSeekTime(cdr.SetSector);
1096 memcpy(cdr.SetSectorPlay, cdr.SetSector, 4);
1097 cdr.SetlocPending = 0;
1100 cdr.FirstSector = 1;
1102 // Fighting Force 2 - update subq time immediately
1104 ReadTrack(cdr.SetSectorPlay);
1106 CDRPLAYSEEKREAD_INT(((cdr.Mode & 0x80) ? (cdReadTime) : cdReadTime * 2) + seekTime, 1);
1108 SetPlaySeekRead(cdr.StatP, STATUS_SEEK);
1113 CDR_LOG_I("Invalid command: %02x%s\n",
1114 Cmd, not_ready ? " (not_ready)" : "");
1115 error = ERROR_INVALIDCMD;
1120 cdr.Result[0] = cdr.StatP | STATUS_ERROR;
1121 cdr.Result[1] = not_ready ? ERROR_NOTREADY : error;
1122 cdr.Stat = DiskError;
1126 if (cdr.DriveState == DRIVESTATE_STOPPED && start_rotating) {
1127 printf("cdr.DriveState %d->%d\n", cdr.DriveState, DRIVESTATE_STANDBY);
1128 cdr.DriveState = DRIVESTATE_STANDBY;
1129 cdr.StatP |= STATUS_ROTATING;
1132 if (second_resp_time) {
1133 cdr.CmdInProgress = Cmd | 0x100;
1134 CDR_INT(second_resp_time);
1136 else if (cdr.Cmd && cdr.Cmd != (Cmd & 0xff)) {
1137 cdr.CmdInProgress = cdr.Cmd;
1138 CDR_LOG_I("%u cdrom: cmd %02x came before %02x finished\n",
1139 psxRegs.cycle, cdr.Cmd, Cmd);
1146 #define ssat32_to_16(v) \
1147 asm("ssat %0,#16,%1" : "=r" (v) : "r" (v))
1149 #define ssat32_to_16(v) do { \
1150 if (v < -32768) v = -32768; \
1151 else if (v > 32767) v = 32767; \
1155 static void cdrPrepCdda(s16 *buf, int samples)
1157 #if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
1159 for (i = 0; i < samples; i++) {
1160 buf[i * 2 + 0] = SWAP16(buf[i * 2 + 0]);
1161 buf[i * 2 + 1] = SWAP16(buf[i * 2 + 1]);
1166 static void cdrAttenuate(s16 *buf, int samples, int stereo)
1169 int ll = cdr.AttenuatorLeftToLeft;
1170 int lr = cdr.AttenuatorLeftToRight;
1171 int rl = cdr.AttenuatorRightToLeft;
1172 int rr = cdr.AttenuatorRightToRight;
1174 if (lr == 0 && rl == 0 && 0x78 <= ll && ll <= 0x88 && 0x78 <= rr && rr <= 0x88)
1177 if (!stereo && ll == 0x40 && lr == 0x40 && rl == 0x40 && rr == 0x40)
1181 for (i = 0; i < samples; i++) {
1184 l = (l * ll + r * rl) >> 7;
1185 r = (r * rr + l * lr) >> 7;
1193 for (i = 0; i < samples; i++) {
1195 l = l * (ll + rl) >> 7;
1196 //r = r * (rr + lr) >> 7;
1204 static void cdrReadInterruptSetResult(unsigned char result)
1207 CDR_LOG_I("cdrom: %d:%02d:%02d irq miss, cmd=%02x irqstat=%02x\n",
1208 cdr.SetSectorPlay[0], cdr.SetSectorPlay[1], cdr.SetSectorPlay[2],
1209 cdr.CmdInProgress, cdr.Stat);
1210 cdr.Irq1Pending = result;
1214 cdr.Result[0] = result;
1215 cdr.Stat = (result & STATUS_ERROR) ? DiskError : DataReady;
1219 static void cdrUpdateTransferBuf(const u8 *buf)
1223 memcpy(cdr.Transfer, buf, DATA_SIZE);
1224 CheckPPFCache(cdr.Transfer, cdr.Prev[0], cdr.Prev[1], cdr.Prev[2]);
1225 CDR_LOG("cdr.Transfer %x:%x:%x\n", cdr.Transfer[0], cdr.Transfer[1], cdr.Transfer[2]);
1226 if (cdr.FifoOffset < 2048 + 12)
1227 CDR_LOG("cdrom: FifoOffset(1) %d/%d\n", cdr.FifoOffset, cdr.FifoSize);
1230 static void cdrReadInterrupt(void)
1232 u8 *buf = NULL, *hdr;
1234 SetPlaySeekRead(cdr.StatP, STATUS_READ | STATUS_ROTATING);
1236 ReadTrack(cdr.SetSectorPlay);
1238 buf = CDR_getBuffer();
1243 CDR_LOG_I("cdrReadInterrupt() Log: err\n");
1244 memset(cdr.Transfer, 0, DATA_SIZE);
1245 cdrReadInterruptSetResult(cdr.StatP | STATUS_ERROR);
1249 if (!cdr.Irq1Pending)
1250 cdrUpdateTransferBuf(buf);
1252 if ((!cdr.Muted) && (cdr.Mode & MODE_STRSND) && (!Config.Xa) && (cdr.FirstSector != -1)) { // CD-XA
1254 // Firemen 2: Multi-XA files - briefings, cutscenes
1255 if( cdr.FirstSector == 1 && (cdr.Mode & MODE_SF)==0 ) {
1257 cdr.Channel = hdr[1];
1261 * Skips playing on channel 255.
1262 * Fixes missing audio in Blue's Clues : Blue's Big Musical. (Should also fix Taxi 2)
1263 * TODO : Check if this is the proper behaviour.
1265 if ((hdr[2] & 0x4) && hdr[0] == cdr.File && hdr[1] == cdr.Channel && cdr.Channel != 255) {
1266 int ret = xa_decode_sector(&cdr.Xa, buf + 4, cdr.FirstSector);
1268 cdrAttenuate(cdr.Xa.pcm, cdr.Xa.nsamples, cdr.Xa.stereo);
1269 SPU_playADPCMchannel(&cdr.Xa, psxRegs.cycle, cdr.FirstSector);
1270 cdr.FirstSector = 0;
1272 else cdr.FirstSector = -1;
1277 Croc 2: $40 - only FORM1 (*)
1278 Judge Dredd: $C8 - only FORM1 (*)
1279 Sim Theme Park - no adpcm at all (zero)
1282 if (!(cdr.Mode & MODE_STRSND) || !(buf[4+2] & 0x4))
1283 cdrReadInterruptSetResult(cdr.StatP);
1285 cdr.SetSectorPlay[2]++;
1286 if (cdr.SetSectorPlay[2] == 75) {
1287 cdr.SetSectorPlay[2] = 0;
1288 cdr.SetSectorPlay[1]++;
1289 if (cdr.SetSectorPlay[1] == 60) {
1290 cdr.SetSectorPlay[1] = 0;
1291 cdr.SetSectorPlay[0]++;
1295 if (!cdr.Irq1Pending) {
1296 // update for CdlGetlocP
1297 ReadTrack(cdr.SetSectorPlay);
1300 CDRPLAYSEEKREAD_INT((cdr.Mode & MODE_SPEED) ? (cdReadTime / 2) : cdReadTime, 0);
1309 bit 5 - 1 result ready
1311 bit 7 - 1 command being processed
1314 unsigned char cdrRead0(void) {
1315 if (cdr.ResultReady)
1320 cdr.Ctrl |= 0x40; // data fifo not empty
1322 // What means the 0x10 and the 0x08 bits? I only saw it used by the bios
1325 CDR_LOG_IO("cdr r0.sta: %02x\n", cdr.Ctrl);
1327 return psxHu8(0x1800) = cdr.Ctrl;
1330 void cdrWrite0(unsigned char rt) {
1331 CDR_LOG_IO("cdr w0.idx: %02x\n", rt);
1333 cdr.Ctrl = (rt & 3) | (cdr.Ctrl & ~3);
1336 unsigned char cdrRead1(void) {
1337 if ((cdr.ResultP & 0xf) < cdr.ResultC)
1338 psxHu8(0x1801) = cdr.Result[cdr.ResultP & 0xf];
1342 if (cdr.ResultP == cdr.ResultC)
1343 cdr.ResultReady = 0;
1345 CDR_LOG_IO("cdr r1.rsp: %02x #%u\n", psxHu8(0x1801), cdr.ResultP - 1);
1347 return psxHu8(0x1801);
1350 void cdrWrite1(unsigned char rt) {
1351 const char *rnames[] = { "cmd", "smd", "smc", "arr" }; (void)rnames;
1352 CDR_LOG_IO("cdr w1.%s: %02x\n", rnames[cdr.Ctrl & 3], rt);
1354 switch (cdr.Ctrl & 3) {
1358 cdr.AttenuatorRightToRightT = rt;
1364 #ifdef CDR_LOG_CMD_IRQ
1365 SysPrintf("%u cdrom: CD1 write: %x (%s)", psxRegs.cycle, rt, CmdName[rt]);
1368 SysPrintf(" Param[%d] = {", cdr.ParamC);
1369 for (i = 0; i < cdr.ParamC; i++)
1370 SysPrintf(" %x,", cdr.Param[i]);
1377 cdr.ResultReady = 0;
1380 if (!cdr.CmdInProgress) {
1381 cdr.CmdInProgress = rt;
1382 // should be something like 12k + controller delays
1386 CDR_LOG_I("%u cdrom: cmd while busy: %02x, prev %02x, busy %02x\n",
1387 psxRegs.cycle, rt, cdr.Cmd, cdr.CmdInProgress);
1388 if (cdr.CmdInProgress < 0x100) // no pending 2nd response
1389 cdr.CmdInProgress = rt;
1395 unsigned char cdrRead2(void) {
1396 unsigned char ret = 0;
1398 if (cdr.FifoOffset < cdr.FifoSize)
1399 ret = cdr.Transfer[cdr.FifoOffset++];
1401 CDR_LOG_I("cdrom: read empty fifo (%d)\n", cdr.FifoSize);
1403 CDR_LOG_IO("cdr r2.dat: %02x\n", ret);
1407 void cdrWrite2(unsigned char rt) {
1408 const char *rnames[] = { "prm", "ien", "all", "arl" }; (void)rnames;
1409 CDR_LOG_IO("cdr w2.%s: %02x\n", rnames[cdr.Ctrl & 3], rt);
1411 switch (cdr.Ctrl & 3) {
1413 if (cdr.ParamC < 8) // FIXME: size and wrapping
1414 cdr.Param[cdr.ParamC++] = rt;
1421 cdr.AttenuatorLeftToLeftT = rt;
1424 cdr.AttenuatorRightToLeftT = rt;
1429 unsigned char cdrRead3(void) {
1431 psxHu8(0x1803) = cdr.Stat | 0xE0;
1433 psxHu8(0x1803) = cdr.Reg2 | 0xE0;
1435 CDR_LOG_IO("cdr r3.%s: %02x\n", (cdr.Ctrl & 1) ? "ifl" : "ien", psxHu8(0x1803));
1436 return psxHu8(0x1803);
1439 void cdrWrite3(unsigned char rt) {
1440 const char *rnames[] = { "req", "ifl", "alr", "ava" }; (void)rnames;
1441 CDR_LOG_IO("cdr w3.%s: %02x\n", rnames[cdr.Ctrl & 3], rt);
1443 switch (cdr.Ctrl & 3) {
1447 if (cdr.Stat & rt) {
1448 #ifdef CDR_LOG_CMD_IRQ
1449 SysPrintf("%u cdrom: ack %02x (w %02x)\n",
1450 psxRegs.cycle, cdr.Stat & rt, rt);
1452 // note: Croc vs Discworld Noir
1453 if (!(psxRegs.interrupt & (1 << PSXINT_CDR)) &&
1454 (cdr.CmdInProgress || cdr.Irq1Pending))
1455 CDR_INT(850); // 711-993
1463 cdr.AttenuatorLeftToRightT = rt;
1467 memcpy(&cdr.AttenuatorLeftToLeft, &cdr.AttenuatorLeftToLeftT, 4);
1468 CDR_LOG("CD-XA Volume: %02x %02x | %02x %02x\n",
1469 cdr.AttenuatorLeftToLeft, cdr.AttenuatorLeftToRight,
1470 cdr.AttenuatorRightToLeft, cdr.AttenuatorRightToRight);
1476 if ((rt & 0x80) && cdr.FifoOffset < cdr.FifoSize) {
1477 CDR_LOG("cdrom: FifoOffset(2) %d/%d\n", cdr.FifoOffset, cdr.FifoSize);
1479 else if (rt & 0x80) {
1480 switch (cdr.Mode & 0x30) {
1481 case MODE_SIZE_2328:
1483 cdr.FifoOffset = 12;
1484 cdr.FifoSize = 2048 + 12;
1487 case MODE_SIZE_2340:
1490 cdr.FifoSize = 2340;
1494 else if (!(rt & 0xc0))
1495 cdr.FifoOffset = DATA_SIZE; // fifo empty
1498 void psxDma3(u32 madr, u32 bcr, u32 chcr) {
1503 CDR_LOG("psxDma3() Log: *** DMA 3 *** %x addr = %x size = %x\n", chcr, madr, bcr);
1505 switch (chcr & 0x71000000) {
1507 ptr = (u8 *)PSXM(madr);
1508 if (ptr == INVALID_PTR) {
1509 CDR_LOG_I("psxDma3() Log: *** DMA 3 *** NULL Pointer!\n");
1513 cdsize = (((bcr - 1) & 0xffff) + 1) * 4;
1516 GS CDX: Enhancement CD crash
1519 - Spams DMA3 and gets buffer overrun
1521 size = DATA_SIZE - cdr.FifoOffset;
1526 memcpy(ptr, cdr.Transfer + cdr.FifoOffset, size);
1527 cdr.FifoOffset += size;
1528 psxCpu->Clear(madr, size / 4);
1531 CDR_LOG_I("cdrom: dma3 %d/%d\n", size, cdsize);
1533 CDRDMA_INT((cdsize/4) * 24);
1535 HW_DMA3_CHCR &= SWAPu32(~0x10000000);
1537 HW_DMA3_MADR = SWAPu32(madr + cdsize);
1538 HW_DMA3_BCR &= SWAPu32(0xffff0000);
1542 psxRegs.cycle += (cdsize/4) * 24 - 20;
1547 CDR_LOG_I("psxDma3() Log: Unknown cddma %x\n", chcr);
1551 HW_DMA3_CHCR &= SWAP32(~0x01000000);
1555 void cdrDmaInterrupt(void)
1557 if (HW_DMA3_CHCR & SWAP32(0x01000000))
1559 HW_DMA3_CHCR &= SWAP32(~0x01000000);
1564 static void getCdInfo(void)
1568 CDR_getTN(cdr.ResultTN);
1569 CDR_getTD(0, cdr.SetSectorEnd);
1570 tmp = cdr.SetSectorEnd[0];
1571 cdr.SetSectorEnd[0] = cdr.SetSectorEnd[2];
1572 cdr.SetSectorEnd[2] = tmp;
1576 memset(&cdr, 0, sizeof(cdr));
1582 cdr.FifoOffset = DATA_SIZE; // fifo empty
1583 if (CdromId[0] == '\0') {
1584 cdr.DriveState = DRIVESTATE_STOPPED;
1588 cdr.DriveState = DRIVESTATE_STANDBY;
1589 cdr.StatP = STATUS_ROTATING;
1592 // BIOS player - default values
1593 cdr.AttenuatorLeftToLeft = 0x80;
1594 cdr.AttenuatorLeftToRight = 0x00;
1595 cdr.AttenuatorRightToLeft = 0x00;
1596 cdr.AttenuatorRightToRight = 0x80;
1601 int cdrFreeze(void *f, int Mode) {
1605 if (Mode == 0 && !Config.Cdda)
1608 cdr.freeze_ver = 0x63647202;
1609 gzfreeze(&cdr, sizeof(cdr));
1612 cdr.ParamP = cdr.ParamC;
1613 tmp = cdr.FifoOffset;
1616 gzfreeze(&tmp, sizeof(tmp));
1621 cdr.FifoOffset = tmp;
1622 cdr.FifoSize = (cdr.Mode & 0x20) ? 2340 : 2048 + 12;
1624 // read right sub data
1625 tmpp[0] = btoi(cdr.Prev[0]);
1626 tmpp[1] = btoi(cdr.Prev[1]);
1627 tmpp[2] = btoi(cdr.Prev[2]);
1632 if (cdr.freeze_ver < 0x63647202)
1633 memcpy(cdr.SetSectorPlay, cdr.SetSector, 3);
1635 Find_CurTrack(cdr.SetSectorPlay);
1637 CDR_play(cdr.SetSectorPlay);
1638 if (psxRegs.interrupt & (1 << PSXINT_CDRPLAY_OLD))
1639 CDRPLAYSEEKREAD_INT((cdr.Mode & 0x80) ? (cdReadTime / 2) : cdReadTime, 1);
1642 if ((cdr.freeze_ver & 0xffffff00) != 0x63647200) {
1643 // old versions did not latch Reg2, have to fixup..
1644 if (cdr.Reg2 == 0) {
1645 SysPrintf("cdrom: fixing up old savestate\n");
1648 // also did not save Attenuator..
1649 if ((cdr.AttenuatorLeftToLeft | cdr.AttenuatorLeftToRight
1650 | cdr.AttenuatorRightToLeft | cdr.AttenuatorRightToRight) == 0)
1652 cdr.AttenuatorLeftToLeft = cdr.AttenuatorRightToRight = 0x80;
1660 void LidInterrupt(void) {
1662 cdrLidSeekInterrupt();