Заикание во время рендеринга моего фильтра DirectShow, несмотря на то, что выходной файл "гладкий"

У меня есть приложение DirectShow, написанное на Delphi 6 с использованием библиотеки компонентов DSPACK. У меня есть два графика фильтров, которые взаимодействуют друг с другом.

Граф основного фильтра имеет такую ​​структуру:

  1. Фильтр захвата с размером буфера 100 мс.
  2. (подключен к) Sample Grabber Filter.

"Вторичный" график фильтра имеет такую ​​структуру.

  1. Custom Push Source Filter, который принимает аудио напрямую в хранилище аудиобуфера, которым он управляет.
  2. (подключен к) Фильтр рендеринга.

Push Source Filter использует Событие для управления доставкой аудио. Его команда FillBuffer() ожидает события. Событие сигнализируется, когда новые аудиоданные добавляются в буфер.

Когда я запускаю графики фильтров, я слышу крошечные "пробелы" в аудио. Обычно я связываю это условие с неправильно сконструированными аудиобуферами, которые не заполнены или имеют "пробелы" в них. Но в качестве теста я добавил Tee Filter и подключил WAV Dest Filter, а затем фильтр File Writer. Когда я проверяю выходной файл WAV, он идеально гладкий и непрерывный. Другими словами, пропуски, которые я слышу от динамика, не видны в выходном файле.

Это указывало бы на то, что, хотя звук из фильтра захвата распространяется успешно, доставка аудиобуферов получает периодические помехи. "Пробелы", которые я слышу, - не 10 раз в секунду, а, скорее, 2 или 3 раза в секунду, иногда с короткими периодами без пропусков вообще. Так что это происходит не в каждом буфере, или я слышу пропуски по 10 раз в секунду.

Моим первым предположением было бы, что это проблема блокировки, но у меня установлено время ожидания для события 150 мс, и если это происходит, генерируется исключение. Никаких исключений не выбрасывается. У меня также есть тайм-аут 40 мс, установленный в каждом критическом разделе, который используется в приложении, и ни один из них не запускается. Я проверил свои дампы OutputDebugString(), и время между не сигнализированными (заблокированными) и сигнализированными (неблокированными) появлениями показывает довольно постоянную картину, чередующуюся от 94 мс до 140 мс. Другими словами, вызов FillBuffer() в моем Push Source Filter остается заблокированным в течение 94 мс, затем 140 мс и повторяется. Обратите внимание, что продолжительность немного смещается, но она довольно регулярна. Кажется, что этот шаблон согласуется с потоком, ожидающим на фильтре захвата, чтобы выгружать свой аудиобуфер в фильтр исходного кода с интервалом 100 мс, учитывая капризы переключения потоков Windows.

Я думаю, что я использую двойную буферизацию в своем фильтре Push Source, поэтому я считаю, что если ни один из механизмов блокировки не использует объединенное время 200 мс или более, я не должен прерывать аудиопоток. Но я не могу думать ни о чем другом, кроме проблемы блокировки, которая могла бы вызвать эти симптомы. Я включил код из моего метода DecideBufferSize() в мой Push Source Filter ниже на случай, если я делаю что-то не так. Хотя это несколько длинно, я также включил вызов FillBuffer() ниже, чтобы показать, как я генерирую временные метки, на случай, если это может оказать влияние.

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

Вопрос: Должен ли я сам реализовать двойную буферизацию? Я полагал, что фильтры рендеринга DirectShow сделают это за вас, иначе другие графики фильтров, которые я создал без моего пользовательского Push Source Filter, не работали бы должным образом. Но, возможно, поскольку я создаю еще одну ситуацию блокировки / разблокировки в графе фильтров, мне нужно добавить собственный слой двойной буферизации? Я хотел бы избежать этого, конечно, чтобы избежать дополнительной задержки, поэтому, если есть другое исправление для моей ситуации, я хотел бы знать.

