/*---------------------------------------------------------------------------- DOS program to 'rip' audio tracks from an audio CD to a .WAV file. Compile with Turbo C, Watcom C, or DJGPP. Chris Giese http://SisAndHappy.com/ChrisGiese/ I, the copyright holder of this work, hereby release it into the public domain. This applies worldwide. If this is not legally possible: I grant any entity the right to use this work for any purpose, without any conditions, unless such conditions are required by law. Sources: SFF-8020 specification plus the SCSI docs: SPC (primary commands), SBC (block commands), MMC (multimedia commands) Note: - The CD (or DVD) drive must support the use of the READ CD command to read CD DA (audio) discs. - The CD drive must also support "accurate" CD DA reading. I hope to lift this restriction in a future version of this code. Dec. 2021: - Moved drive reset code out of the inner loop in main() Aug. 2018: - Code to display sense key and mode sense information moved to separate functions - Code to rip audio moved out of main() to rip_track() - Was overshooting end of disc when reading last track; fixed. - Can now specify track to be ripped from the command line - DEBUG() macro to hide various debug messages - Progress bargraph 19 Jul. 2018: - Skip drives that don't contain a CD, or contain a disc with no readable table of contents (TOC) - Display TOC; let user pick which track gets ripped - Output filename is "trackNN.wav"; where NN is the chosen track number 25 Dec. 2016: initial release - Starting point was my polling ATAPI driver: http://SisAndHappy.com/ChrisGiese/code/atapi.c - Now using 'const' - Added simple .WAV file output code from another project To do soon: - Your ATA/ATAPI code does not detect drive 1 if drive 0 is not present on the interface -- FIX. (Drives shouldn't be connected this way anyway because it degrades performance.) - medium type is being reported wrong in scsi_dump_page42() -- 120 mm (5") disc reported as 80 mm (3") - Command-line and UI options to... - Rip all tracks ? - Let user chose CD-ROM device if more than one is detected ? - Rip entire disk to a single .WAV file instead of multiple .WAV files ? To do later: - drive says it can read UPC codes -- try getting that to work - Work with drives that don't support "accurate" CD-DA TOLERANCE IN THAT CASE IS +/-75 SECTORS (+/-1 SECOND); WHICH MEANS YOU NEED AN ADDITIONAL BUFFER THAT IS 2 SECONDS (=352,800 BYTES) IN SIZE. - Command-line and UI options to... - Rip part of a track (specify track, start time, length/end time) ? - Maybe let user specify output file name (currently trackNN.wav) ? Usage: - List usable drives and TOCs and exit rip-cd 1000 - Rip a single track rip-cd 1 - Rip multiple tracks for %f in (1 2 3 4 5 6 7 8) do rip-cd %f ----------------------------------------------------------------------------*/ #include /* memset() */ #include /* printf() */ #include /* kbhit(), getch() */ #include /* inportb(), outportb(), inportw(), outportw(), delay() */ #if 0 #include #else typedef unsigned char uint8_t; typedef unsigned short uint16_t; typedef unsigned long uint32_t; #endif #if 0 #define DEBUG(X) X #else #define DEBUG(X) /* nothing */ #endif #define COUNT(N) (sizeof(N) / sizeof((N)[0])) #if defined(__TURBOC__) #include /* void *alloca(size_t size); */ #undef outportw #define outportw(P,V) outport(P,V) #undef inportw #define inportw(P) inport(P) #elif defined(__WATCOMC__) #define outportb(P,V) outp(P,V) #define inportb(P) inp(P) #define outportw(P,V) outpw(P,V) #define inportw(P) inpw(P) #endif /****************************************************************************/ #define BPERL 16 /* byte/line for dump */ static void dump(const void *data0, unsigned count) { const uint8_t *data = data0; unsigned j, k; while(count != 0) { for(j = 0; j < BPERL; j++) { if(count == 0) break; printf("%02X ", data[j]); count--; } printf("\t"); for(k = 0; k < j; k++) { if(data[k] < ' ') printf("."); else printf("%c", data[k]); } printf("\n"); data += BPERL; } } /****************************************************************************/ static unsigned read_be16(const void *buf0) { const uint8_t *buf = buf0; unsigned rv; rv = buf[0]; rv <<= 8; rv |= buf[1]; return rv; } /****************************************************************************/ static uint32_t read_be32(const void *buf0) { const uint8_t *buf = buf0; uint32_t rv; rv = buf[0]; rv <<= 8; rv |= buf[1]; rv <<= 8; rv |= buf[2]; rv <<= 8; rv |= buf[3]; return rv; } /****************************************************************************/ static void write_be16(void *buf0, unsigned val) { uint8_t *buf = buf0; buf[1] = val; val >>= 8; buf[0] = val; } /****************************************************************************/ static void write_be32(void *buf0, uint32_t val) { uint8_t *buf = buf0; buf[3] = val; val >>= 8; buf[2] = val; val >>= 8; buf[1] = val; val >>= 8; buf[0] = val; } /****************************************************************************/ static void write_le16(void *buf0, unsigned val) { uint8_t *buf = buf0; buf[0] = val; val >>= 8; buf[1] = val; } /****************************************************************************/ static void write_le32(void *buf0, uint32_t val) { uint8_t *buf = buf0; buf[0] = val; val >>= 8; buf[1] = val; val >>= 8; buf[2] = val; val >>= 8; buf[3] = val; } /****************************************************************************/ static void insw(unsigned io, void *data0, unsigned word_count) { uint16_t *data = data0; /* read and discard */ if(data0 == NULL) { for(; word_count != 0; word_count--) (void)inportw(io); } else { for(; word_count != 0; word_count--) { *data = inportw(io); data++; } } } /****************************************************************************/ static void outsw(unsigned io, const void *data0, unsigned word_count) { const uint16_t *data = data0; for(; word_count != 0; word_count--) { outportw(io, *data); data++; } } /***************************************************************************** when 'mask' is ANDed with the drive status bits, it should equal 'want' *****************************************************************************/ #define POSITIVE 0x0100 #define NEGATIVE (-1 & ~0xFF) /* 0xFF00 or 0xFFFFFF00 or ? */ static int await_drive(unsigned io, unsigned milliseconds, unsigned mask, unsigned want) { unsigned j, status = 0; /* poll until BSY=0 */ for(j = milliseconds / 10; j != 0; j--) { /* read status register */ status = inportb(io + 7); if((status & 0x80) == 0) break; delay(10); } if(j == 0) return 0; /* timeout: return 0 */ if((status & mask) != want) return NEGATIVE | status; /* wrong status: return <0 */ else return POSITIVE | status; /* success: return >0 */ } /****************************************************************************/ static const char *phase_name(unsigned phase) { static const char *pname[] = { "??", "??", "??", "DONE", "BUS RELEASE", "??", "??", "??", "DATA OUT", "CMD OUT", "DATA IN", "(CMD IN ?)" }; /**/ return (phase >= COUNT(pname)) ? "???" : pname[phase]; } /****************************************************************************/ #define ATAPI_TIMEOUT 10000 typedef struct { unsigned long start_lba, num_sectors; } track_t; #define MAX_TRACKS 35 typedef struct { /* ATA */ unsigned cmd_io, ctl_io; unsigned char unit; /* tracks on the CD */ track_t track[MAX_TRACKS]; unsigned num_tracks; } drive_t; /****************************************************************************/ static int detect_atapi_drive(const drive_t *ata) { unsigned status, sig; DEBUG( printf("detect_atapi_drive(): unit %u on interface 0x%X: ", ata->unit, ata->cmd_io);) /* select drive #1 */ if(ata->unit != 0) outportb(ata->cmd_io + 6, 0xA0 | 0x10); /* Wait up to 1 second for drive to become ready. NOTE: Do NOT check for DRDY=1 here. */ if(await_drive(ata->cmd_io, 1000, 0, 0) <= 0) NOPE: { DEBUG( printf("nothing there\n");) return 0; } /* read drive signature from cylinder registers */ sig = inportb(ata->cmd_io + 5); sig <<= 8; sig |= inportb(ata->cmd_io + 4); /* (parallel interface) ATA drive */ if(sig == 0) { /* test if ATA device has DRDY=1 (prevents false detection of non-existent unit #1) */ status = inportb(ata->cmd_io + 7); if((status & 0x40) == 0) goto NOPE; DEBUG( printf("ATA drive\n");) return 0; } /* SATA? SATAPI? */ else if(sig != 0xEB14) { DEBUG( printf("unknown drive signature 0x%X\n", sig);) return 0; } /* issue IDENTIFY PACKET DEVICE command to verify the drive's there */ outportb(ata->cmd_io + 7, 0xA1); if(await_drive(ata->cmd_io, 1000, 0x69, 0x48) <= 0) goto NOPE; /* read and discard IDENTIFY DEVICE data */ insw(ata->cmd_io + 0, NULL, 256); DEBUG( printf("ATAPI drive\n");) return 1; } /***************************************************************************** ATAPI drives are supposed to respond to the DEVICE RESET command at any time; not just when status.BSY=0. ATA drives do not implement DEVICE RESET. *****************************************************************************/ static int atapi_reset(const drive_t *ata) { unsigned io; io = ata->cmd_io; /* select drive */ outportb(io + 6, 0xA0 | (ata->unit ? 0x10 : 0x00)); outportb(io + 7, 0x08); /* 0x08 = ATAPI DEVICE RESET command */ return (await_drive(io, ATAPI_TIMEOUT, 0x00, 0x00) <= 0) ? -1 : 0; } /****************************************************************************/ /* Rea- I/!O C/!D DRQ son b1 b0 --- ---- ---- ---- */ #define PHASE_DONE (0 + 3) /* 1 1 */ #define PHASE_DATA_IN (8 + 2) /* 1 0 */ #define PHASE_CMD_OUT (8 + 1) /* 0 1 */ #define PHASE_DATA_OUT (8 + 0) /* 0 0 */ static int atapi_read(const drive_t *ata, const void *pkt0, void *buf0, unsigned want) { unsigned io, phase, got, rv, j; const uint8_t *pkt = pkt0; uint8_t *buf = buf0; int i; io = ata->cmd_io; /* select drive */ outportb(io + 6, ata->unit ? 0xB0 : 0xA0); /* want BSY=0 DRQ=0 */ if((i = await_drive(io, ATAPI_TIMEOUT, 0x88, 0)) <= 0) { printf("Error in atapi_read(): drive did not become " "ready after select\n"); return -1; } outportb(io + 1, 0); /* b0=0: use PIO */ outportb(io + 4, 0); /* max. byte count... */ outportb(io + 5, 0x80); /* ...=32768 */ outportb(io + 7, 0xA0); /* 0xA0 = ATAPI PACKET command */ /* should take no longer than 10ms */ if((i = await_drive(io, 10, 0x80, 0)) <= 0) { printf("Error in atapi_read(): drive did not become ready " "after PACKET (status=0x%02X)\n", ~i & 0xFF); return -1; } /* 'phase' = DRQ and... */ phase = i & 0x08; /* ...I/!O and C/!D bits from ATAPI "Interrupt Reason" register (same as ATA "Sector Count") register */ phase |= (inportb(io + 2) & 3); if(phase != PHASE_CMD_OUT) { printf("Error in atapi_read(): wanted phase 0x09 (CMD OUT), " "got phase 0x%02X (%s)\n", phase, phase_name(phase)); return -1; } /* xxx - I am assuming the drive uses 12-byte packets... */ outsw(io + 0, pkt, 6); rv = 0; while(1) { if((i = await_drive(io, ATAPI_TIMEOUT, 0x81, 0)) <= 0) { //printf("Error in atapi_read(): drive did not become ready in read loop\n"); return -1; } phase = i & 0x08; phase |= (inportb(io + 2) & 3); if(phase == PHASE_DONE) break; else if(phase == PHASE_DATA_IN) { /* how many bytes does the drive have available? */ got = inportb(io + 5); got <<= 8; got |= inportb(io + 4); /* is it <, =, or > the number of bytes we want? */ if(want != 0 && got != 0) { j = (got < want) ? got : want; insw(io + 0, buf, j / 2); buf += j; want -= j; got -= j; rv += j; } /* if the drive has more data than we want, read and discard */ if(got != 0) insw(io + 0, NULL, got / 2); } else { printf("Error in atapi_read(): drive in strange " "phase 0x%02X (%s)\n", phase, phase_name(phase)); return -1; } } return rv; /* return non-negative value: number of bytes read */ } /****************************************************************************/ static void scsi_dump_sense(unsigned sense_key, unsigned asc, int ascq) { static const char *sk_msg[] = { "NO SENSE", "RECOVERED ERROR", "NOT READY", "MEDIUM ERROR", "HARDWARE ERROR", "ILLEGAL REQUEST", "UNIT ATTENTION", "DATA PROTECT", "BLANK CHECK", "vendor specific", "COPY ABORTED", "ABORTED COMMAND", "obsolete", "VOLUME OVERFLOW", "MISCOMPARE", "reserved" }; static const struct { uint8_t asc, ascq; const char *text; } a_msg[] = { /* copy-and-pasted from SFF-8020i */ { 0x00, 0x00, "NO ADDITIONAL SENSE INFORMATION" }, { 0x00, 0x11, "PLAY OPERATION IN PROGRESS" }, { 0x00, 0x12, "PLAY OPERATION PAUSED" }, { 0x00, 0x13, "PLAY OPERATION SUCCESSFULLY COMPLETED" }, { 0x00, 0x14, "PLAY OPERATION STOPPED DUE TO ERROR" }, { 0x00, 0x15, "NO CURRENT AUDIO STATUS TO RETURN" }, { 0x01, 0x00, "MECHANICAL POSITIONING OR CHANGER ERROR" }, { 0x02, 0x00, "NO SEEK COMPLETE" }, { 0x04, 0x00, "LOGICAL DRIVE NOT READY - CAUSE NOT REPORTABLE" }, { 0x04, 0x01, "LOGICAL DRIVE NOT READY - IN PROGRESS OF BECOMING READY" }, { 0x04, 0x02, "LOGICAL DRIVE NOT READY - INITIALIZING COMMAND REQUIRED" }, { 0x04, 0x03, "LOGICAL DRIVE NOT READY - MANUAL INTERVENTION REQUIRED" }, { 0x05, 0x01, "MEDIA LOAD - EJECT FAILED" }, { 0x06, 0x00, "NO REFERENCE POSITION FOUND (disk may be upside-down)" }, { 0x09, 0x00, "TRACK FOLLOWING ERROR" }, { 0x09, 0x01, "TRACKING SERVO FAILURE" }, { 0x09, 0x02, "FOCUS SERVO FAILURE" }, { 0x09, 0x03, "SPINDLE SERVO FAILURE" }, { 0x11, 0x00, "UNRECOVERED READ ERROR" }, { 0x11, 0x06, "CIRC UNRECOVERED ERROR" }, { 0x15, 0x00, "RANDOM POSITIONING ERROR" }, { 0x15, 0x01, "MECHANICAL POSITIONING OR CHANGER ERROR" }, { 0x15, 0x02, "POSITIONING ERROR DETECTED BY READ OF MEDIUM" }, { 0x17, 0x00, "RECOVERED DATA WITH NO ERROR CORRECTION APPLIED" }, { 0x17, 0x01, "RECOVERED DATA WITH RETRIES" }, { 0x17, 0x02, "RECOVERED DATA WITH POSITIVE HEAD OFFSET" }, { 0x17, 0x03, "RECOVERED DATA WITH NEGATIVE HEAD OFFSET" }, { 0x17, 0x04, "RECOVERED DATA WITH RETRIES AND/OR CIRC APPLIED" }, { 0x17, 0x05, "RECOVERED DATA USING PREVIOUS SECTOR ID" }, { 0x18, 0x00, "RECOVERED DATA WITH ERROR CORRECTION APPLIED" }, { 0x18, 0x01, "RECOVERED DATA WITH ERROR CORRECTION & RETRIES APPLIED" }, { 0x18, 0x02, "RECOVERED DATA - THE DATA WAS AUTO-REALLOCATED" }, { 0x18, 0x03, "RECOVERED DATA WITH CIRC" }, { 0x18, 0x04, "RECOVERED DATA WITH L-EC" }, { 0x1A, 0x00, "PARAMETER LIST LENGTH ERROR" }, { 0x20, 0x00, "INVALID COMMAND OPERATION CODE" }, { 0x21, 0x00, "LOGICAL BLOCK ADDRESS OUT OF RANGE" }, { 0x24, 0x00, "INVALID FIELD IN COMMAND PACKET" }, { 0x26, 0x00, "INVALID FIELD IN PARAMETER LIST" }, { 0x26, 0x01, "PARAMETER NOT SUPPORTED" }, { 0x26, 0x02, "PARAMETER VALUE INVALID" }, { 0x28, 0x00, "NOT READY TO READY TRANSITION, MEDIUM MAY HAVE CHANGED" }, { 0x29, 0x00, "POWER ON, RESET OR BUS DEVICE RESET OCCURRED" }, { 0x2A, 0x00, "PARAMETERS CHANGED" }, { 0x2A, 0x01, "MODE PARAMETERS CHANGED" }, { 0x30, 0x00, "INCOMPATIBLE MEDIUM INSTALLED" }, { 0x30, 0x01, "CANNOT READ MEDIUM - UNKNOWN FORMAT" }, { 0x30, 0x02, "CANNOT READ MEDIUM - INCOMPATIBLE FORMAT" }, { 0x39, 0x00, "SAVING PARAMETERS NOT SUPPORTED" }, { 0x3A, 0x00, "MEDIUM NOT PRESENT" }, { 0x3F, 0x00, "ATAPI CD-ROM DRIVE OPERATING CONDITIONS HAVE CHANGED" }, { 0x3F, 0x01, "MICROCODE HAS BEEN CHANGED" }, { 0x40, 0x00, "DIAGNOSTIC FAILURE ON COMPONENT #ascq" }, { 0x44, 0x00, "INTERNAL ATAPI CD-ROM DRIVE FAILURE" }, { 0x4E, 0x00, "OVERLAPPED COMMANDS ATTEMPTED" }, { 0x53, 0x00, "MEDIA LOAD OR EJECT FAILED" }, { 0x53, 0x02, "MEDIUM REMOVAL PREVENTED" }, { 0x57, 0x00, "UNABLE TO RECOVER TABLE OF CONTENTS" }, { 0x5A, 0x00, "OPERATOR REQUEST OR STATE CHANGE INPUT (UNSPECIFIED)" }, { 0x5A, 0x01, "OPERATOR MEDIUM REMOVAL REQUEST" }, { 0x63, 0x00, "END OF USER AREA ENCOUNTERED ON THIS TRACK" }, { 0x64, 0x00, "ILLEGAL MODE FOR THIS TRACK" }, { 0xB9, 0x00, "PLAY OPERATION ABORTED" }, { 0xBF, 0x00, "LOSS OF STREAMING" } }; /**/ unsigned j; printf("sense key=%u (%s)", sense_key, sk_msg[sense_key]); /* find message corresponding to ASC and ASCQ, if any, and display it */ for(j = 0; j < COUNT(a_msg); j++) { if(a_msg[j].asc == asc && (asc == 0x40 || a_msg[j].ascq == ascq)) { if(ascq >= 0) printf(", ASC=%u, ASCQ=%u\n\t(%s)\n", asc, ascq, a_msg[j].text); else printf(", ASC=%u (%s)\n", asc, a_msg[j].text); return; } } if(ascq >= 0) printf(", ASC=%u, ASCQ=%u (?)\n", asc, ascq); else printf(", ASC=%u (?)\n", asc); } /****************************************************************************/ static void scsi_dump_page42(const void *buf0, unsigned buf_len) { static const char *loader[] = { "caddy", "tray", "pop-up", "?", "changer", "changer w/ cartridge", "?", "?" }; static const char *media[] = { /* 0 */ "unknown", "120mm CD-ROM data", "120mm CD-ROM audio", "120mm CD-ROM data+audio", "120mm Photo CD-ROM", /* 5 */ "80mm CD-ROM data", "80mm CD-ROM audio", "80mm CD-ROM data+audio", "80mm Photo CD-ROM", /* 9 */ "?", "?", "?", "?", "?", "?", "?", /* 16 */ "CD-R; unknown size", "120mm CD-R data", "120mm CD-R audio", "120mm CD-R data+audio", "120mm Photo CD-R", /* 21 */ "80mm CD-R data", "80mm CD-R audio", "80mm CD-R data+audio", "80mm Photo CD-R", /* 25 */ "?", "?", "?", "?", "?", "?", "?", /* 32 */ "CD-E; unknown size", "120mm CD-E data", "120mm CD-E audio", "120mm CD-E data+audio", "120mm Photo CD-E", /* 37 */ "80mm CD-E data", "80mm CD-E audio", "80mm CD-E data+audio", "80mm Photo CD-E", /* 41 */ "?", "?", "?", "?", "?", "?", "?", /* 48 */ "unknown" }; /**/ const uint8_t *buf = buf0, *ptr; unsigned len = 0, j; ptr = &buf[8]; for(; buf_len > 8; buf_len -= len) { len = ptr[1]; // printf("code %u, len=%u\n", ptr[0], len); if(ptr[0] == 42) goto OK; ptr += len; } printf("Error: page 42 not found\n"); return; OK: printf("medium type=%u ", buf[2]); if(buf[2] == 0x70) printf("(none)"); else if(buf[2] == 0x71) printf("(door open or no caddy)"); else if(buf[2] == 0x72) printf("(media error)"); else if(buf[2] < COUNT(media)) printf("(%s)", media[buf[2]]); else printf("(?)"); printf("\n"); /* DVD stuff is in MMC-3 but not in SFF-8020 */ #define YN(byte,bit) ((ptr[byte] & bit) ? "y " : "n ") printf( " DVD-RAM DVD-R DVD-ROM Method2 CD-RW CD-R\n" " ------- ------- ------- ------- ------- -------\n" "read: %s %s %s %s %s %s\n" "write: %s %s n n %s %s\n", YN(2,0x20), YN(2,0x10), YN(2,0x08), YN(2,0x04), YN(2,0x02), YN(2,0x01), YN(3,0x20), YN(3,0x10), YN(3,0x02), YN(3,0x01)); printf( "multi-session :%s ""Mode 2 Form 2 :%s\n" "Mode 2 Form 1 :%s ""composite A/V output :%s\n" /* I think "audio play" means these commands are supported: PLAY AUDIO, PLAY AUDIO MSF, PAUSE/RESUME, SCAN, STOP PLAY/SCAN. Bar code is in MMC-3 but not in SFF-8020 */ "audio play :%s ""read bar code :%s\n" "UPC :%s ""accurate READ CD-DA :%s\n" /* "READ CD-DA works" means sector type 1 (CD-DA) can be used with the READ CD command (i.e. it means the drive can "rip" audio) */ "READ CD-DA works :%s ""PREVENT/ALLOW works :%s\n" "drive locked :%s\n", YN(4,0x40), YN(4,0x20), YN(4,0x10), YN(4,0x02), YN(4,0x01), YN(5,0x80), YN(5,0x40), YN(5,0x02), YN(5,0x01), YN(6,0x01), YN(6,0x02)); j = buf[6] >> 5; printf("loading mechanism type=%u (%s)\n", j, loader[j]); } /****************************************************************************/ static void scsi_dump_toc(const track_t *track, unsigned num_tracks) { unsigned t, f, s, m; unsigned long lba; printf( "track start time end\n" "----- -------- -------- --------\n"); for(t = 0; t < num_tracks; t++) { printf("%5u", t + 1); lba = track[t].start_lba; f = (unsigned)(lba % 75); s = (unsigned)((lba / 75) % 60); m = (unsigned)(lba / 75 / 60); printf(" %2u:%02u:%02u", m, s, f); lba = track[t].num_sectors; f = (unsigned)(lba % 75); s = (unsigned)((lba / 75) % 60); m = (unsigned)(lba / 75 / 60); printf(" %2u:%02u:%02u", m, s, f); lba = track[t].start_lba + track[t].num_sectors - 1; f = (unsigned)(lba % 75); s = (unsigned)((lba / 75) % 60); m = (unsigned)(lba / 75 / 60); printf(" %2u:%02u:%02u\n", m, s, f); } } /****************************************************************************/ static int scsi_request_sense(const drive_t *ata, unsigned *sense_key_ptr, unsigned *asc_ptr, int *ascq_ptr) { uint8_t buf[14], pkt[16]; unsigned sense_key, len; int i; DEBUG( printf("\n*** REQUEST SENSE ***\n");) memset(pkt, 0, sizeof(pkt)); pkt[0] = 0x03; /* REQUEST SENSE */ pkt[4] = sizeof(buf); /* REQUEST SENSE is the one SCSI command that should _never_ fail */ if((i = atapi_read(ata, pkt, buf, sizeof(buf))) < 0) { printf("Error: SCSI command REQUEST SENSE failed\n"); return i; } sense_key = buf[2] & 0x0F; if(sense_key_ptr != NULL) *sense_key_ptr = sense_key; len = 8 + buf[7]; if(asc_ptr != NULL && len >= 13) *asc_ptr = buf[12]; if(ascq_ptr != NULL) *ascq_ptr = (len >= 14) ? buf[13] : -1; return 0; } /***************************************************************************** Use SCSI command MODE SENSE (0x5A) to read page 0x2A (42); the "CD-ROM Capabilities & Mechanical Status Page" Other pages: 0 vendor-specific 1 read error recovery page 10.8.6.3 on p.114 of SFF-8020i 2-12 reserved 13 CD-ROM page 10.8.6.2 on p.113 14 CD-ROM audio control page 10.8.6.1 on p.111 15-31 reserved 32-41 vendor-specific 42 CD-ROM capabilities & mechanical status 10.8.6.4 on p.118 43-62 vendor-specific 63 return ALL pages *****************************************************************************/ static int scsi_mode_sense(const drive_t *ata, void *buf, unsigned buf_len) { uint8_t pkt[12] = { 0x5A, /* MODE SENSE */ 0, /* reserved */ 0x2A, /* b7-6=0=return 'current' values, b5-0=page code */ 0, 0, 0, 0, 0, 0, /* allocation length; set below by write_be16() */ 0, 0, 0 }; int i; DEBUG( printf("\n*** MODE SENSE ***\n");) write_be16(&pkt[7], buf_len); if((i = atapi_read(ata, pkt, buf, buf_len)) < 0) { DEBUG( printf("Error: SCSI command MODE SENSE failed\n");) return i; } return 0; } /***************************************************************************** TOC=Table Of Contents. Returns <0 if error else track count. *****************************************************************************/ static int scsi_read_toc(const drive_t *ata, track_t *track, unsigned max_tracks) { unsigned long start_lba, end_lba; unsigned len, num_tracks, t; uint8_t pkt[12], *buf; int i; DEBUG( printf("\n*** READ TOC[/PMA/ATIP] ***\n");) len = 12 + 8 * max_tracks; buf = alloca(len); memset(pkt, 0, sizeof(pkt)); pkt[0] = 0x43; /* READ TOC */ /* I need to subtract block addresses in scsi_dump_toc() That's clutzy in MSF format, so use LBA format: pkt[1] = 0x02; Alas, the LBA-to-MSF conversion isn't all that elegant either... */ write_be16(&pkt[7], len); if((i = atapi_read(ata, pkt, buf, len)) < 0) { DEBUG( printf("Error: SCSI command READ TOC[/PMA/ATIP] failed\n");) return i; } /* actual length of data in the buffer: */ len = 2 + read_be16(&buf[0]); /* track 0 is...what? it's not an audio track, so skip the first 8-byte TOC entry */ num_tracks = (len - 12) / 8; if(num_tracks > max_tracks) num_tracks = max_tracks; start_lba = 0; for(t = 0; t < num_tracks; t++) { track[t].start_lba = start_lba; end_lba = read_be32(&buf[12 + t * 8 + 4]); track[t].num_sectors = end_lba - start_lba; start_lba = end_lba; } /* return value may be < max_tracks */ return num_tracks; } /***************************************************************************** READ CD command packet: byte 0 opcode (=0xBE) 1 b7-5=reserved, b4-2=expected sector type, b1-0=reserved 2-5 start LBA 6-8 sector count 9 flags 10 b7-3=reserved, b2-0=subchannel data selection bits 11 reserved expected sector type: - mandatory: 0=any, 2=mode 1, 3=mode 2, 4=mode 2 form 1, 5=mode 2 form 2 - optional: 1=CD DA (audio) - reserved: 6-7 flags (what information to read from the disc): b7=sync, b6-5=header code, b4=user data, b3=CRC & ECC, b2-1=error flags, b0=reserved header code: 0=none, 1=mode 1/mode 1 form 1 4-byte header, 2=mode 2 form 1/2 subheader, 3=all headers error flags: 0=none, 1=C2, 2=C2 and block error flags, 3=reserved subchannel selection bits: - mandatory: 0=none - optional: 1=raw, 2=Q, 4=R-W - reserved: 3, 5-7 ------------------------------------------------------------------- byte bits description 1 0 0 0 t t t 0 0 t=expected sector (0=any) 9 s-h-h-u-c-e-e-0 s=include sync field h=include header and (optional) subchannel u=include user data c=include ECC data e=include error flag data 10 0 0 0 0 0 b b b b=subchannel to include *****************************************************************************/ static int scsi_read_cd(const drive_t *ata, void *buf, unsigned num_sects, unsigned long lba) { unsigned count; char pkt[12]; int i; DEBUG( printf("\n*** READ CD ***\n");) memset(pkt, 0, sizeof(pkt)); pkt[0] = 0xBE; /* READ CD */ write_be32(&pkt[2], lba); write_be16(&pkt[7], num_sects); pkt[9] = 0x10; /* only user data (i.e. audio) */ count = num_sects * 2352; if((i = atapi_read(ata, pkt, buf, count)) != count) { DEBUG( printf("Error: SCSI command READ CD failed\n");) return i; } return 0; } /***************************************************************************** Skip 44 bytes at the start of the raw PCM output file, then call this function when done to write a .WAV header. *****************************************************************************/ static int write_wav_header(FILE *out_file) { static const unsigned bits_per_sample = 16; static const unsigned freq = 44100u; static const unsigned num_chan = 2; /**/ uint8_t wav_hdr[44] = { 'R', 'I', 'F', 'F', /* 4 */ 0, 0, 0, 0, /* length of what follows */ 'W', 'A', 'V', 'E', 'f', 'm', 't', ' ', 16, 0, 0, 0, /* fmt block is 16 bytes long: */ 1, 0, /* codec=1=PCM */ /* 22 */ 0, 0, /* number of channels */ /* 24 */ 0, 0, 0, 0, /* samples/second */ /* 28 */ 0, 0, 0, 0, /* average block size */ /* 32 */ 0, 0, /* block align */ /* 34 */ 0, 0, /* bits/sample */ 'd', 'a', 't', 'a', /* 40 */ 0, 0, 0, 0 /* length of data that follows */ /* 44 */}; unsigned long len; fseek(out_file, 0, SEEK_END); len = ftell(out_file); write_le32(&wav_hdr[4], len - 8); write_le16(&wav_hdr[22], num_chan); write_le32(&wav_hdr[24], freq); write_le32(&wav_hdr[28], freq * 2L * num_chan); // xxx - should the following be bytes_per_sample * num_chan ? write_le16(&wav_hdr[32], 1);//2 * num_chan); write_le16(&wav_hdr[34], bits_per_sample); write_le32(&wav_hdr[40], len - sizeof(wav_hdr)); fseek(out_file, 0, SEEK_SET); return (fwrite(wav_hdr, 1, sizeof(wav_hdr), out_file) == sizeof(wav_hdr)) ? 0 : -1; } /****************************************************************************/ #include /* time_t, time() */ static void bargraph(unsigned percent) { static time_t start_time; /**/ time_t elapsed_time, total_time, remaining_time; char buf[80]; if(start_time == 0) { start_time = time(NULL); printf( "Elapsed Remaining Total\n"); } elapsed_time = time(NULL) - start_time; total_time = (percent == 0) ? (time_t)(-1L) : (elapsed_time * 100 / percent); remaining_time = (percent == 0) ? (time_t)(-1L) : (total_time - elapsed_time); if(elapsed_time == (time_t)(-1L)) sprintf(buf, "??:?? "); else sprintf(buf, "%02lu:%02lu ", elapsed_time / 60, elapsed_time % 60); if(remaining_time == (time_t)(-1L)) sprintf(&buf[7], " ??:?? "); else sprintf(&buf[7], " %02lu:%02lu ", remaining_time / 60, remaining_time % 60); if(total_time == (time_t)(-1L)) sprintf(&buf[18], " ??:?? ["); else sprintf(&buf[18], " %02lu:%02lu [", total_time / 60, total_time % 60); percent /= 2; memset(&buf[27], '*', percent); memset(&buf[27 + percent], '_', 50 - percent); sprintf(&buf[77], "]\r"); fputs(buf, stdout); } /****************************************************************************/ #define NUM_SECTS 12 #define BYTES_PER_SECT 2352u /* 2048 for data CD, 2352 for audio CD */ #define BUF_SIZE (NUM_SECTS * BYTES_PER_SECT) /* 27.5K */ #define MAX_RETRIES 3 static int rip_track(const drive_t *d, const char *out_name, track_t *track) { static uint8_t buf[BUF_SIZE]; /**/ unsigned j, tries, sense_key, asc; unsigned long start, end, lba; int ascq; FILE *out; /* open output file */ if((out = fopen(out_name, "wb")) == NULL) { printf("Error: can't open output file '%s'\n", out_name); return 3; } /* leave room for 44-byte .WAV header */ fseek(out, 44, SEEK_SET); start = track->start_lba; end = start + track->num_sectors; printf("Press a key to abort\n"); for(lba = start; lba < end; ) { if(kbhit()) { if(getch() == 0) (void)getch(); break; } //printf("LBA=%lu/%lu\n", lba, end); bargraph((unsigned)((lba - start) * 100 / (end - start))); /* don't overshoot end of track (or end of disc) */ if((j = NUM_SECTS) > (end - lba)) j = (unsigned)(end - lba); /* Read some blocks of audio data. Retry if necessary. This code can't recover from LOSS OF STREAMING errors that occur with some drives (those that don't support "accurate" CD-DA) but we should've detected such drives and aborted before now. */ for(tries = MAX_RETRIES; tries != 0; tries--) { if(scsi_read_cd(d, buf, j, lba) == 0) break; if(!scsi_request_sense(d, &sense_key, &asc, &ascq)) DEBUG( scsi_dump_sense(sense_key, asc, ascq)); } if(tries == 0) { printf("Error ripping audio sectors %lu-%lu\n", lba, lba + j); fclose(out); return 4; } if(fwrite(buf, 1, BYTES_PER_SECT * j, out) != BYTES_PER_SECT * j) { printf("Error writing output file '%s' (disk " "full?)\n", out_name); fclose(out); return 5; } lba += j; } (void)write_wav_header(out); fclose(out); return 0; } /***************************************************************************** Return values (exit codes): 1 usage 2 no CD drive detected, or drive doesn't support ripping audio, or no audio disc in any drive 3 invalid track number *****************************************************************************/ #define MAX_DRIVES 4 int main(int arg_c, char *arg_v[]) { static drive_t drive[MAX_DRIVES]; /**/ unsigned num_drives = 0, cmd_io, ctl_io, unit; char out_name[16] = "trackNN.wav", buf[96]; unsigned tries, sense_key, asc, j; int i, ascq; drive_t *d; /* Find an ATAPI drive. A 'real' ATA/ATAPI driver should get these I/O addresses from PCI, and use these addresses only if PCI is not present. primary interface: cmd_io=0x1F0, ctl_io=0x3F6 secondary interface: cmd_io=0x170, ctl_io=0x376 */ for(cmd_io = 0x1F0; cmd_io >= 0x170; cmd_io -= 0x80) { d = &drive[num_drives]; d->cmd_io = cmd_io; ctl_io = cmd_io + 0x206; d->ctl_io = ctl_io; /* reset both units on this interface (selects unit #0) b3=1, b2=SRST, b1=nIEN (enables interrupts when=0), b0=0. */ outportb(d->ctl_io, 0x0C); /* "Some devices may take up to 2 ms to set BSY when coming out of Sleep mode. The host shall not begin polling the Status register until at least 2 ms after the SRST bit has been set to one." -- ATA-4, section 9.3 */ delay(5); outportb(d->ctl_io, 0x08); delay(5); for(unit = 0; unit < 2; unit++) { d = &drive[num_drives]; d->unit = unit; /* continue looking for drives while... ...no ATAPI drive detected... */ if(!detect_atapi_drive(d)) continue; /* ...error resetting drive... */ if(atapi_reset(d)) { DEBUG( printf("Error resetting ATAPI device\n");) continue; } /* ...drive is ATAPI but not a CD... */ for(tries = MAX_RETRIES; tries != 0; tries--) { i = scsi_mode_sense(d, buf, sizeof(buf)); if(i == 0) break; if(!scsi_request_sense(d, &sense_key, &asc, &ascq)) DEBUG( scsi_dump_sense(sense_key, asc, ascq)); } if(tries == 0) continue; DEBUG( scsi_dump_page42(buf, sizeof(buf));) /* ...CD drive does not support "ripping" audio... */ if((buf[13] & 0x01) == 0) { printf("Error: CD drive does not support " "audio ripping\n"); continue; } #if 1 /* ...CD drive does not support "accurate" CD-DA; i.e. this error occurs: *** READ CD *** Error: SCSI command READ CD failed *** REQUEST SENSE *** sense key=11 (ABORTED COMMAND), ASC=191, ASCQ=0 (LOSS OF STREAMING) If you're getting this error, yes: you can comment out this code. But the .WAV file you get will be badly distorted. */ if((buf[13] & 0x02) == 0) { printf("Error: CD drive does not support " "accurate CD-DA\n"); continue; } #endif /* ...Table Of Contents can not be read... */ for(tries = MAX_RETRIES; tries != 0; tries--) { i = scsi_read_toc(d, d->track, MAX_TRACKS); if(i >= 0) { d->num_tracks = i; break; } if(!scsi_request_sense(d, &sense_key, &asc, &ascq)) DEBUG( scsi_dump_sense(sense_key, asc, ascq)); } if(tries == 0) { printf("Error: can't read table-of-contents" " (no disc in drive or not an " "audio CD or ?)\n"); continue; } /* success */ num_drives++; } } if(num_drives == 0) { printf("No usable CD-ROM drive detected\n"); return 2; } printf("\n"); // xxx - add logic to let user select drive d = &drive[0]; printf("Drive at I/O=0x%03X, unit=%u:\n", d->cmd_io, d->unit); scsi_dump_toc(d->track, d->num_tracks); /* set 'j' = track number */ if(arg_c < 2) { do { printf("Enter number (1-%u) of track to be " "'ripped' to a .WAV file:\n", d->num_tracks); if(fgets(buf, BUF_SIZE, stdin) == NULL) return 3; if((i = sscanf(buf, "%u", &j)) == EOF) return 3; else if(i != 1) continue; } while(j == 0 || j > d->num_tracks); } else { if((i = sscanf(arg_v[1], "%u", &j)) == EOF || i != 1 || j == 0 || j > d->num_tracks) { printf("Invalid track number %u -- must be 1-%u\n", j, d->num_tracks); return 3; } } /* rip a track (1 = first track) */ sprintf(out_name, "track%02u.wav", j); j--; return rip_track(d, out_name, &d->track[j]); }