Виртуальный микрофон, виртуальный кабель для маршрутизации звука, цифровой выход, которого физически не существует: всё это строится на одном и том же фундаменте, драйвере ядерного стриминга. Windows предлагает для такой задачи две опоры: классический PortCls с моделями WaveRT и WaveCyclic и более общий AVStream поверх ks.sys. Разработчику, который хочет создать собственное аудиоустройство, придётся выбрать одну из них, написать мини порт с регистрацией фабрик пинов, описать диапазоны форматов и пройти сертификацию в HLK. Ниже материал разложен по рабочим слоям: от архитектуры стека до кода циклического буфера, с командами отладки и типичными ошибками.

Выбор между PortCls и AVStream определяет всю архитектуру драйвера

Семейство PortCls, модуль portcls.sys, исторически предназначено именно для аудио: порт драйвер ведёт очередь буферов, синхронизацию с аудио движком Windows и взаимодействие с Mixer API, а разработчик пишет только мини порт, который описывает возможности устройства. Вторая дорога, AVStream, универсальна и предназначена для любых потоков, включая видео; в ней звук это просто очередной тип фильтра. Для виртуального аудиоустройства почти всегда берут PortCls: так работает учебный пример SysVAD, который сопровождает WDK и демонстрирует полноценный виртуальный микрофон с наушниками. AVStream же оправдан, когда устройство смешанное, например виртуальная камера плюс звук в одном фильтре.

Стек от приложения до драйвера выглядит так: WASAPI клиент открывает endpoint через аудио службу AudioSrv, аудио движок audiodg.exe формирует граф, а внизу графа стоит KS фильтр, его пины и кольцевые буферы, за которые отвечает драйвер. Понимание этой дорожки важно, потому что каждый щелчок, тишина в потоке или искажение локализуются строго по слою.

Структура мини порта WaveRT и регистрация фабрик пинов

Сердце драйвера на PortCls это объект IMiniportWaveRT, который система получает через фабрику. Каркас создания устройства упрощённо выглядит так:

NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT DeviceObject)
{
    return PcAddAdapterDevice(DriverObject, DeviceObject, PcpStartDevice, MAX_MINIPORTS, 0);
}

NTSTATUS PcpStartDevice(PDEVICE_OBJECT DeviceObject, PIRP Irp,
    PRESOURCELIST ResourceList)
{
    PPORT port;
    NTSTATUS st = PcNewPort(&port, CLSID_PortWaveRT);
    if (NT_SUCCESS(st)) {
        IMiniportWaveRT *mini = CreateVirtualMicMiniport();
        st = port->Init(DeviceObject, Irp, mini, NULL, ResourceList);
        mini->Release();
    }
    return st;
}

Дальше мини порт обязан реализовать GetDescription: именно эта функция возвращает структуру PPCFILTER_DESCRIPTOR с массивом пинов. Для виртуального микрофона достаточно двух фабрик: одна WaveRT пин с KSPIN_DATAFLOW_OUT, то есть захват звука, и одна топологическая пин внутри устройства. Каждый пин описывает свой набор KSDATARANGE_AUDIO: частоты, разрядность, число каналов. Например классический набор 48000 Гц, 16 бит, стерео плюс 44100 Гц моно; аудио движок пересечёт эти диапазоны с запросом клиента через DataRangeIntersection и выберет рабочий формат.

Циклический буфер и ритм десяти миллисекунд

В модели WaveRT буфер выделяется один раз и прокручивается по кольцу: драйвер пишет или читает смещения, а движок дёргает событие уведомления каждые 10 миллисекунд, это стандартный период для состояния shared mode. Для виртуального устройства никакого железа не существует, поэтому ритм генерирует таймер ядра. Минимальный каркас такой:

VOID TimerDpc(PKDPC Dpc, PVOID Context, PVOID Arg1, PVOID Arg2)
{
    PADA_CONTEXT ctx = (PADA_CONTEXT)Context;
    KeAcquireSpinLockAtDpcLevel(&ctx->BufferLock);
    ULONG pos = ctx->Position;
    ULONG chunk = ctx->FrameSize * 480; // 48000 Гц за 10 миллисекунд
    GenerateSilenceOrSine(ctx->Buffer + pos, chunk);
    ctx->Position = (pos + chunk) % ctx->BufferSize;
    KeReleaseSpinLockFromDpcLevel(&ctx->BufferLock);
    KeSetTimer(&ctx->Timer, dueTime10ms, Dpc);
}

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

