6 Stimmen

Directsound - Probleme bei der Wiedergabe eines mit Daten aus dem Netzwerk gefüllten Streaming-Puffers! Verwendung portierter DirectX-Header für Delphi

Ich bin wieder zurück mit einer weiteren DirectSound-Frage, dieses Mal zu den Möglichkeiten, DirectSound-Puffer zu verwenden:

Ich habe Pakete, die in Abständen von etwa 30 ms über das Netzwerk eingehen und Audiodaten enthalten, die von anderen Teilen der Anwendung in rohe WAV-Daten dekodiert werden.

Wenn das Indata-Ereignis durch diese anderen Teile des Codes ausgelöst wird, werde ich im Wesentlichen in eine Prozedur mit den Audiodaten als Parameter fallen gelassen.

DSCurrentBuffer wurde wie folgt initialisiert:

ZeroMemory(@BufferDesc, SizeOf(DSBUFFERDESC));
wfx.wFormatTag := WAVE_FORMAT_PCM;
wfx.nChannels := 1;
wfx.nSamplesPerSec := fFrequency;
wfx.wBitsPerSample := 16;
wfx.nBlockAlign := 2; // Channels * (BitsPerSample/8)
wfx.nAvgBytesPerSec := fFrequency * 2; // SamplesPerSec * BlockAlign

BufferDesc.dwSize := SizeOf(DSBUFFERDESC);
BufferDesc.dwFlags := (DSBCAPS_GLOBALFOCUS or DSBCAPS_GETCURRENTPOSITION2 or
    DSBCAPS_CTRLPOSITIONNOTIFY);
BufferDesc.dwBufferBytes := BufferSize;
BufferDesc.lpwfxFormat := @wfx;

case DSInterface.CreateSoundBuffer(BufferDesc, DSCurrentBuffer, nil) of
  DS_OK:
    ;
  DSERR_BADFORMAT:
    ShowMessage('DSERR_BADFORMAT');
  DSERR_INVALIDPARAM:
    ShowMessage('DSERR_INVALIDPARAM');
end;

Ich schreibe diese Daten wie folgt in meinen sekundären Puffer:

var
FirstPart, SecondPart: Pointer;
FirstLength, SecondLength: DWORD;
AudioData: Array [0 .. 511] of Byte;
I, K: Integer;
Status: Cardinal;
begin

Die Eingangsdaten werden hier in Audiodaten umgewandelt, die für die eigentliche Frage nicht relevant sind.

DSCurrentBuffer.GetStatus(Status);

if (Status and DSBSTATUS_PLAYING) = DSBSTATUS_PLAYING then // If it is playing, request the next segment of the buffer for writing
  begin
    DSCurrentBuffer.Lock(LastWrittenByte, 512, @FirstPart, @FirstLength,
      @SecondPart, @SecondLength, DSBLOCK_FROMWRITECURSOR);

    move(AudioData, FirstPart^, FirstLength);
    LastWrittenByte := LastWrittenByte + FirstLength;
    if SecondLength > 0 then
      begin
        move(AudioData[FirstLength], SecondPart^, SecondLength);
        LastWrittenByte := SecondLength;
      end;
    DSCurrentBuffer.GetCurrentPosition(@PlayCursorPosition,
      @WriteCursorPosition);
    DSCurrentBuffer.Unlock(FirstPart, FirstLength, SecondPart, SecondLength);
  end
else // If it isn't playing, set play cursor position to the start of buffer and lock the entire buffer
  begin
    if LastWrittenByte = 0 then
      DSCurrentBuffer.SetCurrentPosition(0);

    LockResult := DSCurrentBuffer.Lock(LastWrittenByte, 512, @FirstPart, @FirstLength,
      @SecondPart, @SecondLength, DSBLOCK_ENTIREBUFFER);
    move(AudioData, FirstPart^, 512);
    LastWrittenByte := LastWrittenByte + 512;

    DSCurrentBuffer.Unlock(FirstPart, 512, SecondPart, 0);
  end;

Der obige Code wird in meinem OnAudioData-Ereignis ausgeführt (definiert durch unsere proprietäre Komponente, die mit unserem Protokoll gesendete Nachrichten dekodiert). Grundsätzlich wird der Code immer dann ausgeführt, wenn ich eine UDP-Nachricht mit Audiodaten erhalte.

Nachdem ich in den Puffer geschrieben habe, tue ich Folgendes, um die Wiedergabe zu starten, sobald sich genügend Daten im Puffer befinden: BufferSize ist übrigens im Moment auf das Äquivalent von 1 Sekunde eingestellt.

if ((Status and DSBSTATUS_PLAYING) <> DSBSTATUS_PLAYING) and
   (LastWrittenByte >= BufferSize div 2) then
  DSCurrentBuffer.Play(0, 0, DSCBSTART_LOOPING);

