Je hebt zo van die momenten dat alles vanzelf op zijn plaats valt. Zo kocht ik afgelopen zomer een nieuwe MP3 speler. En die liet mij allerlei informatie zien die ik nog niet kende. Natuurlijk had ik bij het afspelen op de computer al wel eens gemerkt dat er soms wat merkwaardige titels voorbij kwamen, maar nu zag ik ineens bij bepaalde MP3’s ook plaatjes op mijn display verschijnen. Tijd dus om uit te zoeken hoe dat zit en hoe je die informatie kunt wijzigen, aanvullen of verwijderen.
Bijna op dezelfde dag kreeg ik een testlicentie voor Delphi 2009. Dus waarom niet het nuttige met het aangename verenigen en met de nieuwe Delphi iets maken om mijn mp3’tjes op te schonen.
www.id3.org levert de standaard voor al de toegevoegde informatie die officieel ID3 tags heten
Gelukkig hoef je dat niet allemaal zelf te verzinnen. Naast mp3’s kun je op internet ook eenvoudig achterhalen hoe het allemaal in elkaar steekt. Een belangrijke bron is de website www.id3.org. Die levert de standaard voor al de toegevoegde informatie die officieel ID3 tags heten. Het is ook een goed startpunt voor allerlei voorbeeldprogramma’s, al dan niet met broncode. Maar deze broncode zal niet zomaar gaan werken onder Delphi 2009. En ze maken zeker geen gebruik van de nieuwe mogelijkheden die Delphi 2009 ons biedt. Dit artikel beschrijft een implementatie die zeker wel werkt onder Delphi 2009.
Het MP3 format
Voordat we aan de slag kunnen moeten we natuurlijk eerst weten hoe een MP3 bestand in elkaar steekt. De algemene opzet is eenvoudig:
- ID3v2 Tag (versie 2)
- Audio data
- ID3v1 Tag (versie 1).
Zoals gezegd, heel eenvoudig. Voor de eigenlijke (audio) data kan informatie zijn geplaatst en erachter ook, waarbij opgemerkt moet worden dat dit optioneel is. Alleen audio-data of audio-data met maar één tag mag ook. En ja hoor, strikt genomen mag ook de audio-data ontbreken! En dat kan nog zinvol zijn ook: er bestaat een testset met tags waarmee het lezen van de (ID3v2) tags kan worden getest. En die testbestandjes hebben geen audio nodig!
Wat we gaan doen is een TMP3file-class maken om van daaruit de verschillende onderdelen te benaderen. In eerste opzet ziet dat er dan als volgt uit:
Type TMP3file = class
V1tag : TID3v2Tag;
Mpeg : TMpegData;
V2tag : TID3v2Tag;
Procedure LoadFromFile(aFilename:string);
Procedure SaveToFile (aFilename:string);
end;
Later zal ik daar nog op terugkomen. We beginnen met de drie verschillende onderdelen van het bestand, maar eerst...
Een MP3Toolkit
Meestal loop je bij een implementatie tegen allerlei (bij)zaken aan die opgelost moeten worden. Zo krijgen je bij het lezen van tags te maken met een aantal tabellen (bijvoorbeeld, genres, soorten plaatjes, toegestane talen) en wat specifieke rekenkunde. Om dat niet steeds ad-hoc op te hoeven lossen kies ik ervoor dat allemaal in een aparte unit te stoppen. En binnen die unit definieer ik dan ook (de meeste) types die gebruikt gaan worden. Bovendien kies ik ervoor de algemene functies en procedures in een aparte class te stoppen. Dat maakt het wat duidelijker wat allemaal bij elkaar hoort. Dus als ik het later heb over een tabel of functie uit de toolkit, weet u nu waar ik het over heb. Ik zal u de verdere beschrijving besparen, alle details kunt u vinden in de download.
De ID3v1 Tag
De ID3v1 tag is qua opzet eenvoudig. De definitie dateert van 1996. Het is een blok van 128 bytes dat achter aan het bestand wordt geplakt. De indeling ligt helemaal vast:
type
TID3v1Data = record
ID : array [1..3] of AnsiChar;
Title : Array [1..30] of AnsiChar;
Artist : Array [1..30] of AnsiChar;
Album : Array [1..30] of AnsiChar;
Year : array [1..4] of AnsiChar;
Comment: Array [1..30] of AnsiChar;
Genre : Byte;
end;
Op het internet wemelt het van de voorbeeldprogramma’s om de ID3v1 Tag te lezen (en te schrijven). En de meesten zullen niet meer correct werken onder Delphi 2009. Want het chartype is nu een 2-bytes type terwijl in de definitie toch echt het 1-byte type wordt bedoeld. Vandaar ook dat ik in de definitie specifiek het type AnsiChar gebruik (in alle voorgaande versies van Delphi was dit het standaard type).
Er zijn een paar kleine details:
- Genre. Het Genre is een byte. En die verwijst naar een standaardtabel met genres, die is opgenomen in de eerder genoemde toolkit.
- Tracknummer. Optioneel kan een tracknummer worden toegevoegd. Daar is eigenlijk geen plaats voor in de datastructuur, maar in dat geval worden de laatste twee bytes van het veld “comment” opgeofferd. Als Comment[29] een #0 is dan vinden we in Comment[30] het tracknummer (als byte).
Lezen en Schrijven
Om de ID3v1 tag te lezen hoeven we dus alleen de laatste 128 bytes van het bestand te lezen. Als de header klopt, dat wil zeggen als het ID dat we lezen “TAG” is, dan gaan we ervanuit dat het inderdaad een versie 1 tag is. En als het niet klopt, dan is er dus geen versie 1 tag in het bestand aanwezig.
Bij het schrijven van een ID3v1 tag kijken we eerst of er al een tag in het bestand is opgenomen. Zo ja, dan overschrijven we die; anders plakken we hem eenvoudig weg achter aan het bestand. Het is niet de bedoeling eindeloos tags achter het bestand te plakken.
Karakterset of encoding
Het mag duidelijk zijn dat de gedachte achter de ID3v1 tag is dat alles in ASCII karakters zou worden geschreven. Een meestal is dat ook zo (ik ben nog geen echte voorbeelden van het tegendeel tegengekomen). Een probleem is wel welke ASCII-tabel is gebruikt. De liefhebbers van de Nederpop zullen er niet zo snel tegenaan lopen, maar als je per ongeluk een liefhebber bent van Griekse muziek dan loop je toch een goede kans dat de tag is aangemaakt op een machine met een Griekse codepage. En als een Griek het over “Mikis Theodorakis” heeft, dan schrijft hij “Μίκης Θεοδωράκης”, wat wij dan weer te zien krijgen als “Ìßêçò ÈåïäùñÜêçò”. Op zich kunnen we dat nog wel regelen, we kunnen Windows opdragen met de Griekse codepage te gaan werken, maar als we ook nog liefhebber zijn van Russische muziek, wordt dat toch een moeizaam verhaal.
Gelukkig kunnen we dat probleem nu afvangen … als we tenminste weten in welke codepage de data is geschreven. In SysUtils vinden we het nieuwe TEncoding type. En een Encoding is niets meer of minder dan een verzameling procedures waarmee je een aantal stringmanipulaties kunt doen met als uitgangspunt de bijbehorende karakterset/codepage.
De functie die wij nodig hebben is GetString. Deze functie verwacht als eerste een parameter van het type TBytes (=array of byte). Optioneel kunnen ook nog een startindex en een lengte worden opgegeven. Helaas kun je een “array [1..30] of AnsiChar” niet typecasten naar een TBytes, dus daar moeten we iets voor verzinnen. De code is als volgt:
GRencoding:=TEncoding.GetEncoding(1253);
SetLength(lBytes,30);
system.Move(ID3v1Data.Artist[1], lBytes[0],aSize);
MyTekst:=GRencoding.GetString(lBytes,0,30)
En zo krijgen we onze befaamde musicus weer te zien als een, in ieder geval voor een Griek, leesbare Griek.
Het is overigens heel verleidelijk de laatste drie regels te vervangen door een enkele regel waarin de “artist” wordt getypecast naar een TBytes, dus:
MyTekst:=
GRencoding.GetString(TBytes(ID3v1Data.Artist),0,30)
Maar zoals aangeven, het werkt echt niet. Het compileert uitstekend maar levert gegarandeerd een runtime-error op!
In de implementatie heb ik op het scherm een comboboxje gezet waarin een aantal coderingen is opgenomen (zie verderop). De gekozen Encoding is de encoding die gebruikt wordt bij lezen en schrijven. Het schermpje ziet er als volgt uit.

Fig. 1: Voorbeeld voor een ID3v1 tag in de standaard tekstcodering
Het vinkje geeft aan of er een Tag in het bestand is opgenomen (bij lezen). Bij het schrijven bepaalt het vinkje of de Tag al dan niet in het bestand wordt geschreven (eventueel wordt de tag dan verwijderd).
Verder is het (deel)scherm een rechttoe rechtaan weergave van de datastructuur.
In dit voorbeeld zien we tamelijk onleesbare teksten verschijnen. Dat komt omdat ik, bij wijze van voorbeeld, teksten in afwijkende coderingen heb geschreven. En om het nog erger te maken: ik heb zelfs voor ieder veld een verschillende codering gebruikt, een situatie die je in het echt niet tegen zult komen. Maar hier is het zinvol want het toont wat er zoal gebeurt. De teksten kunnen weer “leesbaar” gemaakt worden door de juiste codering te selecteren. Na voor “Grieks” te hebben gekozen ziet het scherm er als volgt uit:

Fig. 2: Voorbeeld voor een ID3v1 tag met de Griekse tekstcodering
Mikis Theodorakis is nu weer leesbaar (als je tenminste Grieks kunt lezen). De overige teksten worden behandeld alsof ze met de Griekse tekstcodering zijn geschreven. Het lijkt voor u misschien op Grieks, maar dat is het niet. Een Griek zou de titel lezen als “Pxrrth[], Axlgipthp[], Aelipsro” en dat lijkt ook voor hem nergens op. Om die teksten te kunnen “lezen” moeten we overschakelen naar de andere coderingen. Later zullen we nog zien welke.
Over Encodings
Vroeg of laat loop je tegen de vraag aan welke encodings er eigenlijk allemaal bestaan. In het generieke TEncoding-type worden enkele encodings gedefinieerd. Dit zijn class variabelen. Dat wil zeggen dat als je deze gebruikt ze maar één keer aangemaakt worden. Andere karaktersets moet je expliciet zelf aanmaken met de functie
TEncoding.GetEncoding(Codepage:integer);
De meest voorkomende codepages zijn de nummers 1250 (ANSII Europees) tot 1258 (ANSII Vietnamees). Die kunnen we dus eenvoudig aanmaken. Maar er zijn er nog veel meer (op mijn systeem zijn het er 53). Om die allemaal te kunnen vinden kun je de procedure EnumSystemCodePages gebruiken. Die doet precies wat de naam belooft. Je vindt hem in de Windows Unit.
EnumSystemCodePages(@CallBackProc,cp_Supported);
In plaats van cp_Supported kan je ook cp_Installed, gebruiken, bij mij maakt het geen verschil. De CallBackProc ziet er als volgt uit:
function CodePagesProc(aCodePageString:PChar): Cardinal;
stdcall;
var
lStr:String;
lCodePage:integer;
begin
lStr:=StrPas(aCodePageString);
lCodePage:=StrToInt(lStr);
// DOE ER IETS MEE
end;
Dan is er nog een klein detail. Met GetEncoding krijgen we wel de Encoding die hoort bij de betreffende CodePage, maar welke het is weten we, afgezien van het nummer, niet. Gelukkig kunnen we daar wel achterkomen door dit netjes aan Windows te vragen:
Function GetCodePageName(aCodePage:integer):String;
Var
lpCPInfoEx: CPINFOEXW;
begin
if GetCPInfoExW(lCodePage,0,lpCPInfoEx) then
result:= lpCPInfoEx.CodePageName;
end;
De CPINFOEXW structuur bevat ook andere informatie, bijvoorbeeld het MaxCharSize. De TEncoding gebruikt dit intern om het veld “IsSingleByte” te vullen. Helaas gebruikt Codegear deze informatie niet om de Encoding meteen maar een zinvolle caption te geven.
De MPEG data
Het is niet de bedoeling van dit artikel om te beschrijven hoe je zelf de audio-data kunt manipuleren. Maar op internet vond ik wel de informatie [2] hoe je de basisinformatie weer kunt geven. Deze code heb ik integraal in het demoprogramma opgenomen. Op het scherm zien we na het lezen van een MP3-bestand de gegevens over bitrate, etc.

Fig. 3: De Mpeg gegegens uit het voorbeeldbestand
De ID3v2 tag
Het hoeft geen betoog dat de informatie die in de ID3v1 tag gestopt kan worden beperkt is. Daarom is in 1998 ID3v2 tag gedefinieerd. Hiermee kan, praktisch gezien, onbeperkt informatie toegevoegd worden aan het bestand.
De ID3v2 tag wordt aan het begin van het bestand geschreven. Om te weten of er een ID3v2 tag aanwezig is lezen we de eerste 10 bytes van het bestand.
Als dat een geldige ID3v2-header is dan kunnen we de header verder lezen; zo niet, dan nemen we aan dat er geen ID3v2 header aanwezig is.
De algemene structuur van de ID3v2 tag is als volgt:
- Header
- Extended Header
- frame 1
- .....
- frame n
- Footer
- Padding
We zien dat de tag vooral bestaat uit één of meer frames. Afhankelijk van de versie (er bestaan drie versies van ID3v2) kan er ook nog “Extended Header” informatie en/of een “Footer” zijn opgenomen.
De gedachte achter “Padding” is de volgende: omdat de ID3v2 tag aan het begin van het bestand is geplaatst moet in principe bij iedere kleine wijziging het hele bestand opgeschoven worden
En dan is er nog “Padding”. De gedachte achter “Padding” is de volgende: omdat de ID3v2 tag aan het begin van het bestand is geplaatst moet in principe bij iedere kleine wijziging het hele bestand opgeschoven worden. Om dat te voorkomen kan/mag aan het einde van de ID3v2 tag een aantal nullen worden geschreven.
intermezzo
De eerste versie ID3v2 is versie 2.00 en dateert uit 1998. Deze is in 1999 vervangen door id3v2.3.0. In 2000 is deze definitie in id3v2.4.0 aangepast. Het zijn echter maar relatief kleine wijzigingen die vooral betrekking hebben op de header en footer. In de praktijk blijkt vooral versie 2.3 gebruikt te worden. Bij mij thuis heb ik maar een paar bestanden met 2.4 informatie gevonden. Maar dat kan natuurlijk ook gewoon komen omdat de meeste van die bestanden afkomstig zijn van mijn eigen CD’s en LP’s en aangemaakt zijn met hetzelfde programma…
Het TID3v2Tag type
Aangezien de versie2 tag feitelijk een lijst met frames is, kunnen we de TList goed gebruiken als basis. Of beter natuurlijk de TObjectList. Dan hoeven we ons geen zorgen meer te maken over het weer vrijgeven van de frames na gebruik.
Standaard gebruik ik een afgeleide van TObjectList met twee extra functies:
Type TMyObjectList=class(TObjectList)
Protected
Function CreateItem:pointer; virtual;
Public
Function AddNewItem:pointer;
End;
De functie CreateItem is virtual zodat deze eenvoudig in afgeleiden aangepast kan worden. Overigens gebruik ik zelden private properties of procedures. Niets zo frustrerend als gedwongen worden hele lappen codes te dupliceren om ergens een detail te kunnen wijzigen.
De ID3v2 Header
Gelukkig heeft de header van de ID3v2 tag voor alle versies dezelfde opbouw:
TID3v2Header = record
ID: array[1..3] of AnsiChar;
Version: Byte;
Revision: Byte;
Flags: Byte;
TagSize: TInt28;
end;
Ook hier moeten we het ID weer specifiek als AnsiChar definiëren. Version en Revision zijn wat ze lijken te zijn. In de Flags vinden we informatie over het wel of niet aanwezig zijn van een Extended header en/of een Footer, wat dan weer wel afhankelijk is van de versie. De Tagsize geeft ons de totale omvang van de tag (inclusief de header). Dat is een “safe sync integer”. Ik geef het toe, ik had er ook nog nooit van gehoord, maar het blijkt een integer te zijn waarbij steeds bit 8 wordt overgeslagen.
Om daar weer een “gewoon” getal van te maken moeten we wat met bits gaan schuiven. In de MP3toolkit is daar een functie Int28ToInt32 voor opgenomen.
Volgens de definitie mag de informatie uit de extended header desgewenst geskipt worden
In het voorbeeldprogramma heb ik een functie gemaakt om de header te decoderen. Deze functie geeft true terug als er een header wordt aangetroffen en false als dat niet het geval is. Als er een header is, dan wordt de position in de stream aan het begin van de lijst met frames gezet zodat we direct kunnen gaan lezen.
Bij het interpreteren van de header wordt de eventuele Extended Header ingelezen en apart gezet. Deze kan dan eventueel later weer ongewijzigd worden geschreven. Het demoprogramma doet verder niets met deze informatie. Dat is volledig legaal: volgens de definitie mag de informatie uit de extended header desgewenst geskipt worden.
ID3v2 frame
Alle frames bestaan uit een frame-header gevolgd door een aantal bytes content.
Op basis van de content kunnen de frames grofweg in 4 groepen worden verdeeld:
- Teksten: bijvoorbeeld de artiest of de titel van het nummer;
- Commentaar/Lyrics: dat zijn natuurlijk ook teksten, maar het bijzondere is dat deze teksten een taalkenmerk hebben;
- Plaatjes: bijvoorbeeld een afbeelding van de plaathoes;
- Overige: een bonte collectie verschillende frames.
Bij het opzetten van een Frame-class moeten we dus in ieder geval rekening houden met de mogelijkheid van verschillende eigenschappen en/of subclasses.
De eerste opzet van onze FrameClass zou er nu zo uit kunnen zien:
Type
TV2Frame=class
protected
FHeader : TV2Header;
FContent: TBytes;
End;
Maar dat werkt niet echt lekker : de header van versie 2.0 (6 bytes) ziet er anders uit dan de header van versie 2.3/2.4 (10 bytes). Het volgende tabelletje geeft de opbouw van de frame-header in de verschillende versies
| |
Versie 2.0 |
versie 2.3/2.4 |
| ID |
3*AnsiChar |
4*AnsiChar |
| Size |
3 bytes |
4 bytes |
| StatusFlag |
- |
1 byte |
| FormatFlag |
- |
1 byte |
Daarom kies ik ervoor de gegevens uit de frame-header bij het inlezen te vertalen naar de volgende properties.
Het FrameID
Een niet te negeren probleem is het verschil in ID. Een frame dat, als voorbeeld, de songtekst bevat wordt in versie 2.0 gedefinieerd als “TT2” en in 2.3/2.4 als “TIT2”.
Om dat op te lossen heb ik in de toolkit een tabel opgenomen waarmee de conversie van versie 2.00 naar 2.3/2.4 kan worden gemaakt. Andersom zou ook kunnen maar lijkt niet zo zinvol. In die tabel is tevens informatie opgenomen over het type frame. Voor een deel blijkt dat overigens al uit de naamgeving. Zo zijn ID’s die beginnen met een “T” per definitie een TextFrame. Nou ja, natuurlijk is er een uitzondering, het frame “TXXX” is een “User defined text information frame”.
Overigens mag een frameID alleen uit HOOFDletters en cijfers bestaan. Bij het inlezen wordt hierop getest. Als er een ongeldig FrameID wordt aangetroffen dan wordt aangenomen dat er een (lees)fout is gemaakt en wordt het lezen afgebroken.
De Frame-Size
De frame-size die we in de header vinden is in versie 2.3 en 2.4 bijna een gewone integer. Alleen staan de bytes niet in de volgorde die we gewend zijn. Die moeten we bij het inlezen even omdraaien. Hetzelfde is het geval bij versie 2.0 alleen is daar nog een extra complicatie omdat we maar 3 van de 4 bytes geleverd krijgen, de vierde moeten we er zelf even aanplakken. In de MP3toolkit is daar een kleine functie voor opgenomen.
De Frame-vlaggen
In een versie 2.3/2.4 header vinden we ook nog 2 vlaggen.
De Statusvlag geeft informatie over de status van het frame. Zo is er bijvoorbeeld een vlag om aan te geven of een frame wel of niet gewijzigd mag worden (read only). Maar ook een vlag die aangeeft wat er met het frame moet gebeuren als de data wijzigt. Dat klinkt wat curieus maar het volgende voorbeeld maakt het wellicht begrijpelijker: in sommige bestanden kom je een “PRIV”, een “private frame”, tegen waarin het “peaklevel” is opgenomen. Een player kan die informatie gebruiken om het geluidsvolume te corrigeren. Dat frame is dus niet meer zinvol als we aan de audio gaan sleutelen. En dan is het natuurlijk handig als we dat aan een vlag kunnen zien. Maar op dit moment zijn we niet in de audio zelf geïnteresseerd dus negeren we de statusvlag gewoon.
De Formatvlag is een ander geval. Hier vinden we onder andere informatie of er gebruik is gemaakt van encryptie en of compressie. Dat kan dus ook nog. Gelukkig ben ik het in de praktijk nog niet tegengekomen dus die formatvlag laat ik voorlopig ook met rust.
Voor wie er in geïnteresseerd is, of het echt nodig heeft, kan de verdere documentatie over deze flags vinden op www.id3.org.
ID3v2 Textframes
De meest gebruikte frames zijn TextFrames. Zo bevat het frame TPE1 de naam van de artiest, TIT2 de songtitel, TALB de naam van het album, en TRCK het tracknummer. Zoals eerder aangegeven zijn (bijna) alle frames die beginnen met een “T” per definitie textframes.
En ook de frames die beginnen met een “W” zijn textframes, met dien verstande dat het de bedoeling is dat hier een webadres in staat. Zo is bijvoorbeeld WPUB de “Publishers official webpage”. En ook hier geldt weer een uitzondering voor “WXXX”.
Bij het lezen (of beter gezegd: bij het schrijven) van textframes mogen er officieel maar 4 tekstcoderingen worden gebruikt:
| 0: Iso8859-1 |
| 1: UNICODE WITH BOM |
| 2: UTF-16, without BOM |
| 3: UTF-8 |
En voor wie echt oplet en denkt dat de twee codering op basis van de BOM (Byte Order Mask) eigenlijk in twee coderingen uiteenvalt heeft gelijk. Alleen is één van die twee coderingen dan toch weer gelijk aan UTF-16 dus houden we er toch weer 4 over.
En ook hier kunnen we voor het lezen weer goed gebruik maken van het TEncoding type. Aangezien we met maar, maximaal, 4 coderingen te maken hebben genereren we die Encodings vast maar in onze framelist.
Natuurlijk hebben we hier ook weer te maken met de eerder genoemde complicatie bij de ASCII codering. Officieel moet er in Iso8859 geschreven worden. Maar zoals eerder aangegeven is de praktijk dat iedereen schrijft volgens zijn eigen codepage. Dus komt ook hier onze beroemde Griek weer misvormd te voorschijn. En ook hier kunnen we dat eenvoudig oplossen door bij het lezen de standaard Iso8859 te overrulen door de op dat moment zinvolle Encoding.
Eerder hebben we gezien dat we voor het converteren van data naar een string de volgende procedure gebruiken:
MijnLabel := aEncoding.GetString(FData,aStartPos,aSize)
Over de Encoding hebben we het net gehad, de data hebben we gelezen, maar nu moet het begin en einde van de tekst nog worden bepaald.
Voor een Textframe is dat eenvoudig: het eerste byte geeft de codering aan en je begint bij het tweede byte (met index=1).
Wat betreft de lengte: dat is een kwestie van zoeken. Natuurlijk mogen we nooit verder lezen dan de data groot is, maar we moeten ook ophouden als we een nul karakter tegenkomen. En dat is nog even oppassen, want bij de coderingen 1 en 2 betekent dat dus totdat je twee nullen achter elkaar tegenkomt! Ik heb daar maar een tweetrapsraket van gemaakt. In de eerste trap wordt de lengte van de string bepaald, waarna in de tweede trap de string wordt opgehaald.
En dat is alles wat we nodig hebben om textframes te lezen!
Picture Frames
In de ID3v2 tag kunnen ook frames met afbeeldingen worden opgenomen. Een voor de hand liggend voorbeeld is natuurlijk de afbeelding van de (plaat)hoes.
De opbouw van een pictureframe is als volgt:
| Mime |
in versie 2.0 drie ascii karakters, in 2.3/2.4 een zero-terminated ascii string |
| Picturetype |
byte |
| Description |
zero-terminated ascii string |
| Data |
het feitelijke plaatje |
Er zijn 21 voorgedefinieerde afbeeldingstypes, wat weer een functie voor onze toolkit oplevert.
De Mime (Multipurpose Internet Mail Extensions) geeft het type afbeelding weer. Meestal hebben we te maken met “JPG” (versie 2.0) en “image/jpeg” (versie 2.3/2.4). In het “description” veld zou een omschrijving kunnen staan, maar eerlijk gezegd heb ik nog nooit een voorbeeld gezien waar iemand de moeite heeft genomen daar wat in te zetten.
Wat we nu bij het inlezen doen is deze gegevens decoderen en klaarzetten voor gebruik. Daartoe voegen we aan het TFrametype wat extra eigenschappen en functies toe. Een overweging zou zijn hiervoor een afgeleide TPicFrameType voor te maken. Maar daar heb ik vooralsnog van afgezien, en dit werkt prima!
Het verwerken van een PictureFrame gaat als volgt:
procedure TID3v2Frame.PreparePictureFrame
(aVersion :byte);
var
lStartPos:TSizeType;
begin
if FData<>NIL then
begin
case aVersion of
2 : begin
FMime := AsciiEncoding.GetString(FData,1,3);
lStartPos:=4;
end;
else
begin
lStartPos:=1;
FMime :=
AnyTextFromData(lStartPos, AsciiEncoding);
end;
end;
self.FPicType := FData[lStartPos];
inc(lStartPos);
self.Fdescription :=
AnyTextFromData(lStartPos, AsciiEncoding);
FDataStart:=lStartPos;
end;
end;
Om de data van het plaatje op te halen maak gebruik ik van een TStream. Die kan dan direct aan een geschikte TGraphic gegeven worden om op scherm te tonen. Je moet dan overigens wel even aan de hand van de “Mime” controleren of het wel kan/mag! In het programma gebruik ik een TMemoryStream, maar je kan natuurlijk ook een TFileStream gebruiken om het plaatje op te slaan in een bestand.
procedure TID3v2Frame.GetPicture(aStream: TStream);
begin
if FFrameType=v2fPIC then
begin
aStream.WriteBuffer(FData[FStartPicIDX],
FDataSize - FStartPicIDX );
end;
end;
Voor het schrijven gebruik ik ook weer een TStream. Je wordt geacht zelf het juiste mime-type mee te geven zodat daar geen problemen over kunnen ontstaan.
procedure TID3v2Frame.SetPicture(
aStream: TStream; aMime:String);
begin
setlength(FData, aStream.size);
aStream.Seek(0, soFromBeginning);
FDataSize :=aStream.Size;
setLength(FData,FDataSize);
aStream.Read(FData[0], FDataSize);
FDataStart:=0;
SetFrameIDStr(IDv2_PICTURE);
FMime := aMime;
end;
Bij het schrijven overschrijf ik de oorspronkelijke data. Die inhoud had ik toch al bij het inlezen verwerkt!
Commentaar frames
Nog een speciaal geval. Er kunnen commentaarframes worden opgenomen. Deze hebben de volgende structuur:
| Tekstcodering |
1 byte |
| Taal |
3 bytes ascii |
| Omschrijving |
zero terminated string |
| Commentaar |
string, (al dan niet zero terminated) |
De taal mag iedere taal zijn, zolang die maar voorkomt in de standaardlijst met talen. Dat is dus weer een standaardfunctie in onze toolkit!
Omschrijving en commentaar dienen te zijn geschreven in de aangegeven tekstcodering. En opgepast, bij type 1 wordt er twee keer een BOM (Byte Order Mask) geschreven! Het commentaar kan/mag uit meerdere regels bestaan.
Overige frames
En dan is er nog een bonte verzameling overige frames. Hiervoor verwijs ik graag naar de site www.id3.org [1]. Sommige zijn “user defined”, daar kan je dus niet heel veel mee. Voor een aantal anderen geldt dat er een specificatieveld is gedefinieerd. Voor een aantal frames heb ik dat in de broncode verwerkt.
Een Schermvoorbeeld
Voor het weergeven van de ID3v2 gegevens is uiteraard meer nodig dan voor de ID3v1 gegevens. Toch geef ik de basisgegevens op identieke wijze weer. Dit zijn toch degene die je het meeste gebruikt en dan kunnen ze maar overzichtelijk klaarstaan. Voor het voorbeeldbestand ziet het scherm er als volgt uit:

Fig. 4: De basisgegevens in de ID3v2 tag
We zien nu alle talen gewoon door elkaar heen. Ik heb ook maar even aangegeven in welke codepage de gegevens in de ID3v1 tag zijn geschreven.
(Een detail: De velden “Album” en “Commentaar” zijn geschreven in het arabisch en hebreews. Dus eigenlijk zou deze informatie rechts uitgelijnd moeten worden.)
Onder deze standaardgegevens heb ik een tabel met alle gevonden frames opgenomen. Hieronder als voorbeeld een deel van de gegevens:

Fig. 5: Alle frames uit de ID3v2 tag
Rechts zien we het FrameID, met de omschrijving opgehaald in de MP3toolkit, links een deel van de inhoud. Merk op dat ook de standaardgegevens hier weer terugkomen. Die komen immers ook uit frames!
De inhoud in de linkerkolom is in principe maar een gedeelte van de totale informatie in het frame. De rest van de informatie wordt ernaast getoond in een standaard TImage en TMemo component.
Het schrijven de ID3v2 tag
Om een lang verhaal kort te houden: dat is dus gewoon een kwestie van alle frames netjes achter elkaar plakken. Er zijn wel een paar dingen waar we rekening mee moeten houden…
Als er helemaal geen informatie is dan worden we geacht geen ID3v2 tag te schrijven (een lege tag mag dus niet) en voor de meeste frames geldt dat er geen dubbelen voor mogen komen. Dus geen twee titels, maar ook geen twee uitvoerenden! De eerste controle is ingebouwd, de andere laat ik graag over aan een ieders fantasie (het probleem is dat je dat allemaal moet terugkoppelen naar de gebruiker, opties aanbieden wat te doen met het dubbele frame, enzovoort). Het zal dus niet meevallen om een echt waterdicht programma te schrijven.
Standaard schrijf ik in versie ID3v2.3 (die komt immers het meest voor). Bovendien negeer ik bij het schrijven eventuele extended data (al is het maar dat ik dan niet het probleem heb dat de extended data mogelijk uit een andere versie komt). En volgens de definitie mag ik deze gegevens ook negeren! Verder kan ik bij het schrijven opgeven welke padding ik maximaal wil gebruiken.
En niet te vergeten in welke codering er moet worden geschreven. Het standaard kiezen van een Unicode codering heeft als nadeel dat de meeste teksten twee maal zo groot worden zonder dat dit echt zinvol is. Dus misschien zou UTF-8 een goede optie kunnen zijn. Ik heb een iets flexibelere oplossing gekozen: je kan in het voorbeeldprogramma kiezen voor één van de volgende mogelijkheden:
| altijd UTF8 |
| altijd Unicode |
| Ascii waar mogelijk, anders UTF8 |
| Ascii waar mogelijk, anders UNICODE |
Bij de laatste twee opties wordt eerst gekeken of de tekst in de (standaard) ASCII codering goed wordt weergegeven. Zo ja, dan gebruiken we ASCII, zo niet de aangegeven multi-byte codering.
Om te bepalen of er in ASCII kan worden geschreven kunnen we (alweer) gebruik maken van de TEncoding procedures. De truc is eigenlijk heel simpel: laat de encoding de string naar een TBytes vertalen, en zet die TBytes dan meteen weer terug naar een string. Als die hetzelfde is als waar we mee zijn begonnen dan kunnen we de Encoding gebuiken zonder dataverlies, anders schakelen we over naar de opgegeven multi-byte encoding:
lBytes:=MyAsciiEncoding.GetBytes(aValue);
if MyAsciiEncoding.GetString(lBytes)<>aValue
then GebruikMultiByte;
In de broncode is dit geïmplementeerd in de procedure GetAndSetTextType.
Deze bepaalt eerst de te gebruiken codering en zet vervolgens de juiste code in de outputdata. Zoals we gezien hebben bij het lezen is dat het eerste byte van de data.
Het schrijven van de tags in een MP3-bestand
Eerder heb ik aangegeven dat ik een TMP3file class gebruik waarin de tags als properties zijn opgenomen.
Bij het schrijven wordt gekeken naar de EXISTS property van de beide tags om te bepalen of het geschreven moet worden. Als die op false staat, dan wordt een eventueel aanwezige tag automatisch verwijderd. Het vinkje op het scherm bepaalt hoe de property wordt gezet.
Bij het schrijven van ID3v2 tag, die vooraan het bestand geplaatst moet worden, kunnen er twee situaties ontstaan:
- De nieuwe tag past precies in de ruimte van de bestaande tag
- De nieuwe tag is groter of kleiner dan de bestaande tag.
In het eerste geval kunnen we de nieuwe tag eenvoudigweg over de bestaande tag heen schrijven. In het tweede geval moeten we de rest van het bestand verschuiven, naar voren of naar achteren.
Om dat te voorkomen kan een “padding” worden toegestaan. Dat wil zeggen een aantal “zero bytes” aan het einde van de tag. In de TMP3file heb ik een property “MaxPadding” opgenomen. Deze bepaalt de speelruimte waarmee geprobeerd kan worden om van situatie 2 toch een situatie 1 te maken. Standaard heb ik die op een waarde van 512 gezet. Je moet tenslotte iets kiezen.
Maar goed, op een gegeven moment ontkom je er niet aan. In dat geval maak ik een tijdelijk bestand aan, schrijf daar de nieuwe ID3v2 tag in, en plak er de audio-data en eventueel de ID3v1 achter. En vervolgens kopieer ik dat bestand over het oude bestand heen.
Ik gebruik daar een TMP3tempfile type voor dat zelf een unieke naam verzint in de standaard TEMP dir en die dat tijdelijke bestand ook weer voor me opruimt als ik klaar ben. Het aanmaken van dat tijdelijke bestand maken gaat dus wel lukken. Maar wat blijkt nu: overschrijven op een CD lukt niet, zelfs niet met beheersrechten! Het programma test er nog niet op, een verbeterpuntje dus!
En dan even nog dit….
Een klein voorbeeld programma zegt meestal meer dan vele bladzijden tekst. Alle genoemde code, en meer, is terug te vinden in het bijbehorende zip-bestand, te downloaden van de SDN-site. Het staat een ieder vrij om die code te gebruiken en/of naar eigen inzicht aan te passen. Bij verdere verspreiding wordt bronvermelding op prijs gesteld. Uiteraard worden tips, aanvullingen, foutmeldingen enzovoorts ook op prijs gesteld.
Maar voor u begint ….
Het gebruik van het voorbeeldprogramma is “as is”. Aansprakelijkheid voor het verloren gaan van uw dierbare muziekbestanden kan niet worden aanvaard. Aangeraden wordt altijd eerst een backup van uw bestanden te maken voordat u ze aan gaat passen (met dit voorbeeld programma of met ieder ander programma). Met name als de audio-data moet worden verplaatst kunnen er allerlei buffer problemen ontstaan, zeker in een netwerksituatie (vraag maar liever niet hoe ik dat weet).
Maar laat u niet te bang maken. Die problemen zijn eerder incidenteel dan structureel. Het is alleen vervelend om je muziek kwijt te raken alleen omdat het netwerk even te druk is om zich met jouw bestandje bezig te houden.
Daarnaast moet u er rekening mee houden dat iedere speler zijn eigen manier heeft hoe om te gaan met de informatie uit de tags. Gewoon een kwestie van proberen en zien wat er gebeurt. Zo accepteert mijn spelertje geen UTF8 teksten (het moet dan Unicode zijn). En daarnaast moeten de teksten ook nog met een nul afgesloten worden (wat volgens de standaard mag, maar niet hoeft). Het eerste kon ik nog wel uit de documentatie van de speler achterhalen, het laatste niet! En door het schrijven van dit artikel kwam ik er achter dat mijn speler wel Grieks een Russisch “spreekt” maar geen Arabische of Hebreeuwse teksten weer kan geven!
Kortom het opschonen van je mp3 bestanden is maatwerk!
Veel plezier!
Links
Bij het schrijven heb ik gebruik gemaakt van de volgende bronnen:
- [1] www.id3.org: op deze website is alle informatie over de ID3 tags terug te vinden.
- [2] het bestand MP3FileUtils.pas (v0.3a) van Daniel Gaussmann. Dit voorbeeld is te downloaden via www.gausi.de.
De sources die bij dit artikel horen kun je downloaden via Sman_MP3_SRC.zip.
Tussen het inleveren van het artikel en de publicatie is nog een aantal wijzigingen in de code doorgevoerd. Daarom wijkt de code op sommige plaatsen af van de code zoals gepresenteerd in het artikel. Een belangrijke toevoeging is dat de code geschikt is gemaakt voor gebruik in oudere versies van Delphi. Unicode karakters ziet u dan uiteraard niet, maar alles werkt verder wel!