golang

Скажи yay -S say v1.0

  • пятница, 6 ноября 2026 г. в 00:00:09
https://habr.com/ru/articles/1103768/

После долгого перерыва, в день народного единства у меня появилось время на мои пет проекты. И как можно догадаться, первое что людей объединяет это связь. Про мой проект видеозвонков в терминале я рассказывал подробно в этой статье. Проект вырос из очень давней идеи, которая зрела почти 20 лет и наконец созрела буквально год назад. Затем она быстро развилась в несколько видео и фото кодеков, терминальный плеер, а так же в полезную утлилиту для звонков прямо в терминале say.

Say 1.0, видеозвонки в терминале, а что нового?

Еще раз кратенько общее описание утилиты. Внутри у неё свой узел Yggdrasil, поэтому не нужны ни сервер, ни отдельный демон, yggdrasil не боится NAT и обрывов связи. Процесс созвона простой, один запускает say, второй запускает say <адрес>, и вы разговариваете. Видео рисуется прямо в терминале символами Unicode, весь трафик шифруется сквозным образом средствами Yggdrasil. Написано на Go.

Между версиями 0.1.3 и 1.0.0 очень много изменений, как в визуальной части, так и под капотом. Переписан и заменен кодек звука, видео, добавлена передача файлов и чат. 

Видео кодек, несколько мощных оптимизаций и сокращение трафика в 10 раз

Кадр, в моем самописном кодеке это сетка ячеек 4×8 пикселей. Для каждой ячейки кодек подбирает один из 64 блочных символов (▄, ▌, ▝ и так далее) и два цвета, основной и фоновый. Раньше каждый кадр уходил целиком, за исключением ключевого кадра с палитрой. Примерный трафик RxTx для небольшого окна получалось около 30КБ в секунду. Но я заметил, что в созвоне обычно часть пикселей не меняется, например фон, а значит не обязательно передавать все пиксели 30 раз в секунду. Провел исследование с подсвечиванием только тех пикселей которые меняются и оказалось их не так и много. Сразу интегрировал эту оптимизацию в кодек.

Теперь, после полного кадра передаются только ячейки, которые изменились. Полный кадр, примерно раз в полсекунды, так что потерянный пакет на мгновение замораживает картинку, а не портит её. Неподвижная картинка теперь стоит несколько сотен байт на кадр вместо нескольких килобайт. 

Шум камеры заставлял неподвижную картинку «кипеть». Теперь ячейка не меняется, пока её новый вид отличается от старого меньше чем на порог: считается средняя разница цвета на пиксель.

Собеседник сообщает долю потерянных кадров (у каждого кадра есть номер). Отправитель при потерях сначала поднимает порог удержания, то есть картинка грубеет и кадры становятся легче, и только когда порог упёрся в максимум, снижает частоту кадров. Когда канал очищается, всё возвращается в обратном порядке. Максимальный порог зависит от размера картинки: большая картинка переносит огрубление легче.

Так же идет адаптация и по fps, теперь он будет максимальным при хорошем канале и станет меньше при плохом.

В результате всех отпимизаций, весь видеопоток удалось уменьшить в 10 раз, 3КБ в секунду!

Терминал поток данных UI оптимизация и сокращение нагрузки 1000 раз, CPU в два раза ниже

Я профилировал клиента и увидел, что именно сам терминал тратит больше CPU, чем всё остальное. Каждый кадр перерисовывал весь экран с 24-битными цветами: около 140 КБ escape-последовательностей на кадр, 4,4 МБ/с, которые терминалу нужно было разобрать.

Добален кеш экрана. Для каждой ячейки видео помним, что на экране сейчас, и выводим только отличия. Рамки, подписи и строки чата отправляются, только если строка изменилась.

