Bash Script: Reading ID3v1 Tags


mp3 files contain track name, artist name, etc meta data about the music file itself. The meta data format used in mp3 files are called the ID3 tags. There are different versions of ID3 tags available. The objective is to read this meta data information from an mp3 file having an ID3v1 tag.

A detailed overview of the ID3v1 (as well as ID3v2 tags) are done here in this post: What are ID3 Tags all about?. A small ID3v1 library is also implemented (reading and writing tags) in C language here: An ID3v1 Tag Parsing Library. TagLib is available to read and write ID3v1 and ID3v2 tags and is free to use.

The Idea

Here the reading process with bash script is shown. Reading the ID3 tag is very easy and only needs chopping the ID3v1 fixed length fields correctly. The last 128 bytes are simply read into a variable, and then according to the ID3v1 tag format the fixed length fields are separated into appropriate shell variables, and then displayed on stdout. Checkout What are ID3 Tags all about? for the ID3v1 tag format. The script will also read if an ID3v2 tag is present in the file, if present, it will parse the version and revision of it.

Sourcecode

#!/bin/sh

#Shell script to read ID3v1.x Tag from an mp3 audio file
#Known Issue: Because the '\0' are replaced with ' ' blankspaces. The Genre value 0 will be replaced by 32
#To get it right instead of storing the tag string in a variable direct access is needed

while [ -n "$1" ]
 do

  file="$1"

  if [ ! -f "$file" ]
    then
    echo "File \"$file\" does not exist"
    shift 1
    continue
  fi  

  tag=$(tail -c128 "$file" | tr '\0' ' ') # Replace NULL with spaces
  id3=$(head -c10 "$file" | tr '\0' ' ') # NULLs are being omitted

  id3v1_sig=${tag:0:3}

  id3v2_sig=${id3:0:3}

  id3v2_ver=${id3:3:1}
  id3v2_ver=$(printf "%d" "'$id3v2_ver")

  id3v2_rev=${id3:4:1}
  id3v2_rev=$(printf "%d" "'$id3v2_rev")

  if [ "$id3v2_sig" = "ID3" ]
    then
      echo "ID3v2.$id3v2_ver.$id3v2_rev Tag present"
    else
      echo "ID3v2 Tag present Not present"
  fi

  if [ "$id3v1_sig" = "TAG" ]
    then
      echo "ID3v1.x Tag present"
  fi

  if [ "$id3v1_sig" = "TAG" ]
    then
      
      song_name=${tag:3:30}
      artist=${tag:33:30}
      album=${tag:63:30}
      year=${tag:93:4}
      comment=${tag:97:28}
      #The second last byte of the Comment field ie the 126th byte of the tag is always zero in ID3v1.1
      album_track=${tag:126:1} #Last two bytes of comment field was reserved for album track no. in ID3v1.1
      album_track=$(printf "%d" "'$album_track") #Convert Album Track ASCII to value
      genre=${tag:127:1}
      genre=$(printf "%d" "'$genre") #Convert Genre to ASCII value
      
      #Reads the genre string from the file id3v1_genre_string
      if [ -f id3v1_genre_list ]
	then
	  genre_string=$(grep "\<$genre\>" id3v1_genre_list)
	else
	  genre_string="Genre Code = $genre"
      fi
      
      
      echo -e "Displaying ID3v1 Tag of file \"$file\"\n"
      echo "Song Name   : $song_name"
      echo "Artist      : $artist"
      echo "Album       : $album"
      echo "Year        : $year"
      echo "Comment     : $comment"
      echo "Album Track : $album_track"
      echo "Genre       : $genre_string"
      
    else
	echo "The file \"$file\" does not contain an ID3v1 tag"
  fi

shift 1
done

Description

The code is constructed to accept any number of file arguments though shifting the argument parameters. The outermost while loop will run until all the passed parameters are not scanned, by shifting the argument list by 1 at each iterating, and propagating the next argument on the first positional argument $1. Next the code transfers the argument into a shell variable file, and tests if it exists and is a regular file. If it is not a regular file or it does not exist, it would shift once and then continue for the next iteration to evaluate the next file in the list.

When a file is detected as a valid regular file, then the code proceeds to check if the ID3 tags are present in the code or not. The last 128 bytes are filtered into the shell variable tag for checking for an ID3v1 tag, and the 10 bytes are filtered in the id3 variable, to check for the existence of the ID3v2 tag. The first 10 bytes are cutout by the head -c10 command, and last 128 bytes are cut out by the tail -c128 command. If the first 3 bytes of both of the variables tell if the corresponding tag exists in the file. This is done by filtering out the first 3 bytes of both the shell variables. If an ID3v2 is present then id3 variable will have “ID3″ as the first 3 bytes and will follow with the version and revision on the next two bytes, which are also cut out in proper variables id3v2_ver, and id3v2_rev. The if - then - else checks for the “ID3″ string and prints the if the ID3v2 tag exists and its version and revision number.

Next the presence of the ID3v1 tag is checked by inspecting if the first three byte of tag shell variable is “TAG” or not. If yes then it prints that the ID3v1 tag exists then it proceeds further to parse the tag more and extract more information about the music file. Each of the ID3v1 tag fields as per specification is cut out by the shell variable substitution syntax

${tag:x:y}

where x is an integer representing the starting byte or the base byte from where to start the cut, and y is an integer which represents the offset, that is, how many integers to cut starting from the base. For example the Album field starts from the byte 63 and spans 30 bytes, so to cut out the Album portion and store it in the shell variable album the following substitution is used: album=${tag:63:30}. The fields track number and the genre field is not stored as ASCII character codes, and require integer interpretation. The read in album_track and the genre is converted into decimal number strings and reassigned. To do this the below construct is used.

