MP3 ID3 Tools: Source Code
This script allows access to the ID3 header information in an MP3 audio file.
General notes:
- This is a Parent script. This is NOT a behaviour, NOT a movie script.
- The code requires the BinaryIO Xtra. This is available from http://www.updatestage.com.
- The code will work in Macromedia Director 7.0 or later.
Copy and paste the source code below into a parent script.
-----------------------------------------------------------------
-- MP3 Parent
-- General Purpose MP3 Info Parent Script
-- copyright (c) 2000 kendall anderson. all rights reserved.
-- http://invisiblethreads.com
-- History:
----------
-- jun 26, 1999: k. anderson // initial development
-- sep 09, 2000: k. anderson, modified, converted to parent script for mp3 database utility
-- sep 19, 2000: k. anderson, added _WriteMP3Tag function
-- sep 20, 2000: k. anderson, added _ReadMP3Header function
-- sep 21, 2000: k. anderson, additional code documentation for public consumption
-- Requirements:
---------------
-- *REQUIRES* the BinaryIO Xtra by Glenn Picher (go to www.updatestage.com for information)
-- Current Limitations:
----------------------
-- Only reads ID3 v1 tags, does NOT support v2, v3, etc.
-- Major Methods:
----------------
-- _ReadMP3Tag me, dPath, dFilename -- returns the ID3 information as a property list
-- _WriteMP3Tag me, dPath, dFilename, dID3Tag -- writes the ID3 property list 'dID3Tag' to the file
-- _ReadMP3Header me, dPath, dFilename -- returns the MP3 header information as a list containing 2 property lists [ raw info, descriptive info ]
-- Data Formats:
---------------
-- dID3Tag (returned by _ReadMP3Tag and required by _WriteMP3Tag)
----------
-- dID3Tag.TAG always = "TAG"
-- dID3Tag.TITLE Song Title
-- dID3Tag.ARTIST Artist
-- dID3Tag.ALBUM Album
-- dID3Tag.YEAR Year as string
-- dID3Tag.COMMENTS Comments
-- dID3Tag.GENRE Genre number (as character)
-- dID3Tag.GENREDESC Genre Description name, ie: "Jazz", unused by _WriteMP3Tag
-- dID3Tag.FILESIZE Returned by _ReadMP3Tag, unused by _WriteMP3Tag
-- dHeader (returned by _ReadMP3Header)
----------
-- dDescHeader = dHeader[2] The header information, translated into descriptive values
--------------
-- dDescHeader.MPEGVersion ie: "MPEG Version 2.5"
-- dDescHeader.Layer ie: "Layer III"
-- dDescHeader.Protection ie: "Protected by 16bit CRC"
-- dDescHeader.BitRate ie: "128"
-- dDescHeader.SamplingRate ie: "44000"
-- dDescHeader.Padding ie: "frame is padded with one extra bit"
-- dDescHeader.Private unknown
-- dDescHeader.ChannelMode ie: "joint stereo (stereo)"
-- dDescHeader.ModeExtension ie: "off, on"
-- dDescHeader.Copyright ie: "Audio is not copyrighted"
-- dDescHeader.Original ie: "Original media"
-- dDescHeader.Emphasis ie: "none"
-- dRawHeader = dHeader[1] The first 32 bits of the file, parsed into specific properties
-------------
-- dRawHeader.MPEGVersion 2 bits
-- dRawHeader.Layer 2 bits
-- dRawHeader.Protection 1 bit
-- dRawHeader.BitRate 4 bits
-- dRawHeader.SamplingRate 2 bits
-- dRawHeader.Padding 1 bit
-- dRawHeader.Private 1 bit
-- dRawHeader.ChannelMode 2 bits
-- dRawHeader.ModeExtension 2 bits
-- dRawHeader.Copyright 1 bit
-- dRawHeader.Original 1 bit
-- dRawHeader.Emphasis 2 bits
-- Additional Credits:
---------------------
-- The two functions which convert an integer to a bit string,
-- and vice-versa, (_convBase and _ConvToInt) were found at
-- http://www.mediamacros.com, and are credited to:
-- Kevan Dettelbach, Chuck Neal and Joerg Seibert.
-- Local Properties:
-------------------
property pGenreList
-- Methods:
----------
on new me
put "[ ID3 Tag Parent: birthed ]"
register(xtra "binaryio","your-serial-number-here")
_MakeGenreList(me)
return me
end
-- read the specified MP3 file and return the ID3 Tag info as a property list
-- If the file doesn't exist, returns VOID
-- If the file does not have an ID3 Tag, returns a blank ID3 Tag with the filename as the SongTitle
-- returns the ID3 Tag data as a property list
on _ReadMP3Tag me, dPath, dFilename
dFP = new(xtra "binaryio") -- instance the BinaryIO Xtra
dErr = openfile(dFP, 1, dPath & dFilename) -- check for errors opening the file
if (dErr <> "OK") then -- if the file didn't open, halt
put "Error opening file! (" & dPath & dFilename & ")"
dID3Tag = void
else
dTagSize = 128 -- MP3 tag is 128 bytes long
dFileSize = GetFileSize(dFP) -- get the MP3 filesize
SetfilePosition(dFP, dFileSize - 128) -- position ourselves at the end - 128 bytes
dTagData = readBytes(dFp, dTagSize) -- read the tag data
-- sort out the tag data into different fields
dID3Tag = _ParseTagData(me, dTagData, dFileSize, dFilename)
end if
dErr = closefile(dFP)
dFP = void
return dID3Tag
end
-- write the ID3 Tag (dID3Tag) to the specified file
-- if an ID3 Tag already exists, it will be replaced
-- if an ID3 Tag does NOT exist, it will be appended to the end of the file
-- NOTE: NO ERROR CHECKING is applied to the dID3Tag variable as of yet
-- returns string with result of the operation, first word will be either ERROR or OK
on _WriteMP3Tag me, dPath, dFilename, dID3Tag
-- determine whether tag exists in the filename or not before writing
-- pad the dID3Tag so that all fields are the correct length? YES YES
dFP = new(xtra "binaryio") -- instance the BinaryIO Xtra
dErr = openfile(dFP, 2, dPath & dFilename) -- check for errors opening the file
if (dErr <> "OK") then -- if the file didn't open, halt
dResult = "ERROR opening file! (" & dPath & dFilename & ")"
else
dTagSize = 128 -- MP3 tag is 128 bytes long
dFileSize = GetFileSize(dFP) -- get the MP3 filesize
SetfilePosition(dFP, dFileSize - 128) -- position ourselves at the end - 128 bytes
dExistingTagData = readBytes(dFp, dTagSize) -- read the tag data
dID3Tag = _PadID3Tag(me, dID3Tag) -- make sure all fields are correct length
if (dExistingTagData.char[1..3] = "TAG") then
-- data already exists
dResult = "OK Replacing existing ID3 for: [" & dFilename & "]"
SetfilePosition(dFP, dFileSize - 128)
else
-- no data exists, write from the end of the file
dResult = "OK Writing new ID3 for: [" & dFilename & "]"
SetfilePosition(dFP, dFileSize)
end if
WriteBytes(dFP, dID3Tag.tag)
WriteBytes(dFP, dID3Tag.title)
WriteBytes(dFP, dID3Tag.artist)
WriteBytes(dFP, dID3Tag.album)
WriteBytes(dFP, dID3Tag.year)
WriteBytes(dFP, dID3Tag.comments)
WriteBytes(dFP, dID3Tag.genre)
end if
dErr = closefile(dFP)
dFP = void
put dResult
return dResult
end
-- read the header information from the specified MP3 file
-- ie: version, bitrate, error protection, sampling freq, copyright, etc.
-- If the file could not be opened, returns VOID
-- If the read is successful, returns a list containing 2 property lists: [ RawHeader, DescriptiveHeader ]
on _ReadMP3Header me, dPath, dFilename
dHeader = [:]
dFP = new(xtra "binaryio") -- instance the BinaryIO Xtra
dErr = openfile(dFP, 1, dPath & dFilename) -- check for errors opening the file
if (dErr <> "OK") then -- if the file didn't open, halt
put "ERROR opening file! (" & dPath & dFilename & ")"
dHeader = void
else
-- to read the header info, read the first 4 bytes
dHeaderRaw = ReadBytes(dFP, 4)
-- construct a 32 bit long string
dHeaderBitString = ""
repeat with i = 1 to dHeaderRaw.length
dBitString = ""
dBitString = _ConvBase(me, chartonum(dHeaderRaw.char[i]), 2) -- convert number to binary in string format
repeat while(dBitString.length < 8) -- make sure each byte converts to 8 digit bitstring
dBitString = "0" & dBitString
end repeat
dHeaderBitString = dHeaderBitString & dBitString
end repeat
dHeader = _ParseHeader(me, dHeaderBitString) -- figure out what info was in the header
end if
dErr = closefile(dFP)
dFP = void
return dHeader
end
-- given dHeaderBitString = string like "1111110011111010011"... etc representing first 4 bytes of file
-- parse to properties
on _ParseHeader me, dBitString
-- read each block of data into a variable, raw
dFrameSync = dBitString.char[1..11]
dMPEGVersion = dBitString.char[12..13]
dLayer = dBitString.char[14..15]
dProtectionBit = dBitString.char[16]
dBitRate = dBitString.char[17..20]
dSamplingRate = dBitString.char[21.22]
dPadding = dBitString.char[23]
dPrivate = dBitString.char[24]
dChannelMode = dBitString.char[25..26]
dModeExtension = dBitString.char[27..28]
dCopyright = dBitString.char[29]
dOriginal = dBitString.char[30]
dEmphasis = dBitString.char[31..32]
dRawHeader = [:]
dDescHeader = [:]
-- mpeg version #
case (dMPEGVersion) of
"00":
dDescMPEGVersion = "MPEG Version 2.5"
dSamplingRateList = [ "11025", "12000", "8000", "reserved" ]
"01":
dDescMPEGVersion = "reserved"
dSamplingRateList = [ "reserved", "reserved", "reserved", "reserved" ]
"10":
dDescMPEGVersion = "MPEG Version 2"
dSamplingRateList = [ "22050", "24000", "16000", "reserved" ]
"11":
dDescMPEGVersion = "MPEG Version 1"
dSamplingRateList = [ "44100", "48000", "32000", "reserved" ]
end case
addProp dRawHeader, #MPEGVersion, dMPEGVersion
addProp dDescHeader, #MPEGVersion, dDescMPEGVersion
-- layer description
case (dLayer) of
"00": dDescLayer = "reserved"
"01": dDescLayer = "Layer III"
"10": dDescLayer = "Layer II"
"11": dDescLayer = "Layer I"
end case
addProp dRawHeader, #Layer, dLayer
addProp dDescHeader, #Layer, dDescLayer
-- protection bit
case (dProtectionBit) of
"0": dDescProtection = "Protected by 16bit CRC"
"1": dDescProtection = "Not Protected"
end case
addProp dRawHeader, #Protection, dProtectionBit
addProp dDescHeader, #Protection, dDescProtection
-- bitrate index
dL1 = [ "free", "32", "64", "96", "128", "160", "192", "224", "256", "288", "320", "352", "384", "416", "448", "bad" ]
dL2 = [ "free", "32", "40", "48", "56", "64", "80", "96", "112", "128", "160", "192", "224", "256", "320", "bad" ]
dV1L3 = [ "free", "32", "40", "48", "56", "64", "80", "96", "112", "128", "160", "192", "224", "256", "320", "bad" ]
dV2L3 = [ "free", "8", "16", "24", "32", "64", "80", "54", "64", "128", "160", "112", "128", "256", "320" ]
case (dLayer) of
"11": dBitRateList = dL1 -- layer I
"10": dBitRateList = dL2 -- layer II
"01":
if (dMPEGVersion = "11") then -- version 1
dBitRateList = dV1L3
else -- version 2 or version 2.5
dBitRateList = dV2L3
end if
end case
-- now, get element from the list
dIndexPos = _ConvToInt(me, dBitRate, 2) -- convert "1010" etc to integer
dIndexPos = dIndexPos + 1
dDescBitRate = dBitRateList[dIndexPos]
addProp dRawHeader, #BitRate, dBitRate
addProp dDescHeader, #BitRate, dDescBitRate
-- sampling rate
dIndexPos = _ConvToInt(me, dSamplingRate, 2)
dIndexPos = dIndexPos + 1
dDescSamplingRate = dSamplingRateList[dIndexPos]
addProp dRawHeader, #SamplingRate, dSamplingRate
addProp dDescHeader, #SamplingRate, dDescSamplingRate
-- padding
case (dPadding) of
"0": dDescPadding = "frame is not padded"
"1": dDescPadding = "frame is padded with one extra bit"
end case
addProp dRawHeader, #Padding, dPadding
addProp dDescHeader, #Padding, dDescPadding
-- private bit
addProp dRawHeader, #Private, dPrivate
addProp dDescHeader, #Private, dPrivate
-- channel mode
case (dChannelMode) of
"00": dDescChannelMode = "stereo"
"01": dDescChannelMode = "joint stereo (stereo)"
"10": dDescChannelMode = "dual channel (stereo)"
"11": dDescChannelMode = "single channel (mono)"
end case
addProp dRawHeader, #ChannelMode, dChannelMode
addProp dDescHeader, #ChannelMode, dDescChannelMode
-- mode extension -- *** NO IDEA HOW TO INTERPRET THIS ONE CORRECTLY...
case (dModeExtension) of
"00": dDescModeExtension = "off, off"
"01": dDescModeExtension = "on, off"
"10": dDescModeExtension = "off, on"
"11": dDescModeExtension = "on, on"
end case
addProp dRawHeader, #ModeExtension, dModeExtension
addProp dDescHeader, #ModeExtension, dDescModeExtension
-- copyright
case (dCopyright) of
"0": dDescCopyright = "Audio is not copyrighted"
"1": dDescCopyright = "Audio is copyrighted"
end case
addProp dRawHeader, #Copyright, dCopyright
addProp dDescHeader, #Copyright, dDescCopyright
-- original
case (dOriginal) of
"0": dDescOriginal = "Copy of original media"
"1": dDescOriginal = "Original media"
end case
addProp dRawHeader, #Original, dOriginal
addProp dDescHeader, #Original, dDescOriginal
-- emphasis
case (dEmphasis) of
"00": dDescEmphasis = "none"
"01": dDescEmphasis = "50/15 ms"
"10": dDescEmphasis = "reserved"
"11": dDescEmphasis = "CCIT J.17"
end case
addProp dRawHeader, #Emphasis, dEmphasis
addProp dDescHeader, #Emphasis, dDescEmphasis
return [ dRawHeader, dDescHeader ]
end
-- Convert a positive integer in a String based on base
-- for int-to-hex call ConvBase(nConv, 16)
-- for int-to-bit call ConvBase(nConv, 2)
on _convBase me, nConvNum, nBase
if (nBase > 1 and nBase < 17) and (nConvNum > -1) then
baseNums = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"]
szConverted = ""
repeat while nConvNum
nMod = nConvNum mod nBase
szConverted = baseNums[nMod + 1] & szConverted
nConvNum = nConvNum / nBase
end repeat
return szConverted
else
return VOID
end if
end
-- Convert a String into an integer,
-- for hex-to-int call convToInt(szConv, 16)
-- for bit-to-int call convToInt(szConv, 2) .....
-- no complete errorchecking: convToInt("2EF", 2) works, but makes no sense ;-)
on _ConvToInt me, szConvNum, nBase
if nBase > 1 and nBase < 17 then
baseNums = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"]
nConverted = 0
nMax = szConvNum.length
repeat with nIndex = 1 to nMax
nConverted = nConverted * nBase
nConverted = nConverted + baseNums.getOne(szConvNum.char[nIndex]) - 1
end repeat
return nConverted
else
return VOID
end if
end
-- make sure each field which will be written is exactly the correct length
on _PadID3Tag me, dID3Tag
dTagLengths = []
add dTagLengths, [ #title, 30 ]
add dTagLengths, [ #artist, 30 ]
add dTagLengths, [ #album, 30 ]
add dTagLengths, [ #year, 4 ]
add dTagLengths, [ #comments, 30 ]
add dTagLengths, [ #genre, 1 ]
repeat with dLimits in dTagLengths
dProp = dLimits[1]
dValue = dLimits[2]
dItem = dID3Tag[dProp]
dItem = _SetLength(me, dItem, dValue)
dID3Tag[dProp] = dItem
end repeat
return dID3Tag
end
-- set the length of a text string - pad with spaces, or truncate
on _SetLength me, dItem, dValue
if (dItem.length > dValue) then
dItem = dItem.char[1..dValue]
else
repeat while (dItem.length < dValue)
--dItem = dItem & numTochar(0)
dItem = dItem & " " -- should this be a numtochar(0) instead of SPACE?
end repeat
end if
return dItem
end
-- given the raw tag data, convert it into a property list
on _ParseTagData me, dTagData, dFileSize, dFilename
dID3Tag = [:]
if (dTagData.length = 0) then
dIdentity = "ID3 TAG MISSING"
else
dIdentity = dTagData.char[1..3]
end if
addProp dID3Tag, #TAG, dIdentity
if (dIdentity <> "TAG") then
-- put "Invalid tag or this MP3 file does not have any ID data."
-- create temporary tag with blank entries
dID3Tag.TAG = "TAG"
addProp dID3Tag, #TITLE, dFilename
addProp dID3Tag, #ARTIST, ""
addProp dID3Tag, #ALBUM, ""
addProp dID3Tag, #YEAR, ""
addProp dID3Tag, #COMMENTS, ""
addProp dID3Tag, #GENRE, numtochar(126)
addProp dID3Tag, #GENREDESC, "Unknown"
addProp dID3Tag, #FILESIZE, dFileSize
else
dSongTitle = dTagData.char[4..33] -- 30 chars
dArtist = dTagData.char[34..63] -- 30 chars
dAlbum = dTagData.char[64..93] -- 30 chars
dYear = dTagData.char[94..97] -- 4 chars
dComments = dTagData.char[98..127] -- 30 chars
dGenre = dTagData.char[128..128] -- 1 char
dGenreDescription = _GetGenre(me, chartonum(dGenre))
addProp dID3Tag, #TITLE, dSongTitle
addProp dID3Tag, #ARTIST, dArtist
addProp dID3Tag, #ALBUM, dAlbum
addProp dID3Tag, #YEAR, dYear
addProp dID3Tag, #COMMENTS, dComments
addProp dID3Tag, #GENRE, dGenre
addProp dID3Tag, #GENREDESC, dGenreDescription
addProp dID3Tag, #FILESIZE, dFileSize
end if
return dID3Tag
end
-- given genre #, return the text description of the genre
on _GetGenre me, dGenre
dGenre = dGenre + 1 -- because the list starts with a value of 0 (Blues)
if (dGenre > pGenreList.count) then
dGenreText = "Unknown"
else
dGenreText = pGenreList[dGenre]
end if
return dGenreText
end
-- assemble the list of genre descriptions
on _MakeGenreList me
dList = []
add dList, "Blues" -- 000
add dList, "Classic Rock" -- 001
add dList, "Country" -- 002
add dList, "Dance" -- 003
add dList, "Disco" -- 004
add dList, "Funk" -- 005
add dList, "Grunge" -- 006
add dList, "Hip-Hop" -- 007
add dList, "Jazz" -- 008
add dList, "Metal" -- 009
add dList, "New Age" -- 010
add dList, "Oldies" -- 011
add dList, "Other" -- 012
add dList, "Pop" -- 013
add dList, "R&B" -- 014
add dList, "Rap" -- 015
add dList, "Reggae" -- 016
add dList, "Rock" -- 017
add dList, "Techno" -- 018
add dList, "Industrial" -- 019
add dList, "Alternative" -- 020
add dList, "Ska" -- 021
add dList, "Death Metal" -- 022
add dList, "Pranks" -- 023
add dList, "Soundtrack" -- 024
add dList, "Euro-Techno" -- 025
add dList, "Ambient" -- 026
add dList, "Trip-Hop" -- 027
add dList, "Vocal" -- 028
add dList, "Jazz+Funk" -- 029
add dList, "Fusion" -- 030
add dList, "Trance" -- 031
add dList, "Classical" -- 032
add dList, "Instrumental" -- 033
add dList, "Acid" -- 034
add dList, "House" -- 035
add dList, "Game" -- 036
add dList, "Sound Clip" -- 037
add dList, "Gospel" -- 038
add dList, "Noise" -- 039
add dList, "AlternRock" -- 040
add dList, "Bass" -- 041
add dList, "Soul" -- 042
add dList, "Punk" -- 043
add dList, "Space" -- 044
add dList, "Meditative" -- 045
add dList, "Instrumental Pop" -- 046
add dList, "Instrumental Rock" -- 047
add dList, "Ethnic" -- 048
add dList, "Gothic" -- 049
add dList, "Darkwave" -- 050
add dList, "Techno-Industrial" -- 051
add dList, "Electronic" -- 052
add dList, "Pop-Folk" -- 053
add dList, "Eurodance" -- 054
add dList, "Dream" -- 055
add dList, "Southern Rock" -- 056
add dList, "Comedy" -- 057
add dList, "Cult" -- 058
add dList, "Gangsta" -- 059
add dList, "Top 40" -- 060
add dList, "Christian Rap" -- 061
add dList, "Pop/Funk" -- 062
add dList, "Jungle" -- 063
add dList, "Native American" -- 064
add dList, "Cabaret" -- 065
add dList, "New Wave" -- 066
add dList, "Psychadelic" -- 067
add dList, "Rave" -- 068
add dList, "Showtunes" -- 069
add dList, "Trailer" -- 070
add dList, "Lo-Fi" -- 071
add dList, "Tribal" -- 072
add dList, "Acid Punk" -- 073
add dList, "Acid Jazz" -- 074
add dList, "Polka" -- 075
add dList, "Retro" -- 076
add dList, "Musical" -- 077
add dList, "Rock & Roll" -- 078
add dList, "Hard Rock" -- 079
add dList, "Folk" -- 080
add dList, "Folk-Rock" -- 081
add dList, "National Folk" -- 082
add dList, "Swing" -- 083
add dList, "Fast Fusion" -- 084
add dList, "Bebob" -- 085
add dList, "Latin" -- 086
add dList, "Revival" -- 087
add dList, "Celtic" -- 088
add dList, "Bluegrass" -- 089
add dList, "Avantgarde" -- 090
add dList, "Gothic Rock" -- 091
add dList, "Progressive Rock" -- 092
add dList, "Psychedelic Rock" -- 093
add dList, "Symphonic Rock" -- 094
add dList, "Slow Rock" -- 095
add dList, "Big Band" -- 096
add dList, "Chorus" -- 097
add dList, "Easy Listening" -- 098
add dList, "Acoustic" -- 099
add dList, "Humour" -- 100
add dList, "Speech" -- 101
add dList, "Chanson" -- 102
add dList, "Opera" -- 103
add dList, "Chamber Music" -- 104
add dList, "Sonata" -- 105
add dList, "Symphony" -- 106
add dList, "Booty Bass" -- 107
add dList, "Primus" -- 108
add dList, "Porn Groove" -- 109
add dList, "Satire" -- 110
add dList, "Slow Jam" -- 111
add dList, "Club" -- 112
add dList, "Tango" -- 113
add dList, "Samba" -- 114
add dList, "Folklore" -- 115
add dList, "Ballad" -- 116
add dList, "Power Ballad" -- 117
add dList, "Rhythmic Soul" -- 118
add dList, "Freestyle" -- 119
add dList, "Duet" -- 120
add dList, "Punk Rock" -- 121
add dList, "Drum Solo" -- 122
add dList, "Acapella" -- 123
add dList, "Euro-House" -- 124
add dList, "Dance Hall" -- 125
pGenreList = dList
end