Отпимизация вывода. Внутри строки курсор сдвигается относительно (ESC[5C вместо ESC[34;120H), а пропуск в одну-две ячейки того же цвета проще перепечатать. Для пустой ячейки цвет символа не нужен вообще. Если меняются оба цвета, они идут одной SGR-последовательностью. 

Без Fmt. Коды собираются через strconv в буфер на стеке: полная отрисовка панели стала в 2,5 раза быстрее, 25 аллокаций на кадр вместо 11 452. 

Без рывков. Раньше раз в 2 секунды шла полная перерисовка (кадр на 190 КБ) на случай, если что-то испортило экран. Теперь вместо неё каждый кадр обновляет пару строк.

Звук, новый кодек и сокращение трафика в 10 раз

В прошлый раз я подбирал кодек исходя из того насколько кодек хорошо работает, как звучит. Сейчас я погрузился глубже и прошелся по Github и нашел неплохой кодек Adaptive Multi-Rate Narrowband.

Назначение: Разработан для сетей мобильной связи (стандарты GSM и UMTS/3G), а также используется в VoIP-системах и голосовой связи через LTE (VoLTE).

Частота дискретизации: Работает с узкой полосой пропускания — 8 кГц, передавая звуковые частоты в диапазоне, достаточном для разборчивости речи, но без музыкальных деталей.

Адаптивность (динамический битрейт): Умеет "на лету" переключаться между 8 различными скоростями потока (от 4,75 до 12,2 кбит/с). При ухудшении сигнала в радиоэфире кодек снижает битрейт в пользу большей защиты от ошибок передачи, а при хорошем сигнале — повышает качество.

Технология сжатия: Основан на методе MR-ACELP (алгебраическое линейное предсказание с несколькими скоростями).

Звук по умолчанию, AMR-NB на 4,75 кбит/с. Это примерно на 90% меньше, чем G.722 (48 кбит/с) в 0.1.3. Opus и G.722 тоже доступны, по ключам -audio-codec g722 или -audio-codec opus. Для дозвона потребуется что бы и клиент и сервер были запущены с одинаковым кодеком, либо версия одного из них была 1.0.1.

Эхоподавление, шумоподавление и синхронизация видео со звуком

macOS: VoiceProcessingIO через cgo, около 250 строк. Микрофон и динамик идут через один системный блок, и он сам убирает эхо, шум и выравнивает громкость. Задержка для синхронизации видео со звуком тоже через CoreAudio. На MacBook Air это 40,3 мс = буфер 512 + задержка устройства 70 + запас 74 + задержка потока 1280 кадров. 

Windows: Voice Capture DSP (CLSID_CWMAudioAEC) в режиме source mode. DSP сам открывает микрофон и сам берёт звук динамика как эталон. Пока требует тестировния из за отсутствия у меня Windows, новая реализация может содержать проблемы.

Linux: пока miniaudio со своим эхоподавлением. Системного аналога там нет, если пользователь сам не включил эхоподавление в PipeWire или PulseAudio.

Готовой Go-библиотеки, которая выбирала бы системную голосовую обработку под каждую ОС, я не нашел. Ближайший аналог Rust-крейт sys-voice.

Теперь видео синхронизировано со звуком: кадр показывается, когда звук с тем же временем реально выходит из динамика. Позиция воспроизведения плавно интерполируется между вызовами аудиоустройства.

Чат

Я подумал, что если вдруг пропадает связь, ну или просто хочется приватно пообщаться, то было бы великолепно написать собеседнику, а как дополнение отправить и файл. Сказано − сделано! В программе появился милый чат, сообщения резделены на две часть под аватаркой каждого собеседника. Передать файл довольно легко, нужно просто перетащить его и кинуть в окно терминала. Приятная мелочь.

Передача файлов

Файлы идут по тому же TCP-каналу, что чат и пинги, а звонок в это время использует тот же путь через Yggdrasil. 

В проекте есть ограничитель который работает по задержке, как LEDBAT (так устроены торрент-клиенты и фоновые обновления)

Пинг как зонд. Он идёт в одном потоке с данными файла, поэтому его RTT растёт сразу, как только файл набивает очередь. Во время передачи пинг отправляется раз в 250 мс. 

Без верхнего предела. Старт с 1 МБ/с, рост ×1,25 на каждом чистом замере.

Звонок в приоритете. Если лишняя задержка больше 40 мс или собеседник сообщает о потерях звука и видео, скорость ×0,7. При серьёзной перегрузке ×0,5 и короткая пауза. Снижение, не чаще одного раза за круг пинга. 

Файл на 300 МБ между двумя узлами Yggdrasil на одной машине ушёл за 6,2 секунды, в среднем 46 МБ/с, на пике больше 300 МБ/с.

Прогресс показывается прямо в записи файла в чате, у отправителя и у получателя: сколько из скольких, процент и скорость. 

Обновление либ, скорость соединения сократилась в 10 раз, а скорость передачи выросла в 1000

Кстати библиотека ygg переписана и оптимизирована целиком и полностью. Теперь коннект просиходит практически мгновенно, раньше это занимало порядка 15 секунд, сейчас пару секунд, а поиск первого пира происходит мгновенно. Скорость передачи файлов, размер пакетов MTU и другие оптимизации сильно улучшили всё. Так же появился вариант работы прямо в локальной сети, пинг при таком коннекте составляет менее 1 секунды. В локальной сети собеседники находят друг друга через multicast Yggdrasil и соединяются напрямую, а не через интернет-пиры.

Обновлены все библиотеки в проекте, поднята версия go.

Как поставить?

Информация доступна здесь github.com/svanichkin/say, можно поставить руками из релизов Releases, можно скриптом, а можно непосредственно yay -S say. Мне очень интересны отзывы о звуке на Windows и о скорости передачи файлов между реальными машинами.