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
相关产品推荐
相关产品推荐

