New ioctl to support Enhanced CD (or Extra CD)

Jean-Sébastien Pédron dumbbell at FreeBSD.org
Thu Apr 9 07:01:44 PDT 2009


-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Hello,

Enhanced CD (or Extra CA) is an Audio CD with an additionnal data track
at the end. Audio tracks belong to the first session while the data
track belongs to the second session. Therefore the last audio track ends
way before the data track start.

The first consequence is that the duration of the last audio track isn't
reported correctly. Here is the output of cdcontrol(1) with such a CD[1]:
    $ cdcontrol info
    ...
    12  46:03.67   9:54.43  207142   44593  audio
    13  55:58.35   6:38.51  251735   29901   data

The expected output is:
    $ cdcontrol info
    ...
    12  46:03.67   7:22.43  207142   33193  audio
    13  55:58.35   6:38.51  251735   29901   data

A more "audible" consequence is that cdparanoia(1) copies 9'54" of data
instead of 7'22". The end of the ripped file is full of garbage.

I made a patch (attached) that adds a new ioctl to query the start
address of the last session. This new ioctl is named
CDIOREADLASTSESSIONADDR. The patch also includes changes to cdcontrol(1).

I added a new member at the end of struct acd_softc to store the last
session address. I don't know if this causes ABI breakage.

Linux' corresponding ioctl is CDROMMULTISESSION. Beside this address, it
returns a flag named "xa_flag". Currently, I don't understand what it is
but it may be useful to add it to our ioctl too if someone knows its
purpose.

Before I spend some time to teach cdparanoia(1) about this new ioctl,
I'd like some feedback on this patch, especially the name and the struct
ioc_read_last_session_addr. I would appreciate some test reports too! :)

[1] This was tested with Avishai Cohen's last album, "Aurora".

- --
Jean-Sébastien Pédron
http://www.dumbbell.fr/

PGP Key: http://www.dumbbell.fr/pgp/pubkey.asc

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (FreeBSD)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAknd+UIACgkQa+xGJsFYOlNzCgCeNz3PEoL7nqSsc1mMcKOBAYag
MUUAoIFVuhZ9lEPsvI17V3tRYCeOjvDM
=pUJY
-----END PGP SIGNATURE-----
-------------- next part --------------
Index: usr.sbin/cdcontrol/cdcontrol.c
===================================================================
--- usr.sbin/cdcontrol/cdcontrol.c	(revision 190463)
+++ usr.sbin/cdcontrol/cdcontrol.c	(working copy)
@@ -120,6 +120,7 @@
 };
 
 struct cd_toc_entry	toc_buffer[100];
+union msf_lba		last_session_addr;
 
 const char	*cdname;
 int		fd = -1;
@@ -1021,6 +1022,36 @@
 			e[1].addr.msf.frame);
 	else
 		next = ntohl(e[1].addr.lba);
+
+	if (!(e->control & 4) && e[1].control & 4) {
+		int end_lba;
+
+		/*
+		 * The current track is an audio track and the next one is
+		 * a data track: this is an Enhanced CD (or Extra CD). In
+		 * this case, the current track ends way before the data
+		 * track. We must consider the last_session_addr instead.
+		 */
+
+		if (last_session_addr.lba != 0) {
+			if (msf) {
+				end_lba = msf2lba(last_session_addr.msf.minute,
+				    last_session_addr.msf.second,
+				    last_session_addr.msf.frame);
+			} else {
+				end_lba = last_session_addr.lba;
+			}
+			end_lba -= (90 + 60 + 2) * 75;
+
+			if (end_lba >= block && end_lba < next)
+				next = end_lba;
+		} else {
+			warnx("couldn't query sessions informations; "
+			    "last audio track\neffective duration "
+			    "may be shorter than the reported duration!");
+		}
+	}
+
 	len = next - block;
 	/* Take into account a start offset time. */
 	lba2msf (len - 150, &m, &s, &f);
