Mittwoch, 23. März 2011

Spracherkennung Part 2: Command Mode

Wie angekündigt, ist dieser Post eine Fortsetzung des vorigen, es geht um Spracherkennung, insbesondere um den Command Mode.
Wie der Name schon andeutet, wird aufgenommene Sprache damit auf vordefinierte Befehle untersucht.
Man könnte natürlich auch über den im vorigen Post beschriebenen Dictation Mode einen Text aufnehmen und diesen auf bestimmte Begriffe hin untersuchen - der Command Mode bietet dafür allerdings ein paar Vorteile.
Denn einerseits ermöglicht er leicht eine dauerhafte Prüfung, ob Befehle gesagt wurden, und andererseits ist die Erkennung besser, da das Programm nur die Befehlswörter kennt und sich bei der Erkennung nur zwischen diesen entscheiden muss.
Nun direkt zum Code: Die Vorraussetzungen (das Einbinden der Ressource etc.) sind die gleichen wie im vorigen Post.
Zum Erkennen der Befehle benutzen wir jetzt allerdings eine Instanz der Klasse SpeechRecognizer.
Diese erwartet auch eine Grammatik, welche wir aber jetzt manuell mit Wörtern befüllen, die erkannt werden sollen.
Über die Eigenschaft Enabled kann die Spracherkennung aktiviert werden.
Beim Erkennen von Befehlen tritt das Ereignis SpeechRecognized auf, diesem weisen wir eine Funktion zu, welche das Ereignis behandeln soll.
Wird ein SpeechRecognizer initialisiert, öffnet sich unter Windows ein Fenster zur Spracherkennung - hier muss der Benutzer auf den großen Mikrofonknopf drücken, um die Erkennung zu starten.
Das Drücken des Knopfes hat nichts mit der Eigenschaft Enabled zu tun - beide Dinge müssen aktiviert sein.
Einen Weg, die Spracherkennung nur per Code zu starten und das Fenster zu verstecken habe ich nicht gefunden. Mich würde aber interessieren ob das möglich ist, falls jemand eine Idee hat, bitte kommentieren!
Der Code:

        private void StartListening()
        {
            SpeechRecognizer SR = new SpeechRecognizer();
            // die Befehle registrieren
            Choices Commands = new Choices();
            Commands.Add("Stop");
            Commands.Add("Los");
            GrammarBuilder GB = new GrammarBuilder(Commands); // die Befehle mit einem GrammerBuilder laden
            Grammar CommandGrammar = new Grammar(GB); // eine Grammatik über den GrammarBuilder erstellen
            SR.LoadGrammar(CommandGrammar); // die Grammatik laden
            SR.SpeechRecognized += CommandRecognized; // Funktion zur Behandlung des Ereignisses
            SR.Enabled = true;
        }

        private void CommandRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            string Command = e.Result.Text;
            // hier weiterer Verarbeitungscode ...
        }

Dienstag, 22. März 2011

Spracherkennung Part 1: Dictation Mode

Im vorigen Post ging es um die Sprachausgabe mit .Net, dieses Mal widmen wir uns der Umkehrung, nämlich der Spracherkennung.
Diese ist für Computer viel schwieriger als die Ausgabe, das .Net Framework bietet über die Klasse System.Speech.Recognition allerdings wieder fertige Funktionen an, die mit wenig Codeaufwand zur Spracherkennung eingesetzt werden können.
Generell gibt es 2 Modi, in welchen die Spracherkennung betrieben werden kann: In diesem Post geht es um den Dictation Mode, im nächsten um den Command Mode.
Der Dictation Mode eignet sich, wie der Name schon sagt, zum Diktieren von Texten.
Der aufgezeichnete Ton wird wie ein Diktat verstanden und das Programm versucht, die gesprochenen Wörter zu erkennen.
Wie bei der Sprachausgabe muss zuerst ein Verweis auf die Komponente System.Speech eingebunden werden.
Die benötigte Unterklasse heißt nun allerdings Recognition, und so benutzen wir am Anfang folgende using - Direktive:

using System.Speech.Recognition;

