Help Needed with Virtual Audio Driver Development using SimpleAudioSample

Hello All,

I'm developing a virtual audio driver using the example from this GitHub repository.

I want to get the data from a user application and feed the audio data to the virtual driver for the capturing pipeline. I have created an IOCTL and can send audio data from the user application to the driver, which seems to be working fine as I verified by dumping it into a file.

However, I have two problems now:

  1. After creating the IOCTL and handling the major IRP create and IRP close, I'm not getting the endpoints. I have attached my major function handling the IRP below.
  2. How do I send audio data from adapter.cpp where I'm receiving the data to the CMiniportWaveRTStream class? I know I need to pass the data to CMiniportWaveRTStream::WriteBytes to write data to the mic.

In DriverEntry, I have created the IOCTL (IoCreateDevice) and symbolic link (IoCreateSymbolicLink). Here is my code example:

if (!NT_SUCCESS(IoCreateDevice(DriverObject, 0, &DEVICE_NAME, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &DriverObject->DeviceObject))) {
    DPF_ENTER(("Siva : IO Create device failed"));
    ntStatus = STATUS_UNSUCCESSFUL;
    goto Done;
}

// Routines that will execute once a handle to our device's symbolic link is opened/closed
DriverObject->MajorFunction[IRP_MJ_CREATE] = MajorFunctions;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = MajorFunctions;
// Routine for handling IO requests from userland
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = SimpleAudioSampleDeviceControl;

DPF_ENTER(("Siva Create symbolic link"));
if (NT_SUCCESS(IoCreateSymbolicLink(&DEVICE_SYMBOLIC_NAME, &DEVICE_NAME))) {
    DPF_ENTER(("Siva : Symbolic link created"));
} else {
    DPF_ENTER(("Siva : Symbolic link failed"));
    IoDeleteDevice(DriverObject->DeviceObject);
    ntStatus = STATUS_UNSUCCESSFUL;
    goto Done;
}

NTSTATUS MajorFunctions(PDEVICE_OBJECT DriverObject, PIRP Irp) {
    NTSTATUS ntStatus = STATUS_UNSUCCESSFUL;

    UNREFERENCED_PARAMETER(DriverObject);
    PAGED_CODE();
    PIO_STACK_LOCATION stackLocation = IoGetCurrentIrpStackLocation(Irp);

    switch (stackLocation->MajorFunction) {
    case IRP_MJ_CREATE:
        DbgPrint("siva: IRP_MJ_CREATE File Name: %wZ\n", stackLocation->FileObject->FileName);
        break;
    case IRP_MJ_CLOSE:
        DbgPrint("siva: IRP_MJ_CLOSE File Name: %wZ\n", stackLocation->FileObject->FileName);
        break;
    default:
        DbgPrint("siva: default File Name: %wZ\n", stackLocation->FileObject->FileName);
        ntStatus = STATUS_INVALID_DEVICE_REQUEST;
        break;
    }

    ntStatus = PcDispatchIrp(DriverObject, Irp);
    if (ntStatus != STATUS_SUCCESS) {
        DbgPrint("PcDispatchIrp inside if: \n");
        Irp->IoStatus.Information = 0;
        Irp->IoStatus.Status = STATUS_SUCCESS;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
    }
    return STATUS_SUCCESS;
}

Please guide me on how to move forward. Thanks in advance!

This belongs in the NTDEV category, not WINDBG. Perhaps the moderators can move it.

You have to pass the buffer down through the class inheritance tree. The adapter creates a CAdapterCommon object. That's where I store the circular buffers. Your CMiniportWaveCyclic derivative gets a copy of the CAdapterCommon during its Init handler using QueryInterface. You can define a method in CAdapterCommon that returns a pointer to the appropriate circular buffer.

Port class owns the first part of your device extension. Your section begins PORT_CLASS_DEVICE_EXTENSION_SIZE bytes in. You have to ask for the extra space when you call PcAddAdapterDevice in your AddDevice handler.

Hi @Tim_Roberts , thanks for your response! I have added the port class extension, and the PcAddAdapterDevice status is successful. However, I'm still getting a null extension when accessing it in the IOCTL handler. Here is the PcAddAdapterDevice call:

PcAddAdapterDevice(
    DriverObject,
    PhysicalDeviceObject,
    PCPFNSTARTDEVICE(StartDevice),
    maxObjects,
    PORT_CLASS_DEVICE_EXTENSION_SIZE
);

Could you help me figure out why this is happening?"

You're not asking for any extra space here. You need to add the size of your context to PORT_CLASS_DEVICE_EXTENSION_SIZE to get some space for yourself.

There should always be an extension. Are you creating your own device object for receiving these ioctls, or are you using the one Port Class creates?

