В первой статье мы рассказали, как впятером делали голосовую связь и несколько недель обвиняли Opus в провалах, которые на самом деле создавал наш логгер.

После публикации обсуждение довольно быстро вышло за пределы кодека. Спасибо всем, кто прочитал статью и поделился ценными мыслями. Благодаря вам мы внимательнее посмотрели на Windows-клиент и задумались, как отделить работающий звуковой тракт от интерфейса и попробовать более современную оболочку.

Самый простой ответ звучал бы так: «Win32 работает — не трогайте». И это не шутка. Через Win32 у нас уже работали глобальный push-to-talk, трей, прозрачный click-through оверлей поверх игры, выбор устройств и жизненный цикл окон. Голосовой тракт на C++ использовал WASAPI, Opus, RNNoise и SpeexDSP. Переписывать всё ради скруглённых кнопок было бы плохим обменом.

И эти мысли попали в больное место. К этому моменту один только SetupWindow.cpp вырос более чем до 6000 строк. В нём жили создание контролов, раскладка, GDI/GDI+ рисование, анимация человечка, комнаты, аватары, ввод PIN, настройки микрофона и обработка сообщений Windows. Каждая новая панель означала ещё набор координат, состояний, перерисовок и ручных проверок DPI.

Так появилась не идея «переписать БОЛТУН на модный фреймворк», а более узкий вопрос:

На каком стеке развивать следующий Desktop-интерфейс, если real-time голос, PTT, сеть и оверлей должны остаться в уже работающем нативном ядре?

Мы не стали переделывать стабильный клиент на глазах у пользователей. Сначала развернули отдельную тестовую ветку БОЛТУНа. На ней изучили три варианта для нового интерфейсного стека — Electron, WinUI 3 и Tauri 2, — а затем собрали вертикальный прототип на Tauri. Эта статья — промежуточный отчёт: выбор для эксперимента уже сделан, но решение для рабочего клиента ещё предстоит подтвердить интеграцией и замерами.

Почему Win32 вообще был разумным выбором

Первая версия начиналась как небольшая говорилка для пяти человек, а не как продукт с дизайн-системой. Win32 давал всё, что требовалось рядом с игрой:

  • обычный нативный EXE без отдельной GUI-среды выполнения в комплекте;

  • прямой доступ к циклу сообщений, устройствам и системным событиям;

  • глобальные клавиши через RegisterHotKey и GetAsyncKeyState;

  • прозрачный оверлей с WS_EX_LAYERED, WS_EX_TOPMOST, WS_EX_NOACTIVATE и WS_EX_TRANSPARENT;

  • трей и несколько небольших служебных окон;

  • отсутствие ещё одного IPC-слоя между интерфейсом и кодом на C++.

Для первой версии это было быстрее и предсказуемее любого большого архитектурного проекта.

Проблема появилась не потому, что Win32 внезапно испортился. Изменился сам БОЛТУН. К лобби добавились полноценные комнаты, персональная громкость, обработка звука, уведомления, обновления, радио, интерактивный человечек и новый визуальный язык. Интерфейс перестал быть тонкой оболочкой над пятью кнопками.

Win32 по-прежнему прекрасно решал системные задачи, но всё дороже решал задачу «нарисовать и поддерживать современное приложение».

Сначала развернули отдельную тестовую ветку

Тестовая ветка — не новое название продукта и не отдельный голосовой сервис. Это тот же БОЛТУН, но с собственным закрытым контуром для изменений, которые пока рано отдавать всем.

Мы развернули для неё отдельный сайт, отдельный процесс Go-координатора, собственный Hub с комнатами и отдельный каталог данных. Тестовые Windows- и Android-сборки жёстко направлены на тестовый WebSocket. Их комнаты не смешиваются с комнатами стабильной версии. Страница закрыта паролем и помечена noindex, поэтому эксперимент не выглядит как публичный релиз.

При этом контуры пока живут на одном сервере. Изоляция защищает данные, комнаты и версии, но тяжёлый тест всё ещё способен конкурировать со стабильным процессом за CPU и сеть. Поэтому тестовая ветка — страховка от функциональных ошибок, а не лицензия бездумно нагружать машину.

