Tagging MP3 files

My son's new MP3 player is able to display some sound information such as title, artist etc. I wondered how this info is coded in the MP3 file. A short research led me to http://www.dv.co.yu/mpgscript/mpeghdr.htm . A simple 128 byte trailer is appended to the MP3 file. The following program can read, edit and (re)write this trailer.

Have fun tagging your MP3s. US

AK - Can something similar be done for Ogg/Vorbis files ?

EG - AFAIK there's no simple way. On Ogg/Vorbis files, the metadata is located in the second Vorbis packet, near the beginning of the file. So if you want to add/remove comments you have to regenerate the file, along with the headers. See Ogg tools for my attempt to do it in pure tcl.

US - Made a slight change in the binary scan/format lines, now it's exactly ID3v1.1 format. This is also used by some Ogg/Vorbis en-/decoders. It should be possible to use this program unchanged for both MP3 and Ogg/Vorbis. (ID3v2 is a different cup of tea)

NEM - Added variable to keep track of where you last looked for files. Makes my life a little easier.

EE - Note: I've encountered a number of files claiming to be mp3 files that had a huge pile of junk at the *beginning* of the file. This junk contained an extended amount of file identification data, sometimes even including an embedded thumbnail of an album cover image. Some of my mp3 players would complain about "junk at beginning of file", but not all. in particular, xmms would display the band-name and song-name from that data if present, although the "delete ID3 tag" function in xmms wouldn't delete that header junk. Unfortunately, I don't know the format of the junk blob, or how to recognize its start and end, and music starting point..

US - This is probably ID3v2, AFAIK XML based.

miriam - See http://www.id3.org/ for full specifications of the ID3v2 tag. It is quite a lot more complex than ID3v1 but much more flexible. Being at the head of the file it is picked up at the beginning of a streaming file. It can contain descriptions longer than 30 characters. Fields can contain anything, even images if desired. It is an expandable set of chunks like IFF files or PNG files where a reader program can skip parts it doesn't understand. The downside of course is the complexity.

#! /usr/local/bin/tclsh8.3

package require msgcat
namespace import msgcat::*

if {[info exists ::env(HOME)]} {
  set initial_dir $::env(HOME)
} else {
  set initial_dir [pwd]

# Read tag (if there is one)
proc rd_tag {} {
    global title artist album year comment track genre hastag fnam
    set fd [open $fnam r]
    fconfigure $fd -encoding binary -translation binary
    if {[catch {seek $fd -128 end}]} {
        .m.file entryconfigure [mc Schreiben] -state disabled
    set tag [read $fd]
    close $fd
    binary scan $tag A3 id
    if {[string equal $id TAG]} {
        set hastag 1
        set genre 12
        binary scan $tag A3A30A30A30A4A28ccc id title artist album  year comment zero track genre
        } else {
        set hastag 0
        set title ""
        set artist ""
        set album ""
        set year ""
        set comment ""
        set track 0
        set genre 12
    .f3.lbgen selection clear 0 end
    .f3.lbgen selection set $genre
    .f3.lbgen see $genre

# Choose a file to read
proc rd_file {} {
    global fnam initial_dir
    set ftypes {{MP3 .mp3} {All *}}
    set fnam [tk_getOpenFile -filetypes $ftypes -defaultextension .mp3 \
        -initialdir $initial_dir]
    if {[string length $fnam]} {
        set initial_dir [file dirname $fnam]
        .m.file entryconfigure [mc Schreiben] -state normal
    } else {
        .m.file entryconfigure [mc Schreiben] -state disabled

# Write (back) tag to file
proc wr_file {} {
    global title artist album year comment track hastag fnam
    set genre [.f3.lbgen curselection]
    set tag [binary format a3a30a30a30a4a28ccc TAG $title $artist $album $year $comment 0 [string trimleft $track 0] $genre]
    set fd [open $fnam a]
    fconfigure $fd -encoding binary -translation binary
    if {$hastag} {
        seek $fd -128 end
    puts -nonewline $fd $tag
    close $fd

# seek fails, file too short
proc too_short {} {
    global fnam
    tk_messageBox -type ok -icon warning \
        -message "[mc \"Die Datei\"] $fnam [mc \"ist keine MP3-Datei.\"]"

# Message catalogs:

# Deutsch
mcset de Lesen
mcset de Schreiben
mcset de Datei
mcset de Ende
mcset de Titel
mcset de Interpret
mcset de Album
mcset de Kommentar
mcset de Jahr
mcset de Track
mcset de Genre
mcset de "Die Datei"
mcset de "ist keine MP3-Datei."

# English
mcset en Lesen Read
mcset en Schreiben Write
mcset en Datei File
mcset en Ende Exit
mcset en Titel Title
mcset en Interpret Artist
mcset en Album
mcset en Kommentar Comment
mcset en Jahr Year
mcset en Track
mcset en Genre
mcset en "Die Datei" "The file"
mcset en "ist keine MP3-Datei." "is not an MP3 file."

# Add your preferred language here

# Build GUI
menu .m -type menubar
. configure -menu .m
.m add cascade -label [mc Datei] -menu .m.file
menu .m.file -tearoff 0
.m.file add command -label [mc Lesen]     -command rd_file
.m.file add command -label [mc Schreiben] -command wr_file
.m.file add separator
.m.file add command -label [mc Ende]      -command {destroy .}
frame .f1
frame .f2
frame .f3
label .f1.lfil -text [mc Datei]:
entry .f1.efil -textvariable fnam -width 30
label .f1.ltit -text [mc Titel]:
entry .f1.etit -textvariable title -width 30
label .f1.lart -text [mc Interpret]:
entry .f1.eart -textvariable artist -width 30
label .f1.lalb -text [mc Album]:
entry .f1.ealb -textvariable album -width 30
label .f1.lcom -text [mc Kommentar]:
entry .f1.ecom -textvariable comment -width 30
label .f2.lyea -text [mc Jahr]:
entry .f2.eyea -textvariable year -width 4
label .f2.ltrk -text [mc Track]:
entry .f2.etrk -textvariable track -width 2
label .f3.lgen -text [mc Genre]:
listbox .f3.lbgen -listvar lgenre -width 40 -height 5 \
    -yscrollcommand {.f3.sbgen set} \
    -exportselection 0
scrollbar .f3.sbgen -orient vert -command {.f3.lbgen yview}

pack .f1 .f2 .f3 -padx 1m -pady 1m
grid .f1.lfil -row 0 -column 0 -sticky e
grid .f1.efil -row 0 -column 1
grid .f1.ltit -row 1 -column 0 -sticky e
grid .f1.etit -row 1 -column 1
grid .f1.lart -row 2 -column 0 -sticky e
grid .f1.eart -row 2 -column 1
grid .f1.lalb -row 3 -column 0 -sticky e
grid .f1.ealb -row 3 -column 1
grid .f1.lcom -row 4 -column 0 -sticky e
grid .f1.ecom -row 4 -column 1
pack .f2.lyea .f2.eyea .f2.ltrk .f2.etrk -side left
pack .f3.lgen
pack .f3.lbgen .f3.sbgen -side left -fill y

.m.file entryconfigure [mc Schreiben] -state disabled

# Some variables
set hastag 0
set fnam ""

set lgenre {
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
AlternRock             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 American}      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 Brass}
Primus                 {Porn Groove}           Satire                  {Slow Jam}
Club                   Tango                   Samba                   Folklore
Ballad                 {Power Ballad}          {Rhytmic Soul}          Freestyle
Duet                   {Punk Rock}             {Drum Solo}             {A Capela}
Euro-House             {Dance Hall}

# Set defaults
.f3.lbgen selection clear 0 end
.f3.lbgen selection set 12
.f3.lbgen see 12

cf Cool application. One modification to the write procedure:

# Write (back) tag to file
proc wr_file {} {
    global title artist album year comment track hastag fnam
    set genre [.f3.lbgen curselection]
    scan $track %d track
    set tag [binary format a3a30a30a30a4a28ccc TAG $title $artist $album $year $comment 0 $track $genre]
    set fd [open $fnam a]
    fconfigure $fd -encoding binary -translation binary
    if {$hastag} {
        seek $fd -128 end
    puts -nonewline $fd $tag
    close $fd

The trimleft trick returns an empty string when the track number is 0, while the scan function works regardless.

If you need IDV2 read/edit download snackAmp souces from sourceforce and take id3.tcl file which implements both IDV1 and IDV2 read/write. It is not complete and you will need to comment some lines that are snackAmp related like "Trace" and "Update" but it is not so difficult. also some tk_message should be removed. remove the upvar connection to snackAmpSettings and add setting to ID3ReadOnly,preserveTime,preserveV2data and you are done.

I also added proper mp3 jpeg image add/change support. To add it modify/add this lines:

modify line258 - variable v2_3_IDs list "COMM" "TALB" "TCOM" "TCON" "TENC" "TIT2" "TMED" "TPE1" "TRCK" "TSST" "TYER" "APIC"

add line 672 - "APIC" {set data(Image) [string range $String 12 end]}

add line 828 - "Image" {append Tag "APIC" [tagLen [expr {$slen +8}] $flags $uenc "image/jpg" \0\3\0 $String} ;# lang is ENG (for now)

and you can get/set images. this method can also be done (with some changes according to encoding) to other mp3 properties you want to support.

I hope there is a way to make this part of tcllib or other public easily accessible library.