MeMP - Mein einfacher Mp3-Player

Kapitel 2. Informationen in Audiodateien

Bevor wir mit dem eigentlichen Player beginnen, erstmal etwas Grundlegendes. Wenn ich eine mp3-Datei (oder etwas ähnliches) in einem Player öffne, dann erwarte ich, dass mir der Player anzeigt, wer da gerade singt, und wie das Lied heißt. Auch die Länge des Liedes möchte ich angezeigt bekommen.

Mal ehrlich: Würdet ihr einen Player benutzen, der nichts als den Dateinamen anzeigt? Eben.

Mp3-Dateien

Eine Mp3-Datei besteht aus vielen einzelnen MPEG-Frames – in etwa so, wie ein Film aus vielen Einzelbildern besteht. Jeder dieser Frames besitzt einen 4 Byte großen Header, und in diesen 4 Bytes stecken unter anderem Informationen über Bitrate (z.B. 192 kbit/s), Channelmode (z.B. Stereo) und Samplerate (z.B. 44.1 kHz). Über diese Daten kann man dann die Dauer eines Stückes berechnen.

Informationen wie Titel und Interpret sind im Mp3-Standard eigentlich gar nicht vorgesehen. Hier haben sich aber die so genannten ID3-Tags etabliert und gelten als informeller Standard. In einer ersten Version wurde der ID3v1-Tag ans Ende der Datei geschrieben. Der ID3v1-Tag hat eine feste Größe, kann sehr leicht gefunden und ausgewertet werden, ist aber stark eingeschränkt.

Später wurde der ID3v2-Tag entwickelt, der am Anfang der Datei zu finden ist, eine variable Größe besitzt und nahezu beliebige Informationen aufnehmen kann.

Da Version 1 am Ende und Version 2 am Anfang einer Datei zu finden ist, können natürlich auch beide Versionen in einer Datei auftauchen. Und da die beiden unabhängig voneinander sind, müssen die enthaltenen Informationen auch nicht unbedingt zueinander passen.

ID3-Tags in anderen Formaten

ID3-Tags in anderen Formaten gibt es eigentlich nicht. Bei Ogg Vorbis findet man Vorbis Kommentare, in Wma-Dateien Wma-Tags und bei den Formaten von Apple findet man wieder was anderes. Das schöne an Standards ist ja, dass es so viele davon gibt. Das macht leider ein unkompliziertes Auslesen dieser Daten für alle Formate unmöglich.

Den Anwender interessiert es eigentlich nicht, ob er nun eine mp3-Datei oder eine wma-Datei abspielt. Er möchte einfach wissen, wie das Lied heißt, und wir sollten als Programmierer diesen Wunsch beachten. Und wir werden das so erledigen, dass wir uns einmal darüber Gedanken machen, und in der weiteren Entwicklung nicht mehr. Zu diesem Zweck entwerfen wir unsere erste Klasse, die ein wichtiges Basiselement unseres Players bilden wird.

Die Klasse TAudioFile

Unsere Audiodatei-Klasse enthält Properties für Interpret, Titel, Dauer und einige andere Daten, die wir hier der Übersichtlichkeit wegen weglassen. Die Bitrate, Samplerate oder auch das Album wäre sicherlich noch interessant, hat aber zur Klärung des Prinzips keinerlei Bedeutung.

TAudioFile = class
  private
    fInterpret: String;
    fTitel    : String;
    fPfad     : String;
    fDauer    : Integer;
    // ...
    procedure GetMp3Info;
    procedure GetWmaInfo;
    procedure SetUnknown;
    function GetPlaylistTitel: String;
  public
    property Interpret: String  read fInterpret  write fInterpret;
    property Titel    : String  read fTitel      write fTitel    ;
    property Pfad     : String  read fPfad       write fPfad     ;
    property Dauer    : Integer read fDauer      write fDauer    ;
    property PlaylistTitel: String read GetPlaylistTitel         ;
 
    procedure GetAudioInfo(filename: String);
end;

Die vorerst einzige öffentliche Methode GetAudioInfo ermittelt aus einer Datei diese Informationen – oder versucht es zumindest. Sie ruft dabei je nach Dateityp die passende private Methode auf:

procedure TAudioFile.GetAudioInfo(filename: String);
begin
  fPfad := filename;
  if (AnsiLowerCase(ExtractFileExt(filename)) = '.mp3') then
    GetMp3Info
  else
    if AnsiLowerCase(ExtractFileExt(filename)) = '.wma' then
      GetWMAInfo
    else
      SetUnknown;
end;

Die einzelnen Prozeduren für das Auslesen der Daten bei einem bestimmten Dateityp greifen schließlich auf die benutzten Units zurück und übertragen anschließend die enthaltenen Informationen auf unser Gerüst. Bei mp3-Dateien sieht das dann zum Beispiel so aus:

procedure TAudioFile.GetMp3Info;
var mpegInfo: TMpegInfo;
    ID3v2Tag: TID3V2Tag;
    ID3v1tag: TID3v1Tag;
    Stream: TFileStream;
begin
  // Daten mit MP3FileUtils auslesen
  mpeginfo:=TMpegInfo.Create;
  ID3v2Tag:=TID3V2Tag.Create;
  ID3v1tag:=TID3v1Tag.Create;
  stream := TFileStream.Create(fPfad, fmOpenRead or fmShareDenyWrite);
  id3v1tag.ReadFromStream(stream);
  stream.Seek(0, sobeginning);
  id3v2tag.ReadFromStream(stream);
  if Not id3v2Tag.exists then
    stream.Seek(0, sobeginning)
  else
    stream.Seek(id3v2tag.size, soFromBeginning);
  mpeginfo.LoadFromStream(Stream);
  stream.free;
 
  // Daten auf unser Geruest uebertragen
  if mpeginfo.FirstHeaderPosition >- 1 then
  begin
    if id3v2tag.artist <> '' then
      fInterpret := id3v2tag.artist
    else
      fInterpret := id3v1tag.artist;
    if id3v2tag.title <> '' then
      fTitel := id3v2tag.title
    else
      if id3v1tag.title <> '' then
        fTitel := id3v1tag.title
      else
        fTitel := ExtractFileName(fPfad);
    fDauer   := mpeginfo.dauer;
  end else
    SetUnknown;
  MpegInfo.Free;
  Id3v2Tag.Free;
  Id3v1Tag.Free;
end;

Der Vorteil an diesem Konstrukt ist recht einfach. Wir können die Ermittlung der Audio-Informationen zu einem späteren Zeitpunkt sehr einfach erweitern, indem wir z.B. eine weitere private Methode GetOggInfo schreiben, und die Methode GetAudioInfo um einen else-Zweig erweitern. Das Prinzip bei diesen Methoden ist immer dasselbe: Suche die Format-spezifischen Informationen aus der Datei zusammen und fülle damit sinnvoll die Felder, die den Anwender am Ende interessieren. Wenn eine Information nicht gefunden werden kann, wird ein Standardwert dafür genommen – z.B. der Dateiname als Titel.

Wir können die Klasse auch durch weitere Eigenschaften erweitern. Zum Beispiel einen String der Form „Interpret – Titel“, wie man ihn häufig in Playlisten vorfindet. Anstatt dies immer an Ort und Stelle zu erledigen, schreiben wir einmal einen entsprechenden Getter für diese Property, der auch noch ein paar Fehler ausbügeln kann.

function TAudioFile.GetPlaylistTitel: String;
begin
  if Trim(fInterpret) = '' then
    result := fTitel
  else
    result := fInterpret + ' - ' + fTitel;
end;