Именно туда мы сначала добавили новый интерфейс, двухэтапную автокалибровку и экспериментальную Desktop-оболочку. Стабильный БОЛТУН продолжил работать на прежнем коде.

Три кандидата: Electron, WinUI 3 и Tauri 2

Мы не начинали сравнение с вопроса «какой фреймворк моднее». У голосового клиента уже были ограничения, которые нельзя было проигнорировать:

  • существующее ядро на C++ со звуком, сетью и DSP нельзя переписывать вместе с интерфейсом;

  • глобальный PTT, трей и игровой оверлей должны сохранить нативное поведение;

  • PCM-кадры не должны ходить через UI-IPC;

  • хотелось повторно использовать TypeScript, CSS, SVG и часть логики веб-клиента;

  • приложение постоянно висит рядом с игрой, поэтому цену среды выполнения нужно не угадывать, а измерять;

  • переход должен быть поэтапным: сначала тестовая ветка и узкий мост к ядру, затем решение по рабочему клиенту.

С этими критериями у каждого кандидата обнаружился свой сильный сценарий.

Первое сравнение фреймворков мы сделали с помощью ИИ. Описали ему устройство БОЛТУНа, существующее ядро на C++ и наши требования, а он помог быстро разложить Electron, WinUI 3 и Tauri 2 по плюсам, минусам и возможным проблемам. После этого мы перепроверили важные пункты по официальной документации и перешли к прототипу. ИИ заметно ускорил исследование, но окончательный выбор мы делали уже под свой проект и свои ограничения.

Electron: наиболее прямой путь для веб-команды

Electron выглядел самым знакомым вариантом. Интерфейс можно собирать привычными веб-инструментами, поведение Chromium одинаково на поддерживаемых системах, а вокруг фреймворка есть зрелая экосистема. Для быстрого переноса веб-интерфейса это серьёзные плюсы.

Но Electron — это не просто окно с HTML. В его официальной модели процессов главный процесс работает в среде Node.js, а окна получают отдельные renderer-процессы Chromium. В рекомендациях по безопасности отдельно подчёркиваются обновление Electron вместе с Chromium и Node.js, sandbox, context isolation, CSP и проверка IPC.

Для БОЛТУНа это означало бы поставлять собственную браузерную среду и аккуратно защищать ещё одну границу между интерфейсом и системой. Мы не стали заранее объявлять Electron «тяжёлым»: время запуска, RAM и CPU всё равно нужно сравнивать на одной машине. Но для небольшой постоянно работающей утилиты собственный Chromium оказался архитектурной ценой, которую наш прототип пока не оправдывал.

WinUI 3: современный нативный Windows-вариант

WinUI 3 был не кандидатом для галочки, а самой сильной нативной альтернативой. Microsoft рекомендует его для новых Windows-приложений: это C++ или C# с XAML, современными контролами и поддержкой Windows 10 версии 1809 и новее. Windows App SDK можно подключать и к существующим Win32-приложениям, а XAML Islands позволяют внедрять новый интерфейс постепенно.

Для нашего ядра на C++ это естественное соседство: меньше концептуальных слоёв между UI и Windows API, понятный путь к системным функциям и нативный визуальный стек. Если бы единственным критерием была интеграция с текущим Windows-клиентом, WinUI 3 мог бы оказаться первым выбором.

Цена — новый XAML-слой, особенности упаковки и развёртывания Windows App SDK и привязка нового интерфейса к Windows. Уже написанные TypeScript, CSS, SVG и браузерная логика не переезжают в WinUI напрямую. Для нашего эксперимента это означало сначала заново собрать UI, а уже потом проверять архитектурную границу с ядром.

Tauri 2: системный WebView и узкий Rust-bridge

Tauri занял промежуточную позицию. Его frontend остаётся веб-интерфейсом, backend пишется на Rust, а обмен между ними строится через команды и события. На Windows фреймворк использует установленный Microsoft Edge WebView2, то есть не кладёт ещё один Chromium в каждый дистрибутив. Общая схема с системным WebView описана в архитектуре Tauri.

Это позволяло повторно использовать нашу работу с TypeScript, CSS, SVG и motion JSON, а границу с ядром на C++ оформить небольшим Rust-адаптером. Для frontend можно явно ограничить доступ через capabilities и permissions и дополнительно зажать источники контента через CSP.

У Tauri тоже нет бесплатных чудес. Нужно учитывать наличие и версию WebView2, различия системных WebView на разных ОС, освоить Rust и самостоятельно спроектировать bridge к C++. Глобальный PTT, click-through overlay и высокочастотный звук от смены оболочки проще не становятся. А экономию памяти и размера ещё предстоит доказать замерами, а не логотипом фреймворка.

Если свести предварительный выбор к короткой таблице, получилось так:

Критерий

Electron

WinUI 3

Tauri 2

Повторное использование нашего веб-интерфейса

максимальное

минимальное

максимальное

Соседство с существующим C++/Win32-кодом

через нативный мост

наиболее прямое

через мост Rust/C++

Среда интерфейса

Chromium и Node.js в поставке

Windows App SDK

системный WebView2

Другие Desktop-ОС в перспективе

да

нет

да, с учётом различий WebView

Главный риск для нас

цена runtime и поверхность IPC

повторная реализация UI и Windows-only

незавершённый native bridge и необходимость бенчмарков

Мы выбрали Tauri 2 не потому, что он победил остальные варианты по всем пунктам. Для тестовой ветки он дал наиболее полезный компромисс: повторно использовать новый веб-интерфейс, не переносить аудиоядро, не поставлять собственный Chromium и проверить строгую границу WebView -> Rust -> C++. WinUI 3 остался запасным нативным вариантом, а Electron — понятной точкой сравнения для будущих замеров.

Почему для прототипа выбрали Tauri 2

Для проверки выбранной схемы мы создали ещё один изолированный проект внутри тестовой работы. Он не заменяет текущий Windows-клиент и не меняет стабильную сборку.

Целевая граница выглядит так:

┌────────────────────────────────────────────┐
│         Tauri WebView: представление       │
│                                            │
│  комнаты · настройки · радио · SVG UI      │
│  аватары · анимация · состояния кнопок     │
└──────────────────────┬─────────────────────┘
                       │ команды и события
┌──────────────────────▼─────────────────────┐
│             Tauri 2 / Rust                 │
│                                            │
│ validation · lifecycle · capabilities      │
│ адаптер к существующему native core        │
└──────────────────────┬─────────────────────┘
                       │ узкий интерфейс
┌──────────────────────▼─────────────────────┐
│       Существующее ядро C++ / Win32        │
│                                            │
│ WASAPI · Opus · DSP · сеть · PTT · overlay │
└────────────────────────────────────────────┘

Главное правило: высокочастотное аудио не проходит через JSON IPC. WebView не должен получать PCM-буфер каждые 20 мс, рисовать из него бизнес-логику или решать, отправлять ли голос в комнату.

Интерфейс может сказать:

начать автокалибровку
выбрать микрофон
включить push-to-talk
установить громкость участника

Нативное ядро может вернуть:

калибровка завершена
шумовой фон: -52,4 dBFS
рекомендуемый gain: 1,18
VAD threshold: 0,014

Но сами аудиокадры остаются в C++.

Из чего собрали прототип

В эксперименте используются:

  • Tauri CLI 2.11.4;

  • Rust 1.97.1 GNU;

  • TypeScript 7.0.2;

  • Vite 8.2.1;

  • установленный в Windows Microsoft Edge WebView2.

React мы не добавляли. Веб-клиент БОЛТУНа и так построен без тяжёлого UI-фреймворка, а для прототипа хватило TypeScript, CSS и Vite. В production-зависимостях интерфейса остался только @tauri-apps/api.

Tauri-часть тоже пока минимальна. Rust создаёт окно с отдельным каталогом данных WebView2 и предоставляет одну команду-заглушку get_runtime_status. Она доказывает работу первоначального IPC-моста, но ещё не подключает настоящее голосовое ядро на C++.

