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.