Programming Praxis – ID3v1 tags

When the MP3 standard was invented, it did not contain a way to store information about the song until in 1996 Eric Kemp came up with the idea of appending 128 bytes of data to the end of the file containing information like the artist and the album. This information is known as an ID3 tag. This first version, known as ID3v1, was amended in 1997 to optionally store the track number, resulting in ID3v1.1.
In 1998 ID3v2 was released, which is a far more complicated and comprehensive, though technically unrelated, standard.

Your task in today’s exercise is to write a program that can read ID3v1.1 tags. The layout of the ID3 tag can be found in the wikipedia article. When you are finished, you are welcome to read a suggested solution below, or to post your own solution or discuss the exercise in the comments below (to post code, put it between [code][/code] tags).

 
 
 

First, some imports:

import Control.Applicative.Error
import qualified Data.ByteString as B
import Data.ByteString.Char8 (unpack)
import Data.List.Split

We could just use a tuple to store the ID3 info, but that would make function signatures rather hard to read, so we’ll define a data type for it.

data ID3v1 = ID3v1 String String String (Maybe Int) String
                   (Maybe Int) String deriving Show

Since the genre information is stored in a single byte, we’ll need to convert it to the genre name. This is trivial, but the genres take up a lot of space.

genre :: Int -> String
genre c = if c > 147 then "Unknown" else wordsBy (== ',')
    "Blues,Classic Rock,Country,Dance,Disco,Funk,Grunge,Hip-Hop,\
    \Jazz,Metal,New Age,Oldies,Other,Pop,R&B,Rap,Reggae,Rock,\
    \Techno,Industrial,Alternative,Ska,Death Metal,Pranks,\
    \Soundtrack,Euro-Techno,Ambient,Trip-Hop,Vocal,Jazz+Funk,\
    \Fusion,Trance,Classical,Instrumental,Acid,House,Game,\
    \Sound Clip,Gospel,Noise,Alternative Rock,Bass,Soul,Punk,\
    \Space,Meditative,Instrumental Pop,Instrumental Rock,Ethnic,\
    \Gothic,Darkwave,Techno-Industrial,Electronic,Pop-Folk,\
    \Eurodance,Dream,Southern Rock,Comedy,Cult,Gangsta,Top 40,\
    \Christian Rap,Pop/Funk,Jungle,Native US,Cabaret,New Wave,\
    \Psychadelic,Rave,Showtunes,Trailer,Lo-Fi,Tribal,Acid Punk,\
    \Acid Jazz,Polka,Retro,Musical,Rock & Roll,Hard Rock,Folk,\
    \Folk-Rock,National Folk,Swing,Fast Fusion,Bebob,Latin,\
    \Revival,Celtic,Bluegrass,Avantgarde,Gothic Rock,\
    \Progressive Rock,Psychedelic Rock,Symphonic Rock,Slow Rock,\
    \Big Band,Chorus,Easy Listening,Acoustic,Humour,Speech,\
    \Chanson,Opera,Chamber Music,Sonata,Symphony,Booty Bass,\
    \Primus,Porn Groove,Satire,Slow Jam,Club,Tango,Samba,Folklore,\
    \Ballad,Power Ballad,Rhythmic Soul,Freestyle,Duet,Punk Rock,\
    \Drum Solo,Acapella,Euro-House,Dance Hall,Goa,Drum & Bass,\
    \Club - House,Hardcore,Terror,Indie,BritPop,Negerpunk,\
    \Polsk Punk,Beat,Christian Gangsta Rap,Heavy Metal,\
    \Black Metal,Crossover,Contemporary Christian,Christian Rock,\
    \Merengue,Salsa,Thrash Metal,Anime,JPop,Synthpop" !! c

Since each field of the ID3 tag has a fixed length, we don’t need to write a full parser. Instead, we split the last 128 bytes of an mp3 file in the correct places and assemble the tag from that. A bit of extra work is needed (handling null-terminated strings and the optional track number), but it’s nothing complicated.

id3v1 :: B.ByteString -> Maybe ID3v1
id3v1 mp3 = if hdr /= "TAG" then Nothing else Just $ ID3v1
            (nts title) (nts artist) (nts album) (maybeRead year)
            (nts comment) track (genre $ fromEnum gnr) where
    [hdr, title, artist, album, year, cmt, [zbt], [trk], [gnr]] =
        splitPlaces [3,30,30,30,4,28,1,1,1] . unpack $
        B.drop (B.length mp3 - 128) mp3
    nts = takeWhile (> '\NUL')
    (comment, track) | zbt == '\NUL' = (cmt, Just $ fromEnum trk)
                     | otherwise     = (cmt ++ [zbt, trk], Nothing)

Testing reveals that everything is working properly:

main :: IO ()
main = print . id3v1 =<< B.readFile "G:/music/metal/sabaton/40-1.mp3"

Output:

Just (ID3v1 "40 : 1" "SABATON" "The Art of War"
            (Just 2008) "" (Just 4) "Metal")

12 lines of code, 24 lines of data. Not too terrible.

Tags: , , , , , , , , ,

One Response to “Programming Praxis – ID3v1 tags”

  1. phoxis Says:

    Yes id3v1 tags could be read and written very easily, and does not need much code to do that. I made a small shell script to read from an id3 tag. But you can find a better edition of an ID3v1 tag editor demo library written in C Language in my blog : http://phoxis.org/2010/05/08/an-id3v1-tag-parsing-library/

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


Follow

Get every new post delivered to your Inbox.

Join 35 other followers

%d bloggers like this: