Before I head to the beach for the rest of the week (I won’t be at Game Night™), I thought I would unleash unto the world some code I wrote a while back. My brothers and I like to share a modest music library. I encoded my library in Apple’s AAC format using iTunes, while my brothers wanted everything to be in MP3 format to be able to play it on their portable devices. So I wrote a Perl script to combine the powers of faad, lame, and id3v2. I set up a cron job to run this script every night.
Basically, it makes a copy of a directory structure, converting the M4A files to MP3, and it also uses the same modify times. Before converting each file, if there is already an existing MP3, it compares the modify times of each file. If the M4A is newer, it performs the copy.
Use at your own peril.
#!/usr/bin/perl
###############################################################################
#
# aac2mp3.pl
#
# usage: aac2mp3.pl <aac_dir> <mp3_dir>
#
# This script will convert a directory full of AAC files (with .m4a extension)
# into MP3 format. You will need to have faad (AAC decoder), lame (MP3
# encoder), and id3v2 (ID3 tag editor) installed for this to work. The script
# assumes the structure of the directory is of the form "Artist/Album/Track",
# as if you had let iTunes organize your music. The script will preserve the
# following tag information: Title, Artist, Album, Year, Composer, Genre,
# Track Number out of Total Number of Tracks, Disc Number out of Total Number
# of Discs, and Album Artist.
#
###############################################################################
use strict;
use File::stat;
my $aac_dir = $ARGV[0];
my $mp3_dir = $ARGV[1];
mkdir $mp3_dir unless (-e $mp3_dir);
# open the main music directory containing the AAC files
opendir(my $musicdir, $aac_dir);
my @artists = grep(!/^\./, readdir($musicdir));
foreach my $artist (@artists) {
# open the artist's directory
opendir(my $artistdir, "$aac_dir/$artist");
my @albums = grep(!/^\./, readdir($artistdir));
foreach my $album (@albums) {
# open the album's directory
opendir(my $albumdir, "$aac_dir/$artist/$album");
my @tracks = grep(/.*\.m4a/, readdir($albumdir));
foreach my $track (@tracks) {
convert_track($artist, $album, $track);
}
closedir($albumdir);
}
closedir($artistdir);
}
closedir($musicdir);
###############################################################################
# Convert a track from AAC/MP4/M4A to MP3.
# arguments:
# artist - the directory name containing the artist's albums
# album - the subdirectory name containing the artist's album's tracks
# track - the filename for the track to be converted
###############################################################################
sub convert_track {
my ($artist, $album, $track) = @_;
my $filename;
my $tag_options;
my $dirname = "$aac_dir/$artist/$album";
chdir $dirname or die "Can't cd to $dirname\n";
$filename = $track;
# First we check to see if the MP3 exists and is not older than the M4A
# Get the track's modification and access times
my $aac_stat = stat($filename) or die "stat error: $!: $filename\n";
my $mtime = $aac_stat->mtime;
my $atime = $aac_stat->atime; # We use this when setting the MP3's times
$filename =~ s/\.m4a/\.mp3/g;
my $mp3file = "$mp3_dir/$artist/$album/$filename";
# Check the mp3's mod time, and skip if not older than the m4a
if (-e $mp3file) {
my $mp3_stat = stat($mp3file) or die "stat error: $!: $mp3file\n";
return if ($mtime <= $mp3_stat->mtime);
}
# Here we convert the track if the MP3 does not exist or is older
print "Converting $dirname/$track\n";
$track = quotemeta($track);
`faad -q -o /tmp/fifo.wav $track`;
#`faad -o /tmp/fifo.wav $track`;
# print a message if there has been an error
print "faad error: $artist/$album/$track: $?\n" if $?;
my %tag = get_tag($track);
# We have to set up the directories for the mp3 files
$dirname = "$mp3_dir/$artist";
mkdir $dirname unless (-e $dirname);
$dirname = "$mp3_dir/$artist/$album";
mkdir $dirname unless (-e $dirname);
chdir $dirname;
$track =~ s/\.m4a/\.mp3/g;
`lame -S -V 2 -b 192 -h /tmp/fifo.wav $track`;
#`lame -V 2 -b 256 -h /tmp/fifo.wav $track`;
# print a message if there has been an error
print "lame error: $artist/$album/$track: $?\n" if $?;
# set up the tag for the new mp3 file
`id3v2 --song "$tag{'title'}" $track`;
`id3v2 --artist "$tag{'artist'}" $track`;
`id3v2 --album "$tag{'album'}" $track`;
`id3v2 --year "$tag{'date'}" $track`;
`id3v2 --TCOM "$tag{'writer'}" $track`;
`id3v2 --genre "$tag{'genre'}" $track`;
`id3v2 --track "$tag{'track'}/$tag{'totaltracks'}" $track`;
`id3v2 --TPOS "$tag{'disc'}/$tag{'totaldiscs'}" $track`;
`id3v2 --TPE2 "$tag{'album_artist'}" $track`;
# set the mp3's modification and access times to the same as the m4a's
utime $atime, $mtime, $filename or die "utime error: $!: $filename\n";
`rm /tmp/fifo.wav`;
}
###############################################################################
# Get a track's tag information. The function assumes you are already in the
# directory that contains the track.
# arguments:
# filename - the filename for the track; assumed to be fully escaped
###############################################################################
sub get_tag {
my ($filename) = @_;
my %tag = ();
# We have to open a pipe to get the info because faad displays it on stderr
open (PIPE, "faad -i $filename 2>&1 |");
while (<PIPE>) {
if (/^title: (.*)$/) {
$tag{'title'} = $1;
}
elsif (/^artist: (.*)$/) {
$tag{'artist'} = $1;
}
elsif (/^album: (.*)$/) {
$tag{'album'} = $1;
}
elsif (/^genre: (.*)$/) {
$tag{'genre'} = $1;
}
elsif (/^track: (.*)$/) {
$tag{'track'} = $1;
}
elsif (/^totaltracks: (.*)$/) {
$tag{'totaltracks'} = $1;
}
elsif (/^disc: (.*)$/) {
$tag{'disc'} = $1;
}
elsif (/^totaldiscs: (.*)$/) {
$tag{'totaldiscs'} = $1;
}
elsif (/^date: (.*)$/) {
$tag{'date'} = $1;
}
elsif (/^album_artist: (.*)$/) {
$tag{'album_artist'} = $1;
}
elsif (/^writer: (.*)$/) {
$tag{'writer'} = $1;
}
}
return %tag;
}
###############################################################################
#
# aac2mp3.pl
#
# usage: aac2mp3.pl <aac_dir> <mp3_dir>
#
# This script will convert a directory full of AAC files (with .m4a extension)
# into MP3 format. You will need to have faad (AAC decoder), lame (MP3
# encoder), and id3v2 (ID3 tag editor) installed for this to work. The script
# assumes the structure of the directory is of the form “Artist/Album/Track”,
# as if you had let iTunes organize your music. The script will preserve the
# following tag information: Title, Artist, Album, Year, Composer, Genre,
# Track Number out of Total Number of Tracks, Disc Number out of Total Number
# of Discs, and Album Artist.
#
###############################################################################
use strict;
use File::stat;
my $aac_dir = $ARGV[0];
my $mp3_dir = $ARGV[1];
mkdir $mp3_dir unless (-e $mp3_dir);
# open the main music directory containing the AAC files
opendir(my $musicdir, $aac_dir);
my @artists = grep(!/^\./, readdir($musicdir));
foreach my $artist (@artists) {
# open the artist’s directory
opendir(my $artistdir, “$aac_dir/$artist”);
my @albums = grep(!/^\./, readdir($artistdir));
foreach my $album (@albums) {
# open the album’s directory
opendir(my $albumdir, “$aac_dir/$artist/$album”);
my @tracks = grep(/.*\.m4a/, readdir($albumdir));
foreach my $track (@tracks) {
convert_track($artist, $album, $track);
}
closedir($albumdir);
}
closedir($artistdir);
}
closedir($musicdir);
###############################################################################
# Convert a track from AAC/MP4/M4A to MP3.
# arguments:
# artist – the directory name containing the artist’s albums
# album – the subdirectory name containing the artist’s album’s tracks
# track – the filename for the track to be converted
###############################################################################
sub convert_track {
my ($artist, $album, $track) = @_;
my $filename;
my $tag_options;
my $dirname = “$aac_dir/$artist/$album”;
chdir $dirname or die “Can’t cd to $dirname\n”;
$filename = $track;
# First we check to see if the MP3 exists and is not older than the M4A
# Get the track’s modification and access times
my $aac_stat = stat($filename) or die “stat error: $!: $filename\n”;
my $mtime = $aac_stat->mtime;
my $atime = $aac_stat->atime; # We use this when setting the MP3′s times
$filename =~ s/\.m4a/\.mp3/g;
my $mp3file = “$mp3_dir/$artist/$album/$filename”;
# Check the mp3′s mod time, and skip if not older than the m4a
if (-e $mp3file) {
my $mp3_stat = stat($mp3file) or die “stat error: $!: $mp3file\n”;
return if ($mtime <= $mp3_stat->mtime);
}
# Here we convert the track if the MP3 does not exist or is older
print “Converting $dirname/$track\n”;
$track = quotemeta($track);
`faad -q -o /tmp/fifo.wav $track`;
#`faad -o /tmp/fifo.wav $track`;
# print a message if there has been an error
print “faad error: $artist/$album/$track: $?\n” if $?;
my %tag = get_tag($track);
# We have to set up the directories for the mp3 files
$dirname = “$mp3_dir/$artist”;
mkdir $dirname unless (-e $dirname);
$dirname = “$mp3_dir/$artist/$album”;
mkdir $dirname unless (-e $dirname);
chdir $dirname;
$track =~ s/\.m4a/\.mp3/g;
`lame -S -V 2 -b 192 -h /tmp/fifo.wav $track`;
#`lame -V 2 -b 256 -h /tmp/fifo.wav $track`;
# print a message if there has been an error
print “lame error: $artist/$album/$track: $?\n” if $?;
# set up the tag for the new mp3 file
`id3v2 –song “$tag{‘title’}” $track`;
`id3v2 –artist “$tag{‘artist’}” $track`;
`id3v2 –album “$tag{‘album’}” $track`;
`id3v2 –year “$tag{‘date’}” $track`;
`id3v2 –TCOM “$tag{‘writer’}” $track`;
`id3v2 –genre “$tag{‘genre’}” $track`;
`id3v2 –track “$tag{‘track’}/$tag{‘totaltracks’}” $track`;
`id3v2 –TPOS “$tag{‘disc’}/$tag{‘totaldiscs’}” $track`;
`id3v2 –TPE2 “$tag{‘album_artist’}” $track`;
# set the mp3′s modification and access times to the same as the m4a’s
utime $atime, $mtime, $filename or die “utime error: $!: $filename\n”;
`rm /tmp/fifo.wav`;
}
###############################################################################
# Get a track’s tag information. The function assumes you are already in the
# directory that contains the track.
# arguments:
# filename – the filename for the track; assumed to be fully escaped
###############################################################################
sub get_tag {
my ($filename) = @_;
my %tag = ();
# We have to open a pipe to get the info because faad displays it on stderr
open (PIPE, “faad -i $filename 2>&1 |”);
while (<PIPE>) {
if (/^title: (.*)$/) {
$tag{‘title’} = $1;
}
elsif (/^artist: (.*)$/) {
$tag{‘artist’} = $1;
}
elsif (/^album: (.*)$/) {
$tag{‘album’} = $1;
}
elsif (/^genre: (.*)$/) {
$tag{‘genre’} = $1;
}
elsif (/^track: (.*)$/) {
$tag{‘track’} = $1;
}
elsif (/^totaltracks: (.*)$/) {
$tag{‘totaltracks’} = $1;
}
elsif (/^disc: (.*)$/) {
$tag{‘disc’} = $1;
}
elsif (/^totaldiscs: (.*)$/) {
$tag{‘totaldiscs’} = $1;
}
elsif (/^date: (.*)$/) {
$tag{‘date’} = $1;
}
elsif (/^album_artist: (.*)$/) {
$tag{‘album_artist’} = $1;
}
elsif (/^writer: (.*)$/) {
$tag{‘writer’} = $1;
}
}
return %tag;
}