Zur Spracherkennung benutzen wir eine Instanz der Klasse SpeechRecognitionEngine.
Diese benötigt eine Grammatik, also eine Art Anweisungssammlung, wie die Sprache interpretiert werden soll.
Als Grammatik übergeben wir der SpeechRecognitionEngine eine Instanz der Klasse DictationGrammar, um anzuzeigen, dass wir den Diktiermodus benutzen wollen.
Das Erkennen von gesprochenen Wörtern geschieht nun mit Aufruf der Funktion Recognize(). Diese setzt die Spracherkennung in Bereitschaftsmodus und beginnt, wenn das Mikrofon Töne vernimmt.
Macht der Sprecher eine bestimmte Zeit Pause (einstellbar), wird die Spracherkennung beendet und das Programm versucht nun, den Ton als Wörter zu interpretieren (eine asynchrone Aufnahme ist auch möglich).
Zum Schluss wird das Diktierergebnis zurückgegeben.
Hier der Code:

            SpeechRecognitionEngine SRE = new SpeechRecognitionEngine();
            SRE.LoadGrammar(new DictationGrammar()); // Diktiergrammatik laden
            SRE.SetInputToDefaultAudioDevice(); // Aufnahmequelle auf Standard setzen

            RecognitionResult Result = SRE.Recognize(); // Ton aufzeichnen und erkennen
            string ResultString = "";
            // alle erkannten Wörter aus dem Ergebnis dem Ergebnisstring hinzufügen
            foreach (RecognizedWordUnit w in Result.Words)
            {
                ResultString += w.Text;
            }

Montag, 21. März 2011

Sprachausgabe mit C#

Die Überschrift hört sich wahrscheinlich ziemlich kompliziert an, allerdings ist in C# die Realisierung einer Sprachausgabe in nur 2 Zeilen möglich!
Denn die schon mit Windows mitgelieferte Sprachsynthese ist auch im .Net Framework nutzbar, die benötigte Komponente ist die Klasse System.Speech.Synthesis. Um sie nutzen zu können, muss sie zuerst eingebunden werden: Projekt - Verweis hinzufügen - (Reiter .Net) System.Speech.
Um den Code zu vereinfachen, binden wir den Namespace mittels using ein:

using System.Speech.Synthesis;

Zur Sprachausgabe benötigen wir eine Instanz der Klasse SpeechSynthesizer, welcher über die Funktion Speak() der auszugebende Text übergeben werden kann.
Natürlich gibt es aber noch viele weitere Eigenschaften, mit denen herumexperimentiert werden darf.
Ein paar in der Übersicht:
- Rate: Wertebereich von -10 bis 10, Geschwindigkeit der Textausgabe
- Volume: Wertebereich 0 - 100, Lautstärke
- Voice: Stimme
Die letzte Eigenschaft ist schreibtgeschützt, ihr kann direkt nichts zugewiesen werden, sie kann allerdings über Funktionen geändert werden.
Nützlich ist die Funktion SelectVoiceByHints(), über die "Hinweise" gegeben werden können, nach denen eine passende Stimme gesucht werden soll.
Solche Kriterien sind zum Beispiel Geschlecht, Alter und sogar Kulturkreis.
So sucht zum Beispiel folgende Anweisung nach einem Stimmtyp, welcher weiblich und erwachsen klingt und legt diese Stimme bei Erfolg als Ausgabestimme fest:

Speaker.SelectVoiceByHints(VoiceGender.Female, VoiceAge.Adult);

Allerdings wird auf vielen Windows Rechnern nur eine Stimme vorinstalliert sein (bei mir z.B. nur "Microsoft Anna").
Dann hat eine Stimmsuche keinerlei Auswirkung.
Die installierten Stimmen kann man über die Funktion Speaker.GetInstalledVoices() auslesen.
Die einzelnen Stimmen liegen als Objekt vom Typ Voice vor, folgendes Beispiel liest alle Stimmen aus und fügt ihren Namen zu der Liste InstalledVoices hinzu:

List<string> InstalledVoices = new List<string>();
foreach (InstalledVoice voice in Speaker.GetInstalledVoices())
{
    InstalledVoices.Add(voice.VoiceInfo.Name);
}

Freitag, 18. März 2011

Speicherauslastung eines Prozesses ermitteln

Der heute Post wird nach den etwas längeren vorhergenden Posts wieder etwas kürzer, ich möchte euch zeigen, wie man mit C# die Speicherauslastung eines Prozesses ermittelt.
Die dafür erforderliche Eigenschaft heißt WorkingSet64 aus der Klasse System.Diagnostics.Process. Sie gibt die Anzahl an Bytes an, die von dem jeweiligen Prozess im Arbeitsspeicher belegt sind - korreliert also in etwa mit der Spalte "Arbeitsspeicher" im Taskmanager.
Um die Speicherbenutzung eines bestimmten Prozesses zu ermitteln, muss dieser zuerst "abgegriffen" werden, zum Beispiel über die Funktion Process.GetProcessesByName() (ein paar Hintergrundinfos zu dieser gibt's in diesem Post).
Die Funktion sucht nach Prozessen mit dem übergebenen Namen und speichert alle diese im Ergebnisarray.
Der folgende Code ermittelt die Speicherauslastung des angegeben Prozesses, in dem er oben geannten Eigenschaft des ersten Eintrags im Array abfragt - falls mehrere Proezsse mit gleichem Namen vorhanden sind, müssen ggf. weitere Maßnahmen ergriffen werden.
Im Beispiel wird die Auslastung des ersten geöffneten Editorprozesses abgefragt:

Process[] Application;
Application = Process.GetProcessesByName("notepad");
long MemorySize = Application[0].WorkingSet64;

Montag, 14. März 2011

Wave Dateien zusammenmischen (überblenden)

In den vorigen beiden Posts wurde das Wave Format auseinander genommen, es wurde gezeigt, wie mit C# Wave Dateien eingelesen und geschrieben werden können.
In diesem Post möchte ich eine "coole" Anwendung zeigen, und zwar, wie man ein Programm erstellt, welches 2 Lieder ineinander übermischt.
Das Zusammenmischen entsteht hier durch Überblenden (Fading) der Lieder, das erste Lied wird gegen Ende hin immer leiser während das zweite Lied immer lauter wird.
Viele wünschen sich wahrscheinlich eine Anwendung, die das selbe mit MP3 - Dateien macht - allerdings ist das MP3 Format um Längen komplizierter als das Wave Format.
Es lassen sich aber beliebige MP3s mit dem in den vorigen Posts erwähnten Programm Audacity in Wave Dateien umwandeln und dann bearbeiten.
Der in diesem Post verwendete Code baut immer noch auf die Klasse WaveFile auf, die in den beiden vorigen Posts angefangen wurde.
Ein Beispielaufruf der benötigten Zeilen zum Mischen 2er Wave Dateien könnte so aussehen:

            WaveFile WF1 = new WaveFile();
            WF1.LoadWave(@"C:\Users\User\Desktop\101-die_atzen_-_disco_pogo-ysp.wav");

            WaveFile WF2 = new WaveFile();
            WF2.LoadWave(@"C:\Users\User\Desktop\StroboPopDieAtzenFeatNena.wav");

            WaveFile.StoreMixWave(@"C:\Users\User\Desktop\mixed.wav", WF1, WF2, 10);

Es werden also zuerst 2 Wave Dateien eingelesen und schließlich wird die statische Funktion StoreMixWave() aufgerufen, welche als Parameter den Pfad zur Ergebnisdatei, die zu mischenden WaveFiles sowie die Überblendezeit erwartet.
Der Funktionscode sieht so aus:

public static void StoreMixWave(string path, WaveFile wf1, WaveFile wf2, int fadeTime)
{
    WaveFile Mixed = MixWave(wf1, wf2, fadeTime); // Ergebnisdatei mischen
    Mixed.StoreWave(path); // Ergebnisdatei auf Festplatte speichern
}

Der Ergebniswavefile wird also in der Funktion MixWave() zusammengemischt, und dieser WaveFile dann mit der bekannten Funktion StoreWave() geschrieben.
Nun betrachten wir direkt einmal den Code der Funktion MixWave():

        private static WaveFile MixWave(WaveFile wf1, WaveFile wf2, int fadeTime)
        {
            int FadeSamples = fadeTime * wf1.ByteRate / wf1.NumChannels; // Anzahl an aus-/ einzublenden Samples
            int FadeBytes = fadeTime * wf1.ByteRate; // Anzahl an aus-/ einzublendenden Bytes

            WaveFile Result = new WaveFile(); // Ergebnis Wave Datei
            Result.FileSize = wf1.FileSize + wf2.DataSize - 2 * FadeBytes; // neue Dateigröße
            Result.Format = "WAVE";

            // Informationen aus dem fmt Chunk übernehmen
            Result.FmtChunkSize = wf1.FmtChunkSize;
            Result.AudioFormat = wf1.AudioFormat;
            Result.NumChannels = wf1.NumChannels;
            Result.SampleRate = wf1.SampleRate;
            Result.ByteRate = wf1.ByteRate;
            Result.BlockAlign = wf1.BlockAlign;
            Result.BitsPerSample = wf1.BitsPerSample;

            Result.DataSize = wf1.DataSize + wf2.DataSize - 2 * FadeBytes; // neue Größe des Data Chunks
            Result.Data = new int[wf1.NumChannels][]; // Anzahl an Kanälen übernehmen
            int NumSamples = Result.DataSize / (Result.NumChannels * ( Result.BitsPerSample / 8)); // Anzahl an Samples ausrechnen, die sich in de Ergebnisdatei ergeben

            // Die Data Arrays für alle Kanäle auf die Anzahl der Samples dimensionieren.
            for (int i = 0; i < Result.Data.Length; i++)
            {
                Result.Data[i] = new int[NumSamples];
            }

            int PosCounter = 0; // Position des aktuellen Samples in der Ergebnisdatei

            // die Samples aus der ersten Wave Datei in das Data Feld der Ergebnisdatei kopieren
            for (int i = 0; i < wf1.Data[0].Length; i++)
            {
                // das aktuelle Sample in allen Kanälen übernehmen
                for (int j = 0; j < wf1.NumChannels; j++)
                {
                    // fällt das aktuelle Sample in die Zeit, die überblendet werden soll, den Amplitudenwert der 1. Datei mit dem Amplitudenwert aus der 2. Datei mischen
                    if (i > wf1.Data[0].Length - FadeSamples)
                       Result.Data[j][PosCounter] = (int)(wf1.Data[j][i] * Factor(i - (wf1.Data[0].Length - FadeSamples), FadeSamples, 0) + wf2.Data[j][i - (wf1.Data[0].Length - FadeSamples)] * Factor(i - (wf1.Data[0].Length - FadeSamples), FadeSamples, 1));
                    else
                       Result.Data[j][PosCounter] = wf1.Data[j][i];
                }
                PosCounter++;
            }

            // die restlichen Samples in die Ergebnisdatei übernehmen
            for (int i = FadeSamples; i < wf2.Data[0].Length; i++)
            {
                for (int j = 0; j < wf1.NumChannels; j++)
                {
                    Result.Data[j][PosCounter] = wf2.Data[j][i];
                }
                PosCounter++;
            }
            return Result;
        }

Am Anfang dieser wird die Anzahl der Samples berechnet, die überblendet werden sollen.
Zur Erinnerung: Eine Wave Datei ist im Groben eine Sammlung von Luftdruckamplituden. Mit einer bestimmten Abtastrate werden in bestimmten Zeitabständen diese Amplituden gemessen und in der Datei gespeichert. Diese einzelnen Werte sind die Samples.
Um 2 Audiodateien übereinander zu legen, kann man z.B. einfach die Amplituden addieren.
Die Anzahl an zu überblenden Samples berechnet sich demnach als Produkt der Überblendzeit in Sekunden und Byterate (Anzahl der Bytes pro Sekunde, die beim Abspielen bearbeitet werden), dividiert durch die Anzahl der Kanäle (da die Byterate die Anzahl an Bytes von allen Kanälen angibt).
Dann werden die Dateien zusammengemischt:
Die ersten beiden Blöcke ("RIFF" und "fmt ") werden einfach aus der ersten übergebenen Wave Datei übernommen - allerdings wird natürlich die Dateigröße angepasst.
Die neue Dateigröße ergibt sich als Summe der alten Dateigrößen, abzüglich 2mal der Anzahl an Bytes, die überblendet werden.
Damit das Ergebnis auch nach etwas klingt, ist hier also Vorraussetzung, dass beide Wave Dateien einen ähnlichen fmt Block haben, sprich die selbe Abtastrate, Anzahl an Kanälen etc. Das ist aber eh der Fall, wenn man die Wave Dateien mit Audacity erstellt.
Das Mischen der Data Chunks gestaltet sich nun als etwas komplizierter.
Zuerst wird die Größe des resultierende Data Blocks berechnet und daraus dann die Anzahl an Samples, die in jedem Kanal gespeichert werden, um das Data Array für alle Kanäle auf diesen Wert zu dimensionieren.
Diese Anzahl berechnet sich laut Spezifikation des Wave Formats laut der Formel (Größe des Data Blocks) / (Anzahl Kanäle * BitsPerSample / 8).
Die Variable PosCounter zählt im folgenden die Position im Data Array der Ergebnisdatei.
Mit 2 Schleifen werden nun alle Samples der ersten Wave Datei durchgegangen und hierbei in jeder Iteration alle Kanäle dieser.
Die entsprechenden Daten aus dem Data Array werden an die Position PosCounter im Data Array der Ergebnisdatei geschrieben.
Hat die Schleife eine Position erreicht, die näher als die Anzahl der zu überblenden Bytes am Ende der ersten Datei liegt, werden nicht einfach die Daten der 1. Datei kopiert, sondern nun mit denen der 2. Datei gemischt.
Damit das 1. Lied langsam ausgeblendet während das 2. langsam eingeblendet wird, bestimmt die Funktion Factor() den Anteil der jeweiligen Lieder an der Gesamtamplitude.
Anschließend werden die noch nicht betrachteten Samples aus der 2. Datei in die Ergebnisdatei übernommen - fertig ist die Mischung!
Schlussendlich noch eine Übersicht über den Code der kompletten Klasse WaveFile, welchen alle 3 Posts zu diesem Thema benutzen:

    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

        #region Einlesen
        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!
                        */
                    }
                }
            }
        }
        #endregion

        #region Schreiben
        public void StoreWave(string path)
        {
            System.IO.FileStream fs = System.IO.File.OpenWrite(path); // zu schreiben Wave Datei öffnen / erstellen
            StoreChunk(fs, "RIFF"); // RIFF Chunk schreiben
            StoreChunk(fs, "fmt "); // fmt Chunk schreiben
            StoreChunk(fs, "data"); // data Chunk schreiben
        }

        private void StoreChunk(System.IO.FileStream fs, string chunkID)
        {
            System.Text.ASCIIEncoding Decoder = new ASCIIEncoding();
            // den Namen in Bytes konvertieren und schreiben
            fs.Write(Decoder.GetBytes(chunkID), 0, 4);

            if (chunkID == "RIFF")
            {
                // im RIFF Chunk, FileSize als Größe und das Audioformat schreiben
                fs.Write(System.BitConverter.GetBytes(FileSize), 0, 4);
                fs.Write(Decoder.GetBytes(Format), 0, 4);
            }
            if (chunkID == "fmt ")
            {
                // beim fmt Chunk die Größe dieses sowie die weiteren kodierten Informationen schreiben
                fs.Write(System.BitConverter.GetBytes(FmtChunkSize), 0, 4);
                fs.Write(System.BitConverter.GetBytes(AudioFormat), 0, 2);
                fs.Write(System.BitConverter.GetBytes(NumChannels), 0, 2);
                fs.Write(System.BitConverter.GetBytes(SampleRate), 0, 4);
                fs.Write(System.BitConverter.GetBytes(ByteRate), 0, 4);
                fs.Write(System.BitConverter.GetBytes(BlockAlign), 0, 2);
                fs.Write(System.BitConverter.GetBytes(BitsPerSample), 0, 2);
            }
            if (chunkID == "data")
            {
                // beim data Chunk die Größe des Datenblocks als Größenangabe schreiben
                fs.Write(System.BitConverter.GetBytes(DataSize), 0, 4);
                // dann die einzelnen Amplituden, wie beschrieben Sample für Sample mit jeweils allen
                // Audiospuren, schreiben
                for (int i = 0; i < Data[0].Length; i++)
                {
                    for (int j = 0; j < NumChannels; j++)
                    {
                        fs.Write(System.BitConverter.GetBytes(Data[j][i]), 0, BlockAlign / NumChannels);
                    }
                }
            }
        }
        #endregion

        #region Mischen
        private static WaveFile MixWave(WaveFile wf1, WaveFile wf2, int fadeTime)
        {
            int FadeSamples = fadeTime * wf1.ByteRate / wf1.NumChannels; // Anzahl an aus-/ einzublenden Samples
            int FadeBytes = fadeTime * wf1.ByteRate; // Anzahl an aus-/ einzublendenden Bytes

            WaveFile Result = new WaveFile(); // Ergebnis Wave Datei
            Result.FileSize = wf1.FileSize + wf2.DataSize - 2 * FadeBytes; // neue Dateigröße
            Result.Format = "WAVE";

            // Informationen aus dem fmt Chunk übernehmen
            Result.FmtChunkSize = wf1.FmtChunkSize;
            Result.AudioFormat = wf1.AudioFormat;
            Result.NumChannels = wf1.NumChannels;
            Result.SampleRate = wf1.SampleRate;
            Result.ByteRate = wf1.ByteRate;
            Result.BlockAlign = wf1.BlockAlign;
            Result.BitsPerSample = wf1.BitsPerSample;

            Result.DataSize = wf1.DataSize + wf2.DataSize - 2 * FadeBytes; // neue Größe des Data Chunks
            Result.Data = new int[wf1.NumChannels][]; // Anzahl an Kanälen übernehmen
            int NumSamples = Result.DataSize / (Result.NumChannels * ( Result.BitsPerSample / 8)); // Anzahl an Samples ausrechnen, die sich in de Ergebnisdatei ergeben

            // Die Data Arrays für alle Kanäle auf die Anzahl der Samples dimensionieren.
            for (int i = 0; i < Result.Data.Length; i++)
            {
                Result.Data[i] = new int[NumSamples];
            }

            int PosCounter = 0; // Position des aktuellen Samples in der Ergebnisdatei

            // die Samples aus der ersten Wave Datei in das Data Feld der Ergebnisdatei kopieren
            for (int i = 0; i < wf1.Data[0].Length; i++)
            {
                // das aktuelle Sample in allen Kanälen übernehmen
                for (int j = 0; j < wf1.NumChannels; j++)
                {
                    // fällt das aktuelle Sample in die Zeit, die überblendet werden soll, den Amplitudenwert der 1. Datei mit dem Amplitudenwert aus der 2. Datei mischen
                    if (i > wf1.Data[0].Length - FadeSamples)
                       Result.Data[j][PosCounter] = (int)(wf1.Data[j][i] * Factor(i - (wf1.Data[0].Length - FadeSamples), FadeSamples, 0) + wf2.Data[j][i - (wf1.Data[0].Length - FadeSamples)] * Factor(i - (wf1.Data[0].Length - FadeSamples), FadeSamples, 1));
                    else
                       Result.Data[j][PosCounter] = wf1.Data[j][i];
                }
                PosCounter++;
            }

            // die restlichen Samples in die Ergebnisdatei übernehmen
            for (int i = FadeSamples; i < wf2.Data[0].Length; i++)
            {
                for (int j = 0; j < wf1.NumChannels; j++)
                {
                    Result.Data[j][PosCounter] = wf2.Data[j][i];
                }
                PosCounter++;
            }
            return Result;
        }
        
        /// <summary>
       /// Diese Funktion dient zur Berechnung der Gewichtung der Amplituden bei Übermischung.
        /// </summary>
        /// <param name="pos">Position in Datei relativ zum Anfang der Überblendezeit</param>
        /// <param name="max">Ende der Überblendung, relativ zu pos</param>
        /// <param name="song">Kann die Werte 0 (auszublendender Song) oder 1 (einzublender Song) annehmen</param>
        /// <returns></returns>
        private static double Factor(int pos, int max, int song)
        {
            if (song == 0)
                return 1 - Math.Pow((double)pos / (double)max, 2);
            else
                return Math.Pow((double)pos / (double)max, 2);
        }

        public static void StoreMixWave(string path, WaveFile wf1, WaveFile wf2, int fadeTime)
        {
            WaveFile Mixed = MixWave(wf1, wf2, fadeTime); // Ergebnisdatei mischen
            Mixed.StoreWave(path); // Ergebnisdatei auf Festplatte speichern
        }

        #endregion
    }

Donnerstag, 10. März 2011

Wave Dateien schreiben

Im vorigen Post wurde gezeigt, wie man Wave Dateien byteweise einlesen kann. Jetzt möchte ich zeigen, wie man die Audiodateien erstellt.
Hierbei beschränke ich mich nur auf die Umkehrung des vorher beschriebenen Lesens, ich zeige in diesem Post also wie man eine vorher eingelesene Wave Datei wieder als Datei auf die Festplatte schreibt.
Im vorigen Post wurde der Aufbau von Wave Dateien und allerlei anderes Grundlegendes erläutert, deshalb poste ich hier nur kurz den Quellcode von 2 Funktionen, um die die Klasse WaveFile erweitert werden muss, damit Wave Dateien auch geschrieben werden können.
Das Prinzip ist analog zur Implementierung des Lesens, nacheinander werden die 3 Blöcke "RIFF", "fmt " und "data" in eine Datei geschrieben.

        public void StoreWave(string path)
        {
            System.IO.FileStream fs = System.IO.File.OpenWrite(path); // zu schreiben Wave Datei öffnen / erstellen
            StoreChunk(fs, "RIFF"); // RIFF Chunk schreiben
            StoreChunk(fs, "fmt "); // fmt Chunk schreiben
            StoreChunk(fs, "data"); // data Chunk schreiben
        }

        private void StoreChunk(System.IO.FileStream fs, string chunkID)
        {
            System.Text.ASCIIEncoding Decoder = new ASCIIEncoding();
            // den Namen in Bytes konvertieren und schreiben
            fs.Write(Decoder.GetBytes(chunkID), 0, 4);

            if (chunkID == "RIFF")
            {
                // im RIFF Chunk, FileSize als Größe und das Audioformat schreiben
                fs.Write(System.BitConverter.GetBytes(FileSize), 0, 4);
                fs.Write(Decoder.GetBytes(Format), 0, 4);
            }
            if (chunkID == "fmt ")
            {
                // beim fmt Chunk die Größe dieses sowie die weiteren kodierten Informationen schreiben
                fs.Write(System.BitConverter.GetBytes(FmtChunkSize), 0, 4);
                fs.Write(System.BitConverter.GetBytes(AudioFormat), 0, 2);
                fs.Write(System.BitConverter.GetBytes(NumChannels), 0, 2);
                fs.Write(System.BitConverter.GetBytes(SampleRate), 0, 4);
                fs.Write(System.BitConverter.GetBytes(ByteRate), 0, 4);
                fs.Write(System.BitConverter.GetBytes(BlockAlign), 0, 2);
                fs.Write(System.BitConverter.GetBytes(BitsPerSample), 0, 2);
            }
            if (chunkID == "data")
            {
                // beim data Chunk die Größe des Datenblocks als Größenangabe schreiben
                fs.Write(System.BitConverter.GetBytes(DataSize), 0, 4);
                // dann die einzelnen Amplituden, wie beschrieben Sample für Sample mit jeweils allen
                // Audiospuren, schreiben
                for (int i = 0; i < Data[0].Length; i++)
                {
                    for (int j = 0; j < NumChannels; j++)
                    {
                        fs.Write(System.BitConverter.GetBytes(Data[j][i]), 0, BlockAlign / NumChannels);
                    }
                }
            }
        }

Eine beispielhafte Implementierung, welche eine Wave Datei einliest und diese schließlich wieder als Datei auf die Festplatte schreibt, könnte so aussehen:

WaveFile WF1 = new WaveFile();
WF1.LoadWave(@"C:\Users\User\Desktop\mix.wav");
WF1.StoreWave(@"C:\Users\User\Desktop\stored.wav");

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!
                        */
                    }
                }
            }
        }
    }
}