Translating Windows system audio in real time — driverless, with no virtual cable

від

у

Переклад вмісту українською:

Пер Translating Windows system audio in real time — driverless, with no virtual cable
https://ift.tt/aVYjLey

Я створюю Voxis, відкритий застосунок для Windows, який перекладає те, що відтворює ваша система — відео, гру, іншу сторону дзвінка — і відтворює переклад як промовлений голос з затримкою кілька секунд позаду спікера. Без субтитрів, без віртуального аудіокабелю, без бота, що приєднується до вашої зустрічі.

Частина «без віртуального кабелю» — це та частина, яку варто розглядати. майже кожен інструмент для системного аудіо в Windows радить встановити VB-CABLE або VoiceMeeter, або збирати бота в ваш дзвінок. Voxis цього не робить для вхідного аудіо. Цей допис пояснює, як працює цей механізм захоплення, та гострі межі, які я зустрів, розробляючи його на Python.

Я буду конкретно зазначати, що складно, і чесно говорити про те, що не моє виправляти.

Мета

Зчитати точний аудіо, яке відтворює користувач — пост-мікш системного виходу — з частотою 16 кГц моно, і зробити це без встановлення чого-небудь. Потім передати його до моделі перекладу і відтворити результат як перекладений голос, все це поки оригінал продовжує звучати під ним.

З цього випливають три обмеження:

  1. Без драйверів. Якщо потрібне перезавантаження та драйвер, це не нуль-установка.
  2. Немає саморефлексії. застосунок відтворює перекладений аудіо у той самий системний мікс, який він захоплює. Наївно це означало б захоплення власного голосу і переклад того перекладу. Це має бути неможливо за задумом, а не патчем через ехопорог.
  3. Реальна в реальному часі. Захоплення не може зупинятися. Якщо з іншого боку VAD або збірка збирачаGarbage збився з темпу, кільце WASAPI не має переповнюватися.

Процес-петля WASAPI: захоплення міксу мінус себе

У Windows 10 версії 2004 з’явився API ApplicationLoopback — спосіб активувати IAudioClient у режимі петлі з прив’язкою до дерева процесів, або включаючи лише це дерево або не включаючи його. Виключення нашого власного дерева процесів — це саме те, що потрібно для обмеження №2: захоплений мікс — це все, що чує користувач, з видаленим виходом Voxis.

Ви не отримуєте цього клієнта через звичайний шлях IMMDeviceEnumerator. Він активується за ім’ям через ActivateAudioInterfaceAsync, передаючи параметри петлі в PROPVARIANT, що несе BLOB:

params = AUDIOCLIENT_ACTIVATION_PARAMS()
params.ActivationType = AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK
params.u.ProcessLoopbackParams.TargetProcessId = my_pid
params.u.ProcessLoopbackParams.ProcessLoopbackMode = \
    PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE

pv = PROPVARIANT()
pv.vt = VT_BLOB
pv.blob.cbSize = sizeof(params)
pv.blob.pBlobData = ctypes.cast(byref(params), c_void_p)

Ім’я пристрою — магічна рядок VAD\Process_Loopback. Активація асинхронна: ви передаєте ActivateAudioInterfaceAsync обробник завершення і чекаєте його виклику.

Ловушка IAgileObject

Ось та штука, яка коштувала мені половини дня. Обробник завершення — це об’єкт COM, який ви реалізуєте самостійно (на Python через comtypes.COMObject). Якщо він реалізує лише IActivateAudioInterfaceCompletionHandler, ActivateAudioInterfaceAsync повертає E_ILLEGAL_METHOD_CALL і ніщо не підкажете, чому.

Виправлення: обробник має також реалізовувати IAgileObject — маркерний інтерфейс без методів, який заявляє, що об’єкт поза залежністю від «квартири» (apartment-agnostic). Додайте його до списку COM-інтерфейсів і активація успішна:

class _Handler(COMObject):
    _com_interfaces_ = [IActivateAudioInterfaceCompletionHandler, IAgileObject]

IAgileObject має порожній список методів — це суто обіцянка «ви можете викликати мене з будь-якої квартири». WASAPI відмовляється продовжувати без нього.

Запит формату, який дійсно потрібен

Інша вежа зручності: WASAPI дозволяє Initialize петлю-подключення з точним WAVEFORMATEX, який ви хочете. Я запитую 16 кГц, моно, 16-біт PCM безпосередньо — що збігається з потребами моделі перекладу як вхід — тобто в гарячому шляху відсутній етап ресамплінгу:

wfx.nChannels = 1
wfx.nSamplesPerSec = 16000
wfx.wBitsPerSample = 16
client.Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK,
                  2000000, 0, byref(wfx), None)

Той 2000000 означає буфер на 200 мс у одиницях 100 нс.

Забезпечення реального часу захоплення

Цикл захоплення петлі має одну задачу, яку він ніколи не повинен пропускати: викликати GetBuffer, скопіювати байти, викликати ReleaseBuffer. Якщо ReleaseBuffer затримується через повільність з боку ланцюга, кільце переповнюється і з’являються глюки.

Тому захоплення та обробка розділені між двома потоками з обмеженою чергою між ними:

  • Поток захоплення: GetNextPacketSizeGetBuffer → копіюєте у numpy-масив → ReleaseBuffer → додаєте до deque. Це все, що він робить. Він ніколи не запускає VAD або мережевий код.
  • Поток обробки: спорожнює чергу та виконує (іноді повільний) колбек на кожному фрагменті — керування VAD, потім передача до перекладача.

Черга — це collections.deque(maxlen=N)за замовчуванням викидання найстарішого. Якщо процесор відстає, старе аудіо викидається, щоб обмежити затримку, замість того, щоб дозволити захоплювальному потоку блокуватися. Затримка через GC або затримку VAD у споживача ніколи не може затримати ReleaseBuffer. Це єдина найважливіша концепція дизайну в шляху захоплення, і це три рядки коду.

self._queue = collections.deque(maxlen=64)   # обмежена; приблизно буфер пакетів
# потік захоплення:
self._queue.append(x)        # ніколи не блокує; найдорожчий відкидається за тиску

Зменшення гучності без торкання аудіо

Коли переклад говорить, вам потрібно зробити оригінал тихішим, щоб дві голосові джерела не сперечалися. Спокуслива стратегія — змішувати аудіо: захоплюєте звук, знижуєте його гучність і відтворюєте його самостійно. Але тоді ви відповідаєте за відтворення, затримку та маршрутизацію пристроїв для кожного додатку в системі.

Замість цього Voxis знижує гучність на джерелі за допомогою Windows session-volume API (ISimpleAudioVolume через pycaw): зменшуйте гучність аудіосесії програми, що відтворює, а не байти у нашому каналі. Оригінал продовжує звучати через свій шлях, без змін, окрім рівня гучності, і підвищується після зупинки перекладу. Ніякого змішування, жодної додаткової затримки на оригіналі, нічого додатково маршрутизувати.

(Є друга шлях захоплення для людей, які все ж встановлюють віртуальний кабель, де Voxis може виконати справжнє M/S-центр-супресію для приглушення діалогу при збереженні стерео-музики — але це за вибором, а шлях без драйверів вище за замовчанням.)

Тимчасова затримка, яку я не контролюю — і те, що я контролюю

Люди завжди питають, чому це не миттєво. Дві чесні фрази:

Модель перекладу — це внутрішній одночасний перекладач. Якщо подаєте безперервний потік, вона перекладає під час промови спікера та зважує якість відносно синхронності, залишаючись на кілька секунд позаду — цей розрив вух-голосі задуманий (вона чекає достатньо контексту, щоб правильно перекласти речення), і це не регулюється клієнтом. Немає поля «їдь швидше».

Що я можу зробити — уникати додавання затримки зверху:

  • Підігрів з’єднання перед початком захоплення, щоб перше речення не платило за холодний рукопис WebSocket.
  • Вимкнути стиснення повідомлень WebSocket — це чисте накладення для PCM.
  • Надіслати безперервний потік, а не кінцеву точку клієнта. Модель має власну кінцеву точку; обгортання переходів з боку клієнта лише створює конфлікти.
  • Прив’язати VAD до CPU. Silero VAD при пакеті розміром 1 має нижчу затримку на CPU, ніж витративаний обмін з хостом↔приладом, і уникати затримки від CUDA-DLL на машинах без GPU.
  • Обмежити вхідну чергу — викидати найстаріший, щоб повільний момент не перетворювався на зростаючий борг.

Жодна з цих змін не впливає на основну затримку моделі. Я вважаю за краще сказати це чітко, ніж натякати, що кілька клієнтських налаштувань зробили це реальним часом.

Open-core, і чому межа підтримується у CI

Voxis — open-core. Ядро движка на GitHub і працює BYOK — принесіть свій Gemini-ключ, збережений зашифровано на вашому ПК і прив’язаний до вашого облікового запису Windows. Відкритий збірка не робить жодних запитів до мого бекенда: жодної авторизації, жодної квоти, жодної телеметрії, жодного звітування про використання. Єдину мережу, до якої торкається, — це Gemini WebSocket, який відкриває ваш ключ.

Це легко заявити і легко порушити випадково. Тому межа публічного репозиторію контролюється скриптом з дотриманням релізу, підключеним до CI, та хуком перед відправкою: він відмовляє будь-який закритий шлях ядра, будь-яку підписану «таємницю», і будь-яке неконтрольоване імпортування закритого пакету. Чистий запуск — умова релізу. Розділення — це властивість, яку збірка доводить, а не обіцянка в README.

Що він не робить (поки що)

  • Тільки Windows. Увесь шлях захоплення — це специфічна WASAPI-можливість Windows. Інші платформи потребували б зовсім іншої стратегії захоплення.
  • Залежний від Gemini. Побудований на моделях живого перекладу одного постачальника. Якщо модель зміниться, Voxis зміниться разом з нею.
  • Вихід на зустрічі потребує віртуального мікрофона. Відправлення вашого перекладеного голосу в дзвінок означає подати мікрофон у застосунок зустрічі, що може обрати його, і Windows дозволяє це тільки для віртуального аудіодрайвера. Вхідне перекладення не потребує нічого; вихід — повертається до режиму лише прослуховування без кабелю.

Спробуйте / читайте

Движок, код петлі-відтворення та межа CI — усе в репозиторії: https://github.com/DavutAkca/voxislive (PolyForm Noncommercial).

Якщо ви випустили WASAPI петлю з мови, що керується, мені було б справді цікаво порівняти нотатки щодо обробника активації та вимоги про адаптивний об’єкт — залиште коментар.

HI-FI News

через DEV Community https://dev.to

25 червня 2026 року о 06:00 AM

June 25, 2026 at 06:00AM


Коментарі

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *