You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MSVAD通过IOCTL传输音频出现周期性静音,如何实现同步?

音频IOCTL传输周期性静音的同步问题

我正通过IOCTL从用户态向内核态驱动发送音频帧,目前已能正常出声,但每隔X毫秒就会出现短暂静音。尝试调整发送间隔时长与缓冲区大小,均未解决问题。


用户态发送数据代码

using (var reader = new WaveFileReader("Bontempi-B3-C6.wav"))
{
    var outFormat = new WaveFormat(48000, 16, 2);
    var bufferedWaveProvider = new BufferedWaveProvider(outFormat)
    {
        BufferDuration = TimeSpan.FromSeconds(10)
    };

    byte[] fileBuffer = new byte[2048];
    int bytesRead;
    do
    {
        bytesRead = reader.Read(fileBuffer, 0, fileBuffer.Length);
        bufferedWaveProvider.AddSamples(fileBuffer, 0, bytesRead);
    } while (bytesRead > 0);


    byte[] buffer = new byte[1920];

    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    while (bufferedWaveProvider.BufferedBytes > 0)
    {

        int bytesWritten = bufferedWaveProvider.Read(buffer, 0, buffer.Length);
        if (bytesWritten == 0) break;


        bool success = true;
        uint bytesReturned;

        success = DeviceIoControl(hDevice, IOCTL_CSMT_READ_METHOD_BUFFERED, IntPtr.Zero, 0, buffer, (uint)bytesWritten, out bytesReturned, IntPtr.Zero);

        if (!success)
        {
            Console.WriteLine("Error sending IOCTL");
        }
        else
        {
            Console.WriteLine("Sent: " + buffer.Length);
        }

        // Waiting enough time to accomplish with required bitrate. If I try without this ioctl would fail all times as ring buffer get overflow.
        double bytesPerMillisecond = 192;
        double bytesSent = (double)bytesWritten;
        double millisecondsElapsed = (double)stopwatch.ElapsedMilliseconds;
        double timeToWait = (bytesSent / bytesPerMillisecond) - millisecondsElapsed;

        if (timeToWait > 0)
        {
            Thread.Sleep((int)timeToWait);
        }

        stopwatch.Restart();
    }
}

驱动端环形缓冲区Put方法

NTSTATUS RingBuffer::Put(BYTE* pBytes, SIZE_T count) 
{
    if (count > m_BufferLength) return STATUS_BUFFER_TOO_SMALL;
    if (count == 0) return STATUS_SUCCESS;
    if (m_Buffer == NULL) return STATUS_INVALID_DEVICE_STATE; // not initialized

    NTSTATUS status = STATUS_SUCCESS;
    //buffer overrun
    if ((m_LinearBufferWritePosition + count) - m_LinearBufferReadPosition > m_BufferLength)
    {
        status = STATUS_BUFFER_OVERFLOW;
        m_LinearBufferReadPosition = (m_LinearBufferWritePosition + count) - m_BufferLength + 1;
    }

    SIZE_T bufferOffset = m_LinearBufferWritePosition % m_BufferLength;
    SIZE_T bytesWritten = 0;
    while (count > 0)
    {
        SIZE_T runWrite = min(count, m_BufferLength - bufferOffset);
        RtlCopyMemory(m_Buffer + bufferOffset, pBytes, runWrite);
        bufferOffset = (bufferOffset + runWrite) % m_BufferLength;
        count -= runWrite;
        bytesWritten += runWrite;
    }
    m_LinearBufferWritePosition += bytesWritten;

    if (m_IsFilling && (m_LinearBufferWritePosition - m_LinearBufferReadPosition) > (m_BufferLength / 2))
    {
        DPF(D_TERSE, ("RingBuffer filled with %u bytes.", (m_LinearBufferWritePosition - m_LinearBufferReadPosition)));
        m_IsFilling = false;
    }
    return status;
}

驱动端DMA数据复制Take方法

NTSTATUS RingBuffer::Take(BYTE* pTarget, SIZE_T count, SIZE_T* readCount)
{
    KeAcquireSpinLock(m_BufferLock, &m_SpinLockIrql);

    if (m_IsFilling)
    {
        *readCount = 0;
        KeReleaseSpinLock(m_BufferLock, m_SpinLockIrql);
        return STATUS_DEVICE_NOT_READY;
    }

    count = min(count, m_LinearBufferWritePosition - m_LinearBufferReadPosition);
    SIZE_T bufferOffset = m_LinearBufferReadPosition % m_BufferLength;
    SIZE_T bytesRead = 0;
    while (count > 0)
    {
        SIZE_T runWrite = min(count, m_BufferLength - bufferOffset);
        RtlCopyMemory(pTarget + bytesRead, m_Buffer + bufferOffset, runWrite);
        bufferOffset = (bufferOffset + runWrite) % m_BufferLength;
        count -= runWrite;
        bytesRead += runWrite;
    }
    *readCount = bytesRead;
    m_LinearBufferReadPosition += bytesRead;
    if (m_LinearBufferWritePosition - m_LinearBufferReadPosition == 0)
    {
        DPF(D_TERSE, ("RingBuffer empty with %u bytes.", (m_LinearBufferWritePosition - m_LinearBufferReadPosition)));
        m_IsFilling = true;
        //m_nByteAlignBufferCount = 0;
    }

    KeReleaseSpinLock(m_BufferLock, m_SpinLockIrql);
    return STATUS_SUCCESS;
}

缓冲区初始化代码

