Umíme to s Delphi: 138. díl – modifikace ID3 tagů v empétrojkách

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 empétrojky

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:

  • pokud tag v souboru není, nastavíme zápisovou pozici na konec souboru,
  • pokud již nějaký tag v souboru je, využijeme toho, že víme, že je dlouhý 128 bajtů a nastavíme zápisovou pozici na 128. bajt od konce 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ů.

Procedura BlockRead

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.

Procedura BlockWrite

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é.

Naplnění struktury daty

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.

Práce s nekompatibilními řetězci

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

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.

Klepněte pro větší obrázek

Zdrojový kód

Závěrem článku se pojďme podívat na kompletní zdrojový kód celé aplikace:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, MPlayer;

type
  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;

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;


const CelkemZanru = 147;

var ID3zanr: array[0..CelkemZanru] of string = (
    `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;

implementation

{$R *.dfm}

Procedure PrectiID3tag(z_jakeho_souboru: string; var ID3tag: TID3tag_typ);
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;

   if ID3.Zanr in [0..CelkemZanru] then
     Form1.Edit6.Text:=ID3Zanr[ID3.Zanr]
   else
     Form1.Edit6.Text:=IntToStr(ID3.Zanr);
 end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Self.Caption := `Prehravac MP3 :)`;
  MediaPlayer1.VisibleButtons := [btPlay, btPause, btStop];
  Button1.Caption := `Otevri`;
  Button2.Caption := `Uloz ID3`;
  Button2.Enabled := False;

  Label1.Caption := `Nazev`;
  Label2.Caption := `Interpret`;
  Label3.Caption := `Album`;
  Label4.Caption := `Rok`;
  Label5.Caption := `Komentar`;
  Label6.Caption := `Zanr`;

  Edit1.Text := ``;
  Edit2.Text := ``;
  Edit3.Text := ``;
  Edit4.Text := ``;
  Edit5.Text := ``;
  Edit6.Text := ``;
end;

procedure TForm1.Button1Click(Sender: TObject);
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

    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;

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;

end.

Na závěr

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.

Diskuze (2) Další článek: ADSL od Tiscali jinak: Nulový limit, 100 MB za 10 korun

Témata článku: Software, Programování, Soul, Modifikace, Slow, Byte, DEL, Funky, Drum, Techno, Uvedený problém, Díl, Jazz, Zavolání, Bass, Metal, ID3, Celý disk, Rocky, Ukázková aplikace, Folk, Tag, Poslední problém, Bluegrass, Rock


Určitě si přečtěte



Aktuální číslo časopisu Computer

Test 9 bezdrátových reproduktorů

Jak ovládnout Instagram

Test levných 27" herních monitorů

Jak se zbavit nepotřebných věcí na internetu