@Tim_Roberts
I'm using the Port Class device object, which has an m_pCommon pointer sufficient for accessing the common class. However, the extension pointer is coming up as null.

  1. How to pass the data received from an IOCTL call to the common adapter class where I have a circular buffer:
IoCreateDevice(
    DriverObject,
    0,
    &DEVICE_NAME,
    FILE_DEVICE_UNKNOWN,
    FILE_DEVICE_SECURE_OPEN,
    FALSE,
    &DriverObject->DeviceObject
);
  1. How to get the proper extension in an IOCTL call:
ext = static_cast<PortClassDeviceContext*>(DeviceObject->DeviceExtension);

My aim is to receive data in the adapter.cpp class within an IRP_MJ_DEVICE_CONTROL handler and then pass this data to the CAdapterCommon class, where I have a circular buffer to handle the data. I'm new to driver development, so could you provide a step-by-step guide on what I need to do next? If there are any examples available, please point them out.

YOU don't call IoCreateDevice at all in an audio driver. Port Class does that, when you call PcAddAdapterDevice during your AddDevice callback.

The canonical sample is the SImpleAudioSample, but it doesn't allocate any extra device extension space.

The call you have above is asking for 0 bytes of device extension, but you won't need that whole thing.

Hi @Tim_Roberts
Siva here again. I was able to pass the audio buffer to the common class, thanks to your support!

I have two questions:

  1. I want to change the bits per sample from 32-bit float to 16-bit. Where should I make this change, and how can I support multiple sample rates, bits per sample, and channels?
  2. I created a circular buffer in common.cpp. Is it better to keep it there, or should I move the circular buffer instance to miniwavrRT or miniwaveRTStream?

Thanks in advance for your help!

  1. You have to set up the KSDATAFORMAT structures to describe your capabilities. The samples all store that in a file called xxxxwavtable.h. The one in the Simple Audio Sample (speakerwavtable.h) only supports one format at one datarate. You can look at the SysVad sample for other xxxwavtable.h examples that support a wider range.

You also might ask yourself if it's worth the trouble. Why do you need to provide other formats? The system will almost always choose the best stereo format you support, and ignore the rest.

  1. Put the buffer(s) wherever it makes sense to you. I put mine in CAdapterCommon and added a "getter" function to IAdapterCommon, because the AdapterCommon object is available to all the child objects. That's certainly not the only architecture possible, and there is no "best".

Hi @Tim_Roberts , thank you for your help. I have successfully created an end-to-end working pipeline and have a few questions.

Firstly, I'm allocating memory for a circular buffer in the CAdapterCommon class (common.cpp) and freeing the memory in the CAdapterCommon::~CAdapterCommon destructor. However, I noticed that the CAdapterCommon destructor is never called, even when removing the subdevice or unregistering the device, leading to a memory leak.

Where is the best place to release the memory that I allocated earlier? I'm considering allocating the memory when the IOCTL call is triggered. At that time, I'm sending the first audio buffer, initiating the circular buffer object, and allocating the memory. Please advise on where I should release the memory.

Secondly, in the major function, I'm getting the file name as null in IRP_MJ_CREATE and IRP_MJ_CLOSE. I'm using SetupDiGetDeviceInterfaceDetail calls to set and get the proper name for the driver. How can I correctly set and retrieve the device name?

Thank you for your assistance.

Hi @Tim_Roberts and Everyone,
Can you please respond for above query.

I have the circular buffer objects as members of CAdapterCommon, so they get cleaned up when the common is cleaned up. CAdapterCommon is a COM object, so it should get deleted when the last reference is released.

Hello @Tim_Roberts and Everyone
I am currently encountering a crash when enabling the Driver Verifier, and I have observed that the following functions are not being called:

  • CAdapterCommon::Cleanup()
  • CAdapterCommon::~CAdapterCommon()

Below is the detailed bugcheck analysis for your reference:

*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

DRIVER_VERIFIER_DETECTED_VIOLATION (c4)
A device driver attempting to corrupt the system has been caught. This is
because the driver was specified in the registry as being suspect (by the
administrator) and the kernel has enabled substantial checking of this driver.
If the driver attempts to corrupt the system, BugChecks 0xC4, 0xC1, and 0xA will
be among the most commonly seen crashes.
Arguments:
Arg1: 0000000000000062, A driver has forgotten to free its pool allocations prior to unloading.
Arg2: ffff94011d2fb940, name of the driver having the issue.
Arg3: ffff94011d5f9360, verifier internal structure with driver information.
Arg4: 0000000000000035, total # of (paged+nonpaged) allocations that weren't freed.
    Type !verifier 3 drivername.sys for info on the allocations
    that were leaked that caused the bugcheck.