So weit, so gut, auch wenn die Audiowiedergabe etwas abgehackt ist. Leider ist dieser Code nicht genug.

Außerdem muss ich die Wiedergabe anhalten und warten, bis sich der Puffer wieder füllt, wenn keine Daten mehr vorhanden sind. Dies ist, wo ich in Probleme laufen.

Im Grunde muss ich in der Lage sein, herauszufinden, wann die Audiowiedergabe den Punkt erreicht hat, an dem ich zuletzt in den Puffer geschrieben habe, und sie dann zu stoppen. Andernfalls wird jede Verzögerung in den Audiodaten, die ich empfange, natürlich den Ton durcheinander bringen. Leider kann ich nicht einfach aufhören, in den Puffer zu schreiben, denn wenn ich den Puffer weiterlaufen lasse, werden nur die alten Daten von vor einer Sekunde wiedergegeben (da es sich um einen Kreislauf handelt und so weiter). So muss ich wissen, wenn die PlayCursorPosition den Wert LastWrittenByte erreicht hat, die ich in meinem Puffer-Schreibcode verfolgen.

DirectSound Notifications schien dies für mich tun zu können, aber das Anhalten und erneute Starten des Puffers jedes Mal, wenn ich Daten hineinschreibe (SetNotificationPositions() erfordert, dass der Puffer angehalten wird), hat eine bemerkenswerte Auswirkung auf die Wiedergabe selbst, so dass das wiedergegebene Audio noch kaputter klingt als zuvor.

Ich habe dies an das Ende meines Schreibcodes angehängt, um Benachrichtigungen zu erhalten. Ich habe irgendwie erwartet, dass das Setzen einer neuen Benachrichtigung jedes Mal, wenn ich Daten in den Puffer schreibe, wahrscheinlich nicht allzu gut funktionieren würde... Aber hey, ich dachte, es kann nicht schaden, es zu versuchen:

if (Status and DSBStatus_PLAYING) = DSBSTATUS_PLAYING then //If it's playing, create a notification
  begin
    DSCurrentBuffer.QueryInterface(IID_IDirectSoundNotify, DSNotify);
    NotifyDesc.dwOffset := LastWrittenByte;
    NotifyDesc.hEventNotify := NotificationThread.CreateEvent(ReachedLastWrittenByte);
    DSCurrentBuffer.Stop;
    LockResult := DSNotify.SetNotificationPositions(1, @NotifyDesc);
    DSCurrentBuffer.Play(0, 0, DSBPLAY_LOOPING);
  end;

NotificationThread ist ein Thread, der WaitForSingleObject ausführt. CreateEvent erstellt ein neues Ereignis-Handle und sorgt dafür, dass WaitForSingleObject auf dieses Handle wartet, anstatt auf das vorherige. ReachedLastWrittenByte ist eine in meiner Anwendung definierte Prozedur. Der Thread startet einen kritischen Abschnitt und ruft ihn auf, wenn eine Benachrichtigung ausgelöst wird. (WaitForSingleObject wird mit einem Timeout von 20ms aufgerufen, so dass ich das Handle natürlich aktualisieren kann, wenn CreateEvent aufgerufen wird).

ReachedLastWrittenByte() bewirkt Folgendes:

DSCurrentBuffer.Stop;
LastWrittenByte := 0;

Wenn meine Benachrichtigung ausgelöst wird und ich Stop auf dem sekundären Puffer, den ich verwende, aufrufe, läuft das Audio immer noch in einer Schleife weiter, was anscheinend Restdaten im primären Puffer sind...

Und selbst dann werden die Benachrichtigungen nicht richtig ausgelöst. Wenn ich die Übertragung von Audiodaten aus der anderen Anwendung, die diese Audionachrichten sendet, stoppe, werden die Reste im Puffer einfach weitergeschleift. Im Grunde geht es also immer über die letzte von mir eingestellte Benachrichtigung (das letzte geschriebene Byte) hinaus. Bei der Wiedergabe hält es nur gelegentlich an, füllt den Puffer und beginnt dann mit der Wiedergabe... Und überspringt die halbe Sekunde an Daten, die er gerade gepuffert hat, und spielt einfach die Daten ab, die nach dem Füllen des Puffers reinkommen (er füllt also den Puffer, kümmert sich aber anscheinend nicht darum, seinen Inhalt abzuspielen, bevor er anfängt, neue Daten reinzufüllen. Ja. Ich habe auch keine Ahnung.)

Was scheint mir hier zu fehlen? Ist die Idee, DirectSound Notificatiosn zu verwenden, um herauszufinden, wann das zuletzt geschriebene Byte abgespielt wird, ein vergeblicher Versuch? Man sollte meinen, es gäbe irgendeine Möglichkeit, diese Art von Pufferung mit einem Streaming-Puffer zu machen.