printf "%d" "'$var"

'$var evaluates to the ASCII value of the stored number in var and the decimal representation formatted by “%d” is reassigned into album_track and genre. Each genre number represents a corresponding string which are fixed to ID3v1 standards. An external file id3v1_genre_list stores the genre code – genre string data in each row, from which the correct genre string is extracted by greping the file with the genre variable contents, and cutting. If such string is not present in the file, or the file is not present, as a fallback method simply the genre code is stored to be printed (assigned by the else part of the if else). The next segment would simply print the above parsed data in a pretty format on the stdout, and end processing the current file.

Sample Output

[Phoxis@localhost phx]$ ./id3v1.sh /media/disk-1/Music/Schindler\'s\ List\ -\ Soundtrack\ \(1993\)/Track01.mp3 
ID3v2.3.32 Tag present
ID3v1.x Tag present
Displaying ID3v1 Tag of file "/media/disk-1/Music/Schindler's List - Soundtrack (1993)/Track01.mp3"

Song Name   : Theme from Schindler's List   
Artist      : John Williams                 
Album       : Schindler's List              
Year        :     
Comment     :                             
Album Track : 32
Genre       : 32 = Classical 

Genre File

Save the below contents in a file and name it “id3v1_genre_list” and keep it in the same directory as in the above script, or customize as needed.

0 = Blues 
1 = Classic Rock 
2 = Country 
3 = Dance 
4 = Disco 
5 = Funk 
6 = Grunge 
7 = Hip-Hop 
8 = Jazz 
9 = Metal 
10 = New Age 
11 = Oldies 
12 = Other 
13 = Pop 
14 = R&B 
15 = Rap 
16 = Reggae 
17 = Rock 
18 = Techno 
19 = Industrial 
20 = Alternative 
21 = Ska 
22 = Death Metal 
23 = Pranks 
24 = Soundtrack 
25 = Euro-Techno 
26 = Ambient 
27 = Trip-Hop 
28 = Vocal 
29 = Jazz+Funk 
30 = Fusion 
31 = Trance 
32 = Classical 
33 = Instrumental 
34 = Acid 
35 = House 
36 = Game 
37 = Sound Clip 
38 = Gospel 
39 = Noise 
40 = AlternRock 
41 = Bass 
42 = Soul 
43 = Punk 
44 = Space 
45 = Meditative 
46 = Instrumental Pop 
47 = Instrumental Rock 
48 = Ethnic 
49 = Gothic 
50 = Darkwave
51 = Techno-Industrial 
52 = Electronic 
53 = Pop-Folk 
54 = Eurodance 
55 = Dream 
56 = Southern Rock 
57 = Comedy 
58 = Cult 
59 = Gangsta 
60 = Top 40 
61 = Christian Rap 
62 = Pop/Funk 
63 = Jungle 
64 = Native American 
65 = Cabaret 
66 = New Wave 
67 = Psychadelic 
68 = Rave 
69 = Showtunes 
70 = Trailer 
71 = Lo-Fi 
72 = Tribal 
73 = Acid Punk 
74 = Acid Jazz 
75 = Polka 
76 = Retro 
77 = Musical 
78 = Rock & Roll 
79 = Hard Rock 
80 = Folk 
81 = Folk-Rock 
82 = National Folk 
83 = Swing 
84 = Fast Fusion 
85 = Bebob 
86 = Latin 
87 = Revival 
88 = Celtic 
89 = Bluegrass 
90 = Avantgarde 
91 = Gothic Rock 
92 = Progressive Rock 
93 = Psychedelic Rock 
94 = Symphonic Rock 
95 = Slow Rock 
96 = Big Band 
97 = Chorus 
98 = Easy Listening 
99 = Acoustic 
100 = Humour 
101 = Speech
102 = Chanson 
103 = Opera 
104 = Chamber Music 
105 = Sonata 
106 = Symphony 
107 = Booty Bass 
108 = Primus 
109 = Porn Groove 
110 = Satire 
111 = Slow Jam 
112 = Club 
113 = Tango 
114 = Samba 
115 = Folklore 
116 = Ballad 
117 = Power Ballad 
118 = Rhythmic Soul 
119 = Freestyle 
120 = Duet 
121 = Punk Rock 
122 = Drum Solo 
123 = Acapella 
124 = Euro-House 
125 = Dance Hall 
126 = Goa 
127 = Drum & Bass 
128 = Club-House 
129 = Hardcore 
130 = Terror 
131 = Indie 
132 = BritPop 
133 = Negerpunk 
134 = Polsk Punk 
135 = Beat 
136 = Christian Gangsta 
137 = Heavy Metal 
138 = Black Metal 
139 = Crossover 
140 = Contemporary C 
141 = Christian Rock 
142 = Merengue 
143 = Salsa 
144 = Thrash Metal 
145 = Anime 
146 = JPop 
147 = SynthPop 
255 = None

Comments

Script can only read the ID3v1 tags, although a tag writing script can be made easily. For it first the tag structure should be made first with the existing and the updated fields and then appended at the end of the mp3 file.

About these ads

About phoxis

Homo-sapiens
This entry was posted in Computer Science, Linux / Unix Shell and tagged , , , . Bookmark the permalink.

One Response to Bash Script: Reading ID3v1 Tags

  1. Pingback: Unix Programming Course First Assignment – Part 2 – | Raed Marji

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