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