Václav Kadlec 15.2.2005
Dnešní článek vysvětlí, jakým způsobem napsat aplikaci umožňující zobrazovat, editovat a ukládat tagy v souborech MP3. Napíšeme společně jednoduchý editor ID3 tagů. V souvislosti s tím si vysvětlíme, jek si v Delphi poradit s nekompatibilními typy textových řetězců.
Dnešní článek je dalším pokračováním našeho aktuálního tématu, v jehož rámci zkoumáme vlastnosti a možnosti MP3 souborů a jejich zpracování ve vývojovém prostředí Delphi. Před týdnem jsme se zabývali praktickou implementací Id3 tagů v Delphi, nedokončili jsme však všechno: skončili jsme u přečtení tagu z MP3 souboru a u jeho zobrazení.
Dnes plynule navážeme a zaměříme se na další důležitou manipulaci s ID3 tagem: na jeho modifikaci a zapsání zpět do souboru MP3.
Zapsání ID3 tagu do souboru není příliš komplikované, za předpokladu, že víme o ID3 tazích vše, co jsme si dosud uvedli, tedy jejich strukturu, jejich implementaci a jejich umístění v MP3 souborech.
Připomeňme, že implementace ID3 tagu v Delphi může vypadat například takto:
type TID3tag_typ = record
hlavicka: array[0..2] of char;
nazev: array[0..29] of char;
interpret: array[0..29] of char;
album: array[0..29] of char;
rok: array[0..3] of char;
komentar: array[0..29] of char;
zanr: byte;
end;
Pokud chceme napsat aplikaci, která bude umožňovat editaci ID3 tagu a jeho zápis do souboru, musíme předně zajistit zápis nových (aktuálních) údajů do této struktury a následně zapsání celé struktury do diskového MP3 souboru.
Začneme paeradoxně druhým uvedeným problémem, a to zapsáním hotového tagu (hotové struktury) do souboru. Poté, co si takto připravíme „pole působnosti“, si ukážeme, jak zajistit zapsání požadovaných údajů do výše uvedené struktury.
Procedura pro zápis ID3 tagu na disk může vypadat třeba tak, jak je naznačeno v následujícím zdrojovém kódu:
procedure ChangeID3Tag(NovaID3: TID3tag_typ; mp3FileName: string);
var
fMP3: file of Byte;
Stara : TID3Tag_typ;
begin
try
AssignFile(fMP3, mp3FileName);
Reset(fMP3);
try
Seek(fMP3, FileSize(fMP3) - 128);
BlockRead(fMP3, Stara, SizeOf(Stara));
if Stara.Hlavicka = `TAG` then
{ Replace old tag }
Seek(fMP3, FileSize(fMP3) - 128)
else
{ Append tag to file because it doesn`t exist }
Seek(fMP3, FileSize(fMP3));
BlockWrite(fMP3, NovaID3, SizeOf(NovaID3));
finally
end;
finally
CloseFile(fMP3);
end;
end;
Zdrojový kód je asi poměrně dobře čitelný: k realizaci svého cíle (k fyzickému čtení a zápisu na disk) používá procedury BlockRead a BlockWrite.
Procedura je zapsána univerzálně, to znamená, že při požadavku na změnu ID3 tagu v empétrojce nic nepředpokládá (třeba že by soubor s empétrojkou byl již otevřen nebo že by byla dokonce nastavena pozice v souboru na začátku příslušného tagu). Po zavolání procedury dojde k otevření souboru předaného v parametru, následně k (pokusu o) přečtení starého ID3 tagu a k nastavení zápisové pozice na správné místo v souboru:
Možná bychom se mohli ještě velmi zmínit o tom, jaká je funkce procedur BlockRead a BlockWrite, přestože jsme se jimi již nepochybně zabývali v rámci kapitoly týkající se zpracovávání diskových souborů.
procedure BlockRead(var F: File; var Buf; Count: Integer [; var AmtTransferred: Integer]);
Procedura BlockRead načítá jeden nebo více záznamů ze souboru s neudaným typem. Parametr Buf je jakákoliv proměnná, do které se bude zapisovat přečtená hodnota, Count udává počet záznamů, které se mají ze souboru přečíst (pokud jich v souboru existuje méně, jsou přečteny všechny). Aktuální počet přečtených záznamů (tedy méně nebo rovno parametru Count) je vracen v nepovinném parametru AmtTransferred. Pokud není parametr AmtTransferred zadán a přečte se méně záznamů, než udává proměnná Count, dojde k chybě. Velikost načítaného bloku (tedy množství bytů načtených najednou) se nastavuje při otvírání souboru.
procedure BlockWrite(var F: File; var Buf; Count: Integer [; var AmtTransferred: Integer]);
Procedura BlockWrite je zcela analogická s procedurou BlockRead. Pracuje obdobně, zapisuje jeden nebo více záznamů do souboru bez udaného typu. Také parametry procedury jsou zcela stejné.
Tolik k přípravě procedury, která zapíše ID3 tag do souboru na disk. Ještě předtím, než tuto proceduru ze své aplikace zavoláme, však musíme vyřešit, jak požadovaná data do struktury obsahující ID3 tag vůbec uložit.
Možná si říkáte, v čem může být problém, proč prostě nezkopírujeme požadované texty (jméno skladby, jméno autora apod.) do jednotlivých složek struktury. Všimněte si prosím, že jednotlivé složky struktury nejsou implementovány jako řetězce, nýbrž jako pole znaků. Ne že by se z podstaty věci jednalo o velký rozdíl, nicméně z implementačního hlediska se jedná o nekompatibilní datové typy, proto prosté přiřazení nebude fungovat.
Abychom si vše ukázali prakticky, podívejme se na následující ukázku zdrojového kódu. Předpokládejme, že jednotlivé informace o empétrojce jsou uvedeny v editačních polích Edit1 až Edit6. Dále předpokládejme, že máme definován datový typ TID3tag_typ podle deklarace uvedené v úvodu dnešního článku. Zapsání údajů do struktury a následné zavolání zápisové procedury by mohlo vypadat například takto:
procedure TForm1.Button2Click(Sender: TObject);
var NovyTag: TID3tag_typ;
begin
NovyTag.hlavicka := `TAG`;
NovyTag.nazev := Edit1.Text;
NovyTag.interpret := Edit2.Text;
NovyTag.album := Edit3.Text;
NovyTag.rok := Edit4.Text;
NovyTag.komentar := Edit5.Text;
NovyTag.zanr := 0;ChangeID3Tag(NovyTag; OpenDialog1.Filename);
End;
Pokud se ovšem pokusíme takovouto proceduru zkompilovat, obdržíme nanejvýš chybovou hlášku říkající cosi o nekompatibilních typch Array a TCaption. Důvod je uveden výše – pokoušíme se přiřazovat údaj typu TCaption do údaje typu Array. Datový typ TCaption (což je datový typ vlastnosti Text komponenty Edit) je přitom definován (v souboru Controls.pas) takto:
TCaption = type string;
Jedná se tedy o klasický řetězec. Jak si poradit? Protože jsme právě narazili na problém, který se může vyskytnout v celé řadě různých situací a při programování celé řady úloh, nebudeme váhat, učiníme Cimrmanovský úkrok stranou a prozkoumáme celou otázku podrobněji.
Není sporu o tom, že v obou případech pracujeme vlastně s textovými řetězci (neberte mě za slovo, hned vysvětlím, co mám na mysli). Na jedné straně (TCaption) stojí řetězec v pravém slova smyslu, tedy posloupnost znaků definovaná jako string. Na straně druhé (array of char) stojí sice něco trochu jiného, ale ve svém důsledku fungujícím stejně – máme také posloupnost znaků, jen je definovaná jako posloupnost znaků. Z pouhého názoru je tedy asi zřejmé, že nestojíme před otázkou, jak zajistit fyzické uložení údajů do paměti počítače – v obou případech bude vypadat velmi, velmi podobně. Stojíme před otázkou, jak přesvědčit překladač, aby námi požadované uložení údajů do paměti provedl.
Otázka tedy stojí takto: jak přesvědčit překladač, aby znaky reprezentované jako „řetězec“ uložil do paměti, která je reprezentovaná jako „pole znaků“.
Pouhé přiřazení nemůže v jazyku se silnou typovou kontrolou fungovat, proto ty výše uvedená chybová hláška. Přetypování také nepřipadá příliš v úvahu (jak byste přetypovali na array of char?), a proto budeme muset použít specializovanou funkci.
V našem případě bude nejvhodnější funkce StrCopy, která slouží ke kopírování řetězců. Funkce má dva argumenty - prvním je cílové umístění (cílový řetězec), druhým pak zdrojový řetězec:
function StrCopy(Dest: PChar; const Source: PChar): PChar;
Cílovým řetězcem je v našem případě pole znaků, například NovyTag.nazev, zdrojový řetězec je uveden v příslušném editačním poli, například Edit1.Text. Příkaz by tedy mohl vypadat třeba takto:
StrCopy(NovyTag.nazev, Edit1.Text);
Pokud ovšem tento příkaz použijete, bude výsledkem opět pouze chybová hláška. V deklaraci funkce StrCopy si totiž povšimněte, jak jsou zdrojový a cílový řetězec deklarovány: v obou případech jde o data typu PChar, takže nehovoříme o řetězci, ale vlastně o ukazateli na řetězec.
Nyní už nám ale pomůže přetypování, protože přetypování z datového typu „řetězec“ na datový typ „ukazatel na řetězec“ je jednoduché a lze k němu použít následující výraz:
PChar(retezec);
Následně využijeme faktu, že ukazatel na řetězec (datový typ PChar) je kompatibilní s polem znaků indexovaným od nuly (array [0..max] of char). Takovýmto polím se totiž říká „zero-based character array” a jsou používány k uložení nulou ukončených řetězců (což je vlastně charakteristika pro ukazatele na řetězce).
Poslední informace, kterou si v této souvislosti uvedeme, souvisí s možným vylepšením příkazu StrCopy. Tento příkaz totiž nijak nekontroluje délku kopírovaných dat. Protože my víme, jak dlouhá (přesněji řečeno, jak maximálně dlouhá) data budeme kopírovat, použijeme příkaz StrLCopy, který funguje úplně stejně jako příkaz StrCopy, nicméně umožňuje kontrolovat (nastavit maximální) délku kopírovaných dat. Podívejme se na následující příkaz:
StrLCopy(NovyTag.nazev, PChar(Edit1.Text), 30);
Tento příkaz zajistí, že zkopírujeme obsah editačního pole Edit1.Text do položky NovyTag.nazev, přičemž maximální délka kopírovaných dat je 30 znaků.
Posledním problémem, před kterým stojíme, je žánr skladeb. Jak víme z minula, žánr se v ID3 tagu ukládá jako celé číslo, na jehož základě posléze z předdefinovaného seznamu vyberme odpovídající název žánru. Stojíme tedy před otázkou, jak z textového pojmenování žánru dostat index v předdefinovaném seznamu. Řešení je celá řada, jedno z nich ukazuje následující zdrojový kód:
X := 0;
NovyTag.zanr := CelkemZanru + 1;
while (X <= CelkemZanru) do begin
if (Edit6.Text = ID3zanr[X]) then begin
NovyTag.zanr := X;
break;
end;Inc(X);
end;Uveďme si nyní celé znění příslušné procedury:
procedure TForm1.Button2Click(Sender: TObject);
var NovyTag: TID3tag_typ;
x: short;
begin
NovyTag.hlavicka := `TAG`;StrLCopy(NovyTag.nazev, PChar(Edit1.Text), 31);
StrLCopy(NovyTag.interpret, PChar(Edit2.Text), 30);
StrLCopy(NovyTag.album, PChar(Edit3.Text), 30);
StrLCopy(NovyTag.rok, PChar(Edit4.Text), 4);
StrLCopy(NovyTag.komentar, PChar(Edit5.Text), 30);X := 0;
NovyTag.zanr := CelkemZanru + 1;
while (X <= CelkemZanru) do begin
if (Edit6.Text = ID3zanr[X]) then begin
NovyTag.zanr := X;
break;
end;Inc(X);
end;MediaPlayer1.Close;
ChangeID3Tag(NovyTag, OpenDialog1.Filename);
MediaPlayer1.Open;
End;
V souvislosti se zdrojovým kódem je pouze nutné upozornit na jednu věc: před zavoláním procedury ChangeID3tag je nutné zavřít MediaPlayer (proto volání MediaPlayer1.Close). Pokud bychom to neudělali, došlo by při pokusu o zapsání souboru na disk k vyvolání chybové hlášky: pokud je totiž zrovna soubor MediaPlayerem přehráván, je otevřený a nelze do něj zapsat žádným jiným (ani tím samým) procesem. Po provedení operace je možné MediaPlayer opět znovu otevřít.
Ukázková aplikace bude rozšířením verze vytvořené minule. Připomeňme, že v minulém dílu seriálu jsme společně vytvořili aplikaci, která je schopná přehrávat MP3 soubory a zobrazovat jejich ID3 tagy. Dnes do ní pouze doplníme jedno tlačítko (Button) a do zdrojového kódu přidáme výše uvedené dvě procedury a upravíme obsluhu události OnCreate hlavního formuláře (pouze nastavíme titulek tlačítka). Po klepnutí na tlačítko Button2 dojde k zapsání aktuálního vzhledu ID3 tagu do souboru na disk.

Závěrem článku se pojďme podívat na kompletní zdrojový kód celé aplikace:
interface
uses type type TID3tag_typ = record var ID3zanr: array[0..CelkemZanru] of string = ( implementation
{$R *.dfm}
Procedure PrectiID3tag(z_jakeho_souboru: string; var ID3tag:
TID3tag_typ); if ID3.Zanr in [0..CelkemZanru] then
procedure TForm1.FormCreate(Sender: TObject); Label1.Caption := `Nazev`; Edit1.Text := ``; procedure TForm1.Button1Click(Sender: TObject); AssignFile(fMP3, mp3FileName);
procedure TForm1.Button2Click(Sender: TObject); StrLCopy(NovyTag.nazev, PChar(Edit1.Text), 31); X := 0; Inc(X); MediaPlayer1.Close; end.unit Unit1;
Windows, Messages, SysUtils, Variants, Classes, Graphics,
Controls, Forms,
Dialogs, StdCtrls, MPlayer;
TForm1 = class(TForm)
Edit1:
TEdit;
Edit2: TEdit;
Edit3:
TEdit;
Edit4: TEdit;
Edit5:
TEdit;
Edit6: TEdit;
MediaPlayer1:
TMediaPlayer;
OpenDialog1:
TOpenDialog;
Button1: TButton;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
Label6: TLabel;
Button2: TButton;
procedure FormCreate(Sender:
TObject);
procedure Button1Click(Sender:
TObject);
procedure Button2Click(Sender:
TObject);
private
{ Private declarations
}
public
{ Public declarations }
end;
hlavicka: array[0..2] of char;
nazev: array[0..29] of char;
interpret: array[0..29] of
char;
album: array[0..29] of char;
rok: array[0..3] of
char;
komentar: array[0..29] of char;
zanr: byte;
end;
const CelkemZanru = 147;
`Blues`, `Classic Rock`, `Country`, `Dance`, `Disco`, `Funk`,
`Grunge`,
`Hip-Hop`, `Jazz`, `Metal`, `New Age`, `Oldies`,
`Other`, `Pop`, `R&B`,
`Rap`, `Reggae`, `Rock`,
`Techno`, `Industrial`, `Alternative`, `Ska`,
`Death
Metal`, `Pranks`, `Soundtrack`, `Euro-Techno`, `Ambient`,
`Trip-Hop`, `Vocal`, `Jazz+Funk`, `Fusion`, `Trance`,
`Classical`,
`Instrumental`, `Acid`, `House`, `Game`,
`Sound Clip`, `Gospel`,
`Noise`, `AlternRock`, `Bass`,
`Soul`, `Punk`, `Space`, `Meditative`,
`Instrumental Pop`,
`Instrumental Rock`, `Ethnic`, `Gothic`,
`Darkwave`,
`Techno-Industrial`, `Electronic`, `Pop-Folk`,
`Eurodance`, `Dream`, `Southern Rock`, `Comedy`, `Cult`,
`Gangsta`,
`Top 40`, `Christian Rap`, `Pop/Funk`,
`Jungle`, `Native American`,
`Cabaret`, `New Wave`,
`Psychadelic`, `Rave`, `Showtunes`, `Trailer`,
`Lo-Fi`,
`Tribal`, `Acid Punk`, `Acid Jazz`, `Polka`, `Retro`,
`Musical`, `Rock & Roll`, `Hard Rock`, `Folk`,
`Folk-Rock`,
`National Folk`, `Swing`, `Fast Fusion`,
`Bebob`, `Latin`, `Revival`,
`Celtic`, `Bluegrass`,
`Avantgarde`, `Gothic Rock`, `Progressive Rock`,
`Psychedelic Rock`, `Symphonic Rock`, `Slow Rock`, `Big
Band`,
`Chorus`, `Easy Listening`, `Acoustic`, `Humour`,
`Speech`, `Chanson`,
`Opera`, `Chamber Music`, `Sonata`,
`Symphony`, `Booty Bass`, `Primus`,
`Porn Groove`,
`Satire`, `Slow Jam`, `Club`, `Tango`, `Samba`,
`Folklore`, `Ballad`, `Power Ballad`, `Rhythmic Soul`,
`Freestyle`,
`Duet`, `Punk Rock`, `Drum Solo`, `Acapella`,
`Euro-House`, `Dance Hall`,
`Goa`, `Drum & Bass`,
`Club-House`, `Hardcore`, `Terror`, `Indie`,
`BritPop`,
`Negerpunk`, `Polsk Punk`, `Beat`, `Christian Gangsta
Rap`,
`Heavy Metal`, `Black Metal`, `Crossover`,
`Contemporary Christian`,
`Christian Rock`, `Merengue`,
`Salsa`, `Trash Metal`, `Anime`, `Jpop`,
`Synthpop`
);
var
Form1: TForm1;
Var
fmp3: TFileStream;
begin
fmp3:=TFileStream.Create(z_jakeho_souboru, fmOpenRead);
try
fmp3.position:=fmp3.size-128;
fmp3.Read(ID3tag,SizeOf(ID3tag));
finally
fmp3.free;
end;
end;
Procedure ZobrazID3tag(ID3: TID3tag_typ);
begin
if ID3.Hlavicka
<> `TAG` then begin
Form1.Edit1.Text:=`Neplatna nebo
neexistujici ID3 informace`;
Form1.Edit2.Text:=`Neplatna nebo
neexistujici ID3 informace`;
Form1.Edit3.Text:=`Neplatna nebo
neexistujici ID3 informace`;
Form1.Edit4.Text:=`Neplatna nebo
neexistujici ID3 informace`;
Form1.Edit5.Text:=`Neplatna nebo
neexistujici ID3 informace`;
Form1.Edit6.Text:=`Neplatna nebo
neexistujici ID3 informace`;
end else begin
Form1.Edit1.Text:=ID3.nazev;
Form1.Edit2.Text:=ID3.interpret;
Form1.Edit3.Text:=ID3.album;
Form1.Edit4.Text:=ID3.rok;
Form1.Edit5.Text:=ID3.komentar;
Form1.Edit6.Text:=ID3Zanr[ID3.Zanr]
else
Form1.Edit6.Text:=IntToStr(ID3.Zanr);
end;
end;
begin
Self.Caption
:= `Prehravac MP3 :)`;
MediaPlayer1.VisibleButtons := [btPlay,
btPause, btStop];
Button1.Caption := `Otevri`;
Button2.Caption := `Uloz ID3`;
Button2.Enabled := False;
Label2.Caption :=
`Interpret`;
Label3.Caption := `Album`;
Label4.Caption :=
`Rok`;
Label5.Caption := `Komentar`;
Label6.Caption :=
`Zanr`;
Edit2.Text := ``;
Edit3.Text :=
``;
Edit4.Text := ``;
Edit5.Text := ``;
Edit6.Text
:= ``;
end;
var ID3tag:
TID3Tag_typ;
begin
if OpenDialog1.Execute then
begin
MediaPlayer1.Close;
MediaPlayer1.FileName := OpenDialog1.FileName;
PrectiID3tag(OpenDialog1.FileName, ID3tag);
ZobrazID3tag(ID3tag);
MediaPlayer1.Open;
Button2.Enabled := True;
end;
end;
procedure ChangeID3Tag(NovaID3: TID3tag_typ; mp3FileName:
string);
var
fMP3: file of Byte;
Stara :
TID3Tag_typ;
begin
try
Reset(fMP3);
try
Seek(fMP3, FileSize(fMP3) - 128);
BlockRead(fMP3, Stara, SizeOf(Stara));
if
Stara.Hlavicka = `TAG` then
{
Replace old tag }
Seek(fMP3,
FileSize(fMP3) - 128)
else
{ Append tag to file because
it doesn`t exist }
Seek(fMP3,
FileSize(fMP3));
BlockWrite(fMP3, NovaID3,
SizeOf(NovaID3));
finally
end;
finally
CloseFile(fMP3);
end;
end;
var NovyTag:
TID3tag_typ;
x: short;
begin
NovyTag.hlavicka
:= `TAG`;
StrLCopy(NovyTag.interpret, PChar(Edit2.Text), 30);
StrLCopy(NovyTag.album, PChar(Edit3.Text), 30);
StrLCopy(NovyTag.rok,
PChar(Edit4.Text), 4);
StrLCopy(NovyTag.komentar, PChar(Edit5.Text),
30);
NovyTag.zanr := CelkemZanru + 1;
while (X
<= CelkemZanru) do begin
if (Edit6.Text = ID3zanr[X])
then begin
NovyTag.zanr :=
X;
break;
end;
end;
ChangeID3Tag(NovyTag,
OpenDialog1.Filename);
MediaPlayer1.Open;
End;
Dnešní díl seriálu byl věnován modifikacím ID3 tagů. Ukázali jsme si, jakým způsobem změnit obsah ID3 tagu a jak změněnou empétrojku uložit na disk. V souvislosti s tím jsme krátce odbočili a vysvětlili si, jak pracovat s nekompatibilními typy řetězců. Příště se ještě krátce k ID3 tagům vrátíme – podíváme se podrobně na jejich novou, modernější verzi, na ID3v2 tagy.