Directsound-проблемы с воспроизведением буфера потоковой передачи, заполненного данными из сети! Использование портированных заголовков DirectX для Delphi


Вернемся еще к одному вопросу 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 со звуковыми данными.

После записи в буфер я делаю следующее, чтобы начать воспроизведение один раз в буфере достаточно данных: Размер буфера, кстати, устанавливается равным 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;

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

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

Что кажется, я здесь пропал? Является ли идея использовать DirectSound Notificatiosn, чтобы узнать, когда последний написанный байт воспроизводится, бесплодными усилиями? Можно было бы подумать, что есть какой-то способ сделать этот вид буферизации с потоковым буфером.

1 5

1 ответ:

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

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

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

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

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

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

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

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

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

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

Вот так.

Всегда хреново находить людей, задающих тот же вопрос, что и вы, только чтобы в конечном итоге пойти "неважно, что я решил его" и не оставить ответа.

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