//=============================================================================
#pragma code_seg("PAGE")
NTSTATUS MiniportWaveRTStream::AllocateBufferWithNotification
(
    _In_    ULONG               NotificationCount_,
    _In_    ULONG               RequestedSize_,
    _Out_   PMDL                *AudioBufferMdl_,
    _Out_   ULONG               *ActualSize_,
    _Out_   ULONG               *OffsetFromFirstPage_,
    _Out_   MEMORY_CACHING_TYPE *CacheType_
)
{
    PAGED_CODE();

    ULONG ulBufferDurationMs = 0;

    if ((0 == RequestedSize_) || (RequestedSize_ < m_pWfExt->Format.nBlockAlign))
    {
        return STATUS_UNSUCCESSFUL;
    }

    if ((NotificationCount_ == 0) || (RequestedSize_ % NotificationCount_ != 0))
    {
        return STATUS_INVALID_PARAMETER;
    }

    RequestedSize_ -= RequestedSize_ % (m_pWfExt->Format.nBlockAlign);

    PHYSICAL_ADDRESS highAddress;
    highAddress.HighPart = 0;
    highAddress.LowPart = MAXULONG;

    PMDL pBufferMdl = m_pPortStream->AllocatePagesForMdl(highAddress, RequestedSize_);

    if (NULL == pBufferMdl)
    {
        return STATUS_UNSUCCESSFUL;
    }

    // From MSDN: 
    // "Since the Windows audio stack does not support a mechanism to express memory access 
    //  alignment requirements for buffers, audio drivers must select a caching type for mapped
    //  memory buffers that does not impose platform-specific alignment requirements. In other 
    //  words, the caching type used by the audio driver for mapped memory buffers, must not make 
    //  assumptions about the memory alignment requirements for any specific platform.
    //
    //  This method maps the physical memory pages in the MDL into kernel-mode virtual memory. 
    //  Typically, the miniport driver calls this method if it requires software access to the 
    //  scatter-gather list for an audio buffer. In this case, the storage for the scatter-gather 
    //  list must have been allocated by the IPortWaveRTStream::AllocatePagesForMdl or 
    //  IPortWaveRTStream::AllocateContiguousPagesForMdl method. 
    //
    //  A WaveRT miniport driver should not require software access to the audio buffer itself."
    //   
    m_pDmaBuffer = (BYTE*)m_pPortStream->MapAllocatedPages(pBufferMdl, MmCached);
    m_ulNotificationsPerBuffer = NotificationCount_;
    m_ulDmaBufferSize = RequestedSize_;
    ulBufferDurationMs = (RequestedSize_ * 1000) / m_ulDmaMovementRate;
    m_ulNotificationIntervalMs = ulBufferDurationMs / NotificationCount_;

    RingBuffer::GetInstance()->Init(m_ulDmaBufferSize * 4, m_pWfExt->Format.nBlockAlign);

    *AudioBufferMdl_ = pBufferMdl;
    *ActualSize_ = RequestedSize_;
    *OffsetFromFirstPage_ = 0;
    *CacheType_ = MmCached;

    return STATUS_SUCCESS;
}

同步问题解决方案

1. 修正用户态时间同步逻辑

当前用Thread.Sleep和固定bytesPerMillisecond的方式误差极大,Thread.Sleep精度通常只有10-15ms,且无法补偿累计误差。建议改用累计字节数结合高精度计时的方式:

long totalBytesSent = 0;
long startTime = Stopwatch.GetTimestamp();
var bitrate = outFormat.SampleRate * outFormat.BitsPerSample / 8 * outFormat.Channels;

while (bufferedWaveProvider.BufferedBytes > 0)
{
    int bytesWritten = bufferedWaveProvider.Read(buffer, 0, buffer.Length);
    if (bytesWritten == 0) break;

    // 发送数据逻辑...

    totalBytesSent += bytesWritten;
    // 计算理论上应该消耗的时间
    double expectedTimeMs = (totalBytesSent * 1000.0) / bitrate;
    double actualTimeMs = Stopwatch.GetElapsedTime(startTime).TotalMilliseconds;
    double timeToWait = expectedTimeMs - actualTimeMs;

    if (timeToWait > 0)
    {
        // 用SpinWait实现更精确的等待
        while (Stopwatch.GetElapsedTime(startTime).TotalMilliseconds < expectedTimeMs)
        {
            Thread.SpinWait(10);
        }
    }
}

2. 修复环形缓冲区溢出处理

当前Put方法溢出时强制调整读指针会导致数据丢失,引发静音。应改为拒绝写入或等待读指针前进:

if ((m_LinearBufferWritePosition + count) - m_LinearBufferReadPosition > m_BufferLength)
{
    // 返回溢出错误,让用户态等待重试,而非丢弃数据
    return STATUS_BUFFER_OVERFLOW;
}

若必须覆盖旧数据,也要确保丢弃完整的音频帧(对齐m_BlockAlign),避免破坏数据结构。

3. 修正m_IsFilling状态切换逻辑

当前Take方法空缓冲区时设置m_IsFilling = true,但仅当缓冲区填充过半才取消该状态,导致DMA在填充过程中无法取数。应改为:

// Put方法中,只要有足够一个音频块的数据就取消填充状态
if (m_IsFilling && (m_LinearBufferWritePosition - m_LinearBufferReadPosition) >= m_BlockAlign)
{
    DPF(D_TERSE, ("RingBuffer has enough data to play."));
    m_IsFilling = false;
}

或直接移除m_IsFilling状态,根据可用数据量判断是否可读。

4. 确保DMA与音频格式同步

  • 检查m_ulDmaMovementRate是否等于SampleRate * BitsPerSample/8 * Channels,确保DMA速度与音频格式匹配
  • Take方法中强制读取字节数为m_BlockAlign的整数倍,避免传输不完整的音频帧

内容的提问来源于stack exchange,提问作者forlayo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 10:54:57