|
|
General Purpose MP3 Parent
Added on 9/24/2000
|
This parent script will easily allow the reading and writing of ID3 Tags (which contain artist info, song title etc) to MP3 audio files.
It will also read the MP3 header information, which includes items such as the bitrate, sampling frequency, copyright flag, etc.
Requires Director 7.0+ (uses dot.syntax everywhere).
Requires the BinaryIO Xtra.
-- MP3 Parent
-------------
-- General Purpose MP3 Info Parent Script
-- copyright (c) 2000 kendall anderson. all rights reserved.
-- contact: kpander@home.com, http://members.home.com/kpander/
-- 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
|
|