1#!/usr/local/bin/perl 2 3# ogg2mp3 4# Maintained by: James Ausmus <james.ausmus@gmail.com> 5# Homepage: https://github.com/fithp/ogg2mp3 6# Released under the GPLv2 license 7# 8# based on (most of): 9# 10# mp32ogg 11# Author: Nathan Walp <faceprint@faceprint.com> 12# This software released under the terms of the Artistic License 13# <http://www.opensource.org/licenses/artistic-license.html> 14# 15# and (the name and small bits of): 16# 17# ogg2mp3 18# Author: David Ljung Madison <DaveSource.com> 19# http://marginalhacks.com/ 20# License: http://MarginalHacks.com/License 21# 22# 23# Inversion of mp32ogg was done by Mark Draheim <rickscafe.casablanca@gmx.net> 24# as always with a helping hand from guidod 25# 26# 0.4 ogginfo fix thanks to Chris Dance 27# 0.5 cli options thanks to Henry Gomersall 28# 0.5.1 Addition of endian-fix patch by Boris Peterson, modification headers to reflect new maintainership/homepage 29# 0.6 Strictification done 30# 0.6.1 Fix several bugs introduced in 0.6, add --debug option, fix lame --genre-list outputting to stderr bug - thanks to Peter Greenwood and Tobias Rehbein! Also add track number/total number of tracks to possible --rename options 31# 32 33my $version = "v0.6.1"; 34 35# TODO: use Ogg::Vorbis::Header::PurePerl when it becomes usable 36# TODO: strictify the thing 37use strict; 38use File::Find (); 39use File::stat; 40use File::Basename; 41use Getopt::Long; 42use String::ShellQuote; 43 44print "\n"; 45print " ------------------------------------------------------------------- \n"; 46print " ogg2mp3 $version\n"; 47print " based on mp32ogg (c) 2000-2002 Nathan Walp\n"; 48print " inverted and adapted by Mark Draheim\n"; 49print " Maintainership assumed by James Ausmus\n"; 50print " This code is released under the General Public License v2.\n"; 51print " ------------------------------------------------------------------- \n\n"; 52 53my $MP3ENC = "/usr/local/bin/lame"; 54#my $MP3INFO = "/usr/local/bin/mp3_check"; 55my $OGGINFO = "/usr/local/bin/ogginfo"; 56my $OGG123 = "/usr/local/bin/ogg123"; 57 58# check presence of executables 59stat($MP3ENC) or die "Error: $MP3ENC not present!\n"; 60stat($OGGINFO) or die "Error: $OGGINFO not present!\n"; 61stat($OGG123) or die "Error: $OGG123 not present!\n"; 62 63# list of lame's allowed values 64my $frequency_l = " 32 44.1 48 "; 65my $bitrate_l = " 32 40 48 56 64 80 96 112 128 160 192 224 256 320 "; 66my $quality_l = " 0 1 2 3 4 5 6 7 8 9 "; 67 68my $opt_delete; 69my $opt_rename; 70my $opt_lowercase; 71my $opt_no_replace; 72my $opt_bitrate; 73my $opt_quality; 74my $opt_verbose; 75my $opt_debug; 76 77# build genre hash 78my %genres; 79open(GENRES, "$MP3ENC 2>&1 --genre-list|") or die "Couldn't get genre list with $MP3ENC --genre-list\n"; 80while(<GENRES>) { 81 chomp; 82 next if /^\s*$/; 83 # lowercase names are keys, ID number is value 84 $genres{lc($2)} = $1 if /^\s*(\d*)\s(.*)$/; 85} 86close(GENRES); 87 88 89# TODO; add overrides for lame settings, eg change sampling freq 90# right now we inherit ogg settings or fallback to common settings 91GetOptions("help|?",\&showhelp, 92 "delete", \$opt_delete, 93 "rename=s", \$opt_rename, 94 "lowercase", \$opt_lowercase, 95 "no-replace", \$opt_no_replace, 96 "bitrate=s", \$opt_bitrate, 97 "quality=s", \$opt_quality, 98 "verbose", \$opt_verbose, 99 "debug", \$opt_debug, 100 "<>", \&checkfile); 101 102sub showhelp() { 103 print "Usage: $0 [options] dir1 dir2 file1 file2 ...\n\n"; 104 print "Options:\n"; 105 print "--delete Delete files after converting\n"; 106 print "--rename=format Instead of simply replacing the .ogg with\n"; 107 print " .mp3 for the output file, produce output \n"; 108 print " filenames in this format, replacing %a, %t,\n"; 109 print " %l, %n, and %N with artist, title, album name,\n"; 110 print " track number, and total number of tracks for\n"; 111 print " the track/album\n"; 112 print "--bitrate=bitrate Ask lame to use defined bitrate\n"; 113 print "--quality=quality Ask lame to use defined quality (0-9)\n"; 114 print "--lowercase Force lowercase filenames when using --rename\n"; 115 print "--verbose Verbose output\n"; 116 print "--help Display this help message\n"; 117 exit; 118} 119 120 121sub checkfile() { 122 my $file = shift(@_); 123 if(-d $file) { 124 File::Find::find(\&findfunc, $file); 125 } 126 elsif (-f $file) { 127 &ConvertFile($file); 128 } 129} 130 131sub findfunc() { 132 my $file = $_; 133 my ($name,$dir,$ext) = fileparse($file,'\.ogg'); 134 if((/\.ogg/,$ext) && (-f $file)) { 135 &checkfile($file); 136 } 137} 138 139sub ConvertFile() { 140 my $oggfile = shift(@_); 141 my $delete = $opt_delete; 142 my $filename = $opt_rename; 143 my $bitrate = $opt_bitrate; 144 my $quality = $opt_quality; 145 my $lowercase = $opt_lowercase; 146 my $noreplace = $opt_no_replace; 147 my $verbose = $opt_verbose; 148 149 if ($opt_debug) { 150 print "Options:\n\toggfile: $oggfile\n\tdelete: $delete\n\tfilename: $filename\n\tbitrate: $bitrate\n\tquality: $quality\n\tlowercase: $lowercase\n\tnoreplace: $noreplace\n\tverbose: $verbose\n\n"; 151 } 152 153 $_ = $filename; 154 155 # two hashes; title tags use "=" delimiter, spec tags use ":" delimiter 156 open(TAGS, "$OGGINFO \Q$oggfile\E |") or die "Error: Couldn't run ogginfo $oggfile\n"; 157 my %oggtags; 158 my %oggrates; 159 while(<TAGS>) { 160 chomp; 161 next if /^\s*$/; 162 # get title tags 163 if (/^\s*(\S+)=(.*)$/) { 164 $oggtags{lc($1)} = $2; 165 } 166 # grab channels, freq and bitrate 167 # right now we're only using the fixed values and we strip daft ,000000 168 if (/^\s*(Channels|Rate|Nominal bitrate):\s*(\d*).*$/i) { 169 $oggrates{lc($1)} = $2; 170 } 171 } 172 close(TAGS); 173 174 if ($opt_debug) { 175 my $dbg_idx=1; 176 my $dbg_name; 177 print "dumping tags list...\n"; 178 while (($dbg_idx,$dbg_name) = each(%oggtags)) { 179 print "\t$dbg_idx: $dbg_name\n"; 180 } 181 print "\n"; 182 } 183 184 # default to joint stereo for encoding 185 # change to stereo for bitrates >=160 186 my $channels = "j"; 187 if ($oggrates{channels} == 1) { 188 # mono 189 $channels = "m"; 190 } 191 192 # bitrate 193 # Do bitrate stuff. Grab from command line if available. 194 # Also, add stereo channels for higher bitrates 195 if(($bitrate eq "") || !($bitrate_l =~ / $bitrate /)){ 196 $bitrate = ($oggrates{"nominal bitrate"}); 197 # nice fallback quality default 198 if($verbose) { 199 print "No bitrate set - Attempting to extract bitrate from ogg data...\n"; 200 print "Bitrate $bitrate found\n"; 201 } 202 } 203 if(($quality eq "") || !($quality_l =~ / $quality /)){ 204 # nice fallback quality default 205 $quality = 5; 206 if($verbose) { 207 print "No quality set - Falling back to quality 5\n"; 208 print "Info: lower values mean higher quality.\n"; 209 } 210 } 211 if($bitrate_l =~ / $bitrate /) { 212 if($bitrate >= 256) { 213 $channels = "s"; 214 } elsif($bitrate >= 192) { 215 $channels = "s"; 216 } elsif($bitrate >= 160) { 217 $channels = "s"; 218 } 219 } else { 220 # fallback defaults 221 $bitrate = 128; 222 print "Warning: Bitrate $bitrate is invalid\n"; 223 print "Warning: Falling back to default: 128 kbps...\n"; 224 } 225 226 # sampling frequency 227 # ogg returns Hz, lame wants kHz 228 my $frequency = ($oggrates{rate})/1000; 229 # check against allowed values 230 if (!($frequency_l =~ / $frequency /)) { 231 print "Warning: Sampling frequency $frequency kHz is not a legal value for Lame MPEG1!\n"; 232 print "Warning: Falling back to default 44.1 kHz...\n"; 233 $frequency = ""; 234 } 235 if (!$frequency) { 236 # fallback default 237 $frequency = "44.1"; 238 } 239 240 my $dirname = dirname($oggfile); 241 my @trackNumInfo = split(/\//, $oggtags{tracknumber}); 242 243 if($filename eq "" || 244 ((/\%a/) && $oggtags{artist} eq "") || 245 ((/\%t/) && $oggtags{title} eq "") || 246 ((/\%l/) && $oggtags{album} eq "") || 247 ((/\%n/) && $trackNumInfo[0] eq "") || 248 ((/\%N/) && $trackNumInfo[1] eq "") 249 ){ 250 251 if($filename ne "") { 252 print "Warning: Not enough ID3 info to rename!\n"; 253 print "Warning: Reverting to old filename...\n"; 254 } 255 $filename = fileparse($oggfile,'\.ogg'); 256 } 257 else { 258 $filename =~ s/\%a/$oggtags{artist}/g; 259 $filename =~ s/\%t/$oggtags{title}/g; 260 $filename =~ s/\%l/$oggtags{album}/g; 261 $filename =~ s/\%n/$trackNumInfo[0]/g; 262 $filename =~ s/\%N/$trackNumInfo[1]/g; 263 if($lowercase) { 264 $filename = lc($filename); 265 } 266 if(!$noreplace) { 267 $filename =~ s/[\[\]\(\)\{\}!\@#\$\%^&\*\~ ]/_/g; 268 $filename =~ s/[\'\"]//g; 269 } 270 my ($name, $dir, $ext) = fileparse($filename, '.mp3'); 271 $filename = "$dir$name"; 272 } 273 274 my $mp3outputfile = "$filename.mp3"; 275 my $newdir = dirname($mp3outputfile); 276 277 # until i find a way to make perl's mkdir work like mkdir -p... 278 if (! -d $newdir) { 279 system("mkdir -p $newdir"); 280 } 281 282 my $infostring = ""; 283 284 print "Converting $oggfile to MP3...\n"; 285 if (!$verbose) { 286 print "This may take some time depending on your hardware and encoding options,\n"; 287 print "use --verbose to see what's going on,\n"; 288 print "Please wait...\n\n"; 289 } 290 # set verbose to quiet unless verbose is set 291 my $decquiet = "-q"; 292 my $encquiet = "--quiet"; 293 if ($verbose) { 294 $decquiet = ""; 295 $encquiet = ""; 296 print "\n Freq: $frequency kHz\n"; 297 print "Bitrate: $bitrate kbps\tQuality Level: $quality\n"; 298 print " Artist: $oggtags{artist}\n"; 299 print " Album: $oggtags{album}\n"; 300 print " Title: $oggtags{title}\n"; 301 print " Year: $oggtags{date}\n"; 302 print " Genre: $oggtags{genre}\n"; 303 print " Track: $oggtags{tracknumber}\n\n"; 304 } 305 306 if($oggtags{artist} ne "") { 307 $infostring .= " --ta " . shell_quote($oggtags{artist}); 308 } 309 if($oggtags{album} ne "") { 310 $infostring .= " --tl " . shell_quote($oggtags{album}); 311 } 312 if($oggtags{title} ne "") { 313 $infostring .= " --tt " . shell_quote($oggtags{title}); 314 } 315 if($oggtags{tracknumber} ne "") { 316 $infostring .= " --tn " . shell_quote($oggtags{tracknumber}); 317 } 318 if($oggtags{date} ne "") { 319 $infostring .= " --ty " . shell_quote($oggtags{date}); 320 } 321 if($oggtags{genre} ne "") { 322 # need to lowecase ogg tag for match 323 my $genretag = lc($oggtags{genre}); 324 # ogg has spaces in genres underscored, removing them 325 $genretag =~ s/_/ /g; 326 # lookup converted string in genre hash, get ID number 327 $genretag = $genres{$genretag}; 328 # damn, those crazy lame guys use 0 as ID, thanks a bundle 329 if ($genretag || ($genretag == 0)) { 330 $infostring .= " --tg " . shell_quote($genretag); 331 } 332 } 333# if($oggtags->{comment} ne "") { 334# $infostring .= " --comment " . shell_quote("COMMENT=$oggtags->{comment}"); 335# } 336 337 $infostring .= " --tc " . shell_quote("ogg had $oggrates{'nominal bitrate'} kbps $oggrates{rate} Hz"); 338 339 340 my $mp3outputfile_escaped = shell_quote($mp3outputfile); 341 my $oggfile_escaped = shell_quote($oggfile); 342 # this took me some time to figure 343 # note that byte order is swapped by lame via -x option 344 # TODO: somebody please tell me how to supress the "Assuming bla bla" output without devnull 345 my $result = system("$OGG123 $decquiet -d raw -f - $oggfile_escaped | $MP3ENC $encquiet -r -q $quality -b $bitrate -s $frequency -m $channels $infostring - $mp3outputfile_escaped"); 346 347 # TODO: find some widely used mp3 checker 348 # disabled the checking due to lack of checker 349 if(!$result) { 350# open(CHECK,"$MP3INFO $mp3outputfile_escaped |"); 351# while(<CHECK>) 352# { 353# if($_ eq "file_truncated=true\n") 354# { 355# warn "Conversion failed ($mp3outputfile truncated).\n"; 356# close CHECK; 357# return; 358# } 359# elsif($_ eq "header_integrity=fail\n") 360# { 361# warn "Conversion failed ($mp3outputfile header integrity check failed).\n"; 362# close CHECK; 363# return; 364# } 365# elsif($_ eq "stream_integrity=fail\n") 366# { 367# warn "Conversion failed ($mp3outputfile header integrity check failed).\n"; 368# close CHECK; 369# return; 370# } 371# } 372# close CHECK; 373 print "$mp3outputfile done!\n\n"; 374 if($delete) { 375 unlink($oggfile); 376 } 377 378 } 379 else { 380 warn "Conversion failed ($MP3ENC returned $result).\n"; 381 } 382} 383 384 385