Период уведомления       10 мс
Частота дискретизации    48000 Гц
Формат                   16 бит, 2 канала -> кадр = 4 байта
Кадров за период         48000 * 0.010 = 480
Байт за период           480 * 4       = 1920
Кольцо на 10 периодов    1920 * 10     = 19200 байт
Запас хода при потере одного DPC   = 10 мс

Отсюда видна и цена ошибки: запас в десять периодов даёт сто миллисекунд форы, а кольцо на два периода рвётся при первом же длинном DPC соседнего драйвера.

Срыв периода даёт характерный хрип: буфер движка недобирает данные до очередного оборота кольца, и пользователь слышит треск раз в десятую секунды. Именно по этому симптому опытные отладчики сразу смотрят на DPC latency через инструмент LatencyMon или трейс DPC ISR в WPA.

Свойства, контролы громкости и маршрутизация через топологию

Виртуальное устройство становится по настоящему полезным, когда у него есть свойства: мьют, громкость, переключатель источника. KS модель описывает их через KSPROPERTY и узлы топологии в дескрипторе фильтра. Для микрофона достаточно узла KSNODETYPE_VOLUME и опционально KSNODETYPE_MUTE, соединённых маршрутом от входного пина к выходному. Каждый узел регистрирует обработчики: BasicSupport сообщает диапазон значений, Get и Set принимают и фиксируют текущее положение ползунка. Из пользовательского режима всё это видно в стандартной панели управления звуком, и ползунок реально дёргает код драйвера.

Ошибка здесь довольно частая: разработчик описывает топологию, но забывает связать узлы маршрутом через структуру PCCONNECTION_DESCRIPTOR, и в результате аудио движок инициализирует устройство, но выдаёт нулевую громкость при любой позиции ползунка. Диагноз ставится дампом активного графа через HLK Audio LOD тесты или утилитой graphstudionext для KS фильтров.

Отладка драйвера через WinDbg и трассировку событий

Когда виртуальное устройство не появляется в списке конечных точек, причина почти всегда лежит в свежих логах PnP или в отказе запроса на запуск. Рабочий набор команд выглядит так:

