Directsound - Проблемы с воспроизведением потокового буфера, заполненного данными из сети! Использование перенесенных заголовков DirectX для Delphi - PullRequest
6 голосов
/ 27 января 2010

Вернемся к еще одному вопросу о DirectSound, который касается способов использования буферов DirectSound:

У меня есть пакеты, поступающие по сети с интервалами приблизительно 30 мс, содержащие аудиоданные, которые декодируются в необработанные wav-данные другими частями приложения.

Когда событие Indata запускается этими другими частями кода, я, по сути, попадаю в процедуру с аудиоданными в качестве параметра.

DSCurrentBuffer был инициализирован следующим образом:

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;

Я записываю эти данные в мой вторичный буфер следующим образом:

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

Входные данные здесь преобразуются в аудиоданные, не относящиеся к самому вопросу.

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;

Выше приведен код, который я запускаю в своем событии OnAudioData (определенном нашим проприетарным компонентом, который декодирует сообщения, отправленные по нашему протоколу). По сути, код запускается всякий раз, когда я получаю сообщение UDP с аудиоданными.

После записи в буфер я делаю следующее, чтобы начать воспроизведение, как только в буфере окажется достаточно данных: Кстати, для BufferSize в данный момент установлено значение, эквивалентное 1 секунде.

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

Пока все хорошо, хотя воспроизведение звука немного прерывистое. К сожалению, этого кода недостаточно.

Мне также нужно остановить воспроизведение и подождать, пока буфер снова заполнится, когда в нем закончатся данные. Здесь я сталкиваюсь с проблемами.

По сути, мне нужно иметь возможность узнать, когда воспроизведение аудио достигло точки, когда я в последний раз записывал в буфер, и остановить его, когда это произойдет. В противном случае, конечно, любые задержки в аудиоданных, которые я получаю, могут испортить звук. К сожалению, я не могу просто прекратить запись в буфер, потому что если я позволю буферу продолжить воспроизведение, он просто воспроизведет старые данные, оставленные там одну секунду назад (что с циклическим и все.) Поэтому мне нужно знать, достигло ли PlayCursorPosition значения LastWrittenByte, которое я отслеживаю в своем коде для записи в буфер.

Уведомления DirectSound, казалось, могли сделать это для меня, но остановка и последующий запуск буфера заново каждый раз, когда я записываю в него данные (SetNotificationPositions () требует остановки буфера), оказывает заметное влияние на само воспроизведение, поэтому звук, который воспроизводится, звучит еще более прерывисто, чем раньше.

Я добавил это в конец своего кода для получения уведомлений. Я ожидал, что установлю новое Уведомление каждый раз, когда я записываю данные в буфер, вероятно, не будет работать слишком хорошо ... Но, эй, я подумал, что попытка не помешает:

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 - это поток, который выполняет WaitForSingleObject. CreateEvent создает новый дескриптор события и делает его так, чтобы WaitForSingleObject ожидал этого вместо предыдущего. ReachedLastWrittenByte - это процедура, определенная в моем приложении. Поток запускает критический раздел и вызывает его при срабатывании уведомления. (WaitForSingleObject вызывается с тайм-аутом 20 мс, так что я могу обновлять дескриптор всякий раз, когда вызывается CreateEvent, конечно.)

ReachedLastWrittenByte () выполняет следующие действия:

DSCurrentBuffer.Stop;
LastWrittenByte := 0;

Когда мое уведомление срабатывает, и я вызываю Стоп для используемого мной вторичного буфера, звук все еще продолжает зацикливаться на том, что кажется оставшимися данными в первичном буфере ...

И даже тогда уведомления не запускаются должным образом. Если я прекращаю трансляцию аудиоданных из другого приложения, отправляющего эти аудиосообщения, оно просто продолжает зацикливаться на остатках в буфере. Так что, в основном, он проходит мимо последнего установленного мной уведомления (lastwrittenbyte) независимо.При воспроизведении он просто время от времени останавливается, заполняет буфер и затем начинает воспроизведение ... И пропуская полсекунды данных, которые он только что буферизировал, просто продолжая воспроизводить данные, поступающие после заполнения буфера заполняет буфер, но, видимо, не хочет воспроизводить его содержимое до того, как он начнет заполнять новые данные. Да. Я тоже понятия не имею.)

