
Переклад вмісту українською:
Пер 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 кГц моно, і зробити це без встановлення чого-небудь. Потім передати його до моделі перекладу і відтворити результат як перекладений голос, все це поки оригінал продовжує звучати під ним.
З цього випливають три обмеження:
- Без драйверів. Якщо потрібне перезавантаження та драйвер, це не нуль-установка.
- Немає саморефлексії. застосунок відтворює перекладений аудіо у той самий системний мікс, який він захоплює. Наївно це означало б захоплення власного голосу і переклад того перекладу. Це має бути неможливо за задумом, а не патчем через ехопорог.
- Реальна в реальному часі. Захоплення не може зупинятися. Якщо з іншого боку 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 затримується через повільність з боку ланцюга, кільце переповнюється і з’являються глюки.
Тому захоплення та обробка розділені між двома потоками з обмеженою чергою між ними:
- Поток захоплення:
GetNextPacketSize→GetBuffer→ копіюєте у 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

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