kd> !devnode 0 1 audio
kd> !pnpevents
kd> lm m portcls
kd> !drvobj portcls 2
kd> !irpfind
kd> !devstack ffffd10a`12345060

Трассу событий аудио подсистемы снимают отдельной сессией по имени провайдера:

logman create trace AudioDbg ^
    -p "Microsoft-Windows-Kernel-Audio" 0xffffffffffffffff ^
    -o C:\PerfLogs\audio.etl -ets
logman stop AudioDbg -ets
tracerpt C:\PerfLogs\audio.etl -o audio-report.xml -of XML

Порядок проверки складывается в короткий список:

  1. Расширение !devnode 0 1 audio показывает все узлы, а фильтр по классу отсекает лишнее;
  2. Расширение !pnpevents даёт историю последних событий установки с кодами состояния;
  3. Команды lm m portcls и !drvobj portcls 2 подтверждают, что порт-драйвер жив и его таблица диспетчеризации на месте;
  4. Трасса провайдера Microsoft-Windows-Kernel-Audio показывает открытие пинов и отказ в запрошенном формате;
  5. Расширение !irpfind по адресу объекта устройства находит зависшие запросы, например забытое завершение при смене состояния устройства;
  6. Driver Verifier с флагами Special pool и Force IRQL checking ловит гонки кольцевого буфера ещё в лаборатории.

Второй фронт это тесты комплекта HLK. Для виртуального аудиоустройства обязателен набор проверок аудиоустройств: общие тесты звука, проверка точности воспроизведения, способность пережить сон и пробуждение, а также замер отсутствия щелчков при непрерывном потоке. Отказ с кодом 0x80070057 почти всегда означает, что описание форматов не прошло проверку звукового движка, то есть где-то в дескрипторе диапазона осталось недопустимое значение.

Инсталляция через INF и нюансы подписи пакета

Драйвер виртуального звука не опирается на железо, поэтому в INF прописывают идентификатор вида SWD\\VIRTAV, и устройство заводится как программная перечислённая единица. Классический фрагмент INF:

[Standard.NTamd64]
%VirtualMic.DeviceDesc%=VirtualMic_Install, SWD\VIRTAV\VIRTUALMIC

[VirtualMic_Install.NT]
Include=ks.inf, wdmaudio.inf
Needs=KS.Registration, WDMAUDIO.Registration

[VirtualMic_Install.NT.Services]
AddService=VirtualMic, 0x00000002, VirtualMic_ServiceInstall

[VirtualMic_ServiceInstall]
ServiceType=1
StartType=3
ErrorControl=1
ServiceBinary=%10%\virtualmic.sys

После упаковки в cab и тестовой подписи пакет ставят командой pnputil /add-driver virtualmic.inf /install, а удаляют devcon remove SWD\\VIRTAV\\VIRTUALMIC. На реальных машинах с включённой изоляцией ядра драйвер подлежит аттестационной подписи, и здесь разработчика ждёт отдельный этап: билд в Visual Studio с таргетом Attestation подписи через Partner Center портал.

В WaveRT аудио движок не передаёт данные напрямую драйверу после инициализации. Вместо этого обе стороны делят кольцевой буфер, а драйвер сообщает движку текущее положение через позиционный регистр. Для виртуального устройства позиционный регистр это просто 32 разрядное значение в shared memory, которое драйвер обновляет с каждым тиком таймера. От точности этого обновления зависит всё: если позиция обновляется с дрейфом хотя бы на десятые доли миллисекунды, движок начинает замечать рассинхрон и через какое то время ловит переполнение или опустошение клиентского буфера. Хорошая практика это инкремент позиции ровно на число обработанных фреймов, а не по показаниям KeQueryPerformanceCounter, потому что счётчик производительности имеет собственный джиттер и на паравиртуализированных системах ведёт себя капризно. Проверка ровности ведётся по трейсу провайдера Microsoft Windows Audio: в событиях Glitch и PositionJump видно каждую нестыковку.

Самая каверзная ситуация у новичка это устройство, которое инициализируется, отображается в списке, но при любом воспроизведении даёт тишину. Причина почти всегда одна: движок не смог сойтись с драйвером на формате. Аудио движок Windows вызывает DataRangeIntersection и ожидает внятный результат даже в странных сочетаниях, например 2 канала при 96000 Гц, 24 бита в 32 битном контейнере. Если мини порт отвечает STATUS_NO_MATCH там, где знающий ответ это просто ближайший поддерживаемый формат, endpoint остаётся битым на весь сеанс системы. Полезный приём это прописать лог каждого вызова пересечения в WPP трассировку, потом сэмплингом посмотреть какие пары реально запрашивают популярные приложения. Чаще всего список уложится в десяток комбинаций: 44100 и 48000 Гц, 16 и 24 бита, моно и стерео.

Политика энергосбережения и поведение при сне

Виртуальному устройству всё равно выдают power план, и аудио служба ожидает, что endpoint потерпит переход в D3 при простое. Для разработчика это означает реализацию обработчиков остановки streaming с корректным завершением DMA позиции, сохранением состояния регистра позиции и возобновлением без глитча. Пропуск реализации приводит к симптому "после сна ноутбука многие приложения видят микрофон, но звука нет". Тест HLK Audio Power Management ловит ровно этот сценарий, и его стоит прогонять до каждого релиза.

Ещё одна рабочая деталь это частотный джиттер таймера. На виртуальных машинах ядерный таймер может идти с периодом не ровно десять миллисекунд, а с отклонениями в несколько сотен микросекунд. Это незаметно для обычного приложения, но для аудио оно превращается в ритмичный артефакт. Практикующий разработчик решает вопрос статистически: таймер задаёт период чуть короче номинала, скажем 9,8 миллисекунд, а driver подстраивает число генерируемых фреймов под накопленный реальный дрейф. Такой подход называют адаптивной компенсацией, и именно она отличает зрелый виртуальный аудиодрайвер от однодневного прототипа.

На готовом фундаменте виртуального микрофона строятся известные пользовательские сценарии. Виртуальный кабель это два драйвера зеркало: render устройство кладёт данные в shared memory секцию, а capture устройство читает их оттуда, и любой аудиопоток легко перенаправляется между приложениями. Сценарий loopback наоборот: одно устройство принимает системный звук и подчистую отдаёт его обратно как микрофон, что полезно для звонилок без аппаратного микрофона. Наконец, возможность указать собственный модуль обработки эффектов, APO, на виртуальном устройстве позволяет встраивать DSP обработку до уровня движка, и комбинация виртуального драйвера плюс APO сегодня считается канонической архитектурой для программных микшеров под Windows.

Собрав вместе мини порт, фабрики пинов, кольцевой буфер с честным позиционным регистром и аккуратную политику сна, разработчик получает устройство, которое для системы неотличимо от физической звуковой карты. С этого момента виртуальный звук перестаёт быть трюком и становится инженерной дисциплиной с собственными метриками ровности, автоматическими тестами и характерными тонкими местами, которые знающие ловят ещё до выхода в публичный релиз.