NAME
Linux::CDROM cookbook - common recipes featuring your CDROM drive as its main ingredient
DESCRIPTION
There's a gazillion ways of reading the disc inside your CDROM drive. The most high-level ones would be mounting your CD and using it as a normal directory or - in case of an Audio-CD - using a player to play tracks. This is boring stuff and you don't need Linux::CDROM
for any of that.
But when you want to write your own CD-player or -grabber, this is more like it. You can even get at a lower level than that.
PLAYING AUDIO
Linux::CDROM
offers a couple of methods dealing with that. For starting playback, you will use either Linux::CDROM::play_ti
(ti == track index) or Linux::CDROM::play_msf
(msf == minute, second and frame).
All playing operations happen non-blockingly. That means, you start playback and your program does not wait till the playback is done. Instead if will proceed with the next line.
Recipe 1: Playing all tracks on a CD one after the other
use Linux::CDROM; my $cd = Linux::CDROM->new("/dev/cdrom") or die $Linux::CDROM::error; $cd->play_msf( Linux::CDROM::Addr->new(CDROM_LBA, 0), Linux::CDROM::Addr->new(CDROM_LBA, $cd->num_frames) );
Since we want to play from the beginning to the end, we don't care about any minute, second or frame and so it's most convenient to start with frame 0 and end with the last one (as returned by
$cd->num_frames
.Recipe 2: Playing particular tracks
use Linux::CDROM: my $cd = Linux::CDROM->new("/dev/cdrom") or die $Linux::CDROM::error; $cd->play_ti( -from => 2, -to => 4 );
As you can see,
$cd->play_ti
is the right tool when you want to access the data track-wise. For playing back just one track, make sure that the value of -from and -to are the same.Recipe 3: Playing a range on the CD
use Linux::CDROM: my $cd = Linux::CDROM->new("/dev/cdrom") or die $Linux::CDROM::error; my $start = Linux::CDROM::Addr->new( CDROM_MSF, 0, 1, 0 ); my $end = Linux::CDROM::Addr->new( CDROM_MSF, 10, 10, 0); $cd->play_msf( $start, $end );
The above will play the first 10 minutes and 10 seconds of the audio. Note that the numbering of minutes begins with 0 whereas seconds always start at 1.
Recipe 4: Ask the drive where it is currently playing
You will want to do something similar to that when you want to write your own CD-player. Everyone loves process-counters and -bars:
use Linux::CDROM; my $cd = Linux::CDROM->new("/dev/cdrom") or die $Linux::CDROM::error; $| = 1; # play whole CD again $cd->play_msf( Linux::CDROM::Addr->new(CDROM_LBA, 0), Linux::CDROM::Addr->new(CDROM_LBA, $cd->num_frames) ); my $frames = $cd->num_frames; while () { my $poll = $cd->poll; break if $poll->status != CDROM_AUDIO_PLAY; my $track = $poll->track; my $absaddr = $poll->abs_addr; my $reladdr = $poll->rel_addr; printf "\r%02i:%02i:%02i of track %02i |", $reladdr->as_msf, $track; my $offset = int($absaddr->addr->as_lba / $frames * 30); print "-" x $offset, ">", " " x (30 - $offset), "|"; }
The above will produce an output similar to
03:12 of track 02 |--------------> |
and will update it constantly. The progress-bar indicates the total amount of audio played of the whole CD whereas the time-counter shows the position in the current track.
Recipe 5: Pausing, resuming etc. of playback
Since both
Linux::CDROM::play_msf
andLinux::CDROM::play_ti
work non-blockingly, you can easily defer playback with theLinux::CDROM::pause
,Linux::CDROM::stop
and continue it withLinux::CDROM::resume
:use Linux::CDROM; use Term::ReadKey; my $cd = Linux::CDROM->new("/dev/cdrom") or die $Linux::CDROM::error; ReadMode 'raw'; $cd->play_ti( -from => 1, -to => 8 ); { while (not defined (my $key = ReadKey(-1))) { print "\r", get_status(); select undef, undef, undef, 0.2; } $cd->pause if $key eq 'p'; $cd->resume if $key eq 'r'; $cd->stop and last if $key eq 's'; last if $key eq 'q'; redo; } ReadMode 'restore'; sub get_status { my $poll = $cd->poll; my $track = $poll->track; my ($min, $secs) = $poll->rel_addr->as_msf; return sprintf "Track %02i at [%02i:%02i]", $track, $min, $secs; }
The above is actually already a useable little CD-player. It updates its status and can be controlled through single key-strokes 'p', 'r', 's' and 'q'. When hitting 'q' the CD will keep playing.
All events are processed in two nested infinite loops.
Recipe 6: A forking player
The program in Recipe 5 can also be written using
fork
. In the case ofLinux::CDROM
this is actually quite an attractive approach since you may create an object in both your parent and your child process and access them in both. That means that your two processes do not need to be connected through pipes or so.use Linux::CDROM; use Term::ReadKey; my $cd = Linux::CDROM->new( "/dev/cdrom" ) or die $Linux::CDROM::error; my $child = fork; if (! defined $child) { die "Oups, couldn't fork: $!"; } elsif ($child) { parent(); } else { child(); } sub parent { $cd->play_ti( -from => 1, -to => 8 ); ReadMode 'raw'; { my $key; while (not defined ($key = ReadKey(-1))) { # do nothing this time: child handles status display select undef, undef, undef, 0.2; } $cd->pause if $key eq 'p'; $cd->resume if $key eq 'r'; $cd->stop if $key eq 's'; if ($key eq 'q') { kill HUP => $child; # tell our child that we quit wait; # wait for it ReadMode 'restore'; exit; } redo; } } sub child { $SIG{ HUP } = sub { exit }; $| = 1; my $cd = Linux::CDROM->new("/dev/cdrom"); while () { my $poll = $cd->poll; my $track = $poll->track; my ($min, $secs) = $poll->rel_addr->as_msf; printf "\rTrack %02i at [%02i:%02i]", $track, $min, $secs; select undef, undef, undef, 0.2; } }
The only message that can be exchanged between parent process and child is sighup which the parent process will send when it quits. The child has installed a handler for this signal in
$SIG{ HUP }
.Recipe 7: Length of the last track
For calculating the length of a given track i, you'd usually substract the address of track of track i from the address of track i+1. However, you cannot do this for the last track since there is no such i+1 in this case.
Every CD (even non-Audio CDs) have a special track called Leadout which is always the last physical track on a CD. It has the index
CDROM_LEADOUT
which is usually defined to be0xAA
. That means that you have to substract the address of the last track from the address of the Leadout track:use Linux::CDROM: my $cd = Linux::CDROM->new("/dev/cdrom") or die $Linux::CDROM::error; my ($first, $last) = $cd->toc; $last = $last - $first + 1; my $length = $cd->toc_entry(CDROM_LEADOUT)->addr - $cd->toc_entry($last)->addr; printf "Length of track $track: %i frames", $length->as_lba;
GRABBING AUDIO
Grabbing audio data happens through Linux::CDROM::read_audio
. It returns a string of CD_FRAMESIZE_RAW bytes. These data are simply PCM-encoded samples as you find them in WAV files.
However, simply dumping these data to a file wont give you a playable WAV file. The file is lacking an appropriate WAV header that would tell your player how to interpret the PCM-data (as for number of channels, bitrate, sampling-frequency). Linux::CDROM
contains the static method Linux::CDROM::Format->wav_header
to write a header suitable for these PCM data.
Recipe 8: Making a WAV file from an Audio track
The WAV header is the first thing in a WAV file, but in this recipe we will write it after grabbing the data to get the byte-count of the data right. We'll leave a hole at the beginning of the file large enough to hold the header:
use Linux::CDROM: use Fcntl qw/:seek/; my $cd = Linux::CDROM->new( "/dev/cdrom" ) or die $Linux::CDROM::error; my $entry1 = $cd->toc_entry(1); my $entry2 = $cd->toc_entry(2); open WAV, ">track1.wav" or die $!; binmode WAV; # leave room for WAV header (44 bytes) seek WAV, 44, SEEK_SET; $cd->reset_datasize: for ($entry1->addr->as_lba .. $entry2->addr->as_lba-1) { print WAV $cd->read_audio( Linux::CDROM::Addr->new(CDROM_LBA, $_), 1); } # insert WAV header suitable for this track seek WAV, 0, SEEK_SET; print WAV Linux::CDROM::Format->wav_header( $cd->get_datasize );
Note the preliminary call to
$cd->reset_datasize
which resets the internal byte-counter. When grabbing is done (or at any point you wish), the amount of bytes read can be retrieved with$cd->get_datasize
.After the above you have a valid WAV file with 16bit, 44.1kHz and two channels (stereo).
GRABBING DATA
The most common scenario is making an ISO-image from a CD. The first thing to understand is that one iso-image doesn't necessarily represent the whole CD. An ISO-image represent always one session on a CD so that only a single-session data CD fits into one image. If you have a multisession CD, you'd create an image for each session.
Recipe 9: Making ISO images from a CD
This one collects all data-tracks on the CD and makes an ISO-image out of each:
use Linux::CDROM; my $cd = Linux::CDROM->new("/dev/cdrom") or die $Linux::CDROM::error; # collect all data-tracks my @data_tracks; my ($first, $last) = $cd->toc; if ($cd->is_multisession) { foreach ($first .. $last) { push @data_tracks, $_ if $cd->toc_entry($_)->is_data; } } if (! @data_tracks) { die "No grabbable data tracks found"; } grab_track($_) foreach @data_tracks; sub grab_track { my $track = shift; my $start = $cd->toc_entry($track); my $end = $cd->toc_entry($track == $last ? CDROM_LEADOUT : $track + 1); open ISO, ">/tmp/track$track.iso" or die $!; binmode ISO; for ($start->as_lba .. $end->as_lba - 1) { print ISO $cd->read1( Linux::CDROM::Addr->new(CDROM_LBA, $_) ); } close ISO; }
You can see that this is very similar to grabbing Audio tracks. In both cases you need the start and the end of the track. The result is a file that can be burned onto a CD-R. On a Linux system (and maybe on others as well), you can mount the ISO-image as a regular medium:
mount track1.iso /mnt/mountpoint -t iso9660 -o ro,loop=/dev/loop0
DOING EVEN MORE
Linux::CDROM
offers a fairly generous subset of what your kernel permits. There are however a few things not (yet) implemented. For instance, DVD-handling and some of the more obscure things.
Recipe 10: Hand-rolling ioctls
This needs a little bit of explanation maybe. This whole module is essentially just a huge pile of ioctl-calls. Everything about a CD is controlled that way. This means that you can achieve the very same functionality this module offers with Perl's
ioctl
.Linux::CDROM
comes with its ownioctl
and you should use this instead because it does not require you to create a filehandle. Instead it uses its own.Suppose you don't like
Linux::CDROM::toc
a lot and instead want to do it by hand. First have a look at your local cdrom.h file. The ioctl you will need is#define CDROMREADTOCHDR 0x5305 /* Read TOC header (struct cdrom_tochdr) */
This also tells you that
struct cdrom_tochdr
is somewhat involved here. This one might look like this:/* This struct is used by the CDROMREADTOCHDR ioctl */ struct cdrom_tochdr { __u8 cdth_trk0; /* start track */ __u8 cdth_trk1; /* end track */ };
What you don't know (you have to guess) is that CDROMREADTOCHDR takes no argument but only returns one...through the above
struct cdrom_tochdr
. Here's the complete code:use Linux::CDROM qw(:all); my $cd = Linux::CDROM->new("/dev/cdrom") or die $Linux::CDROM::error; $cd->ioctl(CDROMREADTOCHDR, my $buf); # result now stored in $buf # need to unpack it accordingly to the C-structure my ($first, $last) = unpack "C C", $buf;
The
unpack
pattern"C C"
tells perl to treat the string in$buf
as two unsigned char values becausestruct cdrom_tochdr
is a structure of two__u8
values.The next one is a little more tricky since we also have to pack some arguments into a string. Suppose we want to duplicate
my $track = $cd->toc_entry(1);
Again the needed parts from
cdrom.h
:#define CDROMREADTOCENTRY 0x5306 /* Read TOC entry (struct cdrom_tocentry) */ ... /* Address in MSF format */ struct cdrom_msf0 { __u8 minute; __u8 second; __u8 frame; }; /* Address in either MSF or logical format */ union cdrom_addr { struct cdrom_msf0 msf; int lba; }; /* This struct is used by the CDROMREADTOCENTRY ioctl */ struct cdrom_tocentry { __u8 cdte_track; __u8 cdte_adr :4; __u8 cdte_ctrl :4; __u8 cdte_format; union cdrom_addr cdte_addr; __u8 cdte_datamode; };
The tricky part is
union cdrom_addr cdte_addr
. Since it is a union, it can hold two different values. The two possible types are to be found inunion cdrom_addr
: It's either astruct cdrom_msf0
or an integer.You need to tell your kernel which of these two alternatives you want by setting
cdrom_tocentry.cdte_format
to either CDROM_LBA or CDROM_MSF.The other real problem with
struct cdrom_tocentry
is the fact that it contains two bit-fields (two 4-bit wide fields require 1 byte) and that you must consider padding. On most machines, padding happens on 4-byte boundaries. If that is the case, the structure will have these byte offsets if we assume that your integers are 4 bytes wide and that the two bit-fields immediatly followcdte_track
(which is not necessarily the case):byte 0 cdte_track cdte_adr # these two need cdte_ctrl # one byte together cdte_format <padding byte> byte 4 cdte_addr byte 8 cdte_datamode
Now that we have figured out a hopefully sane memory layout, we can pack the buffer so that the ioctl hopefully returns the information belonging to track 2 in LBA format. Note that only
cdte_track
andcdte_format
need to be set. All other slots are only used for the return values:my $buf = pack "C # cdte_track C # cdte_adr + cdte_ctrl C # cdte_format x # <padding byte> i # cdte_addr C # cdte_datamode ", 2, 0, CDROM_LBA; $cd->ioctl(CDROMREADTOCENTRY, $buf); my ($track, $adr_ctrl, $format, $lba, $mode) = unpack "CCCxiC", $buf; print "Track $track starts at frame $lba";
The same in MSF would look like this:
my $buf = pack "CCCxCCCxC", 2, 0, CDROM_MSF; $cd->ioctl(CDROMREADTOCENTRY, $buf); my ($track, $adr_ctrl, $format, $min, $sec, $frame, $mode) = unpack "CCCxCCCxC", $buf;
The
"i"
from the LBA example had to be turned into"CCCx"
because our unioncdte_addr
union cdrom_addr { struct cdrom_msf0 msf; int lba; };
now stores the address in the
msf
slot which looks like this:struct cdrom_msf0 { __u8 minute; __u8 second; __u8 frame; };
It is three bytes wide, therefore we have another padding byte.
SEE ALSO
Device::CDROM for a reference to all methods and classes.
AUTHOR
Tassilo von Parseval, <tassilo.von.parseval@rwth-aachen.de>
COPYRIGHT AND LICENSE
Copyright (C) 2004 by Tassilo von Parseval
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.2 or, at your option, any later version of Perl 5 you may have available.