#!/usr/bin/perl # Written by Paul Rogers. (C) 2007 and All Rights Reserved. # # This is open source and can be re-used, modified and included in other works, providing # credit is given to the original author on code or ideas used. # # I also make no guarantees or warranties on this program - you use it at your own risk. # # You will need to download metamp3 (only available for Windows as there is no source code). # Obviously, if a similar tool to metamp3 can be found on other platforms, please feel free # to modify this so it runs accordingly. # # http://home.broadpark.no/~tylovset/files/metamp3v092b5.zip # # Also, the Perl library MP3::Tag needs to be installed so tags can be read / written to. # # the_uhu@hotmail.com use MP3::Tag; use Getopt::Long; $_ = 1; $optDebug = 0; $optDir = ""; $optHelp = 0; $optRescan = 0; $optUpdMP3s = 0; GetOptions( 'directory=s' => \$optDir, 'debug' => \$optDebug, 'help' => \$optHelp, 'rescan' => \$optRescan, 'update' => \$optUpdMP3s ); if ($optHelp) { &funcDisplayHelp(); exit (0); } # Location of the metamp3.exe command to scan and set the volume information $glbStrMetaMP3Loc = "."; $glbStrMetaMP3Cmd = "metamp3.exe"; $glbStrMetaMP3Args = "--replay-gain"; $strDir=""; @arrDirList; if ($^O eq "MSWin32") { $strDirSep = "\\"; } else { $strDirSep = "/"; } if ($optDir) { $optDir =~ s/\\+$//; $optDir =~ s/\/+$//; $strDir=$optDir.$strDirSep; } else { $strDir=".".$strDirSep; } if (!$optUpdMP3s) { &funcPrintMsg(2, "Only scanning MP3s - will NOT update files. Use --update switch if otherwise required"); } if ($optDebug) { &funcPrintMsg(0,"strDir: $strDir"); } # Excluded albums titles from album gain tagging: @arrAlbumExclusions = ( "", "\"Live tracks\"" ); # Read list of *.mp3 files from ./ &funcPrintMsg(1, "Reading list of MP3 files from $strDir"); opendir(HNDDIR, $strDir) or die "ERROR! Can't open $strDir: $!"; $tmpIntCnt=0; while( defined ($strFile = readdir HNDDIR) ) { if ($strFile =~ /\.mp3$/) { if ($optDebug) { &funcPrintMsg(0,"Found $strFile"); } push (@arrDirList, $strFile); $tmpIntCnt++; } } closedir(HNDDIR); &funcPrintMsg(1, "Found $tmpIntCnt MP3 files"); if ($tmpIntCnt == 0) { &funcPrintMsg(2,"There are no MP3 files in $strDir. Quitting"); exit(0); } # Open each file and pull Album Artist, Album Title, Track Title, Filename and whether gain info is present # (used later if a command line switch is present to ignore files with gain info already) with a unique ID number into an array @arrMP3List; $intI = 0; $intUniqID = 1; &funcPrintMsg(1, "Reading ID3v2 tags..."); foreach $tmpStrFile (@arrDirList) { $#arrMP3Info = -1; $strFileChk = $strDir.$tmpStrFile; $objMP3 = MP3::Tag->new($strFileChk); $objMP3->get_tags(); if (exists $objMP3->{ID3v2}) { $objMP3ID3v2 = $objMP3->{ID3v2}; } else { &funcPrintMsg(2, "$tmpStrFile does not have any ID3v2 tags - skipping"); next; } ($tmpStrAlbumArtist, $tmpStrLong) = $objMP3ID3v2->get_frame("TPE2"); ($tmpStrAlbumTitle, $tmpStrLong) = $objMP3ID3v2->get_frame("TALB"); ($tmpStrTitle, $tmpStrLong) = $objMP3ID3v2->get_frame("TIT2"); ($tmpHashGainInfo1, $tmpStrLong, @tmpHashGainInfoRest) = $objMP3ID3v2->get_frame("TXXX"); $objMP3->close(); if ($tmpStrAlbumArtist eq "") { &funcPrintMsg(2, "$tmpStrFile does not have an Album Artist set (frame TPE2) - skipping."); next; } $tmpIntGainInfoFound = 0; $tmpStrAlbumGain = ""; $tmpStrAlbumPeak = ""; if ($optDebug) { &funcPrintMsg(0,"Reading ID3v2 tags from $tmpStrFile"); } if ($optDebug) { &funcPrintMsg(0,"Filename: $tmpStrFile | AlbumArtist: $tmpStrAlbumArtist | AlbTit: $tmpStrAlbumTitle | Title: $tmpStrTitle"); } $tmpIntGainInfo = 0; while(my ($tmpGainKey,$tmpGainVal)=each %$tmpHashGainInfo1) { if ($optDebug) { &funcPrintMsg(0,"* $tmpGainKey => $tmpGainVal"); } if ($tmpGainKey eq "Description") { if ($tmpGainVal =~ /^replaygain_/) { $tmpIntGainInfoFound++; } if ($tmpGainVal =~ /_album_gain$/) { $tmpIntGainInfo = 1; } elsif ($tmpGainVal =~ /_album_peak$/) { $tmpIntGainInfo = 2; } } elsif ($tmpGainKey eq "Text") { if ($tmpIntGainInfo == 1) { $tmpStrAlbumGain = $tmpGainVal; } elsif ($tmpIntGainInfo == 2) { $tmpStrAlbumPeak = $tmpGainVal; } } } foreach $tmpHashGainInfo (@tmpHashGainInfoRest) { $tmpIntGainInfo = 0; while (my ($tmpGainKey,$tmpGainVal)=each %$tmpHashGainInfo) { if ($optDebug) { &funcPrintMsg(0,"$tmpGainKey => $tmpGainVal"); } if ($tmpGainKey eq "Description") { if ($tmpGainVal =~ /^replaygain_/) { $tmpIntGainInfoFound++; } if ($tmpGainVal =~ /_album_gain$/) { $tmpIntGainInfo = 1; } elsif ($tmpGainVal =~ /_album_peak$/) { $tmpIntGainInfo = 2; } } elsif ($tmpGainKey eq "Text") { if ($tmpIntGainInfo == 1) { $tmpStrAlbumGain = $tmpGainVal; } elsif ($tmpIntGainInfo == 2) { $tmpStrAlbumPeak = $tmpGainVal; } } } } if ($optDebug) { &funcPrintMsg(0,"| AlbumGain: $tmpStrAlbumGain | AlbumPeak: $tmpStrAlbumPeak | GainInfoCount: $tmpIntGainInfoFound"); } push (@arrMP3Info, $intUniqID, $tmpStrAlbumArtist, $tmpStrAlbumTitle, $tmpStrTitle, $tmpIntGainInfoFound, $tmpStrAlbumGain, $tmpStrAlbumPeak, $tmpStrFile); push (@arrMP3List, [ @arrMP3Info ]); if ($optDebug) { &funcPrintMsg(0,"arrMP3Info: ",1); foreach $tmpStrPrint (@arrMP3Info) { &funcPrintMsg(-1,"| $tmpStrPrint ",1); } &funcPrintMsg(-1,"|"); } $intUniqID++; $intI++; } # Sort the array by Album Artist and Album Title &funcPrintMsg(1, "Sorting list of MP3s found by Album Artist and Album Title"); @arrMP3ListSorted = sort { $a->[1] cmp $b->[1] || $a->[2] cmp $b->[2] } @arrMP3List; if ($optDebug) { &funcPrintMsg(0, "Size of sorted array is ".@arrMP3ListSorted); foreach $tmpRefList (@arrMP3ListSorted) { &funcPrintMsg(0, "@$tmpRefList"); } } # Create a blank second arrary @arrToBeVolumeTagged; # Loop through each second dimension items in the sorted array and add the filename, gain info count, album gain and album peak info to the second array. $tmpStrPrevAlbumArtist = ""; $tmpStrPrevAlbumTitle = ""; $tmpIntI = 0; foreach $tmpRefList (@arrMP3ListSorted) { $#arrVolumeTaggedInfo = -1; &funcCheckIfTagReqd(); if ($optDebug) { &funcPrintMsg(0, "Adding to be tagged list: @$tmpRefList[1] | @$tmpRefList[2] | @$tmpRefList[7] | @$tmpRefList[4] | @$tmpRefList[5] | @$tmpRefList[6]"); } push (@arrVolumeTaggedInfo, @$tmpRefList[1], @$tmpRefList[2], @$tmpRefList[7], @$tmpRefList[4], @$tmpRefList[5], @$tmpRefList[6]); push (@arrToBeVolumeTagged, [ @arrVolumeTaggedInfo ]); $tmpStrPrevAlbumArtist = @$tmpRefList[1]; $tmpStrPrevAlbumTitle = @$tmpRefList[2]; $tmpIntI++; } $tmpRefList = $arrMP3ListSorted[$tmpIntI - 1]; if ($optDebug) { &funcPrintMsg(0, "Test if we have rolled back to the last item (should be non-blank): @$tmpRefList[2]"); } &funcCheckIfTagReqd(); &funcPrintMsg(1, "All done!"); exit(0); # Tagging function - loop through the array passed to us and build a command line to run metamp3.exe to set the gain # correctly. # If a command line switch is present, do NOT remove the gain ID3v2 and APEv2 tags within this loop. # Execute the metamp3.exe command line sub funcTagAlbum { @arrToTag = @_; $tmpStrCmdLine = $glbStrMetaMP3Loc.$strDirSep.$glbStrMetaMP3Cmd; $tmpStrCmdArgs = ""; if ($optRescan) { &funcPrintMsg(1, "Removing existing volume gain information so the files can be rescanned"); } foreach $tmpRefToTagList (@arrToTag) { $strFileChk = $strDir.@$tmpRefToTagList[2]; if ($optRescan) { $objMP3 = MP3::Tag->new($strFileChk); $objMP3->get_tags(); if (exists $objMP3->{ID3v2}) { if ($optDebug) { &funcPrintMsg(0, "Deleting TXXX frame from ID3v2 tag within @$tmpRefToTagList[2]",1); } if ($optUpdMP3s) { # This is really crap. All PERL MP3 libraries can only remove one tag name in one instance of the parent object. # Which means when you have multiple tags using the same name (like the gain information [TXXX]) you have to # create a new object each time to remove an instance of the multiple tag name. GRRRRRRRRRR. $objMP3ID3v2 = $objMP3->{ID3v2}; $objMP3ID3v2->remove_frame("TXXX"); $objMP3ID3v2->write_tag(); $objMP3->close(); $objMP3 = MP3::Tag->new($strFileChk); $objMP3->get_tags(); $objMP3ID3v2 = $objMP3->{ID3v2}; $objMP3ID3v2->remove_frame("TXXX"); $objMP3ID3v2->write_tag(); $objMP3->close(); $objMP3 = MP3::Tag->new($strFileChk); $objMP3->get_tags(); $objMP3ID3v2 = $objMP3->{ID3v2}; $objMP3ID3v2->remove_frame("TXXX"); $objMP3ID3v2->write_tag(); $objMP3->close(); $objMP3 = MP3::Tag->new($strFileChk); $objMP3->get_tags(); $objMP3ID3v2 = $objMP3->{ID3v2}; $objMP3ID3v2->remove_frame("TXXX"); $objMP3ID3v2->write_tag(); $objMP3->close(); if ($optDebug) { &funcPrintMsg(-1, " - DELETED"); } } else { if ($optDebug) { &funcPrintMsg(-1, " - NOT deleted, re-run with --update"); } } } else { &funcPrintMsg(2, "@$tmpRefToTagList[2] does not have any ID3v2 tags - skipping"); } } $tmpStrCmdArgs .= " \"".$strFileChk."\""; } $tmpIntErrCode = 0; $tmpStrErr = ""; if ($optUpdMP3s) { &funcPrintMsg(1, "Executing Tagging utility..."); if ($optDebug) { &funcPrintMsg(0, "Command line: $tmpStrCmdLine $glbStrMetaMP3Args$tmpStrCmdArgs"); } $tmpIntErrCode = system($tmpStrCmdLine, $glbStrMetaMP3Args, $tmpStrCmdArgs); if ($tmpIntErrCode == 256) { $tmpStrErr = "Cannot find / access '$tmpStrCmdLine'. Please check"; } } return ($tmpIntErrCode, $tmpStrErr); } # This function USES GLOBAL VARIABLES to work out if tagging is required or if the process should skip as we are in the same album still sub funcCheckIfTagReqd { if ($optDebug) { &funcPrintMsg(0, "Loop counter: $tmpIntI"); } # Check if the previous track's album is in the excluded album list. $tmpBoolExclAlbum = 0; foreach $tmpStrAlbumExclude (@arrAlbumExclusions) { if ((($tmpStrPrevAlbumTitle eq $tmpStrAlbumExclude) && ($tmpIntI > 0)) || ((@$tmpRefList[2] eq $tmpStrAlbumExclude) && ($tmpIntI == 0))) { $tmpBoolExclAlbum = 1; &funcPrintMsg(1, "Album in the Exclusion list"); last; } } # If the previous second dimenstion item in the array doesn't have a matching Album Artist OR Album Title and is not the first iteration # OR the previous track is in the excluded album list OR it's the last item in the to be sorted list, if ((((@$tmpRefList[1] ne $tmpStrPrevAlbumArtist) || (@$tmpRefList[2] ne $tmpStrPrevAlbumTitle)) && ($tmpIntI > 0)) || ($tmpBoolExclAlbum || ($tmpIntI == @arrMP3ListSorted))) { &funcPrintMsg(1, "Reached the end of '$tmpStrPrevAlbumArtist | $tmpStrPrevAlbumTitle'. ",1); # then loop around the second array and check if the album gain and album peak figures match and that no track has a gain info count < 4 # (providing the album is NOT in the exclusion list). # If they don't OR the size of the second array is 1 OR one or more tracks has a gain info count < 4, # pass the second array to a tagging function. # Otherwise do nothing as the list of tracks has the same album volume tag info and are fully populated. $tmpBoolEnoughGainInfo = 1; $tmpBoolAlbumGainInfoMatches = 1; $tmpStrCheckAlbumGain = ""; $tmpStrCheckAlbumPeak = ""; foreach $tmpRefTBTList (@arrToBeVolumeTagged) { if (($tmpBoolExclAlbum) || (@arrToBeVolumeTagged == 1)) { if (@$tmpRefTBTList[3] != 2) { $tmpBoolEnoughGainInfo = 0; } } else { if (@$tmpRefTBTList[3] < 4) { $tmpBoolEnoughGainInfo = 0; } if ((($tmpStrCheckAlbumGain ne @$tmpRefTBTList[4]) && ($tmpStrCheckAlbumGain ne "")) || (($tmpStrCheckAlbumPeak ne @$tmpRefTBTList[5]) && ($tmpStrCheckAlbumPeak ne ""))) { $tmpBoolAlbumGainInfoMatches = 0; } $tmpStrCheckAlbumGain = @$tmpRefTBTList[4]; $tmpStrCheckAlbumPeak = @$tmpRefTBTList[5]; } } if (!$tmpBoolEnoughGainInfo || !$tmpBoolAlbumGainInfoMatches || $optRescan) { &funcPrintMsg(-1, "Tags need updating..."); ($tmpIntErrCode, $tmpStrErr) = &funcTagAlbum(@arrToBeVolumeTagged); if ($tmpIntErrCode != 0) { &funcPrintMsg(3, "Unable to tag the track(s). Error: $tmpIntErrCode $tmpStrErr"); exit(1); } } else { &funcPrintMsg(-1, "Tags do NOT need updating..."); } # Blank the second array at the end of the above IF block. $#arrVolumeTaggedInfo = -1; $#arrToBeVolumeTagged = -1; } } # Displays help on the console sub funcDisplayHelp { print "Usage:\n"; print "======\n\n"; print " $0 [--debug] [--directory=] [--rescan] [--update]|--help\n\n"; print " --debug Displays debug processing information\n"; print " --directory= Script will search for MP3s to analyse their\n"; print " volume gain information. By default, we use the current\n"; print " directory\n"; print " --help Displays this help information\n"; print " --rescan If volume gain information is found, instead of skipping\n"; print " the tracks, they will have the tag information deleted\n"; print " before scanning / updates take place (it's a limitation\n"; print " in metamp3.exe unfortunately)\n"; print " --update Updates the MP3s with volume gain information if it\n"; print " is found to be missing or inconsistent within the\n"; print " MP3s. It will also set Album Gain details for tracks\n"; } sub funcPrintMsg { ($tmpIntType, $tmpStrMessage, $tmpBoolNoNewline) = ($_[0], $_[1], $_[2]); if ($tmpIntType >= 0) { if ($tmpIntType == 0) { $tmpStrType = "DEBUG"; } elsif ($tmpIntType == 1) { $tmpStrType = "INFO"; } elsif ($tmpIntType == 2) { $tmpStrType = "WARNING"; } elsif ($tmpIntType == 3) { $tmpStrType = "ERROR!"; } else { $tmpStrType = "UNKNOWN"; } ($tmpIntSec,$tmpIntMin,$tmpIntHour,$tmpIntMDay,$tmpIntMon,$tmpIntYear,$tmpIntWDay,$tmpIntYDay,$tmpBoolDst)=localtime(time); printf "%4d-%02d-%02d %02d:%02d:%02d",$tmpIntYear+1900,$tmpIntMon+1,$tmpIntMDay,$tmpIntHour,$tmpIntMin,$tmpIntSec; print " - "; printf "%-7s", $tmpStrType; print " - "; } print "$tmpStrMessage"; if (!$tmpBoolNoNewline) { print "\n"; } }