#!/usr/bin/perl use strict; use warnings; use Getopt::Long qw(:config no_ignore_case bundling); use File::Path; use Pod::Usage; use Cwd ('abs_path'); our $VERSION = 1.027_000; =pod =head1 NAME mp3cd - Burns normalized audio CDs from lists of MP3s/WAVs/Oggs/FLACs =head1 SYNOPSIS mp3cd [OPTIONS] [playlist|files...] -s, --stage STAGE Start at a certain stage of processing: clean Start fresh (default, requires playlist) build Does not clean (requires playlist) decode Turns MP3s/Oggs/FLACs into WAVs correct Fix up any WAV formats norm Normalizes WAV volumes toc Builds a Table of Contents from WAVs toc_ok Checks TOC validity cdr_ok Checks for a CDR burn Burns from the TOC -q Quits after one stage of processing -t, --tempdir DIR Set working dir (default "/tmp/mp3cd-$USER") -d, --device PATH Look for CDR at "PATH" (default "/dev/cdrecorder") -r, --driver TYPE Use CDR driver TYPE (default up to cdrdao) -n, --simulate Don't actually burn a disc but do everything else. -E, --no-eject Don't eject drive after the burn. -L, --no-log Don't redirect output to "tool-output.txt" -T, --no-cd-text Don't attempt to write CD-TEXT tags to the audio CD -c, --cdrdao ARGS Pass the option string ARGS to cdrdao. -S, --skip STAGES Skip the comma-separated list of stages in STAGES. -V, --version Report which version of the script this is. -v, --verbose Shows commands as they are executed. -h, --usage Shows brief usage summary. --help Shows detailed help summary. --longhelp Shows complete help. =head1 OPTIONS =over 8 =item B<-s STAGE>, B<--stage STAGE> Starts processing at a given stage. This is used in case you had to stop processing, or a file was missing, or things generally blew up. It is especially useful if a burn fails because then you don't have to start totally over and re-WAV the files. If you just want to perform a single step, use B<--quit> to abort after the stage you request with B<--stage>. Also see B<--skip>. =over 8 =item B This is the default starting stage. The temp directory is cleared out. A playlist is required, since we expect to move to the B stage next, which requires it. =item B This stage examines the playlist from the command line, and tries to create a list of symlinks from the given playlist. So far, C can understand ".m3u" files, XMLPlaylist files, and lists of files. =item B All the files are converted into WAVs. So far, C knows how to decode MP3, Ogg, and FLAC files. (WAVs will be left as they are during this stage.) =item B The WAV files are corrected to have the correct bitrate and number of channels, as required for an audio CD. =item B The WAV files' volumes are normalized so any large differences in volume between records will be less noticeable. =item B Generates a Table of Contents for the audio CD. =item B Validates the TOC, just in case something went really wrong with the WAV files. =item B Verifies that there is a CDR ready for burning. =item B Actually performs the burn of all the WAV files to the waiting CDR. =back =item B<-q>, B<--quit> Aborts after one stage of processing. See B<--stage>. =item B<-t DIR>, B<--tempdir DIR> Use a working directory other than "/tmp/mp3cd-B". This is where all the file processing occurs. You will generally need at least 650M free here (or more depending on the recording length of your destination CD). =item B<-d PATH>, B<--device PATH> Use a device path other than "/dev/cdrecorder". =item B<-r TYPE>, B<--driver TYPE> Use a CDRDAO driver other than what cdrdao automatically detects. Note that some drivers may not support CD-TEXT mode. In this case, try "generic-mmc-raw". =item B<-c ARGS>, B<--cdrdao ARGS> Pass the given option string of ARGS to cdrdao during each command. =item B<-n>, B<--simulate> Do not actually write to the disc but simulate the process instead. =item B<-E>, B<--no-eject> Don't eject drive after the burn. =item B<-L>, B<--no-log> Don't redirect output to "tool-output.txt". All information will instead be redirected to the terminal via standard output (STDOUT). This will cause a lot of low-level detail to be displayed. =item B<-T>, B<--no-cd-text> Don't attempt to write CD-TEXT tags to the audio CD. Some devices and drivers do not support this mode. See B<--driver> for more details. =item B<-S STAGES>, B<--skip STAGES> While processing, skips the stages listed in the comma-separated list of stages given in STAGES. This would only be used if you really know what you're doing. For example, if the audio is already normalized and you didn't want to burn a CD, you could skip the normalizing and burning stages by giving "--skip norm,burn". See B<--stage> and B<--quit>. =item B<-V>, B<--version> Report which version of mp3cd this is. =item B<-v>, B<--verbose> Shows commands as they are executed. =item B<-h>, B<--usage> Show brief usage summary. =item B<--help> Show detailed help summary. =item B<--longhelp> Shows the full command line instructions. =back =head1 DESCRIPTION This script implements the suggested methods outlined in the Linux MP3 CD Burning mini-HOWTO: L This will burn a playlist (.m3u, XMLPlaylist or command line list) of MP3s, Oggs, FLACs, and/or WAVs to an audio CD. The ".m3u" format is really nothing more than a list of fully qualified filenames. The script handles making the WAVs sane by resampling if needed, and normalizing the volume across all tracks. If a failure happens, earlier stages can be skipped with the '-s' flag. The file "tool-output.txt" in the temp directory can be examined to see what went wrong during the stage. Some things are time-consuming (like decoding the audio into WAVs) and if the CD burn fails, it's much nicer not to have to start over from scratch. When doing this, you will not need the m3u file any more, since the files have already been built. See the list of stages using '-h'. =head1 PREREQUISITES Requires C, and that /dev/cdrecorder is a valid symlink to the /dev/sg device that cdrdao will use. Use .cdrdao to edit driver options. (See "man cdrdao" for details.) Requires C to decode MP3 and check/correct WAV formats. http://www.spies.com/Sox/ Requires C to process the audio. http://www.cs.columbia.edu/~cvaill/normalize/ Optionally requires C to decode Ogg to WAV files. http://www.gnu.org/directory/audio/ogg/OggEnc.html/ Optionally requires C to decode flac to WAV files. http://flac.sourceforge.net/ Optionally requires C Perl module if you want to use the .mp3cdrc file. http://search.cpan.org/~sherzodr/Config-Simple/ =head1 FILES =over 8 =item B<~/.mp3cdrc> Default options can be recorded in this file. The option names are the same as their command line long-name. Command line options will override these values. All options are run through perl's eval. For example: tempdir: /scratch/mp3cd/$ENV{'USER'} device: /dev/burner =back =head1 AUTHOR Kees Cook Contributors: J. Katz (Ogg support) Alex Rhomberg (XMLPlaylist support) Kevin C. Krinke (filelist inspiration, and countless many patches) James Greenhalgh (flac support) =head1 SEE ALSO perl(1), cdrdao(1), sox(1), oggdec(1), flac(1), sox(1), normalize(1). =head1 COPYRIGHT Copyright (C) 2003-2011 Kees Cook kees@outflux.net, http://outflux.net/ 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. http://www.gnu.org/copyleft/gpl.html =cut # Change this to a location where you'll have at least a CD's worth of # disk space available. (For the WAVs) # Its contents will be deleted, so be careful. :) my $BURNDIR="/tmp/mp3cd-".getpwuid($<); # Filename to redirect sub-tool stdout/stderr my $LOG="tool-output.txt"; # Filename to write the TOC to my $CDTOC="cdda.toc"; # Filename to write tag info to my $TAGS="tag.data"; # List of audio files to burn (useful only for the "build" stage) my @FILES=(); my %stage_func = ( "clean" => \&Do_Clean, "build" => \&Do_Build, "decode" => \&Do_Decode, "correct" => \&Do_Correct, "norm" => \&Do_Normalize, "toc" => \&Do_TOC, "toc_ok" => \&Do_TOC_Verify, "cdr_ok" => \&Do_CDR_Check, "burn" => \&Do_Burn, ); my $UNKNOWN="unknown-format"; my %decoders = ( "flac" =>{ 'require' => 'flac', 'args' => '--silent -d -F $input -o $output', 'normal' => '--silent', 'verbose' => '', }, "ogg" => { 'require' => 'oggdec', 'args' => '$input -o $output', 'normal' => '--quiet', 'verbose' => '', }, "mp3" => { 'require' => 'sox', 'args' => '$input $output', 'normal' => '', 'verbose' => '-v', }, "m4a" => { 'require' => 'faad', 'args' => '-o $output $input', 'normal' => '--quiet', 'verbose' => '', }, $UNKNOWN => { 'require' => 'mplayer', 'args' => '-hardframedrop -vc null -vo null -ao pcm:fast:file=$output $input', 'normal' => '-quiet', 'verbose' => '', }, # Dummy entry to recognize WAVs "wav" => { 'require' => 'sox', }, ); my @stages; my %stages; my $count=0; my $stage; foreach $stage (qw(clean build decode correct norm toc toc_ok cdr_ok burn)) { push(@stages,$stage); $stages{$stage}=$count++; } our $opt_help=undef; our $opt_longhelp=undef; our $opt_usage=undef; our $opt_version=undef; our $opt_quit=undef; our $opt_stage="clean"; our $opt_tempdir=undef; our $opt_cdrdao=""; our $opt_device="/dev/cdrecorder"; our $opt_driver=undef; our $opt_simulate=undef; our $opt_no_eject=0; our $opt_no_log=0; our $opt_no_cd_text=0; our $opt_skip=""; our $opt_verbose=0; my @options=( 'help', 'longhelp', 'usage|h', 'version|V', 'verbose|v', 'stage|s=s', 'skip|S=s', 'quit|q', 'tempdir|t=s', 'device|d=s', 'driver|r=s', 'cdrdao|c=s', 'simulate|n', 'no-eject|E', 'no-log|L', 'no-cd-text|T', ); # Look for RC defaults my %rc; my $rcfile="$ENV{'HOME'}/.mp3cdrc"; if (-r $rcfile) { require Config::Simple; Config::Simple->import_from($rcfile,\%rc); } foreach my $opt (@options) { my ($name) = $opt =~ /^([^|]+)/; $name=~s/-/_/g; my $is_str = $opt =~ /=s$/ || 0; if (defined($rc{$name})) { eval "\$opt_$name = \"$rc{$name}\";"; if (!$is_str) { eval "\$opt_$name = \$opt_$name ? 1 : 0;"; } } } # Load command line options GetOptions(@options) or pod2usage( -exitval=>1, -verbose=>0 ); # Handle help/usage pod2usage( -exitval=>0, -verbose=>2 ) if ($opt_longhelp); pod2usage( -exitval=>0, -verbose=>1 ) if ($opt_help); pod2usage( -exitval=>0, -verbose=>0 ) if ($opt_usage); Version() if ($opt_version); # cdrdao needs to pick up device and driver from the command line $opt_cdrdao .= " --device $opt_device"; $opt_cdrdao .= " --driver $opt_driver" if (defined($opt_driver)); # Validate starting stage if (!defined($stages{$opt_stage})) { pod2usage( -exitval=>1, -verbose=>0, -msg=>"Unknown start stage '$opt_stage'!" ); } $stage=$opt_stage; # Check if we need (or do not need) a playlist/filelist if ($stage eq "clean" || $stage eq "build") { if (!defined($ARGV[0])) { pod2usage( -exitval=>1, -verbose=>0, -msg=>"Playlist/File list is required!" ); } } elsif (@ARGV) { pod2usage( -exitval=>1, -verbose=>0, -msg=> "Playlists/Files are ignored past stage 'build'!" ); } # Build a hash of the stages to skip my %skip_stage; foreach my $skip (split(/,/,$opt_skip)) { if (!defined($stages{$skip})) { pod2usage( -exitval=>1, -verbose=>0, -msg=>"Unknown stage to skip '$skip'!" ); } $skip_stage{$skip}=1; } # Skip all the stages after the selected one, in case of "--quit" my $cancel_rest = 0; foreach my $last (@stages) { if ($cancel_rest) { $skip_stage{$last}=1; } if ($opt_quit && $last eq $stage) { $cancel_rest = 1; } } # Figure out our burning directory $BURNDIR=$opt_tempdir if (defined($opt_tempdir)); # check for directory if (!opendir(DIR, $BURNDIR)) { eval { mkpath($BURNDIR) }; if ($@) { die "Can't create working directory '$BURNDIR': $@\n"; } opendir(DIR, $BURNDIR) || die "Can't open directory '$BURNDIR': $!\n"; } closedir DIR; # if no_log print all to stdout my $OUTPUT = ( $opt_no_log ) ? "" : ">>$LOG"; sub System { my $cmd = $_[0]; print STDERR $cmd."\n" if $opt_verbose; return system($cmd); } sub Backtick { my $cmd = $_[0]; print STDERR $cmd."\n" if $opt_verbose; # Cannot pipe to "tee" since it will mask exit codes my $output = `$cmd 2>&1`; my $rc = $?; my $logfile; open($logfile, ">>$LOG") or die "Cannot write to $LOG: $!\n"; print $logfile $output; close($logfile); print $output if ($opt_no_log); return $rc, $output; } # For-sure needed tools my %PREREQS = ( 'sox' => 'sox', 'cdrdao' => 'cdrdao', 'gst-launch' => 'gst-launch', ); $PREREQS{'normalize'} = 'normalize,normalize-audio' if (!defined($skip_stage{'norm'})); my %found; sub Lookup_tools { # check for required tools foreach my $requirement (sort keys %PREREQS) { foreach my $dir (split(/:/,$ENV{'PATH'})) { foreach my $prog (split(/,/,$PREREQS{$requirement})) { if (!defined($found{$requirement}) && -x "$dir/$prog") { $found{$requirement}="$dir/$prog"; last; } } } } my $abort=undef; foreach my $requirement (sort keys %PREREQS) { if (!defined($found{$requirement})) { my $tried = "Tried: ".$PREREQS{$requirement}; $tried =~ s/,/, /g; warn "Cannot find program to handle '$requirement'! $tried\n"; $abort=1; } } return $abort; } # Load file list, update needed tools Load_file_list(); pod2usage( -exitval => 1, -verbose => 0 ) if (Lookup_tools()); # check for CDR device my $skip_cdr = defined($skip_stage{'cdr_ok'}) && defined($skip_stage{'burn'}); if (!$skip_cdr && ! -w $opt_device) { pod2usage( -exitval=>1, -verbose=>0, -msg=> "Cannot write to '$opt_device'!" ); } # Run through all the stages we need to... for (; defined($stage) && defined($stages{$stage}); $stage=$stages[$stages{$stage}+1]) { if (defined($skip_stage{$stage})) { print "Skipping '$stage' stage...\n"; next; } $stage_func{$stage}->(); } # end of line exit(0); ### Functions sub require_extension($$) { my ($ext,$file) = @_; my $lookup = $ext; if (!defined($decoders{$lookup})) { # Unknown audio file format print STDERR "Not sure how to handle file type '$ext' ($file),\n"; print STDERR "falling back to ".$decoders{$UNKNOWN}->{'require'}.".\n"; $lookup = $UNKNOWN; } $PREREQS{"decoder:$lookup"}=$decoders{$lookup}->{'require'}; } sub Load_file_list { # Keep a count of how many files we've examined, and stop after, say, # 1000, in case an m3u lists itself (which is REALLY unlikely, but would # effectively put this code into a memory-eating endless loop). my $toomany=1000; while (my $file=shift @ARGV) { $file =~ m/\.([^\.]+)$/i; my $ext = lc($1 || ""); if ($ext eq "m3u" || $ext eq "pls" || $ext eq "xspf" || $ext eq "") { # Playlist open(M3U,$file) || die "Cannot open '$file': $!\n"; my @lines=; close(M3U); my @files; if (scalar(@lines) && $lines[0] =~ //i) { # kaffeine playlists require XML::Simple; my $contents = XML::Simple::XMLin($file); if (ref($contents->{entry}) eq 'ARRAY') { @files = map {$_->{url}} @{$contents->{entry}}; s/^file:// for @files; } else { @files = ($contents->{entry}->{url}); } } else { # regular list of files foreach (@lines) { chomp; next if (/^#/); push(@files,$_); } } unshift(@ARGV,@files); } else { require_extension($ext,$file); push(@FILES,$file); } die ">1000 files in the list?! I must have started looping forever.\n" if (--$toomany<0); } # Get absolute locations @FILES = map { abs_path($_) } @FILES; } sub Do_Clean { print "Cleaning up...\n"; # clear out burn dir my @list = ("$BURNDIR/$CDTOC","$BURNDIR/$LOG", "$BURNDIR/$TAGS"); foreach my $ext ("wav", sort keys %decoders) { push(@list,"$BURNDIR/*.$ext"); } System("rm -f ".join(" ",@list)); } sub append_tag_info($$$) { my ($media, $title, $path) = @_; my $artist = ""; my ($rc, $output) = Backtick("gst-launch -t filesrc location=$media ! decodebin"); die "Could not extract tags: $!\n" if ($rc != 0); my $tags = 0; # Parse gst-launch -t output # FOUND TAG : found by element "qtdemux0". # title: Just Dance # artist: Lady GaGa & Colby O'Donis foreach my $line (split("\n", $output)) { if ($line =~ /^FOUND TAG/) { $tags = 1; next; } next if ($tags != 1); if ($line =~ /^\S/) { $tags = 0; next; } my ($field, $value) = $line =~ /^\s*(\S*)\s*:\s*(.*)$/; next if (!defined($field)); $title=$value if ($field eq "title"); $artist=$value if ($field eq "artist"); } my $tagfile; open($tagfile,">>$TAGS") or die "Cannot write to $TAGS: $!\n"; print $tagfile "$title\n"; print $tagfile "$artist\n"; if ($opt_verbose) { print "\ttitle: $title\n"; print "\tartist: $artist\n"; } } sub Do_Build { # go there chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n"; # Clear the tag file, since we're regenerating it System("rm -f $TAGS"); my $error=undef; my $count=0; # make link for each file, and retain extension foreach my $file (@FILES) { chomp($file); next if ($file =~ /^#/); my @parts=split(/\./,$file); my $ext=lc(pop(@parts)); $ext=~tr/A-Z/a-z/; @parts=split(/\//,$file); my $name=pop(@parts); if (!defined($decoders{$ext}) && !defined($decoders{$UNKNOWN})) { warn "Error: '$file': unknown extension '$ext'!\n"; $error=1; next; } if (!-f $file) { warn "Error: '$file': $!\n"; $error=1; next; } $count++; my $track=sprintf("%02d",$count); print "$track: [...]/$name\n"; symlink($file,"$track.$ext") || die "symlink('$file','$count.$ext'): $!\n"; append_tag_info("$track.$ext", $name, $file); } die "Stopping due to errors...\n" if (defined($error)); # make sure we have some tracks die("No tracks?!\n") unless ($count>0); } sub Do_Decode { # go there chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n"; # leave any WAVs in playlist alone opendir(DIR, $BURNDIR) || die "Can't read directory '$BURNDIR': $!\n"; my @need_decode = grep { /^\d+\.[^\.]+$/i && !/\.wav$/ && -f "$BURNDIR/$_" } readdir(DIR); closedir DIR; # Re-check extensions and tools in case we're restarting foreach my $to_decode (sort {$a cmp $b} @need_decode) { my @parts=split(/\./,$to_decode); my $name=shift(@parts); my $ext=pop(@parts); require_extension($ext, $to_decode); } die "Cannot locate needed decoders\n" if (Lookup_tools()); # decode audio into WAV files foreach my $to_decode (sort {$a cmp $b} @need_decode) { my @parts=split(/\./,$to_decode); my $name=shift(@parts); my $ext=pop(@parts); my $file="${name}.wav"; if (-f $file) { print "Skipping track $name: $file exists.\n"; } else { print "Creating WAV for track $name ...\n"; my $lookup = $ext; if (!defined($decoders{$lookup})) { $lookup = $UNKNOWN; } my $decoder = $decoders{$lookup}; if (!defined($decoder)) { die("No decoder available for extension '$ext' - decoding failed!\n"); } my @cmd = ($found{"decoder:$lookup"}); # chose verbosity level if (!$opt_no_log) { push(@cmd,$decoder->{'normal'}); } else { push(@cmd,$decoder->{'verbose'}); } # set up arguments my $input = $to_decode; my $output = $file; push(@cmd,eval "return \"$decoder->{'args'}\""); # run decoder (don't need to worry about arg splits since we're # operating against symlinked files with known names, etc) my $cmd = join(" ",@cmd); # redirect logging $cmd="$cmd $OUTPUT 2>&1"; System($cmd) == 0 or die("Decoding failed!\n"); } } } sub Do_Correct { # go there chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n"; # get list of wavs from directory opendir(DIR, $BURNDIR) || die "Can't read directory '$BURNDIR': $!\n"; my @wavs = grep { /^\d+\.wav$/i && -f "$BURNDIR/$_" } readdir(DIR); closedir DIR; # correct any wav file formats foreach my $wav (sort {$a cmp $b} @wavs) { my @parts=split(/\./,$wav); my $name=shift(@parts); print "Checking WAV format for track $name ...\n"; my $report=`sox -V $wav $wav.raw trim 0.1 1 2>&1`; my ($channels, $frequency, $samples); if ($report =~ /^Input File/m) { # In version 13.0.0, the report format has changed # Sample Size : 8-bit (1 byte) # Channels : 1 # Sample Rate : 11025 $report =~ m/Sample (?:Size|Encoding)\s*:\s+(\d+)-bit/s or die "sox did not report sample size:\n$report"; $samples = $1; $report =~ m/Channels\s+:\s+(\d+)/s or die "sox did not report channel count:\n$report"; $channels = $1; $report =~ m/Sample Rate\s+:\s+(\d+)/s or die "sox did not report sample frequency:\n$report"; $frequency = $1; } else { # sox: Reading Wave file: Microsoft PCM format, 2 channels, # sox: 44100 samp/sec 176400 byte/sec, block align, 16 bits/samp, # sox: 44886528 data bytes $report =~ m|(\d+) channels?|s or die "sox did not report channel count:\n$report"; $channels = $1; $report =~ m|(\d+) samp/sec|s or die "sox did not report sample frequency:\n$report"; $frequency = $1; $report =~ m|(\d+) bits/samp|s or die "sox did not report sample size:\n$report"; $samples = $1; } unless ($channels == 2 && $frequency == 44100 && $samples == 16) { # only do a "resample" if frequency isn't correct my $resample="resample"; $resample="" if ($frequency == 44100); print "Correcting WAV format for track $name ...\n"; System("sox $wav -r 44100 -c 2 new-$wav $resample $OUTPUT 2>&1") == 0 or die("Correction failed!\n"); unlink($wav) || die "unlink('$wav'): $!\n"; rename("new-$wav",$wav) || die "rename('new-$wav','$wav'): $!\n"; } unlink("$wav.raw"); } } sub Do_Normalize { # go there chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n"; # normalize the volumes print "Normalizing volume levels...\n"; System("$found{'normalize'} -m [0-9]*.wav") == 0 or die("Normalizing failed!\n"); print "Normalizing finished.\n"; } sub encode_cd_text_data($) { my ($data) = @_; my $encoded = ""; # Handle backslash and quotes $data =~ s/\\/\\\\/g; $data =~ s/"/\\"/g; # Using the binary data method seems to fail (missing trailing 0?) # if ($data =~ /"/) { # $encoded = "{ " . join(", ",map(ord, split(//,$data))) . " }"; # } # else { $encoded = "\"" . $data . "\""; # } return $encoded; } sub cd_text($$) { my ($title, $artist) = @_; chomp($title); chomp($artist); my $text = "CD_TEXT {\n LANGUAGE 0 {\n"; $text .= " TITLE " . encode_cd_text_data($title) . "\n"; $text .= " PERFORMER " . encode_cd_text_data($artist) . "\n"; $text .= " }\n}\n"; return $text; } sub Do_TOC { # go there chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n"; print "Generating CDR Table of Contents...\n"; # Get ready to read tags my $tagfile; open($tagfile,"<$TAGS") || die "Cannot read $TAGS: $!\n"; # create a TOC for cdrdao open(TOC,">$CDTOC") || die("Cannot write to '$CDTOC': $!\n"); print TOC "CD_DA\n"; if (!$opt_no_cd_text) { # CDRDAO wants title/performer for the cd itself too, so leave them blank print TOC <), scalar(<$tagfile>)); } # The trailing space was (is?) needed for some versions of cdrdao print TOC "FILE \"$wav\" 0 \n\n"; } close TOC; close $tagfile; } sub Do_TOC_Verify { # go there chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n"; print "Verifying generated Table of Contents...\n"; System(cdrdao('read-test')." $CDTOC $OUTPUT 2>&1") == 0 or die "Failed to create CD Table of Contents?!\n"; } sub Do_CDR_Check { # go there chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n"; print "Checking for CDR...\n"; my ($rc, $report) = Backtick(cdrdao('disk-info')); die "CDR not loaded?!\n" if ($rc != 0); print "\tCDR found.\n"; if (!$opt_no_cd_text) { my $options = undef; my $driver_name = undef; foreach my $line (split("\n",$report)) { chomp($line); if ($line =~ /^Using driver: (.*)\(options (0x[0-9a-fA-F]+)\)$/) { $driver_name = $1; $options = hex($2); } } if (!defined($options)) { die "Could not determine driver options!\n"; } elsif ($opt_verbose) { printf("\tDriver name: %s\n", $driver_name); printf("\tDriver options: 0x%04x\n", $options); } # 0x10 == OPT_MMC_CD_TEXT /usr/share/cdrdao/drivers if (($driver_name =~ /raw writing/) || ($options & 0x10) == 0x10) { print "\tCD-TEXT supported.\n"; } else { print "ERROR: It seems that driver selected by cdrdao for $opt_device\n"; print " does not support CD-TEXT writing. Either disable CD-TEXT via\n"; print " '--no-cd-text' or select a different driver (e.g. try using\n"; print " '--driver generic-mmc-raw').\n"; exit(1); } } } sub Do_Burn { # go there chdir($BURNDIR) || die "Cannot chdir('$BURNDIR'): $!\n"; my $cmd = cdrdao('write'); $cmd.=" --eject" if (!$opt_no_eject); $cmd.=" -n $CDTOC"; System($cmd) == 0 or die "BURN FAILED!\n"; } sub Version { # Create human-readable version with un-human-readable code print "mp3cd version ". join(".",map{$_+0} (sprintf("%.6f",$VERSION) =~/^(\d+)\.?(\d{3})?(\d{3})?$/))."\n"; print <<'EOM'; Copyright 2003-2011 Kees Cook This program is free software; you may redistribute it under the terms of the GNU General Public License. This program has absolutely no warranty. EOM exit(0); } # return a good cdrdao command string prefix sub cdrdao { my $operation = $_[0] || 'simulate'; $operation = 'simulate' if ($opt_simulate && $operation eq 'write'); return "cdrdao $operation $opt_cdrdao"; } # /* vi:set ai ts=4 sw=4 expandtab: */