Hi folks! I haven't posted for a while, so I thought I'd share some useful source code. However, it could probably be improved and I'm happy for people to test and provide feedback. The code allows you to read basic tag information from ID3v2 MP3 files.
There are some helper functions that could probably be improved but the performance is already pretty good. It only takes a minute or so to process ~3000 files and seems robust with ID3v2.3 and ID3v2.4 files.
A couple of notes:
1. It does not process footers
2. For performance reasons it does not use the content of frames longer than 512 bytes. This means all the useful frames are extracted (e.g. title, artist, album) but embedded images are ignored.
3. The GetMemblockString function doesn't handle unicode so I have hacked it so only useful ASCII characters are recognised. This is the thing I want to improve most.
If anybody has any exotic MP3 files (e.g. ID3v2.2 or extended headers) to test with then that would be helpful.
type ID3v2
Filename as string
MajorVersion as integer
MinorVersion as integer
HasTag as integer
HeaderSize as integer
FA_Unsynchronisation as integer
FB_ExtendedHeader as integer
FC_ExperimentalIndicator as integer
FD_Footer as integer
ExtendedHeaderSize as integer
EB_Update as integer
EC_CRC as integer
ED_Restrictions as integer
Restrictions as integer
Frames as ID3v2Frame[]
Title as string
Artist as string
Album as string
Year as string
Comment as string
Genre as string
Track as string
TotalTracks as string
endtype
type ID3v2Frame
FrameName as string
FrameContents as integer // Memblock handle
FrameSize as integer
MajorVersion as integer
F_TagAlterPreservation as integer
F_FileAlterPreservation as integer
F_ReadOnly as integer
F_Compression as integer
F_Encryption as integer
F_GroupingIdentity as integer
F_Unsynchronisation as integer
F_DataLengthIndicator
Padding as integer
endtype
function CreateID3(filename as string)
local tag as ID3v2
if GetFileExists(filename) = 1
f = OpenToRead(filename)
// Read header
if ReadChars(f, 3) = "ID3"
tag.HasTag = 1
tag.MajorVersion = Readbyte(f)
tag.MinorVersion = Readbyte(f)
flags = ReadByte(f)
tag.FA_Unsynchronisation = GetBit(flags, 0)
tag.FB_ExtendedHeader = GetBit(flags, 1)
tag.FC_ExperimentalIndicator = GetBit(flags, 2)
tag.HeaderSize = ReadReversedInteger(f)
tag.HeaderSize = Int28(BitArray(tag.HeaderSize))
if tag.FB_ExtendedHeader = 1
// Read extended header
tag.ExtendedHeaderSize = ReadInteger(f)
tag.ExtendedHeaderSize = Int28(BitArray(tag.ExtendedHeaderSize))
flags = ReadByte(f)
tag.EB_Update = GetBit(flags, 1)
tag.EC_CRC = GetBit(flags, 2)
tag.ED_Restrictions = GetBit(flags, 3)
if tag.EB_Update = 1
ReadByte(f)
endif
if tag.EC_CRC = 1
if ReadByte(f) = 5
for i = 0 to 4
ReadByte(f)
next i
endif
endif
if tag.ED_Restrictions = 1
ReadByte(f)
tag.Restrictions = ReadByte(f)
endif
endif
repeat
local frame as ID3v2Frame
frame = ReadFrame(f, tag.MajorVersion)
if frame.Padding = 1
SetFilePos(f, tag.HeaderSize)
else
tag.Frames.Insert(frame)
endif
until GetFilePos(f) >= tag.HeaderSize
endif
ParseTagFrames(tag)
DeleteID3(tag)
CloseFile(f)
endif
endfunction tag
function DeleteID3(tag ref as ID3v2)
for i = 0 to tag.Frames.Length
DeleteMemblock(tag.Frames[i].FrameContents)
next i
tag.Frames.Length = -1
endfunction
function ParseTagFrames(tag ref as ID3v2)
for i = 0 to tag.Frames.length
if tag.MajorVersion = 1
if tag.Frames[i].FrameName = "TT2"
tag.Title = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "TP1"
tag.Artist = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "TAL"
tag.Album = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "TYE"
tag.Year = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "TRK"
tag.Track = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "TCO"
tag.Genre = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "COM"
tag.Comment = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "TP1"
tag.Artist = MemblockToString(tag.Frames[i].FrameContents)
endif
else
if tag.Frames[i].FrameName = "TIT2"
tag.Title = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "TPE1"
tag.Artist = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "TALB"
tag.Album = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "TYER"
tag.Year = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "TRCK"
tag.Track = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "TCON"
tag.Genre = MemblockToString(tag.Frames[i].FrameContents)
elseif tag.Frames[i].FrameName = "COMM"
tag.Comment = MemblockToString(tag.Frames[i].FrameContents)
endif
endif
next i
endfunction
function ReadFrame(f, version)
local frame as ID3v2Frame
nameSize = 4
frameSize = 0
if version = 2
nameSize = 3
elseif version = 3 or version = 4
nameSize = 4
endif
frame.FrameName = ReadChars(f, nameSize)
frame.MajorVersion = version
padTest$ = Chr(0) + Chr(0) + Chr(0)
if nameSize = 4
padTest$ = padTest$ + Chr(0)
endif
if frame.FrameName = padTest$
frame.Padding = 1
exitfunction frame
endif
if version = 2
b1 = ReadByte(f)
b2 = ReadByte(f)
b3 = ReadByte(f)
temp$ = "00000000" + IntToByte(BitArray(b3)) + IntToByte(BitArray(b2)) + IntToByte(BitArray(b1))
frameSize = Int28(temp$)
elseif version = 3 or version = 4
frameSize = Int28(BitArray(ReadReversedInteger(f)))
endif
frame.FrameSize = frameSize
if version > 2
if version = 3
flags = ReadByte(f)
frame.F_TagAlterPreservation = GetBit(flags, 0)
frame.F_FileAlterPreservation = GetBit(flags, 1)
frame.F_ReadOnly = GetBit(flags, 2)
flags = ReadByte(f)
frame.F_Compression = GetBit(flags, 0)
frame.F_Encryption = GetBit(flags, 1)
frame.F_GroupingIdentity = GetBit(flags, 2)
elseif version = 4
flags = ReadByte(f)
frame.F_TagAlterPreservation = GetBit(flags, 1)
frame.F_FileAlterPreservation = GetBit(flags, 2)
frame.F_ReadOnly = GetBit(flags, 3)
flags = ReadByte(f)
frame.F_GroupingIdentity = GetBit(flags, 1)
frame.F_Compression = GetBit(flags, 4)
frame.F_Encryption = GetBit(flags, 5)
frame.F_Unsynchronisation = GetBit(flags, 6)
frame.F_DataLengthIndicator = GetBit(flags, 7)
endif
if frame.FrameSize > 0
if frame.FrameSize < 512
frame.FrameContents = CreateMemblock(frame.FrameSize)
for i = 0 to frame.FrameSize - 1
SetMemblockByte(frame.FrameContents, i, ReadByte(f))
next i
else
SetFilePos(f, GetFilePos(f) + frame.FrameSize)
endif
endif
endif
endfunction frame
function MemblockToString(mem)
if GetMemblockExists(mem) = 0
exitfunction ""
endif
mblen = GetMemblockSize(mem)
t$ = ""
for i = 0 to mblen - 1
v = GetMemblockByte(mem, i)
if v >= 32 and v <= 126
t$ = t$ + Chr(v)
endif
next i
endfunction t$
function ReadChars(fileID, length)
mem = CreateMemblock(length)
for i = 0 to length - 1
b = ReadByte(fileID)
SetMemblockByte(mem, i, b)
next i
v$ = GetMemblockString(mem, 0, length)
DeleteMemblock(mem)
endfunction v$
function ReadReversedInteger(fileID)
mem = CreateMemblock(4)
for i = 0 to 3
SetMemblockByte(mem, 3 - i, ReadByte(fileID))
next i
r = GetMemblockInt(mem, 0)
DeleteMemblock(mem)
endfunction r
function BitArray(b)
v$ = ""
for i = 31 to 0 step -1
bit = GetBit(b, i)
v$ = v$ + Str(bit)
next i
endfunction v$
function BitsToInteger(b$)
v = 0
for i = 1 to Len(b$)
if Mid(b$, i, 1) = "1"
v = v + Pow(2, 31 - (i - 1))
endif
next i
endfunction v
function IntToByte(b$)
v$ = Right(b$, 8)
endfunction v$
function Int28(a$)
v$ = ""
for i = 1 to Len(a$)
if Mod(i, 8) <> 1
v$ = v$ + Mid(a$, i, 1)
endif
next i
r = BitsToInteger("0000" + v$)
endfunction r
function GetBit(a, b)
if (a && (1 << b)) > 0
exitfunction 1
endif
endfunction 0
TIP: If you're trying to read a path outside of the AppGameKit read/write folders then be sure to use the "raw:" prefix in the path.