#!/usr/bin/perl -w # cd2codec: Rip CD tracks and convert to AAC, other codecs, retaining CDDB data # calls: cdrdao, cdparanoia, normalize, afconvert, mp4tags, flac, lame # # cdrdao: Fink, http://cdrdao.sourceforge.net/w # cdparanoia: http://www.xiph.org/paranoia/ # cuetools: http://cuetools.sourceforge.net/ # normalize: Fink, http://www.freebsd.org/cgi/pds.cgi?ports/audio/normalize # afconvert: Part of XCode, http://developer.apple.com/tools/xcode/ # mp4tags: Part of mpeg4ip, http://mpeg4ip.sourceforge.net/ # mpeg4ip requires SDL: http://www.libsdl.org/ # flac: http://flac.sourceforge.net/ # lame: http://lame.sourceforge.net/ # Author: Steven Thomas Smith , 2007-01-01: Version 0.1 # 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 St, Fifth Floor, Boston, MA 02110-1301 USA use Text::ParseWords; ################### default arguments and constants #################### $default_dao_file = ""; $default_rip_flag = 1; $default_cddb_only = 0; $default_file_format = "m4af"; $default_data_format = "aac"; $default_bitrate_aac_kbps = 256; $default_bitrate_mp3_kbps = 320; $default_aac_quality = 127; $default_codec_strategy = 2; $default_utf8_flag = 1; $default_cleanup_flag = 1; $default_verbose = 1; $default_toc_file = "temp.toc"; $default_normalize_flag = 1; $default_cd_drive = "/dev/disk1"; $DEFAULT_TEMP_DIRECTORY = "."; $DEFAULT_TRASH_DIRECTORY = $ENV{HOME} . "/.Trash"; $dao_file = $default_dao_file; $rip_flag = $default_rip_flag; $cddb_only = $default_cddb_only; $file_format = $default_file_format; $data_format = $default_data_format; $aac_quality = $default_aac_quality; $codec_strategy = $default_codec_strategy; $utf8_flag = $default_utf8_flag; $toc_file = $default_toc_file; $cleanup_flag = $default_cleanup_flag; $verbose = $default_verbose; $normalize_flag = $default_normalize_flag; $cd_drive = $default_cd_drive; $TEMP_DIRECTORY = $DEFAULT_TEMP_DIRECTORY; $TRASH_DIRECTORY = $DEFAULT_TRASH_DIRECTORY; # save the command line $perl_command_line = $0; foreach (@ARGV) { $perl_command_line .= " " .$_; } # first, determine if I'm debugging foreach (@ARGV) { /^--debug$/ | /^--debugging$/ && do { $debugging = 1; next; }; } # $helpme = 1 unless $ARGV[0]; # print help if there are no switches SWITCHES: # these pretty much follow cdparanoia and afconvert while ($ARGV[0]) { $_ = shift; /^-r$/ | /^--rip$/ | /^--rip-off$/ && do { $rip_flag = !$default_rip_flag; next SWITCHES; }; /^--cddb$/ | /^--cddb-only$/ && do { $cddb_only = !$cddb_only; next SWITCHES; }; /^-f$/ | /^--file$/ | /^--file-format$/ && do { $file_format = shift; next SWITCHES; }; /^-d$/ | /^--data$/ | /^--data-format$/ && do { $data_format = shift; next SWITCHES; }; /^-b$/ | /^--bitrate$/ && do { $bitrate_kbps = shift; next SWITCHES; }; /^-q$/ | /^--quality$/ && do { $aac_quality = shift; next SWITCHES; }; /^-s$/ | /^--strategy$/ | /^--set_bitrate_strategy$/ && do { $codec_strategy = shift; next SWITCHES; }; /^-u$/ | /^--utf8$/ | /^--utf-8$/ | /^--UTF8$/ | /^--UTF-8$/ && do { $utf8_flag = !$default_utf8_flag; next SWITCHES; }; /^--dao$/ | /^--dao-file$/ && do { $dao_file = shift; next SWITCHES; }; /^-T$/ | /^--toc$/ | /^--toc-file$/ && do { $toc_file = shift; next SWITCHES; }; /^-t$/ | /^--track$/ | /^--tracks$/ && do { while ($ARGV[0] && !($ARGV[0] =~ /^-/)) { $__ = shift; push(@tracks_to_save,$__); } next SWITCHES; }; /^-n$/ | /^--normalize$/ | /^--normalize-off$/ && do { $normalize_flag = !$default_normalize_flag; next SWITCHES; }; /^--cd$/ | /^--cd-drive$/ && do { $cd_drive = shift; next SWITCHES; }; /^-c$/ | /^--cleanup$/ | /^--cleanup-off$/ && do { $cleanup_flag = !$default_cleanup_flag; next SWITCHES; }; /^--temp$/ | /^--temp-directory$/ && do { $TEMP_DIRECTORY = shift; next SWITCHES; }; /^--trash$/ | /^--trash-directory$/ && do { $TRASH_DIRECTORY = shift; next SWITCHES; }; /^-v$/ | /^--verbose$/ | /^--verbose-off$/ && do { $verbose = !$default_verbose; next SWITCHES; }; /^-d$/ | /^--debug$/ | /^--debugging$/ && do { $debugging = 1; next SWITCHES; }; /^-h$/ | /^--help$/ && do { $helpme = 1; next SWITCHES; }; /^-/ && die sprintf("cd2codec: Unknown flag \`%s'.\n" . "Type \`cd2codec --help' for help.\n",$_); !/^-/ && die sprintf("cd2codec: Unknown argument \`%s'.\n" . "Type \`cd2codec --help' for help.\n",$_); } if ($helpme) { $ENV{LESSCHARSET} = "utf-8"; if ($ENV{PAGER}) { # run the user's PAGER open(PAGER,"| $ENV{PAGER}") || die "Cannot run $ENV{PAGER}: $!\n"; } else { open(PAGER,"| more") || open(PAGER,"| less") || open(PAGER,"| cat") || die "Cannot run more or less or cat: $!\n"; } print PAGER <<"HELP STRING"; cd2codec: The perl script rips a CD and converts the tracks to AAC [or MP3 etc.] formats. The command line is: cd2codec [OPTIONS] OPTIONS -r --rip --rip-off Turn off CD ripping. --cddb --cddb-only Only get the CDDB information in the TOC file $default_toc_file, then exit. This is useful for adding UTF-8 codes to the TOC file for world music. For example, 9rabiyuN 'anaa == \330\271\330\261\331\216\330\250\331\220\331\212\331\214\040\330\243\331\216\331\206\331\216\330\247 [Arabic, by Yuri Mrakadi] Anna Vissi == \316\221\316\275\316\275\316\261\040\316\222\316\271\317\203\317\203\316\267 [Greek] -f --file --file-format _\x08f_\x08i_\x08l_\x08e_\x08__\x08f_\x08o_\x08r_\x08m_\x08a_\x08t 'm4af' = MPEG4 Audio (.m4a) data_formats: 'aac ' 'alac' 'MPG3' = MPEG Layer 3 (.mp3, .mpeg) data_formats: 'mp3' 'FLAC' = Free Lossless Audio Codec (.flac) data_formats: 'flac' [Default is $default_file_format; There are many more possibilities -- see \`afconvert -h']. -d --data --data-format _\x08d_\x08a_\x08t_\x08a_\x08__\x08f_\x08o_\x08r_\x08m_\x08a_\x08t AAC [Advanced audio codec], ALAC [Apple loss audio codec], MP3, FLAC [Default is $default_data_format]. -b --bitrate _\x08b_\x08i_\x08t_\x08r_\x08a_\x08t_\x08e_\x08__\x08k_\x08b_\x08p_\x08s Codec bitrate in kbps [$default_bitrate_aac_kbps kbps default for AAC, $default_bitrate_mp3_kbps kbps for MP3]. -s --strategy _\x08b_\x08i_\x08t_\x08r_\x08a_\x08t_\x08e_\x08__\x08s_\x08t_\x08r_\x08a_\x08t_\x08e_\x08g_\x08y bitrate strategy for encoded file 0 for CBR, 1 for ABR, 2 for VBR [default is $default_codec_strategy]. -u --utf8 --utf-8 --UTF8 --UTF-8 Turn off conversion of 8-bit ASCII to UTF-8 format, i.e., treat strings as UTF-8 format. By default, octal codes in the range \\200-\\377 are converted to the UTF-8 encoding of their ISO Latin equivalents; e.g., Tool's album \303\206nema appears in CDDB databases as "\\306nema", which will be converted to "\\303\\206nema" unless --utf8 is invoked. To add UTF-8 information by hand, use the printf command, as in the example: mp4tags -s "`printf 'L\\303\\251 zaalen?'`" "Soap kills, Bater 01: Le zallen?.m4a" [this produces the correct song title "L\303\251 zaalen?" with an e-acute]. --dao --dao-file _\x08f_\x08i_\x08l_\x08e Read the disk-at-once (DAO) data into a file using cdrdao. Ordinarily, cdrdao is used only to generate the TOC file for CDDB information. -q --quality _\x08c_\x08o_\x08d_\x08e_\x08c_\x08__\x08q_\x08u_\x08a_\x08l_\x08i_\x08t_\x08y Codec quality [0-127, default is $default_aac_quality]. -T --toc-file _\x08f_\x08i_\x08l_\x08e_\x08._\x08t_\x08o_\x08c The cdrdao TOC file [default is $default_toc_file]. -t --track --tracks _\x08n_\x08u_\x08m_\x08b_\x08e_\x08r_\x08s The tracks to process, e.g., 1-2,3 5 10- [commas, spaces okay]. -n --normalize --normalize-off Turn off file normalization. By default, compressed files are normalized to have maximum amplitude, with no clipping; lossless codecs do not use normalization. Edit this script to change behavior is desired. --cd --cd-drive _\x08d_\x08e_\x08v_\x08i_\x08c_\x08e Specify the CD drive [default is $default_cd_drive]. -c --cleanup --cleanup-off Turn off deletion of temporary files when done. --temp --temp-directory _\x08d_\x08i_\x08r_\x08e_\x08c_\x08t_\x08o_\x08r_\x08y Temporary directory for all files [default: $DEFAULT_TEMP_DIRECTORY]. --trash --trash-directory _\x08d_\x08i_\x08r_\x08e_\x08c_\x08t_\x08o_\x08r_\x08y Trash directory for deleted files [default: $DEFAULT_TRASH_DIRECTORY]. -v --verbose --verbose-off Turn off verbose reporting. --debug Debug this script. -h --help Print this help string. HELP STRING close(PAGER); exit 0; } if (! -d $TEMP_DIRECTORY) { mkdir($TEMP_DIRECTORY,0777) || die "Cannot open directory \`$TEMP_DIRECTORY': $!\n"; } # parse comma separated tracks @tracks_csv = @tracks_to_save; @tracks_to_save = (); foreach $csv (@tracks_csv) { @words = "ewords('\s*,\s*',0,$csv); foreach $w (@words) { push(@tracks_to_save,$w) if $w; } } # determine tracks to process $no_last_track = 1; @tracks = (); for $i ($[ .. $#tracks_to_save) { $_ = $tracks_to_save[$i]; /^[0-9]+$/ && do { push(@tracks,$_); $no_last_track = 0; next; }; /^[0-9]+-[0-9]+$/ && do { $first = $_; $first =~ s/^([0-9]+)-[0-9]+$/$1/; $last = $_; $last =~ s/^[0-9]+-([0-9]+)$/$1/; push(@tracks,$first..$last); $no_last_track = 0; next; }; /^-[0-9]+$/ && do { $last = $_; $last =~ s/^-([0-9]+)$/$1/; push(@tracks,1..$last); $no_last_track = 0; next; }; /^[0-9]+-$/ && do { $first = $_; $first =~ s/^([0-9]+)-$/$1/; push(@tracks,$first); $no_last_track = 1; next; }; } @tracks = sort {$a <=> $b} @tracks; if ($debugging) { print "Tracks: "; $, = " "; $\ = "\n"; print @tracks; $, = $\ = ""; } # set the default bitrates if ($data_format eq "aac") { $bitrate_kbps = $default_bitrate_aac_kbps if !$bitrate_kbps; } elsif ($data_format eq "mp3") { $bitrate_kbps = $default_bitrate_mp3_kbps if !$bitrate_kbps; } # change to the temp directory chdir $TEMP_DIRECTORY || die "Cannot chdir to $TEMP_DIRECTORY: $!\n"; # read the CDDB information if (-f $toc_file) { print "Using the TOC file $TEMP_DIRECTORY/$toc_file ...\n"; } else { # print <<"FEEDBACK"; #No TOC file; execute the commands: # sudo umount $cd_drive # cdrdao read-toc --with-cddb --device "IODVDServices" --driver "generic-mmc:0x00000010" $toc_file #FEEDBACK # test if disk is mounted $mounted = 0; open(MOUNT,"mount |") || die "Cannot run mount: $!\n"; while() { m#^$cd_drive# && do { $mounted = 1; }; } close(MOUNT); if ($mounted) { $cmd = "sudo umount $cd_drive"; print "You must unmount the CD as an administrator.\nExecuting \`", $cmd, "'\n"; system($cmd) == 0 || die "Failed to unmount the CD: $!\n"; $mounted = 0; } # cdrdao on OS X # $cmd = qq{cdrdao read-toc --with-cddb --device "IODVDServices" --driver "generic-mmc:0x00000010" $toc_file}; # If the TOC is bad, or if you chose a bad one from a list, reissue the commands # mv ~/.Trash/temp.toc . # cdrdao read-cddb temp.toc # mv ~/.Trash/ ./track??.cdda.wav # cd2codec --rip-off $fn = 'cdrdao'; $fn_src = 'get from Fink or http://cdrdao.sourceforge.net/'; open(WH,"which \\$fn |"), $wh = , close(WH); chop $wh; -x $wh || die("No executable $fn; $fn_src: $!\n"); if (! $dao_file) { $cmd = "cdrdao read-toc --with-cddb $toc_file"; } else { $cmd = "cdrdao read-cd --with-cddb --datafile $dao_file $toc_file"; } print "Executing the command: ", $cmd, " ...\n" if $verbose; system($cmd) == 0 || die "Failed to get the CDDB information: $!\n"; # exit here if you want to add UTF-8 code to the TOC file by hand exit 0 if $cddb_only; # test if disk is mounted $mounted = 0; sleep 5; # wait for the CD open(MOUNT,"mount |") || die "Cannot run mount: $!\n"; while() { m#^$cd_drive# && do { $mounted = 1; }; } close(MOUNT); # mount the disk again if necessary if (!$mounted) { $cmd = "sudo diskutil mount $cd_drive /Volumes/AUDIO_CD"; print "Executing the command: ", $cmd, " ...\n" if $verbose; print "You must mount the CD as an administrator. Executing \`", $cmd, "'\n"; system($cmd) == 0 || die "Failed to mount the CD: $!\n"; sleep 5; # wait for the CD to mount before ripping $mounted = 1; } } # rip the CD to separate WAV files if ($rip_flag) { $fn = 'cdparanoia'; $fn_src = 'get from http://www.xiph.org/paranoia/'; open(WH,"which \\$fn |"), $wh = , close(WH); chop $wh; -x $wh || die("No executable $fn; $fn_src: $!\n"); if ($#tracks == $[-1) { # rip all tracks $cmd = "cdparanoia -Bw"; print "Executing the command: ", $cmd, " ...\n" if $verbose; system($cmd) == 0 || die "Failed to rip the CD: $!\n"; } else { for $i ($[ .. $#tracks) { $t = $tracks[$i]; $cmd = sprintf("cdparanoia -Bw %d",$t); print "Executing the command: ", $cmd, " ...\n" if $verbose; system($cmd) == 0 || die "Failed to rip the CD: $!\n"; } if ($no_last_track) { # rip until the end $cmd = sprintf("cdparanoia -Bw %d-",$tracks[$#tracks]+1); print "Executing the command: ", $cmd, " ...\n" if $verbose; system($cmd) == 0 || die "Failed to rip the CD: $!\n"; } } } # parse the TOC file open(TOC,$toc_file) || die "Cannot open $toc_file: $!\n"; # Parse the global CD_TEXT block $blocklevel = 0; $global_cd_text = 1; while (defined($_=) && $global_cd_text) { chomp; if (/^CD_TEXT *{/) { $global_cd_text = 0; # count braces in the line @words = "ewords('\s+',0,$_); foreach $word (@words) { $blocklevel += $word =~ tr/{// unless $word =~ /".+"/; } while (defined($_=) && $blocklevel >= 1) { chomp; s#\\([0-7][0-7][0-7])#\\\\$1#g; # add double slash to octal for "ewords if (/^ *[A-Za-z_][A-Za-z_0-9]* +[0-9]* *{/) { # count braces in the line @words = "ewords('\s+',0,$_); foreach $word (@words) { $blocklevel += $word =~ tr/{// unless $word =~ /".+"/; } } elsif (/^ *TITLE +/) { @words = "ewords('\s+',0,$_); for ($i = 0; $i <= $#words ; $i++) { last if ($words[$i] eq "TITLE"); } $title_global = $words[$i+1]; } elsif (/^ *PERFORMER +/) { @words = "ewords('\s+',0,$_); for ($i = 0; $i <= $#words ; $i++) { last if ($words[$i] eq "PERFORMER"); } $performer_global = $words[$i+1]; } elsif (/^ *COMPOSER +/) { @words = "ewords('\s+',0,$_); for ($i = 0; $i <= $#words ; $i++) { last if ($words[$i] eq "COMPOSER"); } $composer_global = $words[$i+1]; } elsif (/^ *MESSAGE +/) { @words = "ewords('\s+',0,$_); for ($i = 0; $i <= $#words ; $i++) { last if ($words[$i] eq "MESSAGE"); } $message_global = $words[$i+1]; } elsif (/^ *GENRE +/) { @words = "ewords('\s+',0,$_); for ($i = 0; $i <= $#words ; $i++) { last if ($words[$i] eq "GENRE"); } $genre_global = $words[$i+1]; } if (/}/) { # count braces in the line @words = "ewords('\s+',0,$_); foreach $word (@words) { $blocklevel -= $word =~ tr/}// unless $word =~ /".+"/; } } } } } # initialize the track numbering if (m#// *Track#) { # did the last while block grab this line? $trackno = 0; } else { $trackno = -1; } # Parse the local CD_TEXT blocks $blocklevel = 0; while () { chomp; if (m#// *Track#) { $trackno++; } elsif (/^CD_TEXT *{/) { $global_cd_text = 0; # count braces in the line @words = "ewords('\s+',0,$_); foreach $word (@words) { $blocklevel += $word =~ tr/{// unless $word =~ /".+"/; } while (defined($_=) && $blocklevel >= 1) { chomp; s#\\([0-7][0-7][0-7])#\\\\$1#g; # add double slash to octal for "ewords if (/^ *[A-Za-z_][A-Za-z_0-9]* +[0-9]* *{/) { # count braces in the line @words = "ewords('\s+',0,$_); foreach $word (@words) { $blocklevel += $word =~ tr/{// unless $word =~ /".+"/; } } elsif (/^ *TITLE +/) { @words = "ewords('\s+',0,$_); for ($i = 0; $i <= $#words ; $i++) { last if ($words[$i] eq "TITLE"); } $title[$trackno] = $words[$i+1]; } elsif (/^ *PERFORMER +/) { @words = "ewords('\s+',0,$_); for ($i = 0; $i <= $#words ; $i++) { last if ($words[$i] eq "PERFORMER"); } $performer[$trackno] = $words[$i+1]; } elsif (/^ *COMPOSER +/) { @words = "ewords('\s+',0,$_); for ($i = 0; $i <= $#words ; $i++) { last if ($words[$i] eq "COMPOSER"); } $composer[$trackno] = $words[$i+1]; } elsif (/^ *MESSAGE +/) { for ($i = 0; $i <= $#words ; $i++) { last if ($words[$i] eq "MESSAGE"); } @words = "ewords('\s+',0,$_); $message[$trackno] = $words[$i+1]; } if (/}/) { # count braces in the line @words = "ewords('\s+',0,$_); foreach $word (@words) { $blocklevel -= $word =~ tr/}// unless $word =~ /".+"/; } } } } } close(TOC); $track_artist_global = $composer_global ? $composer_global : $performer_global; # Define file names, title names, etc. for $i ($[ .. $#title) { $track_artist[$i] = $composer[$i] ? $composer[$i] : $performer[$i]; if ($track_artist[$i] eq $track_artist_global) { $track_file[$i] = "$track_artist_global, $title_global " . sprintf("%02d",$i+1) . ": $title[$i]"; } else { $track_file[$i] = "$track_artist_global, $title_global " . sprintf("%02d",$i+1) . ": $track_artist[$i], $title[$i]"; } if (length $track_file[$i] > 250) { # limit file name length $track_file[$i] = "$track_artist_global, $title_global " . sprintf("%02d",$i+1); if (length $track_file[$i] > 250) { $track_file[$i] = "$title_global " . sprintf("%02d",$i+1); if (length $track_file[$i] > 250) { $track_file[$i] = "$performer_global " . sprintf("%02d",$i+1); } } } if (1) { $track_title[$i] = $track_file[$i]; } else { $track_title[$i] = $title[$i]; } # remove illegal filename characters $track_file[$i] =~ s/\//:/g; # backslash # convert escaped metacharacters $track_file[$i] = eval qq{"$track_file[$i]"}; if (1) { # ISO Latin lookup table transliteration decoding # see http://en.wikipedia.org/wiki/Latin_Unicode %latin_lut = ( "\300" => "A", "\301" => "A", "\302" => "A", "\303" => "A", "\304" => "A", "\305" => "A", "\306" => "AE", # \306 eq Æ "\307" => "C", "\310" => "E", "\311" => "E", "\312" => "E", "\313" => "E", "\314" => "I", "\315" => "I", "\316" => "I", "\317" => "I", "\320" => "D", "\321" => "N", "\322" => "O", "\323" => "O", "\324" => "O", "\325" => "O", "\326" => "O", "\327" => "x", "\330" => "O", "\331" => "U", "\332" => "U", "\333" => "U", "\334" => "U", "\335" => "Y", "\336" => "P", "\337" => "ss", # es-zet "\340" => "a", "\341" => "a", "\342" => "a", "\343" => "a", "\344" => "a", "\345" => "a", "\346" => "ae", # \346 eq æ "\347" => "c", "\350" => "e", "\351" => "e", "\352" => "e", "\353" => "e", "\354" => "i", "\355" => "i", "\356" => "i", "\357" => "i", "\360" => "d", "\361" => "n", "\362" => "o", "\363" => "o", "\364" => "o", "\365" => "o", "\366" => "o", "\367" => "-", "\370" => "o", "\371" => "u", "\372" => "u", "\373" => "u", "\374" => "u", "\375" => "y", "\376" => "p", "\377" => "y" ); foreach $n (0200 ... 0277) { $latin_lut{chr $n} = "\#"; } # nonletter Latin Unicode $track_file[$i] =~ s/([\200-\377])/$latin_lut{$1}/g; } } if ($debugging) { for $i ($[ .. $#track_file) { print $track_file[$i], "\n"; } } #subroutine to convert quoted metacharacter strings to utf-8 # Evaluate quoted metastrings sub qmstr { my ($str) = @_; $str =~ s#"#\\\\\\"#g; # escape " $str = eval qq{"$str"}; # evaluate quoted metacharacters return $str; } # Convert quoted metastrings to UTF-8 sub qmstr2utf8 { my ($str) = @_; $str =~ s#"#\\\\\\"#g; # escape " $str = eval qq{"$str"}; # evaluate quoted metacharacters $str =~ s/(.)/&ascii2utf8($&)/ge; # convert 8-bit characters to utf8 return $str; } #subroutine to convert nonword characters to baskslash octal format sub backslashoctal { my ($str) = @_; $str =~ s/(\W)/sprintf("\\%03o",unpack('C',$&))/ge; return $str; } sub ascii2utf8 { # Convert 8-bit ASCII byte into UTF-8 my ($ascii) = @_; my $b = 0; $b = unpack('C',$ascii); # unpack one (the first) byte # See http://en.wikipedia.org/wiki/UTF-8 return ($b < 0x80)? $ascii : pack('C2', (0b11000000 | (0b00011111 & ($b >> 6)), 0b10000000 | (0b00111111 & $b))); } sub unicode2utf8 { # Convert Unicode into UTF-8; e.g., "aleph" == &unicode2utf8(0x5D0) my ($n) = @_; # See http://en.wikipedia.org/wiki/UTF-8 return ($n < 0x80)? chr $n : ($n < 0x800)? pack('C2', (0b11000000 | (0b00011111 & ($n >> 6)), 0b10000000 | 0b00111111 & $n)) : ($n < 0x10000)? pack('C3', (0b11100000 | (0b00001111 & ($n >> 12)), 0b10000000 | (0b00111111 & ($n >> 6)), 0b10000000 | 0b00111111 & $n)) : ($n < 0x110000)? pack('C4', (0b11110000 | (0b00000111 & ($n >> 18)), 0b10000000 | (0b00111111 & ($n >> 12)), 0b10000000 | (0b00111111 & ($n >> 6)), 0b10000000 | 0b00111111 & $n)) : "" ; } # Test the subroutines #$foo = "qmstr2utf8"; #$str = "Hello, world; \346"; #print &$foo($str), " - ", &unicode2utf8(0x40), " - ", &unicode2utf8(0xe6), " - ", &unicode2utf8(0x5D0), # " - ", &unicode2utf8(0x10427), " - ", &unicode2utf8(0x0x110001), ".\n"; #exit 0; # Convert quoted metacharacters to utf-8 $qms_conversion = $utf8_flag ? "qmstr2utf8" : "qmstr" ; $title_global = &$qms_conversion($title_global) if $title_global; # process \octal $performer_global = &$qms_conversion($performer_global) if $performer_global; # process \octal $composer_global = &$qms_conversion($composer_global) if $composer_global; # process \octal $message_global = &$qms_conversion($message_global) if $message_global; # process \octal $genre_global = &$qms_conversion($genre_global) if $genre_global; # process \octal $track_artist_global = &$qms_conversion($track_artist_global) if $track_artist_global; # process \octal for $i ($[ .. $#title) { $title[$i] = &$qms_conversion($title[$i]) if $title[$i]; # process \octal $performer[$i] = &$qms_conversion($performer[$i]) if $performer[$i]; # process \octal $composer[$i] = &$qms_conversion($composer[$i]) if $composer[$i]; # process \octal $message[$i] = &$qms_conversion($message[$i]) if $message[$i]; # process \octal $track_artist[$i] = &$qms_conversion($track_artist[$i]) if $track_artist[$i]; # process \octal } # encode the WAV files # if no_last_track, add these tracks numbers to @tracks now that the TOC file has been parsed if (($#tracks > $[-1) & $no_last_track) { push(@tracks,$tracks[$[+$#tracks]+1 .. $#title+1); @tracks = sort {$a <=> $b} @tracks; } @tracks = ($[+1 .. $#title+1) if ($#tracks == $[-1); if ($debugging) { print "Tracks: "; $, = " "; $\ = "\n"; print @tracks; $, = $\ = ""; } # Process the ripped files for $i ($[ .. $#tracks) { $t = $tracks[$i]; $wav_file = "$track_file[$t-1].wav"; print sprintf("Track %02d ...\n",$t) if $verbose; if (! -f $wav_file) { rename(sprintf("track%02d.cdda.wav",$t),$wav_file) || die "rename failed: $!\n"; } # loop through the requested data formats $_ = $data_format; # lossless codecs -- do these *before* normalization to retain # bit-for-bit with original, HDCD encoding (if any) /^alac$/ | /^ALAC$/ && do { # Convert the WAV file to ALAC (.alac.m4a suffix) $fn = 'afconvert'; $fn_src = 'build from XCode tools at http://developer.apple.com/tools/xcode/'; open(WH,"which \\$fn |"), $wh = , close(WH); chop $wh; -x $wh || die("No executable $fn; $fn_src: $!\n"); $cmd = sprintf(qq{afconvert --file m4af --data alac --quality %d --src-quality 127 --verbose},$aac_quality); $cmd .= qq{ "$wav_file" "$track_file[$t-1].alac.m4a"}; $cmd =~ s/--verbose//g if !$verbose; print "`", $cmd, "` ...\n" if $verbose; system($cmd) == 0 || die "afconvert failed: $!\n"; # Add iTunes metadata $fn = 'mp4tags'; $fn_src = 'get from mpeg4ip, http://mpeg4ip.sourceforge.net/ and SDL, http://www.libsdl.org/'; open(WH,"which \\$fn |"), $wh = , close(WH); chop $wh; -x $wh || die("No executable $fn; $fn_src: $!\n"); # Method by hand: e.g., # mp4tags -s "`printf 'L\303\251 zaalen?'`" "Soapkills, Bater 01: Le zallen?.m4a" $cmd = "mp4tags"; $cmd .= qq{ -A "$title_global"} if $title_global; if ($track_artist[$t-1]) { $cmd .= qq{ -a "$track_artist[$t-1]"}; } elsif ($track_artist_global) { $cmd .= qq{ -a "$track_artist_global"}; } $message = $message_global; $message = $message . " - " . $message[$t-1] if $message[$t-1]; $cmd .= qq{ -c "$message_global"} if $message; $cmd .= qq{ -g "$genre_global"} if $genre_global; $cmd .= qq{ -s "$title[$t-1]"}; $cmd .= sprintf(qq{ -t "%d"},$t); $cmd .= sprintf(qq{ -T "%d"},$#title+1); $cmd .= qq{ -w "$composer_global"} if $composer_global; $cmd .= qq{ "$track_file[$t-1].alac.m4a"}; print "`", $cmd, "` ...\n" if $verbose; system($cmd) == 0 || die "mp4tags failed: $!\n"; }; /^flac$/ | /^FLAC$/ && do { $fn = 'flac'; $fn_src = 'get from http://flac.sourceforge.net/'; open(WH,"which \\$fn |"), $wh = , close(WH); chop $wh; -x $wh || die("No executable $fn; $fn_src: $!\n"); $cmd = qq{flac --best "$wav_file" "$track_file[$t-1].flac"}; print "`", $cmd, "` ...\n" if $verbose; system($cmd) == 0 || die "flac failed: $!\n"; }; # normalize the WAV file if desired prior to lossy codecs if ($normalize_flag) { $fn = 'normalize'; $fn_src = 'get from Fink or http://www.freebsd.org/cgi/pds.cgi?ports/audio/normalize'; open(WH,"which \\$fn |"), $wh = , close(WH); chop $wh; -x $wh || die("No executable $fn; $fn_src: $!\n"); $cmd = qq{normalize --peak "$wav_file"}; print "`", $cmd, "` ...\n" if $verbose; system($cmd) == 0 || die "normalize failed: $!\n"; } # lossy codecs -- do these *after* normalization /^aac$/ | /^AAC$/ && do { # Convert the WAV file to AAC (.m4a suffix) $fn = 'afconvert'; $fn_src = 'build from XCode tools at http://developer.apple.com/tools/xcode/'; open(WH,"which \\$fn |"), $wh = , close(WH); chop $wh; -x $wh || die("No executable $fn; $fn_src: $!\n"); $cmd = sprintf(qq{afconvert --file m4af --data aac --bitrate %d000 --quality %d --src-quality 127 --verbose} . qq{ --strategy %d},$bitrate_kbps,$aac_quality,$codec_strategy); $cmd .= qq{ "$wav_file" "$track_file[$t-1].m4a"}; $cmd =~ s/--verbose//g if !$verbose; print "`", $cmd, "` ...\n" if $verbose; system($cmd) == 0 || die "afconvert failed: $!\n"; # Add iTunes metadata $fn = 'mp4tags'; $fn_src = 'get from mpeg4ip, http://mpeg4ip.sourceforge.net/ and SDL, http://www.libsdl.org/'; open(WH,"which \\$fn |"), $wh = , close(WH); chop $wh; -x $wh || die("No executable $fn; $fn_src: $!\n"); $cmd = "mp4tags"; $cmd .= qq{ -A "$title_global"} if $title_global; if ($track_artist[$t-1]) { $cmd .= qq{ -a "$track_artist[$t-1]"}; } elsif ($track_artist_global) { $cmd .= qq{ -a "$track_artist_global"}; } $message = $message_global; $message = $message . " - " . $message[$t-1] if $message[$t-1]; $cmd .= qq{ -c "$message_global"} if $message; $cmd .= qq{ -g "$genre_global"} if $genre_global; $cmd .= qq{ -s "$title[$t-1]"}; $cmd .= sprintf(qq{ -t "%d"},$t); $cmd .= sprintf(qq{ -T "%d"},$#title+1); $cmd .= qq{ -w "$composer_global"} if $composer_global; $cmd .= qq{ "$track_file[$t-1].m4a"}; print "`", $cmd, "` ...\n" if $verbose; system($cmd) == 0 || die "mp4tags failed: $!\n"; }; /^mp3$/ | /^MP3$/ && do { $fn = 'lame'; $fn_src = 'get from http://lame.sourceforge.net/'; open(WH,"which \\$fn |"), $wh = , close(WH); chop $wh; -x $wh || die("No executable $fn; $fn_src: $!\n"); $cmd = sprintf(qq{lame -h -b %d},$bitrate_kbps); $cmd .= qq{ -tt "$track_title[$t-1]"}; $cmd .= qq{ -ta "$track_artist[$t-1]"}; $cmd .= qq{ -tl "$title_global"}; $cmd .= qq{ "$wav_file" "$track_file[$t-1].mp3"}; print "`", $cmd, "` ...\n" if $verbose; system($cmd) == 0 || die "lame failed: $!\n"; }; } rename($toc_file,"$dao_file.toc") || die("Unable to rename tocfile: $!") if $dao_file; if ($cleanup_flag) { for $i ($[ .. $#tracks) { $t = $tracks[$i]; $wav_file = "$track_file[$t-1].wav"; rename($wav_file, "$TRASH_DIRECTORY/" . $wav_file) || die "Unable to trash file: $!\n."; } if (! $dao_file) { rename($toc_file, "$TRASH_DIRECTORY/" . $toc_file) || die "Unable to trash file: $!\n."; } if (-f "track00.cdda.wav") { rename( "track00.cdda.wav", "$TRASH_DIRECTORY/" . "track00.cdda.wav") || die "Unable to trash file: $!\n."; } }