@@ -1069,14 +1100,28 @@
 
 int read_toc_entrys (int len)
 {
+	int ret;
 	struct ioc_read_toc_entry t;
+	struct ioc_read_last_session_addr lsa;
 
 	t.address_format = msf ? CD_MSF_FORMAT : CD_LBA_FORMAT;
 	t.starting_track = 0;
 	t.data_len = len;
 	t.data = toc_buffer;
 
-	return (ioctl (fd, CDIOREADTOCENTRYS, (char *) &t));
+	ret = ioctl (fd, CDIOREADTOCENTRYS, (char *) &t);
+	if (ret != 0)
+		return (ret);
+
+	lsa.address_format = msf ? CD_MSF_FORMAT : CD_LBA_FORMAT;
+
+	ret = ioctl (fd, CDIOREADLASTSESSIONADDR, (char *) &lsa);
+	if (ret == 0) {
+		memcpy(&last_session_addr, &lsa.addr,
+		    sizeof(last_session_addr));
+	}
+
+	return (0);
 }
 
 int play_msf (int start_m, int start_s, int start_f,
Index: sys/cam/scsi/scsi_cd.c
===================================================================
--- sys/cam/scsi/scsi_cd.c	(revision 190463)
+++ sys/cam/scsi/scsi_cd.c	(working copy)
@@ -253,7 +253,8 @@
 				u_int32_t sense_flags);
 static	int		cdreadtoc(struct cam_periph *periph, u_int32_t mode, 
 				  u_int32_t start, u_int8_t *data, 
-				  u_int32_t len, u_int32_t sense_flags);
+				  u_int32_t len, u_int8_t control,
+				  u_int32_t sense_flags);
 static	int		cdgetmode(struct cam_periph *periph, 
 				  struct cd_mode_params *data, u_int32_t page);
 static	int		cdsetmode(struct cam_periph *periph,
@@ -2122,7 +2123,8 @@
 				  ("trying to do CDIOREADTOCHEADER\n"));
 
 			error = cdreadtoc(periph, 0, 0, (u_int8_t *)th, 
-				          sizeof (*th), /*sense_flags*/0);
+				          sizeof (*th), /*control*/0,
+				          /*sense_flags*/0);
 			if (error) {
 				free(th, M_SCSICD);
 				cam_periph_unlock(periph);
@@ -2174,7 +2176,8 @@
 
 			th = &data->header;
 			error = cdreadtoc(periph, 0, 0, (u_int8_t *)th, 
-					  sizeof (*th), /*sense_flags*/0);
+					  sizeof (*th), /*control*/0,
+					  /*sense_flags*/0);
 			if (error) {
 				free(data, M_SCSICD);
 				free(lead, M_SCSICD);
@@ -2233,6 +2236,7 @@
 						  starting_track,
 						  (u_int8_t *)data,
 						  readlen + sizeof (*th),
+						  /*control*/0,
 						  /*sense_flags*/0);
 				if (error) {
 					free(data, M_SCSICD);
@@ -2250,6 +2254,7 @@
 				error = cdreadtoc(periph, te->address_format,
 						  LEADOUT, (u_int8_t *)lead,
 						  sizeof(*lead),
+						  /*control*/0,
 						  /*sense_flags*/0);
 				if (error) {
 					free(data, M_SCSICD);
@@ -2299,7 +2304,8 @@
 
 			th = &data->header;
 			error = cdreadtoc(periph, 0, 0, (u_int8_t *)th,
-					  sizeof (*th), /*sense_flags*/0);
+					  sizeof (*th), /*control*/0,
+					  /*sense_flags*/0);
 			if (error) {
 				free(data, M_SCSICD);
 				cam_periph_unlock(periph);
@@ -2331,6 +2337,7 @@
 
 			error = cdreadtoc(periph, te->address_format, track,
 					  (u_int8_t *)data, sizeof(*data),
+					  /*control*/0,
 					  /*sense_flags*/0);
 			if (error) {
 				free(data, M_SCSICD);
@@ -2346,6 +2353,96 @@
 			cam_periph_unlock(periph);
 		}
 		break;
+	case CDIOREADLASTSESSIONADDR:
+		/*
+		 * An "Enhanced CD" (or "Extra CD") is a CD with audio
+		 * tracks, then data tracks. Audio tracks are in the
+		 * first session, while data tracks are in the second
+		 * session. Because of the space taken by the first
+		 * session's lead-out and the second session's lead-in,
+		 * the duration of the last audio track can't be determined
+		 * with the address of the data track. Therefore, we need
+		 * to get the address of the second (and last) session for
+		 * this purpose.
+		 */
+		{
+			struct cd_toc_single *data;
+			struct ioc_read_last_session_addr *lsa =
+				(struct ioc_read_last_session_addr *) addr;
+			struct ioc_toc_header *th;
+
+			data = malloc(sizeof(*data), M_SCSICD, M_WAITOK);
+
+			cam_periph_lock(periph);
+			CAM_DEBUG(periph->path, CAM_DEBUG_SUBTRACE, 
+				  ("trying to do CDIOREADLASTSESSIONADDR\n"));
+
+			if (lsa->address_format != CD_MSF_FORMAT
+			    && lsa->address_format != CD_LBA_FORMAT) {
+				printf("error in readlastsessionaddr, "
+				       " returning EINVAL\n");
+				free(data, M_SCSICD);
+				error = EINVAL;
+				cam_periph_unlock(periph);
+				break;
+			}
+
+			th = &data->header;
+			error = cdreadtoc(periph, 0, 0, (u_int8_t *)th,
+					  sizeof (*th), /*control*/0,
+					  /*sense_flags*/0);
+			if (error) {
+				free(data, M_SCSICD);
+				cam_periph_unlock(periph);
+				break;
+			}
+
+			if (softc->quirks & CD_Q_BCD_TRACKS) {
+				/* we are going to have to convert the BCD
+				 * encoding on the cd to what is expected
+				 */
+				th->starting_track =
+				    bcd2bin(th->starting_track);
+				th->ending_track = bcd2bin(th->ending_track);
+			}
+
+			if (th->starting_track == LEADOUT) {
+				/* Linux set an LBA to 0'2" when this is not
+				 * a multisession CD. */
+				if (lsa->address_format == CD_MSF_FORMAT) {
+					lsa->addr.msf.minute = 0;
+					lsa->addr.msf.second = 2;
+					lsa->addr.msf.frame = 0;
+				} else {
+					lsa->addr.lba = 2 * 75 - 150;
+				}
+
+				free(data, M_SCSICD);
+				cam_periph_unlock(periph);
+				break;
+			}
+
+			error = cdreadtoc(periph, lsa->address_format, 0,
+					  (u_int8_t *)data, sizeof(*data),
+					  /*control*/1 << 6,
+					  /*sense_flags*/0);
+			if (error) {
+				free(data, M_SCSICD);
+				cam_periph_unlock(periph);
+				break;
+			}
+
+			if (lsa->address_format == CD_MSF_FORMAT) {
+				bcopy(&data->entry.addr, &lsa->addr,
+				    sizeof(lsa->addr));
+			} else {
+				lsa->addr.lba = ntohl(data->entry.addr.lba);
+			}
+
+			free(data, M_SCSICD);
+			cam_periph_unlock(periph);
+		}
+		break;
 	case CDIOCSETPATCH:
 		{
 			struct ioc_patch *arg = (struct ioc_patch *)addr;
@@ -2803,7 +2900,7 @@
 	 * we don't print anything here if we get an error back.
 	 */
 	error = cdreadtoc(periph, 0, 0, (u_int8_t *)toch, sizeof(*toch),
-			  SF_NO_PRINT);
+			  /*control*/0, SF_NO_PRINT);
 	/*
 	 * Errors in reading the table of contents aren't fatal, we just
 	 * won't have a valid table of contents cached.
@@ -2829,7 +2926,7 @@
 
 	error = cdreadtoc(periph, CD_MSF_FORMAT, toch->starting_track,
 			  (u_int8_t *)&softc->toc, toclen + sizeof(*toch),
-			  SF_NO_PRINT);
+			  /*control*/0, SF_NO_PRINT);
 	if (error != 0) {
 		error = 0;
 		bzero(&softc->toc, sizeof(softc->toc));
@@ -2849,7 +2946,7 @@
 
 		error = cdreadtoc(periph, CD_MSF_FORMAT, LEADOUT, 
 				  (u_int8_t *)&leadout, sizeof(leadout),
-				  SF_NO_PRINT);
+				  /*control*/0, SF_NO_PRINT);
 		if (error != 0) {
 			error = 0;
 			goto bailout;
@@ -3139,7 +3236,8 @@
  */
 static int 
 cdreadtoc(struct cam_periph *periph, u_int32_t mode, u_int32_t start, 
-	  u_int8_t *data, u_int32_t len, u_int32_t sense_flags)
+	  u_int8_t *data, u_int32_t len, u_int8_t control,
+	  u_int32_t sense_flags)
 {
 	struct scsi_read_toc *scsi_cmd;
 	u_int32_t ntoc;
@@ -3176,6 +3274,7 @@
 	scsi_cmd->data_len[1] = (ntoc) & 0xff;
 
 	scsi_cmd->op_code = READ_TOC;
+	scsi_cmd->control = control;
 
 	error = cdrunccb(ccb, cderror, /*cam_flags*/CAM_RETRY_SELTO,
 			 /*sense_flags*/SF_RETRY_UA | sense_flags);
Index: sys/dev/ata/atapi-cd.c
===================================================================
--- sys/dev/ata/atapi-cd.c	(revision 190463)
+++ sys/dev/ata/atapi-cd.c	(working copy)
@@ -400,6 +400,25 @@
 	}
 	break;
 
+    case CDIOREADLASTSESSIONADDR:
+	{
+	    struct ioc_read_last_session_addr *lsa =
+		(struct ioc_read_last_session_addr *)addr;
+
+	    if (!cdp->toc.hdr.ending_track) {
+		error = EIO;
+		break;
+	    }
+
+	    if (lsa->address_format == CD_MSF_FORMAT) {
+		lba2msf(cdp->last_session_lba, &lsa->addr.msf.minute,
+		    &lsa->addr.msf.second, &lsa->addr.msf.frame);
+	    } else {
+		lsa->addr.lba = cdp->last_session_lba;
+	    }
+	}
+	break;
+
 #if __FreeBSD_version > 600008
     case CDIOCREADSUBCHANNEL_SYSSPACE:
 	nocopyout = 1;
@@ -1006,6 +1025,39 @@
 	cdp->pp[track] = NULL;
     }
 
