MSVAD sending audio through IOCTL and getting silences each X ms

I am sending audio frames from userland to kernel mode driver using IOCTL, it’s working so far but I am having a lot of silences each X ms. I am struggling with timings to wait between sendings and with buffer size without luck at the minute.

The code I use to send data is the following:

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();
    }
}

At the driver side I am pushing it to a ring buffer with this put method:

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;
}

And then it’s copied to DMA using this method:

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;
}

The buffer is initialised in this way:

//=============================================================================
#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;
}

As I said, it works so far and it produces sound, but it’s having a silence of few ms each X ms making the sound not continuous…

What should I do to have this in sync ?

I finally found the issue that was based on the situation of using Thread.sleep() to defined the periods of sending buffer to the driver. That doesn’t had the enough preccision and it is usually defaulting to windows clock at aroun 15-25ms in works cases, and these milliseconds matters a lot here.

The implementation is even worst as hadn’t in account the time taken by the call itself making next call being more late. Normally that call is soo fast, but when it’s giving back an error it could take 4-5ms.

The solution I added for this problem is to use Precission-Time.NET to have the cycle of audio sending based on it that uses Windows Multimedia Timers with a preccision up to 1ms.

I hope this would help someone :smiley: