/*************************************************************************** * Copyright (C) 2007 PCSX-df Team * * Copyright (C) 2009 Wei Mingzhi * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Steet, Fifth Floor, Boston, MA 02111-1307 USA. * ***************************************************************************/ #include "psxcommon.h" #include "plugins.h" #include "cdrom.h" #include "cdriso.h" #ifdef _WIN32 #include #include #else #include #include #endif FILE *cdHandle = NULL; FILE *cddaHandle = NULL; FILE *subHandle = NULL; static char subChanInterleaved = 0; static unsigned char cdbuffer[DATA_SIZE]; static unsigned char subbuffer[SUB_FRAMESIZE]; static unsigned char sndbuffer[CD_FRAMESIZE_RAW * 10]; #define CDDA_FRAMETIME (1000 * (sizeof(sndbuffer) / CD_FRAMESIZE_RAW) / 75) #ifdef _WIN32 static HANDLE threadid; #else static pthread_t threadid; #endif static unsigned int initial_offset = 0; static volatile char playing = 0; static char cddaBigEndian = 0; static volatile unsigned int cddaCurOffset = 0; char* CALLBACK CDR__getDriveLetter(void); long CALLBACK CDR__configure(void); long CALLBACK CDR__test(void); void CALLBACK CDR__about(void); long CALLBACK CDR__setfilename(char *filename); long CALLBACK CDR__getStatus(struct CdrStat *stat); extern void *hCDRDriver; struct trackinfo { enum {DATA, CDDA} type; char start[3]; // MSF-format char length[3]; // MSF-format }; #define MAXTRACKS 100 /* How many tracks can a CD hold? */ static int numtracks = 0; static struct trackinfo ti[MAXTRACKS]; // get a sector from a msf-array static unsigned int msf2sec(char *msf) { return ((msf[0] * 60 + msf[1]) * 75) + msf[2]; } static void sec2msf(unsigned int s, char *msf) { msf[0] = s / 75 / 60; s = s - msf[0] * 75 * 60; msf[1] = s / 75; s = s - msf[1] * 75; msf[2] = s; } // divide a string of xx:yy:zz into m, s, f static void tok2msf(char *time, char *msf) { char *token; token = strtok(time, ":"); if (token) { msf[0] = atoi(token); } else { msf[0] = 0; } token = strtok(NULL, ":"); if (token) { msf[1] = atoi(token); } else { msf[1] = 0; } token = strtok(NULL, ":"); if (token) { msf[2] = atoi(token); } else { msf[2] = 0; } } #ifndef _WIN32 static long GetTickCount(void) { static time_t initial_time = 0; struct timeval now; gettimeofday(&now, NULL); if (initial_time == 0) { initial_time = now.tv_sec; } return (now.tv_sec - initial_time) * 1000L + now.tv_usec / 1000L; } #endif // this thread plays audio data #ifdef _WIN32 static void playthread(void *param) #else static void *playthread(void *param) #endif { long d, t, i, s; unsigned char tmp; t = GetTickCount(); while (playing) { d = t - (long)GetTickCount(); if (d <= 0) { d = 1; } else if (d > CDDA_FRAMETIME) { d = CDDA_FRAMETIME; } #ifdef _WIN32 Sleep(d); #else usleep(d * 1000); #endif t = GetTickCount() + CDDA_FRAMETIME; if (subChanInterleaved) { s = 0; for (i = 0; i < sizeof(sndbuffer) / CD_FRAMESIZE_RAW; i++) { // read one sector d = fread(sndbuffer + CD_FRAMESIZE_RAW * i, 1, CD_FRAMESIZE_RAW, cddaHandle); if (d < CD_FRAMESIZE_RAW) { break; } s += d; // skip the subchannel data fseek(cddaHandle, SUB_FRAMESIZE, SEEK_CUR); } } else { s = fread(sndbuffer, 1, sizeof(sndbuffer), cddaHandle); } if (s == 0) { playing = 0; fclose(cddaHandle); cddaHandle = NULL; initial_offset = 0; break; } if (!cdr.Muted && playing) { if (cddaBigEndian) { for (i = 0; i < s / 2; i++) { tmp = sndbuffer[i * 2]; sndbuffer[i * 2] = sndbuffer[i * 2 + 1]; sndbuffer[i * 2 + 1] = tmp; } } SPU_playCDDAchannel((short *)sndbuffer, s); } cddaCurOffset += s; } #ifdef _WIN32 _endthread(); #else pthread_exit(0); return NULL; #endif } // stop the CDDA playback static void stopCDDA() { if (!playing) { return; } playing = 0; #ifdef _WIN32 WaitForSingleObject(threadid, INFINITE); #else pthread_join(threadid, NULL); #endif if (cddaHandle != NULL) { fclose(cddaHandle); cddaHandle = NULL; } initial_offset = 0; } // start the CDDA playback static void startCDDA(unsigned int offset) { if (playing) { if (initial_offset == offset) { return; } stopCDDA(); } cddaHandle = fopen(cdrfilename, "rb"); if (cddaHandle == NULL) { return; } initial_offset = offset; cddaCurOffset = initial_offset; fseek(cddaHandle, initial_offset, SEEK_SET); playing = 1; #ifdef _WIN32 threadid = (HANDLE)_beginthread(playthread, 0, NULL); #else pthread_create(&threadid, NULL, playthread, NULL); #endif } // this function tries to get the .toc file of the given .bin // the necessary data is put into the ti (trackinformation)-array static int parsetoc(const char *isofile) { char tocname[MAXPATHLEN]; FILE *fi; char linebuf[256], dummy[256], name[256]; char *token; char time[20], time2[20]; unsigned int t; numtracks = 0; // copy name of the iso and change extension from .bin to .toc strncpy(tocname, isofile, sizeof(tocname)); tocname[MAXPATHLEN - 1] = '\0'; if (strlen(tocname) >= 4) { strcpy(tocname + strlen(tocname) - 4, ".toc"); } else { return -1; } if ((fi = fopen(tocname, "r")) == NULL) { // check for image.bin.toc (for AcetoneISO) sprintf(tocname, "%s.toc", isofile); if ((fi = fopen(tocname, "r")) == NULL) { // if filename is image.toc.bin, try removing .bin (for Brasero) strcpy(tocname, isofile); t = strlen(tocname); if (t >= 8 && strcmp(tocname + t - 8, ".toc.bin") == 0) { tocname[t - 4] = '\0'; if ((fi = fopen(tocname, "r")) == NULL) { return -1; } } else { return -1; } } } memset(&ti, 0, sizeof(ti)); // parse the .toc file while (fgets(linebuf, sizeof(linebuf), fi) != NULL) { // search for tracks strncpy(dummy, linebuf, sizeof(linebuf)); token = strtok(dummy, " "); if (token == NULL) { continue; } if (!strcmp(token, "TRACK")) { // get type of track token = strtok(NULL, " "); numtracks++; if (!strcmp(token, "MODE2_RAW\n")) { ti[numtracks].type = DATA; sec2msf(2 * 75, ti[numtracks].start); // assume data track on 0:2:0 } else if (!strcmp(token, "AUDIO\n")) { ti[numtracks].type = CDDA; } } else if (!strcmp(token, "DATAFILE")) { sscanf(linebuf, "DATAFILE \"%[^\"]\" %8s", name, time); tok2msf((char *)&time, (char *)&ti[numtracks].length); } else if (!strcmp(token, "FILE")) { sscanf(linebuf, "FILE \"%[^\"]\" #%d %8s %8s", name, &t, time, time2); tok2msf((char *)&time, (char *)&ti[numtracks].start); t /= CD_FRAMESIZE_RAW; t += msf2sec(ti[numtracks].start) + 2 * 75; sec2msf(t, (char *)&ti[numtracks].start); tok2msf((char *)&time2, (char *)&ti[numtracks].length); } } fclose(fi); return 0; } // this function tries to get the .cue file of the given .bin // the necessary data is put into the ti (trackinformation)-array static int parsecue(const char *isofile) { char cuename[MAXPATHLEN]; FILE *fi; char *token; char time[20]; char *tmp; char linebuf[256], dummy[256]; unsigned int t; numtracks = 0; // copy name of the iso and change extension from .bin to .cue strncpy(cuename, isofile, sizeof(cuename)); cuename[MAXPATHLEN - 1] = '\0'; if (strlen(cuename) >= 4) { strcpy(cuename + strlen(cuename) - 4, ".cue"); } else { return -1; } if ((fi = fopen(cuename, "r")) == NULL) { return -1; } memset(&ti, 0, sizeof(ti)); while (fgets(linebuf, sizeof(linebuf), fi) != NULL) { strncpy(dummy, linebuf, sizeof(linebuf)); token = strtok(dummy, " "); if (token == NULL) { continue; } if (!strcmp(token, "TRACK")){ numtracks++; if (strstr(linebuf, "AUDIO") != NULL) { ti[numtracks].type = CDDA; } else if (strstr(linebuf, "MODE1/2352") != NULL || strstr(linebuf, "MODE2/2352") != NULL) { ti[numtracks].type = DATA; } } else if (!strcmp(token, "INDEX")) { tmp = strstr(linebuf, "INDEX"); if (tmp != NULL) { tmp += strlen("INDEX") + 3; // 3 - space + numeric index while (*tmp == ' ') tmp++; if (*tmp != '\n') sscanf(tmp, "%8s", time); } tok2msf((char *)&time, (char *)&ti[numtracks].start); t = msf2sec(ti[numtracks].start) + 2 * 75; sec2msf(t, ti[numtracks].start); // If we've already seen another track, this is its end if (numtracks > 1) { t = msf2sec(ti[numtracks].start) - msf2sec(ti[numtracks - 1].start); sec2msf(t, ti[numtracks - 1].length); } } } fclose(fi); // Fill out the last track's end based on size if (numtracks >= 1) { fseek(cdHandle, 0, SEEK_END); t = ftell(cdHandle) / 2352 - msf2sec(ti[numtracks].start) + 2 * 75; sec2msf(t, ti[numtracks].length); } return 0; } // this function tries to get the .ccd file of the given .img // the necessary data is put into the ti (trackinformation)-array static int parseccd(const char *isofile) { char ccdname[MAXPATHLEN]; FILE *fi; char linebuf[256]; unsigned int t; numtracks = 0; // copy name of the iso and change extension from .img to .ccd strncpy(ccdname, isofile, sizeof(ccdname)); ccdname[MAXPATHLEN - 1] = '\0'; if (strlen(ccdname) >= 4) { strcpy(ccdname + strlen(ccdname) - 4, ".ccd"); } else { return -1; } if ((fi = fopen(ccdname, "r")) == NULL) { return -1; } memset(&ti, 0, sizeof(ti)); while (fgets(linebuf, sizeof(linebuf), fi) != NULL) { if (!strncmp(linebuf, "[TRACK", 6)){ numtracks++; } else if (!strncmp(linebuf, "MODE=", 5)) { sscanf(linebuf, "MODE=%d", &t); ti[numtracks].type = ((t == 0) ? CDDA : DATA); } else if (!strncmp(linebuf, "INDEX 1=", 8)) { sscanf(linebuf, "INDEX 1=%d", &t); sec2msf(t + 2 * 75, ti[numtracks].start); // If we've already seen another track, this is its end if (numtracks > 1) { t = msf2sec(ti[numtracks].start) - msf2sec(ti[numtracks - 1].start); sec2msf(t, ti[numtracks - 1].length); } } } fclose(fi); // Fill out the last track's end based on size if (numtracks >= 1) { fseek(cdHandle, 0, SEEK_END); t = ftell(cdHandle) / 2352 - msf2sec(ti[numtracks].start) + 2 * 75; sec2msf(t, ti[numtracks].length); } return 0; } // this function tries to get the .mds file of the given .mdf // the necessary data is put into the ti (trackinformation)-array static int parsemds(const char *isofile) { char mdsname[MAXPATHLEN]; FILE *fi; unsigned int offset, extra_offset, l, i; unsigned short s; numtracks = 0; // copy name of the iso and change extension from .mdf to .mds strncpy(mdsname, isofile, sizeof(mdsname)); mdsname[MAXPATHLEN - 1] = '\0'; if (strlen(mdsname) >= 4) { strcpy(mdsname + strlen(mdsname) - 4, ".mds"); } else { return -1; } if ((fi = fopen(mdsname, "rb")) == NULL) { return -1; } memset(&ti, 0, sizeof(ti)); // check if it's a valid mds file fread(&i, 1, sizeof(unsigned int), fi); i = SWAP32(i); if (i != 0x4944454D) { // not an valid mds file fclose(fi); return -1; } // get offset to session block fseek(fi, 0x50, SEEK_SET); fread(&offset, 1, sizeof(unsigned int), fi); offset = SWAP32(offset); // get total number of tracks offset += 14; fseek(fi, offset, SEEK_SET); fread(&s, 1, sizeof(unsigned short), fi); s = SWAP16(s); numtracks = s; // get offset to track blocks fseek(fi, 4, SEEK_CUR); fread(&offset, 1, sizeof(unsigned int), fi); offset = SWAP32(offset); // skip lead-in data while (1) { fseek(fi, offset + 4, SEEK_SET); if (fgetc(fi) < 0xA0) { break; } offset += 0x50; } // check if the image contains interleaved subchannel data fseek(fi, offset + 1, SEEK_SET); subChanInterleaved = fgetc(fi); // read track data for (i = 1; i <= numtracks; i++) { fseek(fi, offset, SEEK_SET); // get the track type ti[i].type = ((fgetc(fi) == 0xA9) ? CDDA : DATA); fseek(fi, 8, SEEK_CUR); // get the track starting point ti[i].start[0] = fgetc(fi); ti[i].start[1] = fgetc(fi); ti[i].start[2] = fgetc(fi); if (i > 1) { l = msf2sec(ti[i].start); sec2msf(l - 2 * 75, ti[i].start); // ??? } // get the track length fread(&extra_offset, 1, sizeof(unsigned int), fi); extra_offset = SWAP32(extra_offset); fseek(fi, extra_offset + 4, SEEK_SET); fread(&l, 1, sizeof(unsigned int), fi); l = SWAP32(l); sec2msf(l, ti[i].length); offset += 0x50; } fclose(fi); return 0; } // this function tries to get the .sub file of the given .img static int opensubfile(const char *isoname) { char subname[MAXPATHLEN]; // copy name of the iso and change extension from .img to .sub strncpy(subname, isoname, sizeof(subname)); subname[MAXPATHLEN - 1] = '\0'; if (strlen(subname) >= 4) { strcpy(subname + strlen(subname) - 4, ".sub"); } else { return -1; } subHandle = fopen(subname, "rb"); if (subHandle == NULL) { return -1; } return 0; } static long CALLBACK ISOinit(void) { assert(cdHandle == NULL); assert(subHandle == NULL); return 0; // do nothing } static long CALLBACK ISOshutdown(void) { if (cdHandle != NULL) { fclose(cdHandle); cdHandle = NULL; } if (subHandle != NULL) { fclose(subHandle); subHandle = NULL; } stopCDDA(); return 0; } // This function is invoked by the front-end when opening an ISO // file for playback static long CALLBACK ISOopen(void) { if (cdHandle != NULL) { return 0; // it's already open } cdHandle = fopen(cdrfilename, "rb"); if (cdHandle == NULL) { return -1; } SysPrintf(_("Loaded CD Image: %s"), cdrfilename); cddaBigEndian = 0; subChanInterleaved = 0; if (parsetoc(cdrfilename) == 0) { cddaBigEndian = 1; // cdrdao uses big-endian for CD Audio SysPrintf("[+toc]"); } else if (parsecue(cdrfilename) == 0) { SysPrintf("[+cue]"); } else if (parseccd(cdrfilename) == 0) { SysPrintf("[+ccd]"); } else if (parsemds(cdrfilename) == 0) { SysPrintf("[+mds]"); } if (!subChanInterleaved && opensubfile(cdrfilename) == 0) { SysPrintf("[+sub]"); } SysPrintf(".\n"); return 0; } static long CALLBACK ISOclose(void) { if (cdHandle != NULL) { fclose(cdHandle); cdHandle = NULL; } if (subHandle != NULL) { fclose(subHandle); subHandle = NULL; } stopCDDA(); return 0; } // return Starting and Ending Track // buffer: // byte 0 - start track // byte 1 - end track static long CALLBACK ISOgetTN(unsigned char *buffer) { buffer[0] = 1; if (numtracks > 0) { buffer[1] = numtracks; } else { buffer[1] = 1; } return 0; } // return Track Time // buffer: // byte 0 - frame // byte 1 - second // byte 2 - minute static long CALLBACK ISOgetTD(unsigned char track, unsigned char *buffer) { if (numtracks > 0 && track <= numtracks) { buffer[2] = ti[track].start[0]; buffer[1] = ti[track].start[1]; buffer[0] = ti[track].start[2]; } else { buffer[2] = 0; buffer[1] = 2; buffer[0] = 0; } return 0; } // lookup table for crc calculation static unsigned short crctab[256] = { 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 }; static unsigned short calcCrc(unsigned char *d, int len) { unsigned short crc = 0; int i; for (i = 0; i < len; i++) { crc = crctab[(crc >> 8) ^ d[i]] ^ (crc << 8); } return ~crc; } // read track // time: byte 0 - minute; byte 1 - second; byte 2 - frame // uses bcd format static long CALLBACK ISOreadTrack(unsigned char *time) { if (cdHandle == NULL) { return -1; } if (subChanInterleaved) { fseek(cdHandle, MSF2SECT(btoi(time[0]), btoi(time[1]), btoi(time[2])) * (CD_FRAMESIZE_RAW + SUB_FRAMESIZE) + 12, SEEK_SET); fread(cdbuffer, 1, DATA_SIZE, cdHandle); fread(subbuffer, 1, SUB_FRAMESIZE, cdHandle); if ((((u16)subbuffer[22] << 8) | (u16)subbuffer[23]) != calcCrc(&subbuffer[12], 10)) { memset(&subbuffer[15], 0, 7); // CRC wrong, wipe out time data } } else { fseek(cdHandle, MSF2SECT(btoi(time[0]), btoi(time[1]), btoi(time[2])) * CD_FRAMESIZE_RAW + 12, SEEK_SET); fread(cdbuffer, 1, DATA_SIZE, cdHandle); if (subHandle != NULL) { fseek(subHandle, MSF2SECT(btoi(time[0]), btoi(time[1]), btoi(time[2])) * SUB_FRAMESIZE, SEEK_SET); fread(subbuffer, 1, SUB_FRAMESIZE, subHandle); if ((((u16)subbuffer[22] << 8) | (u16)subbuffer[23]) != calcCrc(&subbuffer[12], 10)) { memset(&subbuffer[15], 0, 7); // CRC wrong, wipe out time data } } } return 0; } // return readed track static unsigned char * CALLBACK ISOgetBuffer(void) { return cdbuffer; } // plays cdda audio // sector: byte 0 - minute; byte 1 - second; byte 2 - frame // does NOT uses bcd format static long CALLBACK ISOplay(unsigned char *time) { if (SPU_playCDDAchannel != NULL) { if (subChanInterleaved) { startCDDA(MSF2SECT(time[0], time[1], time[2]) * (CD_FRAMESIZE_RAW + SUB_FRAMESIZE)); } else { startCDDA(MSF2SECT(time[0], time[1], time[2]) * CD_FRAMESIZE_RAW); } } return 0; } // stops cdda audio static long CALLBACK ISOstop(void) { stopCDDA(); return 0; } // gets subchannel data static unsigned char* CALLBACK ISOgetBufferSub(void) { if (subHandle != NULL || subChanInterleaved) { return subbuffer; } return NULL; } static long CALLBACK ISOgetStatus(struct CdrStat *stat) { int sec; CDR__getStatus(stat); if (playing) { stat->Type = 0x02; stat->Status |= 0x80; sec = cddaCurOffset / CD_FRAMESIZE_RAW; sec2msf(sec, (char *)stat->Time); } else { stat->Type = 0x01; } return 0; } void imageReaderInit(void) { assert(hCDRDriver == NULL); CDR_init = ISOinit; CDR_shutdown = ISOshutdown; CDR_open = ISOopen; CDR_close = ISOclose; CDR_getTN = ISOgetTN; CDR_getTD = ISOgetTD; CDR_readTrack = ISOreadTrack; CDR_getBuffer = ISOgetBuffer; CDR_play = ISOplay; CDR_stop = ISOstop; CDR_getBufferSub = ISOgetBufferSub; CDR_getStatus = ISOgetStatus; CDR_getDriveLetter = CDR__getDriveLetter; CDR_configure = CDR__configure; CDR_test = CDR__test; CDR_about = CDR__about; CDR_setfilename = CDR__setfilename; numtracks = 0; }