Montag, 7. März 2011

Wave Dateien einlesen

In diesem Post möchte ich zeigen, wie man mit der Programmiersprache C# Wave Dateien einliest. Es geht hier allerdings nicht um das Abspielen etc., sondern um die genaue Analyse des Dateiformats, also wie eine Wave Datei aufgebaut ist und wie man sie byteweise einlesen kann (zum Abspielen geht's hier).
Eine Wave Datei ist eine Audiodatei, die somit Schallfrequenzen speichert.
Aufgebaut ist sie auf Datenebene aus mehreren Chunks (Blöcken). Mit diesen lässt sich einiges anstellen, ich werde mich in diesem Post allerdings nur auf die einfachste Form (aber zugleich auch der Standard) des Wave Formats beschränken (dieses wird auf dieser Seite erläutert), welches aus 3 Chunks besteht.
Auf der eben genannten Seite (und auf vielen anderen auch) wird das Wave Format sehr schön erklärt, ich fasse diese Infos hier nur kurz zusammen und lasse einiges weg.
Ein kleiner Tipp noch: Wave Dateien, mit exakt dem Aufbau wie sie hier beschrieben werden, können sehr gut mit dem kostenlosen Programm Audacity erzeugt werden (zum Beispiel aus MP3 - Dateien).

Die ersten 2 Einträge eines Chunks sind immer die gleichen: Mit jeweils 4 Byte werden Name des Chunks und eine Größenangabe codiert.
Der 1. Chunk trägt den Namen "RIFF". Die folgende Größenangabe (1) bezeichnet die Größe der gesamten Wavedatei in Bytes - 8, da der Name und die Größe nicht mitgezählt werden.
An 3. Position im 1. Block steht die Zeichenkette "WAVE" (2).
Nun folgt der 2. Chunk, mit Namen "fmt " (das Leerzeichen ist wichtig).
Die Größenangabe (3) speichert den Wert 16, sie bezeichnet die Größe dieses Blocks.
In den nächsten 2 Bytes wird das Audioformat (4) gespeichert: 1 bedeutet normale Speicherung, andere Werte zeigen eine Kompression an.
Mit den nächsten 2 Bytes werden die Anzahl der Audiospuren (5) kodiert.
Die nächsten 4 Bytes speichern die Abtastrate pro Sekunde (6), also wie viele Werte des Audiosignals pro Sekunde gespeichert werden.
In den nächsten 4 Bytes wird die Byterate (7) gespeichert, das heißt wie viele Bytes pro Sekunde zum Abspielen des Audiosignals abgerufen werden müssen.
Die nächsten 2 Bytes geben die Anzahl an Bytes an, die zur Beschreibung eines einzelnen Abtastwerts (unter Betrachtung aller Audiospuren) genutzt werden (8).
Die letzten 2 Bytes dieses Blocks geben schließlich die Anzahl an Bits (kleine Erinnerung: 1 Byte = 8 Bit) an, die zur Speicherung eines einzelnen Abtastwerts (bezüglich nur eines Kanals) genutzt werden (9).
Nun folgt der 3. Block, der Datenblock.
Am Anfang dieses befinden sich wieder mit je 4 Bytes der Name ("data") und die Größe (10) dieses.
Danach folgen die eigentlichen Daten (11) der Wave Datei, die ja im Prinzip Luftdrücke sind, also Audioamplituden.
Die Samples sind nacheinander abgespeichert, die verschiedenen Audiospuren stehen dabei direkt hintereinander. In dem "data" Block steht also zuerst Sample 1, wobei in diesem Abschnitt erst Audiokanal 1, dann ggf. Kanal 2 ... etc. steht, dann Sample 2 mit Kanal 1 als erstem und ggf. den anderen, usw.
Die entsprechende Anzahl Bytes pro Sample und Kanal ergeben als Integer interpretiert die Amplitude an der aktuellen Abtaststelle.

