![]() |
WAV-Dateien und ID3Tag?
Hallo,
weiß jemand wie man ID3Tags aus wav-Dateien auslesen bzw. sie ändern kann? Für MP3s hab ich entsprechende Units gefunden, aber das funzt leider nicht mit Wave-Dateien.... |
Re: WAV-Dateien und ID3Tag?
Ich wusste gar nicht das wav-Dateien auch ID3-Tags aufnehmen können.
|
Re: WAV-Dateien und ID3Tag?
Liste der Anhänge anzeigen (Anzahl: 1)
Ja, das wusste ich auch nicht, bis ich es selber gesehen habe.
Hier mal ein Beispiel als Anhang: (WMP zeigt die Tags manchmal an, QuickTime zeigt sie immer an, Winamp nicht getestet) |
Re: WAV-Dateien und ID3Tag?
In diesem File sind keine ID3Tags enthalten. Was dabei der eine oder andere Player manchmal anzeigt, sind wohl irgendwelche anderen Tags, die sich am Ende der Datei verstecken (im Hexeditor kann man das erkennen). Vielleicht hilft dir
![]() |
Re: WAV-Dateien und ID3Tag?
Ok, danke erstmal.
Dachte, das wären auch ID3-Tags. Nur hab ich jetzt keine Ahnung, wie ich diesen "INFO List Chunk" auslesen kann oder viel wichtiger, ihn zu ändern bzw. komplett zu entfernen... |
Re: WAV-Dateien und ID3Tag?
Zitat:
einfach mal nach der Spezifiktion googlen bzw. dem Link folgen, der genannt wurde. Da findest Du die einzelne Elemente. Ohne mich selbst detailiert mit der richtigen Spezifikation beschäftigt zu haben, Grob haben sie wohl den Aufbau, dass sie durch ein "INFO" eingeleitet werden. Dem folgen Einträge in der Form <ID><Länge><Wert>, wobei ID eben die Art des Eintrags identifiziert, Länge ist ein 32 Bit Wert (Cardinal) der angibt, wie lang der zugehörige Eintrag ist, Wert enthält dann den String/Wert. Die Strings werden dabei (musst Du schauen ob in der Länge enthalten oder nicht) mit #0#0 beendet, dem folgt dann der nächste String. An sich musst Du also nur schauen, ob es im unteren Teil der Wave-Datei eine INFO-Struktur gibt und Dir dann merken, wo der letzte Eintrag kommt, diesen Teil kannst Du löschen (z.B. durch einen Stream, aus dem Du alle Byte vor dieser Struktur und alle hinter der Struktur in einen neuen Stream kopierst). In diesem Fall endet die Datei sogar mit dieser Struktur, hier solltest Du aber lieber auf Nummer sicher gehen und Dich nicht darauf verlassen. Um die Wave-Datei nicht unnötig zu durchsuchen solltest Du vielleicht schauen, ob Du der Wave-Spezifikation entnehmen kannst, wo der normale Wave-Teil endet und eben optionale Informationen beginnen. Gruß Der Unwissende |
Re: WAV-Dateien und ID3Tag?
du kannst dir ja mal die ATL anschauen ->
![]() |
Re: WAV-Dateien und ID3Tag?
schau dir mal im wiki den entsprechenden beitrag zu RIFF WAVE an.
da ist alles haargenau erklärt. ![]() es gibt aber auch genug beispiele im netz. google mal nach "delphi twave" |
Re: WAV-Dateien und ID3Tag?
Liste der Anhänge anzeigen (Anzahl: 1)
Erstmal vielen Dank an alle, die hier noch geantwortet haben, dachte nicht, dass ich noch so viel Hilfe bekomme :thumb:
Auf Basis des Beitrags von Der_Unwissende hab ich mir mal was zurecht gebastelt, wäre nett wenn ihr mal drüber gucken könntet, vielleicht fällt euch noch was auf, was man anders/besser machen könnte, aber funktionieren tut es perfekt! Um nicht die ganze Datei Byte für Byte nach einem "INFO" zu durchsuchen, hab ich einfach mal beim Dateiende minus einer Konstanten angefangen zu suchen, die Konstante erhält die in etwa maximale Größe des INFO List Chunks:
Delphi-Quellcode:
Ach ja, eine ausführliche Erklärung zu den einzelnen Chunk ID's lässt sich
const
MAX_LISTCHUNK_SIZE = 100000; { INFO List Chunk von einer Datei einlesen: die ChunkID und die einsprechende Beschreibung dazu werden in übergebenen TStrings gespeichert } function ReadINFOListChunk(const FileName: string; var ChunkID, Description: TStrings): Boolean; var Stream: TStream; i: Integer; s: array[0..3] of Char; sl: array[0..255] of Char; begin s := #0#0#0#0; Result := False; Stream := TFileStream.Create(FileName, fmOpenRead, fmShareDenyWrite); try Stream.Seek(-MAX_LISTCHUNK_SIZE, soFromEnd); // bis zu 'INFO' einlesen while Stream.Read(s, 1) = 1 do if s[0] = 'I' then begin if Stream.Read(s, 3) = 3 then begin if (s = 'NFO') then Break else Stream.Seek(-3, soFromCurrent); end else Exit; end; if s <> 'NFO' then Exit; // INFO Chunk einlesen Result := True; while Stream.Read(s, 4) = 4 do begin ChunkID.Append(s); // Chunk ID if Stream.Read(i, 4) = 4 then begin if Stream.Read(sl, i) = i then Description.Append(sl) // Description else Exit; end else Exit; // Null-Bytes überspringen while (Stream.Read(s, 1) = 1) and (s[0] = #0) do begin end; Stream.Seek(-1, soFromCurrent); end; finally FreeAndNil(Stream); end; end; { INFO List Chunk in eine Datei schreiben: die ChunkID und die einsprechende Beschreibung werden in TStrings übergebenen } procedure WriteINFOListChunk(const FileName: string; const ChunkID, Description: TStrings); var Stream: TStream; i, j: Integer; begin // INFO List Chunk löschen, falls vorhanden RemoveINFOListChunk(FileName); Stream := TFileStream.Create(FileName, fmOpenReadWrite, fmShareDenyWrite); try Stream.Seek(0, soFromEnd); // Header schreiben Stream.Write('LIST:'+#0#0#0+'INFO', 12); for i := 0 to ChunkID.Count - 1 do begin Stream.Write(PChar(ChunkID[i])^, 4); // Chunk ID j := Length(Description[i]) + 1; Stream.Write(j, 4); // Länge Stream.Write(PChar(Description[i] + #0)^, j); // Description end; finally FreeAndNil(Stream); end; end; { INFO List Chunk von einer Datei löschen } function RemoveINFOListChunk(const FileName: string): Boolean; var Stream: TStream; s: array[0..3] of Char; begin s := #0#0#0#0; Result := False; Stream := TFileStream.Create(FileName, fmOpenReadWrite, fmShareDenyWrite); try Stream.Seek(-MAX_LISTCHUNK_SIZE, soFromEnd); // bis zu 'INFO' einlesen while Stream.Read(s, 1) = 1 do if s[0] = 'I' then begin if Stream.Read(s, 3) = 3 then begin if (s = 'NFO') then Break else Stream.Seek(-3, soFromCurrent); end else Exit; end; if s <> 'NFO' then Exit; Stream.Seek(-12, soFromCurrent); // List Chunk enthält nur INFO -> kompletten List Chunk löschen if (Stream.Read(s, 4) = 4) and (s = 'LIST') then Stream.Size := Stream.Position - 4 else Stream.Size := Stream.Position + 4; Result := True; finally FreeAndNil(Stream); end; end; ![]() Hier noch Beispiele zur Anwendung:
Delphi-Quellcode:
Im Anhang noch der Code als Unit:
procedure TForm1.ButtonReadClick(Sender: TObject);
var ChunkID, Description: TStrings; i: Integer; s: String; begin ChunkID := TStringList.Create; Description := TStringList.Create; try if ReadINFOListChunk('C:\test.wav', ChunkID, Description) then begin s := ''; for i := 0 to ChunkID.Count - 1 do s := s + ChunkID[i] +#9+ Description[i] +#13#10; ShowMessage('Datei "C:\test.wav" enthält folgenden INFO List Chunk:' +#13#10#13#10+ s); end else ShowMessage('Datei "C:\test.wav" enthält keinen INFO List Chunk.'); finally ChunkID.Free; Description.Free; end; end; procedure TForm1.ButtonWriteClick(Sender: TObject); var ChunkID, Description: TStrings; begin ChunkID := TStringList.Create; Description := TStringList.Create; try ChunkID.Add('IART'); Description.Add('Interpret'); ChunkID.Add('INAM'); Description.Add('Titel'); WriteINFOListChunk('C:\test.wav', ChunkID, Description); finally ChunkID.Free; Description.Free; end; end; procedure TForm1.ButtonRemoveClick(Sender: TObject); begin if RemoveINFOListChunk('C:\test.wav') then ShowMessage('INFO List Chunk wurde gelöscht.') else ShowMessage('Kein INFO List Chunk vorhanden.'); end; |
Re: WAV-Dateien und ID3Tag?
Hi,
auf den ersten Blick fällt mir auf, dass Du das Einlesen/Suchen nach dem Chunk nocht optimieren könntest. So kommt die Datei, die Du lesen möchtest ja von der Festplatte. So eine Festplatte ist etwas anders aufgebaut als der RAM, man hat nicht ganz so schön einen Wahlfreien Zugriff. Natürlich kannst Du ein beliebiges Datum lesen, aber dazu muss es erst auf der Platte gesucht werden. Insbesondere gehört dazu die Platzierung des Lesekopfes (besser gesagt der Leseköpfe) auf der richtigen Spur und dann muss die Platte auch noch warten, bis das richtige Datum unter dem Kopf ist. Einmal gefunden kann diese dann aber sehr sehr sehr schnell viele Daten lesen und als großen Strom weiterreichen. Es gibt aber an sich auch eine untere Grenze, die Blockgröße des Systems. Worauf ich eigentlich hinaus möchte, nach dem 'I' byteweise zu suchen ist total ineffizient! Zwar greifen hier schon einige Caching-Mechanismen des OS, es wird eben ohnehin nicht nur ein Byte ausgelesen, aber etwas besser geht es, wenn Du hier einen Lesepuffer verwendest. Wenn Du von 100.000 Byte max. Größe ausgehst, könntest Du diese eigentlich schon komplett in den Speicher laden (sind knappe 98 KByte, die hat man ja doch noch frei). Der Vorteil liegt einfach darin, dass der ganze Overhead (ggf. Zugriff auf den Hintergrundspeicher, auslesen aus dem Stream, Stream.Position aktualisieren, ...) nun auf die 98 KByte aufgeteilt wird. Der Unterschied ist überraschend deutlich und sollte selbst bei Deiner kurzen Suchen (in nur 100.000 max. Schritten) spürbar oder zumindestens messbar sein. An sich kannst Du aber auch die Suche nach dem 'Info' noch weiter vereinfachen. Das was Du aus dem Stream lädst sind erstmal nur rohe Bytes, die eben interpretiert werden. Ein String funktioniert auf die gleiche Art und Weise. Was Du also machen kannst ist einfach die Daten in einen Stream kopieren. Wichtig dabei, Du musst vorher die Länge des Strings setzen! Grob geht das wie folgt:
Delphi-Quellcode:
Ist jetzt ein einfaches Beispiel, bei dem Du ein beliebiges dyn. Array von Byte übergibst (z.B. aus einer Wave-Datei). In einer Schleife werden immer in 1024-Byte-Happen die Daten aus dem Puffer in einen String kopiert (optimieren kannst Du das natürlich, wenn Du sicherstellst, dass der Puffer ein vielfaches von 1024 ist!). Auf den String wird dann die Pos-Funktion angewendet, die Dir den Index des ersten Vorkommens des Substrings 'Info'. Das zusätzliche -1 bei der letzten Zuweisung kommt daher, dass ein String immer 1 indiziert ist, das erste Element in einem Stream/dyn. Array/... aber immer die 0 bekommt. Somit wäre die Position hier um 1 versetzt.
function getInfoChunkPos(const buffer: TByteDynArray): Integer;
var s: String; index: Integer; count: Integer; begin result := -1; index := 0; // über den ganzen Puffer laufen while (index < length(buffer)) and (result < 0) do begin // max. Länge durch 1024 oder noch nicht gelesene Daten im Puffer beschränken count := min(1024, length(buffer) - index); // Größe des String setzen setLength(s, count); // Daten aus dem Puffer in den String kopieren move(buffer[index], s[1], count); // neuen Startindex im Puffer setzen inc(index, count); // mit dem String arbeiten result := pos('Info', s) - 1; end; end; Ob 1024 hier eine gute Größe ist, darüber lässt sich natürlich streiten. Zudem solltest Du natürlich beachten, dass man den String in zwei Teilen aus dem Puffer holen kann, also ein String mit 'Inf' endet und das erste Zeichen des nächsten Strings dann eben mit 'o' beginnt. Um das zu vermeiden sollte man bessert inc(index, count - 4) verwenden, das sollte sicherstellen, dass das Ende des Vorgängerstrings noch mit berücksichtigt wird. Ansonsten sieht der Code doch ganz ordentlich aus, ein paar Kleinigkeiten gibt es natürlich dennoch (nichts Wildes!). Zum Beispiel kannst Du bei lokalen Variablen wie dem Stream auf das FreeAndNil verzichten. Wird eine Referenz nach ihrer Freigabe eh nicht mehr erreicht, reicht auch ein Free. Es schadet aber auch nichts zusätzlich die Referenz auf NIL zu setzen. Auch der Typ der Variable Stream kann direkt als TFileStream gesetzt werden. Stream verwendet man eigentlich immer dort, wo es einem völlig egal ist, was für ein Stream übergeben wird. macht auch hier nichts, Du könntest eben nur nicht auf Besonderheiten eines TFileStream (ohne Cast) zugreifen. Die letzte Kleinigkeit, die noch am ehesten berücksichtigt werden sollte ist die von Dir verwendete Sperre, wenn Du die Dateien veränderst. Beim Lesen ist das fmShareDenyWrite natürlich sinnvoll, da Du so beliebig vielen Prozessen das Lesen ermöglichst. Aber wenn Du selbst die Datei schreibst, dann sollte möglichst kein anderer Prozess versuchen die Datei während dessen zu lesen. Was man sonst zu sehen bekommt kann eben doch irgendwas völlig Inkonsistentes sein. Besser wäre es, wenn Du hier gleich exklusiv (Lesen und Schreiben) sperrst (fmShareExclusive). Gruß Der Unwissende |
Re: WAV-Dateien und ID3Tag?
Okay, ich werden das dann mal versuchen umzusetzen, aber eine Frage noch:
Wie meinst du das mit den 1024 Byte einlesen? Wenn ich die 100.000 Byte direkt von dem Stream in einem String lese, kann ich doch dann auch direkt in diesem String mit Pos() nach 'INFO' suchen, oder wird das "zu groß" für einen String? Ein anderes Problem was ich noch überhaupt nicht berücksichtigt habe: Ich gehe einfach davon aus, dass die Datei mit dem INFO Chunk endet und danach nichts mehr kommt. Was aber wenn danach noch ein anderer Chunk kommt, der nicht zu dem INFO-Teil gehört? Wie erkenne ich, ob ein anderer Chunk beginnt? Oder ist das immer so, das der INFO Chunk ganz am Ende der Datei steht (was bei meinen Testdateien immer so war) ? |
Re: WAV-Dateien und ID3Tag?
Zitat:
Zitat:
Gruß |
Re: WAV-Dateien und ID3Tag?
Ja unter
![]() |
Re: WAV-Dateien und ID3Tag?
Zitat:
Entsprechend kannst Du die Wave-Datei auf vorhandene INFO-Chunks prüfen, ungetestet ungefähr so:
Delphi-Quellcode:
So ganz grob sollte das klappen. Natürlich ist auch hier das lesen von 2x4 Zeichen nicht wirklich effizient, nur werden mehr Zeichen nicht betrachtet. Besser wäre es sicherlich trotzdem einen Puffer von 8 Byte zu verwenden und die Daten dann in die Variablen zu kopieren, aber das ist eine andere Sache. Hier greift (hoffentlich) die Compileroptimierung und passt das gleich sinnvoll an.
function hasInfoChunk(const file: TStream): Boolean;
var ChunkId: String; count: Cardinal; begin result := false; setLength(ChunkId, 4); // Prüfen ob Datei größer als Header (12 Byte) und erstes Chunk (mind. 8 Byte) if (file.size >= 20) then begin while (file.Position + 8 < file.Size) and (not result) do begin file.Position := 16; file.Read(ChunkId, 4); file.Read(count, 4); // prüfen ob das gelesene Chunk ein Info-Chunk ist if ChunkId = 'Info' then begin result := true; end else begin file.Position := file.Position + count; end; end; end; end; Jedenfalls kannst Du auf die gleiche Art auch den Offset zu dem entsprechenden Chunk zurückgeben lassen, das wäre eben File.Position - 8 (da Du ja mit ChunkId und Count schon weiter gelaufen bist) und könntest so auch gleich das ganze Chunk entfernen (alles bis dorthin und alles dahinter - count sagt Dir ja wie groß das Chunk ist - abschneiden/rauskopieren, Du weißt schon. |
Re: WAV-Dateien und ID3Tag?
Hmm, gut gut...
Du hast mir wirklich sehr viel geholfen. Werde mich heute Abend vielleicht nochmal näher damit beschäftigen. Danke erstmal! Leider hab ich nicht immer so die Zeit... Wenn es geht, würde ich viel mehr Zeit mit Programmieren und hier im Forum verbringen. |
Re: WAV-Dateien und ID3Tag?
So, hab jetzt erstmal das Lesen des INFO Chunks komplett überarbeitet.
Ich springe jetzt nicht mehr 100000 Bytes vom Dateiende rückwärts, sondern lese sauber ein, wie viel Bytes die anderen Chunks der WAV-Datei groß sind und überspringe sie. Des weiteren berücksichtige ich auch, dass der LIST Chunk nicht nur den INFO Chunk enthalten kann, sondern auch noch andere List Chunks. (der INFO Chunk wird mit einem INFO eingeleitet und ist dann zuende, wenn die Chunk ID nicht mehr mit einem 'I' beginnt, denn alle Chunk IDs des INFO Chunks beginnen mit einem 'I') Außerdem hab ich der Einfachheit halber mal die Funktionen ReadID und ReadSize ausgelagert. ReadID liest die ID (Name) eines Chunks ein (4-Byte String) ReadSize liest die Größe des folgenden Blocks ein (4-Byte Integer, folgt auf die ID)
Delphi-Quellcode:
Die Funktionen zum ändern und löschen werd ich dann morgen machen...
type
String4 = array[0..3] of Char; function ReadINFOListChunk(const FileName: string; var ChunkID, Description: TStrings): Boolean; var Stream: TFileStream; function ReadID: String4; begin if Stream.Read(Result, 4) <> 4 then raise Exception.Create('Invalid WAVE Format'); end; function ReadSize: Cardinal; begin if Stream.Read(Result, 4) <> 4 then raise Exception.Create('Invalid WAVE Format'); end; var EndPos: Cardinal; // gibt die Endposition des LIST Chunks an chunk: String4; // zum Speichern der Chunk ID desc: array[0..255] of Char; // zum Einlesen der Beschreibung begin Result := False; Stream := TFileStream.Create(FileName, fmOpenRead, fmShareDenyWrite); try // allgemeiner Header für WAV-Dateien if (ReadID <> 'RIFF') or (ReadSize <> Stream.Size - 8) or (ReadID <> 'WAVE') then raise Exception.Create('Invalid WAVE Format'); // zum LIST Chunk springen while (Stream.Position < Stream.Size) and (ReadID <> 'LIST') do Stream.Seek(ReadSize, soFromCurrent); if Stream.Position < Stream.Size then // enthält LIST Chunk begin EndPos := ReadSize + Stream.Position; // zum INFO Chunk springen while (Stream.Position < EndPos) and (ReadID <> 'INFO') do Stream.Seek(ReadSize, soFromCurrent); if Stream.Position < EndPos then // enthält INFO Chunk begin Result := True; chunk := ReadID; // INFO Chunks einlesen: ID muss mit 'I' beginnen while chunk[0] = 'I' do begin ChunkID.Append(chunk); // Chunk ID hinzufügen Stream.Read(desc, ReadSize); // Beschreibung auslesen Description.Append(desc); // Beschreibung hinzufügen // nächste Chunk ID einlesen, falls LIST Chunk noch nicht zuende if Stream.Position < EndPos then chunk := ReadID else Break; end; end; end; finally Stream.Free; end; end; // Nachtrag: Ich gehe mal davon aus, dass WAVE-Dateien nicht größer als 4 GB sind (also High(Cardinal)). Das sollte eigentlich auch nicht möglich sein, da nach dem RIFF im Header ein 4-Byte Integer folgt, der die gesamte Dateigröße angibt. |
Alle Zeitangaben in WEZ +1. Es ist jetzt 01:09 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz