Soundplayer verschluckt den Anfang eines Sounds



  • Hallo,

    ich habe ein kleines Problem mit diesem Code, der einen kurzen Sinuston erzeugen und sofort abspielen soll. Wenn die Länge des Tons kleiner als ca. 45ms ist, dann hört man nichts. Die gleichzeitig erzeugte *.wav Datei ist aber in Ordnung, wenn ich sie mit FFplay abspiele. Ich habe jetzt als Workaround vor dem eigentlichen Sinuston noch 100ms Stille eingefügt. Aber erstaunlicherweise funktioniert das auch nicht. Es ist immer noch so, dass die ersten 45ms vom Sinuston nicht hörbar sind. Woran mag das liegen?

    Gruß
    Michael

    using System;
    using System.Windows.Forms;
    using System.IO;
    
    namespace test
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
            }
    
            private void button1_Click(object sender, EventArgs e)
            {
                MemoryStream mStrm = new MemoryStream();
                BinaryWriter writer = new BinaryWriter(mStrm);
    
                int samplesPerSecond = 44100;
                double silenceSeconds = 0.1;
                double sineSeconds = 0.05;           
                int samples = (int)(samplesPerSecond * (silenceSeconds + sineSeconds));
    
                int formatChunkSize = 16;
                int headerSize = 8;
                short formatType = 1;
                short tracks = 1;
                short bitsPerSample = 16;
                short frameSize = (short)(tracks * ((bitsPerSample + 7) / 8));
                int bytesPerSecond = samplesPerSecond * frameSize;
                int waveSize = 4;
                int dataChunkSize = samples * frameSize;
                int fileSize = waveSize + headerSize + formatChunkSize + headerSize + dataChunkSize;
                writer.Write(0x46464952); // = encoding.GetBytes("RIFF")
                writer.Write(fileSize);
                writer.Write(0x45564157); // = encoding.GetBytes("WAVE")
                writer.Write(0x20746D66); // = encoding.GetBytes("fmt ")
                writer.Write(formatChunkSize);
                writer.Write(formatType);
                writer.Write(tracks);
                writer.Write(samplesPerSecond);
                writer.Write(bytesPerSecond);
                writer.Write(frameSize);
                writer.Write(bitsPerSample);
                writer.Write(0x61746164); // = encoding.GetBytes("data")
                writer.Write(dataChunkSize);
    
                for (int i = 0; i < samples; i++)
                {
                    double t = (double)i / samplesPerSecond;
                    if (t < silenceSeconds)
                        writer.Write((short)(0));   // Silence
                    else
                        writer.Write((short)(32000 * Math.Sin(2 * 3.1415 * 1000 * t)));   // 1kHz
                }
    
                mStrm.Seek(0, SeekOrigin.Begin);
                new System.Media.SoundPlayer(mStrm).Play();
    
                FileStream fs = new FileStream("out.wav", FileMode.Create);
                mStrm.WriteTo(fs);
                fs.Close();
                
                writer.Close();
                mStrm.Close();
            }
        }
    }
    


  • Ich bin mir ziemlich sicher dass das Problem nicht an meinem Code liegt, sondern am Treiber für die Audio-Ausgabe, oder an der Hardware. Aber ich finde in der Systemsteuerung keine Einstellmöglichkeiten, die irgendwas an dem Problem ändern würden. Möglicherweise wird die Betriebsspannung für die im Notebook eingebauten Lautsprecher über einen Spannungswandler erzeugt, der eine gewisse Zeit zum hochfahren braucht.
    Ich wollte die Tonerzeugung in einem Synthesizer simulieren, und da ist es blöd wenn jetzt am Anfang von jedem Ton 45ms fehlen.



  • Das Problem ist gelöst. Es ist ein Hardware-Problem. Wenn ich die im Notebook eingebauten Lautsprecher ausschalte, indem ich einen Klinkenstecker in den Audio-Ausgang reinstecke, dann sieht das Signal auf dem Oszilloskop genau so aus wie es aussehen soll. Zwar fehlt jetzt manchmal ein Stück am Ende des Sounds, aber das lässt sich einfach korrigieren indem ich noch 10ms Stille hinzufüge.



  • Füge mal vor dem Abspielen noch ein writer.Flush() ein, um die evtl. gepufferten Daten noch in den Stream zu schreiben.



  • @Th69 Habe es eingefügt, aber es macht keinen Unterschied. Nach neusten Erkenntnissen ist es doch nicht so dass der Verstärker zu spät eingeschaltet wird, sondern er wird am Ende des Sounds zu früh ausgeschaltet. Als Workaround genüget es am Ende des Sounds 50ms Stille anzufügen (wenn die Lautsprecher verwendet werden), oder 10ms wenn die Lautsprecher deaktiviert sind und nur der Audio-Ausgang verwendet wird.



  • Ich sehe gerade, daß du ja asynchron die Datei abspielst und es dadurch sein kann, daß der Stream wieder von dir geschlossen wird (bevor die Datei komplett abgespielt wurde).
    Teste mal mit PlaySync bzw. erzeuge mal einen längeren Sound (ob der dann noch vorzeitiger abgebrochen wird?). Alternativ dann Load vor dem Abspielen (Play) verwenden.



  • Mit PlaySync ist kein Unterschied feststellbar. Auch bei längeren Sounds (5 Sekunden) wird nicht mehr abgeschnitten. Unabhängig von der Länge fehlen am Ende ca. 40ms wenn es über die Lautsprecher kommt, oder 10ms wenn ich den Audio-Ausgang verwende. Wenn ich am Ende 50ms Stille anfüge dann liege ich auf der sichern Seite und man hört dann auch keinen Knack am Ende.



  • Wie du den Stream verwendest halte ich für fragwürdig, also in der "play async" Variante. Denn dort wird vermutlich der Stream asynchron im Hintergrund gelesen während du im Main-Thread den selben Stream in eine Datei schreibst (was vermutlich den Stream-Pointer verändert?).

    Davon abgesehen: Anfang/Ende abgeschnitten ist ein bekanntes Problem mit Audio-Treibern. Typisch ist eher abgeschnittener Anfang, aber ich kann mir gut vorstellen dass das von der OS-Version bzw. vom Audio-Treiber abhängig ist. Ganz krass ist das z.B. wenn du Audio über HDMI ausgibst und einen Surround-Receiver mit Ausgangsrelais angeschlossen hast, da fehlt dann oft eine ganze Sekunde am Anfang.

    Ein möglicher Fix dafür ist es das Audio-Device durchgehend einen Stream mit Null-Samples spielen zu lassen. Ein fertiges Programm dafür ist z.B. "SoundKeeper": https://veg.by/en/projects/soundkeeper/



  • Ich verstehe die Bedenken, dass ich den Stream im Hintergrund abspiele während gleichzeitig im anderen Thread möglicherweise der Lese-Zeiger verändert wird, oder der Stream sogar schon geschlossen wird. In der Praxis scheint das aber kein Problem zu sein. Der Programmteil der die *.wav Datei schreibt wird ohnehin nicht mehr benötigt, der war nur zu Testzwecken drin. Der Workaround mit 50ms Stille am Ende funktioniert ohne Probleme. Ich bin mir ziemlich sicher dass die am Ende fehlenden 40ms ein reines Hardware-Problem sind, insbesondere weil das Verhalten davon abhängt ob der 3.5mm Klinkenstecker reingesteckt ist oder nicht.

    Danke für eure Antworten!
    Michael



  • Eine Frage habe ich noch. Im Kopf der *.wav Datei muss ja drinstehen wie lang die Datei ist. Wie würde man das machen, wenn die Länge des Sounds am Anfang noch gar nicht bekannt ist? Beim Synthesizer besteht der Sound aus drei Teilen: Der Anfang mit steilem Anstieg und Abfall auf ein niedrigeres Nieveau, dann der Mittelteil mit variabler Länge (solange die Taste gedrückt gehalten wird), und dann der Schluss mit einem Abfall bis auf Null. Ist euch dazu ein Programmier-Beispiel bekannt?



  • Du könntest mittels Seek(...) zu den entsprechenden Positionen springen und dann dort nachträglich den korrekten Wert eintragen.

    Oder aber du erzeugst zuerst einen MemoryStream, der nur aus den reinen Sample-Werten besteht und erst danach erzeugst du den WAV-Stream daraus.



  • @micha7 sagte in Soundplayer verschluckt den Anfang eines Sounds:

    Eine Frage habe ich noch. Im Kopf der *.wav Datei muss ja drinstehen wie lang die Datei ist. Wie würde man das machen, wenn die Länge des Sounds am Anfang noch gar nicht bekannt ist? Beim Synthesizer besteht der Sound aus drei Teilen: Der Anfang mit steilem Anstieg und Abfall auf ein niedrigeres Nieveau, dann der Mittelteil mit variabler Länge (solange die Taste gedrückt gehalten wird), und dann der Schluss mit einem Abfall bis auf Null. Ist euch dazu ein Programmier-Beispiel bekannt?

    Das kannst du so sowieso nicht so machen, also nicht mit einem MemoryStream in den du schreibst. Weil in den MemoryStream schreiben während der gespielt wird... das wird vermutlich nix.

    Wenn du "live" Sound ausgeben willst, dann musst du den auch "live" abspielen. D.h. du musst die Audio-Daten in kleinen Stücken berechnen und dann Stückchenweise an das Audio-Device schicken. Dabei immer schauen dass nicht zu viel gepuffert ist (weil das Delay sonst zu gross wird), und natürlich darf auch nicht zu wenig gepuffert sein, weil es sonst Aussetzer gibt.

    Je nach API läuft das dann etwas unterschiedlich: Variante 1) ist genau so wie beschrieben, da gibt es einen Aufruf mit dem du einfach ein weiteres Stück Audiodaten an das Device übergibst.

    Variante 2) ist ein Ring-Puffer. Dort definierst zuerst einen Ring-Puffer mit einer bestimmten Länge, sagen wir 200ms. Den kannst du dann "starten", d.h. das Audio-Device fängt an die Daten in dem Puffer als Loop abzuspielen. Und während das Audio-Device den Puffer immer und immer wieder spielt, kannst du ändern was drinnen steht. Das ganze muss natürlich mit dem Abspielvorgang synchronisiert werden. Nicht ganz einfach zu erklären, aber es gibt dazu einige Beispiele im Netz zu finden.

    Ist aber alles wesentlich aufwendiger als mal schnell einen System.Media.SoundPlayer zu erzeugen.

    Vielleicht gibt es aber auch schon brauchbare .NET Libraries die das irgendwie abstrahieren, so dass du nur noch die Funktion schreiben musst die die neuen Audiodaten liefert. Wobei da .NET durch die nicht-deterministischen Pausen durch den GC nicht wirklich gut geeignet ist. Sowas macht man eher mit C oder C++.


Anmelden zum Antworten