Чего мне здесь не хватает? Является ли идея использовать DirectSound Notificatiosn, чтобы выяснить, когда последний записанный байт исполняется бесполезно? Вы могли бы подумать, что есть НЕКОТОРЫЙ способ сделать такую ​​буферизацию с помощью потокового буфера.

1 Ответ

10 голосов
/ 04 февраля 2010

И снова я обнаружил, что отвечаю на свой вопрос ^^;

Было три проблемы: Сначала я посмотрел на позицию Play Cursor, чтобы определить, достиг ли воспроизведение последнего записанного байта. Это работает не совсем правильно, потому что, хотя курсор воспроизведения показывает то, что в данный момент воспроизводится, он не сообщает вам, какой последний байт набора данных для воспроизведения. Курсор записи всегда расположен немного впереди курсора воспроизведения. Область между курсором воспроизведения и курсором записи заблокирована для воспроизведения.

Другими словами, я смотрел не на тот курсор.

Во-вторых, и вот основная причина того, что вещи не работали должным образом, в том, что я записывал данные с флагом DSBLOCK_FROMWRITECURSOR. Это позволяет мне записывать данные с курсора записи и далее. Каждый раз.

Это означает, что когда я думал, что это буферизация данных, он просто снова и снова записывал одни и те же данные. Курсор записи, в отличие от того, что я предположил, не перемещается вперед при записи данных в буфер. Итак, теперь я отслеживаю последний записанный байт вручную и просто создаю DSBLOCK_ENTIREBUFFER для этого смещения. Я управляю переносом, просто отслеживая размер буфера и мою переменную lastwrittenbyte и подтверждаю, что она не переносится. Помогает то, что мне когда-либо нужно всего лишь записать ровно 512 байт данных в буфер. Если мой размер буфера кратен 512, то мне нужно только убедиться, что если lastwrittenbyte> = buffersize, то lastwrittenbyte: = 0 (я изменил lastwrittenbyte на nextbyte, так как теперь он скорее показывает следующий байт для записи. Таким образом, я не нужно беспокоиться о неравных байтах, выравнивая их с +1 и все такое.)

Таким образом, предполагаемый метод для потоковой передачи данных в буфер заставляет вас переписывать свои собственные данные снова и снова. Как они собирались использовать это для потоковой передачи данных в буфер ... Я действительно не знаю. Возможно, они думают, что вы могли бы поместить уведомление посередине и рассматривать его как разделенный буфер?

Что подводит меня к третьей проблеме: Я пытался использовать уведомления. Оказывается, что уведомления ненадежны, и люди в целом рекомендуют избегать их использования, и если вам нужно, то не следует использовать больше, чем несколько.

Так что я покончил с уведомлениями и выбросил их в таймере высокого разрешения, который срабатывает каждые 30 мс (пакеты данных, которые я получаю и извлекаю аудио, которое будет воспроизводиться по желанию в соответствии с нашим протоколом, всегда будут отправляться с интервалом 32 мс так что все, что ниже 32 мс, действительно работает для этого таймера.)

Я отслеживаю, сколько данных мне осталось воспроизвести с помощью простой переменной BytesInBuffer. Когда мой таймер срабатывает, я проверяю, насколько сильно продвинулся курсор записи (который, как я уже говорил, является курсором истинного воспроизведения) с момента последнего срабатывания таймера, и удаляю это число из моей переменной BytesInBuffer.

Оттуда я могу либо посмотреть, исчерпаны ли у меня байты, либо посмотреть позицию записывающего курсора и проверить, не превысила ли она мой последний записанный байт (зная, сколько байтов я сыграл с момента моего последнего события таймера, помогает .

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

Так вот.

Это всегда отстой, когда люди задают тот же вопрос, что и вы, только чтобы в конечном итоге сказать: «Не важно, я это решил» и не оставить ответ.

Здесь можно надеяться, что это может просветить кого-то еще в будущем.

...