Adding Replay Gain in Linux Automatically

UPDATE February 1, 2010: This post is now somewhat obsolete, as mp3gain supports writing to ID3v2 tags instead of APE tags. Read the warning below.

UPDATE July 17, 2010: WARNING: mp3gain version 1.5.1 (latest stable command line version) seems to corrupt ID3v2 tag information with the “-s i” option (write info into ID3v2 tags instead of default APE tags). For me, it seems to corrupt the JPEG image data inside the ID3v2 tag for some reason. If you care about the existing ID3v2 tags in your mp3 files, do NOT use mp3gain on them with the “-s i” option (the “-s a” option which is the default and only writes to APE tags, does not do any harm if you only care about APE tags). A good tool to check your tag info (ID3v2, APE, etc.) is with MP3 Diags (if you’re on Arch Linux, get it here). So for now, the method below is still my preferred way of doing things (mp3gain with APE and then

Replay gain tags in mp3, flac, and ogg files lets the player adjust the volume accordingly to make the song sound not too loud and not too soft. The Music Player Daemon (aka MPD), my favorite music player, recognizes replay gain tags if present. However, for mp3 files, the popular APE format for replay gain tags are not supported; MPD can only read ID3 tags for replay gain for mp3’s. Luckily, an mp3 file can have both APE and ID3 tags. This means that we can use mp3gain (a cute, simple command-line app available in pretty much every Linux distro) to add APE tags into our mp3s with:

mp3gain [file(s)]

This will add APE replay gain tags into the mp3 file(s) chosen. Then, we can use a simple script to read these APE replay gain tags, convert them into ID3 tags, and put these ID3 tags into the same mp3s. Such a script, thankfully, already exists here. Now, the mp3 file will have both APE and ID3 tags for replay gain values! And MPD will happily use the ID3 values.

If you’re in a hurry, you’d automate this process for entire directories, recursively. That’s what this most excellent Linux page on replay gain page suggests. (You should REALLY bookmark that page, as it has the best information hands down about replay gain in Linux.) I’ve modified the scripts for metaflac there to make it work with mp3’s instead. Here are the two scripts:

Script A:

# Define error codes
# Check that the argument passed to this script is a directory.
# If it's not, then exit with an error code.
if [ ! -d "$1" ]
    echo -e "33[1;37;44m Arg "$1" is NOT a directory!33[0m"
echo -e "33[1;37m********************************************************33[0m"
echo -e "33[1;37mCalling on each directory in:33[0m"
echo -e "33[1;36m"$1"33[0m"
echo ""
find "$1" -type d -exec ~/syscfg/shellscripts/replaygain/mp3/ '{}' \;

Script B (the ‘’ script referenced above in Script A):

# Error codes
# Check that the argument passed to this script is a directory.
# If it's not, then exit with an error code.
if [ ! -d "$1" ]
    echo -e "33[1;37mArg "$1" is NOT a directory!33[0m"
# Count the number of mp3 files in this directory.
mp3num=`ls "$1" | grep -c \\.mp3`
# If no mp3 files are found in this directory,
# then exit without error.
if [ $mp3num -lt 1 ]
    echo -e "33[1;33m"$1" 33[1;37m--> (No mp3 files found)33[0m"
    exit 0
    echo -e "33[1;36m"$1" 33[1;37m--> (33[1;32m"$mp3num"33[1;37m mp3 files)33[0m"
# Run mp3gain on the mp3 files in this directory.
echo -e ""
echo -e "33[1;37mForcing (re)calculation of Replay Gain values for mp3 files and adding them as APE2 tags into the mp3 file...33[0m"
echo -e ""
# first delete any APE replay gain tags in the files
mp3gain -s d "$1"/*.mp3
# add fresh APE tags back into the files
mp3gain "$1"/*.mp3
echo -e ""
echo -e "33[1;37mDone.33[0m"
echo -e ""
echo -e "33[1;37mAdding ID3 tags with the same calculated info from above...33[0m"
echo -e ""
# the -d is for debug messages if there are any errors, and the -f is for overwriting any existing ID3 replay gain tags
~/syscfg/shellscripts/replaygain/mp3/ -df "$1"/*.mp3
echo -e ""
echo -e "33[1;37mDone.33[0m"
echo -e ""
echo -e "33[1;37mReplay gain tags (both APE and ID3) successfully added recursively.33[0m"
echo -e ""

And here is the APE to ID3 conversion script from the link above (the ‘’ script called from Script B):

#! /usr/bin/env python

import sys
from optparse import OptionParser

import mutagen
from mutagen.apev2 import APEv2
from mutagen.id3 import ID3, TXXX

def convert_gain(gain):
   if gain[-3:] == " dB":
       gain = gain[:-3]
       gain = float(gain)
   except ValueError:
       raise ValueError, "invalid gain value"
   return "%.2f dB" % gain

def convert_peak(peak):
       peak = float(peak)
   except ValueError:
       raise ValueError, "invalid peak value"
   return "%.6f" % peak

   ("mp3gain_album_minmax", None),
   ("mp3gain_minmax", None),
   ("replaygain_album_gain", convert_gain),
   ("replaygain_album_peak", convert_peak),
   ("replaygain_track_gain", convert_gain),
   ("replaygain_track_peak", convert_peak),

class Logger(object):
   def __init__(self, log_level, prog_name):
       self.log_level = log_level
       self.prog_name = prog_name
       self.filename = None

   def prefix(self, msg):
       if self.filename is None:
           return msg
       return "%s: %s" % (self.filename, msg)

   def debug(self, msg):
       if self.log_level >= 4:
           print self.prefix(msg)

   def info(self, msg):
       if self.log_level >= 3:
           print self.prefix(msg)

   def warning(self, msg):
       if self.log_level >= 2:
           print self.prefix("WARNING: %s" % msg)

   def error(self, msg):
       if self.log_level >= 1:
           sys.stderr.write("%s: %s\n" % (self.prog_name, msg))

   def critical(self, msg, retval=1):

class Ape2Id3(object):
   def __init__(self, logger, force=False):
       self.log = logger
       self.force = force

   def convert_tag(self, name, value):

   def copy_replaygain_tag(self, apev2, id3, name, converter=None):
       self.log.debug("processing '%s' tag" % name)

       if not apev2.has_key(name):
 "no APEv2 '%s' tag found, skipping tag" % name)
           return False
       if not self.force and id3.has_key("TXXX:%s" % name):
 "ID3 '%s' tag already exists, skpping tag" % name)
           return False

       value = str(apev2[name])
       if callable(converter):
           self.log.debug("converting APEv2 '%s' tag from '%s'" %
                          (name, value))
               value = converter(value)
           except ValueError:
               self.log.warning("invalid value for APEv2 '%s' tag" % name)
               return False
           self.log.debug("converted APEv2 '%s' tag to '%s'" % (name, value))

       id3.add(TXXX(encoding=1, desc=name, text=value))"added ID3 '%s' tag with value '%s'" % (name, value))
       return True

   def copy_replaygain_tags(self, filename):
       self.log.filename = filename
       self.log.debug("begin processing file")

           apev2 = APEv2(filename)
       except mutagen.apev2.error:
 "no APEv2 tag found, skipping file")
       except IOError:
           e = sys.exc_info()
           self.log.error("%s" % e[1])

           id3 = ID3(filename)
       except mutagen.id3.error:
 "no ID3 tag found, creating one")
           id3 = ID3()

       modified = False
       for name, converter in REPLAYGAIN_TAGS:
           copied = self.copy_replaygain_tag(apev2, id3, name, converter)
           if copied:
               modified = True
       if modified:
           self.log.debug("saving modified ID3 tag")

       self.log.debug("done processing file")
       self.log.filename = None

def main(prog_name, options, args):
   logger = Logger(options.log_level, prog_name)
   ape2id3 = Ape2Id3(logger, force=options.force)
   for filename in args:

if __name__ == "__main__":
   parser = OptionParser(version="0.1", usage="%prog [OPTION]... FILE...",
                         description="Copy APEv2 ReplayGain tags on "
                                     "FILE(s) to ID3v2.")
   parser.add_option("-q", "--quiet", dest="log_level",
                     action="store_const", const=0, default=1,
                     help="do not output error messages")
   parser.add_option("-v", "--verbose", dest="log_level",
                     action="store_const", const=3,
                     help="output warnings and informational messages")
   parser.add_option("-d", "--debug", dest="log_level",
                     action="store_const", const=4,
                     help="output debug messages")
   parser.add_option("-f", "--force", dest="force",
                     action="store_true", default=False,
                     help="force overwriting of existing ID3v2 "
                          "ReplayGain tags")
   prog_name = parser.get_prog_name()
   options, args = parser.parse_args()

   if len(args) < 1:
       parser.error("no files specified")

       main(prog_name, options, args)
   except KeyboardInterrupt:

# vim: set expandtab shiftwidth=4 softtabstop=4 textwidth=79:

It’s pretty simple. Script A just calls Script B recursively on every directory found inside the designated directory. Script B finds mp3 files, and first tags them with APE replay gain tags with mp3gain. Then, it calls the APE to ID3 conversion script above to add equivalent ID3 tags into those same mp3s. I’ve modified Script B so that it first deletes any APE replay gain tags already present in the mp3 file before doing the replay gain calculations — but this is optional. I also added a bunch of ANSI color escape codes to Script A and B so that they look prettier. These three scripts work beautifully well together with mp3 files inside directories. However, the directories MUST be album directories, as all mp3 files found in a directory are treated as having come from the same album for album replay gain tags (track replay gain tags are always independent on a per-file basis).

I should probably rewrite Script A and B in Python to make it easier to maintain — but everything is pretty simple as it is. If you have 1 big folder full of mp3’s from different artists/albums, then you could change the

mp3gain "$1"/*.mp3

in Script B into something like

for file in $mp3files
    mp3gain "$file"

This way, mp3gain is called separately for each mp3 file (instead of being called once for all mp3 files in the folder). Now you don’t have to worry about the mp3’s in that folder being treated as having come from 1 album for those album gain tags. To top things off, you should edit your shell’s config (e.g., .zshrc), and alias Script A to something easy, like rgmp3, so that you can just do

rgmp3 [directory]

to get this whole thing to work. Now run this command on your master mp3 folder, take a nap, and come back. All your mp3’s will now have both APE and ID3 replay gain tags!

I hope people find this useful. I’ve googled and googled countless times about replay gain in the past, and until I discovered the excellent link mentioned above a couple days ago, I could never really get replay gain tags working for my mp3’s.