Damit wäre meine kleine Beschreibung des Wave Formats fertig, jetzt folgt der Code eines C# Programms, welches Wave Dateien einlesen kann, die nach obiger Beschreibung aufgebaut sind.
Kern des Programms ist die Klasse WaveFile, sie stellt eine Funktion zum Einlesen von Wave Dateien dar und speichert als Instanz die Informationen einer Datei.
Die oben beschriebenen Angaben zum Dateiinhalt (Anzahl der Kanäle etc.) sind im Quellcode mit den oben benutzten Nummern gekennzeichnet.
Oberfunktion zum Einlesen einer Wave Datei ist LoadWave(), der der Pfad zur Datei übergeben werden muss.
In dieser Funktion wird dann 3mal die Funktion LoadChunk() aufruft, welche einen Block ausliest.
Zuerst wird der Name des Blocks analysiert und dann das weitere Vorgehen angepasst.
Ich hoffe der Rest des Quellcodes ist aus den Kommentaren etc. verständlich.
Wie für alle Programme hier gilt: Das Programm ist nur eine erste Anregung zur weiteren Arbeit, eine Fehlerbehandlung o.ä. gibt es nicht.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            WaveFile WF1 = new WaveFile();
            WF1.LoadWave(@"C:\Users\User\Desktop\mix.wav");
        }
    }

    public class WaveFile  
    {
        int FileSize; // 1
        string Format; // 2
        int FmtChunkSize; // 3
        int AudioFormat; // 4
        int NumChannels; // 5
        int SampleRate; // 6
        int ByteRate; // 7
        int BlockAlign; // 8
        int BitsPerSample; // 9
        int DataSize; // 10

        int[][] Data; // 11

        public void LoadWave(string path)
        {
            System.IO.FileStream fs = System.IO.File.OpenRead(path); // zu lesende Wave Datei öffnen
            LoadChunk(fs); // RIFF Chunk einlesen
            LoadChunk(fs); // fmt Chunk einlesen
            LoadChunk(fs); // data Chunk einlesen
        }

        private void LoadChunk(System.IO.FileStream fs)
        {
            System.Text.ASCIIEncoding Encoder = new ASCIIEncoding();

            byte[] bChunkID = new byte[4];
            /* Die ersten 4 Bytes einlesen.
            Diese ergeben bei jedem Chunk den jeweiligen Namen. */
            fs.Read(bChunkID, 0, 4);
            string sChunkID = Encoder.GetString(bChunkID); // den Namen aus den Bytes dekodieren

            byte[] ChunkSize = new byte[4];
            /* Die nächsten 4 Bytes ergeben bei jedem Chunk die Größenangabe. */
            fs.Read(ChunkSize, 0, 4);

            if (sChunkID.Equals("RIFF"))
            {
                // beim Riff Chunk ...
                // die Größe in FileSize speichern
                FileSize = System.BitConverter.ToInt32(ChunkSize, 0);
                // das Format einlesen
                byte[] Format = new byte[4];
                fs.Read(Format, 0, 4);
                // ergibt "WAVE" als String
                this.Format = Encoder.GetString(Format);
            }

            if (sChunkID.Equals("fmt "))
            {
                // beim fmt Chunk die Größe in FmtChunkSize speichern
                FmtChunkSize = System.BitConverter.ToInt32(ChunkSize, 0);
                // sowie die anderen Informationen auslesen und speichern
                byte[] AudioFormat = new byte[2];
                fs.Read(AudioFormat, 0, 2);
                this.AudioFormat = System.BitConverter.ToInt16(AudioFormat, 0);
                byte[] NumChannels = new byte[2];
                fs.Read(NumChannels, 0, 2);
                this.NumChannels = System.BitConverter.ToInt16(NumChannels, 0);
                byte[] SampleRate = new byte[4];
                fs.Read(SampleRate, 0, 4);
                this.SampleRate = System.BitConverter.ToInt32(SampleRate, 0);
                byte[] ByteRate = new byte[4];
                fs.Read(ByteRate, 0, 4);
                this.ByteRate = System.BitConverter.ToInt32(ByteRate, 0);
                byte[] BlockAlign = new byte[2];
                fs.Read(BlockAlign, 0, 2);
                this.BlockAlign = System.BitConverter.ToInt16(BlockAlign, 0);
                byte[] BitsPerSample = new byte[2];
                fs.Read(BitsPerSample, 0, 2);
                this.BitsPerSample = System.BitConverter.ToInt16(BitsPerSample, 0);
            }

            if (sChunkID == "data")
            {
                // beim data Chunk die Größenangabe in DataSize speichern
                DataSize = System.BitConverter.ToInt32(ChunkSize, 0);

                // der 1. Index von Data spezifiziert den Audiokanal, der 2. das Sample
                Data = new int[this.NumChannels][];
                // Temporäres Array zum Einlesen der jeweiligen Bytes eines Kanals pro Sample
                byte[] temp = new byte[BlockAlign / NumChannels];
                // für jeden Kanal das Data Array auf die Anzahl der Samples dimensionalisieren
                for (int i = 0; i < this.NumChannels; i++)
                {
                    Data[i] = new int[this.DataSize / (NumChannels * BitsPerSample / 8)];
                }

                // nacheinander alle Samples durchgehen
                for (int i = 0; i < Data[0].Length; i++)
                {
                    // alle Audiokanäle pro Sample durchgehen
                    for (int j = 0; j < NumChannels; j++)
                    {
                        // die jeweils genutze Anzahl an Bytes pro Sample und Kanal einlesen
                        if (fs.Read(temp, 0, BlockAlign / NumChannels) > 0)
                        {   // je nachdem, wie viele Bytes für einen Wert genutzt werden,
                            // die Amplitude als Int16 oder Int32 interpretieren
                            if (BlockAlign / NumChannels == 2)
                                Data[j][i] = System.BitConverter.ToInt16(temp, 0);
                            else
                                Data[j][i] = System.BitConverter.ToInt32(temp, 0);
                        }
                        /* else
                         * andere Werte als 2 oder 4 werden nicht behandelt, hier bei Bedarf ergänzen!
                        */
                    }
                }
            }
        }
    }
}

Kommentare:

  1. Sehr interessanter Artikel, gewährt ein guten Einblick in den Aufbau einer Wav-Datei. Eine Frage hätte ich aber noch, wie liesst man den Titel, etc. aus? Gibt es auch sowas ähnliches wie die ID3-Tags bei MP3 Dateien?

    AntwortenLöschen
  2. Hallo, danke!
    In dem oben vorgestellten Format (RIFF, fmt , data) ist ja definitiv kein Platz für irgendwelche Meta Tags. Meines Wissens gibt es auch keine anderen Chunks welche hinzugefügt werden können und diese Informationen enthalten.
    Also leider nein denke ich.

    AntwortenLöschen
  3. Hallo,

    kannst du mir mal bitte erklären wie Interleaving im WAV-Format implementiert wird bzw. wie die Bytes angeordnet sind?

    AntwortenLöschen
  4. Hallo Sophia, tut mir leid ich verstehe deine Frage nicht ganz. Was meinst du in diesem Zusammenhang mit Interleaving? Und wo gibt's noch Unklarheiten wie die Bytes angeordnet sind?
    Wie im Artikel oben beschrieben sowie auf der verlinkten Seite, wird das Audiosignal in diskreten Zeitwerten abgetastet, die Daten dieser Samples werden dann nacheinander in den Data Block geschrieben, wobei die einzelnen Kanäle (also z.B. links, rechts ...) nebeneinander stehen.
    War's das?

    AntwortenLöschen