10voto

Michael Stahre Punkte 481

Und schon wieder habe ich mir selbst eine Antwort auf meine eigene Frage gegeben ^^;

Es gab drei Probleme: Erstens habe ich auf die Position des Play Cursors geschaut, um festzustellen, ob die Wiedergabe das letzte geschriebene Byte erreicht hat. Das funktioniert nicht ganz richtig, denn der Play Cursor zeigt zwar an, was gerade abgespielt wird, aber er sagt nicht, welches das letzte Byte der Daten ist, die abgespielt werden. Der Schreibcursor ist つねに ein wenig vor dem Play Cursor positioniert. Der Bereich zwischen dem Play Cursor und dem Write Cursor ist für die Wiedergabe gesperrt.

Mit anderen Worten, ich habe auf den falschen Cursor geschaut.

Zweitens, und das ist der Hauptgrund, warum die Dinge nicht so funktionieren, wie sie sollten, ist, dass ich Daten mit dem DSBLOCK_FROMWRITECURSOR-Flag geschrieben habe. Dies ermöglicht es mir, Daten ab dem Schreibcursor und weiter zu schreiben. Jedes Mal.

Das bedeutet, dass, wenn ich dachte, er würde Daten zwischenspeichern, er in Wirklichkeit nur die gleichen Daten immer wieder überschrieb. Der Schreibcursor bewegt sich, anders als ich angenommen hatte, nicht vorwärts, wenn man Daten in den Puffer schreibt. Deshalb verfolge ich jetzt manuell das zuletzt geschriebene Byte und führe einfach einen DSBLOCK_ENTIREBUFFER auf diesen Offset aus. Ich handhabe Wrapping, indem ich einfach die Puffergröße und meine lastwrittenbyte var im Auge behalte und bestätige, dass es kein Wrapping gibt. Es ist hilfreich, dass ich immer nur genau 512 Byte an Daten in den Puffer schreiben muss. Wenn ich meine Puffergröße zu einem Vielfachen von 512 mache, muss ich nur sicherstellen, dass lastwrittenbyte := 0 ist, wenn lastwrittenbyte >= buffersize ist (ich habe lastwrittenbyte in nextbyte geändert, da es jetzt eher das nächste zu schreibende Byte anzeigt. Auf diese Weise muss ich mich nicht um ungerade Bytes kümmern, die ich mit a + 1 ausgleichen muss und so weiter).

Bei der angeblichen Methode zum Streamen von Daten in den Puffer müssen Sie also immer wieder Ihre eigenen Daten überschreiben. Wie man das für das Streamen von Daten in den Puffer richtig nutzen will... Ich weiß es wirklich nicht. Vielleicht denken sie, man könnte eine Benachrichtigung in die Mitte setzen und sie wie einen geteilten Puffer behandeln?

Und damit komme ich zum dritten Problem: Ich habe versucht, Benachrichtigungen zu verwenden. Es hat sich herausgestellt, dass Benachrichtigungen unzuverlässig sind, und es wird allgemein empfohlen, sie nicht zu verwenden, und wenn man es doch tun muss, sollte man nicht mehr als ein paar verwenden.

Also habe ich weg mit Benachrichtigungen und warf in einem hochauflösenden Timer, der alle 30ms auslöst (die Datenpakete, die ich empfange und extrahieren die Audio abgespielt werden, werden nach unserem Protokoll immer in einem 32 ms Intervall gesendet werden, so dass alles unter 32ms für diesen Timer wirklich funktioniert).

Ich behalte im Auge, wie viele Daten ich mit einer einfachen BytesInBuffer-Variable zu spielen links haben. Wenn mein Timer auslöst, überprüfe ich, wie viel der Schreibcursor (der, wie ich schon sagte, im Wesentlichen der wahre Play Cursor ist) seit dem letzten Mal, wenn der Timer ausgelöst wurde, vorgerückt ist und entferne diese Zahl aus meiner BytesInBuffer-Variable.

Von dort aus kann ich entweder nachsehen, ob mir die Bytes ausgegangen sind, oder ich kann mir die Position des Schreibcursors ansehen und prüfen, ob er das letzte geschriebene Byte überschritten hat (wenn ich weiß, wie viele Bytes ich seit meinem letzten Timer-Ereignis gespielt habe, ist das hilfreich).

if (BytesInBuffer <= 0) or ((WritePos >= LastWrittenByte) and (WritePos-PosDiff <= LastWrittenByte)) then
  DSBuffer.Stop;

Also hier.

Es ist immer ätzend, wenn man Leute findet, die dieselbe Frage stellen wie man selbst, nur um am Ende zu sagen: "Ich habe es schon gelöst" und keine Antwort zu hinterlassen.

Ich hoffe, dass dies in Zukunft noch jemanden aufklären kann.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X