+    /*
+     * An "Enhanced CD" (or "Extra CD") is a CD with audio tracks, then
+     * data tracks. Audio tracks are in the first session, while data
+     * tracks are in the second session. Because of the space taken by
+     * the first session's lead-out and the second session's lead-in,
+     * the duration of the last audio track can't be determined with the
+     * address of the data track. Therefore, we need to get the address
+     * of the second (and last) session for this purpose.
+     */
+    if (cdp->toc.hdr.starting_track != 170) {
+	struct toc last_toc;
+
+	len = sizeof(struct ioc_toc_header) + sizeof(struct cd_toc_entry);
+	bzero(ccb, sizeof(ccb));
+	ccb[0] = ATAPI_READ_TOC;
+	ccb[7] = len >> 8;
+	ccb[8] = len;
+	/* Set this flag to ask for sessions informations instead of tracks
+	 * informations. */
+	ccb[9] = 1 << 6;
+
+	if (ata_atapicmd(dev, ccb, (caddr_t)&last_toc, len,
+	    ATA_R_READ | ATA_R_QUIET, 30)) {
+	    bzero(&cdp->toc, sizeof(cdp->toc));
+	    return;
+	}
+
+	cdp->last_session_lba = ntohl(last_toc.tab[0].addr.lba);
+    } else {
+	/* Linux set an LBA to 0'2" when this is not a multisession CD. */
+	cdp->last_session_lba = msf2lba(0, 2, 0);
+    }
+
 #ifdef ACD_DEBUG
     if (cdp->disk_size && cdp->toc.hdr.ending_track) {
 	device_printf(dev, "(%d sectors (%d bytes)), %d tracks ", 
Index: sys/dev/ata/atapi-cd.h
===================================================================
--- sys/dev/ata/atapi-cd.h	(revision 190463)
+++ sys/dev/ata/atapi-cd.h	(working copy)
@@ -312,4 +312,7 @@
     u_int32_t                   iomax;          /* Max I/O request (bytes) */
     struct g_geom               *gp;            /* geom instance */
     struct g_provider           *pp[MAXTRK+1];  /* providers */
+    int                         last_session_lba; /* last session address (see
+                                                     CDIOREADLASTSESSIONADDR
+                                                     ioctl) */
 };
Index: sys/sys/cdio.h
===================================================================
--- sys/sys/cdio.h	(revision 190463)
+++ sys/sys/cdio.h	(working copy)
@@ -281,4 +281,12 @@
  */
 #define CDIOCREADSUBCHANNEL_SYSSPACE _IOWR('c', 31, struct ioc_read_subchannel)
 
+
+struct ioc_read_last_session_addr {
+	u_char	address_format;
+	union	msf_lba addr;
+};
+#define CDIOREADLASTSESSIONADDR						\
+    _IOWR('c', 32, struct ioc_read_last_session_addr)
+
 #endif /* !_SYS_CDIO_H_ */


More information about the freebsd-current mailing list