1550 changed files with 22647 additions and 455335 deletions

No files matched your search

-7
View File
@@ -2,9 +2,7 @@
/build/
build-win/
tools/chatgui/build/
tools/chatgui/rlottie_build/
tools/chatgui/ffmpeg/ffmpeg_build/
tools/lightsout/build/
# Build outputs
*.o
@@ -13,8 +11,6 @@ tools/lightsout/build/
*.a
*.so
*.so.*
# Prebuilt native runtime для Android VAD (onnxruntime) — коммитим несмотря на *.so
!tools/chatgui-android/app/src/main/jniLibs/*/libonnxruntime.so
/utun
/src/utun
test_serialize
@@ -32,7 +28,6 @@ index.html
*.exe
# Autotools
/.utun-build-state*
Makefile
!tools/etcpmon/Makefile
Makefile.in
@@ -85,7 +80,6 @@ logs/
tests/test_*
!tests/test_*.c
!tests/test_*.h
!tests/test_*.py
!tests/test_*.conf
!tests/test_*.json
tests/bench_*
@@ -114,7 +108,6 @@ tools/proxy/udp_proxy
# Reference projects (not part of utun)
tools/tdesktop-dev/
/hardware/
# Debug/temp artifacts
*.bak
+105 -335
View File
@@ -25,24 +25,12 @@ This file contains essential information for AI coding agents working in the uTu
При поиске багов всегда проверяй достаточно ли отладочной информации. Если не достаточно - фокусируйся на том чтобы добавить нужную информацию в вывод.
Помни что отладка управляется фильтрами по категормяи, которые нужно проверить и при необходимости правильно настроить (в конфиге или в коде).
Для поиска выстараивай информативные логи. По которым максимально чётко понятно что происходит. Если какие-то места которые могут иметь отношение к ошибке в логах пропущены - это повод доработать логи и прогнать еще раз.
Всегда отладку выстраивай через доработку логов, пока точно и однозначно не будет видно в каком точно месте проблема.
По хорошей диагностике сразу должно быть понятно что происходит, в том числе точную причину проблемы. Если приходится догадываться или плохо понятно - это значит что надо улучшать диагностику.
Доствп на узел colo2: ssh -p44322 logs@colo2
Структура .h файлов:
- в начале краткое и понятное описание: что делает модуль, как пользоваться.
- поля структур также кратко описываем - что, для его, если есть нюансы/значения
- для функций: описываем назначение, как пользоваться, описание аргументов, возврата. кратко но понятно.
## Quick Reference
**Repository:** uTun — общее сетевое ядро ETCP, независимо запускаемые UTUN и P2P-чат
**Repository:** uTun - Secure VPN tunnel with ETCP protocol
**Language:** C (C99)
**Build System:** GNU Autotools (autoconf/automake)
**Cryptography:** OpenSSL (AES-CCM, X25519, Ed25519, SHA256)
**Cryptography:** OpenSSL (AES-CCM, X25519, SHA256)
## Build Commands
@@ -56,19 +44,9 @@ This file contains essential information for AI coding agents working in the uTu
powershell build_full.bat # запускает bash build.sh --full через MSYS2 UCRT64
```
### Build (FreeBSD)
```sh
# Первичная установка инструментов от root:
pkg install -y bash gmake autoconf automake libtool pkgconf openssl python3
./build.sh -j4 # общий скрипт; автоматически выбирает gmake и Clang
./build_bsd_full.sh -j4 # полная сборка через общий скрипт
```
Python 3 нужен для проверок скрипта сборки. SHA-256 берётся из базовой системы FreeBSD.
### Incremental Build
```bash
./build.sh -j4 # инкрементальная сборка, конфигурация и очистка при необходимости
./build.sh --no-gui -j4 # только ядро и тесты, без отдельной сборки GUI
./build.sh -j4 # make с авто-конфигурацией если надо
```
```powershell
powershell -Command ".\build.bat" 2>&1 # Windows, логи: build_win.log
@@ -83,10 +61,9 @@ make distclean # Clean everything including configure files
### Partial Builds
```bash
make -C lib # базовые библиотеки
make -C lib/libopus # встроенный Opus
make -C src # основной бинарник src/utun (библиотеки уже должны быть собраны)
make -C tests # тесты (используют объекты src/)
cd lib && make # Build only the library (libuasync.a)
cd src && make # Build only the main program
cd tests && make # Build only tests
```
### Direct Build (Windows, без autotools)
@@ -102,7 +79,7 @@ python3 tools/chatcli members <ch_id> # мемберы с полн
python3 tools/chatcli messages <ch_id> [count] # последние сообщения
python3 tools/chatcli send <ch_id> "text" # отправить сообщение
python3 tools/chatcli invite <ch_id> # создать invite-ссылку
python3 tools/chatcli connect 'utun://...' # join по ссылке
python3 tools/chatcli connect <utun://...> # join по ссылке
python3 tools/chatcli create "Name" # создать канал
python3 tools/chatcli listen # слушать события (Ctrl+C выход)
```
@@ -110,14 +87,13 @@ python3 tools/chatcli listen # слушать событи
### ASAN Build (AddressSanitizer, Linux)
```bash
./build.sh --asan -j4 # собирает lib/ + src/; тесты этот режим скрипта пропускает
./build.sh --asan -j4 # lib/ + src/ only (tests not ASAN-compatible)
```
Запуск с ASAN (detect_leaks отключены чтобы не спамить при shutdown):
```bash
ASAN_OPTIONS=detect_leaks=0:halt_on_error=0:log_path=/tmp/utun_asan ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log
ASAN_OPTIONS=detect_leaks=0:halt_on_error=0 ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log
```
В этом примере отчёты ASAN пишутся в `/tmp/utun_asan.<pid>` благодаря `log_path`.
Для отдельного теста нужно собирать его зависимости с теми же sanitizer-флагами; обычные и ASAN-объекты не смешивать.
Краш-логи ASAN пишутся в `/tmp/utun_asan.<pid>`.
## Test Commands
@@ -135,14 +111,13 @@ cd tests/
./test_etcp_two_instances
```
### Build and Run a Single Test
### Run Single Test with Debug Info
```bash
make -C tests test_services
cd tests && ./test_services
cd tests/
gcc -I../src -I../lib -I../tinycrypt/lib/include \
-o my_test test_file.c ../src/*.c ../lib/*.c ../tinycrypt/lib/source/*.c
./my_test
```
Использовать цель из `tests/Makefile.am`: ручная компиляция `src/*.c` пропускает подкаталоги и добавляет лишний `main`.
Уровни и категории диагностики настраиваются в самом тесте через `debug_set_level` / `debug_set_category_level`.
Проверки самого скрипта сборки запускаются отдельно: `make -C tests check-build-script` (нужен Python 3); в `./check.sh` они не входят.
## Code Style Guidelines
@@ -155,7 +130,7 @@ cd tests && ./test_services
### Formatting
- **Indentation:** 4 spaces, no tabs
- **Braces:** Открывающая скобка на строке функции или условия:
- **Braces:** Same line for functions, new line for control structures:
```c
int function(void) {
if (condition) {
@@ -163,7 +138,7 @@ cd tests && ./test_services
}
}
```
- **Comments:** Краткие описания на русском; сохранять принятые имена API и протокольных полей.
- **Comments:** Primary language is Russian for business logic, English for API docs
- **Line Length:** Aim for 80-100 characters, but can go up to 150 if logically coherent
### Include Order
@@ -219,7 +194,7 @@ sc_context_t ctx;
sc_init_ctx(&ctx, &my_keys);
// 2. Set peer public key (for key exchange)
sc_set_peer_public_key(&ctx, peer_public_key_bin, SC_PEER_PUBKEY_BIN); // 32 бинарных байта; HEX — для hex-строки
sc_set_peer_public_key(&ctx, peer_public_key, SC_PEER_PUBKEY_HEX); // 0=bin, 1=hex
// 3. Ready for encrypt/decrypt
sc_encrypt(&ctx, plaintext, plaintext_len, ciphertext, &ciphertext_len);
@@ -241,97 +216,62 @@ sc_obfuscate_pubkey(salt, peer_pubkey_bin, my_pubkey_bin, obfuscated_output);
### Debug Levels
`none < error < warn < info < debug < trace`
trace - логируем заходы в функции и важные ветвления внутри функций
debug - отладка: всё что нужно для понимание сути происходящего (состояний, важных переменных) алгоритма. тоесть по уровню debug мы должны видеть все важные внутреннии нюансы работы функции включая состояния и переменные.
debug - всё что нужно для понимание сути происходящего (состояний, важных переменных) алгоритма. тоесть по уровню debug мы должны видеть все важные внутреннии нюансы работы функции включая состояния и переменные.
info - сообщения для пользователя о работе. мы должны видеть в читаемом виде понятные и осмысленные сообщения о каких-либо значимых действиях с точки зрения логики работы модуля или приложения
warn / error - ошибки и предупреждения (аномалии). должны быть во всех ошибочных ветках. Молча вываливаться с ошибкой нельзя, все ошибки и предупреждения должны логироваться.
### Debug Categories
Актуальные имена и константы — в `lib/debug_config.h`, текстовые имена — в `lib/debug_config.c`.
Основные: `sys`, `connection`, `etcp`, `crypto`, `config`, `tun`, `routing`, `timers`, `bgp`,
`socket`, `control`, `dump`, `traffic`, `debug`, `general`, `nat`, `keepalive`, `etcp_route`,
`media`, `etcp_dump`, `chat`, `chat_sync`, `member_sync`, `proxy`, `video`, `reality`, `dm`, `call`, `radio`, `vad`, `aec`.
UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в ETCP; старые числовые списки не использовать.
### Debug Categories (28 категорий)
```
NONE=0, UASYNC=1, LL_QUEUE=2, CONNECTION=3, ETCP=4, CRYPTO=5, MEMORY=6,
TIMING=7, CONFIG=8, TUN=9, ROUTING=10, TIMERS=11, NORMALIZER=12, BGP=13,
SOCKET=14, CONTROL=15, DUMP=16, TRAFFIC=17, DEBUG=18, GENERAL=19, NAT=20,
KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26,
MEMBER_SYNC=27
```
### Настройка отладки
- В INI-конфиге: секция `[debug]`, каждая категория отдельной строкой (`etcp=trace`, `config=info`).
- Строку `etcp=trace,config=info` принимает `debug_parse_config()`; GUI хранит её в `[gui] debug_categories`.
- В конфиге: `debug = etcp=trace,config=info` (формат: `категория=уровень,...`)
- В коде: глобальный уровень и per-category уровни из `debug_config_t g_debug_config`
- Макросы: `DEBUG_ERROR(cat,fmt,...)` `DEBUG_WARN` `DEBUG_INFO` `DEBUG_DEBUG` `DEBUG_TRACE`
- `log_dump(level, category, prefix, data, len)` — hex dump в лог
- `log_dump(prefix, data, len)` — hex dump в лог
### Dual Output
- Фильтр уровней общий для выходов; `debug_enable_console()` включает/выключает консоль.
- Консоль и файл настраиваются раздельно (`debug_set_console_level`, `debug_set_file_level`)
- `debug_enable_file_output(path, truncate)` / `debug_disable_file_output()`
## Architecture
### Ядро, сервисы и владение ресурсами
- `utun_instance_create*()` создаёт ядро; `utun_core_start()` запускает общие сетевые службы.
- `utun_service_start/stop()` управляет UTUN-группой, TUN/NAT и запросами к пирам из конфига.
- `chat_service_start/stop()` управляет чатом и CHAT-группами. Чат не требует UTUN; desktop и Android запускают ядро и чат явно.
- Ядро владеет общей SQLite (`db_path/chats.db`, без пути — `:memory:`), идентичностью, транспортом и маршрутизатором.
Остановка сервиса сохраняет БД и ресурсы другого сервиса. `utun_instance_destroy()` освобождает ядро, но не переданный `UASYNC`.
- Группа владеет NCD handle с начала CONNECTING. Инициатор хранит отменяемый `TOPO_PEER_REQUEST`;
закрытие своего запроса не разрывает транспорт других владельцев. Готовая сессия сохраняется у группы.
- UP означает работоспособный транспорт. READY означает завершённое согласование и обмен таблицей конкретной группы.
Recovery ждёт нужных маршрутов через READY-пиров; наличие `ETCP_CONN*` само по себе не гарантирует UP.
- JOIN добавляет подписанного мембера, Merkle распространяет запись. Доступ к CHAT-группе каждый узел даёт по своей таблице участников;
invite-ключ не даёт временного членства. Протокол описан в `src/chat/chat_join.h`, групповое согласование — в `src/routing_layer/topo_group.h`.
- `TOPO_NODE` — общая подписанная запись узла. Более свежий timestamp заменяет запись целиком, с полями и подписью;
адреса не версионируются отдельно. BGP обновляет запись и передаёт изменения NCD (см. `doc/node_snapshot.md`).
- BGP хранит один путь к назначению от каждого соседа. WITHDRAW удаляет только путь отправителя;
NODEINFO с прежним timestamp также обновляет путь. Выбор: живой путь с минимумом хопов, затем меньший ID соседа.
REINIT сохраняет пути до проверки нового снимка; TABLE_COMPLETE удаляет неподтверждённые, зависший обмен перезапускается.
- ETCP-router дописывает ID при транзите и отбрасывает повторное посещение для DATA/ACK/control.
Список переходов находится вне подписанного тела; формат описан в `src/routing_layer/etcp_router.h`.
Все операции с ядром выполняются в его uasync-потоке; stop/destroy — вне callbacks останавливаемых сервисов.
Порядок освобождения ресурсов и нюансы media workers: `doc/service_lifecycle.md`.
### Directory Structure
```
├── lib/ # uasync, очереди, SQLite, Opus, SpeexDSP, Silero VAD, SoundTouch, LMDB
├── lib/ # Core libraries (18 .c + 20 .h + libopus/liblmdb)
├── src/ # Main source code
│ ├── transport_layer/ # ETCP, сессии, crypto, STCP/REALITY, SOCKS-клиент, NCD, BBR
│ ├── routing_layer/ # Routing, группы/BGP, recovery, conn_mgr, etcp_router
│ ├── chat/ # P2P чат, join, db_sync, merkle_sync, member_sync
│ ├── dm/ # Личные сообщения, E2E-вложения и offline-mailbox
│ ├── call/ # Звонки и общий голосовой стек
│ ├── radio/ # Групповая PTT-рация
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек
│ ├── media_delivery/ # Блоки медиа каналов и передача неизменяемых файлов
│ ├── media_async/ # Фоновые задачи, метаданные вложений, подготовка голоса/видео
│ ├── video/ # Пробинг и транскодирование видео через FFmpeg
│ ├── dnsmasq/ # Архив исходников для встроенной сборки dnsmasq
│ └── *.c/h # utun, instance, config, TUN, firewall, NAT, NTP, control, broadcast
├── tests/ # Состав тестов и цели запуска: tests/Makefile.am
│ ├── transport_layer/ # ETCP stack, crypto, STCP, normalizer, BBR (~30 файлов)
│ ├── routing_layer/ # Routing, BGP, conn_mgr, etcp_router, NAT detection (~20 файлов)
│ ├── chat/ # P2P чат, db_sync, merkle_sync, member_sync (17 файлов)
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси (10 файлов)
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек (7 файлов)
│ ├── media_delivery/ # Доставка медиафайлов (6 файлов)
│ ├── media_async/ # Асинхронный движок медиа (2 файла)
│ └── *.c/h # Корневые модули: utun, config, TUN, firewall, NAT, NTP, control (~14 файлов)
├── tests/ # ~70 тестов, 64 в check_PROGRAMS
├── doc/ # Technical Specifications
├── tools/ # Auxiliary tools
│ ├── etcpmon/ # GUI монитор ETCP
│ ├── chatgui/ # GUI чат (Qt 6 с Qt 5 fallback, сборка CMake)
│ ├── chatgui-android/ # Android P2P чат (Jetpack Compose + libutun_lite)
│ ├── logreceiver/ # Приём UDP-логов Android
│ ├── chat_tcp_test/ # Сценарий проверки чата через TCP
│ ├── lightsout/ # Отдельная игра Lights Out на Qt
│ ├── lightsout-android/ # Android-версия Lights Out
│ ├── tdesktop-dev/ # Референс: Telegram Desktop (только для изучения)
│ ├── proxy/ # UDP прокси для тестов
│ ├── bping/ # BPing (bandwidth ping)
│ └── chatcli # Headless CLI; описание команд — chatcli_commands.txt
│ └── bping/ # BPing (bandwidth ping)
├── tinycrypt/ # TinyCrypt crypto library (external)
└── net_emulator/ # Network emulator (delays, loss, reordering)
├── net_emulator/ # Network emulator (delays, loss, reordering)
└── c2/ # Test instance 2 (конфиг и бинарник для тестов)
```
### File Overview
Пути ниже относительны к каталогу секции; `name.c/h` означает пару `.c` и `.h`.
Перечислены модули проекта; внутренние файлы сторонних библиотек сгруппированы по каталогам.
**Core (src/)**
- `utun.c` - Main program entry point, CLI parsing, daemon mode
- `utun_instance.c/h` - Создание ядра и независимый жизненный цикл UTUN/чата, владение общими ресурсами
- `utun_instance.c/h` - Root instance lifecycle, config loading, all submodule init
- `tun_if.c/h` - TUN interface API (init/write/close, cross-platform)
- `tun_linux.c` `tun_freebsd.c` `tun_windows.c` - Platform-specific TUN implementations
- `tun_route.c/h` - TUN routing table sync
@@ -343,22 +283,16 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `nat_transport.c/h` - NAT transport layer (packet relay)
- `ntp_time.c/h` - NTP time synchronization
- `ntp_node_time.c/h` - Inter-node time synchronization
- `broadcast.c/h` - Широковещательная рассылка данных по topo-группе (UUID-дедупликация, TTL)
- `video/video.c/h` - Пробинг и транскодирование видео (FFmpeg, опционально; H.264/MP4 ≤720p)
- `dnsmasq/` - Архив dnsmasq-2.93.tar.xz для встроенной сборки
**Transport Layer (src/transport_layer/)**
- `etcp.c/h` - ETCP protocol implementation (inflight queues, retrans, ACK, RTT/jitter)
- `etcp_api.c/h` - ETCP public API (send/recv/bind callbacks)
- `etcp_connections.c/h` - Socket and link management, INIT handshake, keepalive, PING/PONG
- `etcp_session.c/h` - Согласование эпох ETCP-сессии через HELLO/CHALLENGE/CONFIRM и проверка принадлежности DATA сессии
- `etcp_loadbalancer.c/h` - Multi-link load balancing with traffic shaper
- `etcp_debug.c/h` - ETCP packet dump/formatting
- `etcp_dump.c/h` - ETCP дамп/декодирование пакетов
- `etcp_bbr.c/h` - BBR congestion control for ETCP
- `etcp_connect.c/h` - ETCP outbound connections
- `etcp_keepalive.c/h` - Keepalive-пакеты, адаптивный период, смена режима standby/normal (мобильные узлы)
- `auto_socket.c/h` - Автосоздание UDP+TCP сокетов на всех интерфейсах и линков к ETCP-соединениям (`auto_sockets=yes`)
- `pkt_normalizer.c/h` - Packet fragmentation/reassembly (packer/unpacker)
- `packet_dump.c/h` - Packet hex dump utility
- `dummynet.c/h` - Network emulator integrated into utun (for testing)
@@ -368,13 +302,8 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `stcp_server.c/h` - STCP сервер
- `stcp_client.c/h` - STCP клиент
- `stcp_link.c/h` - STCP линк-уровень
- `socks_client.c/h` - Исходящий SOCKS5-транспорт: TCP CONNECT и UDP ASSOCIATE с переподключением control-сокета
- `node_conn_direct.c/h` - Общий прямой транспорт к пиру с отдельным handle каждого владельца; обновление линков по адресам узла
- `node_conn_direct.c/h` - Direct config-based connections
- `socket_monitor.c/h` - Socket state monitoring
- `reality.c/h` - REALITY-style TLS ClientHello/ServerHello камуфляж для STCP
- `reality_fingerprint.c/h` - Статические отпечатки (cipher suites, groups, ALPN) для REALITY-камуфляжа
- `reality_relay.c/h` - Релей неавторизованных клиентов на реальный HTTPS-сайт
- `call_ring.c/h` - Кольцевой трейс контрольных точек освобождения ETCP/STCP-ресурсов для диагностики teardown/UAF
**BBR (src/transport_layer/BBR/)**
- `bbr_v3.c` - BBR congestion control algorithm v3
@@ -383,78 +312,38 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `routing.c/h` - Routing table (local)
- `route_lib.c/h` - Routing library
- `route6_lib.c/h` - IPv6 routing
- `route_ping.c/h` - Пинг цели через явно выбранного BGP-посредника для NAT-проверки
- `route_ping.c/h` - Route ping probing (NAT check, liveness)
- `route_connectivity.c/h` - Route connectivity checks
- `topo_group.c/h` - Сессии UTUN/CHAT (JOIN/ACCEPT, эпохи, READY), групповые запросы, NODEINFO/WITHDRAW и владение NCD
- `topo_group_connect.c/h` - Подбор пиров CHAT-группы по роли: суперузлы → desktop → mobile; история/RTT внутри класса, backoff и standby burst
- `topo_group_invite.c/h` - Получение описания канала через INVITE_INFO_REQ/RESP; добавление мембера реализовано в chat_join/chat_sync
- `topo_group.c/h` - BGP-like route exchange between nodes (NODEINFO/WITHDRAW, изолированные группы UTUN/CHAT)
- `topo_group_connect.c/h` - Авто-подключение к узлам CHAT-группы (3 фазы, бесконечный цикл)
- `topo_group_invite.c/h` - Invite/join канала через прямое ncd-подключение (INVITE_INFO_REQ/RESP)
- `topo_node.c/h` - Node management (модель узла: TOPO_NODE/TOPO_GROUP_NODE, сериализация, node_registry)
- `topo_node_sqlite.c/h` - SQLite persistence for nodes
- `topo_recovery.c/h` - Восстановление потерянных маршрутов группы через READY-сессии, отмена попыток при stop
- `topo_recovery.c/h` - Восстановление каскадно отвалившихся узлов после разрыва соединения
- `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND)
- `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding)
- `nat_detection.c/h` - NAT type detection
- `sock_match.c/h` - Классификация локальных сокетов и адресов пира, отбор совместимых пар для прямого подключения
- `route_crypto.c/h` - Пред/пост-обработка SVC_ROUTE пакетов (encrypt-then-sign)
**Chat (src/chat/)** — децентрализованный P2P чат: сообщения и участники автоматически синхронизируются между всеми узлами канала без центрального сервера.
- `chat_core.c/h` - API сообщений, каналов, профиля и настроек; chat_service_start/stop управляют чат-сервисом поверх общей БД ядра
- `chat_core.c/h` - Главный модуль чата. Единственная точка входа из GUI: принять сообщение, создать канал, изменить настройку. Управляет БД и жизненным циклом всех chat-модулей
- `chat_core_priv.h` - Внутренний API для частей chat_core: общий контекст и утилиты БД. Снаружи не используется
- `chat_sync.c/h` - Связывает чат с ETCP-сетью: отслеживает появление/разрыв соединений, обновляет онлайн-статус узлов, запускает синхронизацию участников каналов, обрабатывает приглашения
- `chat_event.c/h` - Доставка событий из ядра чата в GUI. Единый механизм: любой модуль отправляет событие (новое сообщение, смена участников, статус), GUI получает через один обработчик
- `chat_setting.c/h` - Проверка и хранение настроек чата per-instance; общая логика для конфигурации и GUI
- `chat_join.c/h` - Регистрация invite-ключа с ACK, пересылка запроса инвайтеру и добавление подписанного мембера; контракт join в заголовке
- `db_sync.c/h` - Репликация подписанных записей SQLite через прямые ETCP-соединения; расхождения по цепочке хешей, новые записи через PUSH
- `chat_setting.c/h` - Настройки чата: проверяет корректность значений, хранит текущие. Используется и headless-парсером конфига, и GUI. Не зависит от других модулей
- `db_sync.c/h` - Автоматическая репликация данных между подключёнными узлами. Запись на одном узле — появляется у всех остальных. Криптоподписи и цепочка хешей защищают от подделок
- `member_sync.c/h` - Синхронизация участников каналов поверх merkle_sync. Два подписанных блока (мембер/владелец), по-блочное сравнение версий, верификация подписей. Удаление — только битая запись
- `merkle_sync.c/h` - Универсальный движок синхронизации на Merkle-деревьях (5 уровней × 32 бакета, SHA256). Пиры обмениваются хешами и передают только различия; per-session out-очередь с backpressure. Не привязан к конкретному типу данных (коллбэки `data_ops`)
- `merkle_tree.c/h` - Производный индекс Merkle-хешей в SQLite; восстанавливается из данных модели, обновляется с ними в одной транзакции
- `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core)
- `chat_channel.c` - Создание и настройка каналов: таблицы, криптоключи, CHAT-группа и планировщик подключения (часть chat_core)
- `chat_channel.c` - Создание и настройка каналов: готовит таблицы в БД, генерирует криптоключи, запускает подключение ко всем участникам (часть chat_core)
- `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core)
- `chat_status.c` - Сбор диагностики: текущее время, активные соединения, типы NAT. Отправляется в GUI (часть chat_core)
- `chat_member.c/h` - Единая структура мембера для отображения в GUI (десктоп/headless/Android), фиксированная сериализация на проводе
- `chat_admin.c/h` - Передача канального приватного ключа (прав админа) другому узлу
- `chat_whisper.c/h` - Whisper speech-to-text транскрипция голосовых сообщений (опционально, асинхронно через media_async)
- `chat_headless_control.c/h` - TCP control-socket для headless chat CLI (JSON line protocol)
- `invite_build.c/h` - Сборка invite-ссылок utun:// (выбор лучшего узла + адреса)
- `invite_link.c/h` - Кодирование utun://: версия, пароль, channel_id, join_key, Reality-параметры, ключ и адреса узла
**Личные сообщения (src/dm/)**
- `dm_core.c/h` - Беседы двух узлов, подписанные E2E-сообщения, SQLite/outbox, доставка через router/mailbox и квитанции
- `dm_crypto.c/h` - Вывод ID беседы и ключа X25519, AES-256-CCM сообщений и потоковое шифрование/проверка медиа
- `dm_mailbox.c/h` - Offline-очередь суперузла: хранение подписанных сообщений и повтор доставки до квитанции получателя
- `dm_media.c/h` - Подготовка, доставка и публикация E2E-вложений; ciphertext-хранилище и отдельные квитанции файлов
- `dm_media_priv.h` - Внутренние SQL/control-операции для dm_media и dm_mailbox_media
- `dm_mailbox_media.c/h` - Сохраняемые задания суперузла для размещения медиа, доставки метаданных и удаления копий до DELETE_ACK
**Звонки (src/call/)**
- `call.c/h` - Сигналинг и медиа P2P-звонка через ETCP-router, состояния звонка и оптимизация пути через conn_mgr
- `call_proto.h` - Wire-формат сигналинга и медиа, треки/кодеки, причины завершения
- `call_audio.c/h` - Общий аудио-движок: кодирование/декодирование Opus, jitter-буфер и тоны, API feed/pull PCM
- `call_jitter.cpp/h` - FIFO кодированных Opus-кадров, адаптивный запас и SoundTouch time-stretch при воспроизведении
- `call_jitter_window.h` - Окно из десяти секундных интервалов для оценки глубины очереди и изменения задержки доставки
- `call_tones.c/h` - Генерация PCM-тонов паузы и завершения звонка
- `call_headless.c/h` - Отдельный TCP-аудиосокет headless-клиента: обмен PCM; сигналинг остаётся на control-сокете
**Рация (src/radio/)**
- `radio.c/h` - Передача Opus по CHAT-группе, подписки, дедупликация и пересылка по ветвям с подписчиками
- `radio_proto.h` - Wire-формат PTT: источник, поток, seq, FIN и hop-list против циклов
- `radio_audio.c/h` - Общий аудио-движок: независимые RX/TX, Opus, микширование источников, ручной PTT и VAD-автомат
- `radio_jitter.cpp/h` - Буфер кодированных кадров каждого источника с pre-roll и SoundTouch time-stretch на pull
- `radio_vad.h` - Ресемплинг 48→16 кГц и автомат авто-PTT с подтверждением речи, задержкой завершения и учётом занятого канала
- `radio_headless.c/h` - TCP-аудиосокет headless-рации: PCM и команды начала/конца PTT
**Прокси (src/proxy/)**
- `proxy_protocol.h` - Общий TCP-протокол и управление потоком: CONNECTED, DATA, кредит WINDOW, упорядоченный FIN
- `socks_proxy.c/h` - Клиентские SOCKS5 CONNECT и HTTP proxy/CONNECT, DNS и передача потока выбранному exit-узлу
- `socks_proxy.c/h` - SOCKS5 прокси
- `udp_proxy.c/h` - UDP прокси
- `icmp_proxy.c/h` - ICMP прокси
- `tcp_proxy_server.c/h` - Exit-сторона TCP-прокси: реальные исходящие сокеты, потоки по (peer, stream_id), окна и half-close
- `tcp_proxy_client.c/h` - Клиентская сторона TCP-прокси: адаптер TUN/lwIP и обмен с настроенным exit через ETCP-router
- `tcp_proxy_server.c/h` - TCP прокси (серверная сторона)
- `tcp_proxy_client.c/h` - TCP прокси (клиентская сторона)
**lwIP TCP (src/lwip_tcp/)**
- `lwip_tcp.c/h` - lwIP TCP стек
@@ -463,50 +352,32 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `lwip_tcp_priv.h`, `lwip_tcp_opts.h` - Внутренние настройки lwIP
**Media Delivery (src/media_delivery/)**
- `media_delivery.c/h` - Доставка блоков медиа каналов, учёт держателей, репликация доступности между суперузлами и relay
- `media_delivery_proto.h` - Кодограммы канального медиа: QUERY, BLOCK_REQ/CHUNK/DONE, HAVE_BLOCK и SUPER_REPL
- `media_download.c/h` - Загрузка блоков с выбором держателей, failover, контролем простоя, проверкой подписей и сборкой файла
- `media_index.c/h` - Индекс media_files: UUID, хеши и подписи блоков, асинхронная регистрация и удаление медиа канала
- `file_transfer.c/h` - Передача неизменяемого файла между заданными узлами; авторизация lookup и владение CM handle до завершения
- `media_delivery.c/h` + `media_delivery_proto.h` - Доставка медиафайлов, протокол
- `media_download.c/h` - Скачивание медиа
- `media_index.c/h` - Индексация медиафайлов
**Media Async (src/media_async/)**
- `media_async.c/h` - Фоновые crypto/file-задачи: work в pthread, done в uasync; destroy ждёт workers и завершает ожидающие callbacks с CANCELLED
- `attachment.c/h` - Общие метаданные file/voice/video, проверка и компактная wire-сериализация
- `voice_file.c/h` - Worker-кодирование PCM в контейнер Opus с длительностью и waveform для desktop/Android
- `attachment_send.c/h` - Асинхронная подготовка голоса/видео и отправка в зафиксированный канал или DM
- `media_async.c/h` - Асинхронный движок (thread-per-task для crypto/file операций)
**Libraries (lib/)**
- `u_async.c/h` - Async event loop (epoll/poll/select, timers via timeout_heap)
- `ll_queue.c/h` - Двусвязная очередь одного uasync-потока с callbacks, хеш-индексом и ожиданием свободного места
- `memory_pool.c/h` - Пул объектов: ленивое выделение и повторное использование, до 64 свободных объектов в кеше
- `ll_queue.c/h` - Lock-free linked list queue with callbacks, hash index, threshold waiter
- `memory_pool.c/h` - Fast object pool allocator (pre-allocated blocks)
- `debug_config.c/h` - Debug logging system (levels, categories, dual output, runtime config)
- `timeout_heap.c/h` - Min-heap for timer management (used by u_async)
- `mem.c/h` - Memory wrappers with leak tracking (`u_malloc`/`u_free`/`u_calloc`/`u_strdup`)
- `serialize.c/h` - Binary serialization utilities
- `reed_solomon.c/h` - Кодирование и восстановление пачки данных Рида—Соломона с настраиваемым числом субблоков и избыточностью
- `swm_min.c/h` - Sliding window minimum (for RTT min tracking)
- `getmyip.c/h` - Локальный IP, который ОС выберет для UDP к заданному адресу (bind/connect/getsockname)
- `getmyip.c/h` - Get local IP / default route detection
- `myip.c` - My IP helper (no header)
- `socket_compat.c/h` - Cross-platform socket compatibility (Linux/FreeBSD/Windows)
- `platform_compat.c/h` - Cross-platform compatibility layer (byte order, time, random)
- `radix.c/h` - Radix tree for IP routing lookups
- `tcp_io.c/h` - Неблокирующий TCP поверх uasync/ll_queue: backpressure, FIN/close и callbacks
- `tcp_io.c/h` - TCP I/O abstraction (Windows IOCP, Linux epoll)
- `sqlite3.c/h` - SQLite3 amalgamation (embedded database)
- `opus_codec.c/h` - Opus audio codec wrapper
- `audio_compressor.c/h` - Audio dynamic range compressor
- `speex_aec.c/h` - Эхоподавление SpeexDSP: согласование playback/capture PCM через линию задержки и диагностика её заполнения
- `silero_vad.c/h` - Стриминговый VAD через ONNX Runtime: окна 512 отсчётов, 16 кГц моно, вероятность речи и сброс состояния
- `silero_vad_model.inc` - Встроенная ONNX-модель Silero VAD для silero_vad_create_default
- `json_flat.c/h` - Flat JSON parser
- `strbuf.c/h` - Безопасный растущий printf-буфер (замена snprintf-цепочек)
- `async_dns.c/h` - Асинхронный (неблокирующий) DNS-резолвер A-записей поверх uasync, адаптер над libdns
- `dns.c/h` - Рекурсивный reentrant DNS-резолвер (libdns, MIT)
- `miniaudio.h` - Воспроизведение/захват аудио (single-header)
- `dr_mp3.h` - MP3-декодер (single-header, на базе minimp3)
- `liblmdb/` - Встраиваемый key-value store LMDB
- `libopus/` - Встраиваемый кодек Opus
- `speexdsp/` - Встроенные исходники эхоканселлера SpeexDSP и KISS FFT
- `soundtouch-master/` - SoundTouch: изменение темпа аудио в jitter-буферах звонка и рации
- `wintun.h` - Wintun API header for Windows TUN driver
**Memory Pools in UTUN_INSTANCE:**
@@ -516,7 +387,7 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
### Key Components
- **UASYNC:** One per thread. Async event loop (epoll/poll), timers via timeout_heap
- **LL_QUEUE:** Очередь с auto-callback, хеш-индексом и threshold waiter; не средство обмена между потоками
- **LL_QUEUE:** Lock-free queue with auto-callback, hash index lookup, threshold waiter
- **Memory Pool:** Fast allocation for hot-path objects (packets, inflight entries, fragments)
- **ETCP:** TCP-like reliable protocol with encryption, multi-link, load balancing
- **Secure Channel:** AES-CCM + X25519 key exchange, nonce-based encryption, pubkey obfuscation
@@ -532,8 +403,6 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- Очереди забивать нельзя. Добавляй следующий элемент только когда очередь стала пустой.
- Порог задаётся через `queue_set_threshold(q, max_packets, max_bytes)`.
- Используй `queue_waiter_wait` для ожидания освобождения очереди до заданного порога.
- Handle ожидания обнуляется целиком, живёт до callback/отмены и отменяется до освобождения владельца.
Callback может быть синхронным. `queue_entry_free` не освобождает dgram — его освобождают отдельно.
### Чтение из очереди
- Используй `queue_set_callback`: при вызове callback обработай один или несколько элементов, потом вызови `queue_resume_callback`.
@@ -553,13 +422,12 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
## Config Rules
- В серверном конфиге только собственные ключи и нет секций `[client]`
- В клиентском конфиге есть собственные ключи и pubkey каждого сервера в секции `[client]`
- Chat-настройки хранятся в `[chatserver]` (также `[chat]`). Парсер обрабатывает общие поля секции,
затем вызывает `chat_setting_set_state(&cfg->global.chat_settings, key, value)`; неизвестные ключи логируются как ошибка. Пример:
- Chat-настройки хранятся в секции `[chat]`. Парсер вызывает `chat_setting_set(key, value)` для каждого ключа — неизвестные ключи вызывают ошибку. Пример:
```ini
[chatserver]
[chat]
storage_autoload=1
storage_unit_size=10M
storage_total_size=1G
storage_autoload_maxsize_mb=10
storage_maxsize_gb=1
opus_codec_preset=1
compressor_enabled=0
compressor_max_gain_db=25
@@ -577,8 +445,7 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
Если видишь спам-логи - подумай как их выборочно отключить чтобы не мешали.
Для отладки добавляй в DEBUG_CATEGORY_DEBUG диагностические сообщения где надо. И включи эту категорию в настройках. Убирай только после проверки (путём запуска) когда все ошибки устранены.
Явный уровень категории заменяет глобальный. DEBUG_LEVEL_NONE наследует глобальный уровень;
DEBUG_LEVEL_DISABLED полностью отключает категорию, в том числе при глобальном TRACE (см. debug_should_output).
сообщение выводится по или - либо debug_set_level(DEBUG_LEVEL_TRACE) - выводится ВСЁ независимо от настроек по категориям. Тоесть берется max(global level, category lavel)
Твой бич - ты постоянно гадаешь и анализируешь код. Что приводит к снежному кобу ошибок и неверных гипотез. 10 раз повторяю - только логи логи логи и никакого гадания. Лоооги!!! правильные логи покажут всё с предельной точностью. Вся суть отладки - информативные логи
И смотри логи. не задавливай их grep-ом. лучше больше. единственное с чем борись - это бесполезные спам логи.
@@ -588,8 +455,7 @@ DEBUG_LEVEL_DISABLED полностью отключает категорию,
Но полезные смотри всегда, и всегда оставляй логи которые выводятся нечасто.
Частые логи - это трафик которые >100 раз повторяются. Но которые мало раз обязательно оставлять и выводить.
В тесте можно временно поднять нужные категории до TRACE, сохранив прежние уровни и восстановив их после участка.
Одного debug_set_level(DEBUG_LEVEL_NONE) недостаточно для отключения категорий с явно заданным уровнем.
можешь в тесте включить логи в нужный интервал чтобы не спамить. debug_set_level(DEBUG_LEVEL_TRACE) и выключить debug_set_level(DEBUG_LEVEL_NONE)
Если видишь проблему и ее решение неочевидно то выстраивай диагностику вокруг неё пока не будет очевидно где и что происходит не так. Это базовое и обязательное требование к отладке.
Подробные логи ты должен выводить не обрезая всегда и анализировать. Если лог большой - выведи в файл и анализируй файл.
Рассуждение должно быть примерно таким:
@@ -631,7 +497,7 @@ DEBUG_LEVEL_DISABLED полностью отключает категорию,
- Нельзя ничего восстанавливать из репозитория не спрашивая
- Для отладки не printf а DEBUG_*
- Запрещено прямое включение arpa/inet.h, sys/time.h, unistd.h — использовать ../lib/platform_compat.h + socket_compat.h
- Меняй режим и флаги сборки через `./build.sh`: он отслеживает конфигурацию и очищает объекты только при необходимости.
- Перед сборкой всегда make clean
- Все лишнее что менял при отладке - строго вернуть назад в состояние до вмешательства
## Написание кода
@@ -640,17 +506,12 @@ DEBUG_LEVEL_DISABLED полностью отключает категорию,
- Обязательно наличие подробной диагностики в функциях кода которые не сильно спамят (не часто вызываются).
- Для каждого отладочного вывода подумай какая из доступной информация будет полезна чтобы можно было наиболее завершенно оценить состояние алгоритма и состояний влияющих на алгоритм.
## Оформление кода
- в .c: перед каждой функцией - краткое но понятное описание что делает функция (кроме совсем простых)
- в .h: в начале - описание модуля - для чего предназначен, как пользоваться, нюансы. Далее структуры с комментариями, далее публичные функции с описаниями (что делает, как пользоваться, какие аргументы, возврат, нюансы работы)
## chatgui (GUI Chat Client)
Chatgui (бинарник `vibechat`) — десктопный чат на Qt 6 (Qt 5 fallback), отдельная CMake-сборка общего ядра.
Корневой `build.sh` дополнительно вызывает её, если уже существует `tools/chatgui/build/CMakeCache.txt`.
Chatgui — десктопный GUI-чат на Qt 6 (Qt 5 fallback), отдельный проект внутри репозитория. Не связан с autotools-сборкой utun.
GUI работает в Qt-потоке, ядро и чат — в uasync-потоке. Команды передаются через
`gui_bridge_post_uasync()` / `uasync_post()`, события возвращаются через Qt-сигналы.
В chat gui интегрированы библиотеки utun. Чат и библиотеки работают в разных потоках. Поэтому нужно использовать семафоры, сокеты или другие механизмы синхронизации (uasync_post, uasync_memsync, uasync_get_wakeup_fd)
База данных sqlite в chatgui: можно писать (update) из потока utun / instance. можно только читать из gui потока.
@@ -658,37 +519,37 @@ GUI работает в Qt-потоке, ядро и чат — в uasync-пот
- **Язык:** C++20
- **Фреймворк:** Qt 6 (предпочитаемый) или Qt 5.15+ (Widgets + Network)
- **Сборка:** CMake 3.16+
- **БД:** SQLite3 (WAL mode, компилируется общий `lib/sqlite3.c`)
- **БД:** SQLite3 (WAL mode, встроенный `db/sqlite3.c` amalgamation)
- **Анимации:** rlottie (C API) + zlib (gzip-декомпрессия TGS)
### Сборка
```bash
# Установка зависимостей
sudo apt install librlottie-dev zlib1g-dev qt6-base-dev libssl-dev libx11-dev libxfixes-dev libpipewire-0.3-dev
sudo apt install librlottie-dev zlib1g-dev qt6-base-dev
# или qtbase5-dev для Qt 5 fallback
# Сборка
cd tools/chatgui && mkdir -p build && cd build
cmake .. && make -j4
./vibechat
./chatgui
```
### Конфиг и логи чатгуи
- **Конфиг:** `vibechat.cfg` рядом с бинарником (обычно `tools/chatgui/build/vibechat.cfg`, содержит приватные ключи)
- **Конфиг:** `tools/chatgui/build/chatgui.cfg` (в `.gitignore`, содержит приватные ключи)
- Секция `[gui]`: `debug_file`, `debug_level`, `debug_categories=cat=level,...`
- Секция `[control]`: `ip`, `port` для подключения etcpmon
- Секция `[ntp]`: синхронизация времени
- **Лог:** `tools/chatgui/build/chatgui.log` (путь задаётся в конфиге `debug_file`)
- **БД чата:** `chats.db` в каталоге `[gui] db_path` (по умолчанию `chat_data/` рядом с бинарником)
- **БД чата:** `tools/chatgui/build/chat_data/` (SQLite, путь из `db_path` в конфиге)
### Структура файлов (src/)
| Файл | Назначение |
|------|-----------|
| `main.cpp` | Точка входа, инициализация QApplication |
| `mainwindow.h/cpp` | Главное окно, списки каналов/DM/участников, сообщения, трей, события ядра и окна звонков |
| `mainwindow.h/cpp` | Главное окно, QSplitter (ChannelList \| MessageList \| AccountList), трей |
| `chatview.h/cpp` | QListView с фоновым изображением, hover-сигнал, drag-to-select |
| `messagelist.h/cpp` | Модель сообщений из БД, обновления доставки/медиа, анимации, состояние просмотра канала |
| `messagelist.h/cpp` | QStandardItemModel, stub-данные, инициализация анимаций, hover→activate |
| `messagedelegate.h/cpp` | QStyledItemDelegate: бабблы, текст, цитаты, реакции, оверлей анимаций |
| `emoji.h/cpp` | EmojiData/EmojiCategory/EmojiType, 4 категории, `g_animatedEmojiMap` |
| `emojipanel.h/cpp` | Эмодзи-пикер: QTabWidget + QGridLayout, EmojiButton с hover-анимацией |
@@ -697,70 +558,20 @@ cmake .. && make -j4
| `lottieicon.h/cpp` | Загрузка TGS (gzip→zlib), рендер кадров через `lottie_animation_render` |
| `animtimer.h/cpp` | Синглтон-таймер 30fps, refcount activate/deactivate, сигнал ticked |
| `channellist.h/cpp` `channeldelegate.h/cpp` | Список каналов с аватарами и превью |
| `accountlist.h/cpp` `accountdelegate.h/cpp` | Список аккаунтов/пользователей и его делегат отрисовки |
| `memberlistmodel.h/cpp` | Модель списка участников канала |
| `memberpropsdialog.h/cpp` | Диалог свойств участника |
| `flagpainter.h/cpp` | Отрисовка иконок мембера (admin/mod/supernode/storage) |
| `accountlist.h/cpp` | Список аккаунтов/пользователей |
| `joindialog.h/cpp` | Диалог присоединения к группе/каналу |
| `invitedialog.h/cpp` | Диалог приглашения пользователя |
| `invite_link.h/cpp` | Логика invite-ссылок |
| `inviteby.h/cpp` | Диалог приглашения по invite-ссылке (отправка через узел) |
| `settingsdialog.h/cpp` | Окно настроек |
| `creategroupdialog.h/cpp` | Диалог создания группы |
| `channelsettingsdialog.h/cpp` | Диалог настроек канала |
| `renamedialog.h/cpp` | Диалог переименования |
| `networksettingspage.h/cpp` | Страница сетевых настроек |
| `nodespage.h/cpp` | Таблица узлов + дамп в настройках |
| `statuspage.h/cpp` | Страница статуса (NTP + соединения + живые часы) |
| `databasesettingspage.h/cpp` | Страница обслуживания БД в настройках |
| `storagesettingspage.h/cpp` | Страница настроек хранилища |
| `soundsettingspage.h/cpp` | Страница настроек звука |
| `audiodevicesettingspage.h/cpp` | Страница настроек аудиоустройств |
| `sound_manager.h/cpp` | Общий miniaudio-контекст, уведомления/PCM, микширование голоса, маршруты устройств и восстановление |
| `audio_device.h/cpp` | Жизненный цикл miniaudio-устройства, прогресс callbacks, пауза и повтор запуска после сбоя |
| `audio_event_queue.h` | Ограниченная очередь PCM/команд с несколькими producers и резервом для управляющих событий |
| `audio_frame_ring.h` | Предвыделенное SPSC-кольцо кадров переменной длины под mutex; при переполнении удаляет старейший |
| `voice_audio_io.h/cpp` | Workers захвата/AEC и воспроизведения; упорядочивание PCM/PTT/mute и итоговый render-reference |
| `audiorecorder.h/cpp` | Захват голосовых сообщений, worker обработки PCM, компрессор, индикатор уровня и передача записи задаче |
| `voicemessageencoder.h/cpp` | Выбор Opus preset и создание медиа-каталога; кодирование выполняет общий voice_file |
| `voiceplayback.h/cpp` | Воспроизведение голосовых сообщений |
| `callwindow.h/cpp` | Окно звонка: состояния, управление, путь соединения и статистика аудио |
| `call_audio_engine.h/cpp` | Desktop-контур звонка: захват, VoiceAudioIo, общий выход SoundManager и завершение аудиосессии |
| `radio_audio_engine.h/cpp` | Desktop-контур рации: независимые захват/прослушивание, PTT/VAD и подключение к общему выходу |
| `radiopanel.h/cpp` | Панель активной рации: выбор аудиоустройств, VAD, PTT и состояние передачи из ядра |
| `radiosettingspage.h/cpp` | Настройки рации и запись глобальных сочетаний PTT |
| `ptt_hotkey_manager.h/cpp` | Глобальный PTT: физические клавиши Windows/X11, системный портал Wayland |
| `ptt_key_chord.h` | Хранение физических сочетаний с отдельными левыми/правыми модификаторами и запись одновременного нажатия |
| `ptt_portal.h/cpp` | D-Bus-сессия GlobalShortcuts для Wayland, настройка сочетаний и обработка нажатий/отпусканий |
| `media_blocks.h/cpp` | Чтение/сборка фрагментированных медиафайлов |
| `imageviewer_window.h/cpp` | Окно просмотра изображений |
| `videoplayer_engine.h/cpp` | Движок декодирования видео |
| `videoplayer_window.h/cpp` | Окно просмотра видео |
| `connmonitorwindow.h/cpp` | Окно мониторинга ETCP-соединений (standalone) |
| `qrcode_utils.h/cpp` | Утилиты для QR-кодов |
| `debug_ui.h` | Отладочный UI |
### Другие поддиректории
- `db/` — `db_manager.h/cpp`: доступ GUI к данным nodes, channels, msg_<channel>, peers_<channel> и состоянию интерфейса;
копия `db/sqlite3.c/h` не используется текущей CMake-целью, SQLite берётся из `lib/`
- `transport/` — интеграция GUI с ядром; файлы перечислены ниже
- `libutun/CMakeLists.txt` — состав и зависимости CMake-сборки общего ядра из `src/` и `lib/`
- `CMakeLists.txt` — приложение vibechat, Qt-зависимости, ресурсы и цели GUI-тестов
- `tests/` — проверки аудиоустройств и восстановления, голосового тракта, jitter-буферов, PTT, кодирования и GUI-компонентов
- `db/` — SQLite3 amalgamation (`sqlite3.c/h`), `db_manager.h/cpp` (схема: nodes, node_addresses, channels, msg_<channel>, peers_<channel>, local_identity, accounts, ui_state)
- `transport/` — интеграция с uTun: `gui_bridge.h/gui_bridge_impl.cpp` (Qt signal-based обмен), `chat_core.h/c` (управление), `chat_sync.h/c` (синхронизация), `db_sync_stub.c` (заглушка БД), `node_config.h/cpp`, `config_updater.h/cpp`, `utun_node.h/cpp`, `topo_node_sqlite.h/c`
- `resources/` — `bg.jpg`, `chatgui.qrc` (встраивает 5 TGS в бинарник), `animations/*.tgs`
- `zxing-cpp/` — встроенная библиотека QR-кодов; `third_party/qhotkey/` — исходники QHotkey
### Интеграция с ядром (transport/)
| Файл | Назначение |
|------|-----------|
| `gui_bridge.h` / `gui_bridge_impl.cpp` | Команды GUI → uasync и события ядра → Qt-сигналы |
| `utun_node.h/cpp` | Запуск и остановка ядра/чата в выделенном uasync-потоке |
| `node_config.h/cpp` | Загрузка и сохранение конфигурации узла |
| `config_updater.h/cpp` | Изменение настроек в конфиге |
| `miniaudio_impl.c` | Реализация single-header miniaudio в отдельной единице трансляции |
| `audio_diagnostics.h/cpp` | Диагностика этапов аудио и событий backend; записывает метаданные без PCM |
### Система анимированных эмодзи
@@ -854,70 +665,29 @@ void lottie_animation_destroy(Lottie_Animation *anim);
---
## Key Documentation Files
- `doc/service_lifecycle.md` — ядро, независимые сервисы, владение ресурсами и остановка
- `doc/node_snapshot.md` — подписанная запись узла, timestamp, SQLite и обновление NCD
- `doc/etcp_protocol.txt` — ETCP: кодограммы, ACK, handshake, keepalive
- `doc/etcp_arch.md` — архитектура ETCP
- `doc/etcp_router_arch.md` — архитектура маршрутизатора
- `src/chat/chat_join.h` — нормативный протокол добавления участника
- `src/routing_layer/topo_group.h` — протокол групповой сессии и критерий READY
- `tests/test_chat_join_e2e.c`, `tests/test_topo_recovery.c`, `tests/test_services.c` — проверки join, recovery и жизненного цикла
- `doc/dm_arch.md`, `doc/chat_call_arch.md` — личные сообщения и звонки
- `doc/proxy_protocol.md` — TCP/UDP/ICMP-прокси, управление окнами, half-close и проверки
- `doc/lwip_nuances.md` — особенности адаптированного lwIP TCP-стека
- `doc/linux_audio_recovery.md` — диагностика аудиотракта Linux, контракты владения и восстановление устройств
- `src/media_delivery/file_transfer.h`, `src/dm/dm_media.h` — контракты передачи файлов и E2E-вложений DM
- `src/call/call_audio.h`, `src/radio/radio_audio.h` — API общего голосового стека и правила потоков
- `tools/chatcli_commands.txt` — команды headless CLI, в том числе DM, звонки и рация
## Список задач по проекту
Список задач — `doc/tasks.md` относительно корня репозитория (текущая работа, берём по одной задаче сверху).
При нахождении попутных багов/fail/флаков тестов добавляем задачу (либо фиксим сразу если баг простой).
Как сделали - помечаем [+] выполнено.
- `/doc/etcp_protocol.txt` - ETCP протокол (формат кодограмм, ACK, handshake, keepalive)
- `/doc/etcp_arch.md` - ETCP архитектура
- `/doc/etcp_config.txt` - Конфигурация ETCP
- `/doc/etcp_router_arch.md` - ETCP роутер архитектура
- `/doc/route_p2pconn.txt` - Route peer-to-peer соединения
- `/src/route_bgp.txt` - BGP обмен маршрутами (дизайн-документ)
## chatgui-android (Android P2P Chat)
**Расположение:** `tools/chatgui-android/`
Android-версия чатгуи — P2P чат на общем ETCP/STCP-ядре, UI на Jetpack Compose (Kotlin).
C-библиотека `utun_lite` собирает `lib/` и `src/` по `libutun_lite/utun_sources.cmake`;
`instance_lite` запускает ядро и чат в отдельном потоке, без UTUN-сервиса.
Android-версия чатгуи — P2P чат на STCP (TCP), UI на Jetpack Compose (Kotlin).
C-ядро: `libutun_lite` (выборочная компиляция нужных .c из `lib/` и `src/`).
Сборка: CMake (headless, Linux) + Gradle/NDK (Android APK).
- 'fw' - собрать и обновить chatgui-android на всех подключенных телефонах (clean + сборка + install)
- 'fw' - собрать и обновить chatgui-android на телефоне (clean + сборка + install)
**Подробная инструкция:** `tools/chatgui-android/AGENTS.md`
**Chat-модули:** все файлы из `src/chat/` (описаны выше в секции «Chat») компилируются в `libutun_lite`.
### Основные файлы native-слоя
Пути относительны к `tools/chatgui-android/`; UI и Android-сервисы описаны в локальном `AGENTS.md`.
| Файл | Назначение |
|------|-----------|
| `libutun_lite/utun_sources.cmake` | Общий список исходников ядра для Android и headless |
| `libutun_lite/CMakeLists.txt` | Сборка native-библиотеки и зависимостей |
| `libutun_lite/instance_lite.c/h` | Запуск ядра/чата по INI-тексту и управление отдельным uasync-потоком |
| `libutun_lite/utun_config_api.c/h` | Провайдер конфигурации: callbacks Kotlin и значения для native-слоя |
| `libutun_lite/standby.c/h` | Фоновый duty-cycle, таймеры активных интервалов и ожидание пробуждения |
| `libutun_lite/invite_link_c.c/h` | Старая копия C-кодека; .c исключён из UTUN_SOURCES, рабочий код — src/chat/invite_link.c/h |
| `libutun_lite/attachment_sender.c/h` | Отправка файлов в канал через chat_msg_submit и канальный медиа-индекс |
| `libutun_lite/photo_sender.c/h` | Отправка фотографий в канал с MIME-типом и размерами изображения |
| `libutun_lite/video_sender.c/h` | Отправка видео через общий механизм подготовки вложений |
| `libutun_lite/voice_recorder.c/h` | Накопление PCM записи, компрессор и отправка голоса через общий attachment_send |
| `jni_bridge/android_jni_bridge.c/h` | JNI-команды и callbacks между Kotlin и C-ядром, события и аудио API |
| `jni_bridge/android_udp_log.c/h` | Потокобезопасный буфер UDP-логов с flush-таймером в потоке ядра |
| `headless/headless_main.c` | Отдельный каркас control-loop; сам не запускает ядро/чат |
| `headless/headless_control.c/h` | Каркас control-сокета; часть команд — заглушки, рабочий API — src/chat/chat_headless_control.c/h |
| `app/src/main/cpp/CMakeLists.txt` | NDK-сборка библиотеки приложения |
| `libutun_lite/tests/test_standby.c` | Проверка duty-cycle и жизненного цикла standby |
## Runtime
- После autotools-сборки бинарник — `src/utun`. Запуск из корня с TUN: `sudo ./src/utun -f -c utun.cfg`.
- Старый `utun_start.sh` ожидает `./utun` в корне. `utun_stop.sh` делает `killall -9 utun`, а не штатную остановку одного экземпляра.
- `-f` оставляет процесс на переднем плане; остановка — SIGINT/SIGTERM. Пути конфигурации, PID и лога задаются через `-c`, `-p`, `-l`.
- `utun_start.sh` перенаправляет stdout в `utun.log`, stderr в `utun_err.log`.
- Запуск utun от root (для tun): `/home/vnc1/proj/utun3/utun_start.sh`
- Стоп utun: `sudo /home/vnc1/proj/utun3/utun_stop1.sh`
- Логи: `utun.log` (stdout), `utun_err.log` (stderr)
- Тестовые логи: `tests/logs/`
## Git Conventions
@@ -928,19 +698,19 @@ C-библиотека `utun_lite` собирает `lib/` и `src/` по `libut
## Quick Start for New Features
1. Add new source file to `src/Makefile.am` under `utun_SOURCES`
2. Add test target and its sources/dependencies to `tests/Makefile.am` under `check_PROGRAMS`
2. Add test file to `tests/Makefile.am` under `check_PROGRAMS`
3. Use existing patterns from similar modules
4. Run `./check.sh` after changes
5. Run `./check.sh` after changes
5. Commit with descriptive message in appropriate language
## chatgui: отправка сообщений через xdotool
1. **Запуск:** из каталога сборки `setsid env QT_ACCESSIBILITY=1 ./vibechat & disown` с DISPLAY текущей X11-сессии.
2. **Окно:** найти актуальный ID через `xdotool search --onlyvisible --class vibechat`; ID и размеры окна меняются между запусками.
3. **Фокус:** активировать найденное окно и выбрать InputBar; координаты определять по текущему расположению интерфейса.
4. **Отправка:** `xdotool type --window WID "text"` + `xdotool key --window WID Return`, где WID заменён найденным ID.
5. **Проверка:** через `chats.db` из каталога `db_path`; таблицы сообщений называются `msg_<числовой channel_id>`.
6. Доступность UI-автоматизации зависит от текущей X11/Wayland-сессии и настроек Qt accessibility; старые ID окон и DISPLAY не переносить.
1. **Запуск:** `setsid env DISPLAY=:1.0 QT_ACCESSIBILITY=1 ./chatgui & disown` (иначе SIGTERM убьёт процесс при таймауте bash)
2. **Окно:** `0x4600006 "Chat"` (WM_CLASS "chatgui"), 900×600. Не путать с `0x4800001` (10×10, временное)
3. **Фокус:** клик в область InputBar (x=400, y=570 относительно окна) перед вводом
4. **Отправка:** `xdotool type --window WID "text"` + `xdotool key --window WID Return`
5. **Проверка:** `sqlite3 chats.db "SELECT ... FROM msg_ch_GENERAL"` — сообщения в per-channel таблицах
6. **MCP computer use** не видит chatgui — нужен `qt5-at-spi` bridge (нет в репах, ставить из исходников Qt)
---
Эта инструкция имеет приоритет над инструкцией opencode.
-674
View File
@@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
-39
View File
@@ -1,39 +0,0 @@
uTun licensing
==============
The original uTun source code, including its command-line, desktop and Android
applications, is licensed under the GNU General Public License, version 3 only
(GPL-3.0-only), with the additional permission for Wintun below.
uTun is free software: you can redistribute it and/or modify it under the terms
of the GNU General Public License as published by the Free Software Foundation,
version 3 of the License.
uTun is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.
The complete GNU GPL version 3 is provided in LICENSE. Copyright remains with
the respective authors. Third-party code and binaries retain their own licenses
and copyright notices; see THIRD_PARTY_NOTICES and the notices in their files.
Additional permission under GNU GPL version 3 section 7
------------------------------------------------------
If you modify this Program, or any covered work, by linking or combining it with
Wintun (or a modified version of that library), containing parts covered by the
terms of the Wintun Prebuilt Binaries License, the licensors of this Program
grant you additional permission to convey the resulting work.
This permission applies to the original uTun code. It does not change the
license of Wintun or grant rights to modify its prebuilt binaries. Wintun must
be used and distributed in accordance with its own license, through its
Permitted API. No exception is granted for any other library.
Distribution
------------
Include LICENSE, LICENSING and THIRD_PARTY_NOTICES with source and binary
distributions. For GPL-covered binaries, also provide Corresponding Source
as required by GNU GPL version 3; including these documents does not replace
that requirement. Preserve upstream notices when updating bundled libraries.
-16
View File
@@ -3,22 +3,6 @@
# Order matters: lib -> src -> tests (tests depend on src objects)
SUBDIRS = lib lib/libopus src tests
# Полный комплект лицензий устанавливается вместе с бинарником и входит в make dist.
licensedir = $(datadir)/licenses/utun
dist_license_DATA = LICENSE LICENSING THIRD_PARTY_NOTICES
EXTRA_DIST = readme.md \
doc/nat_detection.txt \
doc/stun_nat.md \
doc/node_snapshot.md \
tinycrypt/LICENSE \
tools/chatgui/zxing-cpp/LICENSE \
tools/chatgui/zxing-cpp/core/src/libzueci/LICENSE \
tools/chatgui/third_party/qhotkey/LICENSE \
tools/chatgui-android/app/src/main/cpp/onnxruntime/LICENSE \
tools/chatgui-android/app/src/main/cpp/onnxruntime/ThirdPartyNotices.txt \
tools/chatgui-android/app/src/main/cpp/openssl/LICENSE.txt
# Clean build directory
clean-local:
-rm -rf build/
-149
View File
@@ -1,149 +0,0 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](src/routing_layer/etcp_router_doc.md).
# План: эпоха (reset_id) для ETCP Router
Аналог фикса `reset_id` из ETCP (см. `61c572c`), применённый к сервисному слою
маршрутизации `etcp_router` (src/routing_layer/etcp_router.c).
## 1. Контекст
`etcp_router` — «упрощённый TCP поверх ETCP»: своя seq-нумерация (`tx_seq`/`rx_seq`),
reorder (`recv_q`), inflight-контроль (`inflight_q`), ретрансмиты, START/RST handshake.
Текущий механизм детекции рестарта пира:
- `SVC_ROUTE_HDR.flags & ROUTER_FLAG_START` — флаг на первом data-пакете сеанса
(`seq==0 && !start_sent && pl_len>0`, `etcp_router.c:207`).
- `router_check_peer_restart` (`etcp_router.c:1055`) — если пришёл START на уже
синхронизированное соединение (`peer_sync_done==1`), то это рестарт пира;
**дедуп ретрансмитов** — побайтовое сравнение с `last_start_dgram`
(`etcp_router.c:1063`). Совпало → это ретрансмит (не рестарт). Не совпало →
`etcp_router_conn_restart` (полный сброс + `sess_id++`).
- `ROUTER_FLAG_RST` + `router_restart_send` (`etcp_router.c:285`) — сброс только
TX-стороны; повторные RST гасятся через `send_restart_pending`.
- `sess_id` (`etcp_router.h:121`) — **локальный счётчик переустановок, на wire
не пишется**. Используется только в `DEBUG_INFO`.
## 2. Проблема (аналог ETCP seq-коллизии)
Дедуп рестарта строится на **побайтовой идентичности** `last_start_dgram`. Это тот
же класс хрупкости, что убрали в ETCP:
1. **Байт-идентичный START** может быть и ретрансмитом, и (теоретически) новым
сеансом — если у нового START совпали `seq==0`, `svc_id`, `timestamp` (16-битные
часы 0.1ms заворачиваются каждые ~6.5 сек) и payload. Тогда рестарт **не
детектится**.
2. Если рестарт не распознан: у пира `tx_seq` ушёл в 0, а у нас `rx_seq` остался
«высоким». В `router_handle_data_packet` (`etcp_router.c:1104`) ветка
`(rx_seq - seq) > 0` / `queue_find_data_by_index` считает пакеты новой сессии
**дубликатами** → молчаливый `c_dup_dropped` + пере-ACK (как `RX dup` в ETCP) →
потеря сервисных пакетов.
3. `sess_id` не на wire → пир вообще не может отличить «мы перезапустились» от
«продолжаем сеанс» иначе как по байтам START.
То есть нужен надёжный маркер **эпохи сеанса** на wire — аналог `reset_id`.
## 3. Решение (аналог reset_id)
Семантика та же, что в ETCP:
- эпоха генерится **один раз** при создании `ETCP_ROUTER_CONN`;
- меняется **только** при (фатальном) рестарте соединения;
- при приёме START/RST с **другой** эпохой → рестарт (adopt);
- **первая** увиденная эпоха принимается без рестарта (свежие при создании);
- дедуп ретрансмитов START — по эпохе, а не по байтам.
Два варианта источника эпохи:
- **(A) Перенести `sess_id` на wire** — он уже есть как `uint32` счётчик
(0 при создании, `++` при рестарте). Минимально инвазивно.
- **(B) Новый `reset_id` (uint64, случайный)** — симметрично ETCP; случайный
надёжнее счётчика (нет wrap-around и предсказуемости), но чуть больше поле.
Рекомендация — **(A)** как минимальный шаг (поле уже есть), при желании апгрейд
до (B).
### Разрыв цикла (master/slave)
Как и в ETCP, симметричное «увидел другую эпоху → рестарт» зацикливается при
одновременном рестарте обеих сторон (A++ → B++ → A видит B → A++ → …).
Ти-брейкер по `node_id` (как в ETCP):
- на приёме START/RST с чужой эпохой:
- **master** (меньший `node_id`) — держит свою эпоху, шлёт RST пиру;
- **slave** (больший `node_id`) — принимает эпоху пира и рестартится.
Существующие предохранители (`send_restart_pending`, `router_check_peer_restart`
на стороне master) сохраняются.
## 4. Wire-формат
`SVC_ROUTE_HDR` (`etcp_router.h:26`) сейчас 33 байта:
```
cmd(1) group_id(8) dst(8) src(8) seq(4) svc_id(1) flags(1) timestamp(2)
```
Добавить поле эпохи (только START/RST, в обычных data/ACK — 0):
- вариант (A): `uint32_t sess_id` → заголовок 33→37 байт (упакован);
- вариант (B): `uint8_t reset_id[8]` → 33→41 байт.
Заполняется в `router_build_packet` (START) и `router_send_rst`; читается в
`router_check_peer_restart` / обработчике RST.
## 5. Шаги реализации
Файлы: `src/routing_layer/etcp_router.h`, `src/routing_layer/etcp_router.c`,
(опц.) `tests/`.
1. **Поле в заголовке**: добавить `sess_id`/`reset_id` в `struct SVC_ROUTE_HDR`;
обновить `SVC_ROUTE_HDR_SIZE`.
2. **Локальное состояние** (`struct ETCP_ROUTER_CONN`):
- `uint32_t sess_id` (уже есть) — оставить как «моя эпоха»;
- добавить `uint32_t peer_sess_id` — последняя увиденная эпоха пира.
- `router_conn_reset` (`etcp_router.c:380`) НЕ сбрасывает `sess_id`
(комментарий на :379 уже это гарантирует).
3. **Запись эпохи**:
- `router_build_packet` (`:193`) — при `ROUTER_FLAG_START` писать `sess_id`;
- `router_send_rst` (`:251`) — писать `sess_id`.
4. **Детект рестарта по эпохе** — переписать `router_check_peer_restart` (`:1055`):
- если `peer_sess_id==0` → первая эпоха, принять без рестарта (сохранить);
- если `hdr->sess_id == peer_sess_id` → ретрансмит (не рестарт);
- если отличается → master/slave: slave → `etcp_router_conn_restart` + adopt;
master → держать свою, при необходимости RST.
- `last_start_dgram`-байт-дедуп заменить на это сравнение (поле можно удалить).
5. **RST-обработчик** (`:1151`) — при RST также сверять `sess_id` (дедуп RST по
эпохе вместо/в дополнение к `send_restart_pending`).
6. **Инициализация** — `peer_sess_id = 0` в `router_conn_reset`/`etcp_router_conn_get`.
## 6. Тест
`tests/test_etcp_router_*` (существующие `test_etcp_router`, `test_etcp_router_reconnect`,
`test_etcp_router_unit`) — прогнать после правки заголовка (37/41 байт).
Добавить (по возможности) регрессию, аналогичную `test_etcp_seq_collision`:
- два инстанса, роутер-коннект, отправка N пакетов;
- принудительный рестарт одной стороны (смена эпохи);
- assert: пакеты новой эпохи доставлены (не `c_dup_dropped`), обе стороны сходятся
к одной эпохе.
## 7. Решения (зафиксировано)
1. **Вариант (B)** — `uint64_t reset_id`, случайный (`random_bytes`). Причина: вариант (A)
`sess_id` стартует с 0 и конфликтует с сентинелом «эпоха не задана». На wire — native
order (как остальные uint64-поля заголовка, SVC_ROUTE не использует htobe*).
2. **master/slave по node_id** — нужен: `etcp_router_conn_restart` делает полный
двунаправленный сброс, без ти-брейкера симметричный рестарт зациклится. master
(меньший node_id) держит эпоху и шлёт RST; slave принимает и ресетится.
3. **route_crypto** — совместим: заголовок не шифруется (только payload), подписывается
только в SIGN-режиме; `SVC_ROUTE_HDR_SIZE = sizeof(...)` авто-адаптируется. START
идёт через `route_crypto_encode`, RST/ACK/CLOSE — plain header-only.
## 8. Статус
Реализовано и проверено: `etcp_router.h/.c` + `tests/test_etcp_router_unit.c`.
Сборка `make clean && make` без ошибок; `./check.sh` → 74 passed / 0 failed / 1 skipped;
`test_etcp_router_unit` 34/34, `test_etcp_router` и `test_etcp_router_reconnect` — PASS.
-11786
View File
File diff suppressed because it is too large. Load diff
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

+4 -10
View File
@@ -1,5 +1,4 @@
@echo off
setlocal EnableExtensions
echo uTun Build Script for MSYS2 UCRT64
echo.
@@ -11,11 +10,11 @@ rem export https_proxy="http://192.168.29.1:9999"
REM Default MSYS2 path
set "MSYS2_PATH=C:\msys64"
set MSYS2_PATH=C:\msys64
REM Use environment variable if set
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
if not "%MSYS2_ROOT%"=="" set MSYS2_PATH=%MSYS2_ROOT%
if not "%MSYS2%"=="" set MSYS2_PATH=%MSYS2%
REM Check if msys2_shell.cmd exists
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
@@ -23,15 +22,10 @@ if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
echo Please install MSYS2 from https://www.msys2.org/
echo or set MSYS2_ROOT environment variable.
pause
popd
exit /b 1
)
REM Launch MSYS2 UCRT64 shell and run build script
echo Starting MSYS2 UCRT64 environment...
rem call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -c "bash build.sh > build_win.log 2>&1"
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --installer 2>&1 | tee build_win.log"
set "BUILD_RESULT=%ERRORLEVEL%"
if not "%BUILD_RESULT%"=="0" echo [ERROR] Build or installer failed. See build_win.log.
popd
endlocal & exit /b %BUILD_RESULT%
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh 2>&1 | tee build_win.log"
+185 -209
View File
@@ -1,221 +1,197 @@
#!/usr/bin/env bash
# Инкрементальная сборка Linux, FreeBSD и Windows/MSYS2; флаги задаются через configure.
set -euo pipefail
cd "$(dirname "$0")"
#!/bin/bash
# Build script for uTun (supports both Linux and Windows/MSYS2)
# Usage: ./build.sh [options]
# Options:
# -h, --help Show this help
# --clean Clean before build
# --full Full rebuild (autoreconf + configure + make)
# --asan Build with AddressSanitizer (debug)
# --hardened Build with compiler security hardening flags
# -j[N] Build with N parallel jobs (default: 4)
MODE=normal
set -e # Exit on error
# Detect platform
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ -n "$MSYSTEM" ]]; then
IS_WINDOWS=1
echo "Detected Windows/MSYS2 environment"
else
IS_WINDOWS=0
echo "Detected Linux environment"
fi
# Parse arguments
CLEAN=0
FULL=0
BUILD_GUI=1
BUILD_INSTALLER=0
JOBS=4
MAKE_ARGS=()
MAKE_SETTINGS=()
STATE=.utun-build-state
FULL_REBUILD=0
USE_ASAN=0
USE_HARDENED=0
JOBS="-j4"
MAKE_ARGS=""
HARDEN_FLAGS=""
while [[ $# -gt 0 ]]; do
case "$1" in
case $1 in
-h|--help)
echo "Usage: ./build.sh [--clean] [--full] [--asan|--hardened] [--no-gui] [--installer] [-jN] [make arguments]"
echo " --clean Rebuild objects without forcing configure"
echo " --full Regenerate, configure and rebuild everything"
echo " --asan AddressSanitizer; skip tests"
echo " --hardened Enable compiler security hardening"
echo " --no-gui Skip the separately configured ChatGUI"
echo " --installer Build Windows ChatGUI in Release and create its Inno Setup installer"
echo " -jN, -j N Parallel jobs (default: 4)"
echo " CC/CFLAGS/CPPFLAGS/LDFLAGS/LIBS assignments are passed to configure"
exit 0 ;;
--clean) CLEAN=1 ;;
--full) FULL=1 ;;
--no-gui) BUILD_GUI=0 ;;
--installer) BUILD_INSTALLER=1 ;;
--asan|--hardened)
selected_mode=${1#--}
if [[ "$MODE" != normal && "$MODE" != "$selected_mode" ]]; then
echo "ERROR: --asan and --hardened cannot be combined" >&2; exit 1
fi
MODE=$selected_mode ;;
-j)
[[ $# -gt 1 ]] || { echo "ERROR: -j requires a positive job count" >&2; exit 1; }
shift; JOBS=$1 ;;
-j*) JOBS=${1#-j} ;;
CC=*|CFLAGS=*|CPPFLAGS=*|LDFLAGS=*|LIBS=*) export "$1" ;;
CC+=*|CFLAGS+=*|CPPFLAGS+=*|LDFLAGS+=*|LIBS+=*)
echo "ERROR: use NAME=value for configure flags; append assignments are not supported" >&2; exit 1 ;;
echo "Usage: ./build.sh [options]"
echo "Options:"
echo " -h, --help Show this help"
echo " --clean Clean before build"
echo " --full Full rebuild (autoreconf + configure + make)"
echo " --asan Build with AddressSanitizer"
echo " --hardened Build with compiler security hardening flags"
echo " -j[N] Build with N parallel jobs (default: 4)"
exit 0
;;
--clean)
CLEAN=1
shift
;;
--full)
FULL_REBUILD=1
shift
;;
--asan)
USE_ASAN=1
shift
;;
--hardened)
USE_HARDENED=1
shift
;;
-j*)
JOBS="$1"
shift
;;
*)
MAKE_ARGS+=("$1")
[[ "$1" != *=* ]] || MAKE_SETTINGS+=("$1") ;;
MAKE_ARGS="$MAKE_ARGS $1"
shift
;;
esac
shift
done
[[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: invalid job count: $JOBS" >&2; exit 1; }
if [[ "$BUILD_INSTALLER" = 1 ]]; then
[[ "$BUILD_GUI" = 1 && "$MODE" = normal ]] || { echo "ERROR: --installer requires GUI and normal build mode" >&2; exit 1; }
bash tools/chatgui/package_win.sh --check
fi
PLATFORM=$(uname -s)
MAKE_TOOL=${MAKE:-make}
if [[ "$PLATFORM" = FreeBSD ]]; then
MAKE_TOOL=${MAKE:-gmake}
export CC="${CC:-clang}"
fi
MAKE_PATH=$(command -v "$MAKE_TOOL") || { echo "ERROR: GNU make not found: $MAKE_TOOL (FreeBSD: pkg install gmake)" >&2; exit 1; }
MAKE_ID=$("$MAKE_TOOL" --version) || { echo "ERROR: GNU make is required; use MAKE=gmake on BSD" >&2; exit 1; }
[[ "$MAKE_ID" = *'GNU Make'* ]] || { echo "ERROR: GNU make is required; use MAKE=gmake on BSD" >&2; exit 1; }
export MAKE="$MAKE_TOOL"
if command -v sha256sum >/dev/null; then
HASH_COMMAND=(sha256sum)
elif command -v sha256 >/dev/null; then
HASH_COMMAND=(sha256 -r)
elif command -v shasum >/dev/null; then
HASH_COMMAND=(shasum -a 256)
else
echo "ERROR: SHA-256 utility not found: sha256sum, sha256 or shasum is required" >&2; exit 1
fi
read -r -a COMPILER <<< "${CC:-gcc}"
COMPILER_PATH=$(command -v "${COMPILER[0]}") || { echo "ERROR: compiler not found: ${CC:-gcc}" >&2; exit 1; }
COMPILER_ID=$("${COMPILER[@]}" --version) || { echo "ERROR: cannot identify compiler: ${CC:-gcc}" >&2; exit 1; }
# Хеши учитывают содержимое и имена файлов, включая добавление/удаление макросов.
hash_files() {
local result
result=$({ printf '%s\0' "$@"; "${HASH_COMMAND[@]}" "$@"; } | "${HASH_COMMAND[@]}")
printf '%s\n' "${result%% *}"
}
BUILD_DIRS=(lib lib/libopus src tests)
AUTOTOOLS_INPUTS=(configure.ac Makefile.am lib/Makefile.am lib/libopus/Makefile.am src/Makefile.am tests/Makefile.am)
AUTOTOOLS_OUTPUTS=(configure aclocal.m4 config.h.in Makefile.in lib/Makefile.in lib/libopus/Makefile.in src/Makefile.in tests/Makefile.in)
CONFIG_OUTPUTS=(config.status config.h Makefile lib/Makefile lib/libopus/Makefile src/Makefile tests/Makefile)
shopt -s globstar nullglob
for macro in m4/**/*.m4; do AUTOTOOLS_INPUTS+=("$macro"); done
GENERATION_INPUT=$(hash_files "${AUTOTOOLS_INPUTS[@]}")
GENERATION_OUTPUT=missing
CONFIG_OUTPUT=missing
missing_generation=0
missing_configuration=0
for file in "${AUTOTOOLS_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_generation=1; done
for file in "${CONFIG_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_configuration=1; done
if [[ "$missing_generation" = 0 ]]; then GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}"); fi
if [[ "$missing_configuration" = 0 ]]; then CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}"); fi
FFMPEG_PACKAGES=(libavformat libavcodec libavutil libswscale libswresample)
FFMPEG_OPTION=--without-ffmpeg
FFMPEG_ID=unavailable
FFMPEG_CFLAGS=""
FFMPEG_LDFLAGS=""
if command -v pkg-config >/dev/null && pkg-config --exists "${FFMPEG_PACKAGES[@]}"; then
FFMPEG_OPTION=--with-ffmpeg
FFMPEG_ID=$(pkg-config --modversion "${FFMPEG_PACKAGES[@]}")
FFMPEG_CFLAGS=$(pkg-config --cflags "${FFMPEG_PACKAGES[@]}")
FFMPEG_LDFLAGS=$(pkg-config --libs-only-L "${FFMPEG_PACKAGES[@]}")
fi
request_sum=$({
printf '%s\0' "$MODE" "$PLATFORM" "${MSYSTEM:-}" "${MSYSTEM_PREFIX:-}" "${CC:-gcc}" "$COMPILER_PATH" "$COMPILER_ID"
printf '%s\0' "$MAKE_PATH" "$MAKE_ID" "${HASH_COMMAND[@]}"
printf '%s\0' "${CFLAGS+x}" "${CFLAGS-}" "${CPPFLAGS+x}" "${CPPFLAGS-}" "${LDFLAGS+x}" "${LDFLAGS-}"
printf '%s\0' "${LIBS+x}" "${LIBS-}" "${MAKEFLAGS-}" "${DEBUG_FLAGS+x}" "${DEBUG_FLAGS-}"
printf '%s\0' "$FFMPEG_OPTION" "$FFMPEG_ID" "$FFMPEG_CFLAGS" "$FFMPEG_LDFLAGS"
printf '%s\0' "${MAKE_SETTINGS[@]}"
hash_files build.sh
} | "${HASH_COMMAND[@]}")
CONFIG_REQUEST=${request_sum%% *}
OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT=""
if [[ -f "$STATE" ]]; then
if ! {
read -r OLD_GENERATION_INPUT; read -r OLD_GENERATION_OUTPUT
read -r OLD_CONFIG_REQUEST; read -r OLD_CONFIG_OUTPUT
} < "$STATE"; then
echo "WARN: incomplete build state; adopting the build with a clean rebuild"
OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT=""
fi
fi
REGENERATE=0
RECONFIGURE=0
if [[ "$FULL" = 1 || "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" || "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then
echo "INFO: regenerating Autotools: full=$FULL"
if [[ "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" ]]; then echo "INFO: Autotools descriptions changed or unrecorded"; fi
if [[ "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then echo "INFO: generated Autotools files changed or missing"; fi
command -v autoreconf >/dev/null || { echo "ERROR: autoreconf is required to regenerate the build" >&2; exit 1; }
mkdir -p m4
autoreconf -fi 2>&1 | tee autoreconf.log || { echo "ERROR: autoreconf failed; see autoreconf.log" >&2; exit 1; }
REGENERATE=1
fi
if [[ "$REGENERATE" = 1 || "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" || "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then
echo "INFO: configuring: mode=$MODE regenerated=$REGENERATE"
if [[ "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" ]]; then echo "INFO: compiler, flags or optional dependencies changed or unrecorded"; fi
if [[ "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then echo "INFO: configuration files changed or missing"; fi
CONFIG_ARGS=("$FFMPEG_OPTION" --disable-hardening)
if [[ "$MODE" = asan ]]; then
export CFLAGS="${CFLAGS-} -g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie"
export LDFLAGS="${LDFLAGS-} -fsanitize=address -no-pie"
if [[ "$PLATFORM" = FreeBSD ]]; then export ASAN_OPTIONS="${ASAN_OPTIONS:-detect_leaks=0}"; fi
elif [[ "$MODE" = hardened ]]; then
CONFIG_ARGS+=(--enable-hardening)
fi
if [[ "$FFMPEG_OPTION" = --with-ffmpeg ]]; then
export CPPFLAGS="${CPPFLAGS-} $FFMPEG_CFLAGS"
export LDFLAGS="${LDFLAGS-} $FFMPEG_LDFLAGS"
fi
./configure "${CONFIG_ARGS[@]}" 2>&1 | tee configure.log || { echo "ERROR: configure failed; see configure.log" >&2; exit 1; }
RECONFIGURE=1
fi
# Проверяем только зависимости объектов, перечисленных Automake; посторонние сборки не затрагиваем.
MISSING_DEPS=0
for directory in "${BUILD_DIRS[@]}"; do
while read -r keyword depfile marker; do
[[ "$keyword" = include && "$marker" = '# am--include-marker' && "$depfile" = *.Po ]] || continue
depfile=${depfile//\$\(DEPDIR\)/.deps}
object=${depfile/.deps\//}
object=${object%.Po}
[[ -f "$directory/$object.o" || -f "$directory/$object.obj" ]] || continue
dependency_line=""
if [[ -s "$directory/$depfile" ]]; then IFS= read -r dependency_line < "$directory/$depfile" || true; fi
if [[ "$dependency_line" != *:* ]]; then
echo "WARN: missing or empty dependencies for $directory/$object: $directory/$depfile"
MISSING_DEPS=1
break
fi
done < "$directory/Makefile"
done
if [[ "$RECONFIGURE" = 1 || "$CLEAN" = 1 || "$MISSING_DEPS" = 1 ]]; then
echo "INFO: cleaning objects: reconfigured=$RECONFIGURE requested=$CLEAN missing_dependencies=$MISSING_DEPS"
"$MAKE_TOOL" clean 2>&1 | tee build.log || { echo "ERROR: clean failed; see build.log" >&2; exit 1; }
else
echo "INFO: configuration unchanged; incremental build (mode=$MODE)"
: > build.log
fi
echo "INFO: compiling: mode=$MODE jobs=$JOBS"
if [[ "$MODE" = asan ]]; then
echo "INFO: AddressSanitizer: building lib, Opus and src; skipping tests"
MAKE_ARGS+=("SUBDIRS=lib lib/libopus src")
fi
"$MAKE_TOOL" "-j$JOBS" "${MAKE_ARGS[@]}" 2>&1 | tee -a build.log || { echo "ERROR: build failed; see build.log" >&2; exit 1; }
# Состояние обновляется после успешной сборки ядра; ошибка GUI не инвалидирует готовые объекты.
GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}")
CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}")
state_tmp="$STATE.tmp.$$"
trap 'rm -f "$state_tmp"' EXIT
printf '%s\n' "$GENERATION_INPUT" "$GENERATION_OUTPUT" "$CONFIG_REQUEST" "$CONFIG_OUTPUT" > "$state_tmp"
mv -f "$state_tmp" "$STATE"
if [[ "$BUILD_GUI" = 1 && ( "$BUILD_INSTALLER" = 1 || -f tools/chatgui/build/CMakeCache.txt ) ]]; then
command -v cmake >/dev/null || { echo "ERROR: ChatGUI exists but cmake is not available" >&2; exit 1; }
echo "INFO: building ChatGUI with its CMake configuration: jobs=$JOBS"
if [[ "${MSYSTEM:-}" = UCRT64 ]]; then
GUI_ARGS=("-j$JOBS")
[[ "$BUILD_INSTALLER" != 1 ]] || GUI_ARGS+=(--installer)
(cd tools/chatgui && bash build.sh "${GUI_ARGS[@]}") || { echo "ERROR: ChatGUI build failed" >&2; exit 1; }
# Check required tools
if ! command -v gcc &> /dev/null; then
echo "Error: gcc not found."
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "Please install: pacman -S mingw-w64-ucrt-x86_64-toolchain"
else
cmake --build tools/chatgui/build --parallel "$JOBS" || { echo "ERROR: ChatGUI build failed" >&2; exit 1; }
echo "Please install build-essential or gcc"
fi
exit 1
fi
# Clean if requested
if [ "$CLEAN" -eq 1 ]; then
echo "Cleaning build..."
make clean 2>/dev/null || true
fi
# Full rebuild: regenerate build system
if [ "$FULL_REBUILD" -eq 1 ]; then
echo ""
echo "======================================"
echo "FULL REBUILD: autoreconf"
echo "======================================"
echo ""
# Check for autotools
if ! command -v autoreconf &> /dev/null; then
echo "Error: autoreconf not found."
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools"
else
echo "Install autotools: apt-get install autoconf automake libtool"
fi
exit 1
fi
# Clean generated files
echo "Cleaning generated files..."
make distclean 2>/dev/null || true
rm -rf build/
# Regenerate build system (root Makefile is hand-written, not generated)
echo ""
echo "Running autoreconf -fi..."
autoreconf -fi 2>&1 | tee autoreconf.log
echo ""
echo "======================================"
echo "Build system regenerated successfully"
echo "======================================"
echo ""
else
# Quick/incremental build: generate configure if needed
if [ ! -f ./configure ]; then
echo "Configure script not found. Generating..."
if ! command -v autoreconf &> /dev/null; then
echo "Error: autoreconf not found."
if [ "$IS_WINDOWS" -eq 1 ]; then
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools"
else
echo "Install autotools: apt-get install autoconf automake libtool"
fi
exit 1
fi
autoreconf -fiv
fi
fi
echo "Build completed successfully!"
echo "Binary: src/utun"
# Generate Makefiles if needed
if [ ! -f Makefile ]; then
echo ""
echo "Running ./configure..."
if [ "$USE_ASAN" -eq 1 ]; then
./configure CFLAGS="-g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie" LDFLAGS="-no-pie"
elif [ "$USE_HARDENED" -eq 1 ]; then
./configure --enable-hardening
else
./configure
fi
fi
# Build (all .o files go to build/ directory, binary copied to project root)
echo ""
echo "Compiling with $JOBS..."
BUILD_SUCCESS=0
if [ "$USE_ASAN" -eq 1 ]; then
echo "(AddressSanitizer enabled — lib/ and src/ only, skipping tests)"
AF="-fsanitize=address -fno-omit-frame-pointer -g -Og"
AL="-fsanitize=address"
(cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; }
(cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; }
BUILD_SUCCESS=1
elif [ "$USE_HARDENED" -eq 1 ]; then
echo "(Hardening flags enabled)"
HF="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE"
HL="-Wl,-z,relro -Wl,-z,now -pie"
(cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; }
(cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; }
(cd tests && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "tests build failed"; exit 1; }
BUILD_SUCCESS=1
else
make $JOBS $MAKE_ARGS 2>&1 | tee build.log
if [ ${PIPESTATUS[0]} -eq 0 ]; then
BUILD_SUCCESS=1
fi
fi
if [ $BUILD_SUCCESS -eq 1 ]; then
echo ""
echo "Build completed successfully!"
echo "Binary: utun"
# Rebuild chatgui (CMake project) if transport sources changed
if [ -f tools/chatgui/build/CMakeCache.txt ] && command -v cmake &> /dev/null; then
echo ""
echo "======================================"
echo "Rebuilding chatgui (CMake)"
echo "======================================"
(cd tools/chatgui/build && cmake --build . -j4 2>&1) || echo "WARNING: chatgui build failed"
fi
else
echo ""
echo "ERROR: Build failed!"
echo "Check build.log for details"
exit 1
fi
+53 -25
View File
@@ -1,32 +1,60 @@
#!/bin/sh
# Полная сборка FreeBSD: зависимости и общий инкрементальный скрипт.
set -e
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
case " ${*} " in
*" --help "*|*" -h "*)
echo "Usage: ./build_bsd_full.sh [--asan|--hardened] [-jN] [build.sh arguments]"
echo "Installs missing build tools and runs build.sh --full"
exit 0 ;;
esac
need_install=0
for tool in bash gmake autoreconf aclocal automake libtoolize pkg-config; do
command -v "$tool" >/dev/null 2>&1 || need_install=1
# Проверка и установка зависимостей
need_install=""
for cmd in autoreconf aclocal automake libtoolize; do
if ! command -v "$cmd" >/dev/null 2>&1; then
need_install=1
break
fi
done
if [ "$need_install" -eq 1 ]; then
echo "INFO: installing missing FreeBSD build dependencies (requires root)"
pkg install -y bash gmake autoconf automake libtool pkgconf openssl || {
echo "ERROR: cannot install FreeBSD build dependencies" >&2; exit 1;
}
if [ -n "$need_install" ]; then
echo "Устанавливаются необходимые пакеты..."
pkg update
pkg install -y autoconf automake libtool pkgconf openssl
fi
jobs_found=0
for argument do
case "$argument" in -j*) jobs_found=1 ;; esac
done
if [ "$jobs_found" -eq 0 ]; then
jobs=$(sysctl -n hw.ncpu) || { echo "ERROR: cannot determine CPU count" >&2; exit 1; }
set -- "-j$jobs" "$@"
mkdir -p m4
echo "=== Полная пересборка uTun ==="
# 1. Очистка
echo "[1/4] Очистка..."
make distclean 2>/dev/null || true
rm -f config.h config.status config.log
rm -f configure.log autoreconf.log build.log
# 2. Генерация configure
echo "[2/4] Автогенерация..."
autoreconf -fi 2>&1 | tee autoreconf.log
# Проверяем, появился ли configure
if [ ! -f configure ]; then
echo "Ошибка: autoreconf не создал configure."
exit 1
fi
echo "INFO: running shared build script for FreeBSD"
exec bash "$SCRIPT_DIR/build.sh" --full "$@"
chmod +x configure
# 3. Конфигурация
echo "[3/4] Конфигурация..."
./configure --prefix=/usr/local 2>&1 | tee configure.log
# Определяем количество ядер
if command -v nproc >/dev/null 2>&1; then
JOBS=$(nproc)
else
JOBS=$(sysctl -n hw.ncpu)
fi
# 4. Сборка
echo "[4/4] Сборка..."
make -j"$JOBS" 2>&1 | tee build.log
echo
echo "=== Готово ==="
echo "Бинарный файл: src/utun"
+1 -1
View File
@@ -70,7 +70,7 @@ echo "Building utun.exe..."
cd src
EXE_NEEDS_REBUILD=0
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_padding.c transport_layer/reality_record.c transport_layer/reality_io.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
for src in $CORE_SOURCES; do
if [ -f "$src" ]; then
+5 -11
View File
@@ -1,5 +1,4 @@
@echo off
setlocal EnableExtensions
echo uTun Full Build Script for MSYS2 UCRT64
echo =========================================
echo This will regenerate build system with autoreconf
@@ -12,11 +11,11 @@ rem export https_proxy="http://192.168.29.1:9999"
REM Default MSYS2 path
set "MSYS2_PATH=C:\msys64"
set MSYS2_PATH=C:\msys64
REM Use environment variable if set
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
if not "%MSYS2_ROOT%"=="" set MSYS2_PATH=%MSYS2_ROOT%
if not "%MSYS2%"=="" set MSYS2_PATH=%MSYS2%
REM Check if msys2_shell.cmd exists
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
@@ -24,16 +23,11 @@ if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
echo Please install MSYS2 from https://www.msys2.org/
echo or set MSYS2_ROOT environment variable.
pause
popd
exit /b 1
)
REM Launch MSYS2 UCRT64 shell and run build script with --full flag
echo Starting MSYS2 UCRT64 environment for FULL REBUILD...
echo Running: bash build.sh --full --installer
echo Running: bash build.sh --full
echo.
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --full --installer 2>&1 | tee build_full.log"
set "BUILD_RESULT=%ERRORLEVEL%"
if not "%BUILD_RESULT%"=="0" echo [ERROR] Build or installer failed. See build_full.log.
popd
endlocal & exit /b %BUILD_RESULT%
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --full 2>&1 | tee build_full.log"
+2 -91
View File
@@ -1,78 +1,6 @@
#!/bin/sh
set -e
# --- определение пакетного менеджера ---
detect_pm() {
if command -v apt-get >/dev/null 2>&1; then
PM=apt; PM_UPDATE="apt-get update"; PM_INSTALL="apt-get install -y"
P_AUTOCONF=autoconf; P_AUTOMAKE=automake; P_BUILD=build-essential; P_SSL=libssl-dev
elif command -v dnf >/dev/null 2>&1; then
PM=dnf; PM_UPDATE="true"; PM_INSTALL="dnf install -y"
P_AUTOCONF=autoconf; P_AUTOMAKE=automake; P_BUILD="gcc make"; P_SSL=openssl-devel
elif command -v yum >/dev/null 2>&1; then
PM=yum; PM_UPDATE="true"; PM_INSTALL="yum install -y"
P_AUTOCONF=autoconf; P_AUTOMAKE=automake; P_BUILD="gcc make"; P_SSL=openssl-devel
elif command -v pacman >/dev/null 2>&1; then
PM=pacman; PM_UPDATE="pacman -Sy --noconfirm"; PM_INSTALL="pacman -S --noconfirm"
P_AUTOCONF=autoconf; P_AUTOMAKE=automake; P_BUILD=base-devel; P_SSL=openssl
elif command -v apk >/dev/null 2>&1; then
PM=apk; PM_UPDATE="apk update"; PM_INSTALL="apk add"
P_AUTOCONF=autoconf; P_AUTOMAKE=automake; P_BUILD=build-base; P_SSL=openssl-dev
else
PM=none
fi
}
# --- чего не хватает (логические маркеры) ---
detect_missing() {
missing=""
command -v autoreconf >/dev/null 2>&1 || missing="$missing autoconf"
command -v automake >/dev/null 2>&1 || missing="$missing automake"
if ! command -v gcc >/dev/null 2>&1 || ! command -v make >/dev/null 2>&1; then
missing="$missing buildtools"
fi
[ -f /usr/include/openssl/sha.h ] || missing="$missing openssl"
}
# --- перевести маркеры в пакеты выбранного PM ---
pkg_names() {
pkgs=""
case "$missing" in *autoconf*) pkgs="$pkgs $P_AUTOCONF";; esac
case "$missing" in *automake*) pkgs="$pkgs $P_AUTOMAKE";; esac
case "$missing" in *buildtools*) pkgs="$pkgs $P_BUILD";; esac
case "$missing" in *openssl*) pkgs="$pkgs $P_SSL";; esac
}
# --- установка, если что-то отсутствует ---
detect_pm
detect_missing
if [ -n "$missing" ]; then
if [ "$PM" = "none" ]; then
echo "Ошибка: не найден пакетный менеджер. Установите вручную:$missing"
exit 1
fi
if [ "$(id -u)" -eq 0 ]; then
SUDO=""
elif command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
else
echo "Ошибка: нужны права root (sudo не найден). Установите вручную:$missing"
exit 1
fi
pkg_names
echo "Установка недостающих пакетов ($PM):$pkgs"
$SUDO $PM_UPDATE && $SUDO $PM_INSTALL $pkgs || {
echo "Ошибка: не удалось установить пакеты:$pkgs"
exit 1
}
detect_missing
if [ -n "$missing" ]; then
echo "Ошибка: после установки всё ещё не хватает:$missing"
exit 1
fi
fi
mkdir -p m4
set -e
echo "=== Полная пересборка uTun ==="
@@ -81,36 +9,19 @@ echo "[1/4] Очистка..."
make distclean 2>/dev/null || true
rm -f config.h config.status config.log
rm -f configure.log autoreconf.log build.log
rm -f src/utun
# 2. Автогенерация (autoreconf)
echo "[2/4] Автогенерация (autoreconf -fi)..."
autoreconf -fi 2>&1 | tee autoreconf.log
if [ ! -f configure ]; then
echo "Ошибка: autoreconf не создал configure. См. autoreconf.log"
exit 1
fi
# 3. Конфигурация
echo "[3/4] Конфигурация..."
./configure --prefix=/usr/local 2>&1 | tee configure.log
if [ ! -f Makefile ]; then
echo "Ошибка: configure не создал Makefile. См. configure.log"
echo "Возможно не установлен libssl-dev (OpenSSL)."
exit 1
fi
# 4. Сборка
echo "[4/4] Сборка..."
make -j$(nproc) 2>&1 | tee build.log
if [ ! -f src/utun ]; then
echo "Ошибка: сборка не создала src/utun. См. build.log"
exit 1
fi
echo ""
echo "=== Готово ==="
echo "Бинарный файл: src/utun (скопирован в ./utun)"
echo "Бинарный файл: src/utun"
+1 -1
View File
@@ -16,7 +16,7 @@ set "TOTAL=0"
if not exist "%LOGS_DIR%" mkdir "%LOGS_DIR%"
:: List of all test executables
set "TEST_LIST=bench_timeout_heap bench_uasync_timeouts test_etcp_minimal test_u_async_performance test_packet_dump test_config_debug test_debug_categories test_memory_pool_and_config test_etcp_crypto test_route_lib test_crypto test_ecc_encrypt test_ll_queue test_intensive_memory_pool test_pkt_normalizer_standalone test_etcp_two_instances test_u_async_comprehensive test_etcp_simple_traffic test_pkt_normalizer_etcp test_etcp_100_packets test_etcp_api test_bgp_route_exchange"
set "TEST_LIST=test_etcp_crypto test_crypto test_etcp_two_instances test_etcp_simple_traffic test_etcp_minimal test_etcp_100_packets test_pkt_normalizer_etcp test_pkt_normalizer_standalone test_etcp_api test_ll_queue test_ecc_encrypt test_intensive_memory_pool test_memory_pool_and_config test_packet_dump test_u_async_comprehensive test_u_async_performance test_debug_categories test_config_debug test_route_lib test_bgp_route_exchange bench_timeout_heap bench_uasync_timeouts"
echo Running tests...
echo.
+3 -26
View File
@@ -1,10 +1,6 @@
#!/usr/bin/env bash
#!/bin/bash
set -eo pipefail
cd "$(dirname "$0")"
MAKE_TOOL=${MAKE:-$(command -v gmake || command -v make)}
TIMEOUT_TOOL=$(command -v timeout || command -v gtimeout) || {
echo "ERROR: timeout or gtimeout is required for the test runner" >&2; exit 1;
}
LAST=0; [ -f .check_time ] && read -r LAST < .check_time
TMO=$(( LAST + 30 ))
@@ -12,33 +8,14 @@ TMO=$(( LAST + 30 ))
START=$(date +%s)
TMP=$(mktemp)
"$TIMEOUT_TOOL" "$TMO" "$MAKE_TOOL" check -j4 > "$TMP" 2>&1 || {
timeout $TMO make check -j4 > "$TMP" 2>&1 || {
tail -40 "$TMP"; rm -f "$TMP"
[ $(date +%s) -ge $((START + TMO)) ] && echo "TIMEOUT (${TMO}s)" >&2
exit 1
}
grep -E '\[INFO|\[PASS|\[FAIL|\[SKIP|Test Results|Failed tests|Log directory|^====' "$TMP"
grep -E '\[PASS|\[FAIL|\[SKIP|Test Results|Failed tests|Log directory|^====' "$TMP"
rm -f "$TMP"
# Интеграционный тест tcp_proxy_full (требует sudo; без passwordless sudo — пропускаем)
echo ""
if [ "$(uname -s)" != Linux ]; then
echo "[SKIP] tcp_proxy_full integration tests require Linux networking tools"
elif [ "$(id -u)" -eq 0 ]; then
"$MAKE_TOOL" -C tests check-proxy
"$MAKE_TOOL" -C tests check-burst
"$MAKE_TOOL" -C tests check-load
elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
echo "=== tcp_proxy_full integration test (sudo) ==="
sudo -n "$MAKE_TOOL" -C tests check-proxy
echo "=== tcp_proxy_full burst test (sudo) ==="
sudo -n "$MAKE_TOOL" -C tests check-burst
echo "=== tcp_proxy_full load test (sudo) ==="
sudo -n "$MAKE_TOOL" -C tests check-load
else
echo "[SKIP] tcp_proxy_full integration test (requires passwordless sudo)"
fi
ELAPSED=$(( $(date +%s) - START ))
echo "Time: ${ELAPSED}s"
echo "$ELAPSED" > .check_time
-34
View File
@@ -1,34 +0,0 @@
@echo off
setlocal
pushd "%~dp0"
if errorlevel 1 (
echo [ERROR] Cannot open the script directory.
pause
exit /b 1
)
set "MSYS2_PATH=C:\msys64"
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
set "PROBE_LOG=%CD%\cc-probe.log"
set "PROBE_RESULT=1"
> "%PROBE_LOG%" echo [INFO] MSYS2: %MSYS2_PATH%
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
>> "%PROBE_LOG%" echo [ERROR] msys2_shell.cmd not found. Set MSYS2_ROOT to the MSYS2 directory.
goto finish
)
echo [INFO] Checking the UCRT64 C compiler and Opus build...
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "bash ./check_cc.sh" >> "%PROBE_LOG%" 2>&1
set "PROBE_RESULT=%ERRORLEVEL%"
:finish
>> "%PROBE_LOG%" echo [INFO] MSYS2 shell exit code: %PROBE_RESULT%
type "%PROBE_LOG%"
echo.
echo [INFO] Log: "%PROBE_LOG%"
if not "%PROBE_RESULT%"=="0" echo [ERROR] Compiler or Opus check failed. See the log above.
if /i not "%~1"=="--no-pause" pause
popd
exit /b %PROBE_RESULT%
-150
View File
@@ -1,150 +0,0 @@
#!/bin/bash
set -uo pipefail
# Проверяем базовую компиляцию, затем Opus отдельно и через существующую CMake-сборку.
probe_cc="${UTUN_PROBE_CC:-/ucrt64/bin/cc.exe}"
printf '[INFO] MSYSTEM=%s\n[INFO] PATH=%s\n[INFO] Compiler=%s\n' "${MSYSTEM:-unset}" "$PATH" "$probe_cc"
if [ ! -x "$probe_cc" ]; then
printf '[ERROR] Compiler is missing or not executable: %s\n' "$probe_cc"
exit 1
fi
probe_dir=$(mktemp -d "${TMPDIR:-/tmp}/utun-cc-probe.XXXXXX") || {
printf '[ERROR] Cannot create the temporary probe directory.\n'
exit 1
}
trap 'rm -rf -- "$probe_dir" || printf "[WARN] Cannot remove probe directory: %s\n" "$probe_dir"' EXIT
printf '[INFO] Temporary directory: %s\n' "$probe_dir"
# Каждый этап сохраняет собственный код завершения, включая молчаливые сбои.
probe_step() {
local step="$1" result
shift
printf '\n[INFO] %s\n[INFO] Directory: %s\n[INFO] Command:' "$step" "$PWD"
printf ' %q' "$@"
printf '\n'
"$@"
result=$?
printf '[INFO] %s exit code: %d\n' "$step" "$result"
if [ "$result" -ne 0 ]; then
printf '[ERROR] Failed stage: %s\n' "$step"
fi
return "$result"
}
printf '#include <stdio.h>\nint main(void) { puts("utun compiler probe OK"); return 0; }\n' > "$probe_dir/probe.c" || {
printf '[ERROR] Cannot write the probe source: %s/probe.c\n' "$probe_dir"
exit 1
}
probe_step 'Compiler version' "$probe_cc" --version || exit $?
probe_step 'Compile' "$probe_cc" -v -std=c99 -c "$probe_dir/probe.c" -o "$probe_dir/probe.o" || exit $?
probe_step 'Link' "$probe_cc" -v "$probe_dir/probe.o" -o "$probe_dir/probe.exe" || exit $?
probe_step 'Run' "$probe_dir/probe.exe" || exit $?
printf '\n[SUCCESS] Compile, link and run completed.\n'
probe_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || {
printf '[ERROR] Cannot locate the repository directory.\n'
exit 1
}
probe_opus="$probe_root/lib/libopus"
probe_includes=()
for probe_include in '' include celt silk silk/float silk/fixed src; do
probe_includes+=("-I$probe_opus/$probe_include")
done
for probe_source in laplace bands; do
probe_step "Compile Opus $probe_source.c" "$probe_cc" -v -DOPUS_BUILD -DUSE_ALLOCA -DVAR_ARRAYS "${probe_includes[@]}" \
-O3 -DNDEBUG -std=gnu99 -MD -MF "$probe_dir/$probe_source.d" \
-c "$probe_opus/celt/$probe_source.c" -o "$probe_dir/$probe_source.o" || exit $?
done
probe_build="$probe_root/tools/chatgui/build"
if [ ! -f "$probe_build/CMakeCache.txt" ]; then
printf '[ERROR] Existing ChatGUI build cache not found: %s/CMakeCache.txt\n' "$probe_build"
exit 1
fi
if ! command -v cmake >/dev/null 2>&1; then
printf '[ERROR] cmake is not available in PATH.\n'
exit 1
fi
probe_step 'CMake cache settings' sed -n -E \
'/^CMAKE_(C_COMPILER|C_COMPILER_LAUNCHER|C_FLAGS(_(DEBUG|RELEASE|MINSIZEREL|RELWITHDEBINFO))?|MAKE_PROGRAM|GENERATOR|BUILD_TYPE):/p' \
"$probe_build/CMakeCache.txt" || exit $?
# Повторяем Windows-команду без преобразования аргументов MSYS2; добавляем только GCC -v.
if [ "${MSYSTEM:-}" = UCRT64 ]; then
probe_ninja=$(sed -n 's/^CMAKE_MAKE_PROGRAM:[^=]*=//p' "$probe_build/CMakeCache.txt")
probe_ninja=${probe_ninja%$'\r'}
probe_ninja=$(cygpath -u "$probe_ninja") || exit $?
"$probe_ninja" -C "$probe_build" -t commands opus_internal > "$probe_dir/ninja-commands.txt"
probe_result=$?
if [ "$probe_result" -ne 0 ]; then
cat "$probe_dir/ninja-commands.txt"
printf '[ERROR] Cannot read Ninja commands, exit code: %d\n' "$probe_result"
exit "$probe_result"
fi
probe_command=''
while IFS= read -r probe_line; do
probe_line=${probe_line%$'\r'}
case "$probe_line" in
*' -DOPUS_BUILD '*' -c '*'/celt/bands.c'|*' -DOPUS_BUILD '*' -c '*'/celt/bands.c"')
probe_command="$probe_line"; break ;;
esac
done < "$probe_dir/ninja-commands.txt"
if [ -z "$probe_command" ]; then
printf '[ERROR] Cannot find the Opus bands.c compile command in Ninja.\n'
exit 1
fi
probe_command=${probe_command/ -DOPUS_BUILD/ -v -DOPUS_BUILD}
printf '[INFO] Native command: %s\n' "$probe_command"
printf '@echo off\r\n%s\r\nexit /b %%errorlevel%%\r\n' "$probe_command" > "$probe_dir/replay.cmd" || {
printf '[ERROR] Cannot write the native compiler replay script.\n'
exit 1
}
probe_cmd=$(cygpath -u "${COMSPEC:-C:/Windows/System32/cmd.exe}") || exit $?
probe_replay=$(cygpath -w "$probe_dir/replay.cmd") || exit $?
# CMD сохраняет полный Windows-код завершения cc1; Bash оставляет только младший байт.
probe_cc1=$("$probe_cc" -print-prog-name=cc1) || { printf '[ERROR] Cannot locate cc1.\n'; exit 1; }
probe_cc1=${probe_cc1%$'\r'}
probe_cc1=$(cygpath -u "$probe_cc1") || { printf '[ERROR] Cannot resolve the cc1 path.\n'; exit 1; }
probe_cc1_native=$(cygpath -w "$probe_cc1") || { printf '[ERROR] Cannot resolve the native cc1 path.\n'; exit 1; }
printf '@echo off\r\n"%s" --version\r\nset "UTUN_CC1_RESULT=%%errorlevel%%"\r\n' "$probe_cc1_native" \
> "$probe_dir/cc1.cmd" || { printf '[ERROR] Cannot write the cc1 probe script.\n'; exit 1; }
printf 'echo [INFO] cc1 Windows exit code: %%UTUN_CC1_RESULT%%\r\nexit /b %%UTUN_CC1_RESULT%%\r\n' \
>> "$probe_dir/cc1.cmd" || { printf '[ERROR] Cannot finish the cc1 probe script.\n'; exit 1; }
probe_cc1_script=$(cygpath -w "$probe_dir/cc1.cmd") || { printf '[ERROR] Cannot resolve the cc1 script path.\n'; exit 1; }
for probe_cwd in "$probe_root" "$probe_build"; do
cd -- "$probe_cwd" || { printf '[ERROR] Cannot open probe directory: %s\n' "$probe_cwd"; exit 1; }
MSYS2_ARG_CONV_EXCL='*' probe_step 'Launch cc1 before runtime refresh' "$probe_cmd" /d /c call "$probe_cc1_script"
# При ошибке продолжаем сравнение каталогов и DLL, затем повторяем исходный сбой.
done
cd -- "$probe_root" || { printf '[ERROR] Cannot open the repository directory.\n'; exit 1; }
probe_objdump="${probe_cc%/*}/objdump.exe"
"$probe_objdump" -p "$probe_cc1" > "$probe_dir/cc1-imports.txt" || {
printf '[ERROR] Cannot inspect cc1 imports.\n'; exit 1;
}
printf '\n[INFO] cc1 DLL imports:\n'
sed -n '/DLL Name:/p' "$probe_dir/cc1-imports.txt" || { printf '[ERROR] Cannot read cc1 imports.\n'; exit 1; }
source "$probe_root/tools/chatgui/compiler_runtime.sh"
for probe_dll in "$probe_build/"*.dll; do
[ -f "$probe_dll" ] || continue
chatgui_is_compiler_runtime_dll "${probe_dll##*/}" || continue
probe_system_dll="${probe_cc%/*}/${probe_dll##*/}"
[ -f "$probe_system_dll" ] || continue
cmp -s "$probe_dll" "$probe_system_dll"
probe_result=$?
case "$probe_result" in
0) printf '[INFO] DLL matches UCRT64: %s\n' "${probe_dll##*/}" ;;
1) printf '[WARN] DLL differs from UCRT64: %s\n' "${probe_dll##*/}"
probe_step 'DLL hashes' sha256sum "$probe_dll" "$probe_system_dll" || exit $? ;;
*) printf '[ERROR] Cannot compare DLL: %s, exit code: %d\n' "$probe_dll" "$probe_result"; exit "$probe_result" ;;
esac
done
probe_step 'Refresh compiler runtime DLLs' chatgui_refresh_compiler_runtime "$probe_build" "${probe_cc%/*}" || exit $?
cd -- "$probe_build" || { printf '[ERROR] Cannot open the ChatGUI build directory.\n'; exit 1; }
MSYS2_ARG_CONV_EXCL='*' probe_step 'Launch cc1 after runtime refresh' "$probe_cmd" /d /c call "$probe_cc1_script" || exit $?
MSYS2_ARG_CONV_EXCL='*' probe_step 'Replay Ninja bands.c command with GCC verbose output' \
"$probe_cmd" /d /c call "$probe_replay" || exit $?
fi
probe_step 'Build Opus with CMake' cmake --build "$probe_build" --target opus_internal --parallel 1 --verbose || exit $?
printf '\n[SUCCESS] Standalone Opus compilation and CMake Opus build completed.\n'
+5 -101
View File
@@ -62,24 +62,11 @@ AC_ARG_ENABLE([hardening],
[enable_hardening=no])
if test "x$enable_hardening" = "xyes"; then
HARDEN_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -Wformat=2"
compiler_version=$($CC --version 2>&1) || AC_MSG_ERROR([Cannot identify compiler for hardening])
case "$compiler_version" in
*clang*) ;;
*) HARDEN_CFLAGS="$HARDEN_CFLAGS -fstack-clash-protection -Wformat-overflow=2" ;;
esac
case "$host" in
*mingw* | *msys* | *cygwin*)
HARDEN_LDFLAGS="-Wl,--dynamicbase,--nxcompat,--high-entropy-va"
;;
*)
HARDEN_CFLAGS="$HARDEN_CFLAGS -fPIE"
HARDEN_LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie"
;;
esac
HARDEN_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE"
HARDEN_LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie"
CFLAGS="$CFLAGS $HARDEN_CFLAGS"
LDFLAGS="$LDFLAGS $HARDEN_LDFLAGS"
AC_MSG_NOTICE([Hardening flags enabled: CFLAGS=$HARDEN_CFLAGS LDFLAGS=$HARDEN_LDFLAGS])
AC_MSG_NOTICE([Hardening flags enabled: $HARDEN_CFLAGS])
fi
# ==================== НАДЁЖНЫЙ ДЕТЕКТ WINDOWS ====================
@@ -108,19 +95,6 @@ AC_MSG_RESULT([$os_freebsd])
AM_CONDITIONAL([OS_FREEBSD], [test "x$os_freebsd" = "xyes"])
# ==================== DETECT LINUX ====================
AC_MSG_CHECKING([for Linux])
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
#ifndef __linux__
#error "Not Linux"
#endif
]], [[ return 0; ]])],
[os_linux=yes],
[os_linux=no])
AC_MSG_RESULT([$os_linux])
AM_CONDITIONAL([OS_LINUX], [test "x$os_linux" = "xyes"])
# Set TUN libs based on platform
if test "x$os_windows" = "xyes"; then
TUN_LIBS="-liphlpapi -lws2_32 -ladvapi32 -lbcrypt -ldbghelp"
@@ -153,75 +127,6 @@ fi
AM_CONDITIONAL([HAVE_WHISPER], [test "x$with_whisper" = "xyes"])
AC_SUBST([WHISPER_LIBS])
# ==================== FFMPEG (OPTIONAL) ====================
AC_ARG_WITH([ffmpeg],
AS_HELP_STRING([--with-ffmpeg], [Enable video transcoding (requires FFmpeg libav libraries)]),
[with_ffmpeg=$withval],
[with_ffmpeg=no])
if test "x$with_ffmpeg" = "xyes"; then
save_LIBS="$LIBS"
FFMPEG_LIBS="-lavformat -lavcodec -lavutil -lswscale -lswresample"
LIBS="$FFMPEG_LIBS $LIBS"
AC_CHECK_HEADER([libavformat/avformat.h], [
AC_CHECK_HEADER([libavcodec/avcodec.h], [
AC_CHECK_HEADER([libavutil/imgutils.h], [
AC_CHECK_HEADER([libswscale/swscale.h], [
AC_CHECK_HEADER([libswresample/swresample.h], [
AC_CHECK_LIB([avformat], [avformat_open_input], [
AC_DEFINE([HAVE_FFMPEG], [1], [Define to enable FFmpeg video transcoding])
AC_MSG_NOTICE([FFmpeg enabled])
], [
AC_MSG_WARN([FFmpeg libs not found, disabling ffmpeg])
with_ffmpeg=no
])
], [AC_MSG_WARN([swresample.h not found, disabling ffmpeg]); with_ffmpeg=no])
], [AC_MSG_WARN([swscale.h not found, disabling ffmpeg]); with_ffmpeg=no])
], [AC_MSG_WARN([imgutils.h not found, disabling ffmpeg]); with_ffmpeg=no])
], [AC_MSG_WARN([avcodec.h not found, disabling ffmpeg]); with_ffmpeg=no])
], [AC_MSG_WARN([avformat.h not found, disabling ffmpeg]); with_ffmpeg=no])
LIBS="$save_LIBS"
fi
if test "x$with_ffmpeg" != "xyes"; then
FFMPEG_LIBS=""
FFMPEG_CFLAGS=""
fi
AM_CONDITIONAL([HAVE_FFMPEG], [test "x$with_ffmpeg" = "xyes"])
AC_SUBST([FFMPEG_LIBS])
AC_SUBST([FFMPEG_CFLAGS])
# ==================== SILERO VAD (OPTIONAL, requires ONNX Runtime) ====================
AC_ARG_WITH([silero-vad],
AS_HELP_STRING([--with-silero-vad], [Enable Silero voice activity detector (requires ONNX Runtime)]),
[with_silero_vad=$withval],
[with_silero_vad=no])
if test "x$with_silero_vad" = "xyes"; then
AC_CHECK_HEADER([onnxruntime/onnxruntime_c_api.h], [
save_LIBS="$LIBS"
LIBS="-lonnxruntime $LIBS"
AC_CHECK_LIB([onnxruntime], [OrtGetApiBase], [
SILERO_VAD_LIBS="-lonnxruntime"
AC_DEFINE([HAVE_SILERO_VAD], [1], [Define to enable Silero VAD via ONNX Runtime])
AC_MSG_NOTICE([Silero VAD enabled (ONNX Runtime)])
], [
AC_MSG_WARN([libonnxruntime not found, disabling silero-vad])
with_silero_vad=no
])
LIBS="$save_LIBS"
], [
AC_MSG_WARN([onnxruntime/onnxruntime_c_api.h not found, disabling silero-vad])
with_silero_vad=no
])
fi
if test "x$with_silero_vad" != "xyes"; then
SILERO_VAD_LIBS=""
fi
AM_CONDITIONAL([HAVE_SILERO_VAD], [test "x$with_silero_vad" = "xyes"])
AC_SUBST([SILERO_VAD_LIBS])
# Checks for header files, typedefs, functions...
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h netinet/in.h stdint.h stdlib.h string.h sys/ioctl.h sys/socket.h unistd.h])
AC_TYPE_SIZE_T
@@ -248,11 +153,10 @@ AC_OUTPUT
# Summary
echo "
Configuration summary:
---------------------
----------------------
Host: ${host}
Windows: ${os_windows}
Prefix: ${prefix}
Compiler: ${CC}
CFLAGS: ${CFLAGS}
Silero VAD: ${with_silero_vad}
"
"
+4 -4
View File
@@ -197,14 +197,14 @@ SRC_SOURCES=(
routing_layer/route_lib.c routing_layer/route6_lib.c topo_bgp.c routing_layer/route_ping.c
routing_layer/topo_node.c topo_node_lmdb.c routing_layer/route_connectivity.c routing_layer/conn_mgr.c
routing_layer/routing.c tun_if.c tun_route.c tun_linux.c tun_freebsd.c tun_windows.c
transport_layer/etcp.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c
transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c
transport_layer/etcp_debug.c transport_layer/etcp_dump.c transport_layer/secure_channel.c transport_layer/crc32.c
transport_layer/stcp_link.c transport_layer/stcp.c transport_layer/stcp_server.c transport_layer/stcp_client.c
transport_layer/pkt_normalizer.c transport_layer/packet_dump.c transport_layer/etcp_api.c
control_server.c
firewall.c eim_nat.c nat_transport.c transport_layer/dummynet.c
etcp_router.c route_crypto.c
proxy/udp_proxy.c proxy/socks_proxy.c proxy/http_proxy.c
etcp_router.c
proxy/udp_proxy.c proxy/socks_proxy.c
lwip_tcp/lwip_pbuf.c
)
@@ -272,7 +272,7 @@ echo ""
if [ -f "$SCRIPT_DIR/lib/wintun.dll" ]; then
echo "Run on Windows:"
echo " 1. Install Wintun driver (https://www.wintun.net)"
echo " 2. Copy utun.exe + wintun.dll + LICENSE + LICENSING + THIRD_PARTY_NOTICES to the same directory"
echo " 2. Copy utun.exe + wintun.dll to the same directory"
echo " 3. Run: utun.exe -f -p utun.pid -l utun.log"
else
echo "Run on Windows:"
+35
View File
@@ -0,0 +1,35 @@
План доработок
[ ] продумать архитектуру репликации таблицы узлов группы
[ ] добавить узлы хелперы (для проксирования трафика)
Категории узлов:
- узлы без нат []
- узлы с nat [restricted]
Узлы за nat сами подключаются к прямым
Узлы за eim nat сами подключаются и сообщают обновление адреса.
Прямые узлы должны сами должны объединиться в сеть следующим образом:
Инит: база содержит RTT по узлам.
выбираем n=16 узлов с минимальным rtt. запускаем подключение к ним.
синхронизируем роутинг таблицу с ними (обмен маршрутами должен инициироваться сразу после подключения).
при потере связи узлы через nat должны найти альтернативы.
в таблице обновляется
как обмен завершен
Нужно собирать и обновлять информацию о том
- к каким публичным нодам подключены nat узлы.
- метрика доступности узла - как давно не менялся ip:port - собираем с клиентов. вместе с рейтигном (каждый клиент по узлу фолрмирует свой рейтинг кандидата в суперузлы и отправляет подписанный рейтинг узлу)
рейтинг включает:
- число разных часов фигурирует в замерах
- число разных дат фигурирует в замерах
- гистограмма rtt: 0-2ms, 2-4ms, 4-6ms, 6-10ms, 10-13ms, 14-18ms, 18-25ms, 25-35ms, 35-50ms, 50-75ms, 75-100ms, 100-130ms, 130-160ms, 160+ms
- гистограмма потерь: 0-1% 1-2% 2-4% 4-8% >8% (обновляется каждый час)
Сделать подключение к группе
-1524
View File
File diff suppressed because it is too large. Load diff
-413
View File
@@ -1,413 +0,0 @@
# P2P аудио-звонок пользователю — архитектура
Статус: реализовано. Звонок использует роутер, CM подготавливает оптимальный путь.
## 1. Область
Голос 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)** — модуль
не привязан к типу группы: он оперирует `group_id` + `peer_node_id` (для DM `group_id` —
DM-группа, маршрутизация через `etcp_router` идентична). Протокол расширяем под видео
(track-модель), реализуется только аудио. Работает desktop↔desktop и desktop↔android
(обе стороны компилируют общий стек `etcp_router` / `topo_group` (BGP) / `conn_mgr`).
Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE →
аудиопоток через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
### Входящие вызовы и телефонная занятость
Android хранит пять независимых настроек: показывать статус звонка, предупреждать вызывающего,
отклонять при телефонной занятости, отклонять все обычные звонки, отклонять все приглашения в рацию.
Они передаются маской `CALL_POLICY_*` через `[chat] incoming_call_policy` при старте,
а при изменениях — вместе с живым состоянием телефона через JNI в uasync.
Первые три настройки используют `READ_PHONE_STATE`; запреты всех входящих разрешения не требуют.
Наблюдатель учитывает все активные SIM, набор номера, удержание и входящий телефонный вызов.
Без разрешения или без телефонной службы состояние неизвестно, а не свободно.
Приоритет: запрет типа вызова → автоматический отказ при телефонной занятости → предупреждение с выбором.
Отказ при занятости возвращает `CALL_REASON_PHONE_BUSY`/`RADIO_CALL_PHONE_BUSY`.
Предупреждение переводит попытку в `WAIT_DECISION`: у вызываемого нет события INCOMING, рингтона и регистрации в Telecom.
Вызывающий видит «Абонент разговаривает по телефону», «Всё равно позвонить» и «Завершить».
Продолжение подтверждается повторяемой командой внутри той же попытки; освобождение телефона само не запускает звонок.
Повторные пакеты не создают повторных входящих событий. При изменении политики перед продолжением запрет проверяется снова.
Предел всей попытки — 60с от начала, включая выбор; продолжение deadline не меняет.
У рации первый ответ и подтверждение продолжения ограничены 2с; сообщение о занятости является ответом, но не RINGING.
Рация, уже включённая у адресата в указанном канале, возвращает `RADIO_CALL_ALREADY_ACTIVE`:
вызывающий получает уведомление, входящий вызов не показывается, его собственная рация не включается.
Этот ответ описывает существующую подписку и не создаёт нового входящего вызова даже при включённом запрете приглашений.
Принятие обычного приглашения в рацию по-прежнему сначала включает местную подписку, затем отправляет ACCEPT.
Android обычного звонка показывает своё уведомление после `onShowIncomingCallUi` и обрабатывает отказ Telecom.
## 2. Разбиение файлов (отдельный каталог `src/call/`)
| Файл | Язык | Сборки | Назначение |
|---|---|---|---|
| `src/call/call_proto.h` | C | везде | wire-формат, subcmd, svc_id `ETCP_RT_ID_CALL 0x36` |
| `src/call/call.h` / `call.c` | C | везде (autotools + GUI) | сигналинг, реестр сессий, таймеры, watchdog (20с по приёму), релей медиа с backpressure, счётчики |
| `src/call/call_audio.h/c` | C | только GUI-сборки | Opus encode/decode, фрейминг, pull-обвязка с тонами; регистрация через `call_audio_ops` |
| `src/call/call_tones.h/c` | C | только GUI-сборки | генератор тонов (glitch/ended), синус |
| `src/call/call_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
Диагностика: категория `call` (`DEBUG_CATEGORY_CALL`); транспортная доставка — `etcp_route`.
## 3. Протокол (сигналинг svc_id 0x36, первый байт payload = subcmd)
Формат приёма: `entry->dgram[0]=svc_id`, служебный заголовок роутера до
`ROUTER_SVC_PAYLOAD_OFF=26`, затем subcmd и тело команды.
```
CALL_INVITE 0x01 {call_id:8, group_id:8, num_tracks:1, force:1, expires_us:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
CALL_RINGING 0x02 {call_id:8}
CALL_ACCEPT 0x03 {call_id:8}
CALL_DECLINE 0x04 {call_id:8, reason:1}
CALL_BUSY 0x05 {call_id:8}
CALL_CANCEL 0x06 {call_id:8}
CALL_HANGUP 0x07 {call_id:8, reason:1}
CALL_END_ACK 0x08 {call_id:8}
CALL_PHONE_BUSY 0x09 {call_id:8}
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capture_epoch:8, codec_delay_samples:4, payload}
```
- `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах.
- `group_id` — CHAT-группа или DM-группа (общая маршрутизация).
- Сигналинг: `etcp_route_send(..., ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)`.
- Медиа: без подписи (уже внутри шифрованного канала etcp_router).
- `CALL_MEDIA` — общий контейнер; `track_id` оставляет место видео (`type=VIDEO`).
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с.
- Заголовок медиа — 36 байт с subcmd. `ts_ms` — время отправки для оценки сетевого джиттера;
`capture_pts_us` — время первого входного сэмпла в монотонной шкале отправителя, `INT64_MIN` при отсутствии метки.
`capture_epoch` различает разрывы захвата; `codec_delay_samples` передаёт Opus lookahead.
Старый формат не поддерживается; медиа принимается только от пира текущего звонка.
## 4. Машина состояний
```
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → ACTIVE(ACCEPT) → ENDED
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACTIVE(accept) → ENDED
```
`ENDED` reasons: `LOCAL_HANGUP / REMOTE_HANGUP / DECLINE / BUSY / CANCEL /
NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
Таймауты:
- **кодограмма INVITE 300мс** — вызывающий шлёт её весь период вызова
(`OUTGOING` + `RINGING`) как «сердцебиение»: покрывает потерю INVITE/RINGING и
держит маршрут живым;
- **watchdog 2с у вызываемого** — нет кодограммы 2с → вызов считается сорванным
(отмена потерялась), сессия закрывается локально (`CALL_REASON_CANCEL`);
- ring-timeout 60с от начала, включая ожидание решения вызывающего (нет ответа → CANCEL);
- `CONN_EVENT_DOWN/TIMEOUT` CM не завершают звонок: роутер использует доступные пути;
- **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP.
Завершающие кодограммы HANGUP/CANCEL/DECLINE повторяются каждые 300мс до END_ACK,
не дольше 20с. Это необходимо при смене эпохи роутера: старые очереди удаляются,
даже если отправитель ранее получил успешный результат постановки команды в очередь.
Медиа и их локальные очереди освобождаются сразу, ENDED публикуется один раз.
В реестре до ACK/таймаута остаётся только завершённая сессия с CM handle и таймером.
Получатель подтверждает также повторные команды после удаления сессии, без повторного ENDED.
Команда и ACK проверяются по подписи, шифрованию, размеру, call_id, пиру и группе существующей сессии.
ACK не подтверждается. Новый звонок имеет другой call_id и не затрагивается поздними командами старого.
При одновременном завершении стороны независимо подтверждают команды друг друга.
После ACK сессия удаляется на ближайшем tick завершения; при удалённом завершении CM удерживается
ещё 500мс для отправки ACK. Остановка ядра отменяет таймер без повторных отправок.
Запоздалые кодограммы вызова при идущем разговоре (`ACTIVE`) игнорируются.
Онлайн-проверка: `topo_node_find_by_id(group, peer) == NULL` → «не в сети» (event).
CM handle инициирует оптимальное подключение и удерживается до завершения звонка.
Медиа всегда идёт через `etcp_route_send`: роутер сам выбирает лучший путь и fallback.
Звонок не хранит ETCP_CONN и не завершается при потере отдельного транспорта.
Очистка записи CM отсоединяет handles и отменяет отложенные UP; handle закрывает владелец.
Закрытие CM handle не посылает DISCONNECT удалённым владельцам. Механизм NCD не меняется.
## 5. Отправка медиа — backpressure через локальный буфер
Требование: не дропать аудио при переполнении роутера — держим в локальном буфере
отправки, дренируем при появлении места.
Механика (`etcp_router` уже предоставляет):
- `etcp_route_send(force=0)` возвращает `-1`, когда `send_q` роутера полон
(`count >= ROUTER_MAX_SEND_Q_PACKETS`).
- `etcp_router_send_q_has_room(...)` — есть ли место (порог `ROUTER_MAX_SEND_Q_PACKETS-1`).
- `etcp_router_on_send_ready(..., &waiter, drain_cb, arg)` — одноразовый waiter:
колбэк вызывается, когда `send_q->count <= threshold` (освободилось место).
В сессии:
- `tx_q` — локальная FIFO очередь закодированных кадров (ll_entry с dgram), ёмкостью
15с (750 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
- Алгоритм (`call_tx_flush`): пока `tx_q` не пуст:
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=1)` (после проверки места);
- иначе — если waiter не зарегистрирован → `etcp_router_on_send_ready(... drain_cb)` и выход.
- `drain_cb(q, arg)` → снова `call_tx_flush(session)` (re-arm при необходимости).
- Весь код — в uasync-потоке (single-threaded, реентерабельность как в
`media_delivery.c:stream_send_chunk_cb`).
## 6. Потоки и жизненный цикл
`call_session`, CM handle, очередь TX и таймеры принадлежат uasync-потоку.
Аудиопоток кодирует PCM и постит `call_send_media`; приём в uasync кладёт Opus в `call_jitter`.
Декодер, SoundTouch и генератор тонов принадлежат аудиопотоку.
Кольцо кодированных кадров и снимок статистики защищены мьютексом; статистика не читает SoundTouch из другого потока.
`CallTimeline` хранит фактическое соответствие входных участков SoundTouch выходному PCM.
Платформа публикует позицию воспроизведения с локальной монотонной меткой; `call_audio_get_clock`
возвращает исходный PTS слышимого участка. Локальная тишина и тоны удерживают PTS.
Невалидная/устаревшая отметка устройства не разрешает продвижение видео. Контракт и ограничения — `call_video.txt`.
GUI запускает аудиоустройство по CALL_ACCEPTED. При CALL_ENDED начинается финальный тон;
устройство останавливается перед `call_audio_release`. Явное завершение звонка прекращает
воспроизведение накопленной речи и включает тон завершения — на обеих платформах.
Android останавливает гудки до настройки звонкового аудиотракта. После выбора маршрута
capture/play потоки независимо создают и запускают AudioRecord/AudioTrack; каждый сразу обслуживает своё направление.
`start()` подтверждает запуск подготовки, ошибки устройств передаются владельцу на main через `onFailure`.
Ошибка любого направления завершает звонок. Общий `stop()` отменяет подготовку, разблокирует I/O и ждёт оба потока
до освобождения Android-устройств и native-аудиостека. Отдельные locks защищают подготовку/остановку каждого устройства.
Замер AEC планируется через 300мс после готовности обоих направлений и не задерживает воспроизведение.
При наличии Telecom Connection режимом и выходом звонка управляет только Telecom.
ACTIVE запрашивается до создания аудиоустройств; AudioManager/прямой preferred device не дублируют выбор.
На API 34+ маршрут запрашивается через доступный CallEndpoint, на старых — через Connection.setAudioRoute.
Подтверждённый маршрут приходит из callback системы, отдельно от желания пользователя; ошибка запроса логируется.
Если Connection ещё нет, используется AudioManager. Поздняя регистрация передаёт управление Telecom,
снимая заявку приложения на режим и прямые устройства. Рация сохраняет отдельный AudioManager-путь.
Реальное время запуска/переключения зависит от Android и требует проверки на устройстве.
Маршрутизатор использует один снимок входных/выходных устройств; обновляет его при старте,
событиях устройств/BT-профиля и явной переоценке. По завершении снимок освобождается.
INFO-логи Android показывают длительности настройки режима, регистрации callback, BT-профиля, выбора маршрута,
создания/старта устройств, первого чтения микрофона и первой записи в AudioTrack, а также готовность другого направления.
Общий `call_audio` логирует время ожидания первого PCM; запись в AudioTrack может содержать начальную тишину.
Эффективный рабочий буфер AudioTrack на всех поддерживаемых API при старте запрашивается размером
два блока PCM (40мс); на API 31+ стартовый порог следует фактически принятому размеру.
Ёмкость выделенной памяти может быть больше. Ограничение повторяется по callback фактической
смены выхода на main, под trackLock; callback старого AudioTrack игнорируется и снимается при stop.
Подачу PCM задаёт свободное место в AudioTrack, без дополнительного таймера 20мс.
Частичная запись удерживает остаток одного блока до полной передачи; следующий pull ждёт.
Сетевой запас остаётся в адаптивном JB, его рост и SoundTouch-догон не изменены.
Лог `playback buffering` показывает причину настройки, запрошенный/принятый размер, порог и ёмкость;
ежесекундный `audio summary` — queueUs, текущий порог и underruns; новый episode underrun имеет уровень WARN.
Это очередь AudioTrack, а не полная сквозная задержка и не сетевой jitter reserve.
Ранее на Samsung startFrames=1920 был принят, но без ограничения рабочего буфера
written-played оставался 120–160мс; одного стартового порога недостаточно.
Замер Samsung подтвердил постоянную очередь 40мс. При прежнем параллельном управлении маршрутом
AudioTrack пересоздавался, очередь временно достигала 160мс, а ожидание свободного места — 328–336мс.
При старте AudioTrack.play занимал 1080мс. После перехода к единственному владельцу Telecom
в проверенном звонке старт занял 62мс, очередь оставалась не более 40мс и при смене выхода.
Переключение всё ещё останавливало потребление на 293–328мс; этот всплеск принимает сетевой JB.
На desktop `AudioDiag: capture clock` при call=debug выводит секундную сводку исходных backend-блоков:
число блоков/сэмплов, накопленную последовательность, диапазон ошибки относительно конца предыдущего блока,
latency PulseAudio, число скачков >2мс, hole-сэмплы и блоки без достоверного измерения.
Первый скачок за секунду содержит обе latency/поправки конвертера, expected/actual PTS и монотонное время.
Backend публикует только числовой снимок в ограниченное кольцо; форматирование выполняет reporter.
Исходный backend-снимок остаётся наблюдением. В capture worker позиция PCM задаёт непрерывные PTS,
а backend медленно корректирует их привязку: постоянная времени 2с, предел 0.5% (100мкс на 20мс PCM).
Скорректированное время используется и для Opus, и для выбора AEC-референса. Сам PCM не изменяется.
Разрыв sequence, смена route и скачок исходных меток >20мс по-прежнему сбрасывают capture/AEC и эпоху;
порог 2мс в call_audio не увеличивается. Секундный `voice-io: capture clock` показывает phase/error/adjustment.
Тесты проверяют десять минут дрейфа +/-300ppm, скачки оценки до 14мс, разбиение чанков 25мс на Opus 20мс,
сохранность PCM, явную потерю, смену устройства и большой разрыв исходных часов.
Счётчик sequence описывает полученные backend-сэмплы; отсутствие потерь очереди само по себе
не доказывает непрерывность физического захвата. Проверять вместе с hole/overflow и route-событиями.
Для захвата звонка desktop явно запрашивает период 20мс вместо дефолта PulseAudio 25мс;
фактический период сообщает `audio: call-capture initialized`. Сборщик по-прежнему принимает произвольные чанки.
Секундные замеры задержки при `call=debug`:
- `voice-io: capture latency`: возраст первого сэмпла перед native feed (включает сам блок 20мс),
подготовка/AEC в worker и вызов native feed; диапазоны min/avg/max в микросекундах.
- `voice-io: playback latency`: приблизительный остаток PCM после mix, включая частичный блок,
длительность pull и недостающие сэмплы. Это очередь приложения, отдельно от backend.
- `AudioDiag: playback clock`: latency backend, задержка конвертера/cache и максимальный размер
backend-порции в микросекундах. При measured=0 нулевые границы не означают нулевую задержку.
- Windows `AudioDiag: WASAPI queue`: текущий padding shared-mode, отдельный stream latency,
converter/cache, размер callback и незавершённого mapped buffer. Диапазоны — min/avg/max в мкс;
`padding_samples=0` означает отсутствие пригодного замера, а не пустую очередь.
Снимок снимается перед заполнением client PCM; запись и форматирование выполняет reporter.
`AudioDiag: audio timing` для стадий `callback`/`write` при `call=debug` показывает интервалы
callback и время блокирующей передачи в WASAPI. HRESULT ошибок и потери диагностической очереди логируются.
Снимки только наблюдают: Windows-аудиочасы остаются `callback-estimate` (`measured=0`).
- `video playout`: показанные кадры, `speed_ppm`, диапазон A/V-фазы, интервалы показа,
опоздание worker и ожидания аудиочасов. Опоздание кадра не вызывает пропуск; догон до 1.6x.
- Desktop `video RX worker`: число декодированных кадров, максимальное время декодирования,
возраст кадра после декодирования и опоздание пробуждения worker после sleep. Снимок аудио
содержит `audio_measured`, PTS и позиции played/written. Все эти строки относятся к `call=debug`;
`video=trace` для этой диагностики не требуется.
- `video GUI delivery`: доставленные изображения, ожидание уведомления, промежуток доставки,
`paint_wait_max_us` до подтверждения рисования. В GUI один ожидающий кадр, замены нет.
- `video port TX paused/resumed`: сохранённый хвост пачки, первый ID, длительность ожидания
и число повторов. Busy сохраняет encoded-кадры и не запрашивает KEY.
- `video port RX full/drained`: заполнение и полная разгрузка encoded FIFO перед возобновлением
приёма. Hysteresis переносит ограничение до новых входов sender encoder, освобождая запас для догона.
- `call audio TX timing`: ожидание g_mtx, обработка/кодирование под ним, ожидание posted task;
post включает ожидание lock при выполнении задачи, его разрешение 100мкс. На Android этот же
замер обработки включает программный AEC, на desktop AEC уже выполнен в capture worker.
- `call audio TX queue`: число ожидающих кадров, возраст старейшего и avg/max ожидания кадров,
переданных роутеру за интервал. Отправка роутеру не является подтверждением доставки пиру.
- Android `audio capture latency`: возраст первого сэмпла перед native feed и число измеренных
привязок; дополнительные поля старта устройств разделяют min-buffer query, constructor, routing и buffer setup.
Возраст PCM содержит часть измеренной обработки: эти числа нельзя механически складывать.
PTS/возраст используют локальный MONOTONIC, интервалы внутри native — локальный get_time_us/get_time_tb;
эти временные базы и часы разных устройств напрямую не вычитаются. Все сводки формируются вне device callback.
Период захвата 20мс и новые измерения требуют контрольного звонка; дополнительная desktop PCM-очередь,
жизненный цикл подготовки Android и таймер показа видео этим изменением не менялись.
На Android `CallPlaybackBuffer` подбирает рабочую очередь отдельно от JB/SoundTouch:
после 30с наблюдаемого продвижения playback head без underrun пробует уменьшить её на один
блок PCM (960 сэмплов, 20мс), до одного блока или фактического минимума устройства.
При росте AudioTrack.underrunCount добавляет один блок в пределах выделенной ёмкости.
Интервал следующей пробы удваивается (60/120/240/300с), чтобы не повторять частые неудачные уменьшения.
После старта, запроса/подтверждения маршрута и увеличения даются 2с на стабилизацию;
после уменьшения первый underrun учитывается немедленно. Отсутствие продвижения head или
пропуск наблюдений >1с обнуляет стабильный участок. Сброс системного счётчика учитывается отдельно.
Фактическая смена выхода начинает подбор с 40мс заново. Непринятая меньшая заявка фиксирует
нижнюю границу до следующего выхода; ошибки API выключают подбор с WARN/ERROR, сохраняя воспроизведение.
Reporter управляет размером под trackLock независимо от фильтра debug; playback не берёт этот lock.
Нет flush/recreate, частичная запись PCM сохраняется. После смены размера/выхода через секунду
повторно измеряется задержка для AEC. Ожидание полного буфера и недостача сети не увеличивают его размер.
Лог настройки содержит reason, previous/requested/actualFrames и прирост underruns;
достижение ёмкости предупреждает не чаще раза в 10с. Принятие и стабильность 20мс ещё требуют
проверки на Samsung; буферы HAL и Bluetooth могут добавлять собственную задержку.
На desktop адаптация устройства ещё не реализована: выход принадлежит SoundManager,
изменение backend-буфера должно выполняться его владельцем, а не отдельным звонком.
Диагностика Android при `call=debug`: playback сохраняет один числовой снимок на блок (20мс)
в кольце 512 элементов, без форматирования/вывода. Отдельный reporter раз в 200мс забирает записи;
раз в секунду выводит `audio summary` с min/avg/max очередей, задержек pull/write и опоздания потока,
целевым запасом/разбросом доставки JB, темпом, недостающим PCM и отдельными счётчиками JB/AudioTrack.
Снимок JB не сбрасывает события для CALL_STATS. encodedMs — очередь Opus после pull,
processingMs — оставшийся PCM SoundTouch в длительности исходной речи; reserveMs зависит только от доставки.
Reporter опрашивает AudioTimestamp, routedDevice, underrunCount и размеры буфера не чаще раза в 200мс.
Playback читает только опубликованный неизменяемый снимок. Его begin/end и возраст deviceAgeUs
отличают задержку опроса от задержки аудио; stampQueryUs/routeQueryUs/propertiesQueryUs показывают стоимость запросов.
Android API может удерживать общие внутренние locks даже при вызове из reporter: перенос убирает
прямое ожидание диагностического запроса в playback, но сам по себе не доказывает отсутствие влияния на write/head.
frame/time аппаратного timestamp и время начала опроса печатаются отдельно;
при недоступности стоит stampOk=0. Времена trace — MONOTONIC; очередь устройства — written-played.
Эти показатели нельзя складывать с возрастом AudioTimestamp как независимые задержки.
В playback отдельно измеряются writeCallUs (только вызовы write), headCallUs (чтение позиции),
progressCallUs (публикация позиции ядру), jbSnapshotUs (нативный снимок), writeSpanUs (весь цикл записи)
и blockUs (pull до формирования строки). При частичных записях длительности вызовов суммируются за блок.
pullGapExcessUs — превышение интервала между началами соседних pull над 20мс;
включает ожидание устройства, а не только задержку планировщика (прежнее поле wakeLateUs удалено).
Старое поле writeUs включало чтение позиции и прогресс: его нельзя трактовать как длительность одного AudioTrack.write.
Stop дожидается reporter, включая непрерываемый Android-запрос, до освобождения AudioTrack.
`audio detail` сохраняет 2с до и 3с после старта, фактической смены routedDevice или новой аномалии:
очередь устройства >100мс в течение секунды, отсутствие продвижения head 100мс при наличии данных,
либо новый episode underrun (затухает через 3с без повторений). Порог очереди — диагностический,
не настройка JB и не изменение воспроизведения. События внутри окна объединяются, между выгрузками
не менее 10с; продолжающаяся аномалия не создаёт повторных дампов. Восстановление логируется INFO.
Подробные строки содержат по четыре снимка и заголовок с порядком колонок; при stop окно может быть неполным.
Переполнение кольца при задержке reporter имеет отдельный WARN lost=N. Аудиоданные не сохраняются.
Выключение call=debug прекращает сбор/выгрузку; буфер и reporter освобождаются с playback worker.
Сравнивать сводки следует по call ID и monoUs; рост сетевого запаса сам по себе не является ошибкой.
## 7. Буфер звонка и восстановление
`call_jitter` независим от `radio_jitter`: рация сохраняет свои правила задержки и сброса.
Буфер звонка хранит до 750 кодированных кадров (15с): 10с перебоя и 5с запаса.
Целевой запас — `clamp(max-min, 60, max_reserve_ms)` мс по изменению задержки доставки:
на входящем кадре накапливается разность интервалов приёма и отправки, десять секундных интервалов хранят её экстремумы.
Отправитель уже передаёт timestamp в CALL_MEDIA; смещение часов пиров исключается, переполнение uint32 учитывается.
Подготовка местного аудиоустройства и ускоренное воспроизведение очереди не изменяют оценку доставки.
Минимум/максимум глубины очереди для GUI собираются отдельно в аудиопотоке за те же десять секунд.
Верхний предел задаётся `call_jitter_max_reserve_ms`: 500–2000 мс, по умолчанию 1000 мс.
Настройка есть в аудиопараметрах desktop и Android и применяется к следующему звонку.
Начальное и повторное накопление ждёт этот запас; увеличение запаса не прерывает текущую речь.
Ускорение зависит от превышения запаса. При отсутствии входящих кадров обычное воспроизведение
может исчерпать запас: это не жёсткий запрет воспроизведения ниже цели.
Статистика раз в секунду отображается в desktop-окне и Android-экране звонка (под кнопками): глубина, запас, минимум/максимум,
темп, RTT, удаления при переполнении и недостачи PCM. Диагностика буфера — категория `call=debug`.
После восстановления кадры последовательно декодируются; темп плавно растёт до 1.6x
без изменения высоты голоса: линейный предел роста 1.0x→1.6x за 500мс воспроизведения.
При уменьшении накопления целевой темп снижается к 1.0x; скорость снижения ограничена тем же пределом.
Пауза аудиоустройства не сбрасывает буфер. Переполнение удаляет старейший кадр с WARN и счётчиком.
Роутер сохраняет последовательность и неподтверждённые данные при смене пути.
Для сервиса звонка срок без прогресса ACK увеличен примерно до 20с; прочие сервисы сохраняют прежний срок.
Перезапуск удалённого процесса/роутерной сессии не является кратковременным перебоем пути и может сбросить данные.
SoundTouch: `USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4`.
## 8. Тоны и диагностика
- **Исходящий звонок — две фазы с разными гудками** (у вызывающего):
- фаза 1 «пытаемся связаться» (`OUTGOING`: INVITE отправлен/ретранслируется, маршрут
устанавливается) — гудок «соединение» (короткие бипы);
- фаза 2 «звоним» (`RINGING`: получен RINGING, у пира идёт вызов) — длинные гудки (ringback).
Гудки генерируются платформенно-нативно: desktop — цикличные MP3 через `SoundManager`
(`call_connecting`/`call_ringing`), Android — цикличные MP3 через `CallRingback`/`MediaPlayer`.
Переключение — по `CHAT_EVT_CALL_RINGING`,
остановка — по `CALL_ACCEPTED`/`CALL_ENDED`/`CALL_ERROR`/hangup.
- **Начальное ожидание первого PCM** → тишина без сигнала потери звука. Подготовка удалённого микрофона
и начальное накопление jitter не считаются перебоем. Существующий таймаут 20с без медиа действует с ACTIVE.
- **Недобор PCM ≥500мс после начала воспроизведения** → в `call_audio_pull_pcm` воспроизводится
`CALL_TONE_GLITCH`: `пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс)
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл. До порога — тишина при нехватке PCM.
При возобновлении PCM сигнал останавливается; одного входящего пакета без достаточного запаса недостаточно.
- **Завершение звонка → всегда** `CALL_TONE_ENDED`: 3 нисходящих 700/500/400 Гц по 150 мс.
- Счётчики: tx/rx кадры, `c_tx_dropped`/`c_rx_dropped`, дропы буфера, сталлы,
tempo-события, причины завершения. Логи `DEBUG_CATEGORY_CALL` (+стата раз в 1с).
## 9. GUI-события (extend `chat_event.h` + `gui_bridge.h` + android)
```
CHAT_EVT_CALL_INCOMING 32 [ch_id_len][ch_id][caller_node_id:8][call_id:8]
CHAT_EVT_CALL_RINGING 33 [call_id:8]
CHAT_EVT_CALL_ACCEPTED 34 [call_id:8]
CHAT_EVT_CALL_ENDED 35 [call_id:8][reason:1]
CHAT_EVT_CALL_DECLINED 36 [call_id:8][reason:1]
CHAT_EVT_CALL_STATS 39 [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2]
CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
```
## 10. Публичный API (call.h)
```
int call_start(ch_id, peer_node_id, out_call_id); // caller (post в uasync)
void call_accept(call_id); // callee
void call_decline(call_id);
void call_hangup(call_id);
int call_is_online(ch_id, peer_node_id); // BGP-проверка для UI
// аудио-поток (общий процессинг в C-ядре):
int call_encode_pcm(call_id, pcm, n, opus_out, cap); // single-writer: audio thread
int call_send_encoded(call_id, opus, len); // post → uasync → tx_q + flush
int call_pull_pcm(call_id, pcm_out, max); // single-writer: audio thread (ajb+tones)
void call_release(call_id); // post → uasync: освободить аудио-объекты
```
`call_init / call_destroy` — вызываются из `chat_core_init / chat_core_destroy`.
## 11. Платформенный I/O
- **Desktop:** `tools/chatgui/src/call_audio_engine.cpp` — miniaudio full-duplex
(capture → `call_encode_pcm`, `call_pull_pcm` → playback) + окно звонка (UI)
+ события в `gui_bridge_impl.cpp`.
- **Android:** JNI `nativeCall*` + Kotlin `AudioRecord`/`AudioTrack`.
**Переключение источника:** `AudioManager` (API31+ `setCommunicationDevice` +
`availableCommunicationDevices`; fallback `setSpeakerphoneOn` + `startBluetoothSco`),
детект гарнитуры (`ACTION_HEADSET_PLUG`, BT-профиль, `ACTION_SCO_AUDIO_STATE_UPDATED`),
кнопка-цикл трубка/громкая/гарнитура, авто-выбор гарнитуры. `nativeCallSetAudioRoute(route)`.
## 12. Сборка
- `src/Makefile.am` + `tests/Makefile.am`: `call.c` (C, без `.cpp`). `src/call/` в include path.
- `tools/chatgui/libutun/CMakeLists.txt`: +`call_audio.c`, `call_tones.c`,
`audio_jitter.cpp`, soundtouch sources (AAFilter, FIFOSampleBuffer, FIRFilter,
InterpolateCubic/Linear/Shannon, RateTransposer, SoundTouch, TDStretch, cpu_detect_x86,
mmx_optimized, sse_optimized) как C++. `call.c` попадёт через GLOB `src/*.c`.
- `tools/chatgui-android/libutun_lite/CMakeLists.txt` + `utun_sources.cmake`:
`LANGUAGES C CXX`, те же файлы.
- `src/transport_layer/etcp_api.h`: `#define ETCP_RT_ID_CALL 0x34`.
- `lib/debug_config.h`: `DEBUG_CATEGORY_CALL 31`, `DEBUG_CATEGORY_COUNT` → 32.
## 13. Порядок задач
1. `call_proto.h` + svc_id + `DEBUG_CATEGORY_CALL`.
2. `call.c` (машина состояний, backpressure-media, watchdog, события, API) + `chat_core` хуки.
3. `audio_jitter.cpp` (SoundTouch) + `test_audio_jitter.c`.
4. `call_tones.c` + `call_audio.c` (Opus + фрейминг + тон-обвязка).
5. Desktop: `call_audio_engine.cpp` + UI + события.
6. Android: JNI + Kotlin AudioRecord/Track + переключение источника + UI.
7. Сборка (Makefile.am / CMake обе).
8. `test_call.c` (два инстанса), интеграция desktop↔desktop и desktop↔android.
+292
View File
@@ -0,0 +1,292 @@
# DB Sync Protocol v2 — Техническое задание
## 1. Проблема
chain_hash = SHA256(prev_chain_hash || id || ts || datahash) — хеш-цепочка, где хеш каждой записи зависит от предыдущей.
`db_record_insert` (db_sync.c:341-463) при вставке записи в середину сортированного порядка делает каскадный пересчёт chain_hash всех последующих записей — O(N). При двустороннем обмене PUSH'ами получается O(N²).
## 2. Решение
**Разделить два концерна:**
- **Сравнение при синхронизации** — перейти на `datahash` (первые 8 байт SHA256(data), позиционно-независимый). Не требует каскада.
- **Целостность цепочки** — `chain_hash` остаётся в схеме БД, но используется только для однократной стартап-проверки и диагностики.
**Синхронизация:** per-peer позиционная (`synced_pos`). При PUSH каскад не делается. При SEND_DATA батч-вставка + один каскад после батча.
## 3. Модель данных
### 3.1. SI_PEER
```c
struct SI_PEER {
uint64_t node_id;
uint32_t synced_pos; // последняя подтверждённо общая позиция (0-based index)
uint8_t sync_state; // 0=not_synced, 1=syncing, 2=synced
};
```
- `synced_pos` = индекс последней записи в глобальном порядке `ORDER BY timestamp, datahash`, для которой chain_hash (а после v2 — datahash) подтверждённо совпадает у обоих пиров.
- При PUSH-вставке записи на позицию P, для всех peer'ов у которых `synced_pos >= P`: `synced_pos = P - 1`.
- При SEND_DATA: `synced_pos = from + received_count - 1`.
### 3.2. Схема БД (без изменений)
```sql
CREATE TABLE "db_sync_<name>_<id>" (
timestamp INTEGER NOT NULL,
datahash INTEGER NOT NULL,
id INTEGER NOT NULL,
chain_hash BLOB NOT NULL,
author INTEGER NOT NULL,
flags INTEGER NOT NULL DEFAULT 0,
data BLOB,
author_signature BLOB,
delivered_peers INTEGER NOT NULL DEFAULT 0,
delivery_chain TEXT NOT NULL DEFAULT '',
PRIMARY KEY (timestamp, datahash)
);
```
## 4. Wire-формат
### 4.1. Сообщения, которые меняются
#### INIT_SYNC
```
Было: [type:1][count:4][last_chain_hash:32] = 37 байт
Стало: [type:1][count:4] = 5 байт
```
#### INIT_RESP
```
Было: [type:1][tp:4][chain_at_tp:32][sc:1][(pos:4,chain_hash:32)*sc]
Стало: [type:1][tp:4][dh_at_tp:8] [sc:1][(pos:4,dh:8)*sc]
dh_at_tp — datahash на позиции tp (8 байт вместо 32)
sparse — datahash вместо chain_hash (8 байт на элемент вместо 32)
При sc=0 (exact match): длина 1+4+8+1 = 14 байт (было 37)
При sc=16 sparse: длина 14 + 16*12 = 206 байт (было 37 + 16*36 = 613)
```
#### SYNC_DONE
```
Было: [type:1][count:4][last_chain_hash:32] = 37 байт
Стало: [type:1][count:4][last_dh:8] = 13 байт
```
### 4.2. Сообщения, которые НЕ меняются
- **PUSH**: `[type:1][id:8][ts:8][dh:8][dlen:4][data][sig_len:1][sig]` — без изменений. `prev_ch` НЕ добавляется.
- **ACK_PUSH**: `[type:1][dh:8][ts:8]` — без изменений.
- **REFINE**: `[type:1][from:4][to:4][hc:1][(pos:4,dh:8)*hc]` — уже использует datahash, без изменений.
- **SEND_DATA**: `[type:1][from:4][count:2][(id:8,ts:8,dh:8,dlen:4,data,sig_len:1,sig)*count]` — без изменений.
- **ERROR**: `[type:1][code:1]` — без изменений.
## 5. Новые/изменённые функции
### 5.1. `db_cascade_from`
```c
static void db_cascade_from(struct DB_SYNC_INSTANCE* si, uint32_t from_pos);
```
Выполняет каскадный пересчёт `chain_hash` для записей с позиции `from_pos` до конца таблицы.
Алгоритм:
1. BEGIN IMMEDIATE
2. SELECT chain_hash записи на позиции `from_pos - 1` (или zero если from_pos = 0) как prev_ch
3. SELECT id, timestamp, datahash от позиции from_pos до конца
4. Для каждой: chain_hash = SHA256(prev_ch || id || ts || dh), UPDATE в БД, prev_ch = новый chain_hash
5. COMMIT
Выделяется из текущего кода `db_record_insert` (строки 412-451) в отдельную функцию.
### 5.2. `db_record_insert` — новый параметр
```c
static int db_record_insert(struct DB_SYNC_INSTANCE* si,
uint64_t id, uint64_t ts, uint64_t dh,
const char* json, size_t jlen,
const uint8_t* sig, size_t sig_len,
int do_cascade);
```
- `do_cascade=1` — после INSERT выполняется cascade (строки 412-451). Используется для локальных вставок.
- `do_cascade=0` — без cascade. Используется для PUSH и SEND_DATA (каскад делается отдельно).
### 5.3. `db_verify_chain` (стартап-проверка)
```c
static void db_verify_chain(struct DB_SYNC_INSTANCE* si);
```
Вызывается один раз после `db_sync_instance_add` (перед инициацией sync с пирами).
Алгоритм:
1. Проход записей 0..N-1 в порядке `ORDER BY timestamp, datahash`
2. Для каждой: chain_hash = SHA256(prev_ch || id || ts || dh)
3. Сравнить с хранимым в БД
4. При первом расхождении на позиции P: пересчитать цепочку от P до конца через `db_cascade_from(si, P)`. Завершить.
Сложность: O(N) SHA256, однократно при старте.
### 5.4. `si_find_pos` (новая)
```c
static uint32_t si_find_pos(struct DB_SYNC_INSTANCE* si,
uint64_t ts, uint64_t dh);
```
Возвращает позицию (0-based index) записи с ключом `(ts, dh)` в глобальном порядке. Используется после вставки PUSH для корректировки `synced_pos`.
### 5.5. `db_datahash_at` — уже существует (стр. 295)
Возвращает `datahash` на заданной позиции. Используется для сравнения в INIT_SYNC/INIT_RESP/SYNC_DONE.
## 6. Обработчики сообщений
### 6.1. PUSH (`db_handle_push`)
```
Было: db_record_insert(si, ..., /* cascade встроен */)
Стало: db_record_insert(si, ..., /* do_cascade= */ 0)
uint32_t pos = si_find_pos(si, ts, dh);
for each peer: if peer->synced_pos >= pos: peer->synced_pos = pos - 1;
```
### 6.2. INIT_SYNC (`db_handle_init_sync`)
```
Изменения:
- Принимает [count:4] вместо [count:4][chain_hash:32] (проверка len >= 5 вместо 36)
- Сравнение: datahash вместо chain_hash (8 байт вместо 32)
- sparse: datahash вместо chain_hash (8 байт на элемент вместо 32)
- Размер sparse-элемента: 4(pos) + 8(dh) = 12 байт (было 4+32=36)
- resp буфер: 4096 → достаточно (14 + 16*12 = 206 байт при max sparse)
```
### 6.3. INIT_RESP (`db_handle_init_resp`)
```
Изменения:
- Принимает [tp:4][dh:8][sc:1][...] вместо [tp:4][ch:32][sc:1][...]
- Проверка len >= 13 вместо 37
- Сравнение: datahash вместо chain_hash
- Пустая БД пира: zero-хеш 8 байт вместо 32
- Sparse элементы: 12 байт вместо 36
- При совпадении: synced_pos = tp (в дополнение к sync_state = 2)
```
### 6.4. SEND_DATA (`db_handle_send_data`)
```
Стало:
struct SI_PEER* sp = si_peer_find(si, src);
uint32_t fix_from = sp ? sp->synced_pos : 0;
for each record:
db_record_insert(si, ..., /* do_cascade= */ 0);
db_cascade_from(si, fix_from);
sp->synced_pos = from + received_count - 1;
sp->sync_state = 2;
```
Каскад от `fix_from` (позиция, которая была synced ДО этого батча) гарантирует, что все chain_hash после этой точки пересчитаны — независимо от того, какие PUSH'и испортили их между синхронизациями.
### 6.5. SYNC_DONE (`db_handle_sync_done`)
```
Изменения:
- Принимает [count:4][last_dh:8] вместо [count:4][last_chain_hash:32] (len >= 13 вместо 36)
- Сравнение последнего datahash вместо chain_hash
```
## 7. Механика многопировой синхронизации
При наличии нескольких пиров с одинаковым `sync_state` (например, syncing=1) выбирается пир с минимальным `synced_pos` — у него самый старый общий префикс. После завершения его синхронизации выбор повторяется.
Это гарантирует: двигаемся от самого старого несинхронизированного участка, не прыгая. Новые записи (в хвосте) синхронизируются последними.
## 8. Полный сценарий: три участника A, B, C
```
Начальное состояние: все пусты.
=== Шаг 1. A создаёт R1 (ts=1000) ===
A: PUSH R1 → B, C. Все вставляют в конец, cascade 0.
synced_pos = 0 у всех.
=== Шаг 2. B создаёт R2 (ts=2000), C создаёт R3 (ts=1500) ===
B: [R1(1000), R2(2000)], PUSH R2 → A, C.
A: R2 в конец. C: R2 в конец.
C: [R1(1000), R2(2000)]? Нет — C создал R3(1500).
C: [R1(1000), R3(1500)] локально, потом PUSH R2 → R2.ts=2000 → в конец.
C: [R1(1000), R3(1500), R2(2000)]
C: PUSH R3 → A, B.
A: [R1(1000), R2(2000)] + R3(1500) → между R1 и R2:
A: [R1, R3, R2] ← R3 в середину, cascade ПРОПУЩЕН
B: [R1, R3, R2] ← аналогично
synced_pos после PUSH R3 на A относительно B:
A вставил R3 на поз.1 → synced_pos_B: было 1 → стало 0
(A считает, что совпадает с B только на позиции 0)
=== Шаг 3. A инициирует sync с B (чемпион: min synced_pos) ===
A→B: INIT_SYNC [count=3]
B→A: INIT_RESP [tp=min(3,3)-1=2, dh_at_tp=H2, sc=2, sparse:{(1,H3),(0,H1)}]
A проверяет: A@2=R2→dh=H2✓, A@1=R3→dh=H3✓, A@0=R1→dh=H1✓
→ sync complete, synced_pos_B=2, sync_state=2
=== Шаг 4. Рестарт A ===
db_verify_chain:
pos 0: R1.ch = SHA256(0||R1) ✓
pos 1: R3.ch = SHA256(R1.ch||R3) ✓ (посчитан при PUSH без cascade — корректен)
pos 2: R2.ch = SHA256(R3.ch||R2) ✗ (STALE: был посчитан как SHA256(R1.ch||R2) до PUSH R3)
→ расхождение на pos=2 → db_cascade_from(2): пересчёт R2.ch = SHA256(R3.ch||R2) ✓
=== Шаг 5. A, B, C активно обмениваются ===
Каждый PUSH: вставка без cascade.
Периодические sync (по min synced_pos): SEND_DATA + один cascade от synced_pos.
synced_pos растёт.
```
## 9. Что НЕ трогать
- `db_sync_insert_signed` — локальная вставка. ts всегда монотонный, запись в конец, `do_cascade=1`, каскад всегда 0 строк.
- TTL cleanup (`db_sync_instance_ttl_cb`) — удаление старых записей. Нужен cascade после удаления (уже существующая логика, не меняется).
- ACK_PUSH, ERROR, REFINE — без изменений.
- Схема БД — без изменений.
- `etcp_bind`/эпилог — без изменений.
## 10. Сложность операций (итого)
| Операция | Каскад | Сложность |
|----------|--------|-----------|
| Локальная вставка | 0 строк (конец) | O(1) |
| PUSH (приём) | нет | O(1) |
| SEND_DATA (батч N записей) | 1 каскад после батча | O(N + tail) |
| Стартап-проверка | 1 раз | O(total) |
| Sync-протокол (INIT_SYNC/INIT_RESP/REFINE) | нет cascade | O(log N) сообщений |
## 11. Порядок реализации
1. **Выделить `db_cascade_from(si, from_pos)`** из тела `db_record_insert`.
2. **Добавить параметр `do_cascade`** в `db_record_insert`. При `0` — только INSERT, cascade вызывается отдельно.
3. **Добавить `synced_pos`** в `struct SI_PEER`. Инициализировать в 0.
4. **Добавить `si_find_pos(si, ts, dh)`** — поиск позиции записи в глобальном порядке.
5. **Изменить wire-формат** INIT_SYNC, INIT_RESP, SYNC_DONE (chain_hash → datahash, 32→8 байт).
6. **Изменить обработчики**:
- `db_handle_init_sync`: новые размеры, datahash вместо chain_hash
- `db_handle_init_resp`: новые размеры, datahash вместо chain_hash, обновлять synced_pos
- `db_handle_send_data`: `do_cascade=0`, `db_cascade_from(fix_from)`, обновить synced_pos
- `db_handle_push`: `do_cascade=0`, скорректировать все synced_pos
- `db_handle_sync_done`: обновить synced_pos
7. **Добавить `db_verify_chain(si)`** — вызвать при старте после `db_sync_instance_add`.
8. **Многопировая синхронизация**: в `db_sync_peer_check_cb` выбирать пира с min synced_pos вместо первого попавшегося.
-222
View File
@@ -1,222 +0,0 @@
# DM: доставка сообщений и файлов
Личные беседы принадлежат `src/dm/`. Общие CHAT-группы используются для знакомства,
проверки ролей и маршрутизации; группа не становится идентичностью беседы.
## Реализовано
- История и постоянная исходящая очередь сообщений в общей SQLite ядра.
- Доставка через живой маршрут router, включая BGP-транзит.
- При отсутствии маршрута — подписанный PUT доступному суперузлу общей группы сторон.
- Приём после проверки подписи и расшифровки, подтверждение только после commit.
- Доставка с суперузла после возвращения получателя, независимо от отправителя.
- Точное удаление промежуточного сообщения и сохранение квитанции для поздних повторов.
- Подготовка, шифрование, проверка и durable публикация медиа в рабочих потоках.
- Прямая передача файлов либо custody на отдельном M, доставка после restart и подтверждённое удаление.
- TTL и квота M; вложения и статусы доставки в Qt и Android.
- Голосовые Opus и видео MP4 с зашифрованными метаданными; общий ввод и отображение с каналами.
## Идентичность и шифрование
```text
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
content_key = SHA256(X25519(my_private, target_public) || "utun_dm_content")
msg_nonce = SHA256(conv_id || author || seq)[..13]
media_nonce = SHA256("utun_dm_media" || author || media_id || part)[..13]
```
Сообщения и медиа используют один content_key target, без ключей отдельных файлов.
AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная порция plaintext — 65535 байт;
порция медиа — 32768 байт. UUID медиа новый для каждой подготовки. Повтор доставки
использует неизменяемый ciphertext. Повторная подготовка после ошибки требует нового UUID.
Каноническое тело сообщения:
```text
[conv:8][seq:8][ts:8][author:8][ct_len:1][ct][enc_len:2][ciphertext+tag][author_sig:64]
```
Автор подписывает все поля до sig. conv связывает автора с получателем.
Для `ct=file` перед подписью добавляется descriptor `[UUID:16][plaintext_size:8][SHA256(ciphertext):32]`.
В зашифрованном теле находится `attachment_info`; пути хранения строятся только из UUID.
Размер plaintext сообщения ограничен 1024 байтами. seq — положительное int64.
```text
[kind:1][name_len:2][duration_ms:4][width:2][height:2][UTF8_name:name_len][waveform:100, только voice]
```
Поля метаданных многобайтной длины имеют big-endian формат. kind: 1=file,
2=voice, 3=video. Имя — basename до 255 байт, без разделителей, управляющих символов
и некорректного UTF-8. FILE имеет нулевые duration/dimensions; VOICE — положительную
длительность и 100 символов уровней waveform; VIDEO — длительность и размеры.
Длительность ограничена 24 часами, размеры — 16384. Метаданные проверяются до
локального commit и до подтверждения получателем; S/M видят только ciphertext.
Медиа ciphertext — последовательность порций с отдельным tag каждой порции.
Последняя порция короче; пустой файл содержит один tag. Размер plaintext и SHA256
ciphertext должны входить в подписанный manifest. Потоковая расшифровка проверяет все
tags, точный размер и хеш; владелец публикует временный файл только после успеха
и собственного durable сохранения.
## Владение и данные
- `dm_core`: беседы, история, outbox и подтверждение получателя.
- `dm_mailbox`: промежуточные сообщения суперузла и квитанции.
- `dm_crypto`: вывод ключей, nonce, CCM и файловые примитивы без БД/сети.
- `dm_media`: файлы endpoint, загрузки и отдельная custody M.
- `dm_mailbox_media`: durable задания S, выбор M и повтор удаления до DELETE_ACK.
- `file_transfer`: подписанная передача байтов, backpressure и отдельный CM handle каждой операции.
- `attachment`: формат, валидация и тип метаданных, без сети и БД.
- `voice_file`: общий Opus-кодек отправки; `attachment_send` готовит файл в worker
и передаёт результат отдельно в `chat_core` либо `dm_media`.
```text
dm_conversations(conv_id, peer_node_id, peer_x25519, peer_ed25519, peer_name,
group_id, last_out_seq, last_in_seq, created_at, last_ts)
dm_messages(conv_id, dir, seq, ts, author, ct, data, sig)
dm_outbox(conv_id, seq, body, retry_at)
dm_pending_messages(recipient, sender, seq, msg, group_id, retry_at)
dm_mail_receipts(recipient, sender, seq, receipt, group_id)
```
Запись исходящего сообщения, увеличение seq и запись неизменяемого body в outbox —
одна транзакция. При ошибке откатывается всё, сообщение не выходит в сеть.
Приём, создание/обновление беседы и история — также одна транзакция.
Dedup — точный ключ (conv,dir,seq) с проверкой сохранённой подписи. last_in_seq —
статистика максимального номера, не курсор подтверждения: seq=99 после seq=100
сохраняется. Тело с другой подписью для уже сохранённого seq отклоняется.
У каждой локальной очереди таймер 1с; порция обработки до 32 задач, повтор задачи
через 5с. retry_at предотвращает голодание хвоста очереди. Перед отправкой проверяется
backpressure router. Потеря пути сохраняет durable задачу; остановка отменяет таймеры,
но сохраняет БД. При старте очередь продолжает работу.
## Подтверждения и протокол
Сервис `ETCP_RT_ID_DM=0x34`: MSG=1, RECEIPT=2.
```text
["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(message):32][recipient_sig:64]
```
Квитанцию создаёт только получатель после commit. Подпись охватывает первые 72 байта.
Отправитель удаляет outbox только после проверки подписи и точного хеша тела.
История беседы сохраняется. Подтверждение транспорта router не заменяет квитанцию.
Сервис `ETCP_RT_ID_DM_MAILBOX=0x35`:
```text
PUT=1 [recipient:8][message]
DELIVER=2 [message]
RECEIPT=3 [receipt:136]
STORED=4 [conv:8][seq:8]
```
Все пакеты идут с подписью router; unsigned control отклоняется.
PUT принимается только от автора, при роли supernode=yes у локального узла
в общей группе автора и получателя. Роль берётся из подписанного adm_tags;
node_type=4 — производная сетевой классификации, для авторизации не используется.
storage=yes само по себе не даёт права хранить сообщения.
STORED подтверждает durable запись суперузлом, outbox до квитанции получателя остаётся
у автора. Суперузел повторяет DELIVER при появлении живого маршрута. RECEIPT проверяется
по точному сохранённому телу; запись квитанции и удаление копии — одна транзакция.
Квитанция пересылается автору. Поздний PUT возвращает её и не возрождает задачу.
dm_mail_receipts содержит только служебные квитанции; доставленное содержимое
с промежуточного узла удаляется. Ранее использовавшийся dm_mail/PULL/HELLO и
диапазонные ACK не поддерживаются: это devel, совместимость не сохраняется.
TTL текстовых сообщений не введён; недельный TTL из pm.txt относится к медиа.
## Доставка медиа по pm.txt
1. Общая передача байтов: явные peer/group/file/size, backpressure и CM handle
на всю активную операцию. Владение handle сохраняется после DOWN/TIMEOUT.
Используется `ETCP_RT_ID_FILE_TRANSFER=0x37`. Один callback читает/отправляет
не более 8 КиБ, продолжение через `call_soon`; inflight ограничен 16 пакетами.
2. Отдельный owner dm_media: подготовка ciphertext, manifest, отправка/скачивание,
временные файлы, TTL и лимит хранения. Каталоги dm_media/ и dm_pending/ вне
группового обхода очистки orphan-файлов; групповые объявления HAVE_BLOCK не используются.
3. Суперузел сначала durable регистрирует сообщение и media job, затем выбирает M.
M скачивает ciphertext у A и сообщает S подтверждённое хранение, место и expiry.
4. Текстовая квитанция и квитанция медиа независимы. Получатель подтверждает медиа
после проверки, публикации файла и commit. S хранит задачу удаления у M до DELETE_ACK;
поздний STORED не возрождает завершённую доставку.
5. TTL медиа `dm_media_ttl_sec` по умолчанию 604800 с; квота `dm_media_storage_mb`
по умолчанию 1024 МиБ. Настройки на M. Новая custody отклоняется при
недостатке квоты; уже подтверждённые копии не вытесняются молча раньше срока.
6. Stop снимает новые задачи и закрывает transfer/CM handles, затем ждёт
media_async с живыми owner-контекстами. Группы уничтожаются после owners.
Control `ETCP_RT_ID_DM_MEDIA=0x38`: OFFER, STORE, STORED, RECEIPT, DELETE,
DELETE_ACK, EXPIRED, REJECT; точные форматы заданы в `dm_media.h` и реализациях owners.
Подпись router обязательна. Media receipt содержит `DMFILE01`, conv, seq, author,
recipient, UUID и SHA256 полного подписанного сообщения; B подписывает первые 88 байт.
Отдельные таблицы `dm_files`, `dm_custody`, `dm_custody_done`, `dm_media_jobs`,
`dm_media_holders` сохраняют состояния и tombstones. Все выданные grants M
учитываются при удалении; повторный STORE не продлевает TTL.
Qt и Android получают историю через события ядра: 41 — conv + JSON сообщений,
45 — JSON бесед. Снимки ограничены 200 записями. Отправка вложения вызывает
`dm_send_file` в uasync; длительная обработка выполняется в `media_async`.
Android копирует выбранный URI и сохраняет полученный файл через `Dispatchers.IO`;
Qt сохраняет файл из контекстного меню в отдельном потоке. Выбор «как файл» сохраняет
оригинал; «как видео» в Qt использует общий FFmpeg worker и метаданные конечного MP4.
Android записывает MP4 через CameraX; импорт MP4 проходит через платформенный
MediaMetadataRetriever в IO. Другие контейнеры на Android отправляются как файлы.
Голосовая запись отдаёт worker собственный PCM либо компрессор без копирования
в GUI/uasync; worker выполняет flush, Opus-кодирование и вычисление waveform.
Кодирование Qt и Android использует один `voice_file`. Адресат фиксируется до
записи, выбора URI и подготовки; завершение не читает текущую беседу GUI.
Подготовленный PM-файл и Android cache удаляются после копирования, ошибки или отмены;
пользовательский исходник Qt остаётся во владении пользователя.
Событие 46 `[target_len:1][target][job_UUID:16][state:1]` сообщает начало (0),
готовность (1) и ошибку (2). Target канала — числовой ID, PM — `dm:<conv_id>`.
Подготовка, доставка и воспроизведение имеют независимые состояния. UI отображает
только задания текущей беседы. Состояния файла и delivery receipt приходят в JSON
41; `content_type`, duration, dimensions и waveform преобразуются в общие роли
сообщения. Канальные скачивания вызываются контроллером только для каналов;
PM-файлы доставляются автоматически через `dm_media`.
Android использует общие `ConversationComposer`/`ConversationMessages` для обеих
бесед; Qt — `InputBar` и `MessageDelegate`. Превью Qt создаются в общем пуле
с двумя workers; декодирование длинного Opus-файла выполняется вне GUI. Оба плеера
проверяют заголовок, пакеты и end marker; Qt ограничивает PCM буфер 256 МиБ.
## Проверки
`test_dm`: симметрия ключа target, CCM round-trip, пустое тело, пределы длины,
неверный ключ/tag, разделение nonce; потоковые файлы на границах 32 КиБ,
неверные author/UUID/hash, обрезание и лишние байты.
`test_dm_e2e`: реальные A–C–B соединения и BGP; нет физического A–B линка.
Проверяет out-of-order, dedup/conflict, подписи, SQL rollback при приёме и enqueue,
ACK чужого тела, отказ mailbox без роли, точное удаление и поздний PUT.
Первый сценарий передаёт 40 offline-сообщений (больше порции pump) и перезапускает источник с pending outbox; второй перезапускает
суперузел и доставляет первое сообщение при выключенном источнике.
`test_dm_media`: реальные A/B/S/M с раздельными ролями supernode и storage;
прямая передача, custody, restart S/M при выключенном A, точный файл на B,
удаление копии и тела S, поздние STORE/STORED, TTL, отказ по квоте, неверная квитанция,
SQL rollback регистрации, состояния JSON и отмена операции с CM handle.
Голосовое передаётся напрямую; метаданные видео сохраняются после offline custody
и restart S/M при выключенном источнике.
`test_dm_worker`: повторное шифрование 8 МиБ не менее 0,5 с и проверка расшифровки;
work выполняется вне uasync, done в его потоке, heartbeat продолжает работать.
Destroy отменяет уже posted completion, callback вызывается ровно один раз.
`test_attachment`: строгий формат, повреждения и граничные длины, реальное
кодирование/декодирование 50 Opus-кадров, длительность и waveform.
`test_attachment_send`: выбранная беседа после смены текущего target, освобождение
PCM в worker, остановка до completion без регистрации и удаление отклонённого cache.
Qt `test_voice_file`: реальные PCM/Opus, повреждённые файлы и отмена async decoder.
Проверка 01.10.2026: чистые сборки Qt и Android APK; полный `check.sh` —
117 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root), proxy/burst/load PASS.
Qt CTest: 7 PASS, 0 FAIL; Android APK собран после `clean`.
В тесте worker максимальный интервал heartbeat составил 10,3 мс.
Интерактивная проверка GUI на телефоне и десктопе не выполнялась.
-118
View File
@@ -1,118 +0,0 @@
# Зашифрованные вложения группового чата
В desktop chatgui после выбора файла или перетаскивания в чат рядом с его именем
появляется флажок «Зашифровать вложение». Он применяется к выбранному файлу,
изображению или видео. Удаление, замена файла и переход в другую беседу сбрасывают
флажок. Выбор фиксируется вместе с адресатом при отправке, до подготовки.
В DM флажок скрыт: используется существующее автоматическое E2E-шифрование лички.
Android показывает зашифрованное вложение отдельной карточкой со статусом запроса
ключа. После одобрения карточка обновляется на исходный файл, изображение, видео
или голосовое. В интерфейс передаётся только расшифрованный путь; контейнер `.uenc`
нельзя открыть или сохранить как готовое вложение. Цитаты и список чатов показывают
название «Зашифрованное вложение» вместо manifest.
Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую
подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер
передаётся через обычные media_index/media_delivery. UUID транспортного медиа
и crypto_UUID имеют разные назначения: первый идентифицирует сетевые блоки,
второй — контейнер и ключ. Повтор доставки передаёт прежний контейнер.
## Подписанное сообщение и контейнер
В подписанном сообщении `ct=application/x-utun-encrypted`, базовое имя тела —
`<crypto_UUID:32hex>.uenc`. Остальные поля manifest описывают шифротекст:
размер, SHA256 всего контейнера, UUID и подписи блоков. Ключ, исходное имя,
MIME, waveform и остальные медиаметаданные в историю канала не записываются.
Контейнер:
```text
magic "UTENC001":8
crypto_UUID:16
plaintext_size:8BE
encrypted_metadata_size:2BE
SHA256(encrypted_file_body):32
encrypted_metadata:variable
encrypted_file_body:variable
```
Метаданные содержат MIME в 32 байтах и сериализованный `attachment_info`.
Каждая часть шифруется AES-256-CCM с тегом 16 байт и nonce 13 байт.
Nonce метаданных использует `part=UINT64_MAX`; файл — порции 32768 байт
с номерами от нуля. Общие примитивы находятся в `media_async/media_crypto`;
формат существующего DM при их выделении не менялся.
Получатель проверяет подписанный SHA256 полного контейнера, UUID, tag метаданных,
все tags файла, точный размер и hash тела. Результат пишется во временный файл
и публикуется после успешной проверки и durable rename. Все длительные файловые
операции и криптография выполняются в media_async.
## Хранение и разрешения
Локальные таблицы не регистрируются в db_sync или Merkle:
- `attachment_keys`: ключ автора/получателя, связь с общим message_id,
crypto_UUID, hash и пути двух копий, расшифрованные метаданные и состояние.
- `attachment_key_requests`: один запрос на `(channel,message_id,peer)`,
направление, решение и дата создания.
- `attachment_key_outbox`: команда, адресат и время следующей попытки.
Шифротекст хранится в `media/<channel>/<crypto_UUID>.uenc`, локальная открытая
копия — в `decrypted/<channel>/<crypto_UUID>.file`. В media_files и объявлениях
держателей остаётся только шифротекст, включая backfill после restart.
Квота медиакеша учитывает обе копии; очистка кеша не удаляет ключ. При повторном
открытии удалённая открытая копия восстанавливается с сохранённым ключом.
После полной проверенной загрузки обычный клиент автоматически запрашивает
ключ автора. Фоновая копия узла с подписанной ролью `storage=yes` запрос не
создаёт; ручная пользовательская загрузка на таком узле запрашивает доступ.
Сетевая классификация `peers.storage` для этого не используется.
В шапке канала кнопка «Запросы расшифровки» показывает число запросов без решения.
Список содержит участника, вложение, дату и отдельные кнопки выдачи/отказа.
Двойной клик переводит к исходному сообщению. После выдачи видны ожидание доставки
и подтверждённая доставка ключа; отказ также сохраняется.
## Адресный протокол
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x40`. Каждый пакет обязан иметь
`ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED`; промежуточные узлы ключа не видят.
Протокол не создаёт личную беседу и не публикует служебные сообщения в канал.
```text
magic "AK01":4 | command:1 | message_id:32 | crypto_UUID:16 | cipher_hash:32
GRANT дополнительно содержит key:32
REQUEST=1, REQUEST_ACK=2, GRANT=3, DENY=4, RESULT_ACK=5
```
Автор принимает запрос только на собственное опубликованное сообщение с
сохранённым ключом. Участие запрашивающего проверяется по полному подписанному
мемберу, join и дереву приглашений; удалённые мемберы отклоняются. Проверка
повторяется при решении и перед каждой отправкой GRANT. Получатель принимает
ответ только от автора, по своему существующему запросу и точной идентичности
контейнера. Конфликтующие ключи и результаты отклоняются.
REQUEST_ACK отправляется после сохранения запроса. GRANT/DENY появляются
в outbox только после сохранения решения. Полученный ключ сохраняется до
RESULT_ACK и до запуска расшифровки. Повторы возвращают прежнее решение и не
создают новых строк. Таймер обрабатывает ограниченную порцию очереди с
backpressure и повтором через 5 секунд; restart продолжает очереди.
Для доставки нужен живой маршрут обеих сторон: офлайн-mailbox этого сервиса
не реализован. Запрос ждёт появления автора; решение ждёт появления получателя.
Уже выданный ключ невозможно отозвать у получателя. Защита содержимого не
скрывает автора, канал, время и размер шифротекста. Автоматическая публикация
транскрипции зашифрованного голосового в канал не запускается.
## Проверки
`test_encrypted_file`: границы порций, пустой файл, metadata roundtrip,
неверные ключ/автор/UUID/hash, подмена метаданных с пересчитанным внешним hash,
лишние байты и отсутствие частичного plaintext после отказа.
`test_encrypted_attachment`: реальные A–S–B/C, скачивание с S при выключенном A,
перезапуски автора с запросами и получателя с ключом, выдача B и отказ C,
отсутствие ключа на S/C, отрицательные control-пакеты, повтор GRANT,
восстановление удалённого plaintext без нового согласия и удаление канала.
+10
View File
@@ -0,0 +1,10 @@
В конфиге явно прописываются сокеты с ip и портом.
Через эти сокеты идёт весь трафик, случайные порты клиент не использует - только эти сокеты.
Соответственно, возможен только один линк между двумя пирами по одному каналу.
виртуальные сокеты - плохо т.к. используются ip/port для поиск подключения
./configure --with-openssl - openssl
./configure --without-openssl - tinycrypt
-178
View File
@@ -1,178 +0,0 @@
# Разбор полного прогона 2026-09-27
Исходный результат `./check.sh` вне песочницы: 92 passed, 5 failed, 1 skipped (98 тестов).
Песочница запрещает сетевые сокеты; её результаты не использовались для оценки протокола.
Промежуточный прогон: 96 passed, 1 failed, 1 skipped; оставался `test_chat_join_e2e`.
После KEY_REGISTER_ACK и исправления невыровненных чтений повторный полный
`make check -j4`: **97 passed, 0 failed, 1 skipped (98)**.
Лог: `/tmp/utun-ack-final-check.log`. Исходное падение отдельного root-теста и
последующая доработка описаны ниже. После исправления autosockets общий набор дал
97 PASS / 1 SKIP, а `check-proxy`, `check-burst`, `check-load` прошли:
`/tmp/root-fix-full-check.log`.
`test_etcp_lifecycle`, `test_etcp_link_stress`, `test_conn_mgr_phases` дополнительно
прошли ASan/UBSan/LeakSanitizer. Инструментированы тесты и изменённые транспортные/
маршрутные модули, остальные объекты библиотек взяты из обычной сборки.
Логи: `/tmp/ncd-late-asan.log`, `/tmp/link-role-asan.log`, `/tmp/cm-phases-asan.log`.
## Исправленные причины
- `test_conn_mgr_phases`: после TIMEOUT фазы DIRECT NCD навсегда подавлял UP,
хотя conn_mgr сохранял handle для REVERSE. В логе физический ETCP уже UP,
но conn_mgr продолжал DIRECT_REQ до своего таймаута. TIMEOUT теперь обозначает
истечение срока первой попытки, CLOSED — окончательное завершение. Удерживаемый
handle получает поздний UP. Отдельный lifecycle-регрессионный тест воспроизводит
TIMEOUT → INIT/UP (одно уведомление) → DOWN → UP; до исправления он падал.
- `test_etcp_link_stress`: ожидал reset всей сессии при коллизии дополнительного
линка. Проверка заменена на сохранение эпох, отсутствие reset и дублей линков,
доставку и ACK сообщений через новый линк. Прежний тест забирал сырые фрагменты
вместо normalizer и мог считать служебные пакеты полезным трафиком. Теперь он
использует отдельный ETCP service и проверяет каждый байт и порядок сообщений.
Эта проверка выявила ошибку реализации: при принятии встречного INIT сохранённый
исходящий линк не менял `is_server`. Роль обновляется при отправке INIT_RESPONSE.
- `test_bgp_route_exchange`: после разрыва B–C recovery успешно строил C–A,
нарушая предположение теста о фиксированной цепочке. Теперь A и C принимают
только ключ B: все прежние проверки withdraw/restore сохранены, обход запрещён
условиями тестовой сети. Автоматический обход отдельно проверяет `test_topo_recovery`.
- `test_tcp_io`: адрес 192.0.2.1:81 в окружении устанавливал TCP-соединение,
поэтому connect timeout закономерно не срабатывал. Теперь используется локальный
listener с заполненной accept-очередью, проверяется ETIMEDOUT. Перед уничтожением
uasync тест завершает отложенное освобождение TCP и проверяет баланс таймеров.
Диагностика тестов включается `UTUN_TEST_DEBUG=1`; обычный запуск не включает DEBUG.
Остаточные DATA после удаления соединения могут быть undecryptable и сами по себе
не означают ошибку транспорта.
## Исправлено: гонка регистрации приглашения
`test_chat_join_e2e`, happy path A != C, падал из-за отсутствия подтверждения KEY_REGISTER.
Раньше `chat_invite_build_link()` вызывал `chat_join_register_key()` и сразу возвращал ссылку.
Успех `etcp_route_send()` означает постановку в очередь. Он не подтверждает обработку
ключа посредником C. Параллельное подключение J–C может завершиться раньше доставки A–C.
Доказательство: `/tmp/join-e2e-debug.log`, один ключ `90c4b081833450a0`:
- 01:39:02.437851 — A пишет `join-key registered`, фактически ключ только поставлен в отправку.
- 01:39:02.446527 — C отвергает JOIN_INFO_REQ: ключ ещё неизвестен.
- 01:39:02.449724 — C сохраняет этот ключ, но J уже получил терминальный ERROR.
Теперь C отвечает KEY_REGISTER_ACK только после сохранения ключа. A сопоставляет
channel/target/key и выдаёт ссылку после успешного ACK и локального сохранения ключа.
Отрицательный ACK завершает запрос ошибкой. Чужие, неподписанные, незашифрованные,
повторные и запоздалые подтверждения не могут завершить другой запрос.
Контракт асинхронных API `chat_join_register_key` / `chat_invite_build_link`:
NULL означает ошибку запуска без callback; принятый запрос даёт ровно один callback.
Обычное завершение происходит после возврата API, включая self. Cancel/destroy
завершают запрос синхронно с CANCELLED. Handle недействителен после callback.
Для удалённого узла дедлайн 5 секунд; доставку и повтор потерянных пакетов обеспечивает
надёжный router. Повторная регистрация того же ключа тем же инвайтером обновляет TTL;
замена инвайтера для существующего ключа запрещена. Отмена может оставить ключ на C
до истечения TTL, но A не авторизует по нему вход, поскольку локально такой ключ
сохраняется только при успешном завершении.
GUI получает request_id в событии и игнорирует устаревшие результаты. Новый запрос
отменяет предыдущее ожидание GUI. Headless связывает ответ с JSON id и отменяет все
ожидания отключившегося клиента. Ни один из путей не запускает вложенный event loop.
Дополнительные исправления:
- Устранён double-free при ошибке `etcp_route_send`: пакет уже освобождён роутером.
- Headless прекращает разбор пакета после `quit`; последующая команда не создаёт
запрос на уже закрытом клиенте. При destroy отменяются cleanup_timer и запросы,
удаляются клиентские сокеты и освобождается контекст.
- UBSan обнаружил невыровненное чтение group_id из `d + 1` в chat_sync. Оно и три
аналогичных диагностических чтения заменены на memcpy.
- Тест chat_join теперь останавливает вручную запущенный member_sync;
проверяется баланс выделения/освобождения таймеров.
Проверки:
- `test_chat_join`: 31/31, включая задержанный/отсутствующий ACK, таймаут,
неверные channel/peer/key/flags/размер/status, дубликат, отказ хранения из-за
конфликта инвайтера, отмену, shutdown, параллельные ключи и ошибку отправки.
- `test_chat_join_e2e`: 3/3 (A≠C, A=C, неверный ключ). C принудительно откладывает
регистрацию на 200 мс; проверяется отсутствие ссылки до обработки регистрации.
Дополнительно проверяются подавление старого GUI-запроса и реальный headless
control socket: параллельные ответы, валидность ссылок, quit и команда после него.
- ASan/UBSan: оба теста проходят. LeakSanitizer и баланс таймеров проверены в
test_chat_join; E2E использует fork/_exit, поэтому сам по себе не проверяет утечки
при завершении дочерних процессов. Инструментированы изменённые chat-модули и
transport/routing, остальные объекты взяты из обычной библиотеки.
Логи: `/tmp/utun-ack-join-asan.log`, `/tmp/utun-ack-e2e-asan.log`.
## Отдельный root-тест auto_socket_dynamic
Запуск в `sudo unshare --net` с поднятым loopback закончился TIMEOUT через 30 секунд.
Лог: `/tmp/utun-ack-root-checks.log`. Это не PASS основного набора: в нём тест пропущен.
В конфиге теста включён auto_sockets, но не отключён стандартный фильтр
`auto_socket_skip_no_default_route=1`. Dummy-интерфейсы теста не имеют default route,
а `auto_socket.c:reconcile_iface()` исключает такие интерфейсы. Лог показывает
`no links created`, затем TIMEOUT. Исправленный тест создаёт собственный network
namespace, поднимает loopback и добавляет default route только клиентским dummy.
Фильтр остаётся включённым; отдельно проверяется исключение серверного dummy,
иначе его сокет скрывал бы полный обрыв клиентской сети.
Кроме того, тест сохраняет handle общего дедлайна, traffic_send и traffic_monitor
в одну переменную timeout_handle; фазовые таймеры вообще не сохраняются. При выходе
не отменялись все таймеры; зафиксирован остаток 9 таймеров (665/656). В обработчиках
трафика освобождался entry без queue_dgram_free. Теперь у каждого таймера свой slot,
waiters отменяются до teardown, payload освобождается, deferred cleanup завершается
до проверки баланса таймеров, сокетов и `u_get_allocated_count()`.
Новый генератор шлёт 1024-байтовые пакеты независимо в обе стороны, по одному через
threshold waiter, с бюджетом 64 пакета на tick. Проверяются содержимое, строгий seq,
ограниченность TX/RX очередей, реальное срабатывание backpressure, остановка
генераторов при полном обрыве и полное опустошение после восстановления. NCD handle
удерживает логическую сессию: смена интерфейса не является разрешением терять её данные.
Остатки после действительно удалённого соединения — отдельный допустимый случай.
Прогон нагрузки обнаружил ошибки, скрытые прежними проверками:
- `tcp_socket_add` не сохранял `netif_index`: монитор принимал interface-bound сокет
за сокет default route и менял его адрес при событиях другого интерфейса.
- Удаление TCP модели искало её по имени уже удалённого интерфейса и порту из БД.
Теперь запись интерфейса хранит имя и прямую ссылку на модель; один путь закрывает
listener, линки и модель. Портовые записи удаляются с сохранённым именем.
- `tcp_socket_remove` не закрывал линки, созданные вне NCD. Теперь закрывает все
связанные линки до освобождения модели; root-тест создаёт такой линк отдельно.
- Поиск UDP модели не исключал TCP. Добавлена проверка типа сокета.
- При selective ACK sender мог выдавать новые seq за границей `MAX_INFLIGHT_SIZE`
относительно cumulative ACK. Теперь генерация seq останавливается на границе,
cumulative ACK возобновляет очередь даже при отсутствии уже selectively-acked записей.
Тесты проверяют блокировку, возобновление и uint32 wrap. ACK не может подтвердить
ещё не назначенный seq; декодирование старшего байта seq/ACK не делает signed shift.
- Timestamp Q16 умножался как signed int и переполнялся на циклических/отрицательных
смещениях. Перевод выполняется в uint32. Jitter раньше всегда получал нулевую разницу:
предыдущий RTT читался после перезаписи; теперь берётся до неё, EWMA использует int64.
- UBSan выявил невыровненные canary/указатели в `mem.c` и `memory_pool.c`. Метаданные
читаются/пишутся через memcpy; покрыты размеры блоков 1..17 и повторное использование пула.
- Повторный общий прогон поймал `test_etcp_connect`: PID-порты пересекались с занятым
UDP-портом (`EADDRINUSE`). Теперь сервер bind-ится на порт 0, клиент получает фактический
порт через getsockname; собственного алгоритма поиска свободного порта нет.
Root ASan/UBSan/LSan: PASS, 153335 / 175912 пакетов, reinit=0, все очереди пусты,
баланс памяти/таймеров/сокетов совпал (`/tmp/root-fix-asan.log`). Инструментированы
тест, ETCP/session/router/NCD/autosocket/STCP/normalizer, очереди, event loop и аллокаторы;
прочие объекты архивов обычные. Unit-логи: `/tmp/root-session-asan.log`,
`/tmp/root-memory-asan.log`. LSan запускается вне sandbox: под ptrace он не работает.
Финальная чистая обычная сборка: `./check.sh` — **97 PASS / 0 FAIL / 1 SKIP**;
proxy, burst и load — PASS (`/tmp/root-fix-final-check.log`). Отдельный root-тест —
PASS, 188069 / 191552 пакета, все очереди пусты, reinit=0, баланс ресурсов совпал
(`/tmp/root-test-final.log`). После добавления drain отложенной очистки
`test_etcp_connect` повторно прошёл без остаточных таймеров (`/tmp/root-connect-final.log`).
Подробные socket/connection INFO включаются только через `UTUN_TEST_DEBUG=1`.
`ENETUNREACH` непосредственно при принудительном удалении IP/интерфейса ожидаем до
обработки netlink; тест требует последующей доставки всех данных. Ошибки выхода
за receive window недопустимы и после исправления sender gate не наблюдаются.
## Попутная проблема диагностики
`lib/debug_config.c:debug_parse_config()` требует `:`, хотя документация описывает `=`.
Кроме того, функция всё ещё вычисляет индекс через битовую маску, хотя категории
теперь являются индексами. Исправление в этот набор не включено; тесты используют
`debug_set_category_level()` напрямую.
+4 -68
View File
@@ -41,12 +41,6 @@ inflight - две очереди (ll_queue):
- send_hist[8] - история через какие линки отправлялся (индекс линка)
- inflight_bytes/inflight_packets per-link отслеживается в ETCP_LINK
Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight.
Отдельно ограничиваем размах sequence относительно кумулятивного ACK: новый seq выдаётся
только если (uint32_t)(next_tx_id - rx_ack_till) <= MAX_INFLIGHT_SIZE (16384).
Selective ACK освобождает отдельную inflight-запись, но не продвигает эту границу:
приёмник пока не может освободить пакеты за дыркой. Кумулятивный ACK возобновляет
input_queue даже если подтверждённые записи уже удалены по selective ACK.
ACK ограничивается последним назначенным seq (next_tx_id - 1); сравнения учитывают uint32 wrap.
optimal_inflight расчитывается из RTT и bandwidth линка.
bandwidth по каждому линку адаптивно подстраиваем:
@@ -159,7 +153,7 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
- session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует)
- mtu_local: uint16_t локальный MTU сервера
- local_link_id: uint8_t идентификатор канала сервера
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
- remote_socket_id: uint8_t (копируется из запроса)
- only_local: uint8_t флаг сервера
- type: uint8_t тип сокета сервера
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal)
@@ -167,21 +161,15 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
- random_padding: случайные байты
3) PING (0x06):
[0x06] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...] [padding...]
[0x06] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...]
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
Отправляется с отдельным crypto-контекстом (не связан с ETCP-сессией).
Используется для проверки доступности узла и NAT-тестирования.
4) PONG (0x07):
[0x07] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] [padding...]
[0x07] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...]
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
Ответ на PING. nonce копируется из запроса для сопоставления.
Nonce генерируется CSPRNG. Проверяются также ключ отправителя, source IP:port,
ID и поколение принимающего сокета. Флаг OBSERVE (0x04) запрашивает ответ
address(19) || tag(25): family(4/6), IP(16), port(2, BE), NAT2UDP!(8),
socket_id(1), generation(8, BE), round(8, BE).
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
При получении init получатель пакета должен:
- reset ETCP_LINK с этим ip_port если он есть
@@ -189,65 +177,13 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
приём кодограмм из UDP выглядит так:
uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue
Keepalive: [0x08] [flags(1)] [period_ms(2 LE)] [sleep_fields?:4] [padding...].
При KA_FLAG_SLEEP добавляются active_remaining(2 LE) и sleep_duration(2 LE), оба в единицах 0.1 с.
Полезное тело занимает 4 байта без SLEEP или 8 байт с SLEEP; остальные байты — padding.
Keepalive: пакет без секций (только timestamp+flag_up в шифрованном заголовке, data_len=0).
keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика.
Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента).
flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты).
Padding keepalive, PING/PONG и STCP-handshake:
- ETCP_CONN хранит собственный диапазон min/max и независимый таймер для всех своих линков.
- UTUN_INSTANCE хранит отдельный диапазон и таймер для PING/PONG без существующего ETCP_CONN.
- При создании владельца и каждом обновлении: min=rand(0,30), max=min+rand(32,128), границы включительные.
- Следующее обновление назначается через rand(50,300) секунд. При закрытии владельца таймер отменяется.
- Каждый пакет независимо выбирает длину rand(min,max); padding заполняется нулями ДО шифрования.
- UDP PING/PONG: padding шифруется перед CRC/tag; открытый salt+pubkey остаётся в конце датаграммы.
user_data_len/resp_data_len обозначают только полезные данные. PONG не копирует padding запроса.
- STCP: salt(8)+pubkey(32) открыты, затем 51 байт зашифрованного handshake и padding_size байт
нулевого padding, зашифрованного продолжением того же CTR. Приём продвигает CTR через padding;
padding_size=0 сразу завершает handshake. Параметры берутся у ETCP_CONN, иначе у instance.
- Каждый STCP-поток использует соль своего отправителя: клиентскую для client→server, серверную для server→client.
CTR IV = первые 12 байт SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream") + четыре нулевых байта.
Отправка инициализируется после генерации своей соли, приём — после чтения соли пира, до расшифровки тела.
После handshake CTR непрерывно продолжается через padding и DATA, без повторной инициализации на кадр.
- Padding не должен превышать 158 байт. Для UDP длина ограничивается оставшимся MTU;
если не помещается даже min, отправка завершается ошибкой без обрезания пользовательских данных.
- Диагностика диапазонов/обновлений/PING/PONG: etcp=debug; keepalive: keepalive=debug.
Клиент: если все линки =down то начинается процедура восстановления связи:
- клиент посылает init без сброса (0x04), link_state=2
- сервер как получает init проверяет session_id:
- если session_id совпадает — шлёт подтверждение без сброса (0x05)
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03)
**** Connection session confirmation (2026-09) ****
Physical UDP INIT / STCP handshake establishes an authenticated link. A stream reset
uses the same control protocol on every initialized UDP or TCP link, bypassing the
normalizer and reliable DATA/ACK queues. A physical reset does not generate DOWN/UP.
Encrypted DATA/ACK plaintext after timestamp(2) + flag_up(1):
09 | sender_epoch:u64be | receiver_epoch:u64be | existing ACK/DATA sections
The extra 17 bytes participate in the MTU budget. Unwrapped DATA/ACK and frames with
an epoch mismatch are discarded before stream state or link liveness is updated.
Control frames (all 25 bytes after timestamp/flag):
0a HELLO | sender_epoch | receiver_epoch | 0
0b CHALLENGE | sender_epoch | receiver_epoch | random_cookie:u64be
0c CONFIRM | sender_epoch | receiver_epoch | echoed_cookie:u64be
Each connection owns one pending peer epoch, fresh challenge, and retry timer.
HELLO and a changed epoch advertised by INIT only request a challenge. Only a
CONFIRM for the current local epoch and pending random challenge commits a peer
epoch. Changing the peer epoch resets the stream but preserves our own epoch.
A local fatal reset generates a new local epoch and blocks DATA until confirmation.
Both sides may reset concurrently. Loss of any control frame is recovered by a
500 ms retry on all initialized links. After 10 s an unconfirmed local reset closes
the connection; an unconfirmed candidate alone is discarded without closing a
working session. Close cancels the timer before destroying links.
TCP may initially receive peer epoch zero because the server creates ETCP_CONN
only after STCP handshake. It completes the same session confirmation before INIT/UP.
REINIT notifies stream consumers without tearing down physical ownership. The
router retains end-to-end inflight; BGP repeats JOIN_GROUP and TABLE_REQUEST because
its previous control messages may have been discarded by the stream reset.
+42 -47
View File
@@ -1,5 +1,3 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md).
# ETCP Router Architecture
## Структуры данных
@@ -22,8 +20,7 @@ ETCP_ROUTER_CONN (один на пару remote_node_id + svc_id)
│ │
│ ack_timer: void* периодический 100ms │
│ idle_ack_timer: void* idle таймаут 500ms │
│ send_waiter: queue_waiter_handle waiter на send_input_q │
│ watchdog_timer: void* проверка инварианта 500ms │
│ send_resume_timer: void* retry send_q 50ms │
│ send_blocked: u8 inflight полон │
└─────────────────────────────────────────────────────┘
@@ -53,41 +50,40 @@ SVC_ROUTE_HDR (22 байта)
│
▼
┌──────────────────────────────┐
│ router_enqueue_send(rconn) │ всегда через send_q (FIFO)
│ if send_q >= 64 && !force: │ → вернуть -1 (backpressure)
│ return -1 │
└──────┬───────────────────────┘
│ копия в send_q + router_send_kick()
▼
┌─────────────────────────────────────────┐
│ router_send_kick(rconn) │
│ closed/no_route → return │
│ send_q пуст → return │
│ inflight >= limit → send_blocked=1, ret │
│ waiter уже ждёт → return │
│ нет conn → set_no_route │
│ queue_waiter_wait(send_input_q, │
│ &send_waiter, drain_cb) │
└──────┬───────────────────────────────────┘
│ send_input_q опустел (round-robin: FIFO waiter-ов)
▼
┌──────────────────────────────┐
│ router_send_drain_cb(rconn) │ шлём ОДИН пакет
│ e = send_q.pop() │
│ router_send_one_flags(e) │
│ → tx_seq++ │
│ → etcp_send(conn, entry) │
│ если send_q ещё непуст: │
│ inflight полон → blocked=1│
│ иначе → queue_waiter_wait │ (в хвост → round-robin)
│ если пуст → send_blocked=0 │
└──────────────────────────────┘
Повторный запуск drain:
• приход ACK → router_handle_ack → kick
• маршрут появился → router_no_route_retry_cb → kick
• рост inflight-лимита → router_update_inflight_limit → kick
• страховка → router_send_watchdog_cb (500ms) → DEBUG_ERROR + kick
│ inflight < ROUTER_MAX(256)? │
│ tx_seq - tx_acked < 256 ? │
└──────┬───────────┬───────────┘
│ YES │ NO (inflight полон)
▼ ▼
┌──────────────┐ ┌─────────────────────┐
│router_send_one│ │ router_enqueue_send │ → send_q (FIFO)
│ (прямая) │ │ (в очередь) │ send_blocked = 1
└──────┬────────┘ └─────────┬───────────┘ send_resume_timer (50ms)
│ │
▼ │
┌────────────────────┐ │
│ tx_seq++ │ │
│ hdr.dst = dst │ │
│ hdr.src = node_id │ │
│ hdr.seq = tx_seq │ │
│ hdr.svc_id = svc │ │
│ │ │
│ etcp_send(conn, e) │ │
└────────────────────┘ │
│
┌────────────────────────────┘
│ (при получении ACK от remote)
▼
┌────────────────────┐
│ router_drain_send_q│ вызывается из ACK handler
│ │ или по send_resume_timer
│ while inflight < 256│
│ e = send_q.pop() │
│ router_send_one(e)│
│ │
│ если send_q пуст: │
│ send_blocked = 0 │
└────────────────────┘
```
## Receive Path (приём данных)
@@ -123,7 +119,7 @@ SVC_ROUTE_HDR (22 байта)
│ │ = hdr.seq││ |d| < ROUTER_MAX_INFLIGHT? │
│ │ (только ││ │
│ │ вперёд) ││ Дубликат? │
│ │ kick ││ rx_seq - seq > 0 ИЛИ │
│ │ drain_q ││ rx_seq - seq > 0 ИЛИ │
│ └──────────┘│ seq уже в recv_q? │
│ └──────┬─────────────┬──────────────────┘
│ │ OK │ out-of-bounds / dup
@@ -230,8 +226,7 @@ SVC_ROUTE_HDR (22 байта)
│ DEBUG_WARN "stale ACK" │
│ │
│ if send_blocked: │
│ send_blocked = 0 │
│ router_send_kick(rconn) │
│ router_drain_send_q(rconn) │
└────────────────────────────────────────┘
```
@@ -246,12 +241,12 @@ SVC_ROUTE_HDR (22 байта)
При блокировке:
данные → send_q (FIFO, без дропов)
send_blocked = 1
send_resume_timer = 50ms retry
Разблокировка:
приход ACK от remote → tx_acked обновляется
→ router_send_kick() → waiter → drain_cb разгребает send_q
или маршрут появился / inflight-лимит вырос → kick
страховка: router_send_watchdog_cb (500ms) → force kick
→ router_drain_send_q() разгребает send_q
или send_resume_timer (50ms) → router_drain_send_q()
send_q полностью разобран → send_blocked = 0
```
@@ -309,8 +304,8 @@ SVC_ROUTE_HDR (22 байта)
│ idle_ack_timer ─── 500ms ─────▶ router_idle_ack_cb │
│ (досылка последнего ACK) │
│ │
│ watchdog_timer ── 500ms ─────▶ router_send_watchdog│
│ (страховка инварианта drain, force kick) │
│ send_resume_timer ─ 50ms ─────▶ router_send_resume │
│ (retry когда send_q не пуст) │
│ │
└─────────────────────────────────────────────────────┘
```
-81
View File
@@ -1,81 +0,0 @@
Аватарка CHAT-группы
===================
Модуль src/chat/group_avatar.c. Картинка — бинарный BLOB до 16384 байт в общей
SQLite каждого участника: channels.avatar_data/version/hash/signature.
Storage-сервер, каталоги файлов и пользовательские квоты не используются.
При старте добавляются отсутствующие колонки одной транзакцией, записи/ключи
каналов сохраняются. channel_put использует UPSERT и сохраняет аватарку,
UI-состояние и локальные приватные ключи; смена публичных ключей существующего
channel_id запрещена. Удаление группы удаляет и её аватарку.
Право публикации — приватный Ed25519 ключ группы плюс собственное действующее
членство. creator UID/admin-флаг не заменяют ключ. group_avatar_set подписывает:
ASCII "group-avatar-v1" без NUL, group:u64 LE, version:u64 LE, size:u16 LE, bytes.
Версия — max(NTP microseconds/1000, previous+1), ограничена INT64_MAX.
SHA256 считается по картинке; при одинаковой версии побеждает больший SHA256.
Обе конкурирующие записи должны иметь правильную подпись группы; конфликт — WARN.
Публикация одинаковой картинки не создаёт новую версию. Удаление — подписанная
запись с size=0 и новой версией; подпись/версия сохраняются, старый пир не может
восстановить удалённую картинку. Чтение проверяет сохранённый хеш и подпись.
Транспорт
---------
ETCP_RT_ID_GROUP_AVATAR=0x3A, router wire version=1. Отправитель/группа берутся
из проверенного router-заголовка. Все пакеты подписаны ключом узла и зашифрованы
end-to-end; в DATA дополнительно передаётся подпись группы над всей картинкой.
Обмен только между READY-соседями CHAT-группы с действующим подписанным членством.
Любой участник пересылает сохранённую запись, владелец ключа может быть offline.
Все числа протокола LE. Общая часть, 44 байта:
version:u8=1, command:u8, avatar_version:u64, total_size:u16, SHA256:32.
STATE=1 объявляет локальную версию/хеш; начальное отсутствие — version=0, hash=0.
GET=2 запрашивает актуальную запись. Объявление не меняет БД без полной DATA.
DATA=3 добавляет group_signature:64, offset:u16, block_size:u16, bytes.
DATA header=112 байт. Картинка разбивается на блоки по 4096 байт; последний
короче. Полная картинка 16 КиБ — ровно четыре DATA. Удаление — один DATA без bytes.
Порядок блоков гарантирует router. Сборка проверяет offset, постоянные version,
total_size/hash/signature и допустимые размеры. До полной сборки прежняя картинка
остаётся в БД. После SHA256/Ed25519 вся запись заменяется одним UPDATE, затем
CHANNEL_UPDATED доставляет изменение GUI и STATE распространяется соседям.
Промежуточные блоки не публикуются, ACK/retransmit каждого блока — только router.
Отправка заполняет router до нижнего порога send_q count<=0 и ждёт send_ready;
планировщик отправляет максимум четыре пакета за callback. Snapshot отправителя
не меняется при новом локальном обновлении. Максимум 16 tx и отдельно 16 rx
сборок на экземпляр, каждая до 16 КиБ. receive_ready при заполнении rx сохраняет
пакет в router recv_q без продвижения ACK; освобождение слота возобновляет приём.
Несовпадение эпох/закрытие router отменяет незавершённую сборку и начинает сверку.
Эпоха нового контекста берётся из текущей READY-сессии, не сбрасывает первый приём.
Обнаружение соседей/таймауты — таймер 1 с; STATE также раз в 30 с. Нет прогресса
15 с — отмена незавершённого snapshot, повтор запроса/сверки. Остановка сервиса
отменяет таймеры/waiters/call_soon до удаления группы/ядра.
ChatGUI
-------
Настройки группы: Upload group avatar/Remove group avatar, предпросмотр и статус.
Кнопки доступны только при локальном приватном ключе. Применение сразу, отдельно
от Save локальных настроек. Worker обрезает центральный квадрат, создаёт JPEG
128×128, снижает качество/размер до лимита 16 КиБ. Вход ограничен 16 мегапикселями.
GUI отправляет байты в uasync; ядро повторно проверяет размер/членство/ключ.
SQLite — постоянный кеш. GroupAvatarManager хранит QImage до 32 МиБ, ключ кеша:
group/version/hash. Декодирование в отдельном worker, проверка SHA256, JPEG и
квадратных размеров до 128×128. Поздний результат старой версии не заменяет
новую. Готовая картинка обновляет только строку группы; без аватарки — инициалы.
Приватные ключи/байты картинки не выводятся в диагностику.
Проверки
--------
test_group_avatar: настоящие два ядра/UDP/router, 16 КиБ четырьмя блоками,
атомарность сборки, hash/signature/cross-group отказ, приостановка receive,
перезапуск канала, stale/conflict, редактор с ключом вне creator UID, подписанное
удаление, повторное описание группы и restart БД. Schema failure откатывает
частичные ALTER, сохраняет прежние данные/ключи, повторное добавление идемпотентно.
test_group_avatars (Qt): нормализация, лимит/отображение, кеш, права кнопок и отказ
ядра без ключа, удаление. chat=debug показывает состояния и каждый 4КиБ блок;
chat=info — публикации и жизненный цикл. Ошибки содержат group/peer/version/offset.
-528
View File
@@ -1,528 +0,0 @@
# Linux: диагностика и восстановление аудио
Проверка 2026-10-02: desktop chatgui, miniaudio/PulseAudio, общий голосовой стек,
звонки, рация и запись сообщений. Исправления обработки PCM, владения устройствами
и автоматическое восстановление реализованы. Ниже сохранены исходные результаты
аудита; раздел «Реализация» описывает действующие контракты и проверки.
## Вывод
До исправлений тракт был некорректен при сбоях. Самое существенное: воспроизведение
звонка зависит от наличия микрофонных кадров в duplex-ring. Его опустошение
останавливает вызов пользовательского callback, оставляя хвост выходного буфера
незаполненным. Восстановление jitter-буфера не исправляет остановившееся устройство.
Отдельного управляющего механизма восстановления устройств не было.
В реализации восстановлены три гарантии: каждый запрос output полностью заполнен; обработка
одного callback всегда ограничена; остановка/пересоздание устройства выполняются
вне аудиопотока и не уничтожают сетевую сессию звонка.
## Что установлено по работающим клиентам
В локальном старом логе звонка `132e4a9698f47bdf` с Samsung SM_A525F наблюдались
паузы callback примерно 0,5–2,6 секунды при зарегистрированной обработке 0–2 мс.
Входящий буфер desktop достигал 15 секунд, росли потери, включалось ускорение 160%.
На Samsung захват продолжал давать около 50 кадров/с, playback регистрировал
underrun и тоны разрыва. RTT порядка 1–4 мс не объясняет эти паузы сам по себе.
Это признаки проблемы обслуживания локального аудио, но ещё не доказательство
конкретной причины зависания PulseAudio. Старое измерение исключало время
синхронных DEBUG-вызовов между концом обработки и обновлением timestamp.
Новая диагностика охватывает весь callback, включая эти логи.
Работающий GUI не перезапускался, поэтому новый инструмент ещё не записал этот
сбой на реальном звонке. Лог Samsung сохранён в `/tmp/sm-audio-current.log`.
## Найденные дефекты и ограничения до исправлений
| Место | Поведение и последствия | Необходимая доработка |
| --- | --- | --- |
| `lib/miniaudio.h`, `ma_device__handle_duplex_callback_playback()` | При пустом capture-ring `acquire_read` возвращает success с нулём кадров. Выполняется `break`, output остаётся частично незаполненным, call callback не вызывается. PulseAudio затем отправляет весь mapped buffer. | Обеспечить полный output и продолжение playback при отсутствии capture. Нельзя делать commit_read для синтетических кадров. |
| `ma_device_on_write__pulse()` | Успешная запись с нулём обработанных кадров не уменьшает остаток цикла. Возможно бесконечное вращение с блокированием того же PA mainloop, включая capture. | Немедленно выходить при отсутствии продвижения, возвращать управление mainloop. Логировать состояние и запрос, не пытаться записывать до успеха в этом callback. |
| `ma_device_on_read__pulse()` | PA hole отбрасывается без подачи соответствующего количества нулевых кадров. Из capture timeline исчезает интервал, duplex-ring может опустеть. | Сохранять длительность hole тишиной либо явно обозначать discontinuity независимому capture-тракту. |
| `lib/speex_aec.c`, `speex_aec_feed_playback()` | Обрабатывает только остаток одного 960-sample кадра, отбрасывая хвост входного блока. Контракт разрешает произвольный count. | Циклом потреблять весь count, складывать все полные кадры, сохранять остаток. |
| `CallAudioEngine::setPlaybackDeviceIndex()` | После неудачного пересоздания `m_active` остаётся true при отсутствии устройства; `start` того же звонка возвращает успех. | Разделить желание продолжать звонок и работоспособность устройства. Ошибка запуска переводит устройство в recovering. |
| `RadioAudioEngine::teardownDevice()` | Если playback init успешен, а start нет, uninit пропускается: он зависит от флага started. Объект удаляется вместе с незавершёнными ресурсами. | Разделить initialized и started; каждый успешный init обязательно завершать uninit. |
| `MainWindow::~MainWindow()` / `SoundManager::shutdown()` | Общий context освобождается вместе с engine, но звонок и рация здесь явно не останавливаются. Их singleton-объекты могут оставлять живые устройства. `m_context` также не обнуляется. | Сначала остановить все зависимые устройства и дождаться callbacks, затем освобождать общий context. |
| `AudioRecorder` | Callback выбирает глобальный `g_recorder`, игнорируя `pUserData`. Второй recorder, в том числе тест микрофона, заменяет получателя. Флаги/уровень/вектор waveform используются из GUI и аудиопотока без согласованной синхронизации. | Использовать `pUserData`; передавать уровни атомарно, waveform — безопасным snapshot; буфер записи должен иметь однозначного владельца. |
До исправлений первые два дефекта воспроизводились стендом без аппаратуры:
duplex оставлял sentinel в output при starvation, а PA-write без продвижения
требовал внешней остановки. AEC из 1200 + 720 samples сохранял один кадр вместо
двух. Теперь эти проверки требуют полного output, самостоятельного возврата
из callback и двух сохранённых AEC-кадров; результаты приведены ниже.
Capture-AEC также обрабатывает не более одного кадра за вызов. В звонке его
вызывают ровно с 960 samples, поэтому текущий call feed этот случай не нарушает.
При расширении API нельзя объявлять произвольный count без согласованного размера
выходного буфера. `speex_aec_reset()` уже существует: новый API для сброса самого
AEC не нужен, но нужен согласованный сброс всего локального voice-тракта.
## Чистота реализации
Этот раздел фиксирует исходный аудит; изменения владения и ограничение работы
callback описаны в разделе «Реализация».
Нормальная структура уже есть: GUI владеет устройствами, сервисный слой кодирует
Opus и обрабатывает jitter, сетевой поток доставляет пакеты. Нет необходимости
переписывать весь стек или заводить новый backend.
Однако границы владения не завершены: context фактически принадлежит engine
звуковых уведомлений, а используется независимыми устройствами звонка/рации.
Флаги active/started/initialized местами смешивают три разных состояния.
Выбор input применяется к recorder, звонок использует default input, рация —
default устройства. Output звонка и SoundManager тоже выбираются отдельно.
Индекс перечисления не является устойчивой идентичностью устройства после hotplug.
В radio capture fallback `useDefault` фактически повторяет ту же попытку:
`pDeviceID` и так NULL в обеих ветках. Callbacks рации запускаются раньше
подготовки capture-буфера и установки VAD; порядок запуска нужно привести к
«сначала состояние и буферы, затем устройства», чтобы исключить гонки при старте.
Callback не является строго realtime-safe. Capture удерживает `g_mtx` во время
AEC/AGC/Opus; отправка выделяет память и вызывает `uasync_post`. Playback вызывает
SoundTouch и код с возможным расширением контейнеров, mutex и синхронными логами.
Приём пакетов тоже может держать mutex при работе с jitter и логировании.
Плохие Opus-кадры могут последовательно выбираться из большой очереди в одном
pull: нужен предел работы на callback. Новая диагностика покажет, где это
действительно нарушает deadline. Перенос всего DSP в ещё один worker заранее
увеличит число очередей и точек синхронизации без установленной необходимости.
AEC получает reference только звонка. Звуки отдельного SoundManager в него не
попадают; delay по умолчанию фиксирован, не вычисляется по фактической задержке
устройств. Это ограничение качества эхоподавления, а не установленная причина
текущего зависания.
Jitter умеет повторно накопить резерв после сетевого underrun, но диапазон tempo
ограничен 1,0–1,6. Ускорение убирает избыток, а замедления для компенсации более
медленных часов отправителя нет. При устойчивом отрицательном дрейфе резерв будет
снова опустошаться. Это отдельная задача длительных звонков: после восстановления
устройств проверить наклон глубины буфера и при необходимости добавить небольшую
компенсацию дрейфа в обе стороны, не смешивая её с быстрым catch-up после паузы.
Принудительные 480 frames задают период 10 мс при 48 кГц. Собственный PA default
miniaudio — 25 мс с комментарием о проблемах PipeWire при периодах меньше ~20 мс.
Период backend и Opus-кадр 20 мс должны оставаться независимыми. Увеличивать
период до исправления AEC нельзя: 1200-sample блоки уже воспроизводят потерю хвоста.
После исправления разумно сначала проверить default backend и измерить задержку.
## Предлагаемое восстановление без большого усложнения
### 1. Сначала восстановить инварианты обработки и владения
Исправить полный output, zero-progress, AEC accumulation и uninit при неудачном
start. Перед освобождением context явно остановить recorder, звонок, рацию и
прочие его устройства. До остановки отменять recovery timer; после join можно
сбрасывать voice-ресурсы. Проверять возвраты feed/start/init и логировать причину.
Самый небольшой вариант — оставить один duplex-device: при нехватке capture
подать в call callback нулевой input, полностью сформировать playback и не
продвигать реальный capture-ring за синтетические samples. Это чинит непосредственный
дефект, но сохраняет общий жизненный цикл input/output и синтетический TX по
playback-clock.
Для устойчивости к исчезновению микрофона предпочтительнее два устройства
capture/playback внутри существующего `CallAudioEngine`, как уже сделано в рации.
Capture только накапливает 960 samples и вызывает feed; playback независимо
вызывает pull и всегда заполняет output. Это два handle с одним владельцем,
без нового DSP-worker или дополнительной PCM-очереди. Потребуется уточнить
потоковый контракт voice-слоя: один capture-writer и один playback-writer,
согласованный доступ к AEC, остановка обоих до release. Текущий `g_mtx` уже
защищает AEC, но остальные общие поля нужно проверить перед разделением.
### 2. Разделить краткий underrun и неисправность устройства
| Событие | Действие |
| --- | --- |
| Нет сетевого PCM, callback продолжает приходить | Тишина/существующий gap tone; jitter автоматически накапливает данные. Устройство не пересоздавать. |
| Один backend underrun, новые кадры принимаются | Подать следующий полностью заполненный буфер; собрать счётчик. Не сбрасывать AEC и не запускать restart на каждый xrun. |
| Hole/переполнение capture | Учесть потерянную длительность; краткую потерю пережить, длительную отметить discontinuity для AEC. |
| Поток failed/stopped или нет продвижения кадров длительное время | Перейти в recovering и пересоздать затронутое устройство вне callback. |
| Suspend/resume ОС | Учитывать suspend как отдельную причину, восстановление проверять после resume; не запускать частый restart-loop во время сна. |
| Потерян общий backend/context | Однократно остановить всех его пользователей, восстановить владельца, затем требуемые устройства. |
Один owner timer и состояния `Stopped / Running / Recovering` достаточны.
`wanted` хранит желание пользователя продолжать; generation меняется при stop,
новом звонке и смене устройства. Каждая отложенная попытка проверяет generation
и wanted — законченный звонок никогда не возрождается.
Callbacks только обновляют атомарные счётчики успешно обработанных кадров и
признак backend failure. Watchdog проверяет фактический прогресс отдельно по
направлениям. Тишина в PCM не означает остановку устройства. Reporter диагностической
очереди не должен становиться watchdog: его события могут теряться.
Начальные значения для проверки: timer 200 мс, подозрение при отсутствии прогресса
500 мс после startup grace; повторные попытки через 250/500/1000 мс с потолком
2 секунды. Это предлагаемые настройки, не доказанные универсальные пределы.
Suspend и фактический согласованный backend period должны учитываться отдельно.
Нельзя обещать bounded recovery, пока init/stop/uninit сами могут зависнуть:
сначала устранить spin-loop и проверить время backend-операций. Если они реально
блокируют GUI, вынести только управление устройствами в один owner-thread,
сохранив тот же автомат состояний.
На пересоздании сбросить capture-accumulator, AEC delay/filter и локальный PCM/
stretch, сохранив call ID и сетевую сессию. После длительной аппаратной паузы
устаревшую речь нужно явно отбросить до актуального резерва jitter, иначе
15 секунд накопленного звука при скорости 160% разгружаются примерно 25 секунд.
Этот сброс применять только при аппаратной discontinuity; обычный сетевой jitter
не должен автоматически приводить к отбрасыванию очереди.
Устройство хранить по backend ID либо как default, не по индексу. При его
исчезновении попробовать default и явно сообщить о смене; выбранное устройство
можно вернуть при появлении. Нужны ошибки с ID/именем/направлением и результатом
попытки. Для PA-ошибок следующий уровень диагностики — stream/context state и
`pa_context_errno`, потому что один `-1` от API не объясняет причину.
### 3. Проверить сценарии, а не только успешный старт
После реализации добавить проверки: capture starvation 20/500 мс и возврат;
hole; AEC блоки, пересекающие границу кадра; zero writable; ошибка записи;
исчезновение выбранного устройства и появление default; suspend/resume;
перезапуск звукового сервера; stop во время backoff; закрытие GUI при активном
звонке/рации. Проверять полный output, bounded очереди и работу callback,
возобновление прогресса, отсутствие callbacks после release и отсутствие
повторного старта после пользовательского stop.
## Как читать добавленную диагностику
В GUI-категориях включить `call=debug` либо задать отдельную строку в INI:
```ini
[debug]
call=debug
```
Обычные сводки идут на DEBUG, аномалии — WARN/ERROR; miniaudio info перенесён
на DEBUG, подробный backend log — TRACE. Все устройства используют категорию
call, включая recorder, radio и sound-engine; role и ID различают их в строках.
Не включать глобальный trace для всего проекта при измерении аудио.
Reporter раз в секунду печатает stage, tid, callbacks/s, requested frames/s,
максимальный интервал между входами, полное wall-time обработки и thread CPU-time,
незавершённый вход и время с последнего входа. `work_max` и `cpu_max` — отдельные
максимумы за окно, они не обязательно относятся к одному вызову.
| Stage | Что измеряется |
| --- | --- |
| loop | Один `pa_mainloop_iterate`, включая ожидание и вложенные callbacks. Долгое время само по себе не доказывает тяжёлый DSP. |
| read / write | Полный backend read/write callback. Frames/s здесь — запрошенные, не гарантированно обработанные кадры. |
| map | `writable_size` и `begin_write`; пустой/ошибочный выход тоже закрывает измерение. |
| submit | Вызов `pa_stream_write`. |
| duplex | Перенос capture-ring в duplex-output; detail содержит глубину capture-ring. |
| callback | Полный miniaudio data callback, включая код приложения и его синхронные логи. |
| feed / pull | Capture DSP/encode/send и playback jitter/stretch/tone в звонке. |
`unfilled_frames` — незаполненный output, `hole_frames` — потеря capture timeline,
`capture_drop_frames` — переполнение capture-ring, `no_progress` — PA-write без
продвижения. `backend_underruns` — отдельный callback самого PA-stream; это не
то же самое, что пустой сетевой jitter. `backend playback started/resumed`
показывает фактическое начало/возобновление playback. Notification содержит
числовые miniaudio type/state.
Первое событие каждого типа за окно имеет tid и CLOCK_MONOTONIC timestamp;
повторения суммируются. Для задержавшихся потоков reporter читает `/proc` wchan
и накопленные schedstat CPU/ready-wait/slices. Сравнивать соседние snapshots:
сами totals не являются длительностью текущей паузы. CPU-time намного меньше
wall-time означает ожидание/вытеснение; wchan помогает отличить poll от mutex/
I/O wait. Это снимок в момент отчёта, не исторический стек зависшего вызова.
Producer не выделяет память, не ждёт mutex reporter и не пишет диагностические
логи: ограниченная MPSC-очередь 8192 события, не более четырёх CAS-попыток.
Reporter не разыменовывает ma_device/pa_stream. Переполнение логируется как
`diagnostic events lost`; такое окно нельзя считать полным. Инструментация
добавляет получение часов/TID и атомарные операции, поэтому overhead ненулевой.
Частые события call jitter/AEC/TX теперь публикуются счётчиками и выводятся из
uasync вне аудиомьютекса. Часть существующих логов рации остаётся синхронной;
тракт не объявляется строго realtime-safe.
После close устройство удаляется из reporter: для устройств короче секунды
доступны lifecycle и первые аномалии, но не полная секундная сводка.
Новый binary собран в `tools/chatgui/build/vibechat`; для нового измерения нужен
его обычный перезапуск. Изменение файла binary не обновляет уже запущенный процесс.
## Реализация (2026-10-02)
`SoundManager` владеет общим контекстом и конечными выходами. На каждый фактический
выход приходится один микшер: PCM сообщений/видео, заранее декодированные сигналы,
готовый голос звонка и рации. После суммирования float PCM ограничивается, и один
и тот же stereo буфер отправляется устройству и AEC соответствующего микрофона.
Звук другого выхода в референс не входит. Default/явный backend ID объединяются;
PulseAudio также публикует идентификатор фактического маршрута. После server move
совпавшие выходы объединяются в GUI только после подтверждения фактического
маршрута; предположение о default не используется вместо PA snapshot. При временной коллизии дополнительный выход
отдаёт тишину до объединения. Изменение списка источников выполняется после stop
callback, с сохранением backend stream и выбранного сервером маршрута.
Устройства сохраняются как backend ID вместо индекса. При init/start failure
явного выбора пробуется default, причина логируется. Выбранный ID сохраняется;
вернуться к нему можно повторным выбором или при следующем recovery. Автоматический
опрос hotplug для возврата на первоначальный выбор не добавлен.
`AudioDevice` — владелец одного miniaudio device в GUI. Успешный init всегда имеет
парный uninit, включая ошибку start. Callback и его состояние готовятся до start.
Close останавливает/join-ит device worker перед освобождением callback. Отдельный
атомарный счётчик кадров контролирует прогресс. Таймер 200 мс проверяет stopped
и отсутствие продвижения 1 с; suspend продлевает grace, отдельный xrun не вызывает
restart. Backoff: 250/500/1000/2000 мс, сброс после продвижения.
Miniaudio не добавляет накопитель фиксированного периода: `noFixedSizedCallback`
включён, hardware chunks могут иметь любую длину. В PulseAudio физическое начало
блока определяется по timing snapshot с AUTO_TIMING_UPDATE/INTERPOLATE_TIMING.
Capture: monotonic now минус latency read index, без повторного вычитания длины
блока. Playback: now плюс latency write index. Учитываются задержка converter и
остающийся input cache. Один backend dispatch имеет один snapshot, позиции
последующих client callbacks продвигаются по числу кадров. В отсутствие snapshot
используется оценка по callback; источник времени и physical route логируются.
PA hole отдельно помечен как отсутствующий capture, а не как реальная тишина.
`VoiceAudioIo` создаётся только для звонка и рации. Capture и RX обрабатывают
независимые потоки. Hardware capture только копирует PCM в фиксированную MPSC-очередь;
RX worker выполняет decode/SoundTouch/тоны и поддерживает до 40 мс готового голоса.
В сервисном слое рации вычисление Silero держит только TX mutex: общий mutex
отпущен, поэтому VAD не блокирует RX; lifetime и порядок PCM/PTT сохранены.
Hardware output смешивает готовый PCM. Opus, AEC, VAD, AGC и растущие буферы записи
не исполняются в аппаратных callbacks. Обработка самого backend, PCM/resampler
узлов miniaudio и атомарные уведомления остаются в callback; строгая hard realtime
гарантия не заявляется.
Capture worker — единственный владелец AEC и порядка PCM/PTT/mute. Кадр AEC —
960 отсчётов на канал; 1/2 канала микрофона и 2 независимых канала референса.
Противофазное стерео не усредняется. История рендера содержит до 1,92 с PCM и
физические timestamps. Reference выбирается по времени микрофона; небольшое
различие часов компенсируется интерполяцией только референса. Реальная тишина
сохраняется как валидный PCM; при отсутствующем reference AEC пропускает микрофон
и не адаптируется. Потеря reference, смена маршрута/поколения и скачок часов
сбрасывают фильтр. Возраст capture-очереди ограничен 100 мс от enqueue, отдельно
от собственной задержки backend. Потери и очередь видны в сводках категории `aec`.
Mute применяется после AEC/AGC: фильтр продолжает получать настоящий микрофон,
а Opus получает нули только в отмеченной части кадра. Desktop запускает core
через `call_audio_start_prepared`: повторного core AEC нет. Raw API с core AEC
сохранён для Android/headless; смешивать политики в активном звонке нельзя.
У рации один автомат передачи: manual PTT или VAD. VAD читает непрерывный очищенный
сигнал до AGC, включая время ручного PTT. Передача использует отдельный накопитель
Opus-кадра; границы PTT внутри AEC-кадра сохраняются. При закрытии серии хвост
дополняется нулями только перед Opus и помещается в очередь перед FIN. Короткий
capture tail при stop проходит без AEC, а не дополняет непрерывный фильтр. Разрыв
захвата завершает старый сетевой хвост и сбрасывает VAD, исключая склейку через
потерянные отсчёты. Capture restart завершает принятую серию; новое прослушивание
того же канала отменяет старые BEGIN/media. FIN проверяет токен своего capture/burst
и поколение экземпляра ядра, поэтому не закрывает более новую серию и не обращается
к освобождённому ядру даже при повторном использовании его адреса.
TX ограничен 8 media-задачами, возраст — 200 мс. У рации дополнительно ограничено
16 control-задач; FIN выделяется заранее вместе с BEGIN. Если control-очередь
занята, новый BEGIN откладывается до освобождения; это логируется. Очередь PCM
резервирует 8 слотов для управляющих сообщений; producer не меняет consumer и
публикует слот только после копирования. Capture callback никогда не ждёт слот.
Отказ постановки GUI-команды завершает capture; таймер запускает recovery.
`AudioRecorder` получает raw microphone без AEC. Его worker обрабатывает AGC,
накапливает запись и публикует атомарный уровень/длительность. Переполнение или
ошибка обработки отклоняют запись с логом. Meter-only использует тот же capture,
но не сохраняет PCM и не создаёт голосовое сообщение. Stop: close device, drain
и join worker, затем передача владельца записи задаче attachment. Настройки
компрессора в ходе записи применяются к следующей.
PCM воспроизводится через независимые handles. Сообщение, видео и тест динамика
не останавливают чужой PCM; sample rate принадлежит источнику. Seek пересоздаёт
узел и очищает его read/resampler cache, сохраняя handle и владельца PCM. Cursor
вычисляется по реально обработанным графом кадрам, а не по prefetched datasource.
Pause отсоединяет узел и ждёт начатое чтение графа; resume подключает тот же узел,
сохраняя его PCM и cache. Соседние проигрыватели продолжают работать.
У асинхронного открытия видео/голоса проверяется generation.
Потоковый AGC использует фиксированные кольца истории и PCM, нулевой lookahead
и непосредственный process_frame без растущего output. У записи lookahead явно
100 мс; растёт только сохраняемый результат. Push/flush/configure возвращают
ошибки; flush проверяется перед кодированием вложения.
Порядок остановки голосового направления: close capture → join capture worker →
detach output → join RX worker → release core. При failed context синхронный сигнал
сначала закрывает всех клиентов, затем освобождает context. Mixer, PCM handles,
call ID и группа переживают recovery. Stop выключает recovery; таймер завершения
звонка проверяет generation. MainWindow останавливает клиентов до SoundManager
и сетевого ядра. Сетевые операции и регистрация audio ops выполняются в uasync.
PulseAudio control-waits ограничены 1 с на операцию; при timeout operation
отменяется до возврата, исключая callback с просроченным stack pointer. Partial
init освобождает контекст/mainloop/события. Failed stream/context содержит PA
errno в логе. Zero writable/zero converter progress возвращают управление.
Последовательность нескольких операций init/uninit всё ещё может задержать GUI
дольше секунды; отдельный device owner-thread не добавлен.
## Проверки реализации
`vibechat` и аудиотесты собираются через CMake. Набор проверки:
```sh
ctest --test-dir tools/chatgui/build --output-on-failure \
-R 'test_(linux_audio|audio_timing|audio_diagnostics|audio_recovery|audio_event_queue|voice_audio_io|voice_tx|call_jitter|radio_audio|radio_jitter|voice_file)$'
make -C tests test_audio_compressor test_speex_aec
./tests/test_audio_compressor
./tests/test_speex_aec
```
Добавлены отдельные проверки MPSC (80000 сообщений, reserve/control, слот ещё не
опубликован), точных PCM/PTT/mute границ, короткого хвоста, независимого RX при
задержке capture worker, противофазного stereo AEC, старого физического timestamp
при свежем enqueue, совпадения реальных Opus-пакетов с правильно дополненным
хвостом, rapid PTT, capture restart, отмены старого прослушивания и bounded control.
Проверяются manual/VAD handover
без второй серии и продвижение настоящего сервисного RX при заблокированном VAD.
Timing test проверяет read-index timestamp, несколько converter callbacks одного
блока и метку hole. Recorder тестирует raw PCM и отсутствие вложения в meter-only.
Speex проверяет подавление эха и сохранение double-talk также для двух микрофонов
с двумя независимыми каналами reference. В детерминированном синтетическом тесте
остаточная энергия эха — около 0,009 исходной; корреляция near-end — около 0,95.
Эти величины относятся только к тесту, а не к реальным акустическим условиям.
В `/tmp/utun-audio-asan` проверяются пять тестов нового тракта с ASAN/UBSAN и
LeakSanitizer; LeakSanitizer запускается вне песочницы. MPSC отдельно проверяется
ThreadSanitizer. Изолированный интеграционный запуск:
```sh
dbus-run-session -- python3 tools/chatgui/tests/run_pulse_recovery.py
```
Итоговый прогон 2026-10-02: сборка `vibechat` успешна, CTest — 11/11,
ASAN/UBSAN/LeakSanitizer — 5/5, компрессор — 479/479, Speex — PASS,
MPSC под ThreadSanitizer — PASS. Timing-стенд прошёл ещё 100 последовательных
повторов. Пауза PCM проверяется также серией pause/resume при работающем соседнем
проигрывателе; она ждёт чтение узла, а не только меняет его флаг.
Тест создаёт собственные PipeWire/PulseAudio/WirePlumber в `/tmp`, отключает
аппаратные monitors и использует два виртуальных sink. Проверяет объединение
voice с общим выходом после `move-sink-input`, сохранение desired выхода звонка
при recovery общего выхода (включая server stream-restore), затем восстановление capture/PCM
после остановки и запуска своего PulseAudio. Звонок и рация сохраняют идентичность;
recorder и медиа продолжают работу. Системный аудиосервер не затрагивается.
Исторические проверки исходного recovery: полный `check.sh` — 119 passed,
0 failed, 1 skipped; proxy/burst/load интеграции прошли. По указанию пользователя
async-тесты исключены из дальнейших запусков. APK ранее собран offline; установка
на телефон не выполнялась.
Физический USB/Bluetooth hotplug, сон ОС, внешние звуки других приложений и качество
AEC в длительном реальном звонке этими проверками не подтверждены. Reference
охватывает звук этого приложения на том же выходе, согласно выбранному контракту.
## Период Linux/PulseAudio и сравнительный замер
`AudioDevice::requestLowLatency()` задаёт запрос 10 мс для capture звонка/рации.
Он применяется при каждом открытии, включая recovery и fallback.
Остальные backends сохраняют прежние параметры.
Общий и отдельный voice output сохраняют прежний период: запрос 10 мс
на проверенной системе не снизил измеренную задержку воспроизведения.
Кадры AEC/кодека остаются 20 мс; `noFixedSizedCallback` позволяет обслуживать
фактические порции backend без дополнительного накопления в miniaudio.
Это запрос задержки, а не гарантия размера callback. PulseAudio согласует
`tlength`, `fragsize`, `minreq`, `maxlength`, `prebuf`; PipeWire также ограничивает
quantum и минимальные порции. `AudioDiag` при init выводит внутренний PCM-формат
и согласованные параметры в миллисекундах (`-1` — значение по умолчанию).
Пересчёт использует внутренние rate/channels/format, а не формат приложения.
Секундные clock-отчёты содержат `latency_us`/`backend_us` и `block_us` в виде
`min/avg/max`. В задержку включены только измеренные timing snapshots;
`estimated_blocks`/`measured` позволяют отличить их отсутствие от нулевой задержки.
Для времени исполнения callback включать `call=trace`, для состояния AEC —
`aec=debug`. Логи агрегируются вне аппаратного callback.
Ручной режим существующего стенда использует настоящие устройства и голосовые
workers, не сохраняет микрофонный PCM и не отправляет его по сети. Выход звонка
без удалённого собеседника обслуживается тишиной; акустическое подавление эха
этот режим не измеряет. Аргументы: capture frames, playback frames, секунды,
необязательный режим `call` (по умолчанию), `radio` или `output`, затем путь лога.
Ноль frames оставляет стандартный период backend; override применяется только
в тестовом executable, до настоящего `ma_device_init`.
```sh
cmake --build tools/chatgui/build --target test_audio_recovery -j4
tools/chatgui/build/test_audio_recovery --latency 960 0 60 > /tmp/audio-baseline.log 2>&1
tools/chatgui/build/test_audio_recovery --latency 480 0 60 > /tmp/audio-capture10.log 2>&1
tools/chatgui/build/test_audio_recovery --latency 960 480 60 > /tmp/audio-playback10.log 2>&1
tools/chatgui/build/test_audio_recovery --latency 480 480 60 > /tmp/audio-both10.log 2>&1
```
Запуски последовательные: несколько потоков могут менять общий quantum сервера.
Сравнивать одинаковые маршруты и нагрузку, отдельно от старта/остановки и после
стабилизации адаптивного PCM-резерва. Проверять backend latency, возраст первого
сэмпла перед готовым TX-кадром, очередь после mix, starvation, holes, потери PCM,
AEC reference и событий диагностики. Затем повторить baseline и проверить
выбранный вариант под нагрузкой. Меньший возраст PCM в capture callback сам по
себе не доказывает такой же выигрыш до кодирования: worker собирает кадры 20 мс.
`test_voice_audio_io` дополнительно проверяет порции по 256 samples: порядок PCM,
временные метки, границы mute/PTT и позицию частичного playback. AEC проверяется
при разных порциях capture/reference (256/480 и 480/256) с противофазным stereo
reference. Очереди по-прежнему ограничены числом событий; уменьшение периода
не увеличивает их ёмкость в миллисекундах.
Сравнение 2026-10-15 на встроенном HDA, PipeWire 1.6.2, 48 кГц. Каждый вариант
работал 60 секунд; таблица содержит средние по callback/кадрам после 45 секунд
прогрева (PCM-резерв к этому моменту уменьшился до одного блока).
| Запрос capture / playback | Capture backend, мс | Первый сэмпл готового TX-кадра, мс | Playback backend, мс | PCM после mix, мс |
| --- | ---: | ---: | ---: | ---: |
| 20 мс / default 25 мс | 30,69 | 34,53 | 16,32 | 18,67 |
| 10 мс / default 25 мс | 16,02 | 29,79 | 16,15 | 18,67 |
| 20 мс / 10 мс | 31,51 | 34,97 | 18,09 | 24,00 |
| 10 мс / 10 мс | 16,13 | 29,66 | 18,46 | 24,00 |
Изменение capture даёт около 14,7 мс на границе backend и около 4,7 мс перед
передачей полного кадра в голосовой стек. Для playback сервер согласовал
`tlength=16ms`, `minreq=5.333ms` при запросе 10 мс; callback уменьшился с 512
до 256 samples, но выигрыша по задержке не получилось. Поэтому в рабочем коде
изменён только capture. Значения в столбцах не следует складывать как независимые
участки общей задержки: capture backend входит в возраст TX-кадра, а очередь после
mix снята уже после извлечения текущего выходного блока из очереди.
В этих четырёх прогретых окнах не было потерь capture/reference PCM, выходного
starvation, backend underrun/overflow или потери событий диагностики.
Отдельные отсутствующие AEC references были и в baseline (4 кадра за окно),
и в остальных вариантах (4–5). Это существующее ограничение привязки reference,
а не проверка акустического качества. Первоначальный output starvation и
временное увеличение PCM-резерва учитываются отдельно от стабильного окна.
Повтор baseline дал 30,76 мс capture backend и 34,99 мс возраста TX-кадра.
Двухминутный прогон выбранного capture 10 мс под нагрузкой четырёх CPU-процессов
дал после прогрева 15,81 и 28,55 мс соответственно; PCM после mix — 18,67 мс.
Потерь PCM, backend underrun/overflow и потери событий диагностики в этом окне
не было. За 75 секунд окна зарегистрировано 25 отсутствующих AEC references.
Отдельный минутный прогон рации: capture backend в среднем 15,48 мс, без потерь PCM.
Сборка `vibechat` и 6 аудиотестов прошли. Изолированный интеграционный тест
подтвердил объединение выходных маршрутов и восстановление capture/playback после
перезапуска тестового PulseAudio. Запускать весь стенд через `dbus-run-session`,
чтобы имя его PulseAudio не конфликтовало с сервером пользовательской сессии.
В fixture перед `radio_audio_init` создаётся `radio_instance`, освобождаемый после
`radio_audio_destroy`; это исключает ложную ошибку регистрации mute callback.
Полный `check.sh` вне sandbox: 156/156, 9/9 proxy-интеграций, burst и load — PASS.
В sandbox сетевые тесты не проходят из-за запрета сокетов; этот запуск не является
проверкой сетевых регрессий.
## Референсы
Первичные исходники и документация просмотрены 2026-10-01; ссылки на master могут
изменяться. Используем принципы, а не копируем целиком архитектуру библиотеки.
- [PipeWire audio-src-ring.c](https://docs.pipewire.org/audio-src-ring_8c-example.html):
callback читает доступные кадры, заполняет остаток тишиной и возвращает полный
буфер; пополнение инициирует через событие управляющего потока. Хороший минимальный
пример поведения при пустой PCM-очереди.
- [PulseAudio stream API](https://github.com/pulseaudio/pulseaudio/blob/master/src/pulse/stream.h):
отдельные underflow/overflow/started/suspended/moved callbacks; peek различает
пустую очередь и hole с ненулевой длиной. Нужно разделять эти состояния в логах.
- [PulseAudio pacat.c](https://github.com/pulseaudio/pulseaudio/blob/master/src/utils/pacat.c):
underflow callback сообщает событие без пересоздания stream. При failed context
утилита завершает работу — это пример диагностики, не готовая self-healing система.
- [Mozilla cubeb PulseAudio backend](https://github.com/mozilla/cubeb/blob/master/src/cubeb_pulse.c):
явные context/stream states, пересоздание ошибочного context при инициализации,
согласованное отключение callbacks и доступ под mainloop lock; minimum latency
25 мс. Это полезный референс владения и границ управления, а не основание
автоматически заимствовать всю библиотеку.
- [miniaudio](https://github.com/mackron/miniaudio/blob/master/miniaudio.h):
init/start/stop/uninit запрещены внутри data callback из-за deadlock. Просмотренный
upstream тоже содержит break при пустом duplex-ring: обновление библиотеки само
по себе не решает этот найденный дефект.
-66
View File
@@ -1,66 +0,0 @@
# Важные нюансы работы встроенного lwIP TCP-стека
Краткая памятка по тонким местам `src/lwip_tcp/`, критичным для прокси.
Полное описание модуля — в `src/lwip_tcp/lwip_tcp_doc.md`.
## Ключевые константы (`lwip_tcp_opts.h`)
| Константа | Значение | Смысл |
|-----------|----------|-------|
| `TCP_MSS` | 1460 | максимальный размер сегмента |
| `TCP_WND` | 8×MSS = 11680 | окно приёма lwIP (объявляется пиру) |
| `TCP_SND_BUF` | 16×MSS = 23360 | буфер отправки |
| `TCP_TMR_INTERVAL` | 250 мс | базовый интервал таймера |
| `tmr_interval_ms` | `TCP_TMR_INTERVAL/4` = 62.5 мс | фактический шаг `tcp_fasttmr` |
| `TCP_WND_UPDATE_THRESHOLD` | `TCP_WND/4` = 2920 | порог немедленного ACK окна |
**Важно:** таймер uasync использует timebase 0.1 мс — `uasync_set_timeout(ua, N, …)`
задаёт N×0.1 мс. Например, 5000 = 500 мс.
## Контракт recv_cb — главный источник багов
lwIP вызывает `recv_cb(arg, pcb, pbuf, err)` для данных, **уже извлечённых из pcb**.
Дальше приложение само решает судьбу данных и окна:
| Действие | Результат |
|----------|-----------|
| скопировать + `tcp_recved(pcb, len)` | данные приняты, окно восстановлено (норма) |
| `return != LERR_OK` | lwIP кладёт pbuf в `refused_data` и отдаст позже — **без потери**, окно закрывается |
| `pbuf_free(p)` без `tcp_recved` | данные потеряны, окно не восстановлено → схлопывание окна |
`refused_data` — штатный backpressure lwIP: одноканальный (один pbuf), ре-доставка
через `tcp_fasttmr` (62.5 мс) или при следующем входящем сегменте.
**Ошибка, из-за которой падал `tcp_proxy_full`:** при backpressure прокси делал
`pbuf_free(p)` без `tcp_recved`. Каждый такой сброс уменьшал `rcv_wnd` на MSS; после
~8 пакетов окно падало в 0, OS-TCP переставал слать, и передача умирала.
## Управление окном и «деградация 62.5 мс»
`tcp_recved(pcb, len)` увеличивает `rcv_wnd` и через `tcp_update_rcv_ann_wnd` решает,
слать ли ACK окна немедленно. Немедленный ACK идёт только если прирост окна
`wnd_inflation ≥ TCP_WND_UPDATE_THRESHOLD` (=2920 = 2×MSS); иначе ACK откладывается
до `tcp_fasttmr` (62.5 мс).
Отсюда классическая ловушка: если после backpressure окно восстанавливать **по одному
пакету** (`tcp_recved(1460)` за раз), прирост 1460 < 2920 → ACK уходит раз в 62.5 мс →
пир шлёт 1 пакет за 62.5 мс ≈ 23 КБ/с. Это выглядит как «пропускная способность
застряла», хотя сеть свободна.
**Правильно:** восстанавливать окно **пачкой** (несколько `tcp_recved` подряд либо один
`tcp_recved(TCP_WND_MAX - rcv_wnd)`), тогда прирост ≥ 2920 → ACK немедленный → пир
сразу возобновляет бурст.
## Рекомендуемый паттерн для relay-потребителя (как в `tcp_proxy_client.c`)
1. При получении данных — копировать в ограниченную очередь, **не вызывать**
`tcp_recved` (окно само плавно закрывается; очередь ограничена `TCP_WND`).
2. Дрейн очереди по сигналу освобождения нижележащего канала
(`etcp_router_on_send_ready`) + retry-таймер (force=1) как safety-net.
3. `tcp_recved(len)` вызывать **только после успешной отправки** — окно
восстанавливается пачкой по мере дрена, ACK уходит немедленно.
4. Никогда не сбрасывать pbuf без `tcp_recved`; не держать данные в `refused_data`
дольше одного пакета (single-slot).
См. реализацию: `tcp_proxy_client_tx_queue_drain_cb` / `tcp_proxy_client_fin_flush`
в `src/proxy/tcp_proxy_client.c`.
-375
View File
@@ -1,375 +0,0 @@
Файлы участников CHAT-группы
===========================
Модуль: src/member_files/. Desktop-браузер: tools/chatgui/src/member_files/.
Отдельный сервис router ETCP_RT_ID_MEMBER_FILES (0x39), отдельно от вложений
сообщений media_delivery и E2E-файлов DM. Серверы выбираются явно; между
серверными деревьями автоматической репликации нет.
Хранение и конфигурация
----------------------
Корень по умолчанию: <db_path>/member_files/ (без db_path — ./member_files/).
Публичное дерево: <корень>/<group_id>/<node_id>/<относительный путь>.
Временные файлы: <корень>/.uploads/<upload UUID в hex>.
При старте сервис создаёт/открывает корень в дисковом worker, удаляет старые
незавершённые temp и сканирует опубликованные файлы. До готовности отвечает BUSY.
Ошибка сканирования оставляет локальное хранилище недоступным до restart;
частичные счётчики клиенту не выдаются. Клиентские передачи к другим серверам продолжают работать.
Очистка кэша вложений media/ это дерево не обходит.
Настройки [chatserver] / [chat]:
member_files_enabled=1
member_files_user_quota_mb=1024
member_files_upload_ttl_sec=3600
member_files_path=/путь/к/хранилищу (опционально, до 255 байт UTF-8)
Путь, квота и TTL читаются при старте чат-сервиса; изменение требует его restart.
enabled проверяется на каждом запросе. Лимит одинаков для каждого UID отдельно,
размер UID суммируется по всем группам на этом сервере. Значение в МиБ (1024*1024 байт).
Счётчики used/reserved/files/directories хранятся в памяти дискового worker и восстанавливаются с диска только при старте.
Каталоги не занимают байтовую квоту. files/directories считают опубликованные файлы и каталоги внутри деревьев UID
по всем группам; корни групп, корни UID и .uploads исключены. Пустой опубликованный файл считается файлом.
Сохранённые файлы удалённых участников также занимают квоту.
При начале загрузки резервируется размер нового файла или положительный прирост при замене.
WRITE не изменяет used; COMMIT обновляет used на разницу размеров и освобождает резерв.
ABORT/Cancel/TTL освобождают резерв после удаления temp; REMOVE вычитает размер файла.
Переименование внутри UID не меняет размер и числа объектов. Новый COMMIT увеличивает files на 1;
замена существующего файла, включая нулевой размер, сохраняет files. MKDIR увеличивает directories на 1,
REMOVE уменьшает соответствующий счётчик. Для mkdir учитывается факт создания даже при последующей ошибке sync.
BEGIN/WRITE/ABORT/Cancel/TTL не увеличивают files: temp не опубликован.
Все изменения корректируют счётчики инкрементально, без обхода каталога пользователя;
QUOTA только читает их из памяти. LIST выполняет свой обычный обход для отображения записей, не пересчитывая статистику.
Повторы BEGIN/COMMIT/MKDIR/REMOVE не меняют учёт повторно.
Уже превышенная квота не удаляет данные: чтение, удаление и уменьшение файла доступны,
новый положительный прирост запрещён. Физический temp может занимать больше места,
чем резерв прироста; нехватка места файловой системы остаётся отдельной I/O-ошибкой.
Нехватка квоты/места возвращает NO_SPACE; опубликованные файлы не вытесняются.
Общая member_files_quota_mb удалена. Корень обслуживается только файловым сервисом;
ручные изменения требуют повторного сканирования через restart.
Сервер — действующий полный мембер группы с подписанным adm_tags.storage="yes".
Колонка peers.storage — производный сетевой приоритет, для полномочий не используется.
Каждый запрос и ответ подписаны Ed25519 и зашифрованы end-to-end средствами
route_crypto. group/src берутся только из проверенного заголовка router;
ETCP peer промежуточного хопа не считается автором запроса.
Читать могут действующие участники той же группы. Изменять дерево может только
src == owner. Удалённые записи и source=1 (плейсхолдеры) доступа не дают.
Открытый fd/UUID загрузки не отменяют повторную проверку членства.
Файлы удалённого участника сохраняются на диске, но через сервис не доступны.
Wire, версия 4
--------------
Все числа LE, без сырых структур и padding. Фиксированный заголовок 125 байт:
0 protocol_version:u8 = 4
1 kind:u8 (1=request, 2=response)
2 command:u8
3 status:u16 (request=0)
5 request_id:u64, ненулевой случайный ID
13 owner:u64 (UID каталога; 0 только для LIST виртуального корня группы)
21 offset:u64
29 size:u64
37 cursor:u64
45 version:16
61 upload:16
77 hash:32
109 mtime:u64, Unix seconds
117 type:u8 (1=file, 2=directory)
118 more:u8 (0/1)
119 path_len:u16
121 target_len:u16
123 data_len:u16
После заголовка: path[path_len], target[target_len], data[data_len], без NUL.
Длина пакета обязана точно совпадать с указанными размерами.
Путь максимум 1024 байта, компонент максимум 255 байт, максимум 60 компонентов.
Данные максимум 8192 байт; вместе с полным router-путём и crypto укладываются
в лимит normalizer 16384 байта. Размер файла/смещение ограничены INT64_MAX.
Пути — строгий UTF-8, относительные, разделитель '/'. Запрещены пустые компоненты,
компоненты с ведущей '.', завершающей точкой/пробелом, управляющие символы,
Windows device names и символы \ : * ? " < > |. Корень внутри UID — пустой путь.
POSIX разрешает путь через *at/O_NOFOLLOW; regular-файлы с hardlink отвергаются.
Windows удерживает родительские каталоги без SHARE_DELETE и не принимает reparse points.
Команды
---------------
1 LIST: owner/path/cursor/version. Первая страница cursor=0, version=0.
Ответ data — записи: name_len:u16,type:u8,size:u64,mtime:u64,version:16,name.
cursor — сколько записей уже выдано. more=1 требует следующую страницу с
возвращёнными cursor/version. При изменении снимка — STALE, начать сначала.
Размер каталога не ограничивает память страницы. Виртуальный корень группы
выдаёт UID действующих полных участников, даже если каталог ещё не создан.
Изменение namespace сервера консервативно инвалидирует дисковые LIST-курсоры.
2 STAT: owner/path. Ответ type/size/mtime/version. Не хеширует весь файл.
version — непрозрачный снимок identity/size/mtime/ctime и эпохи сервера.
Закрытие fd по таймауту токен не меняет; restart меняет дисковые токены.
3 READ: owner/path/version/offset/size (1..8192).
Ответ size — полный размер файла, data_len — фактически прочитано,
more — есть ли байты после ответа. offset==EOF возвращает 0 байт/more=0;
offset>EOF — INVALID_RANGE. Изменение файла между блоками — STALE.
Клиент не смешивает версии и публикует локальный файл только по завершении.
4 BEGIN_WRITE: owner/path/upload/size/hash/version.
upload — 16 случайных байт, size — конечный размер, hash — SHA256 содержимого.
version=0 означает создать новый файл, существующий возвращает EXISTS.
Для замены требуется версия из STAT/LIST. В один путь одна активная загрузка.
UUID привязан к group/src/owner/path; повтор BEGIN с теми же параметрами безопасен.
5 WRITE: owner/path/upload/offset/size/data. size==data_len, диапазон внутри
конечного размера. Можно писать в произвольном порядке и повторять блок.
ACK подтверждает запись в temp, а не durable публикацию всего файла.
6 COMMIT: owner/path/upload. Проверяет SHA256, fsync/_commit и ожидаемую
версию назначения, затем атомарно заменяет файл и синхронизирует namespace.
Прежний файл виден до публикации. Ответ содержит размер/версию/SHA256.
Незаписанные sparse-области читаются как нули; неправильное содержимое не
проходит SHA256. Пустые файлы поддерживаются. При I/O-ошибке во время
публикации результат может быть неопределённым; нужно повторно проверить STAT.
7 ABORT: owner/path/upload. Удаляет temp, освобождает резервацию.
Неизвестный UUID — OK; опубликованные файлы никогда не удаляет.
8 MKDIR: owner/path. Создаёт один каталог; родители должны существовать.
9 RENAME: owner/path/target/version. Только внутри UID/группы, без замены
существующего назначения. Активная загрузка в затрагиваемом пути — BUSY.
10 REMOVE: owner/path/version. Файл или пустой каталог, без рекурсивного удаления.
11 STREAM_WRITE: открытие потоковой загрузки; поля как BEGIN_WRITE,
cursor — ненулевой ID потока. Ответ подтверждает готовность temp и конечный размер.
12 STREAM_READ: открытие потокового скачивания; owner/path/version/size/cursor,
upload — случайный токен клиента. Сервер сверяет снимок и размер перед началом.
13 STREAM_DATA: request_id — ID потока, owner/path/upload, offset/size/data.
kind=request для upload, response для download. Ответа на блок нет.
offset обязан равняться следующему принятому смещению; size==data_len, не более 8192.
14 STREAM_END: ID потока, owner/path/upload, offset==size==размер файла, SHA256.
Upload: клиент отправляет END после всех DATA, сервер отвечает END после COMMIT.
Download: сервер отправляет END после DATA и повторной проверки версии; клиент
проверяет SHA256 записанных данных, синхронизирует и публикует локальный файл.
Ошибка сервера приходит END.status!=OK; ошибка открытия — ответом на STREAM_READ/WRITE.
15 STREAM_CANCEL: клиент отменяет конкретный поток. Поздние DATA не публикуются.
16 QUOTA: owner=UID, path/target пустые, cursor=0, data_len=0.
Права чтения те же, что у STAT/LIST выбранного участника. Ответ data_len=40:
used:u64, limit:u64, reserved:u64, files:u64, directories:u64, LE.
Первые три значения в байтах, последние два — числа объектов. Снимок общий для UID во всех группах.
Не резервирует место и не занимает квитанцию мутации. used может превышать limit
после уменьшения лимита; сервер не выдаёт фиктивный нулевой размер при ошибке учёта.
Открытие потока — обычный повторяемый запрос с отдельным request_id. cursor связывает
его с DATA/END/CANCEL. Поток определяется group/peer/kind/ID; один ID не может
одновременно обозначать разные открытия. Каждый пакет по-прежнему подписан и зашифрован.
При закрытии/перезапуске router-канала его handles завершаются с IO_ERROR;
байты разных эпох не смешиваются. Возобновление через новую эпоху пока не реализовано.
Статусы (0..13): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT.
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ на 120 секунд
(64 квитанции); тот же request_id с другим содержимым — BAD_PACKET.
COMMIT дополнительно узнаётся по upload UUID. При заполнении квитанций новая
мутация возвращает BUSY до истечения старых. Клиент повторяет запрос с прежним
request_id раз в 5 секунд, общий срок 120 секунд. Повторы после этого срока
не дают гарантии того же результата; перед новой мутацией требуется STAT.
Потоки, таймауты и остановка
---------------------------
На экземпляр один дисковый pthread с очередью до 64 обычных задач. Сеть,
ACL, таймеры и state machines выполняются в uasync; open/stat/readdir/read/write,
resize/hash/sync/rename/remove/close — в дисковом потоке. GUI только передаёт
команды через bridge и получает события. Нет вызова дискового I/O из callback
таймера. Completion резервируется до постановки задачи; без polling workers.
Локальные файлы transfer и серверное дерево используют тот же дисковый поток.
Для обязательной очистки резервируется отдельная задача до открытия файла.
Обращение к своему серверу доставляется через отдельную ограниченную очередь
call_soon в тот же обработчик, без сетевого канала. ACL и дисковый поток общие;
сетевой recv не принимает unsigned даже при src == self.
Сервер держит до 64 открытых файлов. Каждый READ/WRITE/BEGIN продлевает idle.
После 10 секунд простоя fd закрывается на очередном дисковом sweep (таймер 1 с);
занятый дисковый поток откладывает sweep до освобождения. Закрытие не публикует
и не удаляет файл. Temp можно открыть заново до истечения upload TTL.
После restart незавершённые temp удаляются; опубликованные файлы остаются.
Лимиты: 16 клиентских запросов, 32 серверных задания/ответа, 16 временных
загрузок, 16 клиентских и 16 серверных потоков на экземпляр. Каждый поток имеет
кольцо 32 x 8192 байт (256 КиБ); worker обрабатывает до 16 блоков за задачу.
Чтение следующей порции перекрывается с отправкой уже подготовленной.
Все серверные и локальные I/O используют один FIFO worker, без блокировки uasync.
Отправитель добавляет пакеты, пока send_q router находится под нижним порогом
(сейчас count<=0), затем ждёт etcp_router_on_send_ready. Файловый слой не устанавливает
своё окно inflight и не повторяет DATA; транспортные ACK, retransmit и порядок — router.
Callback отправляет не более 8 блоков за запуск, уступая очередь другим потокам.
Параллельные файлы одного пира разделяют канал и FIFO/round-robin waiters.
Готовность приёмника проверяется router до доставки следующего seq. Заполненное
кольцо или очередь заданий оставляют пакет в recv_q: rx_seq/ACK не продвигаются.
Освобождение места вызывает отложенный resume; pump отменяется при reset/close.
ACK RECEIVE_BUSY (flags bit0) раз в 100 мс отличает ожидание диска от потери транспорта.
Отправитель приостанавливает новые DATA, а живые busy ACK продлевают таймаут router.
Буфер router ограничен его receive window. Локальная доставка использует такую же
проверку готовности и ограниченный FIFO, а не сеть или отдельные подтверждения.
При остановке сначала отменяются producers/requests и дожидаются все stream cleanup
continuations; только затем worker освобождает storage. Отмена после уже выполненной
публикации не удаляет опубликованный файл.
Transfer владеет CM handle до окончания/отмены; DOWN/TIMEOUT handle не освобождает.
quiesce запрещает новые команды, отменяет таймеры, waiter и запросы; destroy
вне callbacks дожидается только своих работ/completions и закрывает дисковый
поток до уничтожения CHAT-групп. Контексты не освобождаются под работающим I/O.
Отмена не прерывает текущий системный I/O или хеширование, а ждёт его completion.
Desktop
-------
Контекстное меню канала: Group files. Меню участника: Member files.
Окно: выбор сервера/UID, путь, Up/Refresh, таблица каталогов/файлов,
Upload/Download/New folder/Rename/Delete, прогресс и Cancel transfer.
Чужой UID доступен только для чтения. Автор может быть offline.
Open скачивает в выбранный пользователем локальный файл и открывает его
приложением ОС после завершения. Смена контекста отбрасывает поздние ответы.
Для выбранного UID строка хранилища показывает занято/всего, число файлов/каталогов и резерв текущих загрузок.
Точные байты доступны в tooltip. QUOTA запрашивается отдельно от LIST при выборе
сервера/UID, обновлении каталога, завершении передачи и каждые две секунды видимого окна.
Поздние ответы старого UID/сервера игнорируются. BUSY при стартовом сканировании
сохраняет Loading directory и повторяет LIST через секунду; новый переход отменяет повтор.
API параллельных передач
-----------------------
member_files_transfer_start(request, callback, arg) копирует request и возвращает
отдельный mf_transfer* на каждый файл. upload задаёт направление, group/server/owner
выбираются для каждой операции. id=0 выбирает случайный ID; id в callback — фактический.
NULL означает отказ запуска без callback; причина пишется в лог.
max_bytes=0 оставляет прежний лимит; ненулевое значение ограничивает размер файла.
Upload проверяет размер источника до хеширования, download — ответ STAT до открытия
локального temp. STREAM привязан к проверенным размеру/version; превышение — INVALID_RANGE.
callback(arg, mf_transfer_progress*) получает id/done/total/status/finished.
Прогресс заимствован до возврата callback. done для отправителя означает байты,
принятые router в очередь; успешный finished подтверждает завершение операции,
для upload — публикацию на сервере, для download — публикацию локального файла.
finished приходит ровно один раз, handle к этому моменту уже освобождён;
вызывающий обнуляет свой указатель в callback. member_files_transfer_cancel(handle)
отменяет только этот файл, ждёт его I/O и ставит CANCEL в очередь с backpressure.
Stop сервиса/группы завершает все принадлежащие ему handles.
Все API/callbacks выполняются в uasync-потоке экземпляра. GUI trampolines используют
тот же API и превращают callback в существующие события прогресса; Qt не делает I/O.
Один локальный файл назначения не может одновременно принадлежать двум download.
Аватарки пользователей
---------------------
Относительный путь avatars/<sha256>.<расширение> находится внутри обычного дерева UID:
<корень>/<group>/<UID>/avatars/<sha256>.<расширение>. Дополнительного UID в API/пути нет.
Аватарка по умолчанию копируется в каждую группу, где выбранный сервер имеет
подписанную storage-роль. Ссылка каждого канала публикуется только после COMMIT
соответствующей копии; отсутствующий сервер в другом канале не создаёт битую ссылку.
Новое членство обнаруживается через снимки участников и повторяет публикацию.
Между серверами автоматического копирования нет.
userinfo содержит строковые avatar_server (16 hex цифр) и avatar_file (64 hex SHA256 + .json).
Оба поля отсутствуют при удалённой аватарке. Профиль подписан ключом участника;
смена имени/адресов сохраняет остальные плоские JSON-атрибуты. userinfo ограничен
1024 байтами UTF-8. Merkle, JOIN_INFO_RESP, CHANNEL_INVITE и JOIN_REQUEST используют
длину userinfo:u16 BE вместо прежнего u8. Bootstrap передаёт существующий подписанный
профиль инвайтера, не создавая более свежую запись только с именем. Обе стороны
нужно обновить: обратной совместимости нет.
Снимок участника CHAT_MEMBER_DISPLAY_SIZE теперь 158 байт: прежние 79 байт,
avatar_server:8LE, avatar_file:70 (с NUL), storage_role:1. storage_role обозначает
подписанную роль сервера, независимо от существующей колонки storage — сетевого
приоритета, который может быть 0 у сервера за NAT. Потребители используют общий
chat_member.h; локальное изменение профиля также вызывает MEMBER_UPDATED.
В Settings → Profile доступны Upload avatar, выбор storage-сервера, предпросмотр
и Remove avatar. Изменения применяются сразу, независимо от Save других настроек.
Перед загрузкой desktop/Android показывают редактор: неподвижная квадратная рамка,
перемещение фото и масштабирование (мышь/колесо или жесты). Отмена не публикует новую ссылку.
Worker принимает JPEG/PNG/WebP до 32 МиБ и 16 мегапикселей. Исходные байты, включая EXIF,
сохраняются без изменений. Выбранная область после EXIF-ориентации становится WebP lossy
256×256, quality=85, максимум 1 МиБ. Описание JSON (максимум 4096 байт): version=1,
original/preview — имена объектов; original_bytes/preview_bytes, width/height — размеры
ориентированного оригинала; x/y/w/h — нормализованный квадрат в его координатах.
Все объекты лежат в avatars/<SHA256 собственных байтов>.<jpg|png|webp|json>.
Порядок COMMIT: оригинал, превью, описание; затем подписанная ссылка на описание.
Повтор использует уже сохранённые неизменяемые объекты. Сервер проверяет имя/SHA256,
лимит объекта и запрещает замену под прежним именем и rename в/из avatars.
Файлы и каталог входят в обычные квоты. Прежние версии сохраняются на сервере;
владелец может удалить их через файловый браузер. Старые PNG-ссылки не поддерживаются.
Клик по аватарке сообщения/участника открывает галерею пользователя: LIST каталога
выбирает только описания, проходит все страницы cursor/version, сортирует mtime DESC,
при равенстве — по имени. Изначально выбрана текущая подписанная ссылка. Большое фото —
полный оригинал, до его загрузки показывается превью. Внизу пять квадратных позиций:
две предыдущие, выбранная по центру, две следующие; за границами списка позиции пустые.
Desktop: кнопки/клавиши ←/→; Android: кнопки/свайпы при исходном масштабе. Превью кликабельны.
Загружаются выбранный оригинал и видимые превью; закрытие отменяет запросы без других владельцев.
Для DM источник — общий канал, из которого берётся отображаемая аватарка.
AvatarManager общий для списка участников и сообщений, максимум две операции
одновременно, одинаковые хеши не скачиваются параллельно. Изменение своего профиля
имеет приоритет перед фоновыми загрузками. Для DM используются ссылки из общих
каналов; при отсутствии доступной ссылки остаются инициалы. Онлайн-статус списка
отображается отдельной точкой поверх фотографии. Поздний результат старой ссылки
пополняет кеш, но не заменяет картинку новой ссылки.
Кеш аккаунта: <db_path>/avatar_cache/<local_UID_hex>/<sha256>.<расширение>; без db_path
используется Qt AppLocalDataLocation. Проверяются SHA256 объектов, описание и WebP 256×256;
превышение/битая картинка не публикуются. Публикация кеша атомарна через QSaveFile,
незавершённые download/part очищаются при старте. Готовые файлы не требуют TTL/STAT:
смена содержимого меняет имя. Дисковый LRU — 100 МиБ, кеш QImage — 32 МиБ;
собственная аватарка и источники незавершённых upload сохраняются при вытеснении.
Дисковое I/O и декодирование выполняются вне GUI/uasync. Повторы неудачных загрузок:
5/10/20/40/80/160 секунд; новая ссылка сбрасывает задержку. RTT/online не меняют ссылку
и не вызывают повторной загрузки. Желаемая собственная ссылка/удаление сохраняются
атомарно одной строкой ui_state.avatar_default, незавершённая публикация повторяется
после запуска при наличии локального источника.
test_avatars проверяет нормализацию, хеш/размеры, очистку temp, настоящее storage
в двух каналах, публикацию, сохранение при смене имени, DM-кеш, restart без сети,
автоматический download после потери кеша и удаление ссылок. Также проверяются
max_bytes до открытия local temp и смена аватарки при задержанном download старой
версии: поздний результат не заменяет новый рисунок. test_member_adapter проверяет
подписанные профили 191/255/256/512/1023/1024 байт вместе с adm_tags до 8192.
Категория media=debug показывает discovery, cache hit/eviction, направление,
group/owner/server/hash, ошибки проверки и retry; media=info — передачи/публикации.
Проверки и диагностика
---------------------
tests/test_member_files: wire/UTF-8, произвольные блоки, повторы, EOF, версии,
короткая замена, SHA256, пустой файл, независимые квоты UID, общий учёт по группам,
резервирование/повторы/TTL, инкрементальные files/directories, замены пустых файлов,
создание/удаление/rename каталогов, отсутствие runtime-rescan, стартовый scan и его ошибки, ссылки, expiry/restart, 10-секундный
fd idle и принадлежность work/done потокам.
tests/test_member_files_network: настоящие A/B/M, подписанная storage-роль,
параллельные upload/download одного пира, local loopback, пустые файлы, побайтная
проверка, отмена одного handle, restart router и destroy с активным server I/O.
Шестисекундная задержка worker проверяет receive backpressure и busy ACK.
Также проверяются чужая запись, виртуальный корень и подписанное удаление читателя.
QUOTA проверяется на настоящем подписанном/зашифрованном канале; удалённый читатель не получает счётчики.
test_services — stop/start общего чат-сервиса.
Текущая диагностика выделена в категорию debug. Настройка для полного трасса:
[debug]
debug=trace
debug=debug показывает запрос/ответ, ACL, очереди, маршрут, ACK, crypto-решения,
I/O, fd idle и состояние браузера. trace дополнительно показывает каждый
read/write и передачу задач/завершений между дисковым потоком и uasync.
stream progress раз в секунду показывает ID/направление, done/total, число блоков
в кольце, активность I/O, причину ожидания и скорость B/s. Router логирует переходы
receive paused/resumed и peer receive busy без повторного сообщения на каждый блок.
quota scan started/ready показывают путь, число пользователей и время обхода;
quota updated показывает UID/операцию/used/reserved/files/directories; quota exceeded — used/reserved/growth/limit.
По request_id видны GUI post → core request → router seq → server rx → ACL →
disk queued/start/done → response tx → GUI result/bridge → directory ready.
PID создания worker, PID отправки задачи и startup PID позволяют однозначно
обнаружить fork после создания потока; несовпадение PID логируется как ошибка.
Ключи шифрования, plaintext файлов и сырые медиа в debug не выводятся.
Старые VAD/рация/GUI/вложения сообщения перенесены в vad/radio/chat/media/config;
ошибки и пользовательские info остаются в тематических категориях.
-36
View File
@@ -1,36 +0,0 @@
# Ссылки в сообщениях
Android и Qt распознают `http://`, `https://` и `utun://` в обычном тексте
личных и групповых сообщений. HTTP/HTTPS нечувствительны к регистру схемы.
Ссылки выделяются цветом и подчёркиванием; длинные веб-адреса переносятся по ширине.
Сокращённая строка структурированного PM-приглашения сохраняет полный `utun://` адрес.
Пробелы, управляющие символы, кавычки и угловые скобки отделяют ссылку от текста.
Конечные `. , ; : ! ?` и лишние закрывающие скобки удаляются из распознанного адреса.
Сбалансированные скобки в адресе, параметры, fragment и Unicode сохраняются.
Адреса без схемы не распознаются. Некорректные веб-адреса остаются обычным текстом.
Исходное сообщение в БД и протоколе не меняется.
Разборщики возвращают диапазоны UTF-16 и полные адреса:
`tools/chatgui/src/message_links.cpp`, Android `data/MessageLinks.kt`.
Подсветка и нажатие используют эти диапазоны. Android подставляет emoji placeholders
только в обычный текст между ссылками: emoji внутри адреса остаётся частью полного URL.
Qt использует один QTextLayout для размеров, отрисовки, выделения и координат нажатия.
Перетаскивание выделяет текст без открытия ссылки.
HTTP/HTTPS открываются из GUI системным обработчиком: Android `ACTION_VIEW`,
Qt `QDesktopServices::openUrl`. `utun://` запускает существующий JOIN.
Ошибки открытия показываются через перевод интерфейса и логируются.
Диагностика открытия содержит схему и host, без полного пути/query.
Общие случаи: `tools/chatgui/tests/message_links_cases.tsv`.
Qt `test_invite_links` проверяет разбор, перенесённые веб-ссылки, смесь схем,
нажатие, drag/copy и сокращённые приглашения; `test_emoji_playback` — жесты приглашений.
Android `tests/test_message_links.kt` проверяет тот же TSV на классах из APK-сборки:
```bash
cd tools/chatgui-android
./build.sh noinstall
kotlinc tests/test_message_links.kt -classpath app/build/tmp/kotlin-classes/debug -d /tmp/message-links-tests.jar
kotlin -classpath /tmp/message-links-tests.jar:app/build/tmp/kotlin-classes/debug Test_message_linksKt ../chatgui/tests/message_links_cases.tsv
```
-109
View File
@@ -1,109 +0,0 @@
Как работает детектор NAT
========================
Детектор выясняет две вещи: какой внешний IP:порт получает рабочий UDP-сокет
при обращении к разным IP и проходят ли к нему пакеты от незнакомого IP.
Проверяется именно сокет, через который работает приложение.
Есть быстрый путь без помощников: если рабочий сокет принял подключение
и локальный IP и фактический IP подключившегося узла публичные,
ему сразу ставится direct/open (UNCHANGED + OPEN). Это правило действует
для UDP и TCP, даже если с IP пира раньше уже был исходящий обмен.
Локальные подключения и SOCKS интернет-вход не подтверждают. Для TCP
фактический локальный IP должен совпадать с IP интерфейса сокета.
Нужны три узла: A проверяет себя, B наблюдает его внешний адрес,
C помогает проверить входящие пакеты. Команды и результаты передаются
по уже работающей сети uTun, в том числе через TCP или промежуточные узлы.
Сами тестовые пакеты идут напрямую по UDP.
Порядок проверки
---------------
1. A отправляет пакет узлу B. Тот сообщает, с какого IP:порта его получил.
Ответ идёт по управляющему соединению, поэтому блокировка прямого
UDP-ответа не мешает узнать внешний адрес A.
2. Через B узел A просит C отправить несколько пакетов на этот внешний адрес.
До этого A не должен отправлять ничего на IP узла C с проверяемого сокета.
Приложение хранит историю таких отправок и учитывает новые во время проверки.
На входящие тестовые пакеты A автоматически не отвечает.
3. Получатель проверяет настоящий адрес отправителя каждого тестового пакета.
Локальный или другой непубличный IP не подходит для интернет-проверки.
Для проверки незнакомого входа также не подходят IP узла B, собственный
внешний IP узла A и IP, на который A уже отправлял пакеты.
Если помощник не подходит, A выбирает другого. Разные узлы с одним IP
не считаются разными источниками. Проверяются также подлинность пакета
и принадлежность текущей попытке, чтобы чужой или старый пакет не влиял на результат.
4. Затем A сам отправляет пакет C и узнаёт, какой внешний IP:порт видит C.
Если входящие до этого не проходили и внешний адрес совпал с увиденным B,
C повторяет отправку с того же IP:порта на исходный внешний адрес A.
Так проверяется, открыл ли исходящий контакт путь для входящих.
Параллельный обмен с B подтверждает, что UDP-связь в целом работает.
5. A повторяет измерение внешнего адреса через B и C. Результат принимается,
только если адрес для каждого направления остаётся стабильным.
Сравниваются разные IP назначения; другие порты того же IP не проверяются.
Что означает результат
----------------------
strict — B и C стабильно видят разные внешние IP:порты узла A.
Единого внешнего адреса для подключения с любого IP нет.
eim open — B и C видят одинаковый внешний IP:порт, и пакет от C пришёл
ещё до исходящего контакта A с его IP.
eim filtered — внешний IP:порт одинаковый, серия пакетов до контакта не прошла,
а после исходящего контакта пакеты с того же адреса C начали приходить.
Это оценка: потерю UDP-пакетов нельзя абсолютно отличить от блокировки.
EIM? — два направления видят одинаковый внешний IP:порт, но незнакомый вход
проверить не удалось, например все доступные помощники уже были знакомы.
Это EIM с неизвестной доступностью входящих, без обещания открытого входа.
Такой же предварительный EIM? дают два STUN-сервера, видящие одинаковый внешний
IP:порт тестового сокета на том же локальном IP и интерфейсе. Поддержка запроса
с незнакомого IP для этого не нужна. Источник оценки — STUN, вход остаётся UNKNOWN.
Измерение рабочего сокета имеет приоритет; STUN-оценка действует до пяти минут
и обновляется автоматически. Публичный порт для подключения берётся только
из наблюдения рабочего сокета: внешний порт STUN в него не подставляется.
Если внешний IP:порт совпадает с локальным, отдельно отмечается отсутствие
трансляции адреса. Доступность входящих всё равно проверяется.
Если данных недостаточно, соответствующая часть результата остаётся UNKNOWN.
Известный тип NAT можно сохранить, даже если входящие проверить не удалось.
Таймаут и отсутствие подходящих узлов сами по себе не означают strict.
Если один пир уже увидел публичный IP:порт, отличный от локального, этот адрес
сразу сохраняется как NAT с type=UNKNOWN. Он публикуется отдельно от адреса
интерфейса и остаётся кандидатом для пробного подключения: одного наблюдения
недостаточно, чтобы обещать тот же адрес для других IP или открытый вход.
Уже знакомый помощник пригоден для измерения типа NAT, хотя проверить незнакомый
вход через него нельзя. Потеря результата входящих не останавливает сравнение
адресов через доступные публичные узлы; ожидание одного этапа ограничено по времени.
Измеренный mapping сохраняется сразу, а проверка незнакомого входа продолжается
через остальных помощников. Порядок обхода фиксирован на его начало; фактический
source всегда проверяется по актуальной истории. При паузе по общему бюджету
незавершённая попытка повторяется с новыми идентификаторами, без сброса обхода.
Если попытка уже получила полный бюджет, его исчерпание считается таймаутом:
обход переходит дальше, а не повторяет одного помощника бесконечно.
Старый сохранённый результат входящих не считается успехом новой проверки.
Соединения по LAN работают независимо и не сбрасывают интернет-профиль.
Если доступны только локальные узлы, измерить тип интернет-NAT для UDP нельзя.
STUN на другом порту даёт предварительную оценку типа NAT для того же IP/интерфейса.
Работа TCP не заменяет проверку UDP.
Для TCP определяется только наличие NAT. Непубличный локальный IP сразу означает
NAT с неизвестным типом. Если локальный IP публичный, достаточно одного публичного
TCP-пира: по готовому защищённому соединению он сообщает увиденный IP.
IP совпали — трансляция IP не обнаружена; отличаются — NAT есть, тип UNKNOWN.
Порты при сравнении не учитываются. Публичный внешний IP сохраняется с портом 0:
входящий порт неизвестен, поэтому NCD не использует эту запись для подключения.
Активных проб типа NAT и TCP-входа нет; OPEN устанавливается по принятому
публичному подключению и сохраняется при последующих совпавших наблюдениях IP.
Через SOCKS внешний IP прокси
не записывается как собственный адрес.
-96
View File
@@ -1,96 +0,0 @@
# Подписанная запись узла
`TOPO_NODE.timestamp` — версия всей подписанной записи, а не отдельного адреса.
Узел-владелец назначает Unix microseconds, строго больше предыдущей сохранённой
версии. Имя, тип, активность, адреса, метаданные сокетов, REALITY и подпись
заменяются вместе. Timestamp входит в подпись. Групповые пути и подсети
остаются в `TOPO_GROUP_NODE` и не являются частью общей записи узла.
Получатель проверяет подпись до изменения реестра или маршрутов. Более свежая
запись заменяет старую целиком, включая пустые списки адресов. Старые записи
игнорируются; разные подписанные записи с одинаковым timestamp — конфликт.
Повторное BGP-объявление может обновить путь, но не содержимое общей записи.
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется
только timestamp.
Живая телефонная занятость передаётся без изменения структуры NODEINFO: в существующем
`flags` бит `0x04` означает известное состояние, `0x08` — занятость звонком.
Это групповые флаги `TOPO_GROUP_NODE.phone_status`, как флаг рации `0x02`;
они не входят в подпись общей записи и используются только для цвета трубки в GUI.
Без разрешения Android, при выключенной публикации и после ухода узла статус неизвестен.
Решение о начале вызова принимает адресат по локальному состоянию ОС, а не по BGP-подсказке.
Изменения публикуются обычной новой версией NODEINFO во всех CHAT-группах; отдельного опроса и таймера нет.
Профиль endpoint в SOCKMETA содержит независимые `mapping` и `inbound`, источник `source`.
ID и время измерения не передаются: последний успешный профиль действует до
явного изменения endpoint или подтверждённого нового результата. Повторная
проверка с тем же результатом не меняет NODEINFO. Все поля SOCKMETA — отдельные байты;
границы списков metadata/адресов/REALITY также входят в каноническую подпись.
Алгоритм и условия неопределённости описаны в `doc/nat_detection.txt`.
`source=ETCP` обозначает результат детектора рабочего endpoint. Одно UDP-наблюдение может дать
адрес `TOPO_ADDR_NAT` при `mapping=UNKNOWN`, `inbound=UNKNOWN`: это NAT-кандидат
для пробного подключения, а не адрес интерфейса или подтверждённый открытый вход.
Общий внешний адрес подтверждает EIM; strict не публикует универсальный NAT-адрес.
Source входит в подпись; SOCKMETA занимает 5 байт. `source=STUN` означает предварительный
EIM? для того же локального IP/netif с неизвестным входом; измерение ETCP приоритетно.
Владелец снимает STUN-оценку после TTL. Внешний адрес тестового порта не публикуется:
NAT-кандидат по-прежнему требует наблюдения рабочего сокета.
Для TCP достаточно одного публичного пира: внешний IP с неизвестным входящим
портом публикуется как `TOPO_ADDR_NAT` с `port=0`, `mapping=UNKNOWN`, `inbound=UNKNOWN`.
NCD пропускает нулевые порты. Непубличный локальный TCP IP сразу означает NAT;
совпавший с наблюдением публичный IP даёт UNCHANGED без подтверждения открытого входа.
Таблица `node_snapshots` хранит полную сериализованную запись без группового
окружения. `node_addresses` остаётся проекцией для поиска и отображения, а не
источником для восстановления подписанной записи. При наличии сохранённого
snapshot загрузка использует его. Локальная публикация сохраняется до рассылки;
ошибка сохранения не разрешает публикацию новой версии.
В проекции адресов сохраняются `nat_mapping`, `nat_inbound`, `nat_source`, `address_kind`.
Уникальность определяется endpoint и видом адреса,
а не отображаемым классом NAT. Старый производный формат пересоздаётся;
bootstrap-загрузка без подписанного snapshot не подтверждает NAT-профиль.
При удалении старых `nat_evidence`/`nat_checked` адреса и REALITY сохраняются для bootstrap,
старые профили сбрасываются, сериализованные snapshots инвалидируются.
Максимальные timestamp snapshots сохраняются, чтобы новая локальная публикация
не откатывала версию даже при коррекции часов. Чат и ключи миграция не изменяет.
Timestamp 0 обозначает bootstrap-сведения без подписи (конфиг, приглашение,
начальные адреса). Они могут помочь установить первое соединение, но не
заменяют принятую подписанную запись. По BGP принимаются подписанные записи
с ненулевым timestamp. Формат NODEINFO изменён без обратной совместимости.
После принятия BGP-записи `node_conn_direct_update_node` применяет адреса к
существующему NCD-соединению. Отсутствующее соединение этот вызов не создаёт.
Линки дополняются как при UP, так и до установления соединения; повторная
доставка не создаёт дубликаты UDP/TCP. Работающие линки не закрываются только
из-за смены списка адресов. NCD удерживает ссылку на общую запись, пока
обслуживает соединение, и использует её при восстановлении.
`node_conn_direct_open_node` сверяет переданную запись с реестром/БД. Более
свежая подписанная запись принимается целиком, старая уступает уже известной.
Bootstrap-адреса не перекрывают подписанную запись даже при DOWN.
Конфигурационные подключения UTUN используют один адаптер
`init_client_connections` для старта и reload. Все адреса, TCP и REALITY
передаются NCD; адаптер не создаёт ETCP-линки. Новый набор handles открывается
до освобождения старого; при ошибке старые handles сохраняются. Конфигурация
удерживает один handle на клиента, независимо от количества его адресов.
Включение конфигурационного пира в UTUN-группу выполняет callback NCD UP.
Транспортный UP и наличие callback старого `etcp_connect` больше не включают
соединение в UTUN-группу автоматически. Входящий JOIN остаётся явным запросом
участия. При пересоздании UTUN-группы учитываются её конфигурационные handles.
Удаление клиента отправляет `LEAVE_GROUP` и освобождает удержание UTUN-группы
с обеих сторон. Другие владельцы NCD сохраняют транспорт. Поздние NODEINFO и
WITHDRAW от отсоединённого UTUN-пира игнорируются, чтобы не вернуть его маршруты.
LEAVE ждёт backpressure через queue waiter и временно удерживает собственный
NCD handle. Закрытие транспорта отменяет ожидание; новое участие в группе
делает ожидающий LEAVE устаревшим, и он не отправляется.
Причина удаления пира передаётся явно: только потеря транспорта запускает
recovery каскадных маршрутов. Локальный/удалённый LEAVE и удаление битой
member-записи не должны восстанавливать намеренно закрытое участие.
REALITY использует полное SNI до 255 символов; размер поля в записи узла
согласован с транспортом. Если локального TCP listen-сокета нет, NCD может
создать исходящий TCP-линк с выбором локального адреса операционной системой.
-12
View File
@@ -1,12 +0,0 @@
Работа лички
1. отдели логику лички от логики групповых чатов. не смешивай.
Работа лички. сообщения и медиа доставляется напрямую если узел доступен через bgp.
если недоступен - то pending сообщения сохраняются на суперузле и pending media сохраняется на каком-либо медиаузле, а суперузлу сообщается где хранится медиа.
при появлении пира суперузел организует доставку медиа до получателя. после успешной доставки медиа/сообщений они удаляются с промежуточных узлов.
время хранения медиафайлов на промежуточных узлах ограничено - 1 неделя по умолчанию (задачется в конфиге этих узлов).
для доставки медиа поднимаем линк (conn_mgr).
и посмотри реализацию медиа для групповых чатов.
-66
View File
@@ -1,66 +0,0 @@
# Приглашение в группу через PM
В desktop chatgui и Android кнопка «+» доступна в строке другого участника
и в шапке личной беседы. Она открывает один диалог с фиксированным получателем
и выбором группы, в которой отправитель состоит. Группа не выбирается автоматически.
После нажатия «Пригласить» клиент создаёт свежую ссылку через `invite_build`,
ожидает подтверждения регистрации ключа и отправляет PM с `ct="invite"`.
Зашифрованное тело: `[group_name:UTF8][NUL][полная ссылка:UTF8]`, без конечного NUL.
Снимок для GUI содержит полную ссылку в `data` и название в `tags.group_name`.
GUI добавляет заголовок «Приглашение в группу «название»» из перевода интерфейса;
язык отправителя не фиксирует язык сообщения у получателя.
Успех означает сохранение сообщения и исходящей задачи в одной транзакции.
Доставку, повтор и шифрование обеспечивает существующий механизм DM.
После успешной отправки открывается личная беседа с получателем.
Срок действия ключа — **10 минут с момента создания**, а не получения PM.
Он указан в диалогах отправки и подключения. Поздняя offline-доставка
может принести уже просроченную ссылку; в этом случае нужно новое приглашение.
Ссылка не содержит время истечения, поэтому клиент показывает длительность,
без обратного отсчёта и вычисления оставшегося времени.
Нажатие на `utun://` в сообщении запускает существующий JOIN и показывает его результат.
Если клиент уже состоит в группе, открывается эта группа. Ссылка приглашения отображается
одной строкой с многоточием; нажатие и копирование используют полный адрес.
В Android долгое нажатие копирует ссылку, в desktop выделение сокращённой строки
копирует ссылку целиком без запуска JOIN. Обычные текстовые PM с вручную
вставленными ссылками сохраняют текстовое отображение.
## Контракт операции
`dm_invite_trampoline` выполняется в uasync-потоке. Запрос содержит `request_id`,
ID получателя, выбранную группу и исходную общую группу для создания PM.
Из шапки существующей PM исходная группа может быть пустой.
На instance допускается одно ожидание регистрации приглашения в PM.
Повтор во время ожидания возвращает BUSY. Закрытие диалога отменяет только
соответствующий `request_id`; уже отправленное сообщение не отзывается.
Отмена и остановка DM завершают принятый запрос ровно один раз.
`CHAT_EVT_DM_INVITE_RESULT` содержит 28 байт:
`[request_id:u64][result:i32][peer:u64][conv_id:u64]` в порядке байтов ядра.
Desktop и Android сопоставляют результат с запросом и получателем.
Ошибки регистрации или сохранения сообщения оставляют диалог открытым для повторной попытки.
JOIN во вторую группу может использовать уже открытый транспорт общей группы.
Поэтому обработчики JOIN_REQUEST и JOIN_READY явно планируют member_sync,
а JOIN_READY обновляет кеш каналов до запуска синхронизации.
## Проверка и диагностика
- `tests/test_dm_invite`: настоящая UDP-доставка ссылки в другую группу, JOIN,
репликация участника и отправка группового сообщения после вступления;
отмена, BUSY, неверная группа, rollback при ошибке commit и остановка DM;
передача названия с Unicode, кавычками и переводом строки.
- `tests/test_chat_join`, `tests/test_chat_join_e2e`, `tests/test_dm_e2e`:
существующие сценарии регистрации ключей, JOIN и PM-доставки.
- Qt `test_invite_links`: одна строка приглашения на разных ширинах, полный адрес
при нажатии и копировании, переносы двух обычных ссылок, их полное содержимое при нажатии,
выделение текста и выбор получателя кнопкой в строке участника.
- Qt `test_emoji_playback`: перетаскивание ссылки не активирует подключение.
Для сетевого теста подробности включаются через `UTUN_TEST_DEBUG=1`.
В приложении полезны категории `dm=debug`, `chat_sync=debug`, `member_sync=debug`.
Операция пишет request, peer, group, результат и TTL; сообщения диагностики
новой операции не выводят саму invite-ссылку.
-61
View File
@@ -1,61 +0,0 @@
# Proxy: потоки TCP и датаграммы
Клиентские входы — TUN (lwIP), SOCKS5 CONNECT и HTTP proxy/CONNECT. Exit открывает
обычный TCP-сокет к назначению. TCP-поток на exit определяется парой `(peer_node_id,
stream_id)`. Клиент принимает ответы и уведомления restart только от настроенного
exit в UTUN-группе. UDP/ICMP-контексты принадлежат конкретному `UTUN_INSTANCE`.
## TCP
Общие определения находятся в `src/proxy/proxy_protocol.h`. Передаваемый заголовок:
`svc_id:1, command:1, stream_id:4`; поля маршрута добавляет ETCP-router.
- CONNECT (1): IPv4:4 и порт:2 в сетевом порядке.
- CONNECTED (2): exit подтвердил успешный TCP connect. Только после этого клиент
сообщает SOCKS success / HTTP 200 и начинает передавать накопленные данные.
- DATA (3): от 1 до 4096 байт.
- CLOSE (4), ERROR (5): прекращение потока.
- FIN (6): конец одного направления; все предшествующие DATA должны быть переданы.
- WINDOW (7): uint32_t, число дополнительно разрешённых байт.
У каждого направления начальное окно 65536 байт. DATA уменьшает окно отправителя
и получателя. Получатель возвращает кредит после записи в локальный TCP-сокет
либо принятия данных ограниченным send-buffer lwIP. Очередь ETCP сама по себе
не является подтверждением потребления данных конечным TCP.
FIN ждёт освобождения исходящей очереди, включая данные, остановленные окном
или backpressure роутера. Закрытие одного направления не прекращает другое.
Окончательное освобождение происходит после обоих FIN и передачи оставшихся данных.
Непереданные управляющие сообщения повторяются таймером. Ошибки и переходы
состояния диагностируются категорией `proxy`; состояние сокетов — `socket`.
Парсеры сохраняют остаток после greeting/request/HTTP headers. HTTP CONNECT ждёт
полного блока заголовков. Во время DNS/connect чтение ограничено очередью tcp_io;
накопленные HTTP headers/body передаются частями размером DATA.
## UDP и ICMP
REQUEST и REPLY различаются явно. Ответ допускается только от настроенного exit.
UDP-ответ восстанавливается с нулевой UDP checksum (допустимо для IPv4), IPv4 checksum
пересчитывается. ICMP checksum учитывает нечётный последний байт без чтения за буфером.
Exit заменяет ICMP id/sequence уникальной среди ожидающих запросов парой и сопоставляет
ответ также с IP назначения. Исходные id/sequence восстанавливаются для клиента.
Это исключает смешивание ping-запросов разных клиентов с одинаковыми исходными ID.
## Проверки и ограничения
`test_proxy_packets` проверяет IPv4/UDP/ICMP и payload длиной 0..1501.
`test_tcp_io_flush` проверяет уведомление после последнего синхронного send.
`test_proxy_regressions` использует реальные локальные сокеты и управляемую доставку
ETCP: совместные/раздельные handshake, ранние данные, отложенный CONNECTED, HTTP 502,
большой POST, одинаковые stream ID разных пиров, FIN при закрытом окне, RST и IPv4 options.
На Linux полный `./check.sh`: 109 passed, 0 failed, 1 skipped; дополнительно прошли
настоящий TUN, full-duplex burst и нагрузка 8 соединений / 128 МиБ.
Windows/FreeBSD в этом прогоне не проверялись.
SOCKS IPv6 явно отклоняется кодом address type not supported. IPv4-фрагменты
на входе proxy TUN отклоняются с диагностикой; сборка фрагментов не реализована.
Формат TCP дополнен CONNECTED/WINDOW, UDP-команды разделены: клиент и exit необходимо
обновлять вместе; совместимость со старым proxy-протоколом не предусмотрена.
-293
View File
@@ -1,293 +0,0 @@
# Жизненный цикл ядра и сервисов
CLI выполняет daemon fork до создания `UASYNC`, SQLite, сокетов и фоновых потоков.
Сервисы и их workers создаются уже в дочернем процессе. Родитель ждёт подтверждения
успешной инициализации и записи PID-файла; ошибка старта возвращается вызывающему.
`test_daemon_worker` проверяет настоящий запуск без `-f`, наличие дискового потока,
штатный SIGTERM и ненулевой статус launcher при ошибке PID-файла.
Все операции выполняются в потоке единственного `UASYNC` экземпляра, вне callbacks
останавливаемых модулей. GUI передаёт команды в этот поток.
```c
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config);
if (!inst || utun_core_start(inst) < 0) /* обработать ошибку */;
if (chat_service_start(inst) < 0) /* обработать ошибку */;
if (utun_service_start(inst) < 0) /* обработать ошибку */;
chat_service_stop(inst); /* UTUN продолжает работать */
chat_service_start(inst); /* загружает сохранённые каналы */
utun_service_stop(inst); /* чат продолжает работать */
utun_instance_destroy(inst);
```
`utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён
в конфигурации. Desktop GUI запускает ядро, UTUN и чат явно; TUN создаётся при
`[global] tun_enabled=yes`. Без TUN и NAT запуск чата не требует прав администратора.
Desktop подтверждает start только после готовности всех сервисов и uasync poll;
ошибка TUN откатывает запуск и возвращается в GUI. Android запускает ядро и чат без UTUN.
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл
приложения; освобождение ядра выполняет `utun_instance_destroy()`.
Для TUN в desktop `vibechat.cfg` задаются те же параметры, что и у CLI:
```ini
[global]
tun_enabled=yes
tun_ifname=vibechat
tun_ip=10.42.0.1
[routing]
my_subnet=10.42.0.1/32
route_subnet=10.42.0.0/24
```
Адреса и маршруты выбираются под свою сеть; изменения применяются после перезапуска.
На Windows нужна `wintun.dll` рядом с `vibechat.exe` и запуск с правами администратора.
CMake копирует DLL и лицензии из `lib/`; Windows-упаковка явно включает DLL, проверяет
её архитектуру x64 и зависимости, поскольку `LoadLibrary` не отражается в импортах EXE.
На Linux необходимы доступ к `/dev/net/tun` и права на настройку интерфейса и маршрутов.
Успех запуска требует успешной инициализации всех обязательных ресурсов: заданных
listen-сокетов, мониторинга, TUN и всех `route_subnet` при включённом TUN. Частичный
набор сокетов или маршрутов — ошибка запуска. `utun_started` устанавливается только
после завершения всех шагов. Системные маршруты хранятся отдельным списком успешно
созданных записей; stop и откат удаляют только этот список, в обратном порядке.
Маршрут, на добавлении которого ядро вернуло ошибку, не становится собственностью
UTUN и при откате не удаляется. Ошибка одного удаления не прекращает очистку остальных.
`auto_sockets=android` выбирает внешнее управление сетью: frontend регистрирует
ConnectivityManager до запроса запуска и передаёт конфигурацию сокетов через JNI.
Оба нативных монитора ОС в этом режиме не создаются. Рабочие listen-сокеты по-прежнему
должны открыться полностью. Android подтверждает запуск по `SERVICE_STARTED`, а не
по успешному созданию потока; после события передаётся свежий снимок адресов.
| Владелец | Ресурсы |
|---|---|
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг |
| UTUN | Группа UTUN, запросы соединений из конфигурации, DATA binding, TUN, системные маршруты, NAT transport |
| Чат | CHAT-группы, запросы подключения и join, экземпляры синхронизации каналов, chat API, медиа, личные сообщения, звонки и headless API |
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery |
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки |
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
`:memory:` данные сохраняются только до уничтожения ядра.
Работающий общий транспорт сохраняется, пока он нужен хотя бы одному из пиров. Закрытие запроса одной группы
не разрывает соединение, нужное другой группе. При остановке группы отменяются
её планировщики, запросы, таймеры и ожидания очередей; отправляется LEAVE,
закрываются логические каналы маршрутизатора и удаляется её ожидающий транзит
на общих соединениях. Транзит других групп сохраняется. Отложенный LEAVE может временно
удерживать транспорт самостоятельно, не обращаясь к уже удалённой группе.
ETCP удаляет транспорт после 30 секунд непрерывного DOWN только при отсутствии локальных NCD handles.
Первоначальное ожидание UP тоже учитывает владение. Повторный DOWN не продлевает
таймаут; восстановление UP отменяет его. Если срок истёк при наличии handles, транспорт
сохраняется, а исходный deadline остаётся. Освобождение последнего handle во время DOWN
оставляет транспорт до этого deadline либо запускает удаление в следующем callback,
если срок уже истёк. Внешние ETCP refs задерживают только освобождение памяти.
Явный force-close и отмена неудачной попытки последнего владельца закрывают сразу.
Входящий UP без локальных handles сохраняется: он может быть нужен пиру. CLOSE/KEEP_ALIVE
привязан при запуске ядра, поэтому мягкое закрытие работает и на чисто входящем узле.
На CLOSE при наличии handles отвечаем KEEP_ALIVE; без них закрываем транспорт.
KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE обычно занимает 5 секунд,
для спящего пира увеличивается до его RX deadline. Сам факт DOWN во время FIN_WAIT
завершает ранее запрошенное мягкое закрытие.
Контроль RX независим от отправки keepalive. Объявленный сон не отключает проверку
жизни: срок от последнего RX равен максимуму согласованного keepalive timeout и
объявленного остатка ACTIVE+SLEEP с запасом normal timeout. Фазовые таймеры и наши
отправки срок жизни пира не продлевают. Пробуждение сразу сокращает deadline до normal timeout.
После истечения последнего линка транспорт
становится DOWN, затем может удаляться через 30 секунд, когда локальных NCD handles нет. `test_etcp_server_lifecycle`
проверяет сроки, несколько путей, восстановление, владение, сон и CLOSE/KEEP_ALIVE
с управляемыми часами и контролем освобождения памяти/таймеров.
Остановка чата сначала прекращает внешние команды и фоновые задания, затем
закрывает медиа и сервисы, синхронизацию и группы, после чего освобождает chat core.
`media_async_destroy()` дожидается завершения native workers и вызывает каждый
ожидающий callback с `MEDIA_ASYNC_CANCELLED`, пока его контекст ещё существует.
Worker резервирует completion до запуска и передаёт его через `uasync_post_reserved`;
периодического polling нет. Destroy выполняется вне callbacks uasync, после join
отменяет pending post и завершает done ровно один раз. Uasync живёт до конца этой очистки.
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное
транскодирование или распознавание речи может задержать stop; принудительного
прерывания этих библиотек нет.
Live media хранится в RS-кольце с общим индексом PTS; каждый локальный handle владеет
отдельным cursor. Пауза player не останавливает сетевой ingest и не закрепляет старые
части против вытеснения. После FIN publisher может снять своё владение, сохранив
завершённый источник для оставшихся локальных readers. Последний close освобождает
источник, индекс и кольцо. Новая публикация отсоединяет readers прежней эпохи.
Android preview собственного эфира не создаёт reader: Camera2 пишет прямо в TextureView
и encoder Surface. UI attach/detach меняет лишь camera session; audio/encoder/origin сохраняются.
UI, ожидающие команды camera handler и каждая camera session удерживают preview target отдельно.
TextureView возвращает false при destroy и снимает ссылку UI; Surface/SurfaceTexture освобождаются
после закрытия последней session. CameraDevice.onClosed снимает также оставшиеся ссылки своих
sessions, если Android не прислал отдельные session.onClosed. Повторные callbacks безопасны.
Capture stop запрещает attach и снимает своё владение target; camera HandlerThread завершается
после camera close и освобождения outputs всех созданных sessions.
Отказ preview output возвращает encoder-only session и не завершает сам эфир.
При stop/destroy отменяются delivery и media-index callbacks до освобождения store;
destroy отсоединённого cursor не обращается к store. Frontend прекращает audio callback
и worker до освобождения своего DSP; borrowed handle/RS pointers между потоками не передаются.
Чтение video cursor запрашивает следующий кадр по готовности decoder, независимо от
PTS показа; audio cursor сохраняет временной lookahead. Bridges готовят один ответ
на незавершённый запрос и не читают следующий кадр после получения ответа автоматически.
Android worker держит один рабочий input и один готовый output MediaCodec; свободный
input buffer получает лишь для уже готовых данных и сразу возвращает codec.
Первый decoded frame разрешает движение clock/audio. Будущий output ждёт PTS и
приостанавливает новые video requests, сохраняя независимую обработку audio/control.
Если первый доступный звук позже KEY, cursor отсчитывает стартовый audio lookahead от
первого аудиопакета, сохраняя это основание неизменным. Opus/SoundTouch получает PCM до
запуска audio clock; неподвижные часы не разрешают читать дальше этого ограниченного окна.
PTS поданных кадров служат диагностике/проверке запаздывания, а не лимиту заполнения codec.
Смена revision заменяет только video decoder и его измеренный защитный guard;
GAP/EXPIRED начинают новую точку KEY. Нехватка сети сохраняет decoder references.
Desktop FFmpeg держит один рабочий AVPacket и два готовых изображения; EAGAIN оставляет
packet до следующего прохода. FIN подаёт EOS и дренирует выход до decoder EOF;
конец GUI требует также показа оставшихся изображений и завершения audio tail.
Отсутствие прогресса decoder при наличии работы имеет отдельную диагностику и срок 10s.
ACK-история принадлежит ядру и обновляется только при первом найденном подтверждении
пакета. REINIT/close соединения не стирает уже подтверждённые за последнюю секунду байты.
Общий `stream_publisher` Android/desktop владеет отдельным 100ms таймером TX-снимков;
close/detach отменяют его в uasync до уничтожения instance. Bridges копируют снимок
под mutex, frontend не обращается к живым очередям транспорта. Android экран подписан
на отдельный StateFlow статистики; desktop обновляет статистику отдельно от сырого превью.
Publisher не регистрирует stream event callback и не подменяет callback viewer.
Bridge удерживает внешний mutex на время producer/open/stop и destroy общего publisher;
actor не вызывает frontend callbacks под своим mutex. Reserved post, media timer,
stats timer и stream handle освобождаются до контекста publisher.
Desktop `MainWindow` владеет `StreamPublishEngine`, а окно управления лишь отображает
его состояние. Закрытие окна сохраняет эфир, новый запуск запрещён до конца FIN drain.
Qt capture callback сохраняет последний `QVideoFrame` для preview и ограниченно ставит кадр
в очередь отдельного долгоживущего image worker. Он конвертирует `QVideoFrame` в `QImage`;
video worker получает готовый image и не создаёт Qt Multimedia TLS, которое могло задержать его exit на Windows.
Video/audio workers выполняют encode/DSP вне GUI и hardware callbacks. Отдельные поколения источника и encoder позволяют менять
видеокодек/bitrate/rotation без повторного выбора экрана в портале. Ошибки Qt capture
обрабатываются через queued callback, чтобы не уничтожать backend из собственного callback.
Stop запрещает producers, закрывает Qt capture и подписку на аудиозахват. Отдельный join worker
дожидается полного выхода video/audio threads, включая native TLS Qt; GUI продолжает обрабатывать события.
После join отправляется FIN с drain 2s; закрытие последнего микрофона может продолжаться параллельно.
Microphone lease и owner сохраняются до завершения обоих путей. Новый start ждёт также FIN drain.
При завершении приложения engine уничтожается до SoundManager и ядра; detach в uasync
закрывает оставшийся publisher. Destructor ждёт workers в локальном event loop без пользовательского ввода,
чтобы thread cleanup мог обратиться к GUI; start и внешние сигналы engine в это время запрещены.
Workers никогда не держат borrowed instance/handle.
Microphone lease публикации исключает call/recorder; рация согласует вход через `AudioCapture`.
Одинаковый выбранный вход имеет одну аппаратную сессию и профиль трансляции. Разные входы независимы.
Подключение/отключение рации не останавливает устройство трансляции: атомарный слот и счётчик readers
дожидаются только callback удаляемого потребителя. Последняя подписка закрывает устройство.
Если первой работала рация, публикация заменяет профиль захвата своим; после остановки публикации
оставшаяся рация продолжает тот же захват. Recovery останавливает callbacks, workers TX и один раз
открывает общий вход; RX рации сохраняется. Каждый потребитель обрабатывает собственную копию PCM.
На Android `MicrophoneCapture` владеет AudioRecord и reader-потоком, подписчики — ограниченными очередями.
Stop публикации удаляет её подписку до join Opus worker и FIN; AudioRecord остаётся живым для рации.
PTT и PCM рации проходят через одну очередь. Последний подписчик делает stop/read join/release;
приоритет глобального AudioManager сохраняется до освобождения трансляционного захвата.
Ограничения фонового захвата рации, её разрешения и foreground-сервис продолжают действовать.
Fatal ошибка audio worker передаётся в GUI с session ID и завершает всю публикацию;
откат видеоконфигурации применяется только к video/capture. Поздний отказ старого publisher
не завершает новую сессию. Причина ошибки сохраняется в окне после окончания drain.
Desktop-звонок и рация проверяют поступление корректного PCM, включая первый callback.
Секунда без данных отключает TX и фиксирует понятную ошибку в GUI; цифровая тишина является данными.
RX продолжает работать, рация снимает PTT, автоматического переоткрытия неисправного входа нет.
Выбор другого микрофона или новая сессия снимают ошибку. Неисправное устройство закрывается
в отдельном потоке; stop не присоединяет уже запущенный close worker. Новый захват ждёт его завершения.
В звонке microphone lease удерживается до конца закрытия даже после stop. Общий AudioCapture
рации сначала отсоединяет callback потребителя; последний владелец освобождает backend в worker.
Сброс контекста и уничтожение владельца дожидаются worker до освобождения miniaudio-контекста.
Штатное закрытие исправного устройства сохраняет синхронный порядок.
`AudioDevice` подстраивает буфер по подтверждённым backend XRUN: WASAPI DATA_DISCONTINUITY
на входе, ALSA EPIPE, PulseAudio capture hole/overflow и playback underflow. Notification
только увеличивает атомарный счётчик, решение и лог выполняются в GUI recovery timer.
Исходные параметры, включая Linux capture period=10мс, сохраняются до повторных сбоев.
Первая секунда после start исключена; нужны ошибки в двух опросах за 5с. Период увеличивается
на 5мс, не чаще раза в 5с, до общей запрошенной ёмкости 100мс с учётом числа периодов.
Фактический размер согласует backend; исходный или уже достигнутый больший размер не уменьшается.
При достижении предела повторные сбои логируются без новых перезапусков. Подобранный запрос
сохраняется при recovery, сбрасывается при смене устройства/сессии; стабильный звук не вызывает reopen.
Подстройка микрофона закрывает устройство асинхронно, останавливая TX worker и сохраняя RX.
Общая capture-сессия отсоединяет callbacks до close и заново подготавливает потребителей после init.
Новая подписка во время close откладывается, stop удаляет её без ожидания backend.
Первое переоткрытие после подстройки не использует backoff; при ошибке открытия действует обычный retry.
В desktop-видеозвонке Windows/Linux камера живёт в отдельном процессе того же
`vibechat --call-camera-worker`. Служебный вход выполняется до конфига, БД и
single-instance lock. Перечисление устройств, Qt/V4L2, преобразование и освобождение
аппаратных кадров происходят только там. GUI получает собственный `QImage`; TX не
обращается к драйверу. Смена codec не перезапускает захват и не ждёт следующего GOP.
Пиксели одного кадра передаются через mapping (Windows — pagefile-backed объект ОС,
Linux — временный файл), метаданные/команды — через локальный сокет.
Worker записывает mapping до FRAME; следующий write разрешён
только после ACK, который GUI посылает после копирования в свою память. Общего mutex
нет. В worker ожидает только последний raw-кадр; поколения отсекают старые результаты.
Preview обновляется по готовности, TX получает condition-variable wake. PTS сохраняет
время захвата; кадры старше 250мс отбрасываются до encode и перед send. Отброшенный
encoded frame требует следующего KEY, поскольку encoder уже продвинул reference chain.
Отключение камеры и удаление окна не ждут worker. Независимый Session сохраняет
QProcess и mapping до finished, затем удаляет себя после отсоединения владельца.
Зависший stop получает kill через 1.5с, отсутствие прогресса — через 5с; таймеры
живут в родительском процессе. При выходе приложения kill отправляется без wait;
GUI не уничтожает живой QProcess. При отказе новый процесс запускается лишь по
явной команде пользователя и после finished прежнего. Лог категории call показывает
последнюю стадию enumerate/start/convert/release/stop и возраст кадра.
`test_call_camera_capture` проверяет настоящую IPC-цепочку с подставным устройством:
блокировки start/map/stop, поздние кадры после смены поколения, задержку 720p,
повтор после отказа, отсутствие рестарта камеры при смене codec, удаление окна,
выход приложения и независимое освобождение mapping в каждом процессе.
Статус транспорта UP подтверждает восстановление физического пути. READY сессии
группы означает согласованный JOIN/ACCEPT и завершённый обмен её таблицей; именно
его ждёт recovery. REINIT меняет эпохи обмена, сохраняя рабочие маршруты. Эпохи
защищают от сообщений старой сессии и не задают срок жизни маршрута.
Проверки: `test_services` проверяет независимый stop/start, общий NCD в CONNECTING,
сохранение идентичности/сокетов/БД, повторную загрузку каналов, отмену workers,
заблокированного медиа-потока, транзита и ожиданий заменённой очереди. `test_chat_join_e2e`
проверяет join и распространение membership между узлами без UTUN-сервиса.
В Android звонковой сессией владеет `CallController` уровня приложения; события
ядра и команды Telecom обрабатываются один раз в main thread, без повторного
проигрывания событий при создании ViewModel. UI получает `StateFlow` и передаёт
пользовательские команды. `CallService` владеет `CallAudioEngine` и частичным
wake lock: перед исходящим вызовом или принятием входящего запускается foreground
режим `phoneCall|microphone`, затем сигналинг и аудио. Завершение сначала
останавливает I/O и дожидается аудиопотоков, затем освобождает native-аудио,
wake lock и сервис. Перед `NativeLib.stop/restart/destroy` выполняется тот же
порядок; новый звонок разрешается после `SERVICE_STARTED` нового ядра.
Активность ядра задаётся явно: видимая Activity или незавершённый звонок удерживают
ACTIVE и выключают Android standby. Состояние не истекает по таймеру. После
завершения звонка в фоне возвращается standby; экран может гаснуть во время
разговора независимо от частичного wake lock сервиса.
Если работают рация, публикация или просмотр трансляции, исходящий Android-звонок
требует подтверждения их остановки; входящий показывает предупреждение до принятия.
После подтверждения/принятия `CallController` выключает участие в рации, завершает
публикацию и просмотр, асинхронно ждёт освобождения их аудио и только затем запускает
сигналинг и звонковое аудио. Foreground-сервис резервируется сразу из видимой Activity.
Отмена во время освобождения не запускает устаревший звонок.
До окончания звонка повторное включение рации/трансляции заблокировано.
После закрытия звонкового аудио и Telecom Connection ранее включённая рация
возвращается в прежний канал; удержания PTT сброшены. Удалённый канал и остановленное
ядро не восстанавливаются. Публикация и просмотр трансляции автоматически не возобновляются.
`utun_set_client_activity()` в сборке с `UTUN_HAVE_STANDBY` применяет фактический
режим duty-cycle до рассылки событий активности; JNI не переключает его отдельно.
Повторная команда не перезапускает фазу и анонсы, но первый фоновый запуск включает
standby даже при начальном `client_activity=STANDBY`. Keepalive подписан на режим
и фазы самого standby; новые UDP/TCP-линки сразу анонсируют текущую фазу после
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра.
-1250
View File
File diff suppressed because it is too large. Load diff
-104
View File
@@ -1,104 +0,0 @@
# Независимая STUN-диагностика NAT
Модуль `src/transport_layer/stun_nat.c/h` измеряет NAT на уникальных путях существующих IPv4 UDP-сокетов.
Ключ группы — семейство, фактический IP и netif; имена и рабочие порты не участвуют.
Для каждой группы создаётся один собственный неблокирующий UDP-сокет с тем же локальным IP,
привязкой к устройству и Linux SO_MARK / FreeBSD FIB. Тестовый порт выбирается случайно из 49152–65535.
Модуль сам регистрирует чтение в uasync, принимает STUN и закрывает свой сокет после измерения.
STUN не меняет исходный ETCP fd, его порт и обработчики. Через `stun_nat` API модуль
`nat_detection` получает предварительную оценку для рабочих сокетов того же IP/netif
с совпадающими mark/FIB/device. Если два разных IP видят одинаковый стабильный внешний
IP:порт и есть трансляция адреса, рабочий профиль получает EIM? с source=STUN и inbound=UNKNOWN.
Это действует и для обычных серверов без запроса с незнакомого IP. Filtering тестового порта
не переносится; измерение mapping рабочего сокета всегда имеет приоритет.
Оценка снимается после TTL, отключения STUN, смены пути или противоречащего внешнего IP.
Тестовый mapped endpoint не копируется в `nat_addr` и NODEINFO: адрес для подключения
должен быть получен наблюдением самого рабочего сокета. Детектор описан в `doc/nat_detection.txt`.
## Конфигурация
```ini
[stun]
enabled=yes
server=stun-a.example.org:3478
server=stun-b.example.org:3478
# Дополнительные строки — резервные серверы.
[debug]
nat=debug
```
Без `enabled=yes` модуль выключен. Без порта используется 3478. Список сохраняет порядок конфига.
При включении нужны минимум две строки `server`; для измерения выбираются два ответивших сервера с различными IP.
Совпадающие IP после DNS пропускаются. DNS асинхронный, с использованием существующего IPv4 A-резолвера.
IPv6, SOCKS UDP и `only_local` пропускаются с явным статусом; TCP не проверяется.
## Результат
`stun_nat_get_result(instance, "имя исходного сокета", &result)` копирует локальный снимок:
Имя сначала разрешается в текущий рабочий сокет, затем в группу. API ядра
`stun_nat_get_socket_result(instance, socket, &result)` дополнительно проверяет
текущие IP/netif и параметры маршрутизации. Несколько рабочих портов получают общий снимок.
- `local` — тестовый IP:port; `servers[2]` и `mapped[2]` — реальные серверы и наблюдавшиеся внешние адреса;
- `translated`: -1 неизвестно, 0 трансляция не наблюдалась, 1 внешний адрес отличается;
- `mapping`: UNKNOWN, MATCH, DEPENDENT, EIM, ADM, APDM;
- `filtering`: UNKNOWN, EIF, ADF, APDF;
- причины неполного результата, статус, время измерения и обнаруженная поддержка RFC 5780.
- ключ группы, generation, evidence_id, measured_at_us, количество рабочих сокетов и признак refresh.
Обычный STUN даёт MATCH при одинаковых адресах и DEPENDENT при разных. MATCH не доказывает EIM,
а обычные Binding-ответы не позволяют определить filtering. Первоначальный сервер опрашивается повторно:
смена его mapping во время проверки инвалидирует классификацию как нестабильную.
## RFC 5780
Корректные OTHER-ADDRESS и RESPONSE-ORIGIN включают автоматическую попытку Behavior Discovery.
Нужны две публичные IP и два порта на серверной стороне, доступные во всех четырёх комбинациях.
1. Обычный Binding получает внешний адрес и альтернативный endpoint.
2. CHANGE-REQUEST с флагами IP+port проверяет EIF.
3. Если ответ не пришёл, контрольный Binding проверяет живость сервера и стабильность mapping.
4. CHANGE-REQUEST только port различает ADF и APDF; после таймаута снова выполняется контрольный Binding.
5. Перед публикацией ADF/APDF альтернативные endpoints проверяются обычными Binding-запросами.
Недоступный альтернативный endpoint даёт UNKNOWN, а не ложную строгую фильтрацию.
6. После filtering проверяется mapping: альтернативный IP с первоначальным портом, затем при необходимости
альтернативный IP с альтернативным портом. Сравнение различает EIM, ADM и APDM.
7. Второй сервер и повтор первого проверяют независимую пару и стабильность результата.
Все filtering-тесты выполняются до открывающих фильтр отправок на альтернативные endpoints.
Собственные предыдущие STUN-отправки учитываются: при пересечении адресов следующего сервера
сомнительный тест filtering пропускается. ERROR 420 и игнорирование CHANGE-REQUEST дают UNKNOWN.
Аутентификация STUN не реализована: требующий её сервер фиксируется как ошибка, затем используется резервный.
Запрос содержит случайный 96-битный transaction ID и FINGERPRINT. Проверяются источник ответа,
transaction ID, границы/выравнивание атрибутов, XOR-MAPPED-ADDRESS, RESPONSE-ORIGIN и FINGERPRINT при его наличии.
Повторы одной транзакции сохраняют ID: RTO от 500 мс удваивается, всего семь отправок, последнее ожидание 8 секунд.
Новые транзакции всех проб ограничены десятью в секунду; ожидания не блокируют event loop.
## Жизненный цикл и проверки
Ядро запускает модуль при `utun_core_start`; остановка уничтожает его до сетевых ресурсов и config.
Перезагрузка конфига отменяет старые DNS/таймеры и создаёт новый модуль.
Раз в три секунды выполняется локальная сверка групп: новые/изменённые проверяются,
группа удаляется после исчезновения последнего рабочего сокета. Смена имени/представителя
или удаление дубликата не теряет кэш. Несовпадение mark/FIB/device внутри одного ключа
даёт UNKNOWN с причиной incompatible routing policy.
TTL результата 300 секунд, refresh 180 секунд; неполные/неудачные проверки повторяются через 30 секунд.
Во время штатного refresh доступен прежний результат до TTL; его возраст не обновляется.
Использованные тестовые порты не переиспользуются в течение жизни ядра, включая reload STUN.
История портов освобождается только при окончательном destroy ядра.
`stun_nat_recheck(instance)` явно повторяет измерение на новых портах, в том числе после смены маршрута без смены IP.
Публичные операции выполняются в uasync-потоке экземпляра.
INFO содержит начало измерения и итог с адресами, классификацией и причинами UNKNOWN.
DEBUG содержит этапы, transaction ID, повторы, RTT, DNS и очистку; WARN/ERROR описывают аномалии.
Linux-тест `test_stun_nat` использует реальные локальные UDP-сокеты и ускоренные часы:
обычный STUN, все классы RFC 5780, недоступные альтернативы, потеря запроса, неверные/поздние ответы,
резервные серверы, DNS, нестабильный mapping, отмена и повторная проверка, изменение состава сокетов.
Сценарии проверяют сохранность рабочего fd/порта, предварительный EIM?, приоритет ETCP,
снятие оценки после TTL, совпадение IP/netif и освобождение таймеров.
Ссылки: [RFC 5780](https://www.rfc-editor.org/rfc/rfc5780), [STUN RFC 8489](https://www.rfc-editor.org/rfc/rfc8489).
-1211
View File
File diff suppressed because it is too large. Load diff
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020-present Silero Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+2 -37
View File
@@ -25,10 +25,6 @@ libuasync_a_SOURCES = \
radix.h \
serialize.c \
serialize.h \
video_plane.c \
video_plane.h \
reed_solomon.c \
reed_solomon.h \
json_flat.c \
json_flat.h \
tcp_io.c \
@@ -44,48 +40,17 @@ libuasync_a_SOURCES = \
dns.c \
dns.h \
async_dns.c \
async_dns.h \
speex_aec.c \
speex_aec.h \
speexdsp/mdf.c \
speexdsp/fftwrap.c \
speexdsp/kiss_fft.c \
speexdsp/kiss_fftr.c
if HAVE_SILERO_VAD
libuasync_a_SOURCES += \
silero_vad.c \
silero_vad.h
endif
async_dns.h
libuasync_a_CFLAGS = \
-D_ISOC99_SOURCE \
-DDEBUG_OUTPUT_STDERR \
-DSQLITE_THREADSAFE=1 \
-DDNS_RANDOM=RAND_bytes \
-DFLOATING_POINT \
-DUSE_KISS_FFT \
-DEXPORT= \
-g \
-I$(top_srcdir)/src \
-I$(top_srcdir)/lib \
-I$(top_srcdir)/lib/libopus/include \
-I$(top_srcdir)/lib/speexdsp \
-I$(top_srcdir)/lib/speexdsp/include
if HAVE_SILERO_VAD
libuasync_a_CFLAGS += -DHAVE_SILERO_VAD
endif
# Вшитая модель Silero VAD (включается из silero_vad.c)
EXTRA_DIST = silero_vad_model.inc \
COPYING.silero-vad \
wintun-LICENSE.txt \
wintun-api-LICENSE.txt \
speexdsp/COPYING \
liblmdb/LICENSE \
liblmdb/COPYRIGHT \
soundtouch-master/COPYING.TXT
-I$(top_srcdir)/lib/libopus/include
# Clean build directory
clean-local:
+2 -119
View File
@@ -20,7 +20,6 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#ifdef _WIN32
#include <iphlpapi.h>
@@ -38,9 +37,6 @@ struct adns_query {
void* timer; /* handle таймаута (NULL когда нет) */
int finished;
char* name;
int fast; /* 1 = готовый /etc/hosts-ответ, без libdns */
struct adns_result fast_result;
void* call_soon_id; /* отложенный вызов (fast-путь и первый драйв slow-пути) */
};
static void adns_drive(struct adns_query* q);
@@ -48,8 +44,6 @@ static void adns_finish(struct adns_query* q, int status, struct sockaddr_in* ad
static void adns_read_cb(socket_t sock, void* arg);
static void adns_write_cb(socket_t sock, void* arg);
static void adns_timer_cb(void* arg);
static void adns_fast_ready_cb(void* arg);
static int adns_hosts_lookup(const char* name, struct sockaddr_in* out);
/* ─── system DNS servers discovery ─── */
@@ -130,49 +124,6 @@ int adns_system_servers(struct sockaddr_in* out, int max) {
}
#endif
/* ─── синхронный /etc/hosts-лукап (быстрый путь, без libdns) ─── */
// Ищет name в /etc/hosts. Возвращает 1 если найден IPv4-адрес (out заполнен).
// Блокирующее чтение небольшого файла — только для локальных имён (localhost и т.п.).
static int adns_hosts_lookup(const char* name, struct sockaddr_in* out) {
#ifndef _WIN32
if (!name || !out) return 0;
FILE* f = fopen("/etc/hosts", "r");
if (!f) return 0;
char line[512];
int found = 0;
while (!found && fgets(line, sizeof(line), f)) {
char ip[64]; int n = 0;
const char* p = line;
while (*p && !isspace(*p) && n < 63) ip[n++] = *p++;
ip[n] = '\0';
if (!ip[0] || ip[0] == '#') continue;
struct in_addr a;
if (inet_pton(AF_INET, ip, &a) != 1) continue;
while (*p && isspace(*p)) p++;
while (*p && *p != '\n') {
if (*p == '#') break;
char h[256]; int hn = 0;
while (*p && !isspace(*p) && *p != '#' && hn < 255) h[hn++] = *p++;
h[hn] = '\0';
if (h[0] && strcasecmp(h, name) == 0) {
memset(out, 0, sizeof(*out));
out->sin_family = AF_INET;
out->sin_addr = a;
found = 1;
break;
}
while (*p && isspace(*p)) p++;
}
}
fclose(f);
return found;
#else
(void)name; (void)out;
return 0;
#endif
}
/* ─── resolver setup ─── */
static struct dns_resolver* adns_open_resolver(const struct sockaddr_in* servers, int count,
@@ -204,11 +155,6 @@ static struct dns_resolver* adns_open_resolver(const struct sockaddr_in* servers
dns_resconf_close(rc);
return NULL;
}
#ifndef _WIN32
// Локальные имена из /etc/hosts (localhost и т.п.). Не фатально, если файла нет.
if (dns_hosts_loadpath(hosts, "/etc/hosts") != 0)
DEBUG_DEBUG(ADNS_DEBUG_CAT, "adns: /etc/hosts not loaded");
#endif
struct dns_hints* hints = dns_hints_local(rc, &error);
if (!hints) {
@@ -251,7 +197,7 @@ static void adns_rearm(struct adns_query* q) {
}
q->cur_fd = -1;
if (fd >= 0) {
q->socket_id = uasync_add_socket_t(q->ua, (socket_t)fd, adns_read_cb, adns_write_cb, NULL, "dns_query", q);
q->socket_id = uasync_add_socket_t(q->ua, (socket_t)fd, adns_read_cb, adns_write_cb, NULL, q);
if (q->socket_id) q->cur_fd = fd;
else DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: uasync_add_socket_t(%d) failed", fd);
}
@@ -380,34 +326,6 @@ static void adns_finish(struct adns_query* q, int status, struct sockaddr_in* ad
u_free(name);
}
// Отложенный вызов коллбэка для fast-пути (/etc/hosts): сохраняет async-контракт
// (adns_resolve возвращает handle, коллбэк вызывается в потоке event loop).
static void adns_fast_ready_cb(void* arg) {
struct adns_query* q = (struct adns_query*)arg;
q->call_soon_id = NULL;
if (q->finished) return; // отменён до срабатывания
q->finished = 1;
adns_done_cb cb = q->cb;
void* a = q->arg;
struct adns_result res = q->fast_result;
char* nm = q->name;
q->name = NULL;
u_free(q);
if (cb) cb(&res, a);
u_free(nm);
}
// Отложенный первый драйв slow-пути (libdns): adns_resolve возвращает живой handle,
// а реальный запуск запроса происходит в event loop. Это исключает ситуацию, когда
// dns_res_check завершается синхронно прямо внутри adns_resolve() и освобождает q
// до того, как вызывающий код получил и сохранил handle.
static void adns_drive_trampoline(void* arg) {
struct adns_query* q = (struct adns_query*)arg;
q->call_soon_id = NULL;
if (q->finished) return; // отменён до срабатывания
adns_drive(q);
}
/* ─── public API ─── */
struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
@@ -416,28 +334,6 @@ struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
if (!ua || !name || !name[0] || !cb) return NULL;
if (strlen(name) > 255) { DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: name too long"); return NULL; }
// Быстрый путь: имя из /etc/hosts (localhost и т.п.) — без libdns и сети.
// libdns по умолчанию идёт в DNS первым (lookup="bf"), поэтому локальные имена
// ждали бы таймаут DNS-запроса (2×2s); обрабатываем их сразу.
{
struct sockaddr_in ha;
if (adns_hosts_lookup(name, &ha)) {
struct adns_query* q = u_calloc(1, sizeof(*q));
if (!q) { DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: calloc failed"); return NULL; }
q->ua = ua; q->cb = cb; q->arg = arg;
q->name = u_strdup(name);
if (!q->name) { u_free(q); DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: strdup failed"); return NULL; }
q->fast = 1;
q->fast_result.status = ADNS_OK;
q->fast_result.count = 1;
q->fast_result.addrs[0] = ha;
q->call_soon_id = uasync_call_soon(ua, q, adns_fast_ready_cb);
if (!q->call_soon_id) { u_free(q->name); u_free(q); return NULL; }
DEBUG_DEBUG(ADNS_DEBUG_CAT, "adns: '%s' resolved from /etc/hosts (fast)", name);
return q;
}
}
struct sockaddr_in servers[3];
int count;
if (opts && opts->server.sin_family != 0) {
@@ -477,26 +373,13 @@ struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
DEBUG_DEBUG(ADNS_DEBUG_CAT, "adns: resolving '%s' via %d server(s), timeout=%dms attempts=%d",
name, count, timeout_ms, attempts);
q->call_soon_id = uasync_call_soon(ua, q, adns_drive_trampoline);
if (!q->call_soon_id) {
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: uasync_call_soon failed for '%s'", name);
dns_res_close(q->res);
u_free(q->name);
u_free(q);
return NULL;
}
adns_drive(q);
return q;
}
void adns_cancel(struct adns_query* q) {
if (!q || q->finished) return;
q->finished = 1;
if (q->call_soon_id) { uasync_call_soon_cancel(q->ua, q->call_soon_id); q->call_soon_id = NULL; }
if (q->fast) {
u_free(q->name);
u_free(q);
return;
}
adns_teardown(q);
u_free(q->name);
u_free(q);
+226 -187
View File
@@ -3,144 +3,223 @@
#include "mem.h"
#include <math.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#define BLOCK_LEVELS_CHUNK 64
#define PENDING_CHUNK 32
/* История и задержанные блоки ограничены окном настройки. Только запись накапливает output. */
struct audio_compressor {
audio_compressor_config_t cfg;
int enabled, configured, failed, block_samples, lookback_blocks, lookahead_blocks;
float rise_factor_per_block, release_factor_per_block, max_gain, gain_smoothed;
float* block_levels;
size_t levels_cap, blocks_seen;
int16_t* pending;
size_t pending_cap, pending_head, pending_count, accum_count;
int16_t* output;
size_t output_size, output_cap;
uint32_t clip_count;
unsigned dbg_counter;
};
int enabled;
/* Начальное усиление в середине диапазона в дБ. */
static float initial_gain(float max_gain) { return max_gain > 0.0f ? sqrtf(max_gain) : 1.0f; }
int block_samples;
int lookback_blocks;
int lookahead_blocks;
float rise_factor_per_block;
float max_gain;
float gain_smoothed;
float* block_levels;
size_t block_levels_count;
size_t block_levels_cap;
struct pending_block {
int16_t* samples;
size_t count;
size_t level_index;
}* pending;
size_t pending_count;
size_t pending_cap;
int16_t* accumulator;
size_t accum_count;
int16_t* output;
size_t output_size;
size_t output_cap;
int dbg_counter;
};
struct audio_compressor* audio_compressor_create(void) {
struct audio_compressor* ac = u_calloc(1, sizeof(*ac));
if (!ac) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: state allocation failed"); return NULL; }
if (!ac) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: OOM");
return NULL;
}
ac->enabled = 1;
ac->gain_smoothed = 1.0f;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: ok");
return ac;
}
void audio_compressor_destroy(struct audio_compressor* ac) {
if (!ac) return;
u_free(ac->pending); u_free(ac->block_levels); u_free(ac->output); u_free(ac);
for (size_t i = 0; i < ac->pending_count; i++)
u_free(ac->pending[i].samples);
u_free(ac->pending);
u_free(ac->block_levels);
u_free(ac->accumulator);
u_free(ac->output);
u_free(ac);
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor_destroy");
}
void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg) {
if (!ac || !cfg) return;
ac->cfg = *cfg;
if (ac->cfg.sample_rate <= 0) ac->cfg.sample_rate = 48000;
if (ac->cfg.channels <= 0) ac->cfg.channels = 1;
if (ac->cfg.block_duration_ms <= 0) ac->cfg.block_duration_ms = 20;
if (ac->cfg.lookback_ms <= 0) ac->cfg.lookback_ms = 200;
if (ac->cfg.lookahead_ms <= 0) ac->cfg.lookahead_ms = 100;
if (ac->cfg.target_level <= 0.0f) ac->cfg.target_level = 0.25f;
ac->block_samples = ac->cfg.sample_rate * ac->cfg.block_duration_ms / 1000 * ac->cfg.channels;
ac->lookback_blocks = ac->cfg.lookback_ms / ac->cfg.block_duration_ms;
ac->lookahead_blocks = ac->cfg.lookahead_ms / ac->cfg.block_duration_ms;
ac->rise_factor_per_block = powf(ac->cfg.rise_rate_per_sec, (float)ac->cfg.block_duration_ms / 1000.0f);
ac->max_gain = powf(10.0f, ac->cfg.max_gain_db / 20.0f);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"audio_compressor_configure: rate=%d ch=%d block=%dms lookback=%dms lookahead=%dms maxGain=%.0fdB riseRate=%.1f/sec target=%.0fdBFS",
ac->cfg.sample_rate, ac->cfg.channels, ac->cfg.block_duration_ms,
ac->cfg.lookback_ms, ac->cfg.lookahead_ms, ac->cfg.max_gain_db,
ac->cfg.rise_rate_per_sec, 20.0f * log10f(ac->cfg.target_level));
}
void audio_compressor_reset(struct audio_compressor* ac) {
if (!ac) return;
ac->pending_head = ac->pending_count = ac->accum_count = ac->blocks_seen = ac->output_size = 0;
ac->gain_smoothed = initial_gain(ac->max_gain);
ac->clip_count = ac->dbg_counter = 0;
ac->failed = 0;
for (size_t i = 0; i < ac->pending_count; i++)
u_free(ac->pending[i].samples);
ac->pending_count = 0;
ac->block_levels_count = 0;
ac->accum_count = 0;
ac->output_size = 0;
ac->gain_smoothed = 1.0f;
ac->dbg_counter = 0;
}
/* Ноль lookahead означает отсутствие задержки; defaults остальных полей задаются явно здесь. */
int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* config) {
if (!ac || !config) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: missing configuration"); return -1; }
audio_compressor_config_t cfg = *config;
if (!cfg.sample_rate) cfg.sample_rate = 48000;
if (!cfg.channels) cfg.channels = 1;
if (!cfg.block_duration_ms) cfg.block_duration_ms = 20;
if (!cfg.lookback_ms) cfg.lookback_ms = 200;
if (!cfg.target_level) cfg.target_level = 0.25f;
if (!cfg.rise_rate_per_sec) cfg.rise_rate_per_sec = 4.0f;
if (!cfg.release_rate_per_sec) cfg.release_rate_per_sec = cfg.rise_rate_per_sec * 5.0f;
if (cfg.sample_rate < 8000 || cfg.sample_rate > 192000 || cfg.channels < 1 || cfg.channels > 2 ||
cfg.block_duration_ms < 1 || cfg.block_duration_ms > 1000 || cfg.lookback_ms < 0 || cfg.lookback_ms > 5000 ||
cfg.lookahead_ms < 0 || cfg.lookahead_ms > 5000 || !isfinite(cfg.target_level) || cfg.target_level <= 0 ||
!isfinite(cfg.max_gain_db) || cfg.max_gain_db < 0 || cfg.max_gain_db > 60 ||
!isfinite(cfg.rise_rate_per_sec) || cfg.rise_rate_per_sec <= 0 ||
!isfinite(cfg.release_rate_per_sec) || cfg.release_rate_per_sec <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid rate=%d ch=%d block=%d back=%d ahead=%d",
cfg.sample_rate, cfg.channels, cfg.block_duration_ms, cfg.lookback_ms, cfg.lookahead_ms);
return -1;
}
int block = cfg.sample_rate * cfg.block_duration_ms / 1000 * cfg.channels;
int back = cfg.lookback_ms / cfg.block_duration_ms, ahead = cfg.lookahead_ms / cfg.block_duration_ms;
size_t levels_cap = (size_t)back + ahead + 2, pending_cap = (size_t)ahead + 2;
float* levels = u_calloc(levels_cap, sizeof(float));
int16_t* pending = u_calloc(pending_cap * (size_t)block, sizeof(int16_t));
if (!levels || !pending) {
u_free(levels); u_free(pending);
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: buffers allocation failed");
return -1;
}
u_free(ac->block_levels); u_free(ac->pending);
ac->cfg = cfg; ac->block_samples = block;
ac->lookback_blocks = back; ac->lookahead_blocks = ahead;
ac->block_levels = levels; ac->levels_cap = levels_cap;
ac->pending = pending; ac->pending_cap = pending_cap;
ac->rise_factor_per_block = powf(cfg.rise_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f);
ac->release_factor_per_block = powf(cfg.release_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f);
ac->max_gain = powf(10.0f, cfg.max_gain_db / 20.0f);
ac->configured = 1;
audio_compressor_reset(ac);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "compressor: configured rate=%d ch=%d ahead=%dms history=%zu pending=%zu",
cfg.sample_rate, cfg.channels, cfg.lookahead_ms, levels_cap, pending_cap);
return 0;
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) {
if (!ac) return;
ac->enabled = enabled ? 1 : 0;
}
struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source) {
if (!source || !source->configured) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: cannot clone configuration"); return NULL; }
struct audio_compressor* copy = audio_compressor_create();
if (copy && audio_compressor_configure(copy, &source->cfg) != 0) { audio_compressor_destroy(copy); return NULL; }
if (copy) copy->enabled = source->enabled;
return copy;
int audio_compressor_is_enabled(const struct audio_compressor* ac) {
return ac ? ac->enabled : 0;
}
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) { if (ac) ac->enabled = !!enabled; }
int audio_compressor_is_enabled(const struct audio_compressor* ac) { return ac && ac->enabled; }
/* Одна оценка для всех каналов сохраняет баланс стерео. */
static float compute_block_level(const int16_t* samples, size_t count) {
double sum_sq = 0.0;
float peak = 0.0f;
for (size_t i = 0; i < count; i++) {
float v = (float)samples[i] / 32768.0f;
sum_sq += (double)v * v;
if (fabsf(v) > peak) peak = fabsf(v);
float av = fabsf(v);
if (av > peak) peak = av;
}
return (2.0f * sqrtf((float)(sum_sq / count)) + peak) / 2.0f;
float rms = (float)sqrt(sum_sq / (double)count);
return (rms * 2.0f + peak) / 2.0f;
}
/* Выход записи растёт только вне аппаратного callback; OOM переводит запись в failed. */
static int reserve_output(struct audio_compressor* ac, size_t count) {
if (count > SIZE_MAX / sizeof(int16_t) - ac->output_size) goto failed;
size_t want = ac->output_size + count;
if (want <= ac->output_cap) return 0;
size_t capacity = ac->output_cap ? ac->output_cap : 4096;
while (capacity < want) {
if (capacity > SIZE_MAX / sizeof(int16_t) / 2) { capacity = want; break; }
capacity *= 2;
}
int16_t* samples = u_realloc(ac->output, capacity * sizeof(int16_t));
if (!samples) goto failed;
ac->output = samples; ac->output_cap = capacity;
return 0;
failed:
ac->failed = 1;
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: recording output allocation failed samples=%zu", count);
return -1;
static void ensure_output_cap(struct audio_compressor* ac, size_t needed) {
size_t want = ac->output_size + needed;
if (want <= ac->output_cap) return;
size_t new_cap = ac->output_cap ? ac->output_cap * 2 : 4096;
while (new_cap < want) new_cap *= 2;
int16_t* tmp = u_realloc(ac->output, new_cap * sizeof(int16_t));
if (!tmp) return;
ac->output = tmp;
ac->output_cap = new_cap;
}
/* Общий алгоритм усиления для записи с lookahead и потокового кадра без lookahead. */
static void apply_gain(struct audio_compressor* ac, int16_t* samples, size_t count, size_t idx) {
size_t begin = idx >= (size_t)ac->lookback_blocks ? idx - ac->lookback_blocks : 0;
size_t end = idx + ac->lookahead_blocks;
if (end >= ac->blocks_seen) end = ac->blocks_seen - 1;
float envelope = 0;
for (size_t i = begin; i <= end; i++) {
float level = ac->block_levels[i % ac->levels_cap];
if (level > envelope) envelope = level;
static void append_output(struct audio_compressor* ac, const int16_t* samples, size_t count) {
ensure_output_cap(ac, count);
memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
ac->output_size += count;
}
static void ensure_block_levels_cap(struct audio_compressor* ac) {
if (ac->block_levels_count < ac->block_levels_cap) return;
size_t new_cap = ac->block_levels_cap ? ac->block_levels_cap * 2 : BLOCK_LEVELS_CHUNK;
float* tmp = u_realloc(ac->block_levels, new_cap * sizeof(float));
if (!tmp) return;
ac->block_levels = tmp;
ac->block_levels_cap = new_cap;
}
static void add_block_level(struct audio_compressor* ac, float level) {
ensure_block_levels_cap(ac);
ac->block_levels[ac->block_levels_count++] = level;
}
static void ensure_pending_cap(struct audio_compressor* ac) {
if (ac->pending_count < ac->pending_cap) return;
size_t new_cap = ac->pending_cap ? ac->pending_cap * 2 : PENDING_CHUNK;
struct pending_block* tmp = u_realloc(ac->pending, new_cap * sizeof(*tmp));
if (!tmp) return;
ac->pending = tmp;
ac->pending_cap = new_cap;
}
static void process_pending_block(struct audio_compressor* ac);
void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) {
if (!ac || !samples || count == 0) return;
if (!ac->enabled) {
append_output(ac, samples, count);
return;
}
size_t remaining = count;
const int16_t* src = samples;
while (remaining > 0) {
size_t need = (size_t)ac->block_samples - ac->accum_count;
size_t take = remaining < need ? remaining : need;
size_t new_cnt = ac->accum_count + take;
int16_t* tmp = u_realloc(ac->accumulator, new_cnt * sizeof(int16_t));
if (!tmp) return;
ac->accumulator = tmp;
memcpy(ac->accumulator + ac->accum_count, src, take * sizeof(int16_t));
ac->accum_count = new_cnt;
src += take;
remaining -= take;
if (ac->accum_count == (size_t)ac->block_samples) {
float level = compute_block_level(ac->accumulator, ac->accum_count);
add_block_level(ac, level);
ensure_pending_cap(ac);
ac->pending[ac->pending_count].samples = ac->accumulator;
ac->pending[ac->pending_count].count = ac->accum_count;
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
ac->pending_count++;
ac->accumulator = NULL;
ac->accum_count = 0;
}
}
while (ac->pending_count > (size_t)ac->lookahead_blocks)
process_pending_block(ac);
}
static void process_pending_block(struct audio_compressor* ac) {
if (ac->pending_count == 0) return;
size_t idx = ac->pending[0].level_index;
size_t win_start = idx >= (size_t)ac->lookback_blocks ? idx - (size_t)ac->lookback_blocks : 0;
size_t win_end = idx + (size_t)ac->lookahead_blocks;
if (win_end >= ac->block_levels_count) win_end = ac->block_levels_count - 1;
float envelope = 0.0f;
for (size_t i = win_start; i <= win_end; i++) {
if (ac->block_levels[i] > envelope) envelope = ac->block_levels[i];
}
if (envelope < 0.0001f) envelope = 0.0001f;
@@ -148,106 +227,66 @@ static void apply_gain(struct audio_compressor* ac, int16_t* samples, size_t cou
float G_prev = ac->gain_smoothed;
if (G_raw > ac->gain_smoothed) {
/* сигнал стал тише — плавно поднимаем gain (атака) */
float max_rise = ac->gain_smoothed * ac->rise_factor_per_block;
ac->gain_smoothed = G_raw < max_rise ? G_raw : max_rise;
} else {
/* сигнал стал громче — плавно опускаем gain (release), без мгновенной ступеньки
* (мгновенный дроп давал клик на границе 20мс-блока). */
float min_fall = ac->gain_smoothed / ac->release_factor_per_block;
ac->gain_smoothed = G_raw > min_fall ? G_raw : min_fall;
ac->gain_smoothed = G_raw;
}
if (ac->gain_smoothed > ac->max_gain) ac->gain_smoothed = ac->max_gain;
float gain_db = 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f);
float prev_db = 20.0f * log10f(G_prev > 0.0001f ? G_prev : 0.0001f);
if (ac->dbg_counter % 25 == 0 || fabsf(gain_db - prev_db) > 3.0f) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB clip=%u",
idx, ac->block_levels[idx % ac->levels_cap], envelope, 20.0f * log10f(G_raw), gain_db, (unsigned)ac->clip_count);
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB",
idx, ac->block_levels[idx], envelope, 20.0f * log10f(G_raw), gain_db);
}
ac->dbg_counter++;
for (size_t i = 0; i < count; i++) {
float v = (float)samples[i] * ac->gain_smoothed;
if (v > 32767.0f) { v = 32767.0f; ac->clip_count++; }
if (v < -32768.0f) { v = -32768.0f; ac->clip_count++; }
samples[i] = (int16_t)(int)v;
for (size_t i = 0; i < ac->pending[0].count; i++) {
float v = (float)ac->pending[0].samples[i] * ac->gain_smoothed;
if (v > 32767.0f) v = 32767.0f;
if (v < -32768.0f) v = -32768.0f;
ac->pending[0].samples[i] = (int16_t)(int)v;
}
append_output(ac, ac->pending[0].samples, ac->pending[0].count);
u_free(ac->pending[0].samples);
/* shift remaining pending blocks down */
if (ac->pending_count > 1)
memmove(ac->pending, ac->pending + 1, (ac->pending_count - 1) * sizeof(*ac->pending));
ac->pending_count--;
}
/* Слить один готовый блок записи. История хранится по абсолютному номеру в кольце. */
static int drain_block(struct audio_compressor* ac, size_t count) {
if (reserve_output(ac, count) != 0) return -1;
int16_t* samples = ac->pending + ac->pending_head * ac->block_samples;
apply_gain(ac, samples, count, ac->blocks_seen - ac->pending_count);
memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
ac->output_size += count;
ac->pending_head = (ac->pending_head + 1) % ac->pending_cap;
--ac->pending_count;
return 0;
void audio_compressor_flush(struct audio_compressor* ac) {
if (!ac) return;
if (!ac->enabled) return;
if (ac->accum_count > 0) {
float level = compute_block_level(ac->accumulator, ac->accum_count);
add_block_level(ac, level);
ensure_pending_cap(ac);
ac->pending[ac->pending_count].samples = ac->accumulator;
ac->pending[ac->pending_count].count = ac->accum_count;
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
ac->pending_count++;
ac->accumulator = NULL;
ac->accum_count = 0;
}
if (ac->pending_count > 0)
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_flush: %zu pending blocks (gainSmoothed=%.1fdB)",
ac->pending_count, 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f));
while (ac->pending_count > 0)
process_pending_block(ac);
}
/* Ровно один кадр, без output-накопителя и выделений памяти. in/out могут совпадать. */
int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count) {
if (!ac || !ac->configured || ac->failed || !in || !out || count != (size_t)ac->block_samples ||
ac->lookahead_blocks || ac->pending_count || ac->accum_count) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid streaming frame count=%zu", count);
return -1;
}
memmove(out, in, count * sizeof(int16_t));
if (ac->enabled) {
size_t idx = ac->blocks_seen++;
ac->block_levels[idx % ac->levels_cap] = compute_block_level(in, count);
apply_gain(ac, out, count, idx);
}
return 0;
const int16_t* audio_compressor_output(const struct audio_compressor* ac) {
return ac ? ac->output : NULL;
}
int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) {
if (!ac || !ac->configured || ac->failed || (!samples && count)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid recording input"); return -1;
}
if (!ac->enabled) {
if (reserve_output(ac, count) != 0) return -1;
if (count) memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
ac->output_size += count;
return 0;
}
while (count) {
size_t take = (size_t)ac->block_samples - ac->accum_count;
if (take > count) take = count;
int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples;
memcpy(block + ac->accum_count, samples, take * sizeof(int16_t));
samples += take; count -= take; ac->accum_count += take;
if (ac->accum_count == (size_t)ac->block_samples) {
ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, ac->accum_count);
ac->accum_count = 0; ++ac->pending_count;
if (ac->pending_count > (size_t)ac->lookahead_blocks && drain_block(ac, ac->block_samples) != 0) return -1;
}
}
return 0;
}
int audio_compressor_flush(struct audio_compressor* ac) {
if (!ac || !ac->configured || ac->failed) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid flush"); return -1; }
if (!ac->enabled) return 0;
size_t tail = ac->accum_count;
if (tail) {
int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples;
ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, tail);
ac->accum_count = 0; ++ac->pending_count;
}
while (ac->pending_count) {
size_t count = tail && ac->pending_count == 1 ? tail : (size_t)ac->block_samples;
if (drain_block(ac, count) != 0) return -1;
}
return 0;
}
const int16_t* audio_compressor_output(const struct audio_compressor* ac) { return ac ? ac->output : NULL; }
size_t audio_compressor_output_size(const struct audio_compressor* ac) { return ac ? ac->output_size : 0; }
void audio_compressor_clear_output(struct audio_compressor* ac) { if (ac) ac->output_size = 0; }
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac) { return ac ? ac->clip_count : 0; }
float audio_compressor_gain_db(const struct audio_compressor* ac) {
return ac && ac->configured && ac->enabled ? 20.0f * log10f(ac->gain_smoothed) : 0.0f;
size_t audio_compressor_output_size(const struct audio_compressor* ac) {
return ac ? ac->output_size : 0;
}
+4 -26
View File
@@ -1,9 +1,3 @@
/* audio_compressor — адаптивное усиление int16 PCM (моно/стерео) с общей огибающей каналов.
* create → configure → обработка → destroy; объект использует один владелец, блокировок нет.
* Запись: push принимает произвольные порции, flush завершает хвост и lookahead; output принадлежит объекту.
* Поток: process_frame обрабатывает ровно sample_rate*block_duration_ms/1000*channels отсчётов,
* требует нулевого lookahead и пустого входного накопителя; in/out могут совпадать.
* count/output_size — interleaved отсчёты, не байты и не кадры на канал. Выключение даёт исходный PCM. */
#ifndef AUDIO_COMPRESSOR_H
#define AUDIO_COMPRESSOR_H
@@ -24,40 +18,24 @@ typedef struct {
int lookahead_ms;
float max_gain_db;
float rise_rate_per_sec;
float release_rate_per_sec; /* скорость плавного снижения gain (0 = авто: rise*5) */
float target_level; /* Линейная амплитуда огибающей, не дБ; 0 задаёт default 0.25. */
float target_level;
} audio_compressor_config_t;
struct audio_compressor* audio_compressor_create(void);
/* Новый пустой compressor с теми же настройками; позволяет передать запись worker без копии PCM. */
struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source);
void audio_compressor_destroy(struct audio_compressor* ac);
int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg);
/* Потоковый кадр без lookahead: результат немедленный, память не выделяется; 0 / -1. */
int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count);
void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg);
void audio_compressor_reset(struct audio_compressor* ac);
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled);
int audio_compressor_is_enabled(const struct audio_compressor* ac);
/* push копирует вход; push/flush возвращают 0 / -1. Ошибка накопления требует reset перед повтором. */
int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count);
int audio_compressor_flush(struct audio_compressor* ac);
void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count);
void audio_compressor_flush(struct audio_compressor* ac);
/* Указатель действителен до изменения/освобождения выходного буфера; вызывающий его не освобождает. */
const int16_t* audio_compressor_output(const struct audio_compressor* ac);
size_t audio_compressor_output_size(const struct audio_compressor* ac);
/* Сбросить накопленный выход (output_size=0) после слива — для потокового режима.
* НЕ трогает gain_smoothed (адаптивная громкость сохраняется). */
void audio_compressor_clear_output(struct audio_compressor* ac);
/* Число сэмплов, обрезанных по потолку int16 (клиппинг) с момента create/reset. */
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac);
/* Текущее фактическое усиление в дБ; выключенный compressor возвращает 0. */
float audio_compressor_gain_db(const struct audio_compressor* ac);
#ifdef __cplusplus
}
#endif
-9
View File
@@ -129,15 +129,6 @@ static const struct {
{"chat_sync", DEBUG_CATEGORY_CHAT_SYNC},
{"member_sync", DEBUG_CATEGORY_MEMBER_SYNC},
{"proxy", DEBUG_CATEGORY_PROXY},
{"video", DEBUG_CATEGORY_VIDEO},
{"reality", DEBUG_CATEGORY_REALITY},
{"dm", DEBUG_CATEGORY_DM},
{"call", DEBUG_CATEGORY_CALL},
{"radio", DEBUG_CATEGORY_RADIO},
{"vad", DEBUG_CATEGORY_VAD},
{"aec", DEBUG_CATEGORY_AEC},
{"stream", DEBUG_CATEGORY_STREAM},
{"discovery", DEBUG_CATEGORY_DISCOVERY},
{"all", DEBUG_CATEGORY_ALL},
{NULL, DEBUG_CATEGORY_NONE}
};
+8 -18
View File
@@ -41,7 +41,7 @@ typedef int debug_category_t;
#define DEBUG_CATEGORY_NONE 0
#define DEBUG_CATEGORY_SYS 1 // System: u_async, ll_queue, memory, timing
// #define DEBUG_CATEGORY_LL_QUEUE 2 (merged into SYS)
#define DEBUG_CATEGORY_CONNECTION 3 // Connection: connect/reconnect/disconnect events only
#define DEBUG_CATEGORY_CONNECTION 3 // Connection: links, keepalive, status events
#define DEBUG_CATEGORY_ETCP 4 // ETCP: protocol, BBR, normalizer
#define DEBUG_CATEGORY_CRYPTO 5 // crypto operations
// #define DEBUG_CATEGORY_MEMORY 6 (merged into SYS)
@@ -67,22 +67,13 @@ typedef int debug_category_t;
#define DEBUG_CATEGORY_CHAT_SYNC 26 // Message DB sync (INIT_SYNC, PUSH, chain hash, recalc)
#define DEBUG_CATEGORY_MEMBER_SYNC 27 // Member + address + merkle sync
#define DEBUG_CATEGORY_PROXY 28 // Proxy modules (SOCKS5, TCP, UDP, ICMP)
#define DEBUG_CATEGORY_VIDEO 29 // Video module (probe/transcode)
#define DEBUG_CATEGORY_REALITY 30 // REALITY TLS camouflage
#define DEBUG_CATEGORY_DM 31 // DM — direct p2p chat between two users
#define DEBUG_CATEGORY_CALL 32 // Call — P2P audio call (signaling + media)
#define DEBUG_CATEGORY_RADIO 33 // Radio — PTT walkie-talkie multicast (voice)
#define DEBUG_CATEGORY_VAD 34 // VAD — Silero voice activity detector (ONNX Runtime)
#define DEBUG_CATEGORY_AEC 35 // AEC — acoustic echo cancellation (SpeexDSP)
#define DEBUG_CATEGORY_STREAM 36 // Подписанные потоки, FEC, буферы и выбор поставщиков
#define DEBUG_CATEGORY_DISCOVERY 37 // LAN UDP: discovery and delivery
#define DEBUG_CATEGORY_COUNT 38 // Total number of categories
#define DEBUG_CATEGORY_COUNT 29 // Total number of categories
#define DEBUG_CATEGORY_ALL (-1) // special value for all categories
/* Debug configuration structure */
typedef struct {
debug_level_t level; // Global debug level (default: ERROR)
debug_level_t category_levels[DEBUG_CATEGORY_COUNT]; // Уровень категории: NONE наследует общий, DISABLED выключает
debug_level_t category_levels[DEBUG_CATEGORY_COUNT]; // Per-category levels (0 = disabled)
int timestamp_enabled; // Include timestamps in output
int function_name_enabled; // Include function names
int file_line_enabled; // Include file:line info
@@ -101,19 +92,18 @@ extern debug_config_t g_debug_config;
void debug_config_init(void);
/* Set debug level */
// Общий уровень применяется к категориям с уровнем NONE.
// итоговый level = max (global level - здесь задается, category level)
void debug_set_level(debug_level_t level);
/* Уровень категории заменяет общий; NONE наследует его, DISABLED подавляет вывод. */
/* Set debug level for specific category (0 = disabled, otherwise uses that level) */
void debug_set_category_level(debug_category_t category, debug_level_t level);
/* enable фиксирует текущий общий уровень; disable возвращает к наследованию (NONE).
* Для полного выключения использовать debug_set_category_level(cat, DEBUG_LEVEL_DISABLED). */
/* Enable/disable specific categories */
void debug_enable_category(debug_category_t category);
void debug_disable_category(debug_category_t category);
void debug_set_categories(debug_category_t categories);
/* Установка уровня одной категории (аргумент categories — индекс, не битовая маска). */
/* Set masks directly */
void debug_set_masks(debug_category_t categories, debug_level_t level);
/* Configure output options */
@@ -129,7 +119,7 @@ int debug_reopen_log(void);
void debug_enable_console(int enable);
// IP/sockaddr в строку: результат возвращается по значению, освобождать не требуется.
// IP address to string (static buffer, single-threaded)
typedef struct {
char str[54]; // INET6_ADDRSTRLEN(45) + 6chars (:port) + \0
} ip_str_t;
+22 -22
View File
@@ -3403,33 +3403,33 @@ size_t dns_txt_print(void *dst_, size_t lim, struct dns_txt *txt) {
} /* dns_txt_print() */
typedef int (*dns_parse_fn)(union dns_any *, struct dns_rr *, struct dns_packet *);
typedef int (*dns_push_fn)(struct dns_packet *, union dns_any *);
typedef int (*dns_cmp_fn)(const union dns_any *, const union dns_any *);
typedef size_t (*dns_print_fn)(void *, size_t, union dns_any *);
typedef size_t (*dns_cname_fn)(void *, size_t, union dns_any *);
typedef int (*dns_rrtype_parse_fn)(union dns_any *, struct dns_rr *, struct dns_packet *);
typedef int (*dns_rrtype_push_fn)(struct dns_packet *, union dns_any *);
typedef int (*dns_rrtype_cmp_fn)(const union dns_any *, const union dns_any *);
typedef size_t (*dns_rrtype_print_fn)(void *, size_t, union dns_any *);
typedef size_t (*dns_rrtype_cname_fn)(void *, size_t, union dns_any *);
static const struct {
enum dns_type type;
const char *name;
dns_parse_fn parse;
dns_push_fn push;
dns_cmp_fn cmp;
dns_print_fn print;
dns_cname_fn cname;
dns_rrtype_parse_fn parse;
dns_rrtype_push_fn push;
dns_rrtype_cmp_fn cmp;
dns_rrtype_print_fn print;
dns_rrtype_cname_fn cname;
} dns_rrtypes[] = {
{ DNS_T_A, "A", (dns_parse_fn)&dns_a_parse, (dns_push_fn)&dns_a_push, (dns_cmp_fn)&dns_a_cmp, (dns_print_fn)&dns_a_print, 0 },
{ DNS_T_AAAA, "AAAA", (dns_parse_fn)&dns_aaaa_parse, (dns_push_fn)&dns_aaaa_push, (dns_cmp_fn)&dns_aaaa_cmp, (dns_print_fn)&dns_aaaa_print, 0 },
{ DNS_T_MX, "MX", (dns_parse_fn)&dns_mx_parse, (dns_push_fn)&dns_mx_push, (dns_cmp_fn)&dns_mx_cmp, (dns_print_fn)&dns_mx_print, (dns_cname_fn)&dns_mx_cname },
{ DNS_T_NS, "NS", (dns_parse_fn)&dns_ns_parse, (dns_push_fn)&dns_ns_push, (dns_cmp_fn)&dns_ns_cmp, (dns_print_fn)&dns_ns_print, (dns_cname_fn)&dns_ns_cname },
{ DNS_T_CNAME, "CNAME", (dns_parse_fn)&dns_cname_parse, (dns_push_fn)&dns_cname_push, (dns_cmp_fn)&dns_cname_cmp, (dns_print_fn)&dns_cname_print, (dns_cname_fn)&dns_cname_cname },
{ DNS_T_SOA, "SOA", (dns_parse_fn)&dns_soa_parse, (dns_push_fn)&dns_soa_push, (dns_cmp_fn)&dns_soa_cmp, (dns_print_fn)&dns_soa_print, 0 },
{ DNS_T_SRV, "SRV", (dns_parse_fn)&dns_srv_parse, (dns_push_fn)&dns_srv_push, (dns_cmp_fn)&dns_srv_cmp, (dns_print_fn)&dns_srv_print, (dns_cname_fn)&dns_srv_cname },
{ DNS_T_OPT, "OPT", (dns_parse_fn)&dns_opt_parse, (dns_push_fn)&dns_opt_push, (dns_cmp_fn)&dns_opt_cmp, (dns_print_fn)&dns_opt_print, 0 },
{ DNS_T_PTR, "PTR", (dns_parse_fn)&dns_ptr_parse, (dns_push_fn)&dns_ptr_push, (dns_cmp_fn)&dns_ptr_cmp, (dns_print_fn)&dns_ptr_print, (dns_cname_fn)&dns_ptr_cname },
{ DNS_T_TXT, "TXT", (dns_parse_fn)&dns_txt_parse, (dns_push_fn)&dns_txt_push, (dns_cmp_fn)&dns_txt_cmp, (dns_print_fn)&dns_txt_print, 0 },
{ DNS_T_SPF, "SPF", (dns_parse_fn)&dns_txt_parse, (dns_push_fn)&dns_txt_push, (dns_cmp_fn)&dns_txt_cmp, (dns_print_fn)&dns_txt_print, 0 },
{ DNS_T_SSHFP, "SSHFP", (dns_parse_fn)&dns_sshfp_parse, (dns_push_fn)&dns_sshfp_push, (dns_cmp_fn)&dns_sshfp_cmp, (dns_print_fn)&dns_sshfp_print, 0 },
{ DNS_T_A, "A", (dns_rrtype_parse_fn)&dns_a_parse, (dns_rrtype_push_fn)&dns_a_push, (dns_rrtype_cmp_fn)&dns_a_cmp, (dns_rrtype_print_fn)&dns_a_print, 0 },
{ DNS_T_AAAA, "AAAA", (dns_rrtype_parse_fn)&dns_aaaa_parse, (dns_rrtype_push_fn)&dns_aaaa_push, (dns_rrtype_cmp_fn)&dns_aaaa_cmp, (dns_rrtype_print_fn)&dns_aaaa_print, 0 },
{ DNS_T_MX, "MX", (dns_rrtype_parse_fn)&dns_mx_parse, (dns_rrtype_push_fn)&dns_mx_push, (dns_rrtype_cmp_fn)&dns_mx_cmp, (dns_rrtype_print_fn)&dns_mx_print, (dns_rrtype_cname_fn)&dns_mx_cname },
{ DNS_T_NS, "NS", (dns_rrtype_parse_fn)&dns_ns_parse, (dns_rrtype_push_fn)&dns_ns_push, (dns_rrtype_cmp_fn)&dns_ns_cmp, (dns_rrtype_print_fn)&dns_ns_print, (dns_rrtype_cname_fn)&dns_ns_cname },
{ DNS_T_CNAME, "CNAME", (dns_rrtype_parse_fn)&dns_cname_parse, (dns_rrtype_push_fn)&dns_cname_push, (dns_rrtype_cmp_fn)&dns_cname_cmp, (dns_rrtype_print_fn)&dns_cname_print, (dns_rrtype_cname_fn)&dns_cname_cname },
{ DNS_T_SOA, "SOA", (dns_rrtype_parse_fn)&dns_soa_parse, (dns_rrtype_push_fn)&dns_soa_push, (dns_rrtype_cmp_fn)&dns_soa_cmp, (dns_rrtype_print_fn)&dns_soa_print, 0 },
{ DNS_T_SRV, "SRV", (dns_rrtype_parse_fn)&dns_srv_parse, (dns_rrtype_push_fn)&dns_srv_push, (dns_rrtype_cmp_fn)&dns_srv_cmp, (dns_rrtype_print_fn)&dns_srv_print, (dns_rrtype_cname_fn)&dns_srv_cname },
{ DNS_T_OPT, "OPT", (dns_rrtype_parse_fn)&dns_opt_parse, (dns_rrtype_push_fn)&dns_opt_push, (dns_rrtype_cmp_fn)&dns_opt_cmp, (dns_rrtype_print_fn)&dns_opt_print, 0 },
{ DNS_T_PTR, "PTR", (dns_rrtype_parse_fn)&dns_ptr_parse, (dns_rrtype_push_fn)&dns_ptr_push, (dns_rrtype_cmp_fn)&dns_ptr_cmp, (dns_rrtype_print_fn)&dns_ptr_print, (dns_rrtype_cname_fn)&dns_ptr_cname },
{ DNS_T_TXT, "TXT", (dns_rrtype_parse_fn)&dns_txt_parse, (dns_rrtype_push_fn)&dns_txt_push, (dns_rrtype_cmp_fn)&dns_txt_cmp, (dns_rrtype_print_fn)&dns_txt_print, 0 },
{ DNS_T_SPF, "SPF", (dns_rrtype_parse_fn)&dns_txt_parse, (dns_rrtype_push_fn)&dns_txt_push, (dns_rrtype_cmp_fn)&dns_txt_cmp, (dns_rrtype_print_fn)&dns_txt_print, 0 },
{ DNS_T_SSHFP, "SSHFP", (dns_rrtype_parse_fn)&dns_sshfp_parse, (dns_rrtype_push_fn)&dns_sshfp_push, (dns_rrtype_cmp_fn)&dns_sshfp_cmp, (dns_rrtype_print_fn)&dns_sshfp_print, 0 },
}; /* dns_rrtypes[] */
+7 -7
View File
@@ -23,24 +23,24 @@ int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
struct sockaddr_storage *result)
{
if (!local_addr || !remote_addr || !result) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: NULL argument");
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: NULL argument");
return -1;
}
int family = local_addr->ss_family;
if (family != AF_INET && family != AF_INET6) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: unsupported family %d", family);
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: unsupported family %d", family);
return -1;
}
if (remote_addr->ss_family != family) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: family mismatch");
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: family mismatch");
return -1;
}
socket_t sock = socket_create_udp(family);
if (sock == SOCKET_INVALID) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: socket_create_udp failed");
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: socket_create_udp failed");
return -1;
}
@@ -56,7 +56,7 @@ int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
socklen_t bind_len = (family == AF_INET) ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
if (bind(sock, (struct sockaddr *)&bind_addr, bind_len) != 0) {
int err = socket_get_error();
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: bind failed: %s", socket_strerror(err));
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: bind failed: %s", socket_strerror(err));
socket_close_wrapper(sock);
return -1;
}
@@ -65,7 +65,7 @@ int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
socklen_t remote_len = (family == AF_INET) ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
if (connect(sock, (struct sockaddr *)remote_addr, remote_len) != 0) {
int err = socket_get_error();
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: connect failed: %s", socket_strerror(err));
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: connect failed: %s", socket_strerror(err));
socket_close_wrapper(sock);
return -1;
}
@@ -75,7 +75,7 @@ int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
socklen_t local_len = sizeof(local);
if (getsockname(sock, (struct sockaddr *)&local, &local_len) != 0) {
int err = socket_get_error();
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: getsockname failed: %s", socket_strerror(err));
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: getsockname failed: %s", socket_strerror(err));
socket_close_wrapper(sock);
return -1;
}
+10 -27
View File
@@ -1,6 +1,4 @@
#include "json_flat.h"
#include "mem.h"
#include "debug_config.h"
#include <string.h>
#define SKIP_WS(p, end) while ((p) < (end) && (*(p) == ' ' || *(p) == '\t' || *(p) == '\n' || *(p) == '\r')) (p)++
@@ -21,8 +19,6 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
case 'n': ch = '\n'; break;
case 'r': ch = '\r'; break;
case 't': ch = '\t'; break;
case 'b': ch = '\b'; break;
case 'f': ch = '\f'; break;
default: return -1;
}
}
@@ -37,22 +33,20 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
return 0;
}
static int json_parse_pair(const char** pp, const char* end, char* scratch, size_t capacity,
static int json_parse_pair(const char** pp, const char* end,
json_flat_cb cb, void* arg) {
const char* p = *pp;
SKIP_WS(p, end); CHECK(p, end);
if (*p == '}') { *pp = p; return 0; }
char* key = scratch;
if (json_read_string(&p, end, key, capacity) != 0) return -1;
size_t key_size = strlen(key) + 1;
char* val = scratch + key_size;
char key[128], val[256];
if (json_read_string(&p, end, key, sizeof(key)) != 0) return -1;
SKIP_WS(p, end); CHECK(p, end);
if (*p != ':') return -1; p++;
SKIP_WS(p, end); CHECK(p, end);
if (json_read_string(&p, end, val, capacity - key_size) != 0) return -1;
if (json_read_string(&p, end, val, sizeof(val)) != 0) return -1;
if (cb) { int rc = cb(key, val, arg); if (rc != 0) { *pp = p; return rc < 0 ? -1 : 1; } }
if (cb && cb(key, val, arg) != 0) { *pp = p; return 1; }
SKIP_WS(p, end); CHECK(p, end);
if (*p == ',') p++;
@@ -67,23 +61,13 @@ int json_flat_parse(const char* json, json_flat_cb cb, void* arg) {
SKIP_WS(p, end); CHECK(p, end);
if (*p != '{') return -1; p++;
/* Decoded key + value + two NULs fit within the encoded object size. */
size_t capacity = strlen(json) + 1;
if (capacity > UINT32_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: input too large"); return -1; }
char local[384];
char* scratch = capacity <= sizeof(local) ? local : u_malloc(capacity);
if (!scratch) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: allocation failed bytes=%zu", capacity); return -1; }
int result = 0;
for (;;) {
int rc = json_parse_pair(&p, end, scratch, capacity, cb, arg);
if (rc < 0) { result = -1; break; }
if (rc > 0) break;
SKIP_WS(p, end);
if (p >= end) { result = -1; break; }
if (*p == '}') break;
int rc = json_parse_pair(&p, end, cb, arg);
if (rc < 0) return -1;
if (rc > 0) return 0; /* callback stopped */
SKIP_WS(p, end); CHECK(p, end);
if (*p == '}') return 0;
}
if (scratch != local) u_free(scratch);
return result;
}
struct get_ctx { const char* key; char* out; size_t sz; int found; };
@@ -97,7 +81,6 @@ static int get_cb(const char* k, const char* v, void* arg) {
}
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz) {
if (!key || (value_out && !value_sz)) return -1;
struct get_ctx g = { key, value_out, value_sz, 0 };
if (json_flat_parse(json, get_cb, &g) != 0) return -1;
return g.found ? 0 : -1;
+1 -5
View File
@@ -2,10 +2,7 @@
* @file json_flat.h
* @brief Парсер плоского JSON без вложенности: {"key":"value",...}
*
* Только строковые значения и обычные escapes; \uXXXX не поддерживается.
* Callback получает полные key/value, действительные только до его возврата.
* parse возвращает 0 при успехе/остановке callback, -1 при ошибке; остановка не проверяет остаток ввода.
* get ищет первый ключ и копирует значение с NUL, при малом буфере обрезает; 0 найден, -1 ошибка/нет ключа.
* Только строковые значения. Без malloc, без зависимостей.
*/
#ifndef JSON_FLAT_H
@@ -18,7 +15,6 @@ extern "C" {
#endif
typedef int (*json_flat_cb)(const char* key, const char* value, void* arg);
/* Callback: 0 = continue, >0 = stop successfully, <0 = error. */
int json_flat_parse(const char* json, json_flat_cb cb, void* arg);
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz);
-44
View File
@@ -1,44 +0,0 @@
Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic,
Jean-Marc Valin, Timothy B. Terriberry,
CSIRO, Gregory Maxwell, Mark Borgerding,
Erik de Castro Lopo, Mozilla, Amazon
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of Internet Society, IETF or IETF Trust, nor the
names of specific contributors, may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Opus is subject to the royalty-free patent licenses which are
specified at:
Xiph.Org Foundation:
https://datatracker.ietf.org/ipr/1524/
Microsoft Corporation:
https://datatracker.ietf.org/ipr/1914/
Broadcom Corporation:
https://datatracker.ietf.org/ipr/1526/
-2
View File
@@ -1,7 +1,5 @@
noinst_LIBRARIES = libopus_internal.a
EXTRA_DIST = COPYING
# Override global CFLAGS to disable ASAN (makes .a incompatible with PIE linking)
CFLAGS = -g -Og -fno-omit-frame-pointer -fno-pie -fPIC
+40 -125
View File
@@ -52,23 +52,15 @@ static void on_get_trampoline(void* arg);
static void empty_trampoline(void* arg) {
struct ll_queue* q = (struct ll_queue*)arg;
q->empty_call_soon_id = NULL;
if (q->count != 0) return;
queue_callback_fn cb = q->empty_callback;
void* cb_arg = q->empty_callback_arg;
q->empty_callback = NULL;
q->empty_callback_arg = NULL;
if (cb) cb(q, cb_arg);
if (q->count == 0 && cb) cb(q, cb_arg);
}
static void on_get_trampoline(void* arg) {
struct ll_queue* q = (struct ll_queue*)arg;
q->on_get_call_soon_id = NULL;
if (!q->on_get_pending || !q->on_get) return;
q->on_get_pending--;
if (q->on_get_pending) {
q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline);
if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending);
}
if (q->on_get) q->on_get(q, q->on_get_arg);
}
@@ -138,7 +130,6 @@ void queue_free(struct ll_queue* q) {
// ВАЖНО: Не освобождаем элементы в очереди - они должны быть извлечены отдельно
// Это упрощает архитектуру и предотвращает double-u_free
while (q->deferred_waiters) queue_waiter_cancel(q, q->deferred_waiters);
// Освободить список waiters (сбросить handle->internal, отменить отложенные вызовы)
struct queue_waiter* w = q->waiter_head;
while (w) {
@@ -168,10 +159,6 @@ void queue_free(struct ll_queue* q) {
q->empty_callback_arg = NULL;
q->on_get = NULL;
q->on_get_arg = NULL;
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
if (q->head) DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] freeing nonempty queue: count=%d; entries remain caller-owned", q->name, q->count);
for (struct ll_entry* e = q->head; e; e = e->next) e->owner = NULL;
u_free(q);
}
@@ -207,19 +194,6 @@ void queue_resume_callback(struct ll_queue* q) {
}
}
// Общий запуск коллбэка автозабора: immediate (default) или deferred через uasync_call_soon.
// Deferred-режим очищает стек производителя перед обработкой первого элемента.
static void queue_trigger_callback(struct ll_queue* q) {
if (!q->head || q->callback_suspended || !q->callback) return;
if (q->callback_defer) {
if (!q->resume_timeout_id)
q->resume_timeout_id = uasync_call_soon(q->ua, q, queue_resume_timeout_cb);
} else {
q->callback(q, q->callback_arg);
}
}
void queue_set_size_limit(struct ll_queue* q, int lim) {
if (!q) return;
q->size_limit = lim;
@@ -329,42 +303,22 @@ static uint32_t make_hash(const void* data, uint16_t len) {// алгоритм F
return hash;
}
static void unlink_deferred_waiter(struct ll_queue* q, struct queue_waiter_handle* h) {
struct queue_waiter_handle** p = &q->deferred_waiters;
while (*p && *p != h) p = &(*p)->defer_next;
if (*p) *p = h->defer_next;
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter not found: handle=%p", q->name, h);
h->defer_next = NULL; h->defer_q = NULL;
}
static void waiter_defer_cb(void* arg) {
struct queue_waiter_handle* h = (struct queue_waiter_handle*)arg;
struct ll_queue* q = h->defer_q;
queue_threshold_callback_fn cb = h->defer_cb;
void* cb_arg = h->defer_arg;
h->call_soon_id = NULL;
unlink_deferred_waiter(q, h);
h->defer_cb = NULL; h->defer_arg = NULL;
if (cb) cb(q, cb_arg);
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter has no callback", q->name);
if (h->defer_cb) h->defer_cb(h->defer_q, h->defer_arg);
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "waiter_defer_cb: NULL callback (stale?) q=%p", (void*)h->defer_q);
}
static inline int queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg,
static inline void queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg,
struct queue_waiter_handle* h) {
if (q->waiter_defer) {
if (h->call_soon_id) queue_waiter_cancel(h->defer_q, h);
if (h->call_soon_id) { DEBUG_WARN(DEBUG_CATEGORY_SYS, "queue_waiter_call: overwriting existing call_soon_id=%p", h->call_soon_id); uasync_call_soon_cancel(q->ua, h->call_soon_id); }
h->defer_q = q; h->defer_cb = cb; h->defer_arg = arg;
h->call_soon_id = uasync_call_soon(q->ua, h, waiter_defer_cb);
if (h->call_soon_id) { h->defer_next = q->deferred_waiters; q->deferred_waiters = h; }
else {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter scheduling failed: handle=%p", q->name, h);
h->defer_q = NULL; h->defer_cb = NULL; h->defer_arg = NULL;
return -1;
}
} else {
cb(q, arg);
}
return 0;
}
// Проверить и запустить первый ожидающий коллбэк (FIFO / round-robin)
@@ -384,18 +338,13 @@ static void check_waiters(struct ll_queue* q) {
queue_threshold_callback_fn cb = waiter->callback;
void* arg = waiter->callback_arg;
u_free(waiter);
if (cb) {
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "check_waiters: waking head waiter, count=%d<=%d, bytes=%zu<=%zu",
q->count, q->threshold_max_packets, q->total_bytes, q->threshold_max_bytes);
if (queue_waiter_call(q, cb, arg, h) < 0) {
// Отказ планирования не теряет ожидание: следующий get/remove повторит попытку.
waiter->next = q->waiter_head; q->waiter_head = waiter;
if (!q->waiter_tail) q->waiter_tail = waiter;
h->internal = waiter;
return;
}
queue_waiter_call(q, cb, arg, h);
}
u_free(waiter);
}
int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
@@ -404,7 +353,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
queue_check_thread(q);
#endif
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put detected! entry=%p prev=%p next=%p len=%u",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next, entry->len);
abort();
@@ -418,7 +367,6 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
}
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry);
queue_entry_free(entry);
return -1;
@@ -430,18 +378,19 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
if (q->tail) q->tail->next = entry; else q->head = entry;
q->tail = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
// НЕ вызываем add_to_hash
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@@ -451,7 +400,7 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
queue_check_thread(q);
#endif
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_with_index detected! entry=%p prev=%p next=%p",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
abort();
@@ -467,7 +416,6 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size);
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry);
queue_entry_free(entry);
return -1;
@@ -479,18 +427,19 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
if (q->tail) q->tail->next = entry; else q->head = entry;
q->tail = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
add_to_hash(q, entry);
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@@ -500,7 +449,7 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) {
queue_check_thread(q);
#endif
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first detected! entry=%p prev=%p next=%p",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
abort();
@@ -513,25 +462,22 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) {
return -1;
}
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry); queue_entry_free(entry); return -1;
}
entry->next = q->head;
entry->prev = NULL;
if (q->head) q->head->prev = entry; else q->tail = entry;
q->head = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@@ -542,7 +488,7 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
queue_check_thread(q);
#endif
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first_with_index detected! entry=%p prev=%p next=%p",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
abort();
@@ -558,7 +504,6 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size);
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry);
queue_entry_free(entry);
return -1;
@@ -569,18 +514,19 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
if (q->head) q->head->prev = entry; else q->tail = entry;
q->head = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
add_to_hash(q, entry);
if (q->count == 1 && !q->callback_suspended && q->callback) {
q->callback(q, q->callback_arg);
}
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@@ -603,7 +549,6 @@ struct ll_entry* queue_data_get(struct ll_queue* q) {
entry->next = NULL;
entry->prev = NULL;
entry->owner = NULL;
remove_from_hash(q, entry);
@@ -612,21 +557,18 @@ struct ll_entry* queue_data_get(struct ll_queue* q) {
// Приостановить коллбэки для предотвращения рекурсии
q->callback_suspended = 1;
// Проверить ожидающие коллбэки
check_waiters(q);
if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id)
q->empty_call_soon_id = uasync_call_soon(q->ua, q, (timeout_callback_t)empty_trampoline);
if (q->on_get) {
q->on_get_pending++;
if (!q->on_get_call_soon_id) {
q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline);
if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending);
}
}
if (q->on_get)
uasync_call_soon(q->ua, q, (timeout_callback_t)on_get_trampoline);
#ifdef QUEUE_DEBUG
queue_check_consistency(q);// !!!! for debug
#endif
check_waiters(q);
return entry;
}
@@ -705,11 +647,6 @@ void queue_set_waiter_defer(struct ll_queue* q, int enable) {
q->waiter_defer = enable;
}
void queue_set_callback_defer(struct ll_queue* q, int enable) {
if (!q) return;
q->callback_defer = enable;
}
void queue_set_empty_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) {
if (!q) return;
void* old = q->empty_call_soon_id;
@@ -725,24 +662,18 @@ void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) {
if (!q) return;
q->on_get = cbk_fn;
q->on_get_arg = arg;
if (!cbk_fn) {
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
}
}
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
queue_threshold_callback_fn callback, void* arg) {
if (!q || !h || !callback) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "queue_waiter_wait: invalid arguments q=%p handle=%p callback=%p", q, h, (void*)callback);
return -1;
}
if (!q || !h) return -1;
if (h->internal || h->call_soon_id) queue_waiter_cancel(h->call_soon_id ? h->defer_q : q, h);
if (h->internal) queue_waiter_cancel(q, h);
if (q->count <= q->threshold_max_packets
&& (q->threshold_max_bytes == 0 || q->total_bytes <= q->threshold_max_bytes)) {
return queue_waiter_call(q, callback, arg, h) < 0 ? -1 : 1;
queue_waiter_call(q, callback, arg, h);
return 1;
}
struct queue_waiter* waiter = u_malloc(sizeof(struct queue_waiter));
@@ -763,18 +694,9 @@ int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
}
void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h) {
if (!q || !h) return;
/* Отложенный вызов (waiter_defer) может висеть и при internal==NULL:
* queue_waiter_wait по ветке «очередь свободна» планирует call_soon_id,
* не регистрируя waiter. Ранний выход по !internal оставлял бы висячий
* callback с arg=handle, который после teardown разыменовывал бы freed-память. */
if (h->call_soon_id) {
struct ll_queue* deferred_q = h->defer_q;
uasync_call_soon_cancel(deferred_q->ua, h->call_soon_id); h->call_soon_id = NULL;
unlink_deferred_waiter(deferred_q, h); h->defer_cb = NULL; h->defer_arg = NULL;
}
if (!h->internal) return;
if (!q || !h || !h->internal) return;
if (h->call_soon_id) { uasync_call_soon_cancel(q->ua, h->call_soon_id); h->call_soon_id = NULL; }
struct queue_waiter* waiter = h->internal;
struct queue_waiter* prev = NULL;
struct queue_waiter* curr = q->waiter_head;
@@ -832,10 +754,6 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q, const void* index_
int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
if (!q || !entry) return -1;
if (entry->owner != q) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] cannot remove foreign entry=%p owner=%p", q->name, entry, entry->owner);
return -1;
}
#ifdef QUEUE_THREAD_CHECK
queue_check_thread(q);
@@ -859,7 +777,6 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
entry->next = NULL;
entry->prev = NULL;
entry->owner = NULL;
remove_from_hash(q, entry);
@@ -867,8 +784,6 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
#ifdef QUEUE_DEBUG
queue_check_consistency(q);// !!!! for debug
#endif
if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id)
q->empty_call_soon_id = uasync_call_soon(q->ua, q, empty_trampoline);
check_waiters(q);
return 0;
}
+5 -25
View File
@@ -1,10 +1,6 @@
#ifndef LL_QUEUE_H
#define LL_QUEUE_H
/* Очередь одного uasync-потока; наличие pthread-заголовков не делает её межпоточной.
* put может синхронно вызвать consumer/waiter. Держать владельцев живыми до завершения callbacks,
* waiter handle обнулять целиком и отменять до освобождения; entry и dgram освобождаются отдельно. */
#ifdef __cplusplus
extern "C" {
#endif
@@ -65,7 +61,6 @@ struct queue_waiter_handle;
*/
struct ll_entry {
char* name;
struct ll_queue* owner; // Владелец элемента; NULL после извлечения
struct ll_entry* next; // Следующий элемент в очереди
struct ll_entry* prev; // Предыдущий элемент в очереди
uint16_t size; // Размер пользовательского буфера data[] (байт)
@@ -128,7 +123,6 @@ struct queue_waiter_handle {
queue_threshold_callback_fn defer_cb;
void* defer_arg;
void* call_soon_id; ///< хендл отложенного вызова (для отмены)
struct queue_waiter_handle* defer_next; ///< список отложенных уведомлений очереди
};
/**
@@ -152,11 +146,9 @@ struct ll_queue {
struct queue_waiter* waiter_head; // Голова списка ожидающих
struct queue_waiter* waiter_tail; // Хвост списка ожидающих
struct queue_waiter_handle* deferred_waiters; // Уже запланированные уведомления
int threshold_max_packets; // Общий порог: макс. кол-во элементов (по умолчанию 0)
size_t threshold_max_bytes; // Общий порог: макс. объём данных (0 = не проверять)
int waiter_defer; // 0=immediate callback, 1=defer via uasync_call_soon
int callback_defer; // 0=immediate callback из put (default), 1=defer via uasync_call_soon (очистка стека)
queue_callback_fn empty_callback; // одноразовый коллбэк при опустошении очереди
void* empty_callback_arg;
@@ -164,8 +156,6 @@ struct ll_queue {
queue_callback_fn on_get; // коллбэк после каждого queue_data_get (deferred)
void* on_get_arg;
void* on_get_call_soon_id;
size_t on_get_pending;
struct ll_entry** hash_table; // Хеш-таблица для поиска по id (если hash_size > 0)
size_t hash_size; // Размер хеш-таблицы
@@ -251,17 +241,6 @@ void queue_set_threshold(struct ll_queue* q, int max_packets, size_t max_bytes);
void queue_set_waiter_defer(struct ll_queue* q, int enable);
/**
* @brief Управляет способом вызова коллбэка автозабора при добавлении элемента.
* @param q очередь
* @param enable 0 — первый запуск коллбэка из queue_data_put* вызывается синхронно (по умолчанию);
* 1 — первый запуск откладывается через uasync_call_soon (очищает стек вызывающего).
*
* @note Влияет только на первый запуск из put. Повторные запуски (queue_resume_callback)
* всегда идут через uasync_call_soon.
*/
void queue_set_callback_defer(struct ll_queue* q, int enable);
/**
* @brief Устанавливает одноразовый коллбэк при опустошении очереди (count==0).
* @param q очередь
@@ -284,13 +263,12 @@ void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
/**
* @brief Регистрирует ожидание освобождения очереди до общего порога.
* @param q очередь
* @param h полностью обнулённый handle: `struct queue_waiter_handle h = {0}`
* @param h указатель на handle (создаётся как `struct queue_waiter_handle h = {0}` или `h.internal = NULL`)
* @param callback функция, вызываемая при достижении порога
* @param arg аргумент коллбэка
* @return 1 — условие выполнено (callback вызван или отложен), 0 — ожидает порог, -1 — ошибка
* @return 1 — условие уже выполнено (callback вызван немедленно), 0 — зарегистрирован в списке, -1 — ошибка
*
* Если handle уже ожидает или имеет отложенный callback — старое ожидание отменяется.
* При queue_free отменяются также уже запланированные уведомления.
* Если handle уже ожидает (h->internal != NULL) — старый waiter отменяется и создаётся новый.
*/
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
queue_threshold_callback_fn callback, void* arg);
@@ -310,6 +288,7 @@ void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h);
* @brief Добавляет элемент в конец очереди (FIFO).
* @param q очередь
* @param entry элемент
* @param id идентификатор для поиска
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
*/
int queue_data_put(struct ll_queue* q, struct ll_entry* entry);
@@ -318,6 +297,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry);
* @brief Добавляет элемент в начало очереди (LIFO, высокий приоритет).
* @param q очередь
* @param entry элемент
* @param id идентификатор
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
*/
int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry);
-1
View File
@@ -195,7 +195,6 @@ queue_set_empty_callback(q, on_empty, my_data);
| `queue_set_callback(q, cbk_fn, arg)` | Установить callback автозабора. Вызывается при наличии элементов. |
| `queue_data_get(q)` | Извлечь элемент из головы. Приостанавливает callback до `queue_resume_callback()`. |
| `queue_resume_callback(q)` | **Обязательно** вызвать после обработки элемента. Планирует отложенный вызов callback через uasync_call_soon. |
| `queue_set_callback_defer(q, enable)` | `enable=1` — первый запуск callback из `put` откладывается через uasync_call_soon (очистка стека). По умолчанию `0` (синхронно). Повторные запуски всегда deferred. |
### Запись
+4 -17
View File
@@ -18,9 +18,6 @@
#define PREV_OFFSET LOCATION_MAX
#define NEXT_OFFSET (LOCATION_MAX + POINTER_SIZE)
// Порог детекта аномально больших аллокаций (гигантские mmap-резервации, напр. VSZ ~4G)
#define LARGE_ALLOC_LOG_THRESHOLD (8u * 1024u * 1024u) // 8 МБ
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
#include <execinfo.h>
#include <pthread.h>
@@ -109,10 +106,9 @@ void u_check(void* ptr, const char* text, const char* location) {
error = true;
}
}
uint32_t post_canary;
memcpy(&post_canary, user + size, sizeof(post_canary));
if (post_canary != CANARY) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: overflow canary corrupted at %p (expected 0x%X, got 0x%X)", location, text, ptr, CANARY, post_canary);
uint32_t* post_canary = (uint32_t*)(user + size);
if (*post_canary != CANARY) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: overflow canary corrupted at %p (expected 0x%X, got 0x%X)", location, text, ptr, CANARY, *post_canary);
error = true;
}
if (error) {
@@ -125,10 +121,6 @@ void u_check(void* ptr, const char* text, const char* location) {
}
void* u_malloc_impl(uint32_t size, const char* location) {
if (size >= LARGE_ALLOC_LOG_THRESHOLD) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "LARGE ALLOC: %u bytes (%.1f MB) at %s",
size, (double)size / (1024.0 * 1024.0), location);
}
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
uint8_t* base = (uint8_t*)malloc(total);
if (!base && size > 0) return NULL;
@@ -142,8 +134,7 @@ void* u_malloc_impl(uint32_t size, const char* location) {
uint8_t* prefix_start = base + METADATA_SIZE;
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE) = CANARY;
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 4) = size;
const uint32_t post_canary = CANARY; /* размер пользователя не обязан быть кратен выравниванию uint32_t */
memcpy(prefix_start + BOUNDARY_CHECK_SIZE + 8 + size, &post_canary, sizeof(post_canary));
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 8 + size) = CANARY;
// Add to list (thread-safe)
LOCK();
void** prev_ptr = (void**)(base + PREV_OFFSET);
@@ -160,10 +151,6 @@ void* u_malloc_impl(uint32_t size, const char* location) {
}
void* u_calloc_impl(uint32_t nmemb, uint32_t size, const char* location) {
if (nmemb != 0 && size > UINT32_MAX / nmemb) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "CALLOC OVERFLOW: %u * %u (wraps uint32) at %s", nmemb, size, location);
return NULL;
}
uint32_t total_size = nmemb * size;
void* ptr = u_malloc_impl(total_size, location);
if (ptr) memset(ptr, 0, total_size);
+2 -5
View File
@@ -1,9 +1,6 @@
/**
* mem — выделение памяти с защитными полями, местом аллокации и учётом живых блоков.
* Использовать u_malloc/u_calloc/u_realloc/u_strdup и парный u_free; обычный free к этим блокам неприменим.
* Макросы передают LOCATION автоматически. OOM возвращает NULL; обнаруженное повреждение завершает процесс.
* realloc при ошибке сохраняет старый блок, при size=0 освобождает его.
* Отчёт u_report_unfreed_blocks вызывать после остановки потоков, меняющих список аллокаций.
* Memory management layer
* Provides wrappers for malloc/realloc/calloc/free with error handling
*/
#ifndef MEM_H
#define MEM_H
+14 -26
View File
@@ -5,59 +5,47 @@
#include "memory_pool.h"
#include "mem.h"
// Layout: [user_data] [canary: 4] [alloc_loc: pointer] [free_loc: pointer] [counter: 1]
// Layout: [user_data: object_size] [canary: 4 bytes] [alloc_loc: pointer] [counter: 1 byte]
#define POOL_CANARY_OFF (pool->object_size)
#define POOL_LOC_OFF (pool->object_size + 4)
#define POOL_FREE_LOC_OFF (pool->object_size + 4 + sizeof(const char*))
#define POOL_COUNTER_OFF (pool->object_size + 4 + 2 * sizeof(const char*))
#define POOL_ALLOC_SIZE (pool->object_size + 4 + 2 * sizeof(const char*) + 1)
#define POOL_COUNTER_OFF (pool->object_size + 4 + sizeof(const char*))
#define POOL_ALLOC_SIZE (pool->object_size + 4 + sizeof(const char*) + 1)
#define POOL_CANARY_VAL 0xDEADBEAF
static void pool_init_tags(struct memory_pool* pool, void* obj, const char* location)
{
const uint32_t canary = POOL_CANARY_VAL;
uint32_t* canary = (uint32_t*)((uint8_t*)obj + POOL_CANARY_OFF);
const char** loc = (const char**)((uint8_t*)obj + POOL_LOC_OFF);
uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF;
/* Метаданные следуют за payload произвольного размера и могут быть невыровненными. */
memcpy((uint8_t*)obj + POOL_CANARY_OFF, &canary, sizeof(canary));
memcpy((uint8_t*)obj + POOL_LOC_OFF, &location, sizeof(location));
const char* free_loc = NULL;
memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &free_loc, sizeof(free_loc));
*canary = POOL_CANARY_VAL;
*loc = location;
*counter = 1;
}
static void pool_check_and_clear_tags(struct memory_pool* pool, void* obj, const char* location)
{
uint32_t canary;
const char *loc, *free_loc;
memcpy(&canary, (uint8_t*)obj + POOL_CANARY_OFF, sizeof(canary));
memcpy(&loc, (uint8_t*)obj + POOL_LOC_OFF, sizeof(loc));
memcpy(&free_loc, (uint8_t*)obj + POOL_FREE_LOC_OFF, sizeof(free_loc));
uint32_t* canary = (uint32_t*)((uint8_t*)obj + POOL_CANARY_OFF);
const char** loc = (const char**)((uint8_t*)obj + POOL_LOC_OFF);
uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF;
if (canary != POOL_CANARY_VAL) {
if (*canary != POOL_CANARY_VAL) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free BUFFER OVERFLOW pool=%p name=%s sz=%zu allocs=%zu reuse=%zu free=%d",
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count, pool->free_count);
/* Повреждённый хвост может содержать битые указатели; их нельзя читать как строки. */
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc_loc_ptr=%p free_loc_ptr=%p free_attempt=%s canary=0x%08x expected=0x%08x",
obj, (void*)loc, (void*)free_loc, location, canary, POOL_CANARY_VAL);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc=%s free=%s canary=0x%08x expected=0x%08x",
obj, *loc ? *loc : "(null)", location, *canary, POOL_CANARY_VAL);
if (pool->object_size)
log_dump(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_SYS, " user_data", obj, pool->object_size);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj);
volatile int _halt = 1;
while (_halt) {}
}
if (*counter == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu obj=%p",
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu alloc=%s free=%s",
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count,
obj);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " alloc=%s first_free=%s repeated_free=%s",
loc ? loc : "(null)", free_loc ? free_loc : "(null)", location);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj);
*loc ? *loc : "(null)", location);
volatile int _halt = 1;
while (_halt) {}
}
*counter = 0;
memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &location, sizeof(location));
}
static size_t g_total_free = 0;
+1 -7
View File
@@ -1,9 +1,4 @@
/* memory_pool — повторное использование блоков фиксированного размера в одном потоке.
* init создаёт пустой пул; alloc выделяет блок по требованию или берёт свободный и обнуляет payload.
* free проверяет защитный хвост и возвращает не более MEMORY_POOL_MAX_FREE блоков в кеш.
* object_size должен вмещать void*: свободный блок хранит next в начале payload.
* Перед destroy вернуть все выданные блоки: destroy освобождает только кеш и сам пул.
* name заимствован до destroy; блок возвращать только своему пулу, внутренней синхронизации нет. */
// memory_pool.h
#ifndef MEMORY_POOL_H
#define MEMORY_POOL_H
@@ -45,7 +40,6 @@ size_t memory_pool_get_total_free_blocks(void);
#define memory_pool_alloc(pool) memory_pool_alloc_impl(pool, PLOCATION)
#define memory_pool_free(pool, obj) memory_pool_free_impl(pool, obj, PLOCATION)
/* Проверяет присутствие в кеше свободных блоков; не определяет состояние уже освобождённого через u_free блока. */
int memory_pool_is_freed(struct memory_pool* pool, void* obj);
+48 -206
View File
@@ -6780,9 +6780,7 @@ typedef enum
ma_device_notification_type_rerouted,
ma_device_notification_type_interruption_began,
ma_device_notification_type_interruption_ended,
ma_device_notification_type_unlocked,
ma_device_notification_type_capture_overrun, /* uTun: подтверждённый backend разрыв входного PCM. */
ma_device_notification_type_playback_underrun /* uTun: backend исчерпал буфер воспроизведения. */
ma_device_notification_type_unlocked
} ma_device_notification_type;
typedef struct
@@ -7545,13 +7543,11 @@ struct ma_context
ma_proc pa_context_disconnect;
ma_proc pa_context_set_state_callback;
ma_proc pa_context_get_state;
ma_proc pa_context_errno;
ma_proc pa_context_get_sink_info_list;
ma_proc pa_context_get_source_info_list;
ma_proc pa_context_get_sink_info_by_name;
ma_proc pa_context_get_source_info_by_name;
ma_proc pa_operation_unref;
ma_proc pa_operation_cancel;
ma_proc pa_operation_get_state;
ma_proc pa_channel_map_init_extend;
ma_proc pa_channel_map_valid;
@@ -7567,7 +7563,6 @@ struct ma_context
ma_proc pa_stream_get_buffer_attr;
ma_proc pa_stream_set_buffer_attr;
ma_proc pa_stream_get_device_name;
ma_proc pa_stream_get_latency;
ma_proc pa_stream_set_write_callback;
ma_proc pa_stream_set_read_callback;
ma_proc pa_stream_set_suspended_callback;
@@ -7579,7 +7574,6 @@ struct ma_context
ma_proc pa_stream_cork;
ma_proc pa_stream_trigger;
ma_proc pa_stream_begin_write;
ma_proc pa_stream_cancel_write;
ma_proc pa_stream_write;
ma_proc pa_stream_peek;
ma_proc pa_stream_drop;
@@ -11559,17 +11553,6 @@ IMPLEMENTATION
#ifndef miniaudio_c
#define miniaudio_c
/* uTun: необязательные наблюдатели, без изменения ABI и поведения аудиотракта. */
#ifndef MA_DIAGNOSTIC_EVENT
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) ((void)0)
#endif
#ifndef MA_DIAGNOSTIC_DEVICE_INIT
#define MA_DIAGNOSTIC_DEVICE_INIT(device) ((void)0)
#endif
#ifndef MA_DIAGNOSTIC_LOG
#define MA_DIAGNOSTIC_LOG(log, level, message) ((void)0)
#endif
#include <assert.h>
#include <limits.h> /* For INT_MAX */
#include <math.h> /* sin(), etc. */
@@ -13769,7 +13752,6 @@ MA_API ma_result ma_log_post(ma_log* pLog, ma_uint32 level, const char* pMessage
return MA_INVALID_ARGS;
}
MA_DIAGNOSTIC_LOG(pLog, level, pMessage);
ma_log_lock(pLog);
{
ma_uint32 iLog;
@@ -20218,14 +20200,6 @@ static void ma_device__on_notification(ma_device_notification notification)
{
MA_ASSERT(notification.pDevice != NULL);
if (notification.type == ma_device_notification_type_capture_overrun) {
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_BACKEND_OVERFLOW, AUDIO_DIAG_READ, 1, 0);
} else if (notification.type == ma_device_notification_type_playback_underrun) {
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_BACKEND_UNDERRUN, AUDIO_DIAG_WRITE, 1, 0);
} else {
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_NOTIFY, -1, notification.type, ma_device_get_state(notification.pDevice));
}
if (notification.pDevice->onNotification != NULL) {
notification.pDevice->onNotification(&notification);
}
@@ -20386,7 +20360,6 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut
{
float masterVolumeFactor;
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_CALLBACK, frameCount, (pFramesIn != NULL) | ((pFramesOut != NULL) << 1));
ma_device_get_master_volume(pDevice, &masterVolumeFactor); /* Use ma_device_get_master_volume() to ensure the volume is loaded atomically. */
if (pDevice->onData) {
@@ -20434,7 +20407,6 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut
ma_silence_pcm_frames(pFramesOut, frameCount, pDevice->playback.format, pDevice->playback.channels);
}
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_CALLBACK, frameCount, 0);
}
@@ -20618,7 +20590,6 @@ static ma_result ma_device__handle_duplex_callback_capture(ma_device* pDevice, m
if (framesToProcessInClientFormat == 0) {
if (ma_pcm_rb_pointer_distance(pRB) == (ma_int32)ma_pcm_rb_get_subbuffer_size(pRB)) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CAPTURE_DROP, AUDIO_DIAG_READ, framesToProcessInDeviceFormat, ma_pcm_rb_available_read(pRB));
break; /* Overrun. Not enough room in the ring buffer for input frame. Excess frames are dropped. */
}
}
@@ -20654,7 +20625,6 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
ma_result result;
ma_uint8 silentInputFrames[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
ma_uint32 totalFramesReadOut = 0;
ma_silence_pcm_frames(pFramesInInternalFormat, frameCount, pDevice->playback.internalFormat, pDevice->playback.internalChannels);
MA_ASSERT(pDevice != NULL);
MA_ASSERT(frameCount > 0);
@@ -20666,7 +20636,6 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
Sitting in the ring buffer should be captured data from the capture callback in external format. If there's not enough data in there for
the whole frameCount frames we just use silence instead for the input data.
*/
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_DUPLEX, frameCount, ma_pcm_rb_available_read(pRB));
MA_ZERO_MEMORY(silentInputFrames, sizeof(silentInputFrames));
while (totalFramesReadOut < frameCount && ma_device_is_started(pDevice)) {
@@ -20677,12 +20646,8 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
if (pDevice->playback.inputCacheRemaining > 0) {
ma_uint64 framesConvertedIn = pDevice->playback.inputCacheRemaining;
ma_uint64 framesConvertedOut = (frameCount - totalFramesReadOut);
result = ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut);
ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut);
if (result != MA_SUCCESS || (framesConvertedIn == 0 && framesConvertedOut == 0)) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
break;
}
pDevice->playback.inputCacheConsumed += framesConvertedIn;
pDevice->playback.inputCacheRemaining -= framesConvertedIn;
@@ -20697,34 +20662,30 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
inputFrameCount = (ma_uint32)pDevice->playback.inputCacheCap;
result = ma_pcm_rb_acquire_read(pRB, &inputFrameCount, &pInputFrames);
if (result != MA_SUCCESS) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
break;
}
if (inputFrameCount > 0) {
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount);
result = ma_pcm_rb_commit_read(pRB, inputFrameCount);
if (result != MA_SUCCESS) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
break;
if (result == MA_SUCCESS) {
if (inputFrameCount > 0) {
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount);
} else {
if (ma_pcm_rb_pointer_distance(pRB) == 0) {
break; /* Underrun. */
}
}
} else {
/* Отсутствие микрофонных кадров не останавливает рендер; синтетический ввод не сдвигает capture-ring. */
inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap,
sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels));
/* No capture data available. Feed in silence. */
inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap, sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels));
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, silentInputFrames, inputFrameCount);
}
pDevice->playback.inputCacheConsumed = 0;
pDevice->playback.inputCacheRemaining = inputFrameCount;
result = ma_pcm_rb_commit_read(pRB, inputFrameCount);
if (result != MA_SUCCESS) {
return result; /* Should never happen. */
}
}
}
if (totalFramesReadOut < frameCount) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_UNFILLED, AUDIO_DIAG_DUPLEX, frameCount - totalFramesReadOut, ma_pcm_rb_available_read(pRB));
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_DUPLEX, totalFramesReadOut, ma_pcm_rb_available_read(pRB));
return MA_SUCCESS;
}
@@ -24832,7 +24793,6 @@ static ma_result ma_device_read__wasapi(ma_device* pDevice, void* pFrames, ma_ui
/* Overrun detection. */
if ((flags & MA_AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) != 0) {
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
/* Glitched. Probably due to an overrun. */
/*
@@ -24963,7 +24923,6 @@ static ma_result ma_device_write__wasapi(ma_device* pDevice, const void* pFrames
{
ma_result result = MA_SUCCESS;
ma_uint32 totalFramesProcessed = 0;
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_WRITE, frameCount, pDevice->wasapi.mappedBufferPlaybackLen);
/* Keep writing to the device until it's stopped or we've consumed all of our input. */
while (ma_device_get_state(pDevice) == ma_device_state_started && totalFramesProcessed < frameCount) {
@@ -25050,7 +25009,6 @@ static ma_result ma_device_write__wasapi(ma_device* pDevice, const void* pFrames
*pFramesWritten = totalFramesProcessed;
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_WRITE, totalFramesProcessed, result);
return result;
}
@@ -29911,7 +29869,6 @@ static ma_result ma_device_read__alsa(ma_device* pDevice, void* pFramesOut, ma_u
continue; /* Try again. */
} else if (resultALSA == -EPIPE) {
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "EPIPE (read)\n");
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
/* Overrun. Recover and try again. If this fails we need to return an error. */
resultALSA = ((ma_snd_pcm_recover_proc)pDevice->pContext->alsa.snd_pcm_recover)((ma_snd_pcm_t*)pDevice->alsa.pPCMCapture, resultALSA, MA_TRUE);
@@ -29967,7 +29924,6 @@ static ma_result ma_device_write__alsa(ma_device* pDevice, const void* pFrames,
continue; /* Try again. */
} else if (resultALSA == -EPIPE) {
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "EPIPE (write)\n");
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_playback_underrun));
/* Underrun. Recover and try again. If this fails we need to return an error. */
resultALSA = ((ma_snd_pcm_recover_proc)pDevice->pContext->alsa.snd_pcm_recover)((ma_snd_pcm_t*)pDevice->alsa.pPCMPlayback, resultALSA, MA_TRUE); /* MA_TRUE=silent (don't print anything on error). */
@@ -30952,13 +30908,11 @@ typedef int (* ma_pa_context_connect_proc) (
typedef void (* ma_pa_context_disconnect_proc) (ma_pa_context* c);
typedef void (* ma_pa_context_set_state_callback_proc) (ma_pa_context* c, ma_pa_context_notify_cb_t cb, void* userdata);
typedef ma_pa_context_state_t (* ma_pa_context_get_state_proc) (const ma_pa_context* c);
typedef int (* ma_pa_context_errno_proc) (const ma_pa_context* c);
typedef ma_pa_operation* (* ma_pa_context_get_sink_info_list_proc) (ma_pa_context* c, ma_pa_sink_info_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_context_get_source_info_list_proc) (ma_pa_context* c, ma_pa_source_info_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_context_get_sink_info_by_name_proc) (ma_pa_context* c, const char* name, ma_pa_sink_info_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_context_get_source_info_by_name_proc)(ma_pa_context* c, const char* name, ma_pa_source_info_cb_t cb, void* userdata);
typedef void (* ma_pa_operation_unref_proc) (ma_pa_operation* o);
typedef void (* ma_pa_operation_cancel_proc) (ma_pa_operation* o);
typedef ma_pa_operation_state_t (* ma_pa_operation_get_state_proc) (const ma_pa_operation* o);
typedef ma_pa_channel_map* (* ma_pa_channel_map_init_extend_proc) (ma_pa_channel_map* m, unsigned channels, ma_pa_channel_map_def_t def);
typedef int (* ma_pa_channel_map_valid_proc) (const ma_pa_channel_map* m);
@@ -30973,7 +30927,6 @@ typedef const ma_pa_sample_spec* (* ma_pa_stream_get_sample_spec_proc) (
typedef const ma_pa_channel_map* (* ma_pa_stream_get_channel_map_proc) (ma_pa_stream* s);
typedef const ma_pa_buffer_attr* (* ma_pa_stream_get_buffer_attr_proc) (ma_pa_stream* s);
typedef ma_pa_operation* (* ma_pa_stream_set_buffer_attr_proc) (ma_pa_stream* s, const ma_pa_buffer_attr* attr, ma_pa_stream_success_cb_t cb, void* userdata);
typedef int (* ma_pa_stream_get_latency_proc)(const ma_pa_stream* s, ma_uint64* usec, int* negative);
typedef const char* (* ma_pa_stream_get_device_name_proc) (const ma_pa_stream* s);
typedef void (* ma_pa_stream_set_write_callback_proc) (ma_pa_stream* s, ma_pa_stream_request_cb_t cb, void* userdata);
typedef void (* ma_pa_stream_set_read_callback_proc) (ma_pa_stream* s, ma_pa_stream_request_cb_t cb, void* userdata);
@@ -30986,7 +30939,6 @@ typedef int (* ma_pa_stream_is_corked_proc) (
typedef ma_pa_operation* (* ma_pa_stream_cork_proc) (ma_pa_stream* s, int b, ma_pa_stream_success_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_stream_trigger_proc) (ma_pa_stream* s, ma_pa_stream_success_cb_t cb, void* userdata);
typedef int (* ma_pa_stream_begin_write_proc) (ma_pa_stream* s, void** data, size_t* nbytes);
typedef int (* ma_pa_stream_cancel_write_proc) (ma_pa_stream* s);
typedef int (* ma_pa_stream_write_proc) (ma_pa_stream* s, const void* data, size_t nbytes, ma_pa_free_cb_t free_cb, int64_t offset, ma_pa_seek_mode_t seek);
typedef int (* ma_pa_stream_peek_proc) (ma_pa_stream* s, const void** data, size_t* nbytes);
typedef int (* ma_pa_stream_drop_proc) (ma_pa_stream* s);
@@ -31174,49 +31126,23 @@ static ma_pa_channel_position_t ma_channel_position_to_pulse(ma_channel position
}
#endif
#ifndef MA_PULSE_WAIT_TIMEOUT_MS
#define MA_PULSE_WAIT_TIMEOUT_MS 1000
#endif
static ma_result ma_pulse_wait_step(ma_context* pContext, ma_ptr pMainLoop, ma_timer* pTimer, const char* operation)
{
int resultPA;
if (ma_timer_get_time_in_seconds(pTimer) * 1000 >= MA_PULSE_WAIT_TIMEOUT_MS) {
ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR,
"[PulseAudio] %s timed out after %d ms.\n", operation, MA_PULSE_WAIT_TIMEOUT_MS);
return MA_TIMEOUT;
}
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 0, NULL);
if (resultPA < 0) {
ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] %s iterate failed: %d.\n", operation, resultPA);
return ma_result_from_pulse(resultPA);
}
if (resultPA == 0) ma_sleep(1);
return MA_SUCCESS;
}
static ma_result ma_wait_for_operation__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_pa_operation* pOP)
{
ma_result result;
ma_timer timer;
int resultPA;
ma_pa_operation_state_t state;
MA_ASSERT(pContext != NULL);
MA_ASSERT(pOP != NULL);
ma_timer_init(&timer);
for (;;) {
state = ((ma_pa_operation_get_state_proc)pContext->pulse.pa_operation_get_state)(pOP);
if (state == MA_PA_OPERATION_DONE) return MA_SUCCESS;
if (state != MA_PA_OPERATION_RUNNING) {
ma_log_post(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] Operation cancelled.\n");
return MA_ERROR;
break; /* Done. */
}
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "wait");
if (result != MA_SUCCESS) {
((ma_pa_operation_cancel_proc)pContext->pulse.pa_operation_cancel)(pOP);
return result;
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
if (resultPA < 0) {
return ma_result_from_pulse(resultPA);
}
}
@@ -31239,11 +31165,9 @@ static ma_result ma_wait_for_operation_and_unref__pulse(ma_context* pContext, ma
static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pPulseContext)
{
ma_result result;
ma_timer timer;
int resultPA;
ma_pa_context_state_t state;
ma_timer_init(&timer);
for (;;) {
state = ((ma_pa_context_get_state_proc)pContext->pulse.pa_context_get_state)((ma_pa_context*)pPulseContext);
if (state == MA_PA_CONTEXT_READY) {
@@ -31255,8 +31179,10 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext,
return MA_ERROR;
}
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "context connect");
if (result != MA_SUCCESS) return result;
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
if (resultPA < 0) {
return ma_result_from_pulse(resultPA);
}
}
/* Should never get here. */
@@ -31265,11 +31191,9 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext,
static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pStream)
{
ma_result result;
ma_timer timer;
int resultPA;
ma_pa_stream_state_t state;
ma_timer_init(&timer);
for (;;) {
state = ((ma_pa_stream_get_state_proc)pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pStream);
if (state == MA_PA_STREAM_READY) {
@@ -31281,8 +31205,10 @@ static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, m
return MA_ERROR;
}
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "stream connect");
if (result != MA_SUCCESS) return result;
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
if (resultPA < 0) {
return ma_result_from_pulse(resultPA);
}
}
return MA_SUCCESS;
@@ -31456,13 +31382,10 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext
if (deviceType == ma_device_type_playback) {
ma_pa_sink_info sinkInfo;
MA_ZERO_OBJECT(&sinkInfo);
sinkInfo.index = (ma_uint32)-1;
result = ma_context_get_sink_info__pulse(pContext, NULL, &sinkInfo);
if (result != MA_SUCCESS) {
return result;
}
if (sinkInfo.index == (ma_uint32)-1) return MA_NO_DEVICE;
if (pIndex != NULL) {
*pIndex = sinkInfo.index;
@@ -31471,13 +31394,10 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext
if (deviceType == ma_device_type_capture) {
ma_pa_source_info sourceInfo;
MA_ZERO_OBJECT(&sourceInfo);
sourceInfo.index = (ma_uint32)-1;
result = ma_context_get_source_info__pulse(pContext, NULL, &sourceInfo);
if (result != MA_SUCCESS) {
return result;
}
if (sourceInfo.index == (ma_uint32)-1) return MA_NO_DEVICE;
if (pIndex != NULL) {
*pIndex = sourceInfo.index;
@@ -31631,7 +31551,7 @@ static void ma_context_get_device_info_sink_callback__pulse(ma_pa_context* pPuls
{
ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData;
if (endOfList != 0 || pInfo == NULL) {
if (endOfList > 0) {
return;
}
@@ -31668,7 +31588,7 @@ static void ma_context_get_device_info_source_callback__pulse(ma_pa_context* pPu
{
ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData;
if (endOfList != 0 || pInfo == NULL) {
if (endOfList > 0) {
return;
}
@@ -31720,7 +31640,6 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi
}
result = ma_context_get_default_device_index__pulse(pContext, deviceType, &callbackData.defaultDeviceIndex);
if (result != MA_SUCCESS) return result;
if (deviceType == ma_device_type_playback) {
pOP = ((ma_pa_context_get_sink_info_by_name_proc)pContext->pulse.pa_context_get_sink_info_by_name)((ma_pa_context*)(pContext->pulse.pPulseContext), pDeviceName, ma_context_get_device_info_sink_callback__pulse, &callbackData);
@@ -31729,8 +31648,7 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi
}
if (pOP != NULL) {
result = ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP);
if (result != MA_SUCCESS) goto done;
ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP);
} else {
result = MA_ERROR;
goto done;
@@ -31829,7 +31747,6 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
MA_ASSERT(bpf > 0);
frameCount = byteCount / bpf;
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_READ, frameCount, deviceState);
framesProcessed = 0;
while (ma_device_get_state(pDevice) == ma_device_state_started && framesProcessed < frameCount) {
@@ -31839,7 +31756,6 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
int pulseResult = ((ma_pa_stream_peek_proc)pDevice->pContext->pulse.pa_stream_peek)(pStream, &pMappedPCMFrames, &bytesMapped);
if (pulseResult < 0) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult);
break; /* Failed to map. Abort. */
}
@@ -31848,25 +31764,12 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
if (pMappedPCMFrames != NULL) {
ma_device_handle_backend_data_callback(pDevice, NULL, pMappedPCMFrames, framesMapped);
} else {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, framesMapped, 0);
ma_uint8 silence[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
ma_uint64 remaining = framesMapped;
ma_uint32 capacity = sizeof(silence) / bpf;
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
ma_silence_pcm_frames(silence, capacity, pDevice->capture.internalFormat, pDevice->capture.internalChannels);
while (remaining > 0 && ma_device_get_state(pDevice) == ma_device_state_started) {
ma_uint32 n = (ma_uint32)ma_min(remaining, capacity);
#ifdef MA_AUDIO_CAPTURE_HOLE
MA_AUDIO_CAPTURE_HOLE(framesMapped - remaining);
#endif
ma_device_handle_backend_data_callback(pDevice, NULL, silence, n);
remaining -= n;
}
/* It's a hole. */
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[PulseAudio] ma_device_on_read__pulse: Hole.\n");
}
pulseResult = ((ma_pa_stream_drop_proc)pDevice->pContext->pulse.pa_stream_drop)(pStream);
if (pulseResult < 0) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult);
break; /* Failed to drop the buffer. */
}
@@ -31877,7 +31780,6 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
break;
}
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_READ, framesProcessed, 0);
}
static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stream* pStream, ma_uint64* pFramesProcessed)
@@ -31896,7 +31798,6 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
deviceState = ma_device_get_state(pDevice);
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_MAP, 0, deviceState);
bytesMapped = ((ma_pa_stream_writable_size_proc)pDevice->pContext->pulse.pa_stream_writable_size)(pStream);
if (bytesMapped != (size_t)-1) {
if (bytesMapped > 0) {
@@ -31909,23 +31810,15 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
}
framesMapped = bytesMapped / bpf;
if (framesMapped == 0 || pMappedPCMFrames == NULL) {
((ma_pa_stream_cancel_write_proc)pDevice->pContext->pulse.pa_stream_cancel_write)(pStream);
result = MA_ERROR;
goto done;
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesMapped, 0);
if (deviceState == ma_device_state_started || deviceState == ma_device_state_starting) { /* Check for starting state just in case this is being used to do the initial fill. */
ma_device_handle_backend_data_callback(pDevice, pMappedPCMFrames, NULL, framesMapped);
} else {
/* Device is not started. Write silence. */
ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.internalFormat, pDevice->playback.internalChannels);
ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.format, pDevice->playback.channels);
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_SUBMIT, framesMapped, 0);
pulseResult = ((ma_pa_stream_write_proc)pDevice->pContext->pulse.pa_stream_write)(pStream, pMappedPCMFrames, bytesMapped, NULL, 0, MA_PA_SEEK_RELATIVE);
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_SUBMIT, framesMapped, pulseResult);
if (pulseResult < 0) {
result = ma_result_from_pulse(pulseResult);
goto done; /* Failed to write data to stream. */
@@ -31942,10 +31835,6 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
}
done:
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesProcessed, result);
if (result != MA_SUCCESS) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_WRITE, 1, result);
}
if (pFramesProcessed != NULL) {
*pFramesProcessed = framesProcessed;
}
@@ -31961,7 +31850,6 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
ma_uint64 framesProcessed;
ma_uint32 deviceState;
ma_result result;
ma_bool32 diagnosticNoProgress = MA_FALSE;
MA_ASSERT(pDevice != NULL);
@@ -31978,7 +31866,6 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
MA_ASSERT(bpf > 0);
frameCount = byteCount / bpf;
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_WRITE, frameCount, deviceState);
framesProcessed = 0;
while (framesProcessed < frameCount) {
@@ -31995,14 +31882,8 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
break;
}
if (framesProcessedThisIteration == 0 && !diagnosticNoProgress) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_NO_PROGRESS, AUDIO_DIAG_WRITE, 1, frameCount - framesProcessed);
diagnosticNoProgress = MA_TRUE;
}
if (framesProcessedThisIteration == 0) break;
framesProcessed += framesProcessedThisIteration;
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_WRITE, framesProcessed, 0);
}
static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData)
@@ -32021,10 +31902,10 @@ static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData
if (suspended == 1) {
ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Suspended.\n");
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_began));
ma_device__on_notification_stopped(pDevice);
} else {
ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Resumed.\n");
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_ended));
ma_device__on_notification_started(pDevice);
}
}
@@ -32163,7 +32044,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi
if (pDescriptorCapture->sampleRate != 0) {
ss.rate = pDescriptorCapture->sampleRate;
}
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY | MA_PA_STREAM_AUTO_TIMING_UPDATE | MA_PA_STREAM_INTERPOLATE_TIMING;
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY;
if (ma_format_from_pulse(ss.format) == ma_format_unknown) {
if (ma_is_little_endian()) {
@@ -32322,7 +32203,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi
ss.rate = pDescriptorPlayback->sampleRate;
}
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY | MA_PA_STREAM_AUTO_TIMING_UPDATE | MA_PA_STREAM_INTERPOLATE_TIMING;
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY;
if (ma_format_from_pulse(ss.format) == ma_format_unknown) {
if (ma_is_little_endian()) {
ss.format = MA_PA_SAMPLE_FLOAT32LE;
@@ -32494,10 +32375,6 @@ on_error1:
((ma_pa_stream_unref_proc)pDevice->pContext->pulse.pa_stream_unref)((ma_pa_stream*)pDevice->pulse.pStreamCapture);
}
on_error0:
((ma_pa_context_disconnect_proc)pDevice->pContext->pulse.pa_context_disconnect)((ma_pa_context*)pDevice->pulse.pPulseContext);
((ma_pa_context_unref_proc)pDevice->pContext->pulse.pa_context_unref)((ma_pa_context*)pDevice->pulse.pPulseContext);
((ma_pa_mainloop_free_proc)pDevice->pContext->pulse.pa_mainloop_free)((ma_pa_mainloop*)pDevice->pulse.pMainLoop);
MA_ZERO_OBJECT(&pDevice->pulse);
return result;
}
@@ -32627,24 +32504,8 @@ static ma_result ma_device_data_loop__pulse(ma_device* pDevice)
the callbacks deal with it.
*/
while (ma_device_get_state(pDevice) == ma_device_state_started) {
ma_pa_context_state_t contextState = ((ma_pa_context_get_state_proc)pDevice->pContext->pulse.pa_context_get_state)((ma_pa_context*)pDevice->pulse.pPulseContext);
ma_pa_stream_state_t captureState = pDevice->pulse.pStreamCapture ?
((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamCapture) : MA_PA_STREAM_READY;
ma_pa_stream_state_t playbackState = pDevice->pulse.pStreamPlayback ?
((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamPlayback) : MA_PA_STREAM_READY;
if (contextState == MA_PA_CONTEXT_FAILED || contextState == MA_PA_CONTEXT_TERMINATED ||
captureState == MA_PA_STREAM_FAILED || captureState == MA_PA_STREAM_TERMINATED ||
playbackState == MA_PA_STREAM_FAILED || playbackState == MA_PA_STREAM_TERMINATED) {
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR,
"[PulseAudio] Device failed context=%d capture=%d playback=%d errno=%d.\n", contextState, captureState,
playbackState, ((ma_pa_context_errno_proc)pDevice->pContext->pulse.pa_context_errno)((ma_pa_context*)pDevice->pulse.pPulseContext));
return MA_ERROR;
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_LOOP, 0, 0);
resultPA = ((ma_pa_mainloop_iterate_proc)pDevice->pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pDevice->pulse.pMainLoop, 1, NULL);
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_LOOP, 0, resultPA);
if (resultPA < 0) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_LOOP, 1, resultPA);
break;
}
}
@@ -32727,13 +32588,11 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_context_disconnect = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_disconnect");
pContext->pulse.pa_context_set_state_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_set_state_callback");
pContext->pulse.pa_context_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_state");
pContext->pulse.pa_context_errno = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_errno");
pContext->pulse.pa_context_get_sink_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_list");
pContext->pulse.pa_context_get_source_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_list");
pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_by_name");
pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_by_name");
pContext->pulse.pa_operation_unref = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_unref");
pContext->pulse.pa_operation_cancel = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_cancel");
pContext->pulse.pa_operation_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_get_state");
pContext->pulse.pa_channel_map_init_extend = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_init_extend");
pContext->pulse.pa_channel_map_valid = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_valid");
@@ -32748,7 +32607,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_get_channel_map = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_channel_map");
pContext->pulse.pa_stream_get_buffer_attr = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_buffer_attr");
pContext->pulse.pa_stream_set_buffer_attr = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_buffer_attr");
pContext->pulse.pa_stream_get_latency = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_latency");
pContext->pulse.pa_stream_get_device_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_device_name");
pContext->pulse.pa_stream_set_write_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_write_callback");
pContext->pulse.pa_stream_set_read_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_read_callback");
@@ -32761,7 +32619,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_cork = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cork");
pContext->pulse.pa_stream_trigger = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_trigger");
pContext->pulse.pa_stream_begin_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_begin_write");
pContext->pulse.pa_stream_cancel_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cancel_write");
pContext->pulse.pa_stream_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_write");
pContext->pulse.pa_stream_peek = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_peek");
pContext->pulse.pa_stream_drop = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_drop");
@@ -32794,13 +32651,11 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
ma_pa_context_disconnect_proc _pa_context_disconnect = pa_context_disconnect;
ma_pa_context_set_state_callback_proc _pa_context_set_state_callback = pa_context_set_state_callback;
ma_pa_context_get_state_proc _pa_context_get_state = pa_context_get_state;
ma_pa_context_errno_proc _pa_context_errno = pa_context_errno;
ma_pa_context_get_sink_info_list_proc _pa_context_get_sink_info_list = pa_context_get_sink_info_list;
ma_pa_context_get_source_info_list_proc _pa_context_get_source_info_list = pa_context_get_source_info_list;
ma_pa_context_get_sink_info_by_name_proc _pa_context_get_sink_info_by_name = pa_context_get_sink_info_by_name;
ma_pa_context_get_source_info_by_name_proc _pa_context_get_source_info_by_name= pa_context_get_source_info_by_name;
ma_pa_operation_unref_proc _pa_operation_unref = pa_operation_unref;
ma_pa_operation_cancel_proc _pa_operation_cancel = pa_operation_cancel;
ma_pa_operation_get_state_proc _pa_operation_get_state = pa_operation_get_state;
ma_pa_channel_map_init_extend_proc _pa_channel_map_init_extend = pa_channel_map_init_extend;
ma_pa_channel_map_valid_proc _pa_channel_map_valid = pa_channel_map_valid;
@@ -32815,7 +32670,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
ma_pa_stream_get_channel_map_proc _pa_stream_get_channel_map = pa_stream_get_channel_map;
ma_pa_stream_get_buffer_attr_proc _pa_stream_get_buffer_attr = pa_stream_get_buffer_attr;
ma_pa_stream_set_buffer_attr_proc _pa_stream_set_buffer_attr = pa_stream_set_buffer_attr;
ma_pa_stream_get_latency_proc _pa_stream_get_latency = pa_stream_get_latency;
ma_pa_stream_get_device_name_proc _pa_stream_get_device_name = pa_stream_get_device_name;
ma_pa_stream_set_write_callback_proc _pa_stream_set_write_callback = pa_stream_set_write_callback;
ma_pa_stream_set_read_callback_proc _pa_stream_set_read_callback = pa_stream_set_read_callback;
@@ -32828,7 +32682,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
ma_pa_stream_cork_proc _pa_stream_cork = pa_stream_cork;
ma_pa_stream_trigger_proc _pa_stream_trigger = pa_stream_trigger;
ma_pa_stream_begin_write_proc _pa_stream_begin_write = pa_stream_begin_write;
ma_pa_stream_cancel_write_proc _pa_stream_cancel_write = pa_stream_cancel_write;
ma_pa_stream_write_proc _pa_stream_write = pa_stream_write;
ma_pa_stream_peek_proc _pa_stream_peek = pa_stream_peek;
ma_pa_stream_drop_proc _pa_stream_drop = pa_stream_drop;
@@ -32860,13 +32713,11 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_context_disconnect = (ma_proc)_pa_context_disconnect;
pContext->pulse.pa_context_set_state_callback = (ma_proc)_pa_context_set_state_callback;
pContext->pulse.pa_context_get_state = (ma_proc)_pa_context_get_state;
pContext->pulse.pa_context_errno = (ma_proc)_pa_context_errno;
pContext->pulse.pa_context_get_sink_info_list = (ma_proc)_pa_context_get_sink_info_list;
pContext->pulse.pa_context_get_source_info_list = (ma_proc)_pa_context_get_source_info_list;
pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)_pa_context_get_sink_info_by_name;
pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)_pa_context_get_source_info_by_name;
pContext->pulse.pa_operation_unref = (ma_proc)_pa_operation_unref;
pContext->pulse.pa_operation_cancel = (ma_proc)_pa_operation_cancel;
pContext->pulse.pa_operation_get_state = (ma_proc)_pa_operation_get_state;
pContext->pulse.pa_channel_map_init_extend = (ma_proc)_pa_channel_map_init_extend;
pContext->pulse.pa_channel_map_valid = (ma_proc)_pa_channel_map_valid;
@@ -32881,7 +32732,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_get_channel_map = (ma_proc)_pa_stream_get_channel_map;
pContext->pulse.pa_stream_get_buffer_attr = (ma_proc)_pa_stream_get_buffer_attr;
pContext->pulse.pa_stream_set_buffer_attr = (ma_proc)_pa_stream_set_buffer_attr;
pContext->pulse.pa_stream_get_latency = (ma_proc)_pa_stream_get_latency;
pContext->pulse.pa_stream_get_device_name = (ma_proc)_pa_stream_get_device_name;
pContext->pulse.pa_stream_set_write_callback = (ma_proc)_pa_stream_set_write_callback;
pContext->pulse.pa_stream_set_read_callback = (ma_proc)_pa_stream_set_read_callback;
@@ -32894,7 +32744,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_cork = (ma_proc)_pa_stream_cork;
pContext->pulse.pa_stream_trigger = (ma_proc)_pa_stream_trigger;
pContext->pulse.pa_stream_begin_write = (ma_proc)_pa_stream_begin_write;
pContext->pulse.pa_stream_cancel_write = (ma_proc)_pa_stream_cancel_write;
pContext->pulse.pa_stream_write = (ma_proc)_pa_stream_write;
pContext->pulse.pa_stream_peek = (ma_proc)_pa_stream_peek;
pContext->pulse.pa_stream_drop = (ma_proc)_pa_stream_drop;
@@ -43918,7 +43767,6 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC
result = pContext->callbacks.onDeviceInit(pDevice, pConfig, &descriptorPlayback, &descriptorCapture);
if (result != MA_SUCCESS) {
ma_event_uninit(&pDevice->stopEvent);
ma_event_uninit(&pDevice->startEvent);
ma_event_uninit(&pDevice->wakeupEvent);
ma_mutex_uninit(&pDevice->startStopLock);
@@ -44160,8 +44008,6 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC
}
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_OPEN, -1, pDevice->type, pDevice->sampleRate);
MA_DIAGNOSTIC_DEVICE_INIT(pDevice);
MA_ASSERT(ma_device_get_state(pDevice) == ma_device_state_stopped);
return MA_SUCCESS;
}
@@ -44329,7 +44175,6 @@ MA_API void ma_device_uninit(ma_device* pDevice)
ma_free(pDevice->pContext, &allocationCallbacks);
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CLOSE, -1, 0, 0);
MA_ZERO_OBJECT(pDevice);
}
@@ -44661,14 +44506,6 @@ MA_API ma_result ma_device_handle_backend_data_callback(ma_device* pDevice, void
if (frameCount == 0) {
return MA_INVALID_ARGS;
}
if (((pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_loopback) && pInput == NULL) ||
(pDevice->type == ma_device_type_playback && pOutput == NULL)) {
return MA_INVALID_ARGS;
}
#ifdef MA_AUDIO_TIMING_BEGIN
MA_AUDIO_TIMING_BEGIN(pDevice, frameCount, pInput != NULL);
#endif
if (pDevice->type == ma_device_type_duplex) {
if (pInput != NULL) {
@@ -44680,17 +44517,22 @@ MA_API ma_result ma_device_handle_backend_data_callback(ma_device* pDevice, void
}
} else {
if (pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_loopback) {
if (pInput == NULL) {
return MA_INVALID_ARGS;
}
ma_device__send_frames_to_client(pDevice, frameCount, pInput);
}
if (pDevice->type == ma_device_type_playback) {
if (pOutput == NULL) {
return MA_INVALID_ARGS;
}
ma_device__read_frames_from_client(pDevice, frameCount, pOutput);
}
}
#ifdef MA_AUDIO_TIMING_END
MA_AUDIO_TIMING_END();
#endif
return MA_SUCCESS;
}
-17
View File
@@ -146,23 +146,6 @@ int opus_codec_encoder_signal_set(opus_codec_encoder_t* enc, int signal_type) {
return opus_encoder_ctl(enc->opus_enc, OPUS_SET_SIGNAL(signal_type));
}
int opus_codec_encoder_application_set(opus_codec_encoder_t* enc, int application) {
if (!enc || !enc->opus_enc) return -1;
return opus_encoder_ctl(enc->opus_enc, OPUS_SET_APPLICATION(application));
}
int opus_codec_encoder_delay(opus_codec_encoder_t* enc, int* samples) {
if (!enc || !samples) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Opus delay invalid arguments"); return -1; }
opus_int32 delay = 0;
int result = opus_encoder_ctl(enc->opus_enc, OPUS_GET_LOOKAHEAD(&delay));
if (result != OPUS_OK || delay < 0 || delay > enc->sample_rate) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Opus delay query failed result=%d samples=%d", result, (int)delay); return -1;
}
*samples = delay;
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "Opus capture delay samples=%d rate=%d", (int)delay, enc->sample_rate);
return 0;
}
const char* opus_codec_strerror(int err) {
switch (err) {
case OPUS_OK: return "OK";
-13
View File
@@ -1,9 +1,3 @@
/* opus_codec — владеющие обёртки encoder/decoder libopus для int16 interleaved PCM.
* create принимает частоту и 1/2 канала; неположительная частота → 48000, неверные каналы → моно.
* Encoder/decoder имеют состояние потока; каждому нужен один последовательный владелец.
* frame_samples — отсчёты НА КАНАЛ: PCM вмещает frame_samples*channels элементов.
* encode возвращает байты Opus, decode — отсчёты на канал; отрицательное значение означает ошибку.
* out_cap — байты; decode с data=NULL/len=0 использует PLC libopus. Освобождение — соответствующим destroy. */
#ifndef OPUS_CODEC_H
#define OPUS_CODEC_H
@@ -16,10 +10,6 @@ extern "C" {
typedef struct opus_codec_encoder opus_codec_encoder_t;
typedef struct opus_codec_decoder opus_codec_decoder_t;
/* Режим кодирования Opus: голос (VOIP, default) или музыка (AUDIO). */
#define OPUS_CODEC_APP_VOIP 2048 /* OPUS_APPLICATION_VOIP */
#define OPUS_CODEC_APP_AUDIO 2049 /* OPUS_APPLICATION_AUDIO */
opus_codec_encoder_t* opus_codec_encoder_create(int sample_rate, int channels);
void opus_codec_encoder_destroy(opus_codec_encoder_t* enc);
int opus_codec_encode(opus_codec_encoder_t* enc, const int16_t* pcm, int frame_samples,
@@ -33,9 +23,6 @@ int opus_codec_decode(opus_codec_decoder_t* dec, const uint8_t* data, int len,
int opus_codec_encoder_bitrate_set(opus_codec_encoder_t* enc, int bitrate_bps);
int opus_codec_encoder_complexity_set(opus_codec_encoder_t* enc, int complexity);
int opus_codec_encoder_signal_set(opus_codec_encoder_t* enc, int signal_type);
int opus_codec_encoder_application_set(opus_codec_encoder_t* enc, int application);
/* Задержка codec в сэмплах на канал; запрашивать после настройки encoder, не задавать константой. */
int opus_codec_encoder_delay(opus_codec_encoder_t* enc, int* samples);
const char* opus_codec_strerror(int err);
+22 -60
View File
@@ -39,25 +39,6 @@
#endif
#endif
/*
* Классифицирует IPv4-адрес (network byte order):
* 1 = глобальный/публичный (маршрутизируемый в интернете),
* 0 = приватный, CGNAT, loopback, link-local, multicast или зарезервированный.
*/
int ip_is_public(uint32_t addr_be) {
uint32_t a = ntohl(addr_be);
if ((a & 0xFF000000u) == 0x0A000000u) return 0; /* 10.0.0.0/8 */
if ((a & 0xFFF00000u) == 0xAC100000u) return 0; /* 172.16.0.0/12 */
if ((a & 0xFFFF0000u) == 0xC0A80000u) return 0; /* 192.168.0.0/16 */
if ((a & 0xFF000000u) == 0x7F000000u) return 0; /* 127.0.0.0/8 */
if ((a & 0xFFFF0000u) == 0xA9FE0000u) return 0; /* 169.254.0.0/16 */
if ((a & 0xFF000000u) == 0x00000000u) return 0; /* 0.0.0.0/8 */
if ((a & 0xF0000000u) == 0xE0000000u) return 0; /* 224.0.0.0/4 */
if ((a & 0xF0000000u) == 0xF0000000u) return 0; /* 240.0.0.0/4 */
if ((a & 0xFFC00000u) == 0x64400000u) return 0; /* 100.64.0.0/10 (CGNAT) */
return 1;
}
/*
* Generate cryptographically secure random bytes
* Returns 0 on success, -1 on error
@@ -79,12 +60,6 @@ int random_bytes(uint8_t *buffer, size_t len) {
#endif
}
/* Глобальный unicast IPv6 (2000::/3). При выборе адреса интерфейса предпочитаем его
* ULA (fc00::/7) и link-local (fe80::/10). */
static int ipv6_is_global_unicast(const uint8_t* a) {
return (a[0] & 0xe0) == 0x20;
}
#ifndef _WIN32
uint32_t get_interface_ip_by_index(uint32_t netif_index) {
struct ifaddrs *ifaddr, *ifa;
@@ -114,8 +89,7 @@ int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* ad
struct ifaddrs *ifaddr, *ifa;
if (getifaddrs(&ifaddr) == -1) return -1;
uint8_t global_match[16], local_match[16];
int has_global = 0, has_local = 0;
int found = 0;
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
if (ifa->ifa_addr == NULL) continue;
if (ifa->ifa_name == NULL) continue;
@@ -126,18 +100,20 @@ int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* ad
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)ifa->ifa_addr;
if ((sin6->sin6_addr.s6_addr[0] & 0xfe) == 0xfe && sin6->sin6_addr.s6_addr[1] == 0x80) continue;
int is_temporary = (ifa->ifa_flags & IFA_F_TEMPORARY) ? 1 : 0;
if ((temporary && !is_temporary) || (!temporary && is_temporary)) continue;
if (ipv6_is_global_unicast(sin6->sin6_addr.s6_addr)) {
if (!has_global) { memcpy(global_match, &sin6->sin6_addr, 16); has_global = 1; }
} else {
if (!has_local) { memcpy(local_match, &sin6->sin6_addr, 16); has_local = 1; }
if (temporary && is_temporary) {
memcpy(addr_out, &sin6->sin6_addr, 16);
found = 1;
break;
}
if (!temporary && !is_temporary) {
memcpy(addr_out, &sin6->sin6_addr, 16);
found = 1;
break;
}
}
freeifaddrs(ifaddr);
if (has_global) { memcpy(addr_out, global_match, 16); return 0; }
if (has_local) { memcpy(addr_out, local_match, 16); return 0; }
return -1;
return found ? 0 : -1;
}
#ifdef __linux__
@@ -175,8 +151,8 @@ int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t*
if (send(nl_sock, &req, req.nlh.nlmsg_len, 0) < 0) { close(nl_sock); return -1; }
uint8_t fallback[16], g_stable[16], g_temp[16], l_stable[16], l_temp[16];
int has_fallback = 0, has_g_stable = 0, has_g_temp = 0, has_l_stable = 0, has_l_temp = 0;
uint8_t fallback[16], stable_addr[16], temp_addr[16];
int has_fallback = 0, has_stable = 0, has_temp = 0;
char buf[8192];
int done = 0;
@@ -205,32 +181,19 @@ int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t*
int is_temp = (ifa_flags & IFA_F_TEMPORARY) != 0;
int is_mngtmp = (ifa_flags & IFA_F_MANAGETEMPADDR) != 0;
int is_global = ipv6_is_global_unicast(addr);
if (!has_fallback) { memcpy(fallback, addr, 16); has_fallback = 1; }
if (is_global) {
if (!has_g_stable && (!is_temp || is_mngtmp)) { memcpy(g_stable, addr, 16); has_g_stable = 1; }
if (!has_g_temp && is_temp && !is_mngtmp) { memcpy(g_temp, addr, 16); has_g_temp = 1; }
} else {
if (!has_l_stable && (!is_temp || is_mngtmp)) { memcpy(l_stable, addr, 16); has_l_stable = 1; }
if (!has_l_temp && is_temp && !is_mngtmp) { memcpy(l_temp, addr, 16); has_l_temp = 1; }
}
if (!has_stable && (!is_temp || is_mngtmp)) { memcpy(stable_addr, addr, 16); has_stable = 1; }
if (!has_temp && is_temp && !is_mngtmp) { memcpy(temp_addr, addr, 16); has_temp = 1; }
}
}
close(nl_sock);
if (prefer_stable) {
if (has_g_stable) { memcpy(addr_out, g_stable, 16); return 0; }
if (has_g_temp) { memcpy(addr_out, g_temp, 16); return 0; }
if (has_l_stable) { memcpy(addr_out, l_stable, 16); return 0; }
if (has_l_temp) { memcpy(addr_out, l_temp, 16); return 0; }
} else {
if (has_g_temp) { memcpy(addr_out, g_temp, 16); return 0; }
if (has_g_stable) { memcpy(addr_out, g_stable, 16); return 0; }
if (has_l_temp) { memcpy(addr_out, l_temp, 16); return 0; }
if (has_l_stable) { memcpy(addr_out, l_stable, 16); return 0; }
}
if (prefer_stable && has_stable) { memcpy(addr_out, stable_addr, 16); return 0; }
if (!prefer_stable && has_temp) { memcpy(addr_out, temp_addr, 16); return 0; }
if (has_stable) { memcpy(addr_out, stable_addr, 16); return 0; }
if (has_temp) { memcpy(addr_out, temp_addr, 16); return 0; }
if (has_fallback) { memcpy(addr_out, fallback, 16); return 0; }
return -1;
}
@@ -372,7 +335,6 @@ int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t*
PIP_ADAPTER_ADDRESSES adapters = plat_win_get_adapters(AF_INET6);
if (!adapters) return -1;
int found = 0;
uint8_t local_match[16]; int has_local = 0;
PIP_ADAPTER_ADDRESSES a = plat_win_find_adapter(adapters, netif_index);
if (a) {
for (PIP_ADAPTER_UNICAST_ADDRESS u = a->FirstUnicastAddress; u; u = u->Next) {
@@ -382,12 +344,12 @@ int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t*
{ static const uint8_t lb[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1};
if (memcmp(b, lb, 16) == 0) continue; } /* loopback */
{ static const uint8_t z[16]; if (memcmp(b, z, 16) == 0) continue; } /* :: */
if (ipv6_is_global_unicast(b)) { memcpy(addr_out, b, 16); found = 1; break; }
if (!has_local) { memcpy(local_match, b, 16); has_local = 1; }
memcpy(addr_out, b, 16);
found = 1;
break;
}
}
free(adapters);
if (!found && has_local) { memcpy(addr_out, local_match, 16); found = 1; }
return found ? 0 : -1;
}
+1 -8
View File
@@ -1,7 +1,5 @@
/**
* platform_compat — общие заголовки POSIX/Windows и адаптеры отсутствующих POSIX-функций.
* Также предоставляет криптографический random_bytes, адреса интерфейсов, выбор default route
* и классификацию публичных IPv4. Сокетные типы/ошибки и nonblocking API — в socket_compat.h.
* Platform compatibility layer for POSIX functions on Windows
*/
#ifndef PLATFORM_COMPAT_H
@@ -203,11 +201,6 @@ int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t*
// family: AF_INET or AF_INET6, returns ifindex (0 on failure)
uint32_t get_default_route_netif_index(int family);
// Классифицирует IPv4-адрес (network byte order):
// 1 = глобальный/публичный (маршрутизируемый в интернете),
// 0 = приватный, CGNAT, loopback, link-local, multicast или зарезервированный.
int ip_is_public(uint32_t addr_be);
// Cross-platform mkdir
#ifdef _WIN32
-249
View File
@@ -1,249 +0,0 @@
#include <string.h>
#include "mem.h"
#include "debug_config.h"
#include "reed_solomon.h"
struct rs_codec {
unsigned n;
unsigned k;
size_t max_block_size;
uint8_t gf_log[256];
uint8_t gf_exp[510];
uint8_t *generator;
uint8_t *work;
uint8_t storage[];
};
/* Умножение в GF(256) с примитивным полиномом x^8+x^4+x^3+x^2+1 (0x11d). */
static uint8_t rs_multiply(const struct rs_codec *ctx, uint8_t a, uint8_t b) {
return a && b ? ctx->gf_exp[ctx->gf_log[a] + ctx->gf_log[b]] : 0;
}
/* Обратить k строк матрицы; результат — правая половина рабочей матрицы [A|I]. */
static int rs_invert(struct rs_codec *ctx, const uint8_t *const rows[]) {
unsigned k = ctx->k;
unsigned stride = 2 * k;
for (unsigned i = 0; i < k; i++) {
uint8_t *row = ctx->work + i * stride;
memcpy(row, rows[i], k);
memset(row + k, 0, k);
row[k + i] = 1;
}
for (unsigned col = 0; col < k; col++) {
unsigned pivot = col;
while (pivot < k && !ctx->work[pivot * stride + col]) pivot++;
if (pivot == k) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: singular matrix n=%u k=%u column=%u", ctx->n, k, col);
return RS_ERR_MATRIX;
}
uint8_t *row = ctx->work + col * stride;
if (pivot != col) {
uint8_t *other = ctx->work + pivot * stride;
for (unsigned j = 0; j < stride; j++) {
uint8_t tmp = row[j];
row[j] = other[j];
other[j] = tmp;
}
}
uint8_t inverse = ctx->gf_exp[255 - ctx->gf_log[row[col]]];
for (unsigned j = col; j < stride; j++) row[j] = rs_multiply(ctx, row[j], inverse);
for (unsigned i = 0; i < k; i++) {
if (i == col) continue;
uint8_t *other = ctx->work + i * stride;
uint8_t factor = other[col];
if (!factor) continue;
for (unsigned j = col; j < stride; j++) other[j] ^= rs_multiply(ctx, factor, row[j]);
}
}
return RS_OK;
}
/* Построить систематическую матрицу G=V*inverse(V[0:k]) в точках 2^i поля GF(256). */
static int rs_build_generator(struct rs_codec *ctx) {
unsigned k = ctx->k;
if (ctx->n != k) {
const uint8_t *rows[RS_MAX_BLOCKS];
for (unsigned i = 0; i < ctx->n; i++) {
uint8_t value = 1;
for (unsigned j = 0; j < k; j++) {
ctx->generator[i * k + j] = value;
value = rs_multiply(ctx, value, ctx->gf_exp[i]);
}
}
for (unsigned i = 0; i < k; i++) rows[i] = ctx->generator + i * k;
int rc = rs_invert(ctx, rows);
if (rc != RS_OK) return rc;
for (unsigned i = k; i < ctx->n; i++) {
uint8_t result[RS_MAX_BLOCKS];
for (unsigned j = 0; j < k; j++) {
uint8_t value = 0;
for (unsigned t = 0; t < k; t++) {
value ^= rs_multiply(ctx, ctx->generator[i * k + t], ctx->work[t * 2 * k + k + j]);
}
result[j] = value;
}
memcpy(ctx->generator + i * k, result, k);
}
}
memset(ctx->generator, 0, k * k);
for (unsigned i = 0; i < k; i++) ctx->generator[i * k + i] = 1;
return RS_OK;
}
/* Инициализация параметров, таблиц поля и матрицы; все рабочие данные в одной аллокации. */
int rs_init(struct rs_codec **out, unsigned n, unsigned r, size_t block_size) {
if (out) *out = NULL;
if (!out || !n || n > RS_MAX_BLOCKS || r >= n || block_size <= RS_HEADER_SIZE || block_size > RS_MAX_BLOCK_SIZE) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid init ctx=%p n=%u r=%u block_size=%zu", (void *)out, n, r, block_size);
return RS_ERR_INVALID_ARG;
}
unsigned k = n - r;
size_t bytes = sizeof(struct rs_codec) + n * k + 2 * k * k;
struct rs_codec *ctx = u_calloc(1, (uint32_t)bytes);
if (!ctx) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: allocation failed n=%u r=%u block_size=%zu bytes=%zu", n, r, block_size, bytes);
return RS_ERR_MEMORY;
}
ctx->n = n;
ctx->k = k;
ctx->max_block_size = block_size;
ctx->generator = ctx->storage;
ctx->work = ctx->storage + n * k;
unsigned value = 1;
for (unsigned i = 0; i < 255; i++) {
ctx->gf_exp[i] = (uint8_t)value;
ctx->gf_log[value] = (uint8_t)i;
value <<= 1;
if (value & 256) value ^= 0x11d;
}
for (unsigned i = 255; i < 510; i++) ctx->gf_exp[i] = ctx->gf_exp[i - 255];
int rc = rs_build_generator(ctx);
if (rc != RS_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: generator failed n=%u k=%u rc=%d", n, k, rc);
u_free(ctx);
return rc;
}
*out = ctx;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "reed_solomon: initialized n=%u r=%u max_block_size=%zu input_capacity=%zu memory=%zu",
n, r, block_size, k * (block_size - RS_HEADER_SIZE), bytes);
return RS_OK;
}
void rs_free(struct rs_codec *ctx) {
if (!ctx) return;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "reed_solomon: free n=%u r=%u max_block_size=%zu", ctx->n, ctx->n - ctx->k, ctx->max_block_size);
u_free(ctx);
}
size_t rs_input_capacity(const struct rs_codec *ctx) {
if (!ctx) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: input_capacity requires a context");
return 0;
}
return ctx->k * (ctx->max_block_size - RS_HEADER_SIZE);
}
/* Записать заголовки и исходные субблоки, затем вычислить r проверочных субблоков. */
int rs_encode(struct rs_codec *ctx, uint64_t seq, const uint8_t *input, size_t length,
uint8_t *const blocks[], size_t *block_size) {
if (!ctx || !input || !blocks || !block_size || !length || length > rs_input_capacity(ctx)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid encode ctx=%p input=%p blocks=%p length=%zu size_out=%p",
(void *)ctx, (const void *)input, (const void *)blocks, length, (void *)block_size);
return RS_ERR_INVALID_ARG;
}
size_t payload_size = (length + ctx->k - 1) / ctx->k;
for (unsigned i = 0; i < ctx->n; i++) {
if (!blocks[i]) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: missing encode buffer seq=%llu subblock=%u", (unsigned long long)seq, i);
return RS_ERR_INVALID_ARG;
}
}
for (unsigned i = 0; i < ctx->n; i++) {
for (unsigned j = 0; j < 8; j++) blocks[i][j] = (uint8_t)(seq >> (56 - 8 * j));
blocks[i][8] = (uint8_t)i;
uint8_t *payload = blocks[i] + RS_HEADER_SIZE;
memset(payload, 0, payload_size);
if (i < ctx->k) {
size_t offset = i * payload_size;
size_t take = offset < length ? length - offset : 0;
if (take > payload_size) take = payload_size;
if (take) memcpy(payload, input + offset, take);
continue;
}
for (unsigned j = 0; j < ctx->k; j++) {
uint8_t factor = ctx->generator[i * ctx->k + j];
if (!factor) continue;
const uint8_t *source = blocks[j] + RS_HEADER_SIZE;
for (size_t b = 0; b < payload_size; b++) payload[b] ^= rs_multiply(ctx, factor, source[b]);
}
}
*block_size = RS_HEADER_SIZE + payload_size;
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "reed_solomon: encoded seq=%llu n=%u r=%u length=%zu padding=%zu block_size=%zu max=%zu",
(unsigned long long)seq, ctx->n, ctx->n - ctx->k, length, ctx->k * payload_size - length,
*block_size, ctx->max_block_size);
return RS_OK;
}
/* Проверить всю пачку и восстановить только отсутствующие исходные субблоки. */
int rs_decode(struct rs_codec *ctx, const uint8_t *const blocks[], size_t block_size,
uint8_t *output, size_t output_capacity) {
if (!ctx || !blocks || !output || block_size <= RS_HEADER_SIZE || block_size > ctx->max_block_size
|| output_capacity < ctx->k * (block_size - RS_HEADER_SIZE)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid decode ctx=%p blocks=%p output=%p block_size=%zu capacity=%zu",
(void *)ctx, (const void *)blocks, (void *)output, block_size, output_capacity);
return RS_ERR_INVALID_ARG;
}
size_t payload_size = block_size - RS_HEADER_SIZE;
const uint8_t *rows[RS_MAX_BLOCKS];
unsigned selected[RS_MAX_BLOCKS];
unsigned count = 0;
unsigned missing = 0;
uint64_t seq = 0;
for (unsigned i = 0; i < ctx->n; i++) {
if (!blocks[i]) {
if (i < ctx->k) missing++;
continue;
}
uint64_t block_seq = 0;
for (unsigned j = 0; j < 8; j++) block_seq = (block_seq << 8) | blocks[i][j];
if (blocks[i][8] != i || (count && block_seq != seq)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid header slot=%u subblock=%u seq=%llu expected_seq=%llu",
i, blocks[i][8], (unsigned long long)block_seq, (unsigned long long)(count ? seq : block_seq));
return RS_ERR_HEADER;
}
seq = block_seq;
if (count < ctx->k) {
selected[count] = i;
rows[count] = ctx->generator + i * ctx->k;
}
count++;
}
if (count < ctx->k) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: insufficient blocks seq=%llu received=%u required=%u n=%u",
(unsigned long long)seq, count, ctx->k, ctx->n);
return RS_ERR_INSUFFICIENT;
}
if (missing) {
int rc = rs_invert(ctx, rows);
if (rc != RS_OK) return rc;
}
for (unsigned i = 0; i < ctx->k; i++) {
uint8_t *target = output + i * payload_size;
if (blocks[i]) {
memcpy(target, blocks[i] + RS_HEADER_SIZE, payload_size);
continue;
}
memset(target, 0, payload_size);
for (unsigned j = 0; j < ctx->k; j++) {
uint8_t factor = ctx->work[i * 2 * ctx->k + ctx->k + j];
if (!factor) continue;
const uint8_t *source = blocks[selected[j]] + RS_HEADER_SIZE;
for (size_t b = 0; b < payload_size; b++) target[b] ^= rs_multiply(ctx, factor, source[b]);
}
DEBUG_TRACE(DEBUG_CATEGORY_GENERAL, "reed_solomon: recovered seq=%llu subblock=%u", (unsigned long long)seq, i);
}
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "reed_solomon: decoded seq=%llu received=%u recovered=%u output_size=%zu",
(unsigned long long)seq, count, missing, ctx->k * payload_size);
return RS_OK;
}
-73
View File
@@ -1,73 +0,0 @@
/**
* Reed–Solomon: кодирование и восстановление одной пачки по известным пропускам.
* rs_init задаёт n кодограмм, r проверочных и максимальный размер каждой кодограммы.
* Первые n-r кодограмм содержат исходные данные, остальные — проверочные.
* Формат: seq (8 байт, big-endian), subblock (1 байт), полезные данные.
* blocks[i] всегда соответствует subblock=i; при decode NULL обозначает пропуск.
* Любые n-r присутствующих кодограмм восстанавливают всю исходную пачку.
* Полученные данные и заголовки должны быть проверены транспортом на повреждения.
* Все входные/выходные буферы принадлежат вызывающему коду и не перекрываются.
* Кодек выделяет рабочую память только при init; один контекст используют последовательно.
* encode принимает действительную длину, выбирает минимальный одинаковый размер частей
* и дополняет хвост максимум n-r-1 нулями. Размер и исходную длину хранит вызывающий код.
*/
#ifndef REED_SOLOMON_H
#define REED_SOLOMON_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define RS_HEADER_SIZE 9
#define RS_MAX_BLOCK_SIZE 4096
#define RS_MAX_BLOCKS 255
enum rs_result {
RS_OK = 0,
RS_ERR_INVALID_ARG = -1,
RS_ERR_MEMORY = -2,
RS_ERR_INSUFFICIENT = -3,
RS_ERR_HEADER = -4,
RS_ERR_MATRIX = -5
};
struct rs_codec;
/* Создать кодек: 1 <= n <= 255, 0 <= r < n, 10 <= max_block_size <= 4096.
* ctx — выходной указатель; при ошибке *ctx=NULL. Старый контекст сначала освобождают.
* Возвращает RS_OK или отрицательный rs_result.
*/
int rs_init(struct rs_codec **ctx, unsigned n, unsigned r, size_t max_block_size);
/* Освободить контекст и рабочую память; NULL допустим. */
void rs_free(struct rs_codec *ctx);
/* Максимальная длина входа encode: (n-r)*(max_block_size-RS_HEADER_SIZE).
* Для NULL возвращает 0 и логирует ошибку.
*/
size_t rs_input_capacity(const struct rs_codec *ctx);
/* Читать 0 < length <= rs_input_capacity(ctx) байт input, записать n кодограмм.
* blocks — n ненулевых указателей на буферы ёмкостью max_block_size каждый.
* block_size получает RS_HEADER_SIZE+ceil(length/(n-r)); хвост за этим размером не меняется.
* При ошибке аргументов выходные буферы и block_size не изменяются.
*/
int rs_encode(struct rs_codec *ctx, uint64_t seq, const uint8_t *input, size_t length,
uint8_t *const blocks[], size_t *block_size);
/* Восстановить (n-r)*(block_size-RS_HEADER_SIZE) байт, включая нулевой хвост.
* Ненулевые буферы содержат block_size байт; NULL обозначает пропуск. Размер общий для пачки.
* output_capacity проверяется до записи; block_size не превышает максимум контекста.
* Проверяет одинаковый seq и subblock=i у всех присутствующих кодограмм.
* Входные буферы не изменяются. При ошибке output не изменяется.
*/
int rs_decode(struct rs_codec *ctx, const uint8_t *const blocks[], size_t block_size,
uint8_t *output, size_t output_capacity);
#ifdef __cplusplus
}
#endif
#endif
+4 -7
View File
@@ -22,20 +22,17 @@
* • Все динамические данные (строки, массивы, списки) выделяются через u_malloc.
* • При encode длина переменных полей кодируется всегда 2 байтами (uint16_t, big-endian).
* • Для linked list в буфере сохраняется только количество узлов + сырые данные узлов
* (включая байты next/padding; decode заменяет next восстановленными ссылками).
* (next-указатели НЕ сериализуются, они восстанавливаются при decode).
* • serialize_decode принимает буфер БЕЗ заголовка.
* • serialize_free освобождает ВСЮ структуру и все вложенные динамические объекты.
* • Поля кодируются в порядке schema.fields; фиксированные данные копируются без смены endian.
* • max_size (если > 0) — лимит encode; header при header_len > 0 обязателен.
* • Переменные counts ограничены 65535; elem_size=1 добавляет NUL и для byte-массивов.
* • Linked nodes копируются целиком, без рекурсивного кодирования вложенных указателей.
* • Этот формат зависит от ABI сырых полей; сериализация не нормализует размер/выравнивание структур.
* • Поля в schema.fields должны идти в порядке возрастания offset.
* • max_size (если > 0) — жёсткий лимит размера выходного буфера.
*
* @example
* // 1. Описание структуры
* typedef struct Node {
* uint32_t value;
* struct Node* next; // положение задаётся offsetof(Node, next)
* struct Node* next; // next по offset = 4
* } Node;
*
* typedef struct {
-311
View File
@@ -1,311 +0,0 @@
#include "silero_vad.h"
#include "debug_config.h"
#include "mem.h"
#ifdef HAVE_SILERO_VAD
#include <stdio.h>
#include <string.h>
#include <onnxruntime/onnxruntime_c_api.h>
#include "silero_vad_model.inc"
/* Число float-элементов рекуррентного состояния модели: 2 * 1 * 128 */
#define SILERO_VAD_STATE_COUNT 256
/* ONNX-вход: 64 отсчёта предыдущего окна + 512 новых отсчётов @16кГц. */
#define SILERO_VAD_CONTEXT_SAMPLES 64
#define SILERO_VAD_INPUT_SAMPLES (SILERO_VAD_CONTEXT_SAMPLES + SILERO_VAD_WINDOW_SAMPLES)
struct silero_vad {
const OrtApi* ort;
OrtEnv* env;
OrtSession* session;
OrtMemoryInfo* mem_info;
float state[SILERO_VAD_STATE_COUNT]; /* рекуррентное состояние между вызовами */
float context[SILERO_VAD_CONTEXT_SAMPLES]; /* звуковой контекст предыдущего окна */
uint8_t* model_data; /* байты .onnx, загруженные в память (для file-версии) */
size_t model_len;
};
/* Освободить OrtStatus и залогировать его сообщение как ошибку */
static void vad_report_ort_error(const OrtApi* ort, OrtStatus* status, const char* what) {
const char* msg = ort->GetErrorMessage(status);
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "%s: %s", what, msg ? msg : "unknown onnxruntime error");
ort->ReleaseStatus(status);
}
/* Прочитать файл целиком в буфер. 0 при успехе: буфер пишется в out/out_len (u_malloc). */
static int vad_read_file(const char* path, uint8_t** out, size_t* out_len) {
FILE* f;
long size;
uint8_t* buf;
*out = NULL;
*out_len = 0;
f = fopen(path, "rb");
if (!f) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: cannot open model '%s'", path);
return -1;
}
if (fseek(f, 0, SEEK_END) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: fseek failed on '%s'", path);
fclose(f);
return -1;
}
size = ftell(f);
if (size <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: empty or unreadable model '%s'", path);
fclose(f);
return -1;
}
rewind(f);
buf = (uint8_t*)u_malloc((size_t)size);
if (!buf) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: OOM for %ld bytes", size);
fclose(f);
return -1;
}
if (fread(buf, 1, (size_t)size, f) != (size_t)size) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: short read on '%s'", path);
u_free(buf);
fclose(f);
return -1;
}
fclose(f);
*out = buf;
*out_len = (size_t)size;
return 0;
}
/* Общая инициализация детектора из байтов модели (файл или вшитая модель).
* data может указывать на внешний буфер (вшитая модель) — тогда не освобождается. */
static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t len,
const uint8_t* owned_buf, const char* label) {
silero_vad_t* vad = NULL;
const OrtApi* ort;
OrtStatus* status;
OrtSessionOptions* opts = NULL;
if (!data || len == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): empty model", label);
return NULL;
}
ort = OrtGetApiBase() ? OrtGetApiBase()->GetApi(ORT_API_VERSION) : NULL;
if (!ort) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): onnxruntime C API unavailable", label);
return NULL;
}
vad = (silero_vad_t*)u_calloc(1, sizeof(*vad));
if (!vad) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): OOM", label);
return NULL;
}
vad->ort = ort;
vad->model_data = (uint8_t*)owned_buf;
vad->model_len = len;
status = ort->CreateEnv(ORT_LOGGING_LEVEL_WARNING, "utun_silero_vad", &vad->env);
if (status) { vad_report_ort_error(ort, status, "CreateEnv"); goto fail; }
status = ort->CreateSessionOptions(&opts);
if (status) { vad_report_ort_error(ort, status, "CreateSessionOptions"); goto fail; }
status = ort->SetIntraOpNumThreads(opts, 1);
if (status) { vad_report_ort_error(ort, status, "SetIntraOpNumThreads"); }
status = ort->SetSessionGraphOptimizationLevel(opts, ORT_ENABLE_ALL);
if (status) { vad_report_ort_error(ort, status, "SetSessionGraphOptimizationLevel"); }
status = ort->CreateSessionFromArray(vad->env, data, len, opts, &vad->session);
if (status) { vad_report_ort_error(ort, status, "CreateSessionFromArray"); goto fail; }
status = ort->CreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeDefault, &vad->mem_info);
if (status) { vad_report_ort_error(ort, status, "CreateCpuMemoryInfo"); goto fail; }
ort->ReleaseSessionOptions(opts);
DEBUG_INFO(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): ok bytes=%zu window=%d context=%d input=%d", label, len,
SILERO_VAD_WINDOW_SAMPLES, SILERO_VAD_CONTEXT_SAMPLES, SILERO_VAD_INPUT_SAMPLES);
return vad;
fail:
if (opts) ort->ReleaseSessionOptions(opts);
silero_vad_destroy(vad);
return NULL;
}
silero_vad_t* silero_vad_create(const char* model_path) {
uint8_t* buf = NULL;
size_t len = 0;
silero_vad_t* vad;
if (!model_path || !*model_path) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create: empty model path");
return NULL;
}
if (vad_read_file(model_path, &buf, &len) != 0) return NULL;
vad = silero_vad_create_from_bytes(buf, len, buf, model_path);
if (!vad) u_free(buf);
return vad;
}
silero_vad_t* silero_vad_create_default(void) {
return silero_vad_create_from_bytes(silero_vad_model_data, silero_vad_model_len, NULL, "embedded");
}
void silero_vad_destroy(silero_vad_t* vad) {
if (!vad) return;
const OrtApi* ort = vad->ort;
if (ort) {
if (vad->mem_info) ort->ReleaseMemoryInfo(vad->mem_info);
if (vad->session) ort->ReleaseSession(vad->session);
if (vad->env) ort->ReleaseEnv(vad->env);
}
if (vad->model_data) u_free(vad->model_data);
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "silero_vad_destroy");
u_free(vad);
}
void silero_vad_reset(silero_vad_t* vad) {
if (!vad) return;
memset(vad->state, 0, sizeof(vad->state));
memset(vad->context, 0, sizeof(vad->context));
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "silero_vad_reset: recurrent state and audio context cleared");
}
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob) {
const OrtApi* ort;
OrtStatus* status;
OrtValue* in_input = NULL;
OrtValue* in_state = NULL;
OrtValue* in_sr = NULL;
OrtValue* out_prob = NULL;
OrtValue* out_state = NULL;
const char* input_names[3];
const char* output_names[2];
const OrtValue* inputs[3];
OrtValue* outputs[2];
int64_t shape_input[2] = {1, SILERO_VAD_INPUT_SAMPLES};
int64_t shape_state[3] = {2, 1, 128};
int64_t shape_prob[2] = {1, 1};
int64_t sr_value = SILERO_VAD_SAMPLE_RATE;
float prob_local = 0.0f;
float next_state[SILERO_VAD_STATE_COUNT];
float input[SILERO_VAD_INPUT_SAMPLES];
int rc = -1;
if (!vad || !samples || !prob) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_process: invalid args");
return -1;
}
ort = vad->ort;
memcpy(input, vad->context, sizeof(vad->context));
memcpy(input + SILERO_VAD_CONTEXT_SAMPLES, samples, SILERO_VAD_WINDOW_SAMPLES * sizeof(float));
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, input,
sizeof(input), shape_input, 2,
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &in_input);
if (status) { vad_report_ort_error(ort, status, "input tensor"); return -1; }
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, vad->state,
SILERO_VAD_STATE_COUNT * sizeof(float), shape_state, 3,
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &in_state);
if (status) { vad_report_ort_error(ort, status, "state tensor"); goto done; }
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, &sr_value,
sizeof(sr_value), NULL, 0,
ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64, &in_sr);
if (status) { vad_report_ort_error(ort, status, "sr tensor"); goto done; }
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, &prob_local,
sizeof(float), shape_prob, 2,
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &out_prob);
if (status) { vad_report_ort_error(ort, status, "output tensor"); goto done; }
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, next_state,
SILERO_VAD_STATE_COUNT * sizeof(float), shape_state, 3,
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &out_state);
if (status) { vad_report_ort_error(ort, status, "stateN tensor"); goto done; }
input_names[0] = "input";
input_names[1] = "state";
input_names[2] = "sr";
inputs[0] = in_input;
inputs[1] = in_state;
inputs[2] = in_sr;
output_names[0] = "output";
output_names[1] = "stateN";
outputs[0] = out_prob;
outputs[1] = out_state;
status = ort->Run(vad->session, NULL, input_names, inputs, 3, output_names, 2, outputs);
if (status) {
vad_report_ort_error(ort, status, "Run");
goto done;
}
memcpy(vad->state, next_state, sizeof(next_state));
memcpy(vad->context, input + SILERO_VAD_INPUT_SAMPLES - SILERO_VAD_CONTEXT_SAMPLES, sizeof(vad->context));
*prob = prob_local;
rc = 0;
done:
if (in_input) ort->ReleaseValue(in_input);
if (in_state) ort->ReleaseValue(in_state);
if (in_sr) ort->ReleaseValue(in_sr);
if (out_prob) ort->ReleaseValue(out_prob);
if (out_state) ort->ReleaseValue(out_state);
return rc;
}
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob) {
float samples[SILERO_VAD_WINDOW_SAMPLES];
int i;
if (!pcm) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_process_pcm16: pcm == NULL");
return -1;
}
for (i = 0; i < SILERO_VAD_WINDOW_SAMPLES; i++) {
samples[i] = (float)pcm[i] / 32768.0f;
}
return silero_vad_process(vad, samples, prob);
}
#else /* !HAVE_SILERO_VAD — пустые стабы (сборки без onnxruntime) */
silero_vad_t* silero_vad_create(const char* model_path) {
(void)model_path;
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create: built without onnxruntime (HAVE_SILERO_VAD not defined)");
return NULL;
}
silero_vad_t* silero_vad_create_default(void) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create_default: built without onnxruntime (HAVE_SILERO_VAD not defined)");
return NULL;
}
void silero_vad_destroy(silero_vad_t* vad) {
(void)vad;
}
void silero_vad_reset(silero_vad_t* vad) {
(void)vad;
}
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob) {
(void)vad; (void)samples; (void)prob;
return -1;
}
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob) {
(void)vad; (void)pcm; (void)prob;
return -1;
}
#endif /* HAVE_SILERO_VAD */
-77
View File
@@ -1,77 +0,0 @@
/**
* Silero VAD (Voice Activity Detector) — C-обёртка над ONNX Runtime.
*
* Запускает официальную стриминговую модель silero_vad.onnx (v5, ~2.3 МБ),
* которая по окну аудио возвращает вероятность наличия речи [0..1].
* Рекуррентное состояние модели хранится внутри объекта и переносится между
* вызовами. Обёртка также хранит 64 последних отсчёта аудио и добавляет их
* перед новым окном: ONNX получает 576 отсчётов, публичный API принимает 512.
*
* Требования к входному аудио:
* - частота дискретизации 16 кГц (модель поддерживает только 16k);
* - моно;
* - окно ровно SILERO_VAD_WINDOW_SAMPLES = 512 сэмплов (32 мс);
* - float-сэмплы в диапазоне примерно [-1;1] (для int16 есть
* silero_vad_process_pcm16, который сам делит на 32768).
*
* Порог и гистерезис (задержки начала/конца речи) намеренно оставлены
* вызывающему — радио/звонок сами решают, как интерпретировать вероятность.
*
* Зависимость: libonnxruntime (C API). Сборка включается опцией
* `--with-silero-vad` (см. configure.ac) или макросом HAVE_SILERO_VAD в
* CMake-сборках. Без HAVE_SILERO_VAD модуль компилируется в пустые стабы
* (все create-функции возвращают NULL, process — ошибку), чтобы исходники lib/
* можно было GLOB-ить во всех сборках (Android/chatgui) без onnxruntime.
*
* Лицензия модели: MIT (Silero Team, https://github.com/snakers4/silero-vad).
*/
#ifndef SILERO_VAD_H
#define SILERO_VAD_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Фиксированное окно стриминговой модели: 512 сэмплов @ 16 кГц = 32 мс */
#define SILERO_VAD_WINDOW_SAMPLES 512
#define SILERO_VAD_SAMPLE_RATE 16000
typedef struct silero_vad silero_vad_t;
/**
* Создать детектор, загрузив ONNX-модель из файла model_path.
* Возвращает NULL при ошибке (подробности — в лог категории "vad").
*/
silero_vad_t* silero_vad_create(const char* model_path);
/**
* Создать детектор из модели, вшитой в бинарник (silero_vad_model_data в
* silero_vad.c). Не требует файла на диске. NULL при ошибке.
*/
silero_vad_t* silero_vad_create_default(void);
/* Освободить детектор. NULL безопасен. */
void silero_vad_destroy(silero_vad_t* vad);
/* Сбросить рекуррентное состояние и звуковой контекст (начало нового потока / разрыв захвата). */
void silero_vad_reset(silero_vad_t* vad);
/**
* Обработать окно из 512 float-сэмплов @ 16 кГц (примерно [-1;1]).
* При успехе возвращает 0 и заполняет *prob вероятностью речи [0..1];
* при ошибке — отрицательное значение (проб не трогается).
*/
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob);
/**
* То же, но вход — int16 PCM: сэмплы преобразуются к float делением на 32768.
*/
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob);
#ifdef __cplusplus
}
#endif
#endif /* SILERO_VAD_H */
Binary file not shown.
File diff suppressed because it is too large. Load diff
-13
View File
@@ -246,16 +246,3 @@ uint16_t ss_get_port(const struct sockaddr_storage *addr) {
return 0;
}
uint16_t socket_get_bound_port(socket_t sock) {
struct sockaddr_storage address; socklen_t size = sizeof(address);
if (getsockname(sock, (struct sockaddr*)&address, &size) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "getsockname fd=%llu failed: %s",
(unsigned long long)sock, socket_strerror(socket_get_error()));
return 0;
}
uint16_t port = ss_get_port(&address);
if (!port) DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "socket fd=%llu family=%d has no bound port",
(unsigned long long)sock, address.ss_family);
return port;
}
+2 -5
View File
@@ -1,7 +1,6 @@
/**
* Единые socket_t, коды ошибок и wrappers сокетов POSIX/Windows (включая MSYS2 UCRT64).
* platform_init/cleanup обслуживают Winsock; UDP/nonblocking/options/sendto/recvfrom — операции ОС.
* Созданные сокеты закрывает владелец через socket_close_wrapper; event loop находится в u_async.h.
* Socket compatibility layer for cross-platform support (POSIX / Windows)
* MSYS2 UCRT64 compatible
*/
#ifndef SOCKET_COMPAT_H
@@ -78,8 +77,6 @@ const char* socket_strerror(int err);
// взять порт из sockaddr_storage
uint16_t ss_get_port(const struct sockaddr_storage *addr);
// Фактический IPv4/IPv6-порт уже привязанного сокета; 0 при ошибке.
uint16_t socket_get_bound_port(socket_t sock);
#ifdef __cplusplus
-55
View File
@@ -1,55 +0,0 @@
bin
lib
# Win build files
*.dll
*.exe
*.lib
# GNU build files
*.o
*.so*
*.lo
*.P*
*.la*
*.a
*.pc
*config*
Makefile
Makefile.in
.libs
aclocal.m4
autom4te.cache
stamp-*
libtool
soundstretch
# Files generated by MSVC
*.bsc
*.suo
*.sdf
*.filters
*.user
source/SoundTouch/Win32/
source/SoundTouch/x64/
source/SoundStretch/Win32/
source/SoundStretch/x64/
source/SoundTouchDll/Win32/
source/SoundTouchDll/x64/
source/SoundTouchDll/DllTest/Win32/
source/SoundTouchDll/DllTest/x64/
.vs
# Files generated by Android Studio
source/android-lib/.gradle
source/android-lib/.idea
**/*.iml
source/android-lib/local.properties
source/android-lib/build
source/android-lib/.externalNativeBuild
# CMake build directory
build*
CMakeFiles
CMakeCache.txt
*.cmake
-131
View File
@@ -1,131 +0,0 @@
cmake_minimum_required(VERSION 3.16)
project(SoundTouch VERSION 2.3.3 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
include(GNUInstallDirs)
set(COMPILE_OPTIONS)
if(MSVC)
list(APPEND COMPILE_OPTIONS /O2 /fp:fast)
else()
list(APPEND COMPILE_OPTIONS -Wall -Wextra -Wzero-as-null-pointer-constant -Wno-unknown-pragmas)
if(EMSCRIPTEN)
list(APPEND COMPILE_OPTIONS -O3)
else()
list(APPEND COMPILE_OPTIONS -Ofast)
endif()
endif()
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
#####################
# SoundTouch library
add_library(SoundTouch STATIC
source/SoundTouch/AAFilter.cpp
source/SoundTouch/BPMDetect.cpp
source/SoundTouch/cpu_detect_x86.cpp
source/SoundTouch/FIFOSampleBuffer.cpp
source/SoundTouch/FIRFilter.cpp
source/SoundTouch/InterpolateCubic.cpp
source/SoundTouch/InterpolateLinear.cpp
source/SoundTouch/InterpolateShannon.cpp
source/SoundTouch/mmx_optimized.cpp
source/SoundTouch/PeakFinder.cpp
source/SoundTouch/RateTransposer.cpp
source/SoundTouch/SoundTouch.cpp
source/SoundTouch/sse_optimized.cpp
source/SoundTouch/TDStretch.cpp
)
target_include_directories(SoundTouch PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_include_directories(SoundTouch PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include/soundtouch
)
target_compile_definitions(SoundTouch PRIVATE ${COMPILE_DEFINITIONS})
target_compile_options(SoundTouch PRIVATE ${COMPILE_OPTIONS})
set_target_properties(SoundTouch PROPERTIES OUTPUT_NAME "soundtouch-static")
option(INTEGER_SAMPLES "Use integers instead of floats for samples" OFF)
if(INTEGER_SAMPLES)
target_compile_definitions(SoundTouch PRIVATE SOUNDTOUCH_INTEGER_SAMPLES)
set(SOUNDTOUCH_INTEGER_SAMPLES TRUE)
else()
target_compile_definitions(SoundTouch PRIVATE SOUNDTOUCH_FLOAT_SAMPLES)
set(SOUNDTOUCH_FLOAT_SAMPLES TRUE)
endif()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv7.*)$")
target_compile_options(SoundTouch PRIVATE -mfpu=neon)
endif()
install(
FILES
include/soundtouch/BPMDetect.h
include/soundtouch/FIFOSampleBuffer.h
include/soundtouch/FIFOSamplePipe.h
include/soundtouch/STTypes.h
include/soundtouch/SoundTouch.h
DESTINATION
"${CMAKE_INSTALL_INCLUDEDIR}/soundtouch"
)
########################
# SoundTouchDll library
option(SOUNDTOUCH_DLL "Build SoundTouchDLL C wrapper library" ON)
if(SOUNDTOUCH_DLL)
add_library(SoundTouchDLL SHARED
source/SoundTouchDLL/SoundTouchDLL.cpp
source/SoundTouchDLL/SoundTouchDLL.rc
)
set_target_properties(SoundTouch PROPERTIES POSITION_INDEPENDENT_CODE TRUE)
target_compile_options(SoundTouchDLL PRIVATE ${COMPILE_OPTIONS})
set_target_properties(SoundTouchDLL PROPERTIES CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(SoundTouchDLL PRIVATE DLL_EXPORTS)
target_include_directories(SoundTouchDLL INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
target_include_directories(SoundTouchDLL PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include/soundtouch")
target_link_libraries(SoundTouchDLL PRIVATE SoundTouch)
set_target_properties(SoundTouchDLL PROPERTIES OUTPUT_NAME "soundtouch")
if(NOT ANDROID)
set_target_properties(SoundTouchDLL PROPERTIES VERSION ${CMAKE_PROJECT_VERSION})
if(NOT WIN32)
set_target_properties(SoundTouchDLL PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR})
endif()
endif()
install(FILES include/soundtouch/SoundTouchDLL.h DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/soundtouch")
install(TARGETS SoundTouchDLL EXPORT SoundTouchTargets)
endif()
########################
# CMake config
include(CMakePackageConfigHelpers)
set(SOUNDTOUCH_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/SoundTouch")
install(
EXPORT SoundTouchTargets
FILE SoundTouchTargets.cmake
NAMESPACE SoundTouch::
DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}"
)
configure_package_config_file(SoundTouchConfig.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfig.cmake"
INSTALL_DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}"
)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfigVersion.cmake"
VERSION "${CMAKE_PROJECT_VERSION}"
COMPATIBILITY SameMajorVersion
)
install(
FILES
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfigVersion.cmake"
DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}"
)
-458
View File
@@ -1,458 +0,0 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
File diff suppressed because it is too large. Load diff
@@ -1,11 +0,0 @@
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/SoundTouchTargets.cmake")
check_required_components(SoundTouch)
if(@SOUNDTOUCH_DLL@)
check_required_components(SoundTouchDLL)
get_target_property(SoundTouchDLL_LOCATION SoundTouch::SoundTouchDLL LOCATION)
message(STATUS "Found SoundTouchDLL: ${SoundTouchDLL_LOCATION}")
endif()
@@ -1,205 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Beats-per-minute (BPM) detection routine.
///
/// The beat detection algorithm works as follows:
/// - Use function 'inputSamples' to input a chunks of samples to the class for
/// analysis. It's a good idea to enter a large sound file or stream in smallish
/// chunks of around few kilosamples in order not to extinguish too much RAM memory.
/// - Input sound data is decimated to approx 500 Hz to reduce calculation burden,
/// which is basically ok as low (bass) frequencies mostly determine the beat rate.
/// Simple averaging is used for anti-alias filtering because the resulting signal
/// quality isn't of that high importance.
/// - Decimated sound data is enveloped, i.e. the amplitude shape is detected by
/// taking absolute value that's smoothed by sliding average. Signal levels that
/// are below a couple of times the general RMS amplitude level are cut away to
/// leave only notable peaks there.
/// - Repeating sound patterns (e.g. beats) are detected by calculating short-term
/// autocorrelation function of the enveloped signal.
/// - After whole sound data file has been analyzed as above, the bpm level is
/// detected by function 'getBpm' that finds the highest peak of the autocorrelation
/// function, calculates it's precise location and converts this reading to bpm's.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef _BPMDetect_H_
#define _BPMDetect_H_
#include <vector>
#include "STTypes.h"
#include "FIFOSampleBuffer.h"
namespace soundtouch
{
/// Minimum allowed BPM rate. Used to restrict accepted result above a reasonable limit.
#define MIN_BPM 45
/// Maximum allowed BPM rate range. Used for calculating algorithm parametrs
#define MAX_BPM_RANGE 200
/// Maximum allowed BPM rate range. Used to restrict accepted result below a reasonable limit.
#define MAX_BPM_VALID 190
////////////////////////////////////////////////////////////////////////////////
typedef struct
{
float pos;
float strength;
} BEAT;
class IIR2_filter
{
double coeffs[5];
double prev[5];
public:
IIR2_filter(const double *lpf_coeffs);
float update(float x);
};
/// Class for calculating BPM rate for audio data.
class BPMDetect
{
protected:
/// Auto-correlation accumulator bins.
float *xcorr;
/// Sample average counter.
int decimateCount;
/// Sample average accumulator for FIFO-like decimation.
soundtouch::LONG_SAMPLETYPE decimateSum;
/// Decimate sound by this coefficient to reach approx. 500 Hz.
int decimateBy;
/// Auto-correlation window length
int windowLen;
/// Number of channels (1 = mono, 2 = stereo)
int channels;
/// sample rate
int sampleRate;
/// Beginning of auto-correlation window: Autocorrelation isn't being updated for
/// the first these many correlation bins.
int windowStart;
/// window functions for data preconditioning
float *hamw;
float *hamw2;
// beat detection variables
int pos;
int peakPos;
int beatcorr_ringbuffpos;
int init_scaler;
float peakVal;
float *beatcorr_ringbuff;
/// FIFO-buffer for decimated processing samples.
soundtouch::FIFOSampleBuffer *buffer;
/// Collection of detected beat positions
//BeatCollection beats;
std::vector<BEAT> beats;
// 2nd order low-pass-filter
IIR2_filter beat_lpf;
/// Updates auto-correlation function for given number of decimated samples that
/// are read from the internal 'buffer' pipe (samples aren't removed from the pipe
/// though).
void updateXCorr(int process_samples /// How many samples are processed.
);
/// Decimates samples to approx. 500 Hz.
///
/// \return Number of output samples.
int decimate(soundtouch::SAMPLETYPE *dest, ///< Destination buffer
const soundtouch::SAMPLETYPE *src, ///< Source sample buffer
int numsamples ///< Number of source samples.
);
/// Calculates amplitude envelope for the buffer of samples.
/// Result is output to 'samples'.
void calcEnvelope(soundtouch::SAMPLETYPE *samples, ///< Pointer to input/output data buffer
int numsamples ///< Number of samples in buffer
);
/// remove constant bias from xcorr data
void removeBias();
// Detect individual beat positions
void updateBeatPos(int process_samples);
public:
/// Constructor.
BPMDetect(int numChannels, ///< Number of channels in sample data.
int sampleRate ///< Sample rate in Hz.
);
/// Destructor.
virtual ~BPMDetect();
/// Inputs a block of samples for analyzing: Envelopes the samples and then
/// updates the autocorrelation estimation. When whole song data has been input
/// in smaller blocks using this function, read the resulting bpm with 'getBpm'
/// function.
///
/// Notice that data in 'samples' array can be disrupted in processing.
void inputSamples(const soundtouch::SAMPLETYPE *samples, ///< Pointer to input/working data buffer
int numSamples ///< Number of samples in buffer
);
/// Analyzes the results and returns the BPM rate. Use this function to read result
/// after whole song data has been input to the class by consecutive calls of
/// 'inputSamples' function.
///
/// \return Beats-per-minute rate, or zero if detection failed.
float getBpm();
/// Get beat position arrays. Note: The array includes also really low beat detection values
/// in absence of clear strong beats. Consumer may wish to filter low values away.
/// - "pos" receive array of beat positions
/// - "values" receive array of beat detection strengths
/// - max_num indicates max.size of "pos" and "values" array.
///
/// You can query a suitable array sized by calling this with nullptr in "pos" & "values".
///
/// \return number of beats in the arrays.
int getBeats(float *pos, float *strength, int max_num);
};
}
#endif // _BPMDetect_H_
@@ -1,180 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// A buffer class for temporarily storaging sound samples, operates as a
/// first-in-first-out pipe.
///
/// Samples are added to the end of the sample buffer with the 'putSamples'
/// function, and are received from the beginning of the buffer by calling
/// the 'receiveSamples' function. The class automatically removes the
/// output samples from the buffer as well as grows the storage size
/// whenever necessary.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef FIFOSampleBuffer_H
#define FIFOSampleBuffer_H
#include "FIFOSamplePipe.h"
namespace soundtouch
{
/// Sample buffer working in FIFO (first-in-first-out) principle. The class takes
/// care of storage size adjustment and data moving during input/output operations.
///
/// Notice that in case of stereo audio, one sample is considered to consist of
/// both channel data.
class FIFOSampleBuffer : public FIFOSamplePipe
{
private:
/// Sample buffer.
SAMPLETYPE *buffer;
// Raw unaligned buffer memory. 'buffer' is made aligned by pointing it to first
// 16-byte aligned location of this buffer
SAMPLETYPE *bufferUnaligned;
/// Sample buffer size in bytes
uint sizeInBytes;
/// How many samples are currently in buffer.
uint samplesInBuffer;
/// Channels, 1=mono, 2=stereo.
uint channels;
/// Current position pointer to the buffer. This pointer is increased when samples are
/// removed from the pipe so that it's necessary to actually rewind buffer (move data)
/// only new data when is put to the pipe.
uint bufferPos;
/// Rewind the buffer by moving data from position pointed by 'bufferPos' to real
/// beginning of the buffer.
void rewind();
/// Ensures that the buffer has capacity for at least this many samples.
void ensureCapacity(uint capacityRequirement);
/// Returns current capacity.
uint getCapacity() const;
public:
/// Constructor
FIFOSampleBuffer(int numChannels = 2 ///< Number of channels, 1=mono, 2=stereo.
///< Default is stereo.
);
/// destructor
~FIFOSampleBuffer() override;
/// Returns a pointer to the beginning of the output samples.
/// This function is provided for accessing the output samples directly.
/// Please be careful for not to corrupt the book-keeping!
///
/// When using this function to output samples, also remember to 'remove' the
/// output samples from the buffer by calling the
/// 'receiveSamples(numSamples)' function
virtual SAMPLETYPE *ptrBegin() override;
/// Returns a pointer to the end of the used part of the sample buffer (i.e.
/// where the new samples are to be inserted). This function may be used for
/// inserting new samples into the sample buffer directly. Please be careful
/// not corrupt the book-keeping!
///
/// When using this function as means for inserting new samples, also remember
/// to increase the sample count afterwards, by calling the
/// 'putSamples(numSamples)' function.
SAMPLETYPE *ptrEnd(
uint slackCapacity ///< How much free capacity (in samples) there _at least_
///< should be so that the caller can successfully insert the
///< desired samples to the buffer. If necessary, the function
///< grows the buffer size to comply with this requirement.
);
/// Adds 'numSamples' pcs of samples from the 'samples' memory position to
/// the sample buffer.
virtual void putSamples(const SAMPLETYPE *samples, ///< Pointer to samples.
uint numSamples ///< Number of samples to insert.
) override;
/// Adjusts the book-keeping to increase number of samples in the buffer without
/// copying any actual samples.
///
/// This function is used to update the number of samples in the sample buffer
/// when accessing the buffer directly with 'ptrEnd' function. Please be
/// careful though!
virtual void putSamples(uint numSamples ///< Number of samples been inserted.
);
/// Output samples from beginning of the sample buffer. Copies requested samples to
/// output buffer and removes them from the sample buffer. If there are less than
/// 'numsample' samples in the buffer, returns all that available.
///
/// \return Number of samples returned.
virtual uint receiveSamples(SAMPLETYPE *output, ///< Buffer where to copy output samples.
uint maxSamples ///< How many samples to receive at max.
) override;
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
/// sample buffer without copying them anywhere.
///
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
/// with 'ptrBegin' function.
virtual uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
) override;
/// Returns number of samples currently available.
virtual uint numSamples() const override;
/// Sets number of channels, 1 = mono, 2 = stereo.
void setChannels(int numChannels);
/// Get number of channels
int getChannels()
{
return channels;
}
/// Returns nonzero if there aren't any samples available for outputting.
virtual int isEmpty() const override;
/// Clears all the samples.
virtual void clear() override;
/// allow trimming (downwards) amount of samples in pipeline.
/// Returns adjusted amount of samples
uint adjustAmountOfSamples(uint numSamples) override;
/// Add silence to end of buffer
void addSilent(uint nSamples);
};
}
#endif
@@ -1,230 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// 'FIFOSamplePipe' : An abstract base class for classes that manipulate sound
/// samples by operating like a first-in-first-out pipe: New samples are fed
/// into one end of the pipe with the 'putSamples' function, and the processed
/// samples are received from the other end with the 'receiveSamples' function.
///
/// 'FIFOProcessor' : A base class for classes the do signal processing with
/// the samples while operating like a first-in-first-out pipe. When samples
/// are input with the 'putSamples' function, the class processes them
/// and moves the processed samples to the given 'output' pipe object, which
/// may be either another processing stage, or a fifo sample buffer object.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef FIFOSamplePipe_H
#define FIFOSamplePipe_H
#include <assert.h>
#include <stdlib.h>
#include "STTypes.h"
namespace soundtouch
{
/// Abstract base class for FIFO (first-in-first-out) sample processing classes.
class FIFOSamplePipe
{
protected:
bool verifyNumberOfChannels(int nChannels) const
{
if ((nChannels > 0) && (nChannels <= SOUNDTOUCH_MAX_CHANNELS))
{
return true;
}
ST_THROW_RT_ERROR("Error: Illegal number of channels");
return false;
}
public:
// virtual default destructor
virtual ~FIFOSamplePipe() {}
/// Returns a pointer to the beginning of the output samples.
/// This function is provided for accessing the output samples directly.
/// Please be careful for not to corrupt the book-keeping!
///
/// When using this function to output samples, also remember to 'remove' the
/// output samples from the buffer by calling the
/// 'receiveSamples(numSamples)' function
virtual SAMPLETYPE *ptrBegin() = 0;
/// Adds 'numSamples' pcs of samples from the 'samples' memory position to
/// the sample buffer.
virtual void putSamples(const SAMPLETYPE *samples, ///< Pointer to samples.
uint numSamples ///< Number of samples to insert.
) = 0;
// Moves samples from the 'other' pipe instance to this instance.
void moveSamples(FIFOSamplePipe &other ///< Other pipe instance where from the receive the data.
)
{
const uint oNumSamples = other.numSamples();
putSamples(other.ptrBegin(), oNumSamples);
other.receiveSamples(oNumSamples);
}
/// Output samples from beginning of the sample buffer. Copies requested samples to
/// output buffer and removes them from the sample buffer. If there are less than
/// 'numsample' samples in the buffer, returns all that available.
///
/// \return Number of samples returned.
virtual uint receiveSamples(SAMPLETYPE *output, ///< Buffer where to copy output samples.
uint maxSamples ///< How many samples to receive at max.
) = 0;
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
/// sample buffer without copying them anywhere.
///
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
/// with 'ptrBegin' function.
virtual uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
) = 0;
/// Returns number of samples currently available.
virtual uint numSamples() const = 0;
// Returns nonzero if there aren't any samples available for outputting.
virtual int isEmpty() const = 0;
/// Clears all the samples.
virtual void clear() = 0;
/// allow trimming (downwards) amount of samples in pipeline.
/// Returns adjusted amount of samples
virtual uint adjustAmountOfSamples(uint numSamples) = 0;
};
/// Base-class for sound processing routines working in FIFO principle. With this base
/// class it's easy to implement sound processing stages that can be chained together,
/// so that samples that are fed into beginning of the pipe automatically go through
/// all the processing stages.
///
/// When samples are input to this class, they're first processed and then put to
/// the FIFO pipe that's defined as output of this class. This output pipe can be
/// either other processing stage or a FIFO sample buffer.
class FIFOProcessor :public FIFOSamplePipe
{
protected:
/// Internal pipe where processed samples are put.
FIFOSamplePipe *output;
/// Sets output pipe.
void setOutPipe(FIFOSamplePipe *pOutput)
{
assert(output == nullptr);
assert(pOutput != nullptr);
output = pOutput;
}
/// Constructor. Doesn't define output pipe; it has to be set be
/// 'setOutPipe' function.
FIFOProcessor()
{
output = nullptr;
}
/// Constructor. Configures output pipe.
FIFOProcessor(FIFOSamplePipe *pOutput ///< Output pipe.
)
{
output = pOutput;
}
/// Destructor.
virtual ~FIFOProcessor() override
{
}
/// Returns a pointer to the beginning of the output samples.
/// This function is provided for accessing the output samples directly.
/// Please be careful for not to corrupt the book-keeping!
///
/// When using this function to output samples, also remember to 'remove' the
/// output samples from the buffer by calling the
/// 'receiveSamples(numSamples)' function
virtual SAMPLETYPE *ptrBegin() override
{
return output->ptrBegin();
}
public:
/// Output samples from beginning of the sample buffer. Copies requested samples to
/// output buffer and removes them from the sample buffer. If there are less than
/// 'numsample' samples in the buffer, returns all that available.
///
/// \return Number of samples returned.
virtual uint receiveSamples(SAMPLETYPE *outBuffer, ///< Buffer where to copy output samples.
uint maxSamples ///< How many samples to receive at max.
) override
{
return output->receiveSamples(outBuffer, maxSamples);
}
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
/// sample buffer without copying them anywhere.
///
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
/// with 'ptrBegin' function.
virtual uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
) override
{
return output->receiveSamples(maxSamples);
}
/// Returns number of samples currently available.
virtual uint numSamples() const override
{
return output->numSamples();
}
/// Returns nonzero if there aren't any samples available for outputting.
virtual int isEmpty() const override
{
return output->isEmpty();
}
/// allow trimming (downwards) amount of samples in pipeline.
/// Returns adjusted amount of samples
virtual uint adjustAmountOfSamples(uint numSamples) override
{
return output->adjustAmountOfSamples(numSamples);
}
};
}
#endif
@@ -1,176 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Common type definitions for SoundTouch audio processing library.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef STTypes_H
#define STTypes_H
typedef unsigned int uint;
typedef unsigned long ulong;
// Patch for MinGW: on Win64 long is 32-bit
#ifdef _WIN64
typedef unsigned long long ulongptr;
#else
typedef ulong ulongptr;
#endif
// Helper macro for aligning pointer up to next 16-byte boundary
#define SOUNDTOUCH_ALIGN_POINTER_16(x) ( ( (ulongptr)(x) + 15 ) & ~(ulongptr)15 )
namespace soundtouch
{
/// Max allowed number of channels
#define SOUNDTOUCH_MAX_CHANNELS 16
/// Activate these undef's to overrule the possible sampletype
/// setting inherited from some other header file:
//#undef SOUNDTOUCH_INTEGER_SAMPLES
//#undef SOUNDTOUCH_FLOAT_SAMPLES
/// If following flag is defined, always uses multichannel processing
/// routines also for mono and stero sound. This is for routine testing
/// purposes; output should be same with either routines, yet disabling
/// the dedicated mono/stereo processing routines will result in slower
/// runtime performance so recommendation is to keep this off.
// #define USE_MULTICH_ALWAYS
#if !(SOUNDTOUCH_INTEGER_SAMPLES || SOUNDTOUCH_FLOAT_SAMPLES)
/// Choose either 32bit floating point or 16bit integer sampletype
/// by choosing one of the following defines, unless this selection
/// has already been done in some other file.
////
/// Notes:
/// - In Windows environment, choose the sample format with the
/// following defines.
/// - In GNU environment, the floating point samples are used by
/// default, but integer samples can be chosen by giving the
/// following switch to the configure script:
/// ./configure --enable-integer-samples
/// However, if you still prefer to select the sample format here
/// also in GNU environment, then please #undef the INTEGER_SAMPLE
/// and FLOAT_SAMPLE defines first as in comments above.
//#define SOUNDTOUCH_INTEGER_SAMPLES 1 //< 16bit integer samples
#define SOUNDTOUCH_FLOAT_SAMPLES 1 //< 32bit float samples
#endif
#if (_M_IX86 || __i386__ || __x86_64__ || _M_X64)
/// Define this to allow X86-specific assembler/intrinsic optimizations.
/// Notice that library contains also usual C++ versions of each of these
/// these routines, so if you're having difficulties getting the optimized
/// routines compiled for whatever reason, you may disable these optimizations
/// to make the library compile.
#define SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS 1
#undef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS
#elif defined(__arm__) || defined(__aarch64__)
#undef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
#define SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS 1
#else
/// Always disable optimizations when not using a x86 systems.
#undef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
#undef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS
#endif
// If defined, allows the SIMD-optimized routines to skip unevenly aligned
// memory offsets that can cause performance penalty in some SIMD implementations.
// Causes slight compromise in sound quality.
// #define SOUNDTOUCH_ALLOW_NONEXACT_SIMD_OPTIMIZATION 1
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
// 16bit integer sample type
typedef short SAMPLETYPE;
// data type for sample accumulation: Use 32bit integer to prevent overflows
typedef long LONG_SAMPLETYPE;
#ifdef SOUNDTOUCH_FLOAT_SAMPLES
// check that only one sample type is defined
#error "conflicting sample types defined"
#endif // SOUNDTOUCH_FLOAT_SAMPLES
#ifdef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
// Allow MMX optimizations (not available in X64 mode)
#if (!_M_X64)
#define SOUNDTOUCH_ALLOW_MMX 1
#endif
#endif
#else
// floating point samples
typedef float SAMPLETYPE;
// data type for sample accumulation: Use float also here to enable
// efficient autovectorization
typedef float LONG_SAMPLETYPE;
#ifdef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
// Allow SSE optimizations
#define SOUNDTOUCH_ALLOW_SSE 1
#endif
#ifdef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS
#define SOUNDTOUCH_USE_NEON 1
#endif
#endif // SOUNDTOUCH_INTEGER_SAMPLES
#if ((SOUNDTOUCH_ALLOW_SSE) || (__SSE__) || (SOUNDTOUCH_USE_NEON))
#if SOUNDTOUCH_ALLOW_NONEXACT_SIMD_OPTIMIZATION
#define ST_SIMD_AVOID_UNALIGNED
#endif
#endif
}
// define ST_NO_EXCEPTION_HANDLING switch to disable throwing std exceptions:
#define ST_NO_EXCEPTION_HANDLING 1
#ifdef ST_NO_EXCEPTION_HANDLING
// Exceptions disabled. Throw asserts instead if enabled.
#include <assert.h>
#define ST_THROW_RT_ERROR(x) {assert((const char *)x);}
#else
// use c++ standard exceptions
#include <stdexcept>
#include <string>
#define ST_THROW_RT_ERROR(x) {throw std::runtime_error(x);}
#endif
// When this #define is active, eliminates a clicking sound when the "rate" or "pitch"
// parameter setting crosses from value <1 to >=1 or vice versa during processing.
// Default is off as such crossover is untypical case and involves a slight sound
// quality compromise.
//#define SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER 1
#endif
@@ -1,358 +0,0 @@
//////////////////////////////////////////////////////////////////////////////
///
/// SoundTouch - main class for tempo/pitch/rate adjusting routines.
///
/// Notes:
/// - Initialize the SoundTouch object instance by setting up the sound stream
/// parameters with functions 'setSampleRate' and 'setChannels', then set
/// desired tempo/pitch/rate settings with the corresponding functions.
///
/// - The SoundTouch class behaves like a first-in-first-out pipeline: The
/// samples that are to be processed are fed into one of the pipe by calling
/// function 'putSamples', while the ready processed samples can be read
/// from the other end of the pipeline with function 'receiveSamples'.
///
/// - The SoundTouch processing classes require certain sized 'batches' of
/// samples in order to process the sound. For this reason the classes buffer
/// incoming samples until there are enough of samples available for
/// processing, then they carry out the processing step and consequently
/// make the processed samples available for outputting.
///
/// - For the above reason, the processing routines introduce a certain
/// 'latency' between the input and output, so that the samples input to
/// SoundTouch may not be immediately available in the output, and neither
/// the amount of outputtable samples may not immediately be in direct
/// relationship with the amount of previously input samples.
///
/// - The tempo/pitch/rate control parameters can be altered during processing.
/// Please notice though that they aren't currently protected by semaphores,
/// so in multi-thread application external semaphore protection may be
/// required.
///
/// - This class utilizes classes 'TDStretch' for tempo change (without modifying
/// pitch) and 'RateTransposer' for changing the playback rate (that is, both
/// tempo and pitch in the same ratio) of the sound. The third available control
/// 'pitch' (change pitch but maintain tempo) is produced by a combination of
/// combining the two other controls.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef SoundTouch_H
#define SoundTouch_H
#include "FIFOSamplePipe.h"
#include "STTypes.h"
#include "SourceSpan.h"
namespace soundtouch
{
/// Soundtouch library version string
#define SOUNDTOUCH_VERSION "2.3.3"
/// SoundTouch library version id
#define SOUNDTOUCH_VERSION_ID (20303)
//
// Available setting IDs for the 'setSetting' & 'get_setting' functions:
/// Enable/disable anti-alias filter in pitch transposer (0 = disable)
#define SETTING_USE_AA_FILTER 0
/// Pitch transposer anti-alias filter length (8 .. 128 taps, default = 32)
#define SETTING_AA_FILTER_LENGTH 1
/// Enable/disable quick seeking algorithm in tempo changer routine
/// (enabling quick seeking lowers CPU utilization but causes a minor sound
/// quality compromising)
#define SETTING_USE_QUICKSEEK 2
/// Time-stretch algorithm single processing sequence length in milliseconds. This determines
/// to how long sequences the original sound is chopped in the time-stretch algorithm.
/// See "STTypes.h" or README for more information.
#define SETTING_SEQUENCE_MS 3
/// Time-stretch algorithm seeking window length in milliseconds for algorithm that finds the
/// best possible overlapping location. This determines from how wide window the algorithm
/// may look for an optimal joining location when mixing the sound sequences back together.
/// See "STTypes.h" or README for more information.
#define SETTING_SEEKWINDOW_MS 4
/// Time-stretch algorithm overlap length in milliseconds. When the chopped sound sequences
/// are mixed back together, to form a continuous sound stream, this parameter defines over
/// how long period the two consecutive sequences are let to overlap each other.
/// See "STTypes.h" or README for more information.
#define SETTING_OVERLAP_MS 5
/// Call "getSetting" with this ID to query processing sequence size in samples.
/// This value gives approximate value of how many input samples you'll need to
/// feed into SoundTouch after initial buffering to get out a new batch of
/// output samples.
///
/// This value does not include initial buffering at beginning of a new processing
/// stream, use SETTING_INITIAL_LATENCY to get the initial buffering size.
///
/// Notices:
/// - This is read-only parameter, i.e. setSetting ignores this parameter
/// - This parameter value is not constant but change depending on
/// tempo/pitch/rate/samplerate settings.
#define SETTING_NOMINAL_INPUT_SEQUENCE 6
/// Call "getSetting" with this ID to query nominal average processing output
/// size in samples. This value tells approcimate value how many output samples
/// SoundTouch outputs once it does DSP processing run for a batch of input samples.
///
/// Notices:
/// - This is read-only parameter, i.e. setSetting ignores this parameter
/// - This parameter value is not constant but change depending on
/// tempo/pitch/rate/samplerate settings.
#define SETTING_NOMINAL_OUTPUT_SEQUENCE 7
/// Call "getSetting" with this ID to query initial processing latency, i.e.
/// approx. how many samples you'll need to enter to SoundTouch pipeline before
/// you can expect to get first batch of ready output samples out.
///
/// After the first output batch, you can then expect to get approx.
/// SETTING_NOMINAL_OUTPUT_SEQUENCE ready samples out for every
/// SETTING_NOMINAL_INPUT_SEQUENCE samples that you enter into SoundTouch.
///
/// Example:
/// processing with parameter -tempo=5
/// => initial latency = 5509 samples
/// input sequence = 4167 samples
/// output sequence = 3969 samples
///
/// Accordingly, you can expect to feed in approx. 5509 samples at beginning of
/// the stream, and then you'll get out the first 3969 samples. After that, for
/// every approx. 4167 samples that you'll put in, you'll receive again approx.
/// 3969 samples out.
///
/// This also means that average latency during stream processing is
/// INITIAL_LATENCY-OUTPUT_SEQUENCE/2, in the above example case 5509-3969/2
/// = 3524 samples
///
/// Notices:
/// - This is read-only parameter, i.e. setSetting ignores this parameter
/// - This parameter value is not constant but change depending on
/// tempo/pitch/rate/samplerate settings.
#define SETTING_INITIAL_LATENCY 8
class SoundTouch final : public FIFOProcessor
{
private:
/// Rate transposer class instance
class RateTransposer *pRateTransposer;
/// Time-stretch class instance
class TDStretch *pTDStretch;
/// Virtual pitch parameter. Effective rate & tempo are calculated from these parameters.
double virtualRate;
/// Virtual pitch parameter. Effective rate & tempo are calculated from these parameters.
double virtualTempo;
/// Virtual pitch parameter. Effective rate & tempo are calculated from these parameters.
double virtualPitch;
/// Flag: Has sample rate been set?
bool bSrateSet;
/// Accumulator for how many samples in total will be expected as output vs. samples put in,
/// considering current processing settings.
double samplesExpectedOut;
/// Accumulator for how many samples in total have been read out from the processing so far
long samplesOutput;
/// Calculates effective rate & tempo valuescfrom 'virtualRate', 'virtualTempo' and
/// 'virtualPitch' parameters.
void calcEffectiveRateAndTempo();
bool sourceProfileSupported() const;
protected :
/// Number of channels
uint channels;
/// Effective 'rate' value calculated from 'virtualRate', 'virtualTempo' and 'virtualPitch'
double rate;
/// Effective 'tempo' value calculated from 'virtualRate', 'virtualTempo' and 'virtualPitch'
double tempo;
public:
SoundTouch();
~SoundTouch() override;
/* uTun: подключать после настройки, до первого putSamples либо после clear.
* Поддерживается float PCM, unity pitch/rate, cubic interpolation, AA off, mono/stereo.
* Ошибка сообщает Invalid и возвращает false. Изменение профиля после начала и flush
* инвалидируют карту; clear начинает новую. nullptr отключает события в любой момент.
* События описывают произведённый выход, включая ещё не прочитанные receiveSamples данные. */
bool setSourceCallback(SourceCallback callback, void* context);
/// Get SoundTouch library version string
static const char *getVersionString();
/// Get SoundTouch library version Id
static uint getVersionId();
/// Sets new rate control value. Normal rate = 1.0, smaller values
/// represent slower rate, larger faster rates.
void setRate(double newRate);
/// Sets new tempo control value. Normal tempo = 1.0, smaller values
/// represent slower tempo, larger faster tempo.
void setTempo(double newTempo);
/// Sets new rate control value as a difference in percents compared
/// to the original rate (-50 .. +100 %)
void setRateChange(double newRate);
/// Sets new tempo control value as a difference in percents compared
/// to the original tempo (-50 .. +100 %)
void setTempoChange(double newTempo);
/// Sets new pitch control value. Original pitch = 1.0, smaller values
/// represent lower pitches, larger values higher pitch.
void setPitch(double newPitch);
/// Sets pitch change in octaves compared to the original pitch
/// (-1.00 .. +1.00)
void setPitchOctaves(double newPitch);
/// Sets pitch change in semi-tones compared to the original pitch
/// (-12 .. +12)
void setPitchSemiTones(int newPitch);
void setPitchSemiTones(double newPitch);
/// Sets the number of channels, 1 = mono, 2 = stereo
void setChannels(uint numChannels);
/// Sets sample rate.
void setSampleRate(uint srate);
/// Get ratio between input and output audio durations, useful for calculating
/// processed output duration: if you'll process a stream of N samples, then
/// you can expect to get out N * getInputOutputSampleRatio() samples.
///
/// This ratio will give accurate target duration ratio for a full audio track,
/// given that the the whole track is processed with same processing parameters.
///
/// If this ratio is applied to calculate intermediate offsets inside a processing
/// stream, then this ratio is approximate and can deviate +- some tens of milliseconds
/// from ideal offset, yet by end of the audio stream the duration ratio will become
/// exact.
///
/// Example: if processing with parameters "-tempo=15 -pitch=-3", the function
/// will return value 0.8695652... Now, if processing an audio stream whose duration
/// is exactly one million audio samples, then you can expect the processed
/// output duration be 0.869565 * 1000000 = 869565 samples.
double getInputOutputSampleRatio();
/// Flushes the last samples from the processing pipeline to the output.
/// Clears also the internal processing buffers.
//
/// Note: This function is meant for extracting the last samples of a sound
/// stream. This function may introduce additional blank samples in the end
/// of the sound stream, and thus it's not recommended to call this function
/// in the middle of a sound stream.
void flush();
/// Adds 'numSamples' pcs of samples from the 'samples' memory position into
/// the input of the object. Notice that sample rate _has_to_ be set before
/// calling this function, otherwise throws a runtime_error exception.
void putSamples(
const SAMPLETYPE *samples, ///< Pointer to sample buffer.
uint numSamples ///< Number of samples in buffer. Notice
///< that in case of stereo-sound a single sample
///< contains data for both channels.
) override;
/// Output samples from beginning of the sample buffer. Copies requested samples to
/// output buffer and removes them from the sample buffer. If there are less than
/// 'numsample' samples in the buffer, returns all that available.
///
/// \return Number of samples returned.
uint receiveSamples(SAMPLETYPE *output, ///< Buffer where to copy output samples.
uint maxSamples ///< How many samples to receive at max.
) override;
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
/// sample buffer without copying them anywhere.
///
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
/// with 'ptrBegin' function.
uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
) override;
/// Clears all the samples in the object's output and internal processing
/// buffers.
void clear() override;
/// Changes a setting controlling the processing system behaviour. See the
/// 'SETTING_...' defines for available setting ID's.
///
/// \return 'true' if the setting was successfully changed
bool setSetting(int settingId, ///< Setting ID number. see SETTING_... defines.
int value ///< New setting value.
);
/// Reads a setting controlling the processing system behaviour. See the
/// 'SETTING_...' defines for available setting ID's.
///
/// \return the setting value.
int getSetting(int settingId ///< Setting ID number, see SETTING_... defines.
) const;
/// Returns number of samples currently unprocessed.
uint numUnprocessedSamples() const;
/// Return number of channels
uint numChannels() const
{
return channels;
}
/// Other handy functions that are implemented in the ancestor classes (see
/// classes 'FIFOProcessor' and 'FIFOSamplePipe')
///
/// - receiveSamples() : Use this function to receive 'ready' processed samples from SoundTouch.
/// - numSamples() : Get number of 'ready' samples that can be received with
/// function 'receiveSamples()'
/// - isEmpty() : Returns nonzero if there aren't any 'ready' samples.
/// - clear() : Clears all samples from ready/processing buffers.
};
}
#endif
@@ -1,177 +0,0 @@
//////////////////////////////////////////////////////////////////////////////
///
/// SoundTouch DLL wrapper - wraps SoundTouch routines into a Dynamic Load
/// Library interface.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef _SoundTouchDLL_h_
#define _SoundTouchDLL_h_
#if defined(_WIN32) || defined(WIN32)
// Windows
#ifndef __cplusplus
#error "Expected g++"
#endif
#ifdef DLL_EXPORTS
#define SOUNDTOUCHDLL_API extern "C" __declspec(dllexport)
#else
#define SOUNDTOUCHDLL_API extern "C" __declspec(dllimport)
#endif
#else
// GNU version
#if defined(DLL_EXPORTS) || defined(SoundTouchDLL_EXPORTS)
// GCC declaration for exporting functions
#define SOUNDTOUCHDLL_API extern "C" __attribute__((__visibility__("default")))
#else
// import function
#define SOUNDTOUCHDLL_API extern "C"
#endif
// Linux-replacements for Windows declarations:
#define __cdecl
typedef unsigned int DWORD;
#define FALSE 0
#define TRUE 1
#endif
typedef void * ST_HANDLE;
/// Create a new instance of SoundTouch processor.
SOUNDTOUCHDLL_API ST_HANDLE __cdecl soundtouch_createInstance();
/// Destroys a SoundTouch processor instance.
SOUNDTOUCHDLL_API void __cdecl soundtouch_destroyInstance(ST_HANDLE h);
/// Get SoundTouch library version string
SOUNDTOUCHDLL_API const char *__cdecl soundtouch_getVersionString();
/// Get SoundTouch library version Id
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_getVersionId();
/// Sets new rate control value. Normal rate = 1.0, smaller values
/// represent slower rate, larger faster rates.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRate(ST_HANDLE h, float newRate);
/// Sets new tempo control value. Normal tempo = 1.0, smaller values
/// represent slower tempo, larger faster tempo.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setTempo(ST_HANDLE h, float newTempo);
/// Sets new rate control value as a difference in percents compared
/// to the original rate (-50 .. +100 %);
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRateChange(ST_HANDLE h, float newRate);
/// Sets new tempo control value as a difference in percents compared
/// to the original tempo (-50 .. +100 %);
SOUNDTOUCHDLL_API void __cdecl soundtouch_setTempoChange(ST_HANDLE h, float newTempo);
/// Sets new pitch control value. Original pitch = 1.0, smaller values
/// represent lower pitches, larger values higher pitch.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitch(ST_HANDLE h, float newPitch);
/// Sets pitch change in octaves compared to the original pitch
/// (-1.00 .. +1.00);
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitchOctaves(ST_HANDLE h, float newPitch);
/// Sets pitch change in semi-tones compared to the original pitch
/// (-12 .. +12);
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitchSemiTones(ST_HANDLE h, float newPitch);
/// Sets the number of channels, 1 = mono, 2 = stereo, n = multichannel
SOUNDTOUCHDLL_API void __cdecl soundtouch_setChannels(ST_HANDLE h, unsigned int numChannels);
/// Sets sample rate.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setSampleRate(ST_HANDLE h, unsigned int srate);
/// Flushes the last samples from the processing pipeline to the output.
/// Clears also the internal processing buffers.
//
/// Note: This function is meant for extracting the last samples of a sound
/// stream. This function may introduce additional blank samples in the end
/// of the sound stream, and thus it's not recommended to call this function
/// in the middle of a sound stream.
SOUNDTOUCHDLL_API void __cdecl soundtouch_flush(ST_HANDLE h);
/// Adds 'numSamples' pcs of samples from the 'samples' memory position into
/// the input of the object. Notice that sample rate _has_to_ be set before
/// calling this function, otherwise throws a runtime_error exception.
SOUNDTOUCHDLL_API void __cdecl soundtouch_putSamples(ST_HANDLE h,
const float *samples, ///< Pointer to sample buffer.
unsigned int numSamples ///< Number of sample frames in buffer. Notice
///< that in case of multi-channel sound a single
///< sample frame contains data for all channels.
);
/// Clears all the samples in the object's output and internal processing
/// buffers.
SOUNDTOUCHDLL_API void __cdecl soundtouch_clear(ST_HANDLE h);
/// Changes a setting controlling the processing system behaviour. See the
/// 'SETTING_...' defines for available setting ID's.
///
/// \return 'nonzero' if the setting was successfully changed, otherwise zero
SOUNDTOUCHDLL_API int __cdecl soundtouch_setSetting(ST_HANDLE h,
int settingId, ///< Setting ID number. see SETTING_... defines.
int value ///< New setting value.
);
/// Reads a setting controlling the processing system behaviour. See the
/// 'SETTING_...' defines for available setting ID's.
///
/// \return the setting value.
SOUNDTOUCHDLL_API int __cdecl soundtouch_getSetting(ST_HANDLE h,
int settingId ///< Setting ID number, see SETTING_... defines.
);
/// Returns number of samples currently unprocessed.
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_numUnprocessedSamples(ST_HANDLE h);
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
/// sample buffer without copying them anywhere.
///
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
/// with 'ptrBegin' function.
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_receiveSamples(ST_HANDLE h,
float *outBuffer, ///< Buffer where to copy output samples.
unsigned int maxSamples ///< How many samples to receive at max.
);
/// Returns number of samples currently available.
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_numSamples(ST_HANDLE h);
/// Returns nonzero if there aren't any samples available for outputting.
SOUNDTOUCHDLL_API int __cdecl soundtouch_isEmpty(ST_HANDLE h);
#endif // _SoundTouchDLL_h_
@@ -1,25 +0,0 @@
/* uTun: происхождение PCM без изменения DSP. Индексы — сэмплы НА КАНАЛ от clear().
* Copy: out[i] происходит из input0+i. Mix: источники input0+i и input1+i,
* вес второго i/count, первого 1-i/count. Это описание происхождения, не PCM.
* Reset начинает новую шкалу. Invalid запрещает её использование до clear().
* Callback синхронный: нельзя вызывать SoundTouch повторно, блокировать поток или
* сохранять ссылку на событие. Ошибки передаются через Invalid; логирует потребитель. */
#ifndef SOUNDTOUCH_SOURCE_SPAN_H
#define SOUNDTOUCH_SOURCE_SPAN_H
#include <cstdint>
namespace soundtouch {
enum class SourceKind { Reset, Copy, Mix, Invalid };
enum class SourceError { None, Profile, Reconfigure, Flush, LateAttach };
struct SourceSpan {
SourceKind kind;
SourceError error;
uint64_t output, input0, input1;
uint32_t count;
SourceSpan(SourceKind k, SourceError e = SourceError::None, uint64_t out = 0,
uint64_t first = 0, uint64_t second = 0, uint32_t length = 0)
: kind(k), error(e), output(out), input0(first), input1(second), count(length) {}
};
using SourceCallback = void (*)(void*, const SourceSpan&);
}
#endif
-66
View File
@@ -1,66 +0,0 @@
# SoundTouch library
## About
SoundTouch is an open-source audio processing library that allows changing the sound tempo, pitch and playback rate parameters independently from each other:
* Change **tempo** while maintaining the original pitch
* Change **pitch** while maintaining the original tempo
* Change **playback rate** that affects both tempo and pitch at the
same time
* Change any combination of tempo/pitch/rate
Visit [SoundTouch website](https://www.surina.net/soundtouch) and see the [README file](https://www.surina.net/soundtouch/readme.html) for more information and audio examples.
### The latest stable release is 2.3.3
## Example
Use SoundStretch example app for modifying wav audio files, for example as follows:
```
soundstretch my_original_file.wav output_file.wav -tempo=+15 -pitch=-3
```
See the [README file](http://soundtouch.surina.net/README.html) for more usage examples and instructions how to build SoundTouch + SoundStretch.
Ready [SoundStretch application executables](https://www.surina.net/soundtouch/download.html) are available for download for Windows and Mac OS.
## Language & Platforms
SoundTouch is written in C++ and compiles in virtually any platform:
* Windows
* Mac OS
* Linux & Unices (including also Raspberry, Beaglebone, Yocto etc embedded Linux flavors)
* Android
* iOS
* embedded systems
The source code package includes dynamic library import modules for C#, Java and Pascal/Delphi languages.
## Tarballs
Source code release tarballs:
* https://www.surina.net/soundtouch/soundtouch-2.3.3.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.3.2.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.3.1.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.3.0.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.2.0.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.1.2.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.1.1.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.0.0.tar.gz
## License
SoundTouch is released under LGPL v2.1:
This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
See [LGPL v2.1 full license text ](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) for details.
--
Also commercial license free of GPL limitations available upon request
@@ -1,222 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// FIR low-pass (anti-alias) filter with filter coefficient design routine and
/// MMX optimization.
///
/// Anti-alias filter is used to prevent folding of high frequencies when
/// transposing the sample rate with interpolation.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <memory.h>
#include <assert.h>
#include <math.h>
#include <stdlib.h>
#include "AAFilter.h"
#include "FIRFilter.h"
using namespace soundtouch;
#define PI 3.14159265358979323846
#define TWOPI (2 * PI)
// define this to save AA filter coefficients to a file
// #define _DEBUG_SAVE_AAFILTER_COEFFICIENTS 1
#ifdef _DEBUG_SAVE_AAFILTER_COEFFICIENTS
#include <stdio.h>
static void _DEBUG_SAVE_AAFIR_COEFFS(SAMPLETYPE *coeffs, int len)
{
FILE *fptr = fopen("aa_filter_coeffs.txt", "wt");
if (fptr == nullptr) return;
for (int i = 0; i < len; i ++)
{
double temp = coeffs[i];
fprintf(fptr, "%lf\n", temp);
}
fclose(fptr);
}
#else
#define _DEBUG_SAVE_AAFIR_COEFFS(x, y)
#endif
/*****************************************************************************
*
* Implementation of the class 'AAFilter'
*
*****************************************************************************/
AAFilter::AAFilter(uint len)
{
pFIR = FIRFilter::newInstance();
cutoffFreq = 0.5;
setLength(len);
}
AAFilter::~AAFilter()
{
delete pFIR;
}
// Sets new anti-alias filter cut-off edge frequency, scaled to
// sampling frequency (nyquist frequency = 0.5).
// The filter will cut frequencies higher than the given frequency.
void AAFilter::setCutoffFreq(double newCutoffFreq)
{
cutoffFreq = newCutoffFreq;
calculateCoeffs();
}
// Sets number of FIR filter taps
void AAFilter::setLength(uint newLength)
{
length = newLength;
calculateCoeffs();
}
// Calculates coefficients for a low-pass FIR filter using Hamming window
void AAFilter::calculateCoeffs()
{
uint i;
double cntTemp, temp, tempCoeff,h, w;
double wc;
double scaleCoeff, sum;
double *work;
SAMPLETYPE *coeffs;
assert(length >= 2);
assert(length % 4 == 0);
assert(cutoffFreq >= 0);
assert(cutoffFreq <= 0.5);
work = new double[length];
coeffs = new SAMPLETYPE[length];
wc = 2.0 * PI * cutoffFreq;
tempCoeff = TWOPI / (double)length;
sum = 0;
for (i = 0; i < length; i ++)
{
cntTemp = (double)i - (double)(length / 2);
temp = cntTemp * wc;
if (temp != 0)
{
h = sin(temp) / temp; // sinc function
}
else
{
h = 1.0;
}
w = 0.54 + 0.46 * cos(tempCoeff * cntTemp); // hamming window
temp = w * h;
work[i] = temp;
// calc net sum of coefficients
sum += temp;
}
// ensure the sum of coefficients is larger than zero
assert(sum > 0);
// ensure we've really designed a lowpass filter...
assert(work[length/2] > 0);
assert(work[length/2 + 1] > -1e-6);
assert(work[length/2 - 1] > -1e-6);
// Calculate a scaling coefficient in such a way that the result can be
// divided by 16384
scaleCoeff = 16384.0f / sum;
for (i = 0; i < length; i ++)
{
temp = work[i] * scaleCoeff;
// scale & round to nearest integer
temp += (temp >= 0) ? 0.5 : -0.5;
// ensure no overfloods
assert(temp >= -32768 && temp <= 32767);
coeffs[i] = (SAMPLETYPE)temp;
}
// Set coefficients. Use divide factor 14 => divide result by 2^14 = 16384
pFIR->setCoefficients(coeffs, length, 14);
_DEBUG_SAVE_AAFIR_COEFFS(coeffs, length);
delete[] work;
delete[] coeffs;
}
// Applies the filter to the given sequence of samples.
// Note : The amount of outputted samples is by value of 'filter length'
// smaller than the amount of input samples.
uint AAFilter::evaluate(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels) const
{
return pFIR->evaluate(dest, src, numSamples, numChannels);
}
/// Applies the filter to the given src & dest pipes, so that processed amount of
/// samples get removed from src, and produced amount added to dest
/// Note : The amount of outputted samples is by value of 'filter length'
/// smaller than the amount of input samples.
uint AAFilter::evaluate(FIFOSampleBuffer &dest, FIFOSampleBuffer &src) const
{
SAMPLETYPE *pdest;
const SAMPLETYPE *psrc;
uint numSrcSamples;
uint result;
int numChannels = src.getChannels();
assert(numChannels == dest.getChannels());
numSrcSamples = src.numSamples();
psrc = src.ptrBegin();
pdest = dest.ptrEnd(numSrcSamples);
result = pFIR->evaluate(pdest, psrc, numSrcSamples, numChannels);
src.receiveSamples(result);
dest.putSamples(result);
return result;
}
uint AAFilter::getLength() const
{
return pFIR->getLength();
}
@@ -1,93 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Sampled sound tempo changer/time stretch algorithm. Changes the sound tempo
/// while maintaining the original pitch by using a time domain WSOLA-like method
/// with several performance-increasing tweaks.
///
/// Anti-alias filter is used to prevent folding of high frequencies when
/// transposing the sample rate with interpolation.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef AAFilter_H
#define AAFilter_H
#include "STTypes.h"
#include "FIFOSampleBuffer.h"
namespace soundtouch
{
class AAFilter
{
protected:
class FIRFilter *pFIR;
/// Low-pass filter cut-off frequency, negative = invalid
double cutoffFreq;
/// num of filter taps
uint length;
/// Calculate the FIR coefficients realizing the given cutoff-frequency
void calculateCoeffs();
public:
AAFilter(uint length);
~AAFilter();
/// Sets new anti-alias filter cut-off edge frequency, scaled to sampling
/// frequency (nyquist frequency = 0.5). The filter will cut off the
/// frequencies than that.
void setCutoffFreq(double newCutoffFreq);
/// Sets number of FIR filter taps, i.e. ~filter complexity
void setLength(uint newLength);
uint getLength() const;
/// Applies the filter to the given sequence of samples.
/// Note : The amount of outputted samples is by value of 'filter length'
/// smaller than the amount of input samples.
uint evaluate(SAMPLETYPE *dest,
const SAMPLETYPE *src,
uint numSamples,
uint numChannels) const;
/// Applies the filter to the given src & dest pipes, so that processed amount of
/// samples get removed from src, and produced amount added to dest
/// Note : The amount of outputted samples is by value of 'filter length'
/// smaller than the amount of input samples.
uint evaluate(FIFOSampleBuffer &dest,
FIFOSampleBuffer &src) const;
};
}
#endif
@@ -1,569 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Beats-per-minute (BPM) detection routine.
///
/// The beat detection algorithm works as follows:
/// - Use function 'inputSamples' to input a chunks of samples to the class for
/// analysis. It's a good idea to enter a large sound file or stream in smallish
/// chunks of around few kilosamples in order not to extinguish too much RAM memory.
/// - Inputted sound data is decimated to approx 500 Hz to reduce calculation burden,
/// which is basically ok as low (bass) frequencies mostly determine the beat rate.
/// Simple averaging is used for anti-alias filtering because the resulting signal
/// quality isn't of that high importance.
/// - Decimated sound data is enveloped, i.e. the amplitude shape is detected by
/// taking absolute value that's smoothed by sliding average. Signal levels that
/// are below a couple of times the general RMS amplitude level are cut away to
/// leave only notable peaks there.
/// - Repeating sound patterns (e.g. beats) are detected by calculating short-term
/// autocorrelation function of the enveloped signal.
/// - After whole sound data file has been analyzed as above, the bpm level is
/// detected by function 'getBpm' that finds the highest peak of the autocorrelation
/// function, calculates it's precise location and converts this reading to bpm's.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#define _USE_MATH_DEFINES
#include <math.h>
#include <assert.h>
#include <string.h>
#include <stdio.h>
#include <cfloat>
#include "FIFOSampleBuffer.h"
#include "PeakFinder.h"
#include "BPMDetect.h"
using namespace soundtouch;
// algorithm input sample block size
static const int INPUT_BLOCK_SIZE = 2048;
// decimated sample block size
static const int DECIMATED_BLOCK_SIZE = 256;
/// Target sample rate after decimation
static const int TARGET_SRATE = 1000;
/// XCorr update sequence size, update in about 200msec chunks
static const int XCORR_UPDATE_SEQUENCE = (int)(TARGET_SRATE / 5);
/// Moving average N size
static const int MOVING_AVERAGE_N = 15;
/// XCorr decay time constant, decay to half in 30 seconds
/// If it's desired to have the system adapt quicker to beat rate
/// changes within a continuing music stream, then the
/// 'xcorr_decay_time_constant' value can be reduced, yet that
/// can increase possibility of glitches in bpm detection.
static const double XCORR_DECAY_TIME_CONSTANT = 30.0;
/// Data overlap factor for beat detection algorithm
static const int OVERLAP_FACTOR = 4;
static const double TWOPI = (2 * M_PI);
////////////////////////////////////////////////////////////////////////////////
// Enable following define to create bpm analysis file:
//#define _CREATE_BPM_DEBUG_FILE
#ifdef _CREATE_BPM_DEBUG_FILE
static void _SaveDebugData(const char *name, const float *data, int minpos, int maxpos, double coeff)
{
FILE *fptr = fopen(name, "wt");
int i;
if (fptr)
{
printf("\nWriting BPM debug data into file %s\n", name);
for (i = minpos; i < maxpos; i ++)
{
fprintf(fptr, "%d\t%.1lf\t%f\n", i, coeff / (double)i, data[i]);
}
fclose(fptr);
}
}
void _SaveDebugBeatPos(const char *name, const std::vector<BEAT> &beats)
{
printf("\nWriting beat detections data into file %s\n", name);
FILE *fptr = fopen(name, "wt");
if (fptr)
{
for (uint i = 0; i < beats.size(); i++)
{
BEAT b = beats[i];
fprintf(fptr, "%lf\t%lf\n", b.pos, b.strength);
}
fclose(fptr);
}
}
#else
#define _SaveDebugData(name, a,b,c,d)
#define _SaveDebugBeatPos(name, b)
#endif
// Hamming window
void hamming(float *w, int N)
{
for (int i = 0; i < N; i++)
{
w[i] = (float)(0.54 - 0.46 * cos(TWOPI * i / (N - 1)));
}
}
////////////////////////////////////////////////////////////////////////////////
//
// IIR2_filter - 2nd order IIR filter
IIR2_filter::IIR2_filter(const double *lpf_coeffs)
{
memcpy(coeffs, lpf_coeffs, 5 * sizeof(double));
memset(prev, 0, sizeof(prev));
}
float IIR2_filter::update(float x)
{
prev[0] = x;
double y = x * coeffs[0];
for (int i = 4; i >= 1; i--)
{
y += coeffs[i] * prev[i];
prev[i] = prev[i - 1];
}
prev[3] = y;
return (float)y;
}
// IIR low-pass filter coefficients, calculated with matlab/octave cheby2(2,40,0.05)
const double _LPF_coeffs[5] = { 0.00996655391939, -0.01944529148401, 0.00996655391939, 1.96867605796247, -0.96916387431724 };
////////////////////////////////////////////////////////////////////////////////
BPMDetect::BPMDetect(int numChannels, int aSampleRate) :
beat_lpf(_LPF_coeffs)
{
beats.reserve(250); // initial reservation to prevent frequent reallocation
this->sampleRate = aSampleRate;
this->channels = numChannels;
decimateSum = 0;
decimateCount = 0;
// choose decimation factor so that result is approx. 1000 Hz
decimateBy = sampleRate / TARGET_SRATE;
if ((decimateBy <= 0) || (decimateBy * DECIMATED_BLOCK_SIZE < INPUT_BLOCK_SIZE))
{
ST_THROW_RT_ERROR("Too small samplerate");
}
// Calculate window length & starting item according to desired min & max bpms
windowLen = (60 * sampleRate) / (decimateBy * MIN_BPM);
windowStart = (60 * sampleRate) / (decimateBy * MAX_BPM_RANGE);
assert(windowLen > windowStart);
// allocate new working objects
xcorr = new float[windowLen];
memset(xcorr, 0, windowLen * sizeof(float));
pos = 0;
peakPos = 0;
peakVal = 0;
init_scaler = 1;
beatcorr_ringbuffpos = 0;
beatcorr_ringbuff = new float[windowLen];
memset(beatcorr_ringbuff, 0, windowLen * sizeof(float));
// allocate processing buffer
buffer = new FIFOSampleBuffer();
// we do processing in mono mode
buffer->setChannels(1);
buffer->clear();
// calculate hamming windows
hamw = new float[XCORR_UPDATE_SEQUENCE];
hamming(hamw, XCORR_UPDATE_SEQUENCE);
hamw2 = new float[XCORR_UPDATE_SEQUENCE / 2];
hamming(hamw2, XCORR_UPDATE_SEQUENCE / 2);
}
BPMDetect::~BPMDetect()
{
delete[] xcorr;
delete[] beatcorr_ringbuff;
delete[] hamw;
delete[] hamw2;
delete buffer;
}
/// convert to mono, low-pass filter & decimate to about 500 Hz.
/// return number of outputted samples.
///
/// Decimation is used to remove the unnecessary frequencies and thus to reduce
/// the amount of data needed to be processed as calculating autocorrelation
/// function is a very-very heavy operation.
///
/// Anti-alias filtering is done simply by averaging the samples. This is really a
/// poor-man's anti-alias filtering, but it's not so critical in this kind of application
/// (it'd also be difficult to design a high-quality filter with steep cut-off at very
/// narrow band)
int BPMDetect::decimate(SAMPLETYPE *dest, const SAMPLETYPE *src, int numsamples)
{
int count, outcount;
LONG_SAMPLETYPE out;
assert(channels > 0);
assert(decimateBy > 0);
outcount = 0;
for (count = 0; count < numsamples; count ++)
{
int j;
// convert to mono and accumulate
for (j = 0; j < channels; j ++)
{
decimateSum += src[j];
}
src += j;
decimateCount ++;
if (decimateCount >= decimateBy)
{
// Store every Nth sample only
out = (LONG_SAMPLETYPE)(decimateSum / (decimateBy * channels));
decimateSum = 0;
decimateCount = 0;
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
// check ranges for sure (shouldn't actually be necessary)
if (out > 32767)
{
out = 32767;
}
else if (out < -32768)
{
out = -32768;
}
#endif // SOUNDTOUCH_INTEGER_SAMPLES
dest[outcount] = (SAMPLETYPE)out;
outcount ++;
}
}
return outcount;
}
// Calculates autocorrelation function of the sample history buffer
void BPMDetect::updateXCorr(int process_samples)
{
int offs;
SAMPLETYPE *pBuffer;
assert(buffer->numSamples() >= (uint)(process_samples + windowLen));
assert(process_samples == XCORR_UPDATE_SEQUENCE);
pBuffer = buffer->ptrBegin();
// calculate decay factor for xcorr filtering
float xcorr_decay = (float)pow(0.5, 1.0 / (XCORR_DECAY_TIME_CONSTANT * TARGET_SRATE / process_samples));
// prescale pbuffer
float tmp[XCORR_UPDATE_SEQUENCE];
for (int i = 0; i < process_samples; i++)
{
tmp[i] = hamw[i] * hamw[i] * pBuffer[i];
}
for (offs = windowStart; offs < windowLen; offs ++)
{
float sum;
int i;
sum = 0;
for (i = 0; i < process_samples; i ++)
{
sum += tmp[i] * pBuffer[i + offs]; // scaling the sub-result shouldn't be necessary
}
xcorr[offs] *= xcorr_decay; // decay 'xcorr' here with suitable time constant.
xcorr[offs] += (float)fabs(sum);
}
}
// Detect individual beat positions
void BPMDetect::updateBeatPos(int process_samples)
{
SAMPLETYPE *pBuffer;
assert(buffer->numSamples() >= (uint)(process_samples + windowLen));
pBuffer = buffer->ptrBegin();
assert(process_samples == XCORR_UPDATE_SEQUENCE / 2);
// static double thr = 0.0003;
double posScale = (double)this->decimateBy / (double)this->sampleRate;
int resetDur = (int)(0.12 / posScale + 0.5);
// prescale pbuffer
float tmp[XCORR_UPDATE_SEQUENCE / 2];
for (int i = 0; i < process_samples; i++)
{
tmp[i] = hamw2[i] * hamw2[i] * pBuffer[i];
}
for (int offs = windowStart; offs < windowLen; offs++)
{
float sum = 0;
for (int i = 0; i < process_samples; i++)
{
sum += tmp[i] * pBuffer[offs + i];
}
beatcorr_ringbuff[(beatcorr_ringbuffpos + offs) % windowLen] += (float)((sum > 0) ? sum : 0); // accumulate only positive correlations
}
int skipstep = XCORR_UPDATE_SEQUENCE / OVERLAP_FACTOR;
// compensate empty buffer at beginning by scaling coefficient
float scale = (float)windowLen / (float)(skipstep * init_scaler);
if (scale > 1.0f)
{
init_scaler++;
}
else
{
scale = 1.0f;
}
// detect beats
for (int i = 0; i < skipstep; i++)
{
float sum = beatcorr_ringbuff[beatcorr_ringbuffpos];
sum -= beat_lpf.update(sum);
if (sum > peakVal)
{
// found new local largest value
peakVal = sum;
peakPos = pos;
}
if (pos > peakPos + resetDur)
{
// largest value not updated for 200msec => accept as beat
peakPos += skipstep;
if (peakVal > 0)
{
// add detected beat to end of "beats" vector
BEAT temp = { (float)(peakPos * posScale), (float)(peakVal * scale) };
beats.push_back(temp);
}
peakVal = 0;
peakPos = pos;
}
beatcorr_ringbuff[beatcorr_ringbuffpos] = 0;
pos++;
beatcorr_ringbuffpos = (beatcorr_ringbuffpos + 1) % windowLen;
}
}
#define max(x,y) ((x) > (y) ? (x) : (y))
void BPMDetect::inputSamples(const SAMPLETYPE *samples, int numSamples)
{
SAMPLETYPE decimated[DECIMATED_BLOCK_SIZE];
// iterate so that max INPUT_BLOCK_SAMPLES processed per iteration
while (numSamples > 0)
{
int block;
int decSamples;
block = (numSamples > INPUT_BLOCK_SIZE) ? INPUT_BLOCK_SIZE : numSamples;
// decimate. note that converts to mono at the same time
decSamples = decimate(decimated, samples, block);
samples += block * channels;
numSamples -= block;
buffer->putSamples(decimated, decSamples);
}
// when the buffer has enough samples for processing...
int req = max(windowLen + XCORR_UPDATE_SEQUENCE, 2 * XCORR_UPDATE_SEQUENCE);
while ((int)buffer->numSamples() >= req)
{
// ... update autocorrelations...
updateXCorr(XCORR_UPDATE_SEQUENCE);
// ...update beat position calculation...
updateBeatPos(XCORR_UPDATE_SEQUENCE / 2);
// ... and remove proceessed samples from the buffer
int n = XCORR_UPDATE_SEQUENCE / OVERLAP_FACTOR;
buffer->receiveSamples(n);
}
}
void BPMDetect::removeBias()
{
int i;
// Remove linear bias: calculate linear regression coefficient
// 1. calc mean of 'xcorr' and 'i'
double mean_i = 0;
double mean_x = 0;
for (i = windowStart; i < windowLen; i++)
{
mean_x += xcorr[i];
}
mean_x /= (windowLen - windowStart);
mean_i = 0.5 * (windowLen - 1 + windowStart);
// 2. calculate linear regression coefficient
double b = 0;
double div = 0;
for (i = windowStart; i < windowLen; i++)
{
double xt = xcorr[i] - mean_x;
double xi = i - mean_i;
b += xt * xi;
div += xi * xi;
}
b /= div;
// subtract linear regression and resolve min. value bias
float minval = FLT_MAX; // arbitrary large number
for (i = windowStart; i < windowLen; i ++)
{
xcorr[i] -= (float)(b * i);
if (xcorr[i] < minval)
{
minval = xcorr[i];
}
}
// subtract min.value
for (i = windowStart; i < windowLen; i ++)
{
xcorr[i] -= minval;
}
}
// Calculate N-point moving average for "source" values
void MAFilter(float *dest, const float *source, int start, int end, int N)
{
for (int i = start; i < end; i++)
{
int i1 = i - N / 2;
int i2 = i + N / 2 + 1;
if (i1 < start) i1 = start;
if (i2 > end) i2 = end;
double sum = 0;
for (int j = i1; j < i2; j ++)
{
sum += source[j];
}
dest[i] = (float)(sum / (i2 - i1));
}
}
float BPMDetect::getBpm()
{
double peakPos;
double coeff;
PeakFinder peakFinder;
// remove bias from xcorr data
removeBias();
coeff = 60.0 * ((double)sampleRate / (double)decimateBy);
// save bpm debug data if debug data writing enabled
_SaveDebugData("soundtouch-bpm-xcorr.txt", xcorr, windowStart, windowLen, coeff);
// Smoothen by N-point moving-average
float *data = new float[windowLen];
memset(data, 0, sizeof(float) * windowLen);
MAFilter(data, xcorr, windowStart, windowLen, MOVING_AVERAGE_N);
// find peak position
peakPos = peakFinder.detectPeak(data, windowStart, windowLen);
// save bpm debug data if debug data writing enabled
_SaveDebugData("soundtouch-bpm-smoothed.txt", data, windowStart, windowLen, coeff);
delete[] data;
assert(decimateBy != 0);
if (peakPos < 1e-9) return 0.0; // detection failed.
_SaveDebugBeatPos("soundtouch-detected-beats.txt", beats);
// calculate BPM
float bpm = (float)(coeff / peakPos);
return (bpm >= MIN_BPM && bpm <= MAX_BPM_VALID) ? bpm : 0;
}
/// Get beat position arrays. Note: The array includes also really low beat detection values
/// in absence of clear strong beats. Consumer may wish to filter low values away.
/// - "pos" receive array of beat positions
/// - "values" receive array of beat detection strengths
/// - max_num indicates max.size of "pos" and "values" array.
///
/// You can query a suitable array sized by calling this with nullptr in "pos" & "values".
///
/// \return number of beats in the arrays.
int BPMDetect::getBeats(float *pos, float *values, int max_num)
{
int num = (int)beats.size();
if ((!pos) || (!values)) return num; // pos or values nullptr, return just size
for (int i = 0; (i < num) && (i < max_num); i++)
{
pos[i] = beats[i].pos;
values[i] = beats[i].strength;
}
return num;
}
@@ -1,275 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// A buffer class for temporarily storaging sound samples, operates as a
/// first-in-first-out pipe.
///
/// Samples are added to the end of the sample buffer with the 'putSamples'
/// function, and are received from the beginning of the buffer by calling
/// the 'receiveSamples' function. The class automatically removes the
/// outputted samples from the buffer, as well as grows the buffer size
/// whenever necessary.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <stdlib.h>
#include <memory.h>
#include <string.h>
#include <assert.h>
#include "FIFOSampleBuffer.h"
using namespace soundtouch;
// Constructor
FIFOSampleBuffer::FIFOSampleBuffer(int numChannels)
{
assert(numChannels > 0);
sizeInBytes = 0; // reasonable initial value
buffer = nullptr;
bufferUnaligned = nullptr;
samplesInBuffer = 0;
bufferPos = 0;
channels = (uint)numChannels;
ensureCapacity(32); // allocate initial capacity
}
// destructor
FIFOSampleBuffer::~FIFOSampleBuffer()
{
delete[] bufferUnaligned;
bufferUnaligned = nullptr;
buffer = nullptr;
}
// Sets number of channels, 1 = mono, 2 = stereo
void FIFOSampleBuffer::setChannels(int numChannels)
{
uint usedBytes;
if (!verifyNumberOfChannels(numChannels)) return;
usedBytes = channels * samplesInBuffer;
channels = (uint)numChannels;
samplesInBuffer = usedBytes / channels;
}
// if output location pointer 'bufferPos' isn't zero, 'rewinds' the buffer and
// zeroes this pointer by copying samples from the 'bufferPos' pointer
// location on to the beginning of the buffer.
void FIFOSampleBuffer::rewind()
{
if (buffer && bufferPos)
{
memmove(buffer, ptrBegin(), sizeof(SAMPLETYPE) * channels * samplesInBuffer);
bufferPos = 0;
}
}
// Adds 'numSamples' pcs of samples from the 'samples' memory position to
// the sample buffer.
void FIFOSampleBuffer::putSamples(const SAMPLETYPE *samples, uint nSamples)
{
memcpy(ptrEnd(nSamples), samples, sizeof(SAMPLETYPE) * nSamples * channels);
samplesInBuffer += nSamples;
}
// Increases the number of samples in the buffer without copying any actual
// samples.
//
// This function is used to update the number of samples in the sample buffer
// when accessing the buffer directly with 'ptrEnd' function. Please be
// careful though!
void FIFOSampleBuffer::putSamples(uint nSamples)
{
uint req;
req = samplesInBuffer + nSamples;
ensureCapacity(req);
samplesInBuffer += nSamples;
}
// Returns a pointer to the end of the used part of the sample buffer (i.e.
// where the new samples are to be inserted). This function may be used for
// inserting new samples into the sample buffer directly. Please be careful!
//
// Parameter 'slackCapacity' tells the function how much free capacity (in
// terms of samples) there _at least_ should be, in order to the caller to
// successfully insert all the required samples to the buffer. When necessary,
// the function grows the buffer size to comply with this requirement.
//
// When using this function as means for inserting new samples, also remember
// to increase the sample count afterwards, by calling the
// 'putSamples(numSamples)' function.
SAMPLETYPE *FIFOSampleBuffer::ptrEnd(uint slackCapacity)
{
ensureCapacity(samplesInBuffer + slackCapacity);
return buffer + samplesInBuffer * channels;
}
// Returns a pointer to the beginning of the currently non-outputted samples.
// This function is provided for accessing the output samples directly.
// Please be careful!
//
// When using this function to output samples, also remember to 'remove' the
// outputted samples from the buffer by calling the
// 'receiveSamples(numSamples)' function
SAMPLETYPE *FIFOSampleBuffer::ptrBegin()
{
assert(buffer);
return buffer + bufferPos * channels;
}
// Ensures that the buffer has enough capacity, i.e. space for _at least_
// 'capacityRequirement' number of samples. The buffer is grown in steps of
// 4 kilobytes to eliminate the need for frequently growing up the buffer,
// as well as to round the buffer size up to the virtual memory page size.
void FIFOSampleBuffer::ensureCapacity(uint capacityRequirement)
{
SAMPLETYPE *tempUnaligned, *temp;
if (capacityRequirement > getCapacity())
{
// enlarge the buffer in 4kbyte steps (round up to next 4k boundary)
sizeInBytes = (capacityRequirement * channels * sizeof(SAMPLETYPE) + 4095) & (uint)-4096;
assert(sizeInBytes % 2 == 0);
tempUnaligned = new SAMPLETYPE[sizeInBytes / sizeof(SAMPLETYPE) + 16 / sizeof(SAMPLETYPE)];
if (tempUnaligned == nullptr)
{
ST_THROW_RT_ERROR("Couldn't allocate memory!\n");
}
// Align the buffer to begin at 16byte cache line boundary for optimal performance
temp = (SAMPLETYPE *)SOUNDTOUCH_ALIGN_POINTER_16(tempUnaligned);
if (samplesInBuffer)
{
memcpy(temp, ptrBegin(), samplesInBuffer * channels * sizeof(SAMPLETYPE));
}
delete[] bufferUnaligned;
buffer = temp;
bufferUnaligned = tempUnaligned;
bufferPos = 0;
}
else
{
// simply rewind the buffer (if necessary)
rewind();
}
}
// Returns the current buffer capacity in terms of samples
uint FIFOSampleBuffer::getCapacity() const
{
return sizeInBytes / (channels * sizeof(SAMPLETYPE));
}
// Returns the number of samples currently in the buffer
uint FIFOSampleBuffer::numSamples() const
{
return samplesInBuffer;
}
// Output samples from beginning of the sample buffer. Copies demanded number
// of samples to output and removes them from the sample buffer. If there
// are less than 'numsample' samples in the buffer, returns all available.
//
// Returns number of samples copied.
uint FIFOSampleBuffer::receiveSamples(SAMPLETYPE *output, uint maxSamples)
{
uint num;
num = (maxSamples > samplesInBuffer) ? samplesInBuffer : maxSamples;
memcpy(output, ptrBegin(), channels * sizeof(SAMPLETYPE) * num);
return receiveSamples(num);
}
// Removes samples from the beginning of the sample buffer without copying them
// anywhere. Used to reduce the number of samples in the buffer, when accessing
// the sample buffer with the 'ptrBegin' function.
uint FIFOSampleBuffer::receiveSamples(uint maxSamples)
{
if (maxSamples >= samplesInBuffer)
{
uint temp;
temp = samplesInBuffer;
samplesInBuffer = 0;
return temp;
}
samplesInBuffer -= maxSamples;
bufferPos += maxSamples;
return maxSamples;
}
// Returns nonzero if the sample buffer is empty
int FIFOSampleBuffer::isEmpty() const
{
return (samplesInBuffer == 0) ? 1 : 0;
}
// Clears the sample buffer
void FIFOSampleBuffer::clear()
{
samplesInBuffer = 0;
bufferPos = 0;
}
/// allow trimming (downwards) amount of samples in pipeline.
/// Returns adjusted amount of samples
uint FIFOSampleBuffer::adjustAmountOfSamples(uint numSamples)
{
if (numSamples < samplesInBuffer)
{
samplesInBuffer = numSamples;
}
return samplesInBuffer;
}
/// Add silence to end of buffer
void FIFOSampleBuffer::addSilent(uint nSamples)
{
memset(ptrEnd(nSamples), 0, sizeof(SAMPLETYPE) * nSamples * channels);
samplesInBuffer += nSamples;
}
@@ -1,312 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// General FIR digital filter routines with MMX optimization.
///
/// Notes : MMX optimized functions reside in a separate, platform-specific file,
/// e.g. 'mmx_win.cpp' or 'mmx_gcc.cpp'
///
/// This source file contains OpenMP optimizations that allow speeding up the
/// corss-correlation algorithm by executing it in several threads / CPU cores
/// in parallel. See the following article link for more detailed discussion
/// about SoundTouch OpenMP optimizations:
/// http://www.softwarecoven.com/parallel-computing-in-embedded-mobile-devices
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <memory.h>
#include <assert.h>
#include <math.h>
#include <stdlib.h>
#include "FIRFilter.h"
#include "cpu_detect.h"
using namespace soundtouch;
/*****************************************************************************
*
* Implementation of the class 'FIRFilter'
*
*****************************************************************************/
FIRFilter::FIRFilter()
{
resultDivFactor = 0;
resultDivider = 0;
length = 0;
lengthDiv8 = 0;
filterCoeffs = nullptr;
filterCoeffsStereo = nullptr;
}
FIRFilter::~FIRFilter()
{
delete[] filterCoeffs;
delete[] filterCoeffsStereo;
}
// Usual C-version of the filter routine for stereo sound
uint FIRFilter::evaluateFilterStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples) const
{
int j, end;
// hint compiler autovectorization that loop length is divisible by 8
uint ilength = length & -8;
assert((length != 0) && (length == ilength) && (src != nullptr) && (dest != nullptr) && (filterCoeffs != nullptr));
assert(numSamples > ilength);
end = 2 * (numSamples - ilength);
for (j = 0; j < end; j += 2)
{
const SAMPLETYPE *ptr;
LONG_SAMPLETYPE suml, sumr;
suml = sumr = 0;
ptr = src + j;
for (uint i = 0; i < ilength; i ++)
{
suml += ptr[2 * i] * filterCoeffsStereo[2 * i];
sumr += ptr[2 * i + 1] * filterCoeffsStereo[2 * i + 1];
}
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
suml >>= resultDivFactor;
sumr >>= resultDivFactor;
// saturate to 16 bit integer limits
suml = (suml < -32768) ? -32768 : (suml > 32767) ? 32767 : suml;
// saturate to 16 bit integer limits
sumr = (sumr < -32768) ? -32768 : (sumr > 32767) ? 32767 : sumr;
#endif // SOUNDTOUCH_INTEGER_SAMPLES
dest[j] = (SAMPLETYPE)suml;
dest[j + 1] = (SAMPLETYPE)sumr;
}
return numSamples - ilength;
}
// Usual C-version of the filter routine for mono sound
uint FIRFilter::evaluateFilterMono(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples) const
{
int j, end;
// hint compiler autovectorization that loop length is divisible by 8
int ilength = length & -8;
assert(ilength != 0);
end = numSamples - ilength;
for (j = 0; j < end; j ++)
{
const SAMPLETYPE *pSrc = src + j;
LONG_SAMPLETYPE sum;
int i;
sum = 0;
for (i = 0; i < ilength; i ++)
{
sum += pSrc[i] * filterCoeffs[i];
}
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
sum >>= resultDivFactor;
// saturate to 16 bit integer limits
sum = (sum < -32768) ? -32768 : (sum > 32767) ? 32767 : sum;
#endif // SOUNDTOUCH_INTEGER_SAMPLES
dest[j] = (SAMPLETYPE)sum;
}
return end;
}
uint FIRFilter::evaluateFilterMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels)
{
int j, end;
assert(length != 0);
assert(src != nullptr);
assert(dest != nullptr);
assert(filterCoeffs != nullptr);
assert(numChannels < 16);
// hint compiler autovectorization that loop length is divisible by 8
int ilength = length & -8;
end = numChannels * (numSamples - ilength);
for (j = 0; j < end; j += numChannels)
{
const SAMPLETYPE *ptr;
LONG_SAMPLETYPE sums[16];
uint c;
int i;
for (c = 0; c < numChannels; c ++)
{
sums[c] = 0;
}
ptr = src + j;
for (i = 0; i < ilength; i ++)
{
SAMPLETYPE coef=filterCoeffs[i];
for (c = 0; c < numChannels; c ++)
{
sums[c] += ptr[0] * coef;
ptr ++;
}
}
for (c = 0; c < numChannels; c ++)
{
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
sums[c] >>= resultDivFactor;
#endif // SOUNDTOUCH_INTEGER_SAMPLES
dest[j+c] = (SAMPLETYPE)sums[c];
}
}
return numSamples - ilength;
}
// Set filter coeffiecients and length.
//
// Throws an exception if filter length isn't divisible by 8
void FIRFilter::setCoefficients(const SAMPLETYPE *coeffs, uint newLength, uint uResultDivFactor)
{
assert(newLength > 0);
if (newLength % 8) ST_THROW_RT_ERROR("FIR filter length not divisible by 8");
#ifdef SOUNDTOUCH_FLOAT_SAMPLES
// scale coefficients already here if using floating samples
double scale = 1.0 / resultDivider;
#else
short scale = 1;
#endif
lengthDiv8 = newLength / 8;
length = lengthDiv8 * 8;
assert(length == newLength);
resultDivFactor = uResultDivFactor;
resultDivider = (SAMPLETYPE)::pow(2.0, (int)resultDivFactor);
delete[] filterCoeffs;
filterCoeffs = new SAMPLETYPE[length];
delete[] filterCoeffsStereo;
filterCoeffsStereo = new SAMPLETYPE[length*2];
for (uint i = 0; i < length; i ++)
{
filterCoeffs[i] = (SAMPLETYPE)(coeffs[i] * scale);
// create also stereo set of filter coefficients: this allows compiler
// to autovectorize filter evaluation much more efficiently
filterCoeffsStereo[2 * i] = (SAMPLETYPE)(coeffs[i] * scale);
filterCoeffsStereo[2 * i + 1] = (SAMPLETYPE)(coeffs[i] * scale);
}
}
uint FIRFilter::getLength() const
{
return length;
}
// Applies the filter to the given sequence of samples.
//
// Note : The amount of outputted samples is by value of 'filter_length'
// smaller than the amount of input samples.
uint FIRFilter::evaluate(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels)
{
assert(length > 0);
assert(lengthDiv8 * 8 == length);
if (numSamples < length) return 0;
#ifndef USE_MULTICH_ALWAYS
if (numChannels == 1)
{
return evaluateFilterMono(dest, src, numSamples);
}
else if (numChannels == 2)
{
return evaluateFilterStereo(dest, src, numSamples);
}
else
#endif // USE_MULTICH_ALWAYS
{
assert(numChannels > 0);
return evaluateFilterMulti(dest, src, numSamples, numChannels);
}
}
// Operator 'new' is overloaded so that it automatically creates a suitable instance
// depending on if we've a MMX-capable CPU available or not.
void * FIRFilter::operator new(size_t)
{
// Notice! don't use "new FIRFilter" directly, use "newInstance" to create a new instance instead!
ST_THROW_RT_ERROR("Error in FIRFilter::new: Don't use 'new FIRFilter', use 'newInstance' member instead!");
return newInstance();
}
FIRFilter * FIRFilter::newInstance()
{
uint uExtensions;
uExtensions = detectCPUextensions();
(void)uExtensions;
// Check if MMX/SSE instruction set extensions supported by CPU
#ifdef SOUNDTOUCH_ALLOW_MMX
// MMX routines available only with integer sample types
if (uExtensions & SUPPORT_MMX)
{
return ::new FIRFilterMMX;
}
else
#endif // SOUNDTOUCH_ALLOW_MMX
#ifdef SOUNDTOUCH_ALLOW_SSE
if (uExtensions & SUPPORT_SSE)
{
// SSE support
return ::new FIRFilterSSE;
}
else
#endif // SOUNDTOUCH_ALLOW_SSE
{
// ISA optimizations not supported, use plain C version
return ::new FIRFilter;
}
}
@@ -1,140 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// General FIR digital filter routines with MMX optimization.
///
/// Note : MMX optimized functions reside in a separate, platform-specific file,
/// e.g. 'mmx_win.cpp' or 'mmx_gcc.cpp'
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef FIRFilter_H
#define FIRFilter_H
#include <stddef.h>
#include "STTypes.h"
namespace soundtouch
{
class FIRFilter
{
protected:
// Number of FIR filter taps
uint length;
// Number of FIR filter taps divided by 8
uint lengthDiv8;
// Result divider factor in 2^k format
uint resultDivFactor;
// Result divider value.
SAMPLETYPE resultDivider;
// Memory for filter coefficients
SAMPLETYPE *filterCoeffs;
SAMPLETYPE *filterCoeffsStereo;
virtual uint evaluateFilterStereo(SAMPLETYPE *dest,
const SAMPLETYPE *src,
uint numSamples) const;
virtual uint evaluateFilterMono(SAMPLETYPE *dest,
const SAMPLETYPE *src,
uint numSamples) const;
virtual uint evaluateFilterMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels);
public:
FIRFilter();
virtual ~FIRFilter();
/// Operator 'new' is overloaded so that it automatically creates a suitable instance
/// depending on if we've a MMX-capable CPU available or not.
static void * operator new(size_t s);
static FIRFilter *newInstance();
/// Applies the filter to the given sequence of samples.
/// Note : The amount of outputted samples is by value of 'filter_length'
/// smaller than the amount of input samples.
///
/// \return Number of samples copied to 'dest'.
uint evaluate(SAMPLETYPE *dest,
const SAMPLETYPE *src,
uint numSamples,
uint numChannels);
uint getLength() const;
virtual void setCoefficients(const SAMPLETYPE *coeffs,
uint newLength,
uint uResultDivFactor);
};
// Optional subclasses that implement CPU-specific optimizations:
#ifdef SOUNDTOUCH_ALLOW_MMX
/// Class that implements MMX optimized functions exclusive for 16bit integer samples type.
class FIRFilterMMX : public FIRFilter
{
protected:
short *filterCoeffsUnalign;
short *filterCoeffsAlign;
virtual uint evaluateFilterStereo(short *dest, const short *src, uint numSamples) const override;
public:
FIRFilterMMX();
~FIRFilterMMX();
virtual void setCoefficients(const short *coeffs, uint newLength, uint uResultDivFactor) override;
};
#endif // SOUNDTOUCH_ALLOW_MMX
#ifdef SOUNDTOUCH_ALLOW_SSE
/// Class that implements SSE optimized functions exclusive for floating point samples type.
class FIRFilterSSE : public FIRFilter
{
protected:
float *filterCoeffsUnalign;
float *filterCoeffsAlign;
virtual uint evaluateFilterStereo(float *dest, const float *src, uint numSamples) const override;
public:
FIRFilterSSE();
~FIRFilterSSE();
virtual void setCoefficients(const float *coeffs, uint newLength, uint uResultDivFactor) override;
};
#endif // SOUNDTOUCH_ALLOW_SSE
}
#endif // FIRFilter_H
@@ -1,196 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Cubic interpolation routine.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <stddef.h>
#include <math.h>
#include "InterpolateCubic.h"
#include "STTypes.h"
using namespace soundtouch;
// cubic interpolation coefficients
static const float _coeffs[]=
{ -0.5f, 1.0f, -0.5f, 0.0f,
1.5f, -2.5f, 0.0f, 1.0f,
-1.5f, 2.0f, 0.5f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f};
InterpolateCubic::InterpolateCubic()
{
fract = 0;
}
void InterpolateCubic::resetRegisters()
{
fract = 0;
}
/// Transpose mono audio. Returns number of produced output samples, and
/// updates "srcSamples" to amount of consumed source samples
int InterpolateCubic::transposeMono(SAMPLETYPE *pdest,
const SAMPLETYPE *psrc,
int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 4;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
float out;
const float x3 = 1.0f;
const float x2 = (float)fract; // x
const float x1 = x2*x2; // x^2
const float x0 = x1*x2; // x^3
float y0, y1, y2, y3;
assert(fract < 1.0);
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3;
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3;
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3;
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3;
out = y0 * psrc[0] + y1 * psrc[1] + y2 * psrc[2] + y3 * psrc[3];
pdest[i] = (SAMPLETYPE)out;
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
psrc += whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}
/// Transpose stereo audio. Returns number of produced output samples, and
/// updates "srcSamples" to amount of consumed source samples
int InterpolateCubic::transposeStereo(SAMPLETYPE *pdest,
const SAMPLETYPE *psrc,
int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 4;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
const float x3 = 1.0f;
const float x2 = (float)fract; // x
const float x1 = x2*x2; // x^2
const float x0 = x1*x2; // x^3
float y0, y1, y2, y3;
float out0, out1;
assert(fract < 1.0);
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3;
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3;
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3;
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3;
out0 = y0 * psrc[0] + y1 * psrc[2] + y2 * psrc[4] + y3 * psrc[6];
out1 = y0 * psrc[1] + y1 * psrc[3] + y2 * psrc[5] + y3 * psrc[7];
pdest[2*i] = (SAMPLETYPE)out0;
pdest[2*i+1] = (SAMPLETYPE)out1;
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
psrc += 2*whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}
/// Transpose multi-channel audio. Returns number of produced output samples, and
/// updates "srcSamples" to amount of consumed source samples
int InterpolateCubic::transposeMulti(SAMPLETYPE *pdest,
const SAMPLETYPE *psrc,
int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 4;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
const float x3 = 1.0f;
const float x2 = (float)fract; // x
const float x1 = x2*x2; // x^2
const float x0 = x1*x2; // x^3
float y0, y1, y2, y3;
assert(fract < 1.0);
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3;
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3;
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3;
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3;
for (int c = 0; c < numChannels; c ++)
{
float out;
out = y0 * psrc[c] + y1 * psrc[c + numChannels] + y2 * psrc[c + 2 * numChannels] + y3 * psrc[c + 3 * numChannels];
pdest[0] = (SAMPLETYPE)out;
pdest ++;
}
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
psrc += numChannels*whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}
@@ -1,72 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Cubic interpolation routine.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef _InterpolateCubic_H_
#define _InterpolateCubic_H_
#include "RateTransposer.h"
#include "STTypes.h"
namespace soundtouch
{
class InterpolateCubic : public TransposerBase
{
protected:
virtual int transposeMono(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) override;
virtual int transposeStereo(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) override;
virtual int transposeMulti(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) override;
double fract;
public:
InterpolateCubic();
virtual void resetRegisters() override;
// При fract=0 кубический интерполятор копирует psrc[1], сохраняя четыре входных сэмпла.
int unitySourceOffset() const override { return rate == 1.0 && fract == 0.0 ? 1 : -1; }
virtual int getLatency() const override
{
return 1;
}
};
}
#endif
@@ -1,296 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Linear interpolation algorithm.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <assert.h>
#include <stdlib.h>
#include "InterpolateLinear.h"
using namespace soundtouch;
//////////////////////////////////////////////////////////////////////////////
//
// InterpolateLinearInteger - integer arithmetic implementation
//
/// fixed-point interpolation routine precision
#define SCALE 65536
// Constructor
InterpolateLinearInteger::InterpolateLinearInteger() : TransposerBase()
{
// Notice: use local function calling syntax for sake of clarity,
// to indicate the fact that C++ constructor can't call virtual functions.
resetRegisters();
setRate(1.0f);
}
void InterpolateLinearInteger::resetRegisters()
{
iFract = 0;
}
// Transposes the sample rate of the given samples using linear interpolation.
// 'Mono' version of the routine. Returns the number of samples returned in
// the "dest" buffer
int InterpolateLinearInteger::transposeMono(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
LONG_SAMPLETYPE temp;
assert(iFract < SCALE);
temp = (SCALE - iFract) * src[0] + iFract * src[1];
dest[i] = (SAMPLETYPE)(temp / SCALE);
i++;
iFract += iRate;
int iWhole = iFract / SCALE;
iFract -= iWhole * SCALE;
srcCount += iWhole;
src += iWhole;
}
srcSamples = srcCount;
return i;
}
// Transposes the sample rate of the given samples using linear interpolation.
// 'Stereo' version of the routine. Returns the number of samples returned in
// the "dest" buffer
int InterpolateLinearInteger::transposeStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
LONG_SAMPLETYPE temp0;
LONG_SAMPLETYPE temp1;
assert(iFract < SCALE);
temp0 = (SCALE - iFract) * src[0] + iFract * src[2];
temp1 = (SCALE - iFract) * src[1] + iFract * src[3];
dest[0] = (SAMPLETYPE)(temp0 / SCALE);
dest[1] = (SAMPLETYPE)(temp1 / SCALE);
dest += 2;
i++;
iFract += iRate;
int iWhole = iFract / SCALE;
iFract -= iWhole * SCALE;
srcCount += iWhole;
src += 2*iWhole;
}
srcSamples = srcCount;
return i;
}
int InterpolateLinearInteger::transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
LONG_SAMPLETYPE temp, vol1;
assert(iFract < SCALE);
vol1 = (LONG_SAMPLETYPE)(SCALE - iFract);
for (int c = 0; c < numChannels; c ++)
{
temp = vol1 * src[c] + iFract * src[c + numChannels];
dest[0] = (SAMPLETYPE)(temp / SCALE);
dest ++;
}
i++;
iFract += iRate;
int iWhole = iFract / SCALE;
iFract -= iWhole * SCALE;
srcCount += iWhole;
src += iWhole * numChannels;
}
srcSamples = srcCount;
return i;
}
// Sets new target iRate. Normal iRate = 1.0, smaller values represent slower
// iRate, larger faster iRates.
void InterpolateLinearInteger::setRate(double newRate)
{
iRate = (int)(newRate * SCALE + 0.5);
TransposerBase::setRate(newRate);
}
//////////////////////////////////////////////////////////////////////////////
//
// InterpolateLinearFloat - floating point arithmetic implementation
//
//////////////////////////////////////////////////////////////////////////////
// Constructor
InterpolateLinearFloat::InterpolateLinearFloat() : TransposerBase()
{
// Notice: use local function calling syntax for sake of clarity,
// to indicate the fact that C++ constructor can't call virtual functions.
resetRegisters();
setRate(1.0);
}
void InterpolateLinearFloat::resetRegisters()
{
fract = 0;
}
// Transposes the sample rate of the given samples using linear interpolation.
// 'Mono' version of the routine. Returns the number of samples returned in
// the "dest" buffer
int InterpolateLinearFloat::transposeMono(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
double out;
assert(fract < 1.0);
out = (1.0 - fract) * src[0] + fract * src[1];
dest[i] = (SAMPLETYPE)out;
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
src += whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}
// Transposes the sample rate of the given samples using linear interpolation.
// 'Mono' version of the routine. Returns the number of samples returned in
// the "dest" buffer
int InterpolateLinearFloat::transposeStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
double out0, out1;
assert(fract < 1.0);
out0 = (1.0 - fract) * src[0] + fract * src[2];
out1 = (1.0 - fract) * src[1] + fract * src[3];
dest[2*i] = (SAMPLETYPE)out0;
dest[2*i+1] = (SAMPLETYPE)out1;
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
src += 2*whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}
int InterpolateLinearFloat::transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
float temp, vol1, fract_float;
vol1 = (float)(1.0 - fract);
fract_float = (float)fract;
for (int c = 0; c < numChannels; c ++)
{
temp = vol1 * src[c] + fract_float * src[c + numChannels];
*dest = (SAMPLETYPE)temp;
dest ++;
}
i++;
fract += rate;
int iWhole = (int)fract;
fract -= iWhole;
srcCount += iWhole;
src += iWhole * numChannels;
}
srcSamples = srcCount;
return i;
}
Loaded 100 of 1550 files, more files were not shown because too many files have changed in this diff. Show more