Shinobu’s Secrets

March 26, 2009

Adding Replay Gain in Linux Automatically

Filed under: Linux, Music, Python — Shinobu @ 7:25 am

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:

#!/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 'tag-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 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
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 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.

January 17, 2009

How to quickly make a playlist for mplayer

Filed under: FYI, Linux, Music — Shinobu @ 11:37 pm

Mplayer uses a simple kind of playlist: a text file with the path and name of each file to be played. The path to the new file is relative to the location of the playlist file itself. So, you can do something like this:

find -maxdepth 1 -type f -name \*.\* > playlist

This will find all the files in the current directory that have a “.” character in it, and put the results into playlist (a text file) — i.e., it will find all files with some kind of extension, and exclude directory names with a period in them. This works well if the only type of files with extensions are audio files. If you want to search deeper down directory levels, just increase the maxdepth value, or leave out the maxdepth parameter altogether to search recursively.

The “>” operator replaces whatever text was inside the “playlist” file with the output of the previous command (here, the find command with all our options). If you just wanted to append the results to an existing playlist file, you could just use “>>” instead of “>”.

To play the playlist, just do:

mplayer -playlist playlist

…where “playlist” is the file with all the songs in it. Be sure to include the full path to the playlist if you are currently not inside the same directory. To make the entire playlist loop forever, type:

mplayer -playlist playlist -loop 0

and it will loop the entire playlist forever. Unfortunately, putting the “loop=0″ info in my ~/.mplayer/config file makes mplayer read that paramter first, and thus, only repeat the first file in my playlist forever. There seems to be no workaroud to this, except manually appending the loop paramter after the playlist parameter, as shown above.

More info here.

UPDATE November 12, 2009: I discovered a simple hack around the above mentioned problem about trying to loop the entire playlist. The solution is to remove the “loop=0″ line from your ~/.mplayer/config, and instead make use of shell aliases. I use zsh, and this is what I have in my ~/.zshrc (the second alias is the key):

alias m='mplayer -loop 0'
alias mp='mplayer -loop 0 -playlist'

Now, to play any single file infinitely, simply use “m [file]“. To infinitely loop an entire playlist, simply do “mp [playlist]“. You can “>” and “<” hotkeys to move around the playlist.

December 29, 2008

A quick update on what i’ve been up to

Filed under: Linux, Music, Recreation, Updates, Vacation — Shinobu @ 12:54 am

I’ve done a few things here and there this winter break. My law school career is coming to an end — I’ve just 1 more semester to go! Anyway, you can tell from this blog’s evolution that that I like to blog mostly about techie/geeky/nerd things like computer programming, linux, and the like. So that’s what I’ve been doing these days — my most precious, last real “winter break” before getting a job in the real world. Yes, I could work for someone — but I’m going to be working the rest of my life!

Some things I’ve done recently, or am getting into:

  • I got rid of Xubuntu (gasp!) on my old laptop, and replaced it with Arch Linux, after hearing one rave review of it after another. This is the distro that has slowly and steadily climed up in popularity on DistroWatch, enough to rival and surpass Gentoo, while being similarly focused on simplicity and end-user configuration (not “factory defaults” like Ubuntu or Mint). The install and setup wasn’t so smooth, as the wikis and beginner guides were, though highly informative, not really comprehensive. But after I got it running — I too have been generally impressed with Arch, and the whole Arch community, and its pacman packaging system, along with the ABS/AUR duality for getting packages.
  • I started learning text-based (aka “console” or CLI for command line interface) clients for many common things I do on the computer. So far I’ve managed to use and configure irssi for IRC, and I also use aria2 for bittorrent. I want to look into tmux (replacement for GNU Screen), alpine (email client), bitlbee (for AIM, MSN messaging from inside irssi), and clex (file manager). Why a sudden emphasis on console clients, instead of graphical ones based on GTK or KDE? Well, for one, text-based ones run faster, and are more stable. And once you set up text-based clients and have them configured properly, they are easier to use and are much more efficient time-wise to get the same thing done.
  • I switched my default shell from bash to zsh. The tab-completion feature alone makes the switch worth it, in my opinion, as well as the somewhat-difficult-but-doable themeing of the prompt. There are tons and tons of features in zsh that are customizable (the man page for zsh is broken up into 17 sections!), that I will slowly start to learn as I get more and more into programming as a hobby (shell programming, to be more exact).
  • I stopped using xfce4-terminal (aka “Terminal” in the XFCE desktop environment) because of its poor color support (it only has 16? colors) and, after a series of changes, finally settled on a custom, AUR-based package of urxvt (rxvt-unicode) that supports 256 colors. Now my vim looks virtually the same as my gvim using the zenburn theme. Yay!
  • I started exploring other programming languages a little bit. So far, I’ve decided to get more into Haskell and Factor. Haskell has a reputation for being rock solid, from what I can glean from the blogs and news sites out there, while Factor is cute and interesting with its stack programming model. I also want to ditch Ruby and get into Python (I’m tired of writing “end” over and over again), so I’ll look into that more in the future (although, this would mean that I would have to rewrite my custom Rails app that I made last year using Django or something else, even — but I think the sacrifice would be worth it).
  • I got back into practicing the guitar again — but this time focusing only on my classical guitar and playing older songs (not modern ones from rock bands) like the short pieces by Carcassi.

I’ll probably write the obligatory Arch Linux “first impressions” review in the future, from my unique experience of having switched from Linux Mint to Xubuntu to Arch Linux in the course of about 1 year, never having used Linux in the past. Was the switch to Arch worth it? Yes. I’ll explain as much as I can, and why you should also switch, in the review.

August 27, 2006

Two weeks into law school

Filed under: Law School, Music, Updates — Shinobu @ 11:07 am

So those of you who have been reading this site, you’ve been wondering what ever happened to me this past month and two days. Well, like the title of this post explains, I’ve started law school at Mystery Law School. It’s a rather small school (well, considering the tiny number of fellow first-years compared to my undergraduate class, which was a behemoth), but I like the feel of a small, reclusive school after 4 years of invite-all, partyhouse, “twenty-five people from my high school are here” Mystery College. That’s not to say that I didn’t enjoy my college years of being around my fellow undergraduate colleagues. But over all, I enjoy being here in a small, focused setting surrounded by people who are all motivated to get a law degree. No more undergrads who don’t like their major! Everyone here loves law! Haha.

So I ended up finishing that book about How to Learn Anything Quickly. It was a very quick read, and I must admit that it was meant more for educators than students. The biggest drawback for me was that, before reading this book, I already had quite an accurate idea of my strongest learning style, or “superlink” as Linksman puts it.

Getting to Maybe is proving to be an excellent guide to law exams. I highly recommend it. Interestingly, the IRAC format isn’t very appreciated by this book, though it’s been widely discussed by both my Civ Pro and Writing & Research professors. We’ll see how it goes come midterm time!

I ended up returning The Portrait of a Lady before finishing it, because the sheer volume of law school material took over my schedule. I’ll finish it during winter break or something, I guess.

Enough about books. Last weekend I built a computer for the living room. New motherboard, CPU, RAM, and sound (although it’s integrated sound but it’s got 7.1 channel output and 24-bit playback). Now I can hook up my Yamaha PF80 to it via MIDI-USB cable and use Native Instruments Kontakt 2 player after loading up my East West/PMI Boesendorfer 290 sample library without a sweat. It sounds awesome, although I’m in the process of getting Synthogy’s Ivory 1.5 after hearing excellent reviews. I love technology.

Blog at WordPress.com.