Could you please assist me in identifying the root cause of this issue and provide guidance on how to proceed further?

Thank you for your support.

How can we possibly give you any more information than what it has already given you? It's TELLING you the root cause. Your driver make 53 memory allocations that were not freed when your driver unloaded. Did you do as it suggested to find out which allocations you failed to free?

Hello @Tim_Roberts and Everyone
I’m currently working with the SimpleAudio sample driver and trying to extend its functionality to support multiple audio sample rates, specifically 48 kHz and 44.1 kHz. While 48 kHz works as expected, switching to 44.1 kHz still results in the driver behaving as if it's using 48 kHz.

What I’ve Done So Far:

  1. Added both 48 kHz and 44.1 kHz formats to MicArrayPinSupportedDeviceFormats.
  2. Updated MicArrayPinSupportedDeviceModes to include both formats.
  3. Defined separate entries in MicArrayPinDataRangesRawStream for each sample rate.
  4. Updated MicArrayPinDataRangePointersStream accordingly.

Despite these changes, the driver does not seem to negotiate or switch to 44.1 kHz properly.



static KSDATAFORMAT_WAVEFORMATEXTENSIBLE MicArrayPinSupportedDeviceFormats[] =
{
    // 48 KHz
    {
        {
            sizeof(KSDATAFORMAT_WAVEFORMATEXTENSIBLE), 0, 0, 0,
            STATICGUIDOF(KSDATAFORMAT_TYPE_AUDIO),
            STATICGUIDOF(KSDATAFORMAT_SUBTYPE_PCM),
            STATICGUIDOF(KSDATAFORMAT_SPECIFIER_WAVEFORMATEX)
        },
        {
            {
                WAVE_FORMAT_EXTENSIBLE, 2, 48000, 192000, 4, 16,
                sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)
            },
            16, KSAUDIO_SPEAKER_STEREO,
            STATICGUIDOF(KSDATAFORMAT_SUBTYPE_PCM)
        }
    },
    // 44.1 KHz
    {
        {
            sizeof(KSDATAFORMAT_WAVEFORMATEXTENSIBLE), 0, 0, 0,
            STATICGUIDOF(KSDATAFORMAT_TYPE_AUDIO),
            STATICGUIDOF(KSDATAFORMAT_SUBTYPE_PCM),
            STATICGUIDOF(KSDATAFORMAT_SPECIFIER_WAVEFORMATEX)
        },
        {
            {
                WAVE_FORMAT_EXTENSIBLE, 2, 44100, 176400, 4, 16,
                sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)
            },
            16, KSAUDIO_SPEAKER_STEREO,
            STATICGUIDOF(KSDATAFORMAT_SUBTYPE_PCM)
        }
    }
};

static MODE_AND_DEFAULT_FORMAT MicArrayPinSupportedDeviceModes[] =
{
    { STATIC_AUDIO_SIGNALPROCESSINGMODE_RAW, &MicArrayPinSupportedDeviceFormats[0].DataFormat },
    { STATIC_AUDIO_SIGNALPROCESSINGMODE_RAW, &MicArrayPinSupportedDeviceFormats[1].DataFormat }
};

static KSDATARANGE_AUDIO MicArrayPinDataRangesRawStream[] =
{
    {
        {
            sizeof(KSDATARANGE_AUDIO), KSDATARANGE_ATTRIBUTES, 0, 0,
            STATICGUIDOF(KSDATAFORMAT_TYPE_AUDIO),
            STATICGUIDOF(KSDATAFORMAT_SUBTYPE_PCM),
            STATICGUIDOF(KSDATAFORMAT_SPECIFIER_WAVEFORMATEX)
        },
        2, 16, 16, 48000, 48000
    },
    {
        {
            sizeof(KSDATARANGE_AUDIO), KSDATARANGE_ATTRIBUTES, 0, 0,
            STATICGUIDOF(KSDATAFORMAT_TYPE_AUDIO),
            STATICGUIDOF(KSDATAFORMAT_SUBTYPE_PCM),
            STATICGUIDOF(KSDATAFORMAT_SPECIFIER_WAVEFORMATEX)
        },
        2, 16, 16, 44100, 44100
    }
};

static PKSDATARANGE MicArrayPinDataRangePointersStream[] =
{
    PKSDATARANGE(&MicArrayPinDataRangesRawStream[0]),
    PKSDATARANGE(&PinDataRangeAttributeList),
    PKSDATARANGE(&MicArrayPinDataRangesRawStream[1]),
    PKSDATARANGE(&PinDataRangeAttributeList),
};

Questions:

  • Are there any additional places in the driver where I need to declare or handle supported sample rates?
  • Could the issue be related to PinDataRangeAttributeList or the INF file?

Any insights or suggestions would be greatly appreciated.