Это важное ограничение. Красивый экран комнат не доказывает, что Tauri-клиент умеет разговаривать.

Что уже получилось

Прототип воспроизвёл новый интерфейс тестовой ветки БОЛТУНа в отдельном frameless-окне: профиль, комнаты, участники, создание комнаты, настройки звука и панель радио.

Ещё в новом интерфейсе появился маленький анимированный человечек. К голосовой связи он напрямую не относится — это скорее небольшая живая деталь, чтобы окно не выглядело совсем статичным. В Tauri мы собрали его из SVG-частей, поэтому он остаётся чётким при разном масштабе. Человечек умеет ходить, целиться, уклоняться, получать попадания и падать.

Мы начинали с простой анимации, а в итоге неожиданно для себя получили первый опыт создания маленькой игры внутри БОЛТУНа. Но это уже отдельная история для следующей статьи.

Сборка Tauri в режиме release запустилась в нативном WebView2-окне. SVG и motion JSON загружаются под CSP, окно корректно сворачивается, разворачивается, восстанавливается и закрывается. Прототип остался полностью изолирован от рабочего клиента.

Для доступа frontend мы выдали только минимальные capabilities:

{
  "permissions": [
    "core:default",
    "core:window:allow-minimize",
    "core:window:allow-toggle-maximize",
    "core:window:allow-close"
  ]
}

В CSP нет разрешения на произвольные внешние скрипты, shell или файловую систему. Когда появятся настоящие команды ядра, каждую придётся добавлять явно и проверять её входные данные на Rust/native-стороне.

Автокалибровка как проверка архитектурной границы

В интерфейсе тестовой ветки БОЛТУНа есть кнопка «Автокалибровка · 3 сек». В Tauri-прототипе она пока демонстрирует состояние UI. В тестовой версии настоящего клиента за ней уже работает нативный алгоритм на Windows и аналогичная реализация на Android.

Эта функция хорошо показывает, почему мы не хотим переносить весь код в WebView.

Пользователь не должен знать, что такое VAD threshold, dBFS и входной gain. Он нажимает кнопку и три секунды говорит обычным голосом. Приложение должно подобрать чувствительность так, чтобы не отправлять шум комнаты и не отрезать тихие начала слов.

Никакой нейросети и облачного распознавания здесь нет. Калибровка выполняется локально на PCM:

WASAPI / AudioRecord
        ↓
20-миллисекундные PCM-кадры
        ↓
RMS каждого кадра + общий peak
        ↓
оценка шумового фона и голосовых кадров
        ↓
рекомендованные input gain и VAD threshold

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

Три секунды быстро, минута осторожно

Одного трёхсекундного окна мало. Пользователь мог нажать кнопку и не успеть заговорить. Поэтому в тестовой версии появилась двухэтапная схема:

  1. Быстрая фаза собирает три секунды и сразу даёт пригодную настройку.

  2. Фоновая фаза продолжает анализ ещё 57 секунд и уточняет профиль по первой минуте разговора.

Быстрый результат применяется сразу. Финальный — плавно, чтобы порог VAD не перескочил посреди слова. На каждом цикле ядро приближает текущие значения к рассчитанным на 3,5% разницы:

appliedGain += (targetGain - appliedGain) * 0.035f;
appliedVad  += (targetVad  - appliedVad)  * 0.035f;

При push-to-talk в голосовую выборку попадают только кадры, снятые во время удержания назначенной кнопки. Иначе минута честного молчания могла бы убедить калибровщик, что голос у пользователя отсутствует.

После смены микрофона сохранённый профиль сбрасывается. Калибровка встроенного микрофона ноутбука не должна автоматически применяться к USB-гарнитуре.

Почему не стоит писать третий калибровщик на TypeScript

На Windows алгоритм реализован на C++, на Android — на Java рядом с AudioRecord. Формулы близки, но уже имеют небольшие платформенные различия. Например, Android оценивает рекомендуемый VAD из raw RMS и текущего gain, а Windows использует уровень, который видит интерфейс.