function TPushSourcePinBase_wavaudio.DecideBufferSize(Allocator: IMemAllocator; Properties: PAllocatorProperties): HRESULT;
var
    // pvi: PVIDEOINFOHEADER;
    errMsg: string;
    Actual: ALLOCATOR_PROPERTIES;
    sampleSize, numBytesPerBuffer: integer;
    // ourOwnerFilter: TPushSourceFilterBase_wavaudio;
begin
    if (Allocator = nil) or (Properties = nil) then
    begin
        Result := E_POINTER;
        // =========================== EXIT POINT ==============
        Exit;
    end; // if (Allocator = nil) or (Properties = nil) then

    FFilter.StateLock.Lock;
    try
        // Allocate enough space for the desired amount of milliseconds
        //  we want to buffer (approximately).
        numBytesPerBuffer := Trunc((FOurOwnerFilter.WaveFormatEx.nAvgBytesPerSec / 1000) * FBufferLatencyMS);

        // Round it up to be an even multiple of the size of a sample in bytes.
        sampleSize := bytesPerSample(FOurOwnerFilter.WaveFormatEx);

        // Round it down to the nearest increment of sample size.
        numBytesPerBuffer := (numBytesPerBuffer div sampleSize) * sampleSize;

        if gDebug then OutputDebugString(PChar(
            '(TPushSourcePinBase_wavaudio.DecideBufferSize) Resulting buffer size for audio is: ' + IntToStr(numBytesPerBuffer)
        ));

        // Sanity check on the buffer size.
        if numBytesPerBuffer < 1 then
        begin
            errMsg := '(TPushSourcePinBase_wavaudio.DecideBufferSize) The calculated number of bytes per buffer is zero or less.';

            if gDebug then OutputDebugString(PChar(errMsg));
            MessageBox(0, PChar(errMsg), 'PushSource Play Audio File filter error', MB_ICONERROR or MB_OK);

            Result := E_FAIL;
            // =========================== EXIT POINT ==============
            Exit;
        end;

        // --------------- Do the buffer allocation -----------------

        // Ensure a minimum number of buffers
        if (Properties.cBuffers = 0) then
            Properties.cBuffers := 2;

        Properties.cbBuffer := numBytesPerBuffer;

        Result := Allocator.SetProperties(Properties^, Actual);

        if Failed(Result) then
            // =========================== EXIT POINT ==============
            Exit;

        // Is this allocator unsuitable?
        if (Actual.cbBuffer < Properties.cbBuffer) then
            Result := E_FAIL
        else
            Result := S_OK;

    finally
        FFilter.StateLock.UnLock;
    end; // try()
end;

// *******************************************************


// This is where we provide the audio data.
function TPushSourcePinBase_wavaudio.FillBuffer(Sample: IMediaSample): HResult;
    // Given a Wave Format and a Byte count, convert the Byte count
    //  to a REFERENCE_TIME value.
    function byteCountToReferenceTime(waveFormat: TWaveFormat; numBytes: LongInt): REFERENCE_TIME;
    var
        durationInSeconds: Extended;
    begin
        if waveFormat.nAvgBytesPerSec <= 0 then
            raise Exception.Create('(TPushSourcePinBase_wavaudio.FillBuffer::byteCountToReferenceTime) Invalid average bytes per second value found in the wave format parameter: ' + IntToStr(waveFormat.nAvgBytesPerSec));

        // Calculate the duration in seconds given the audio format and the
        //  number of bytes requested.
        durationInSeconds := numBytes / waveFormat.nAvgBytesPerSec;

        // Convert it to increments of 100ns since that is the unit value
        //  for DirectShow timestamps (REFERENCE_TIME).
        Result :=
            Trunc(durationInSeconds * REFTIME_ONE_SECOND);
    end;

    // ---------------------------------------------------------------

    function min(v1, v2: DWord): DWord;
    begin
        if v1 <= v2 then
            Result := v1
        else
            Result := v2;
    end;

    // ---------------------------------------------------------------

var
    pData: PByte;
    cbData: Longint;
    pwfx: PWaveFormat;
    aryOutOfDataIDs: TDynamicStringArray;
    intfAudFiltNotify: IAudioFilterNotification;
    i: integer;
    errMsg: string;
    bIsShuttingDown: boolean;

    // MSDN: The REFERENCE_TIME data type defines the units for reference times
    //  in DirectShow. Each unit of reference time is 100 nanoseconds.
    Start, Stop: REFERENCE_TIME;
    durationInRefTime, ofsInRefTime: REFERENCE_TIME;
    wfOutputPin: TWaveFormat;

    aryDebug: TDynamicByteArray;
begin
    aryDebug := nil;

    if (Sample = nil) then
    begin
        Result := E_POINTER;
        // =========================== EXIT POINT ==============
        Exit;
    end; // if (Sample = nil) then

    // Quick lock to get sample size.
    FSharedState.Lock;
    try
        cbData := Sample.GetSize;
    finally
        // Don't want to have our filter state locked when calling
        //  isEnoughDataOrBlock() since that call can block.
        FSharedState.UnLock;
    end; // try

    aryOutOfDataIDs := nil;
    intfAudFiltNotify := nil;

    // This call will BLOCK until have enough data to satisfy the request
    //  or the buffer storage collection is freed.
    if FOurOwnerFilter.bufferStorageCollection.isEnoughDataOrBlock(cbData, bIsShuttingDown) then
    begin
        // If we are shutting down, just exit with S_FALSE as the return to
        //   tell the caller we are done streaming.
        if bIsShuttingDown then
        begin
            Result := S_FALSE;

            // =========================== EXIT POINT ==============
            exit;
        end; // if bIsShuttingDown then

        // Re-acquire the filter state lock.
        FSharedState.Lock;

        try
            // Get the data and return it.

            // Access the sample's data buffer
            cbData := Sample.GetSize;
            Sample.GetPointer(pData);

            // Make sure this format matches the media type we are supporting.
            pwfx := AMMediaType.pbFormat;       // This is the format that our Output pin is set to.
            wfOutputPin := waveFormatExToWaveFormat(FOurOwnerFilter.waveFormatEx);

            if not isEqualWaveFormat(pwfx^, wfOutputPin) then
            begin
                Result := E_FAIL;

                errMsg :=
                    '(TPushSourcePinBase_wavaudio.FillBuffer) The wave format of the incoming media sample does not match ours.'
                    + CRLF
                    + ' > Incoming sample: ' + waveFormatToString(pwfx^)
                    + CRLF
                    + ' > Our output pin: ' + waveFormatToString(wfOutputPin);
                OutputDebugString(PChar(errMsg));

                postComponentLogMessage_error(errMsg, FOurOwnerFilter.FFilterName);

                MessageBox(0, PChar(errMsg), 'PushSource Play Audio File filter error', MB_ICONERROR or MB_OK);

                Result := E_FAIL;

                // =========================== EXIT POINT ==============
                exit;
            end; // if not isEqualWaveFormatEx(pwfx^, FOurOwnerFilter.waveFormatEx) then

            // Convert the Byte index into the WAV data array into a reference
            //  time value in order to offset the start and end timestamps.
            ofsInRefTime := byteCountToReferenceTime(pwfx^, FWaveByteNdx);

            // Convert the number of bytes requested to a reference time vlaue.
            durationInRefTime := byteCountToReferenceTime(pwfx^, cbData);

            // Now I can calculate the timestamps that will govern the playback
            //  rate.
            Start := ofsInRefTime;
            Stop := Start + durationInRefTime;

            {
            OutputDebugString(PChar(
                '(TPushSourcePinBase_wavaudio.FillBuffer) Wave byte index, start time, stop time: '
                + IntToStr(FWaveByteNdx)
                + ', '
                + IntToStr(Start)
                + ', '
                + IntToStr(Stop)
            ));
            }

            Sample.SetTime(@Start, @Stop);

            // Set TRUE on every sample for uncompressed frames
            Sample.SetSyncPoint(True);

            // Check that we're still using audio
            Assert(IsEqualGUID(AMMediaType.formattype, FORMAT_WaveFormatEx));

{
// Debugging.
FillChar(pData^, cbData, 0);
SetLength(aryDebug, cbData);
if not FOurOwnerFilter.bufferStorageCollection.mixData(@aryDebug[0], cbData, aryOutOfDataIDs) then
}
            // Grab the requested number of bytes from the audio data.
            if not FOurOwnerFilter.bufferStorageCollection.mixData(pData, cbData, aryOutOfDataIDs) then
            begin
                // We should not have had any partial copies since we
                //  called isEnoughDataOrBlock(), which is not supposed to
                //  return TRUE unless there is enough data.
                Result := E_FAIL;

                errMsg := '(TPushSourcePinBase_wavaudio.FillBuffer) The mix-data call returned FALSE despite our waiting for sufficient data from all participating buffer channels.';
                OutputDebugString(PChar(errMsg));

                postComponentLogMessage_error(errMsg, FOurOwnerFilter.FFilterName);

                MessageBox(0, PChar(errMsg), 'PushSource Play Audio File filter error', MB_ICONERROR or MB_OK);

                Result := E_FAIL;

                // =========================== EXIT POINT ==============
                exit;
            end; // if not FOurOwnerFilter.bufferStorageCollection.mixData(pData, cbData, aryOutOfDataIDs) then

            // ------------- OUT OF DATA NOTIFICATIONS -----------------

            {
                WARNING:  TBufferStorageCollection automatically posts
                AudioFilterNotification messages to any buffer storage
                that has a IRequestStep user data interface attached to
                it!.
            }

            if FOurOwnerFilter.wndNotify > 0 then
            begin
                // ----- Post Audio Notification to Filter level notify handle ---
                if Length(aryOutOfDataIDs) > 0 then
                begin
                    for i := Low(aryOutOfDataIDs) to High(aryOutOfDataIDs) do
                    begin
                        // Create a notification and post it.
                        intfAudFiltNotify := TAudioFilterNotification.Create(aryOutOfDataIDs[i], afnOutOfData);

                        // ourOwnerFilter.intfNotifyRequestStep.triggerResult(intfAudFiltNotify);
                        PostMessageWithUserDataIntf(FOurOwnerFilter.wndNotify, WM_PUSH_SOURCE_FILTER_NOTIFY, intfAudFiltNotify);
                    end; // for()
                end; // if Length(aryOutOfDataIDs) > 0 then
            end; // if FOurOwnerFilter.wndNotify > 0 then

            // Advance the Wave Byte index by the number of bytes requested.
            Inc(FWaveByteNdx, cbData);

            Result := S_OK;
        finally
            FSharedState.UnLock;
        end; // try
    end
    else
    begin
        // Tell DirectShow to stop streaming with us.  Something has
        //  gone seriously wrong with the audio streams feeding us.
        errMsg := '(TPushSourcePinBase_wavaudio.FillBuffer) Time-out occurred while waiting for sufficient data to accumulate in our audio buffer channels.';
        OutputDebugString(PChar(errMsg));

        postComponentLogMessage_error(errMsg, FFilter.filterName);
        MessageBox(0, PChar(errMsg), 'PushSource Play Audio File filter error', MB_ICONERROR or MB_OK);

        Result := E_FAIL;
    end;
end;

2 ответа

Решение

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

Расширенные свойства аудио рендерера

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

Что касается типичных причин проблем с воспроизведением, я бы проверил следующее:

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

Оба сценария должны иметь некоторое отражение Advanced вкладка данных.

Можете ли вы попробовать изменить

    // Ensure a minimum number of buffers
    if (Properties.cBuffers = 0) then
        Properties.cBuffers := 2;

в

    // Ensure a minimum number of buffers
    if (Properties.cBuffers < 2) then
        Properties.cBuffers := 2;

Чтобы убедиться, что у вас есть как минимум два буфера. Если у вас есть только один буфер, вы услышите пробелы.

Другие вопросы по тегам