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.
One thought on “Bash Script: Reading ID3v1 Tags”