Если перенести логику в Tauri frontend, появится третья реализация. Она будет зависеть от частоты обновления UI, WebView lifecycle и сериализации. Со временем три клиента начнут по-разному понимать одну и ту же кнопку «Автокалибровка».

Поэтому для Tauri мы хотим оставить простой контракт:

UI -> begin_microphone_calibration(device_id)

native -> calibration_progress(seconds)
native -> calibration_finished({
  noiseFloorDb,
  voiceRmsDb,
  peakDb,
  inputGain,
  vadThreshold,
  voiceDetected,
  clippingDetected
})

PCM, процентили и изменение gain остаются в нативном аудиопути. WebView только показывает процесс и результат.

Что прототип пока не доказал

На текущем этапе таблица выглядит так:

Проверка

Статус

Release-сборка Tauri 2 собирается и запускается

проверено

Новый SVG-интерфейс работает в WebView2

проверено

Окно без рамки сворачивается, разворачивается и закрывается

проверено

SVG-человечек и motion JSON под CSP

проверено

Настоящий мост к голосовому ядру на C++

пока заглушка

Автокалибровка из Tauri вызывает ядро на C++

не подключено

PTT при неактивном окне и поверх игры

не проверено

Существующий click-through overlay сохранён

не проверено

Трей и запуск в одном экземпляре

не завершено

Сравнение времени запуска, RAM и CPU

не проведено

DPI 100/125/150% на одной тестовой машине

требует итогового прогона

План замеров уже составлен. Текущий клиент и Tauri-прототип нужно запустить на одной Windows-машине и сравнить холодный и повторный запуск, RAM и CPU в простое, работу радио, анимации, комнат и настроек. Отдельно проверить PTT при неактивном окне и поведение оверлея.

Пока таблица результатов пуста, утверждение «Tauri легче и быстрее Win32» было бы рекламой. Цель эксперимента скромнее: выяснить, достаточно ли хорош WebView как новый слой представления и можем ли мы подключить его, не ухудшив голос.

Как будет выглядеть миграция, если тест пройдёт

Мы не планируем одним коммитом заменить всё приложение.

Первый этап — подключить Tauri UI к существующему ядру на C++ через узкий адаптер. Rust отвечает за проверку команд, жизненный цикл приложения и разрешения. C++ продолжает захватывать и воспроизводить звук, кодировать Opus, работать с сетью и глобальными клавишами.

Второй этап — перенести в Rust только те системные сервисы, для которых появится измеримая польза. Не потому, что Rust «правильнее», а потому, что конкретный модуль станет безопаснее или проще сопровождать.

Третий этап — отказаться от старого Win32 GUI только после функционального паритета, сравнительных замеров и полевого теста с игрой.

Оверлей может вообще остаться отдельным нативным окном. Его текущие свойства — topmost, click-through и отсутствие фокуса — важнее единообразия технологии.

Что мы вынесли из обсуждения

Обсуждение не доказало, что Win32 был ошибкой. Первая версия благодаря ему вообще появилась и получила рабочий PTT, оверлей и звук без многомесячного фреймворк-проекта.

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

оставить весь интерфейс на Win32 навсегда
                или
переписать всё приложение вместе со звуком

Можно заменить представление, сохранив real-time ядро. Tauri 2 здесь не новая религия, а проверяемая гипотеза.

Сейчас результат такой: тестовая ветка уже доказала, что новый интерфейс БОЛТУНа можно собрать в Tauri/WebView2, сохранить векторную графику и изолировать эксперимент от стабильной версии. Она ещё не доказала, что Tauri готов заменить рабочий Win32-клиент в игре.

Именно поэтому следующий шаг — не дорисовывать ещё одну красивую панель, а подключить настоящий мост к ядру на C++, вызвать через него ту же автокалибровку и измерить, замечает ли аудиопоток существование WebView.

Если не замечает — у эксперимента есть будущее.

Проект: БОЛТУН

Отдельное спасибо Alexme — именно он настоял на внедрении нового фреймворка и подтолкнул нас к этому эксперименту.

Команда: PAPA DAN — разработчик; Alexme — главный тестировщик; Mihanz, San4o и A1ex — участники командных проверок и обратной связи.