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 ape2id3.py).
Replay gain Music Player Daemon (aka MPD), my favorite music player, recognizes replay gain s if present. However, for mp3 files, the popular APE format for replay gain s are not supported; MPD can only read ID3 s for replay gain for mp3’s. Luckily, an mp3 file can have both APE and ID3 s. This means that we can use mp3gain (a cute, simple command-line app available in pretty much every Linux distro) to add APE s into our mp3s with:
s 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. Themp3gain [file(s)]
This will add APE replay gain here. Now, the mp3 file will have both APE and ID3 s for replay gain values! And MPD will happily use the ID3 values.
s into the mp3 file(s) chosen. Then, we can use a simple script to read these APE replay gain s, convert them into ID3 s, and put these ID3 s into the same mp3s. Such a script, thankfully, already existsIf 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:
#!/bin/bash # Define error codes ARGUMENT_NOT_DIRECTORY=10 FILE_NOT_FOUND=11 # Check that the argument passed to this script is a directory. # If it's not, then exit with an error code. if [ ! -d "$1" ] then echo -e "33[1;37;44m Arg "$1" is NOT a directory!33[0m" exit $ARGUMENT_NOT_DIRECTORY fi echo -e "33[1;37m********************************************************33[0m" echo -e "33[1;37mCalling tag-mp3-with-rg.sh on each directory in:33[0m" echo -e "33[1;36m"$1"33[0m" echo "" find "$1" -type d -exec ~/syscfg/shellscripts/replaygain/mp3/tag-mp3-with-rg.sh '{}' \;
Script B (the ‘
-mp3-with-rg.sh’ script referenced above in Script A):#!/bin/bash # Error codes ARGUMENT_NOT_DIRECTORY=10 FILE_NOT_FOUND=11 # Check that the argument passed to this script is a directory. # If it's not, then exit with an error code. if [ ! -d "$1" ] then echo -e "33[1;37mArg "$1" is NOT a directory!33[0m" exit $ARGUMENT_NOT_DIRECTORY fi # 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 ] then echo -e "33[1;33m"$1" 33[1;37m--> (No mp3 files found)33[0m" exit 0 else echo -e "33[1;36m"$1" 33[1;37m--> (33[1;32m"$mp3num"33[1;37m mp3 files)33[0m" fi # 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/ape2id3.py -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 ‘ape2id3.py’ 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] try: gain = float(gain) except ValueError: raise ValueError, "invalid gain value" return "%.2f dB" % gain def convert_peak(peak): try: peak = float(peak) except ValueError: raise ValueError, "invalid peak value" return "%.6f" % peak REPLAYGAIN_TAGS = ( ("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): self.error(msg) sys.exit(retval) class Ape2Id3(object): def __init__(self, logger, force=False): self.log = logger self.force = force def convert_tag(self, name, value): pass def copy_replaygain_tag(self, apev2, id3, name, converter=None): self.log.debug("processing '%s' tag" % name) if not apev2.has_key(name): self.log.info("no APEv2 '%s' tag found, skipping tag" % name) return False if not self.force and id3.has_key("TXXX:%s" % name): self.log.info("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)) try: 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)) self.log.info("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") try: apev2 = APEv2(filename) except mutagen.apev2.error: self.log.info("no APEv2 tag found, skipping file") return except IOError: e = sys.exc_info() self.log.error("%s" % e[1]) return try: id3 = ID3(filename) except mutagen.id3.error: self.log.info("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") id3.save(filename) 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: ape2id3.copy_replaygain_tags(filename) 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") try: main(prog_name, options, args) except KeyboardInterrupt: pass # 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
s them with APE replay gain s with mp3gain. Then, it calls the APE to ID3 conversion script above to add equivalent ID3 s into those same mp3s. I’ve modified Script B so that it first deletes any APE replay gain s 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 s (track replay gain s 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 do mp3gain "$file" done
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
s. 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 dorgmp3 [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
s working for my mp3’s.