Compare commits

..

No commits in common. 'master' and 'v2' have entirely different histories.
master ... v2

  1. 3
      .gitignore
  2. 216
      AGENTS.md
  3. 89
      BUG_bgp_reconnect.md
  4. 2
      PLAN_etcp_router_reset_id.md
  5. BIN
      bg.png
  6. 2
      build.sh
  7. 2
      build_direct.sh
  8. 6
      check.sh
  9. 49
      configure.ac
  10. 2
      cross-build-win.sh
  11. 129
      doc/chat_call_arch.md
  12. 315
      doc/dm_arch.md
  13. 178
      doc/etcp_full_suite_findings.md
  14. 37
      doc/etcp_protocol.txt
  15. 2
      doc/etcp_router_arch.md
  16. 433
      doc/linux_audio_recovery.md
  17. 60
      doc/node_snapshot.md
  18. 12
      doc/pm.txt
  19. 61
      doc/proxy_protocol.md
  20. 86
      doc/service_lifecycle.md
  21. 307
      doc/tasks.md
  22. 28
      lib/Makefile.am
  23. 2
      lib/async_dns.c
  24. 422
      lib/audio_compressor.c
  25. 18
      lib/audio_compressor.h
  26. 4
      lib/debug_config.c
  27. 6
      lib/debug_config.h
  28. 37
      lib/json_flat.c
  29. 3
      lib/json_flat.h
  30. 165
      lib/ll_queue.c
  31. 24
      lib/ll_queue.h
  32. 1
      lib/ll_queue_doc.md
  33. 21
      lib/mem.c
  34. 40
      lib/memory_pool.c
  35. 238
      lib/miniaudio.h
  36. 5
      lib/opus_codec.c
  37. 5
      lib/opus_codec.h
  38. 19
      lib/platform_compat.c
  39. 5
      lib/platform_compat.h
  40. 301
      lib/silero_vad.c
  41. 76
      lib/silero_vad.h
  42. BIN
      lib/silero_vad.onnx
  43. 136918
      lib/silero_vad_model.inc
  44. 178
      lib/speex_aec.c
  45. 80
      lib/speex_aec.h
  46. 35
      lib/speexdsp/COPYING
  47. 160
      lib/speexdsp/_kiss_fft_guts.h
  48. 232
      lib/speexdsp/arch.h
  49. 448
      lib/speexdsp/fftwrap.c
  50. 58
      lib/speexdsp/fftwrap.h
  51. 170
      lib/speexdsp/include/speex/speex_echo.h
  52. 14
      lib/speexdsp/include/speex/speexdsp_config_types.h
  53. 126
      lib/speexdsp/include/speex/speexdsp_types.h
  54. 523
      lib/speexdsp/kiss_fft.c
  55. 108
      lib/speexdsp/kiss_fft.h
  56. 297
      lib/speexdsp/kiss_fftr.c
  57. 51
      lib/speexdsp/kiss_fftr.h
  58. 332
      lib/speexdsp/math_approx.h
  59. 1279
      lib/speexdsp/mdf.c
  60. 169
      lib/speexdsp/os_support.h
  61. 379
      lib/speexdsp/pseudofloat.h
  62. 32
      lib/tcp_io.c
  63. 1
      lib/tcp_io.h
  64. 824
      lib/u_async.c
  65. 16
      lib/u_async.h
  66. 8
      lib/u_async_doc.md
  67. 2
      net_emulator/net_emulator.c
  68. 51
      src/Makefile.am
  69. 1030
      src/call/call.c
  70. 100
      src/call/call.h
  71. 554
      src/call/call_audio.c
  72. 91
      src/call/call_audio.h
  73. 372
      src/call/call_headless.c
  74. 38
      src/call/call_headless.h
  75. 217
      src/call/call_jitter.cpp
  76. 39
      src/call/call_jitter.h
  77. 34
      src/call/call_jitter_window.h
  78. 98
      src/call/call_proto.h
  79. 165
      src/call/call_tones.c
  80. 32
      src/call/call_tones.h
  81. 54
      src/call/voice_sources.cmake
  82. 6
      src/chat/chat_channel.c
  83. 176
      src/chat/chat_core.c
  84. 70
      src/chat/chat_core.h
  85. 6
      src/chat/chat_core_priv.h
  86. 25
      src/chat/chat_event.c
  87. 20
      src/chat/chat_event.h
  88. 210
      src/chat/chat_headless_control.c
  89. 189
      src/chat/chat_join.c
  90. 89
      src/chat/chat_join.h
  91. 4
      src/chat/chat_member.c
  92. 1
      src/chat/chat_member.h
  93. 141
      src/chat/chat_msg.c
  94. 1
      src/chat/chat_profile.c
  95. 21
      src/chat/chat_setting.c
  96. 4
      src/chat/chat_status.c
  97. 283
      src/chat/chat_sync.c
  98. 53
      src/chat/chat_sync.h
  99. 2
      src/chat/chat_whisper.c
  100. 102
      src/chat/db_sync.c
  101. Some files were not shown because too many files have changed in this diff Show More

3
.gitignore vendored

@ -3,7 +3,6 @@
build-win/
tools/chatgui/build/
tools/chatgui/ffmpeg/ffmpeg_build/
tools/lightsout/build/
# Build outputs
*.o
@ -12,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

216
AGENTS.md

@ -34,10 +34,10 @@ This file contains essential information for AI coding agents working in the uTu
## 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
@ -68,10 +68,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)
@ -87,7 +86,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 выход)
```
@ -95,14 +94,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
@ -120,13 +118,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`.
## Code Style Guidelines
@ -139,7 +137,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) {
@ -147,7 +145,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
@ -203,7 +201,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);
@ -229,66 +227,40 @@ 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`.
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/ # Core libraries, SQLite, libopus/liblmdb
├── lib/ # Core libraries (18 .c + 20 .h + libopus/liblmdb)
├── src/ # Main source code
│ ├── transport_layer/ # ETCP, crypto, STCP, normalizer, NCD, BBR
│ ├── routing_layer/ # Routing, группы/BGP, recovery, conn_mgr, etcp_router
│ ├── chat/ # P2P чат, join, db_sync, merkle_sync, member_sync
│ ├── dm/ # Личные сообщения и mailbox
│ ├── call/ # Звонки и общий голосовой стек
│ ├── radio/ # Групповая PTT-рация
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек
│ ├── media_delivery/ # Доставка медиафайлов
│ ├── media_async/ # Фоновые задачи медиа
│ └── *.c/h # utun, config, TUN, firewall, NAT, NTP, control
├── 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
@ -306,7 +278,7 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
**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
@ -342,7 +314,7 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `stcp_server.c/h` - STCP сервер
- `stcp_client.c/h` - STCP клиент
- `stcp_link.c/h` - STCP линк-уровень
- `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-камуфляжа
@ -357,30 +329,28 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `route6_lib.c/h` - IPv6 routing
- `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-группы: исторические → суперузлы/публичные → локальные; отменяемые запросы до READY
- `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
- `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), фиксированная сериализация на проводе
@ -388,7 +358,7 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `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-параметры, ключ и адреса узла
- `invite_link.c/h` - Формат invite-ссылок utun:// для каналов (base64: version|password|channel_id|pubkey|addrs)
**Прокси (src/proxy/)**
- `socks_proxy.c/h` - SOCKS5 прокси
@ -409,11 +379,11 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `media_index.c/h` - Индексация медиафайлов
**Media Async (src/media_async/)**
- `media_async.c/h` - Фоновые crypto/file-задачи: work в pthread, done в uasync; destroy ждёт workers и завершает ожидающие callbacks с CANCELLED
- `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, хеш-индексом и ожиданием свободного места
- `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)
@ -446,7 +416,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
@ -462,8 +432,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`.
@ -483,8 +451,7 @@ 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-настройки хранятся в секции `[chatserver]` (принимается также `[chat]`). Парсер вызывает `chat_setting_set(key, value)` для каждого ключа — неизвестные ключи вызывают ошибку. Пример:
```ini
[chatserver]
storage_autoload=1
@ -507,8 +474,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-ом. лучше больше. единственное с чем борись - это бесполезные спам логи.
@ -518,8 +484,7 @@ DEBUG_LEVEL_DISABLED полностью отключает категорию,
Но полезные смотри всегда, и всегда оставляй логи которые выводятся нечасто.
Частые логи - это трафик которые >100 раз повторяются. Но которые мало раз обязательно оставлять и выводить.
В тесте можно временно поднять нужные категории до TRACE, сохранив прежние уровни и восстановив их после участка.
Одного debug_set_level(DEBUG_LEVEL_NONE) недостаточно для отключения категорий с явно заданным уровнем.
можешь в тесте включить логи в нужный интервал чтобы не спамить. debug_set_level(DEBUG_LEVEL_TRACE) и выключить debug_set_level(DEBUG_LEVEL_NONE)
Если видишь проблему и ее решение неочевидно то выстраивай диагностику вокруг неё пока не будет очевидно где и что происходит не так. Это базовое и обязательное требование к отладке.
Подробные логи ты должен выводить не обрезая всегда и анализировать. Если лог большой - выведи в файл и анализируй файл.
Рассуждение должно быть примерно таким:
@ -570,17 +535,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 потока.
@ -588,28 +548,28 @@ 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
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/)
@ -618,7 +578,7 @@ cmake .. && make -j4
| `main.cpp` | Точка входа, инициализация QApplication |
| `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-анимацией |
@ -659,8 +619,7 @@ cmake .. && make -j4
| `debug_ui.h` | Отладочный UI |
### Другие поддиректории
- `db/` — `db_manager.h/cpp`: доступ GUI к данным nodes, channels, msg_<channel>, peers_<channel> и состоянию интерфейса;
копия `db/sqlite3.c/h` не используется текущей CMake-целью, SQLite берётся из `lib/`
- `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 обмен), `node_config.h/cpp` (конфиг узла), `config_updater.h/cpp` (правка конфига), `utun_node.h/cpp` (обёртка узла), `miniaudio_impl.c` (single-header miniaudio в отдельной единице трансляции). Chat-логика (`chat_core`, `chat_sync` и т.д.) компилируется напрямую из `src/chat/`, роутинг — из `src/routing_layer/` (см. `libutun/CMakeLists.txt`)
- `resources/` — `bg.jpg`, `chatgui.qrc` (встраивает 5 TGS в бинарник), `animations/*.tgs`
@ -756,19 +715,18 @@ 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/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 обмен маршрутами (дизайн-документ)
- `/doc/chat_join_test.md` - ТЗ теста join-протокола (chat_join + дерево приглашений)
- `/doc/chat_join_test_tasks.md` - Список задач по тесту join (текущий статус, пошагово)
## Список задач по проекту
Список задач — `doc/tasks.md` относительно корня репозитория (текущая работа, берём по одной задаче сверху).
Список задач — `/doc/tasks.md` (текущая работа, берём по одной задаче сверху).
При нахождении попутных багов/fail/флаков тестов добавляем задачу (либо фиксим сразу если баг простой).
Как сделали - помечаем [+] выполнено.
@ -776,9 +734,8 @@ void lottie_animation_destroy(Lottie_Animation *anim);
**Расположение:** `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)
@ -786,10 +743,9 @@ C-библиотека `utun_lite` собирает `lib/` и `src/` по `libut
**Chat-модули:** все файлы из `src/chat/` (описаны выше в секции «Chat») компилируются в `libutun_lite`.
## 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
@ -800,19 +756,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.

89
BUG_bgp_reconnect.md

@ -0,0 +1,89 @@
# Задача: BGP NODEINFO теряется при флапающем реконнекте (chatgui member online не возвращается)
## Кратко
После рестарта Android-пира и реконнекта локальный chatgui показывает мембера **offline** и не возвращает **online**.
Корневая причина — **не в chat/member коде**, а в ETCP: BGP-пакет NODEINFO (347 байт) расшифровывается, но **не доходит до BGP-обработчика** `topo_group_receive_cbk()`.
Нужно: **точно локализовать точку потери NODEINFO в receive-пути ETCP и починить её.**
## Что уже сделано (исправлено и работает)
1. `tools/chatgui/src/mainwindow.cpp` — **исправлен** `onMemberUpdatedCallback`/`onMemberRemovedCallback`: проверка `len < 1+64+1+79` отбрасывала все события с channel_id короче 64 симв. Теперь `len < 1` + точная проверка. (Это была причина «не обновляется вообще».)
2. `src/chat/chat_member.c/h` — добавлен `chat_core_member_online(ch_id,node_id)` = присутствие в `topo_group` (`topo_node_find_by_id`). Используется в `chat_core_get_member_list`/`get_single_member`. Убран `nodes.online` из SQL и блок `connected→2`.
3. `src/chat/chat_core.c`, `src/chat/chat_sync.c`, `src/chat/member_sync.c/h` — удалён онлайн-статус из БД + merkle-online gossip (`member_sync_set_online`, `_ms_apply_update`, `push_update`).
4. `src/chat/chat_status.c` + `tools/chatgui/src/accountlist.cpp` — флаг «bgp present» (0x08) для detail-панели.
5. `tools/chatgui/src/nodespage.cpp` — убрана колонка Online из диагностики.
Эти правки корректны; статус работает на чистом connect/disconnect/стабильном реконнекте.
## Найденная причина (подтверждено логами)
- Android **шлёт** свой NODEINFO: `send_nodeinfo: node 1657b3ea8281c0a8 ver=2` (bgp-debug на Android).
- Локально пакет **расшифровывается**: `decrypt: code=01 dlen=347 plen=350` (347-байтный BGP-пакет, `code=0x01` = `ETCP_ID_TOPO_ENTRY`).
- Но `BGP recv NODEINFO nid=1657` (строка 193 в `topo_group.c`) **не появляется** — до обработчика пакет не доходит.
- При этом маленький `TABLE_COMPLETE` (10 байт) **доходит** (виден в логе как «mislabeled» `nid=aaaaaaaaaadeadbe` — это строка 193 логирует не-NODEINFO пакет как NODEINFO, garbage-поля).
- Признак: в стабильных рестартах (18:18/18:20) узел **добавляется** каждый раз, в флапающем (18:07) — нет. Т.е. потеря привязана к флапу (TCP-линки + ETCP reinit).
## Receive-путь ETCP (где искать потерю)
```
decrypt (etcp_connections.c:2262, лог "decrypt: code=.. dlen=..")
→ if (link_state==CONNECTED) etcp_conn_input(pkt)
else memory_pool_free(pkt) ← добавлен WARN "decrypted pkt DROPPED"
→ etcp_conn_input (etcp.c:1506, "RX pkt dlen=%d")
→ нормализатор (reassembly) → int_queue
→ etcp_int_recv (etcp_api.c) ← добавлен WARN "int_recv BGP id=0x01 len=.."
→ dispatch по route id (0x01) → topo_group_receive_cbk (topo_group.c:193 "BGP recv NODEINFO")
```
## Уже добавленный дебаг (для локализации)
1. `src/transport_layer/etcp_connections.c` (после дешифровки):
```c
} else {
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] decrypted pkt DROPPED: link_state=%d code=0x%02x dlen=%u",
link->etcp->log_name, link->link_state, pkt_code, pkt->data_len);
memory_pool_free(e_sock->instance->pkt_pool, pkt);
}
```
2. `src/transport_layer/etcp_api.c` (`etcp_int_recv`, после чтения `id`):
```c
uint8_t id = e->dgram[0];
if (id == 0x01) DEBUG_WARN(DEBUG_CATEGORY_ETCP, "int_recv BGP id=0x01 conn=%s len=%zu", conn->log_name, e->len);
```
3. Android: `tools/chatgui-android/libutun_lite/instance_lite.c` добавлено
`debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);` (пересобрать APK: `cd tools/chatgui-android && ./build.sh`).
4. chatgui конфиг `tools/chatgui/build/vibechat.cfg` — включены `bgp=debug`, `chat_sync=debug`, `member_sync=debug`, `debug=debug`.
Как читать (по таймстампу флапа):
- есть `decrypt code=01 dlen=..` + `DROPPED` → потеря в проверке `link_state==CONNECTED`;
- есть `decrypt` + нет `DROPPED` + нет `int_recv BGP` → потеря в `etcp_conn_input`/нормализаторе;
- есть `int_recv BGP` + нет `BGP recv NODEINFO` → потеря в BGP-обработчике.
## Задача для агента
1. Пересобрать chatgui (`cd tools/chatgui && cmake --build build -j4`) и Android APK (`cd tools/chatgui-android && ./build.sh`).
2. Запустить chatgui (DISPLAY=:0, `./vibechat`), воспроизвести флап реконнекта:
```bash
adb shell am force-stop com.utun.chat && adb shell am start -n com.utun.chat/.MainActivity
```
(повторить несколько раз; флап ловится не каждый раз).
3. В логах (`tools/chatgui/build/chatgui.log`) найти потерю NODEINFO и **точно определить точку** по таблице выше.
4. Починить первопричину (варианты):
- буферизовать расшифрованный пакет до `LINK_STATE_CONNECTED` вместо `memory_pool_free`;
- либо гарантированный BGP-retry (после conn UP, если узел пира не в `group->nodes` через N мс — повторно `topo_group_send_table_request`).
5. Убрать весь диагностический мусор:
- WARN "DROPPED" (etcp_connections.c), WARN "int_recv BGP" (etcp_api.c);
- DEBUG-логи в `chat_member.c` (`on_member_props_changed`), `chat_sync.c` (`cs_on_peer_status_changed`), `memberlistmodel.cpp`;
- `instance_lite.c` BGP-debug;
- `vibechat.cfg` — вернуть категории в закомментированное состояние.
6. Прогнать: connect→online, disconnect→offline, реконнект (флап)→online. `./check.sh` (или `cd src && make -j4`).
## Воспроизведение / ключевые артефакты
- Пир: Android SM-A525F (`com.utun.chat`), node_id `0x1657b3ea8281c0a8`.
- Локальный: chatgui (`tools/chatgui/build/vibechat`), node_id `0x24cd036a6e659b9a`.
- Канал: `ch_id=7206723622466219923`, `group_id=0x64036cf3a783ef93`.
- Лог chatgui: `tools/chatgui/build/chatgui.log` (debug_file из конфига).
- Лог Android: `adb logcat -d | grep -iE "send_nodeinfo|NOT in registry|NODEINFO|DROPPED|int_recv"`.

2
PLAN_etcp_router_reset_id.md

@ -1,5 +1,3 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](src/routing_layer/etcp_router_doc.md).
# План: эпоха (reset_id) для ETCP Router
Аналог фикса `reset_id` из ETCP (см. `61c572c`), применённый к сервисному слою

BIN
bg.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

2
build.sh

@ -170,8 +170,6 @@ if [ "$USE_ASAN" -eq 1 ]; then
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; }
# libopus собирается без ASAN (собственный CFLAGS в Makefile, .a несовместим с PIE)
(cd lib/libopus && make clean $JOBS $MAKE_ARGS 2>&1 && make $JOBS $MAKE_ARGS 2>&1) || { echo "libopus 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

2
build_direct.sh

@ -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_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

6
check.sh

@ -20,15 +20,9 @@ rm -f "$TMP"
echo ""
if [ "$(id -u)" -eq 0 ]; then
make -C tests check-proxy
make -C tests check-burst
make -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 -C tests check-proxy
echo "=== tcp_proxy_full burst test (sudo) ==="
sudo -n make -C tests check-burst
echo "=== tcp_proxy_full load test (sudo) ==="
sudo -n make -C tests check-load
else
echo "[SKIP] tcp_proxy_full integration test (requires passwordless sudo)"
fi

49
configure.ac

@ -95,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"
@ -178,37 +165,6 @@ 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
@ -235,11 +191,10 @@ AC_OUTPUT
# Summary
echo "
Configuration summary:
---------------------
----------------------
Host: ${host}
Windows: ${os_windows}
Prefix: ${prefix}
Compiler: ${CC}
CFLAGS: ${CFLAGS}
Silero VAD: ${with_silero_vad}
"
"

2
cross-build-win.sh

@ -197,7 +197,7 @@ 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

129
doc/chat_call_arch.md

@ -1,6 +1,6 @@
# P2P аудио-звонок пользователю — архитектура
Статус: реализовано. Звонок использует роутер, CM подготавливает оптимальный путь.
Статус: спроектировано, ждёт реализации.
## 1. Область
@ -11,7 +11,7 @@ DM-группа, маршрутизация через `etcp_router` идент
(обе стороны компилируют общий стек `etcp_router` / `topo_group` (BGP) / `conn_mgr`).
Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE →
аудиопоток через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
установка соединения (conn_mgr) → аудиопоток → HANGUP.
## 2. Разбиение файлов (отдельный каталог `src/call/`)
@ -21,12 +21,13 @@ DM-группа, маршрутизация через `etcp_router` идент
| `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 |
| `src/call/audio_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
Диагностика: категория `call` (`DEBUG_CATEGORY_CALL`); транспортная доставка — `etcp_route`.
Новая категория логирования: `DEBUG_CATEGORY_CALL = 31` (добавить в `debug_config.h`,
`DEBUG_CATEGORY_COUNT` → 32).
## 3. Протокол (svc_id 0x34, первый байт payload = subcmd)
@ -54,34 +55,23 @@ CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, payload}
## 4. Машина состояний
```
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → ACTIVE(ACCEPT) → ENDED
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACTIVE(accept) → ENDED
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → CONNECTING(ACCEPT, conn_mgr_open) → ACTIVE → ENDED
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACCEPTING(accept, conn_mgr_open) → ACTIVE → 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 45с (вызывающий, нет ответа → CANCEL);
- `CONN_EVENT_DOWN/TIMEOUT` CM не завершают звонок: роутер использует доступные пути;
- ring-timeout 45с (нет ответа → CANCEL);
- connect: `conn_mgr` `CONN_EVENT_TIMEOUT` → fail;
- **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP.
Завершающие кодограммы (HANGUP/CANCEL/DECLINE) шлются без ретрансмиссий — потеря
компенсируется watchdog'ом 2с у вызываемого. Запоздалые кодограммы вызова при
идущем разговоре (`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 не меняется.
Соединение: на ACCEPT обе стороны `conn_mgr_open(inst, group_id, peer, cb, ...)`;
`CONN_EVENT_UP` → ACTIVE. Медиа идёт `etcp_route_send` (после подъёма прямого линка
роутер идёт напрямую — RTT минимален).
## 5. Отправка медиа — backpressure через локальный буфер
@ -97,78 +87,49 @@ CM handle инициирует оптимальное подключение и
В сессии:
- `tx_q` — локальная FIFO очередь закодированных кадров (ll_entry с dgram), ёмкостью
15с (750 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
~1с (≈50 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
- Алгоритм (`call_tx_flush`): пока `tx_q` не пуст:
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=1)` (после проверки места);
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=0)`;
- иначе — если 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 из другого потока.
GUI запускает аудиоустройство по CALL_ACCEPTED. При CALL_ENDED начинается финальный тон;
устройство останавливается перед `call_audio_release`. Явное завершение звонка прекращает
воспроизведение накопленной речи и включает тон завершения — на обеих платформах.
Android останавливает гудки до настройки звонкового аудиотракта. После выбора маршрута
capture/play потоки независимо создают и запускают AudioRecord/AudioTrack; каждый сразу обслуживает своё направление.
`start()` подтверждает запуск подготовки, ошибки устройств передаются владельцу на main через `onFailure`.
Ошибка любого направления завершает звонок. Общий `stop()` отменяет подготовку, разблокирует I/O и ждёт оба потока
до освобождения Android-устройств и native-аудиостека. Отдельные locks защищают подготовку/остановку каждого устройства.
Замер AEC планируется через 300мс после готовности обоих направлений и не задерживает воспроизведение.
Маршрутизатор использует один снимок входных/выходных устройств; обновляет его при старте,
событиях устройств/BT-профиля и явной переоценке. По завершении снимок освобождается.
INFO-логи Android показывают длительности настройки режима, регистрации callback, BT-профиля, выбора маршрута,
создания/старта устройств, первого чтения микрофона и первой записи в AudioTrack, а также готовность другого направления.
Общий `call_audio` логирует время ожидания первого PCM; запись в AudioTrack может содержать начальную тишину.
## 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с; прочие сервисы сохраняют прежний срок.
Перезапуск удалённого процесса/роутерной сессии не является кратковременным перебоем пути и может сбросить данные.
## 6. Thread-модель (single-writer)
| Объект | Владелец |
|---|---|
| `call_session` (состояние, encoder, decoder, tx_q, таймеры) | uasync-поток |
| Opus encoder (send) | аудио-поток (encode) |
| Opus decoder (recv) | uasync-поток (`decode → ajb_push`) |
| `ajb` push | uasync-поток |
| `ajb` pull + SoundTouch | аудио-поток |
| `call_tones` | аудио-поток |
Поток данных:
- TX: аудио-поток кодирует → `call_send_encoded(call_id, opus, len)` → `uasync_post` →
uasync: `tx_q` + `call_tx_flush` (backpressure).
- RX: uasync принимает `CALL_MEDIA` → decode → `ajb_push`; аудио-поток `call_pull_pcm` → `ajb_pull` (+ тоны).
`ajb` = mutex-кольцо PCM (гранулярность 20ms/50 ops в сек — мьютекс бесплатен);
SoundTouch инстанс заперт на pull-стороне.
**Контракт жизненного цикла:** GUI стартует аудио-устройство по `CALL_ACCEPTED`,
останавливает по `CALL_ENDED` и только после остановки (join аудио-потока) вызывает
`call_release(call_id)` (post в uasync) → uasync освобождает session/encoder/decoder/jb.
Пока устройство работает — `call_pull_pcm`/`call_send_encoded` не гоняются с освобождением.
## 7. Адаптивный джиттер-буфер (SoundTouch)
Буфер ≤ **1000 мс**, target playout ~60 мс; при росте буфера плавно tempo → **1.6x**.
SoundTouch: `USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4`.
API: `ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy`.
## 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` воспроизводится
- **Сталл >500мс** (нет входящих медиа) → в `call_pull_pcm` микшируется
`CALL_TONE_GLITCH`: `пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс)
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл. До порога — тишина при нехватке PCM.
При возобновлении PCM сигнал останавливается; одного входящего пакета без достаточного запаса недостаточно.
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл.
- **Завершение звонка → всегда** `CALL_TONE_ENDED`: 3 нисходящих 700/500/400 Гц по 150 мс.
- Счётчики: tx/rx кадры, `c_tx_dropped`/`c_rx_dropped`, дропы буфера, сталлы,
tempo-события, причины завершения. Логи `DEBUG_CATEGORY_CALL` (+стата раз в 1с).
@ -181,7 +142,7 @@ 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_STATS 37 [call_id:8][rtt_ms:2][buffer_ms:2][tempo_x100:2]
CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
```

315
doc/dm_arch.md

@ -1,222 +1,93 @@
# 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 на телефоне и десктопе не выполнялась.
# DM — прямой p2p чат между двумя пользователями группы
## 1. Цель
Возможность начать личный чат с любым пользователем доступных групп.
Пока target offline сообщения временно хранятся на storage-узлах.
## 2. Ключевые решения
- Отдельная DM-подсистема (НЕ канал: без TOPO_GROUP/member_sync/merkle).
- Идентичность детерминированная: одна ветка на пару, согласование без handshake.
- E2E-шифрование содержимого сразу.
- Offline-хранение: dm_mailbox на storage-узлах с флагом dm_storage.
- Доставка: fallback — сперва проверка наличия узла в BGP-группе; доступен →
прямой send; нет связи → storage.
## 3. Идентичность и ключи (детерминированные)
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
shared = X25519(my_x25519_priv, peer_x25519_pub)
content_key = SHA256(shared || "utun_dm_content")
Обе стороны выводят одинаковые значения из своего privkey и pubkey пира
(известен из общей группы). Смена x25519 у пира = новая ветка.
## 4. Модель данных (SQLite, общий chats.db)
dm_conversations(conv_id TEXT PK, peer_node_id, peer_x25519 BLOB, peer_ed25519 BLOB,
peer_name TEXT, last_out_seq INT, last_in_seq INT, created_at INT, last_ts INT)
dm_messages(conv_id TEXT, dir INT, seq INT, ts INT, author INT, ct TEXT,
data BLOB, sig BLOB, PRIMARY KEY(conv_id,dir,seq))
dm_mail(recipient INT, sender INT, conv_id TEXT, seq INT, ts INT, data BLOB, sig BLOB,
ttl INT, PRIMARY KEY(recipient,sender,seq))
Два независимых направленных потока (A→B, B→A) с монотонным seq.
Без chain-хеша: для 2 пиров достаточно seq + catch-up по диапазону.
## 5. Доставка
1. Проверка доступности: peer присутствует в BGP-группе (общая группа).
2. Доступен → прямой send (сервис ETCP_RT_ID_DM = 0x34), ждём DM_ACK{conv_id,seq}.
Нет ACK за таймаут → fallback в mailbox.
3. Нет связи → MAIL_PUT на 1..N storage-узлов.
4. Catch-up при connect: DM_HELLO{conv_id,last_out_seq,last_in_seq} → пир досылает
(in_seq+1 .. out_seq).
5. Dedup: вставка только при seq > last_in_seq.
## 6. E2E-шифрование
data_enc = nonce(13B, счётчик по seq) || AES-CCM_ct || tag
secure_channel уже умеет AES-CCM. Storage/relay видят только шифротекст.
## 7. Storage-узлы (dm_storage)
- Новый флаг dm_storage=yes в adm_tags (блок B владельца), распространяется
существующим member_sync без нового синка.
- DM читает adm_tags мемберов общих групп (peers_<ch>) и кэширует storage-узлы.
- Протокол mailbox (сервис ETCP_RT_ID_DM_MAILBOX = 0x35):
MAIL_PUT {recipient,sender,conv_id,seq,ts,data,sig}
MAIL_PULL {recipient, per-sender cursor}
MAIL_ACK {up_to_seq}
TTL 7 дней + лимит объёма, dedup по (recipient,sender,seq).
## 8. Старт и anti-spam
- dm_start(peer): создать беседу (вывести conv_id/ключи), обеспечить прямое
соединение (переиспользовать chat_sync_connect_node/node_conn_direct).
- Пир автопринимает на первом DM_HELLO/сообщении, только если отправитель в
общей группе.
## 9. Файлы и интеграция
| Файл | Назначение |
|------|-----------|
| src/dm/dm_core.c/h | беседы, send/recv, таблицы, bind 0x34, conn-события, fallback |
| src/dm/dm_crypto.c/h | derive + AES-CCM |
| src/dm/dm_mailbox.c/h | storage-узел, push/pull/ack, bind 0x35 |
Плюс:
- src/chat/chat_member.c — бит CHAT_MEMBER_FLAG_DM_STORAGE + парсинг dm_storage.
- utun_instance.c — dm_core_init/destroy.
- chat_core — API dm_start/send/list + события CHAT_EVT_DM_*.
- headless CLI — dm list/send/messages.
- src/Makefile.am — новые источники.
## 10. Сервисы ETCP
ETCP_RT_ID_DM = 0x34
ETCP_RT_ID_DM_MAILBOX = 0x35

178
doc/etcp_full_suite_findings.md

@ -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()` напрямую.

37
doc/etcp_protocol.txt

@ -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 по каждому линку адаптивно подстраиваем:
@ -193,34 +187,3 @@ flag_up в заголовке устанавливается в значение
- сервер как получает 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.

2
doc/etcp_router_arch.md

@ -1,5 +1,3 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md).
# ETCP Router Architecture
## Структуры данных

433
doc/linux_audio_recovery.md

@ -1,433 +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
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
охватывает звук этого приложения на том же выходе, согласно выбранному контракту.
## Референсы
Первичные исходники и документация просмотрены 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: обновление библиотеки само
по себе не решает этот найденный дефект.

60
doc/node_snapshot.md

@ -1,60 +0,0 @@
# Подписанная запись узла
`TOPO_NODE.timestamp` — версия всей подписанной записи, а не отдельного адреса.
Узел-владелец назначает Unix microseconds, строго больше предыдущей сохранённой
версии. Имя, тип, активность, адреса, метаданные сокетов, REALITY и подпись
заменяются вместе. Timestamp входит в подпись. Групповые пути и подсети
остаются в `TOPO_GROUP_NODE` и не являются частью общей записи узла.
Получатель проверяет подпись до изменения реестра или маршрутов. Более свежая
запись заменяет старую целиком, включая пустые списки адресов. Старые записи
игнорируются; разные подписанные записи с одинаковым timestamp — конфликт.
Повторное BGP-объявление может обновить путь, но не содержимое общей записи.
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется
только timestamp.
Таблица `node_snapshots` хранит полную сериализованную запись без группового
окружения. `node_addresses` остаётся проекцией для поиска и отображения, а не
источником для восстановления подписанной записи. При наличии сохранённого
snapshot загрузка использует его. Локальная публикация сохраняется до рассылки;
ошибка сохранения не разрешает публикацию новой версии.
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
doc/pm.txt

@ -1,12 +0,0 @@
Работа лички
1. отдели логику лички от логики групповых чатов. не смешивай.
Работа лички. сообщения и медиа доставляется напрямую если узел доступен через bgp.
если недоступен - то pending сообщения сохраняются на суперузле и pending media сохраняется на каком-либо медиаузле, а суперузлу сообщается где хранится медиа.
при появлении пира суперузел организует доставку медиа до получателя. после успешной доставки медиа/сообщений они удаляются с промежуточных узлов.
время хранения медиафайлов на промежуточных узлах ограничено - 1 неделя по умолчанию (задачется в конфиге этих узлов).
для доставки медиа поднимаем линк (conn_mgr).
и посмотри реализацию медиа для групповых чатов.

61
doc/proxy_protocol.md

@ -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-протоколом не предусмотрена.

86
doc/service_lifecycle.md

@ -1,86 +0,0 @@
# Жизненный цикл ядра и сервисов
Все операции выполняются в потоке единственного `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 и Android запускают ядро и чат явно, без UTUN.
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл
приложения; освобождение ядра выполняет `utun_instance_destroy()`.
| Владелец | Ресурсы |
|---|---|
| Ядро | Идентичность узла, реестр узлов и групп, 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 может временно
удерживать транспорт самостоятельно, не обращаясь к уже удалённой группе.
Остановка чата сначала прекращает внешние команды и фоновые задания, затем
закрывает медиа и сервисы, синхронизацию и группы, после чего освобождает 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; принудительного
прерывания этих библиотек нет.
Статус транспорта 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 сервиса.
`utun_set_client_activity()` в сборке с `UTUN_HAVE_STANDBY` применяет фактический
режим duty-cycle до рассылки событий активности; JNI не переключает его отдельно.
Повторная команда не перезапускает фазу и анонсы, но первый фоновый запуск включает
standby даже при начальном `client_activity=STANDBY`. Keepalive подписан на режим
и фазы самого standby; новые UDP/TCP-линки сразу анонсируют текущую фазу после
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра.

307
doc/tasks.md

@ -3,228 +3,6 @@
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
## Текущая задача
[+] **DM: надёжная доставка сообщений, первый этап pm.txt** — 01.10.2026:
атомарные история/seq/outbox, подписанные точные квитанции после commit,
live BGP-маршрут и durable очередь на суперузле с проверкой подписанной роли.
Повторы, out-of-order, rollback БД, неверные ACK и поздний PUT проверены;
40 offline-сообщений доставлены после restart источника, первый offline DM —
после restart суперузла при выключенном источнике. Копии удалены по квитанции.
Подготовка ciphertext медиа использует ключ target и независимые nonce порций;
проверены границы 32 КиБ, пустые файлы, неверные ключи/UUID/hash и повреждения.
Clean-сборка ядра/Qt, test_dm, test_dm_e2e и полный check прошли:
113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load PASS.
Текущие гарантии и оставшаяся интеграция: `doc/dm_arch.md`.
[+] **DM: сетевая доставка и pending-медиа по pm.txt** — 01.10.2026:
отдельные owners dm_media/dm_mailbox_media и файловый transfer с CM handle,
подписанные метаданные и независимая квитанция файла после durable приёма.
Прямая доставка или S→M custody, restart S/M при выключенном A, доставка B,
повтор удаления до DELETE_ACK; поздние STORE/STORED не возрождают копии.
TTL M по умолчанию 7 дней, резервирование квоты, отмена transfer и stop workers.
Копирование, шифрование, хеширование, проверка и durable файловые операции —
в media_async; отправка по 8 КиБ через call_soon. Тест 8 МиБ/0,5 с сохраняет
heartbeat uasync (максимальный интервал 10,3 мс в проверенном прогоне).
Qt/Android: оригинальные файловые вложения, сохранение и статусы доставки;
PM-снимки из ядра асинхронные. Чистые сборки Qt и Android APK прошли.
test_dm_media/test_dm_worker, rollback/quota/cancel и полный check:
115 PASS, 0 FAIL, 1 SKIP; proxy/burst/load PASS. GUI на устройствах не проверялся.
Протокол, владение и проверки: `doc/dm_arch.md`.
[+] **DM: голосовые/видео и общий GUI вложений** — 01.10.2026:
тип FILE/VOICE/VIDEO и метаданные зашифрованы ключом target; в transport остаётся
подписанный descriptor, без новых ключей и канального протокола в DM.
Общие attachment/voice_file/attachment_send: Opus, waveform, копирование и видео
готовятся в workers; completion через reserved post вместо polling. PCM передаётся
без копии; адресат закреплён до записи/выбора. Отмена освобождает собственные файлы.
Android: ConversationComposer/ConversationMessages для каналов и PM, CameraX MP4,
IO для URI/метаданных; Qt: общий InputBar/delegate, FFmpeg, фоновые превью/Opus.
Подготовка адресована беседе и UUID задачи; статусы доставки показаны для всех типов.
test_attachment/test_attachment_send проверяют кодек, формат, владение и отмену;
test_dm_media — voice напрямую и encrypted video metadata после offline restart.
Чистые сборки ядра/Qt/APK, 117 PASS / 0 FAIL / 1 SKIP в check.sh; proxy/burst/load PASS.
Qt CTest: 7 PASS, включая повреждения Opus и отмену decoder. APK не устанавливался,
интерактивная проверка устройств не выполнялась. Подробности: `doc/dm_arch.md`.
[ ] **uasync: ложные ERROR об утечке перед cleanup** — 01.10.2026:
test_dm_e2e и существующий test_conn_mgr проходят, но uasync_destroy()
пишет Timer leak до освобождения отменённых heap/immediate таймеров.
После cleanup счётчики сходятся, финальная проверка проходит. Перенести
предварительные счётчики в DEBUG; ERROR оставить для реального расхождения
после cleanup, отдельно показывать активные и отменённые таймеры.
[+] **Статус подключения звонка в Qt и Android** — фактический путь router:
прямое / reverse / indirect / нет подключения. Учитывается BGP fallback;
события только при изменении, общий таймер 1с, сброс при завершении.
Проверены смена путей настоящего звонка, медиа, ASAN, Qt, Android APK и полный check.
[+] **Единицы RTT в окне звонка** — `call_post_stats()` переводит
`ETCP_CONN.rtt_last` из 0.1мс в миллисекунды до отправки CALL_STATS.
Исправление общее для Qt и Android. DEBUG-категория `debug` раз в секунду
показывает исходный RTT, RTT в мс, транспортного соседа и метрики аудио.
[+] **Восстановление путей conn_mgr на протяжении жизни handle** — начальный
TIMEOUT отделён от срока цикла проб. Поздний обрыв запускает новый цикл;
рабочий резерв и NCD сохраняются, при отсутствии резерва INDIRECT стартует сразу.
Циклы ограничены 15с, паузы 1→2→4→8→15с; новая версия узла или локальные сокеты
обходят паузу. Прямой READY останавливает поиск и получает приоритет.
Исходящие request_id и входящие сроки независимы; устаревшие ответы игнорируются.
UP/DOWN отражают общую доступность, TIMEOUT приходит один раз до первого UP.
Последний close отменяет восстановление, включая закрытие из callback.
Проверены десять сетевых сценариев с реальными данными: поздний обрыв,
повторные циклы, смена адресов/сокетов, потеря маршрута посредника, старый ACK,
начальный TIMEOUT с поздним UP и close. ASAN без ошибок; собраны Qt и Android APK.
Полный check: 113 PASS, 0 FAIL, 1 SKIP; пропущенный auto_socket_dynamic отдельно
прошёл с root в network namespace. Интеграционные proxy, burst и load прошли.
Алгоритм: `src/routing_layer/conn_mgr_doc.md`.
[ ] **Общий лимит check.sh после clean** — 01.10.2026: расчёт по времени предыдущего
прогона дал 205с вместе с полной сборкой и оборвал проверку после успешных тестов
до `test_uasync_socket_race`. Повторный запуск на готовой сборке с большим лимитом
прошёл за 206с, включая proxy/burst/load. Разделить бюджет сборки и запуска тестов
либо учитывать clean при расчёте.
[ ] **Паузы аудио Qt при звонке с SM** — 01.10.2026, звонок
`939a3ea36f3eeac3`, прямой LAN TCP `192.168.40.250:44604 ↔ 192.168.40.247:44173`.
Tcpdump: общие паузы входящего трафика до 645мс; ping до роутера в среднем
100мс, до SM 128мс. ПК на 2.4ГГц, SM на 5ГГц. Одновременно `CallAudio: cb`
падает до 20–40/с вместо 100/с, буфер Qt растёт до 2с, на SM повторяется refill.
В 8с strace аудиопоток продолжает обслуживать PulseAudio без долгих syscall;
причина редких аудиоколлбэков пока не установлена. Добавить измерения интервалов
и длительности callback, кадров capture/playback и сводку CALL_STATS; сравнить
сетевые задержки после подключения ПК к 5ГГц или кабелю.
Повторный звонок `b2a423ed2d36ed83` (01:25:31–01:27:39): DIRECT сохраняется,
RTT 2–3мс; в начале на SM gap tone длится 1.8–2.0с примерно каждые 6с.
На Qt частота callback падает вплоть до 4/с, входящий PCM продолжает приходить:
буфер 65→465→1465→2079мс за 3с, затем растёт до нескольких секунд, dropped=0.
Локализовано прекращение обслуживания duplex-аудио Qt; ожидание backend и
задержка обработки PCM пока не разделены. Часы SM отстают от Qt примерно на 1.24с.
Добавлена диагностика backend/устройств, времени между callback и длительности
capture/playback, фактической частоты callback и PCM кадров за интервал.
Счётчики сбрасываются до старта устройства; нужна повторная запись с новой сборкой.
[+] **Ложный сигнал потери звука при запуске звонка** — 01.10.2026, звонок
`bc3efadcac629947`: Qt запускает аудиоколлбэки в 01:54:56.933, включает gap tone в
01:54:57.415 и выключает в 01:54:58.072 при первом воспроизведении PCM.
На SM между ACCEPT и первым захваченным кадром проходит около 1.15с;
часы SM отстают примерно на 1.23с. Qt стабильно обслуживает 100 callback/с,
RTT около 1мс. Исправлено: до первого PCM начальное ожидание проходит в тишине,
последующий перебой включает тон через 500мс. Стартовый сбой по-прежнему
ограничен существующим watchdog отсутствия медиа. Регрессия проверяет ожидание
1.5с, начальное накопление, настоящий перебой, неполный блок, новый звонок и завершение.
Ускорение запуска Android: router.start занимает около 775мс; система сообщает
длительное удержание AudioManager.updateAudioPortCache. Ещё около 370мс проходит
до запуска capture worker. Гудки теперь останавливаются до startAudio;
router использует снимок устройств с обновлением по событиям и явной переоценке,
capture запускается до подготовки/старта playback. Ошибка playback останавливает capture.
Добавлены длительности отдельных стадий, первого capture и PCM. Ядро, Qt и APK
собраны после clean; три аудиотеста прошли. Выигрыш на SM требует замера в новом звонке.
В звонке Qt `180f046ef22defc2` первый PCM пришёл через 1245мс без стартового gap tone.
Полный check вне песочницы: 113 PASS, 0 FAIL, 1 SKIP; auto_socket_dynamic отдельно
прошёл с root в private netns. Интеграционные proxy, burst и load прошли.
[ ] **Оставшаяся задержка запуска аудио SM** — после обновления 01.10.2026,
звонок `2b9ad4e21a54d8ba`: стартового gap tone нет на обеих сторонах; Qt
стабильно работает около 100 callback/с, max_idle=15.3мс, буфер 55мс, RTT=2мс.
На SM первый capture появляется через 1140мс от startAudio (1164мс от ACCEPT).
router.start=781мс, получение снимка устройств=0мс, AudioRecord create=24мс,
startRecording=7мс; от начала startRecording до первого кадра=328мс.
Capture worker стартует раньше playback, но общий выигрыш пока не виден.
AudioTrack create=21мс, play=327мс; система сообщает AudioPortCache contention=497мс
и восстановление AudioTrack при setPreferredDevice. Разделить замеры регистрации
callback, BT-профиля и применения маршрута; отдельно проверить первый AudioRecord.read.
На SM входящий буфер за время подготовки вырос до 1149мс и к концу звонка снизился
до 570мс; ускорение старта playback также сократит начальное накопление.
Реализован независимый запуск AudioTrack и AudioRecord в play/capture потоках;
частичный старт отменяется до освобождения устройств/native. AEC измеряется после готовности обоих.
Добавлены callbackMs/btProfileMs/applyMs/refreshMs, длительность первого read и время первой записи playback.
Резерв теперь зависит от изменения задержки доставки по timestamps CALL_MEDIA, отдельно от глубины очереди;
стартовое накопление и догон не повышают запас. Сохранность FIFO и uint32 wrap проверяются регрессиями.
Сборки ядра, Qt и Android после clean успешны; три аудиотеста прошли. Полный check:
113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load прошли.
Регрессия refill учитывает остаток SoundTouch: два новых кадра могут уже заполнить запас 60мс.
SM и Q8 обновлены 01.10.2026 в 03:03. Выигрыш по времени требует пробного звонка с новой версией.
[ ] **Гонка invite-файла в test_chat_join_e2e** — `wait_file()` проверяет только
существование файла, а `wf()` создаёт его до записи. Читатель иногда получает
пустую ссылку и ошибку префикса `utun://`. Публиковать готовый файл атомарно.
[ ] **Некорректное время в выводе check-proxy** — успешные `basic_1mb` и
`stress_sessions` иногда печатают огромные положительные/отрицательные миллисекунды,
хотя итоговая скорость и длительность передачи выглядят нормально. Проверить расчёт вывода.
[+] **SpeexDSP AEC (эхоподавление) в lib/** — акустическое эхоподавление звонка.
Вендорен SpeexDSP (mdf/fftwrap/kiss_fft, 3-clause BSD) в `lib/speexdsp/`,
обёртка `lib/speex_aec.c/h` (C-API, FLOATING_POINT + USE_KISS_FFT, без внешних
зависимостей). Собственная линия задержки рендера на `delay_frames` кадров +
синхронный `speex_echo_cancellation` (встроенный буфер SpeexDSP фиксирован в
2 кадра и для Android не годится); дрейф потоков — через переполнение/недозаполнение
линии (drop/dup + passthrough). Категория лога `aec`.
Ядро: `call_audio_set_aec_enabled/delay_frames`, AEC в feed_pcm (захват→encode)
и pull_pcm (рендер-референс), lifecycle в start/release. Desktop — настройка GUI
`aec_enabled` (`[chatserver]`, читается в `call_audio_start`). Android — включается
только на спикерфоне (`route==SPEAKER`, live через JNI `callAudioSetAec`),
задержка меряется `AudioTrack/AudioRecord.getTimestamp`; `CallAudioEngine.playLoop`
переведён на absolute-deadline (устранён систематический дрейф). Тест
`test_speex_aec` (подавление эха 25.5 дБ, сохранность речи 0.956, delay-line, NULL-гарды).
Сборки: autotools + chatgui CMake + Android CMake/APK.
[+] **Silero VAD в lib/** — добавлен детектор голосовой активности (Silero VAD v5,
стриминговая ONNX-модель + обёртка `lib/silero_vad.c/h` над ONNX Runtime C API).
API: `silero_vad_create/create_default/destroy/reset/process/process_pcm16`
(окно 512 сэмплов @16 кГц, вероятность речи [0..1], рекуррентное состояние).
Модель вшита в бинарник (`silero_vad_model.inc`, `create_default`), файл не нужен.
Категория лога `vad`. Модуль под `#ifdef HAVE_SILERO_VAD` — без onnxruntime
компилируется в стабы, поэтому GLOB-сборки (chatgui/Android) не ломаются.
Сборка: `--with-silero-vad` (autotools), `-DHAVE_SILERO_VAD` + libonnxruntime
(chatgui CMake, Android NDK). `make check`: 111/0/1.
[+] **Авто-PTT рации по VAD** — сделано. Отдельный режим `radio_vad_enabled` (ручной
PTT не меняется, при зажатой кнопке — override). Сервисный слой `radio_audio.c`:
ресемплер 48k→16k моно (3-tap box + 3:1, стерео микшируется), накопитель окон
512, фильтр-гистерезис + машина состояний (`src/radio/radio_vad.h`, чистый,
юнит-тестируемый): старт при подтверждении 2 окон (~64мс) выше порога
`radio_vad_threshold`, стоп при тишине `radio_vad_hangover_ms` (200мс) или чужом
разговоре (busy = RX-кадр <200мс). История захвата (pre-roll) удалена; передаётся текущий PCM.
Компрессор в VAD-режиме байпасится. Desktop `radio_audio_engine.cpp` и Android
`RadioAudioEngine.kt` фидят PCM постоянно в VAD-режиме; JNI `radioVadMode()`.
Тест `test_radio_vad` (ресемплер + FSM). Android: onnxruntime AAR 1.23.2 в
`jniLibs/` + `cpp/onnxruntime/include` + `-DHAVE_SILERO_VAD` в CMake.
Android: настройки VAD (включение, порог, задержка после тишины); микрофон в PTT
создаётся только для передачи. PTT запускается при касании, после отпускания
передаёт ещё 250мс; повторное касание отменяет завершение (двойной тап без FIN).
VAD работает 15 минут при горящем экране;
SCREEN_OFF освобождает захват, SCREEN_ON автоматически открывает новое окно.
RX остаётся активен; TX освобождается отдельно через `radio_audio_capture_stop()`.
[+] **chatgui (Qt) не собирался: C++ void* в utun_node.cpp** — сделано. Причина:
`proxy_protocol.h` (C-заголовок, включается в C++ `utun_node.cpp`) делал
`e->dgram = u_malloc(...)` и `struct proxy_flow* f = arg;` без явных приведений —
в C++ нет неявного `void*`→T*. Добавлены `(uint8_t*)`/`(struct proxy_flow*)`
(валидны и в C). `vibechat` собирается (47МБ, с вшитой моделью + onnxruntime).
[+] **test_auto_socket_dynamic: root-регрессия и нагрузка** — изолированный namespace,
корректные маршруты, двусторонний поток через backpressure, строгие seq/payload,
пределы TX/RX, остановка при обрыве и drain после восстановления, баланс ресурсов.
Исправлены привязка TCP к интерфейсу, удаление TCP моделей/линков/портов из БД,
sender receive window при selective ACK, timestamp overflow и нулевой jitter.
ASan/UBSan/LSan: PASS; дополнительно исправлены невыровненные метки аллокаторов
и PID-порты в test_etcp_connect. [Разбор и логи](etcp_full_suite_findings.md).
[ ] **debug_parse_config: формат и индексы категорий** — parser ожидает `:`, документация
описывает `=`; преобразование битовой маски осталось после перехода enum на индексы.
[Разбор](etcp_full_suite_findings.md).
[+] **Q8/Android не подключается к локальному чатгуи (LAN)** — сделано. Причина: тип сокета
в Android-конфиге был захардкожен `type=public`, из-за чего `sock_match` ставил
`has_priv=0` и не создавал линки к приватным (LAN) адресам пиров (`create_links … → 0 links`).
Исправлено: (1) `ip_is_public()` вынесен в `lib/platform_compat` (единый классификатор
IPv4 с корректным ntohl); (2) в `auto_socket_reconcile_config` (Android-режим) тип сокета
выставляется по адресу (public→PUBLIC, приватный/link-local→NAT), конфиг `type` игнорируется;
(3) `sock_collect_views` больше не смотрит на `type` из конфига: `has_priv` — по классификации
локального адреса, `has_inet=1` (исходящие считаем доступными), `is_eim` — по NAT-детекции
(hairpin). `is_local_subnet` упрощён до `!ip_is_public()`. Тест `test_sock_match` обновлён
(+ `ip_is_public` кейсы). `check.sh`: 86 passed / 1 failed / 1 skipped.
[ ] **DM: прямой p2p чат с любым пользователем группы** — отдельная DM-подсистема.
ТЗ и архитектура: `/doc/dm_arch.md`. Статус: реализовано (dm_core/dm_mailbox/dm_crypto),
тесты `test_dm` (крипто) и `test_dm_e2e` (интеграция). Остались задачи ниже.
@ -234,73 +12,7 @@
(локальный буфер отправки при переполнении роутера) + адаптивный джиттер-буфер
(SoundTouch, ускорение до 1.6x) + тоны (глитч/завершение) + watchdog 20с.
Android: переключение источника (трубка/громкая/гарнитура).
Архитектура: `/doc/chat_call_arch.md`.
[+] **Сигналинг + релей медиа** — сделано. `call_proto.h` (svc `ETCP_RT_ID_CALL
0x36`, subcmd INVITE/RINGING/ACCEPT/DECLINE/BUSY/CANCEL/HANGUP/MEDIA),
`call.c` (машина состояний, реестр сессий, conn_mgr, ring-timeout 45с,
watchdog no-traffic 20с, потеря соединения → end, события
CHAT_EVT_CALL_* 34–40), `DEBUG_CATEGORY_CALL 32`. `test_call` (2 инстанса,
happy path + decline + media-релей). Приём медиа — через `call_set_media_recv_cb`.
[+] **Headless API** — сделано. Команды control-сокета `call_start/accept/decline/
hangup` + события `call_incoming/ringing/accepted/ended/declined` (JSON);
аудио-сокет `call/call_headless.c` (TCP, бинарный фрейм
`[type:1][call_id:8][len:2][data]`, HELLO/FRAME), конфиг `call_audio_bind`.
Разрыв conn_mgr — отложенно 500мс (`CALL_TEARDOWN_DELAY_TB`) после ENDED,
чтобы HANGUP успел долететь; защита от stale-handle через conn-status.
Тесты: `test_call` (сигналинг+медиа), `test_call_headless` (полный e2e через
control+audio сокеты).
[+] **Аудио-контур (time-stretch в сервисном слое)** — сделано. `src/call/audio_jitter.cpp`
(SoundTouch, C API `ajb_*`: адаптивный джиттер-буфер ≤1000мс, target 60мс,
tempo 1.0→1.6x — догон live без потери данных при сетевой паузе),
`src/call/call_tones.c` (глитч при сталле >500мс + тон завершения),
`src/call/call_audio.c` (Opus 48kHz/20мс + jitter + тоны + pull API финального PCM).
`call.c`: `call_audio_ops` (on_media c seq/ts, get_stats) + событие
`CHAT_EVT_CALL_STATS` раз в 1с [rtt_ms, buffer_ms, tempo_x100].
Desktop `call_audio_engine.cpp` — только miniaudio I/O (получает финальный PCM).
Тест `test_audio_jitter` (ёмкость/догон/стационар). Демон остаётся C-only.
[+] **Android** — сделано. JNI (`utun_bridge_call_*`/`nativeCall*`), AudioRecord/Track
(CallAudioEngine), роутинг источника (CallAudioRouter: EARPIECE/SPEAKER/HEADSET,
BT SCO), CallScreen + IncomingCallDialog + RingtonePlayer + IncomingCallManager,
`libutun_lite/call_audio.c` (собственный аудио-контур Opus 48kHz/20мс).
[+] **Единый голосовой стек (voice stack) + SoundTouch на Android** — сделано.
Унификация аудио-контура звонка: `libutun_voice` (статическая C++-либа с
C-интерфейсом `call_audio.h` = SoundTouch + `voice_jitter.cpp` + `call_audio.c`
+ `call_tones.c`), собирается через `src/call/voice_sources.cmake` и линкуется
desktop/Android/headless-CLI. Джиттер-буфер держит КОДИРОВАННЫЕ (Opus) кадры,
декодирует только на pull (непосредственно перед воспроизведением) — память ~48x
меньше и uasync-поток разгружен. Android переведён с push-FIFO (Kotlin ArrayDeque)
на pull-модель (`nativeCallAudioPull`), своего джиттера больше нет → убрана
асимметрия задержки (desktop↔Android). Headless-аудио-сокет отдаёт готовый PCM
(декод Opus→PCM, без тактирования; клиент сам задаёт темп). Диагностика:
`CALL_STATS` расширен [dropped, underruns] (раз в 1с), счётчики headless.
Удалены `audio_jitter.cpp` и `libutun_lite/call_audio.c`. Тест `test_voice_jitter`
(ёмкость/догон/стационар). Android — `ANDROID_STL=c++_static`.
[ ] **DM-звонки** (fallback `group_id==0`) — не сделано: `call_is_online`/`call_start`
требуют реальную группу (`topo_groups_find`), для DM (group_id==0) маршрута нет.
## Звонок: две фазы исходящего вызова (индикация + гудки)
[+] **Две фазы вызова** — сделано. Исходящий звонок разбит на 2 фазы с индикацией
в окне вызова и разными гудками:
- фаза 1 «Пытаемся связаться…» (`OUTGOING` — INVITE/маршрут) — гудок «соединение»;
- фаза 2 «Звоним…» (`RINGING` — у пира идёт вызов) — длинные гудки (ringback).
Desktop: `CallWindow::Ringing` + `SoundManager` loop `call_connecting`/`call_ringing`
(MP3 в qrc), переключение по `onCallRinging`, остановка по accepted/ended/error;
добавлена обработка `GUI_EVT_CALL_ERROR` (40) — раньше при «пир офлайн» окно/гудок
зависали навсегда. Android: `CallRingback` (MediaPlayer loop из res/raw),
подписи `OUTGOING`/`RINGING` в CallScreen + ActiveCallBanner.
Звуковые события добавлены в SoundSettingsPage (call_connecting/call_ringing).
[+] **Надёжность отмены вызова (кодограмма + watchdog)** — сделано. Отмена вызова
(HANGUP/CANCEL) шлётся один раз, без ретрансмита, и при флапе маршрута (RST/epoch,
прямой TCP к мобиле не поднимается — только релей) терялась: вызываемый висел в
RINGING вечно и отвечал BUSY на все новые INVITE. Исправлено моделью «сердцебиения»:
- вызывающий шлёт кодограмму INVITE каждые 300мс весь период вызова (OUTGOING+RINGING);
- у вызываемого watchdog 2с — нет кодограммы → вызов сорван (`CALL_REASON_CANCEL`);
- завершающие кодограммы без ретрансмиссий (потерю покрывает watchdog);
- запоздалые кодограммы вызова при разговоре (CONNECTING/ACTIVE) игнорируются.
`CALL_INVITE_RETRY_TB=3000`, `CALL_RING_DEAD_TB=20000`, `CALL_NO_ROUTE_MAX_STRIKES=20`.
Архитектура: `/doc/chat_call_arch.md`. Статус: спроектировано, ждёт реализации.
## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e)
@ -419,24 +131,7 @@
в нескольких прогонах. В `test_dm_e2e` при teardown остаётся timer-leak (~68 узлов,
`router_no_route`), не влияет на результат — стоит разобрать отдельно.
[ ] **lwip/proxy: утечка pcb в FIN_WAIT_2** — `tcp_shutdown(pcb,0,1)` (half-close по FIN от
exit) не ставит `TF_RXCLOSED`, поэтому таймаут FIN_WAIT_2 в `tcp_slowtmr` не срабатывает:
если браузер никогда не пришлёт FIN, pcb навсегда остаётся в FIN_WAIT_2 (утечка, не crash).
Двойной free по TIME_WAIT уже закрыт (detach по локальному закрытию + guard abort).
## Открытые флаки
[+] **test_chat_join_e2e** — сделано. Причина: ложный критерий успеха джойнера J.
`wait_group_started()` проверял `senders_list` непуст в предположении «senders_list
непуст ⟺ J получил JOIN_READY». После коммита b483335c `tgi_to_channel_cb`
(topo_group_invite.c) при `NCD_EVENT_UP` стал вызывать `topo_group_new_conn` →
conn попадает в `senders_list` сразу после установки TCP/UDP-соединения, ДО
завершения хендшейка (JOIN_INFO_REQ→RESP→REQUEST→READY). J выходил «OK» раньше,
чем A получал JOIN_REQUEST → A не подписывал J → `wait_signed` на A/C падал
(`get_sign_rc=-1`). Фикс: критерий J — событие `CHAT_EVT_CONNECT_RESULT(result=0)`
(JOIN_READY), ловится через `chat_event_set_handler`; `wait_group_started` удалён.
Это также убрало гонку «C вышел раньше, чем J синкает свой рекорд» (C выходит сразу
после `wait_signed`, не дожидаясь merkle-синка J). 5 прогонов стабильно 3/3.
[ ] **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`:
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает.

28
lib/Makefile.am

@ -40,41 +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
-I$(top_srcdir)/lib/libopus/include
# Clean build directory
clean-local:

2
lib/async_dns.c

@ -251,7 +251,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);
}

422
lib/audio_compressor.c

@ -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;
int enabled;
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, output_cap;
uint32_t clip_count;
unsigned dbg_counter;
};
size_t output_size;
size_t output_cap;
/* Начальное усиление в середине диапазона в дБ. */
static float initial_gain(float max_gain) { return max_gain > 0.0f ? sqrtf(max_gain) : 1.0f; }
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;
}
/* Ноль 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;
}
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;
}
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; }
/* Одна оценка для всех каналов сохраняет баланс стерео. */
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;
}
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) {
if (!ac) return;
ac->enabled = enabled ? 1 : 0;
}
int audio_compressor_is_enabled(const struct audio_compressor* ac) {
return ac ? ac->enabled : 0;
}
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;
}
/* Выход записи растёт только вне аппаратного 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;
float rms = (float)sqrt(sum_sq / (double)count);
return (rms * 2.0f + peak) / 2.0f;
}
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;
}
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;
}
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;
}
/* Общий алгоритм усиления для записи с 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;
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,103 +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);
/* Слить один готовый блок записи. История хранится по абсолютному номеру в кольце. */
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;
/* 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--;
}
/* Ровно один кадр, без 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;
}
void audio_compressor_flush(struct audio_compressor* ac) {
if (!ac) return;
if (!ac->enabled) return;
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;
}
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;
}
return 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);
}
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;
}
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; }
size_t audio_compressor_output_size(const struct audio_compressor* ac) {
return ac ? ac->output_size : 0;
}

18
lib/audio_compressor.h

@ -18,36 +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;
} 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);
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);
#ifdef __cplusplus
}
#endif

4
lib/debug_config.c

@ -132,10 +132,6 @@ static const struct {
{"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},
{"all", DEBUG_CATEGORY_ALL},
{NULL, DEBUG_CATEGORY_NONE}
};

6
lib/debug_config.h

@ -70,11 +70,7 @@ typedef int debug_category_t;
#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_COUNT 36 // Total number of categories
#define DEBUG_CATEGORY_COUNT 32 // Total number of categories
#define DEBUG_CATEGORY_ALL (-1) // special value for all categories
/* Debug configuration structure */

37
lib/json_flat.c

@ -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;

3
lib/json_flat.h

@ -2,7 +2,7 @@
* @file json_flat.h
* @brief Парсер плоского JSON без вложенности: {"key":"value",...}
*
* Только строковые значения. Callback получает полные строки без обрезания.
* Только строковые значения. Без malloc, без зависимостей.
*/
#ifndef JSON_FLAT_H
@ -15,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);

165
lib/ll_queue.c

@ -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;
}

24
lib/ll_queue.h

@ -61,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[] (байт)
@ -124,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; ///< список отложенных уведомлений очереди
};
/**
@ -148,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;
@ -160,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; // Размер хеш-таблицы
@ -247,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 очередь
@ -280,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);

1
lib/ll_queue_doc.md

@ -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. |
### Запись

21
lib/mem.c

@ -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);

40
lib/memory_pool.c

@ -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;

238
lib/miniaudio.h

@ -7543,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;
@ -7565,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;
@ -7577,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;
@ -11557,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. */
@ -13767,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;
@ -20216,8 +20200,6 @@ static void ma_device__on_notification(ma_device_notification notification)
{
MA_ASSERT(notification.pDevice != NULL);
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);
}
@ -20378,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) {
@ -20426,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);
}
@ -20610,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. */
}
}
@ -20646,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);
@ -20658,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)) {
@ -20669,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;
@ -20689,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;
}
@ -30939,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);
@ -30960,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);
@ -30973,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);
@ -31161,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);
}
}
@ -31226,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) {
@ -31242,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. */
@ -31252,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) {
@ -31268,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;
@ -31443,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;
@ -31458,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;
@ -31618,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;
}
@ -31655,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;
}
@ -31707,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);
@ -31716,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;
@ -31816,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) {
@ -31826,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. */
}
@ -31835,24 +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_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. */
}
@ -31863,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)
@ -31882,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) {
@ -31895,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. */
@ -31928,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;
}
@ -31947,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);
@ -31964,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) {
@ -31981,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)
@ -32007,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);
}
}
@ -32149,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()) {
@ -32308,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;
@ -32480,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;
}
@ -32613,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;
}
}
@ -32713,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");
@ -32734,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");
@ -32747,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");
@ -32780,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;
@ -32801,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;
@ -32814,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;
@ -32846,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;
@ -32867,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;
@ -32880,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;
@ -43904,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);
@ -44146,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;
}
@ -44315,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);
}
@ -44647,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) {
@ -44666,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;
}

5
lib/opus_codec.c

@ -146,11 +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));
}
const char* opus_codec_strerror(int err) {
switch (err) {
case OPUS_OK: return "OK";

5
lib/opus_codec.h

@ -10,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,
@ -27,7 +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);
const char* opus_codec_strerror(int err);

19
lib/platform_compat.c

@ -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

5
lib/platform_compat.h

@ -201,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

301
lib/silero_vad.c

@ -1,301 +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
struct silero_vad {
const OrtApi* ort;
OrtEnv* env;
OrtSession* session;
OrtMemoryInfo* mem_info;
float state[SILERO_VAD_STATE_COUNT]; /* рекуррентное состояние между вызовами */
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", label, len);
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));
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "silero_vad_reset");
}
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_WINDOW_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];
int rc = -1;
if (!vad || !samples || !prob) {
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_process: invalid args");
return -1;
}
ort = vad->ort;
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, (void*)samples,
SILERO_VAD_WINDOW_SAMPLES * sizeof(float), 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));
*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 */

76
lib/silero_vad.h

@ -1,76 +0,0 @@
/**
* Silero VAD (Voice Activity Detector) — C-обёртка над ONNX Runtime.
*
* Запускает официальную стриминговую модель silero_vad.onnx (v5, ~2.3 МБ),
* которая по окну аудио возвращает вероятность наличия речи [0..1].
* Рекуррентное состояние (GRU) хранится внутри объекта и переносится между
* вызовами, поэтому детектор работает в потоковом (real-time) режиме.
*
* Требования к входному аудио:
* - частота дискретизации 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 */

BIN
lib/silero_vad.onnx

Binary file not shown.

136918
lib/silero_vad_model.inc

File diff suppressed because it is too large Load Diff

178
lib/speex_aec.c

@ -1,178 +0,0 @@
// speex_aec.c — C-обёртка над SpeexDSP mdf.c (акустическое эхоподавление).
//
// API см. в speex_aec.h. Ключевые детали реализации:
// - собственная линия задержки рендера на depth кадров (SpeexDSP даёт только
// фиксированные 2 кадра — PLAYBACK_DELAY в mdf.c);
// - синхронный speex_echo_cancellation(rec, play_delayed, out) с выравниванием
// рендер↔захват по линии задержки;
// - дрейф независимых потоков: переполнение линии → отбрасываем старый кадр,
// недозаполнение (старт/underflow) → passthrough без канселлера.
//
// Рендер принимает чанки любого размера; захват — ровно один полный кадр.
// Частые аномалии учитываются счётчиками; владелец публикует сводку вне audio callback.
#include "speex_aec.h"
#include "debug_config.h"
#include "mem.h"
#include <string.h>
#include <limits.h>
#include <speex/speex_echo.h>
#define AEC_ID "speex_aec"
struct speex_aec {
SpeexEchoState* st;
int mic_channels, render_channels;
int frame_samples; /* сэмплов в кадре (960 @48кГц) */
int depth; /* глубина линии задержки в кадрах (>=1) */
int16_t* line; /* кольцо depth*frame_samples */
int head; /* индекс самого старого кадра в line */
int count; /* кадров в линии (0..depth) */
int16_t* play_acc; /* аккумулятор рендера до кадра */
int play_len;
uint32_t overruns; /* отброшено рендер-кадров (дрейф: рендер быстрее) */
uint32_t underruns; /* passthrough-кадров (дрейф: рендер медленнее/старт) */
};
/* Положить полный рендер-кадр в линию задержки (вытесняя старейший при переполнении). */
static void aec_enqueue(speex_aec_t* a, const int16_t* frame) {
if (a->count == a->depth) {
a->head = (a->head + 1) % a->depth;
a->count--;
a->overruns++;
}
memcpy(a->line + (size_t)((a->head + a->count) % a->depth) * a->frame_samples * a->render_channels,
frame, (size_t)a->frame_samples * a->render_channels * sizeof(int16_t));
a->count++;
}
speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames) {
return speex_aec_create_mc(sample_rate, frame_samples, filter_samples, delay_frames, 1, 1);
}
/* Независимые каналы референса: противофазное стерео не усредняется. */
speex_aec_t* speex_aec_create_mc(int sample_rate, int frame_samples, int filter_samples, int delay_frames,
int mic_channels, int render_channels) {
speex_aec_t* a;
int rate;
if (sample_rate <= 0 || frame_samples <= 0 || filter_samples < frame_samples ||
mic_channels < 1 || mic_channels > 2 || render_channels < 1 || render_channels > 2 ||
frame_samples > INT_MAX / render_channels || frame_samples > INT_MAX / mic_channels ||
(delay_frames > 0 && delay_frames > INT_MAX / frame_samples / render_channels)) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: bad args rate=%d frame=%d filter=%d delay=%d",
AEC_ID, sample_rate, frame_samples, filter_samples, delay_frames);
return NULL;
}
a = (speex_aec_t*)u_calloc(1, sizeof(*a));
if (!a) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: OOM for state", AEC_ID);
return NULL;
}
a->st = speex_echo_state_init_mc(frame_samples, filter_samples, mic_channels, render_channels);
if (!a->st) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: echo state allocation failed", AEC_ID);
u_free(a);
return NULL;
}
rate = sample_rate;
if (speex_echo_ctl(a->st, SPEEX_ECHO_SET_SAMPLING_RATE, &rate) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: SET_SAMPLING_RATE(%d) failed", AEC_ID, sample_rate);
speex_echo_state_destroy(a->st);
u_free(a);
return NULL;
}
a->mic_channels = mic_channels; a->render_channels = render_channels;
a->frame_samples = frame_samples;
a->depth = delay_frames > 0 ? delay_frames : 1;
a->line = (int16_t*)u_calloc((uint32_t)(a->depth * frame_samples * render_channels), sizeof(int16_t));
a->play_acc = (int16_t*)u_calloc((uint32_t)(frame_samples * render_channels), sizeof(int16_t));
if (!a->line || !a->play_acc) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: OOM for buffers", AEC_ID);
if (a->line) u_free(a->line);
if (a->play_acc) u_free(a->play_acc);
speex_echo_state_destroy(a->st);
u_free(a);
return NULL;
}
DEBUG_INFO(DEBUG_CATEGORY_AEC, "%s: created rate=%d frame=%d filter=%d delay=%d frames mic=%d render=%d",
AEC_ID, sample_rate, frame_samples, filter_samples, a->depth, mic_channels, render_channels);
return a;
}
void speex_aec_destroy(speex_aec_t* a) {
if (!a) return;
speex_echo_state_destroy(a->st);
u_free(a->line);
u_free(a->play_acc);
u_free(a);
}
void speex_aec_reset(speex_aec_t* a) {
if (!a) return;
speex_echo_state_reset(a->st);
a->head = 0;
a->count = 0;
a->play_len = 0;
DEBUG_INFO(DEBUG_CATEGORY_AEC, "%s: reset (filter + delay line)", AEC_ID);
}
void speex_aec_feed_playback(speex_aec_t* a, const int16_t* pcm, int count) {
int fs, take;
if (!a || !pcm || count <= 0) return;
fs = a->frame_samples * a->render_channels;
while (count > 0) {
take = fs - a->play_len;
if (take > count) take = count;
memcpy(a->play_acc + a->play_len, pcm, (size_t)take * sizeof(int16_t));
a->play_len += take;
pcm += take;
count -= take;
if (a->play_len == fs) {
aec_enqueue(a, a->play_acc);
a->play_len = 0;
}
}
}
int speex_aec_process_capture(speex_aec_t* a, const int16_t* pcm, int count, int16_t* out) {
int fs;
if (!a || !pcm || !out || count <= 0) return 0;
fs = a->frame_samples;
if (count != fs * a->mic_channels) {
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: capture requires one frame samples=%d expected=%d", AEC_ID, count, fs * a->mic_channels);
return 0;
}
if (a->count == a->depth) {
const int16_t* ref = a->line + (size_t)a->head * fs * a->render_channels;
speex_echo_cancellation(a->st, pcm, ref, out);
a->head = (a->head + 1) % a->depth;
a->count--;
} else {
/* линия не наполнена (старт или рендер отстаёт): passthrough без канселлера */
memcpy(out, pcm, (size_t)count * sizeof(int16_t));
a->underruns++;
}
return count;
}
int speex_aec_delay_fill(const speex_aec_t* a) {
return a ? a->count : 0;
}
void speex_aec_get_stats(const speex_aec_t* a, uint32_t* overruns, uint32_t* underruns) {
*overruns = a ? a->overruns : 0;
*underruns = a ? a->underruns : 0;
}

80
lib/speex_aec.h

@ -1,80 +0,0 @@
/*
* speex_aec.h — акустическое эхоподавление (AEC), C-обёртка над SpeexDSP mdf.c.
*
* Подавляет эхо дальнего конца (то, что играем в динамик) в сигнале микрофона
* перед кодированием. Работает на interleaved int16 PCM, 1/2 канала, частота 48000 (можно
* 8000/16000/32000/48000), кадр 20 мс (960 сэмплов @48 кГц).
*
* Модель использования (duplex-контур звонка):
* - рендер (far-end, то что пошло в динамик) → speex_aec_feed_playback();
* - захват (near-end, микрофон) → speex_aec_process_capture().
*
* Обёртка держит собственную линию задержки рендера на `delay_frames` кадров и
* зовёт синхронный speex_echo_cancellation(rec, play_delayed, out). Это делает
* выравнивание рендер↔захват предсказуемым и настраиваемым (встроенный буфер
* SpeexDSP фиксирован в 2 кадра и для Android-задержки не годится).
*
* Дрейф двух независимых потоков (capture/play на Android) компенсируется
* ограниченной глубиной линии: переполнение → отбрасываем старый рендер-кадр,
* недозаполнение → passthrough без канселлера; счётчики публикует владелец вне callback.
*
* Зависимости: SpeexDSP (mdf.c/fftwrap.c/kiss_fft*) из lib/speexdsp, флаги
* FLOATING_POINT + USE_KISS_FFT. Собирается всегда (без внешних зависимостей).
*
* Лицензия SpeexDSP: 3-clause BSD (Xiph) — см. lib/speexdsp/COPYING.
*/
#ifndef SPEEX_AEC_H
#define SPEEX_AEC_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct speex_aec speex_aec_t;
/**
* Создать эхоканселлер.
* sample_rate — 48000 (также 8000/16000/32000);
* frame_samples — сэмплов в кадре (960 = 20 мс @48 кГц);
* filter_samples— длина эхо-хвоста в сэмплах (14400 = 300 мс, кратно кадру);
* delay_frames — глубина линии рендер→захват в кадрах (значения <=0 приводятся к 1).
* Возвращает NULL при ошибке (лог категории "aec").
*/
speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames);
/* Многоканальный AEC: frame_samples на канал, count в feed/process — все interleaved отсчёты.
* mic_channels и render_channels — 1/2; возвращаемый count захвата равен frame_samples*mic_channels. */
speex_aec_t* speex_aec_create_mc(int sample_rate, int frame_samples, int filter_samples, int delay_frames,
int mic_channels, int render_channels);
/* Освободить канселлер. NULL безопасен. */
void speex_aec_destroy(speex_aec_t* aec);
/* Сбросить адаптивный фильтр и линию задержки (смена устройства/роута). */
void speex_aec_reset(speex_aec_t* aec);
/**
* Подать рендер (дальний конец). Накопление до кадра внутри; полный кадр
* кладётся в линию задержки. count может быть любым (обычно 480 или 960).
*/
void speex_aec_feed_playback(speex_aec_t* aec, const int16_t* pcm, int count);
/**
* Обработать ровно один кадр захвата (count == frame_samples*mic_channels).
* Возвращает count при успехе, 0 при ошибке. out вмещает полный interleaved кадр
* и не должен алиаситься с pcm. Накопитель аппаратных чанков принадлежит I/O-движку.
*/
int speex_aec_process_capture(speex_aec_t* aec, const int16_t* pcm, int count, int16_t* out);
/* Актуальная глубина линии задержки (кадров) — для диагностики дрейфа. */
int speex_aec_delay_fill(const speex_aec_t* aec);
/* Под той же блокировкой, что feed/process; выводить снимок вне callback. */
void speex_aec_get_stats(const speex_aec_t* aec, uint32_t* overruns, uint32_t* underruns);
#ifdef __cplusplus
}
#endif
#endif /* SPEEX_AEC_H */

35
lib/speexdsp/COPYING

@ -1,35 +0,0 @@
Copyright 2002-2008 Xiph.org Foundation
Copyright 2002-2008 Jean-Marc Valin
Copyright 2005-2007 Analog Devices Inc.
Copyright 2005-2008 Commonwealth Scientific and Industrial Research
Organisation (CSIRO)
Copyright 1993, 2002, 2006 David Rowe
Copyright 2003 EpicGames
Copyright 1992-1994 Jutta Degener, Carsten Bormann
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 the Xiph.org Foundation nor the names of its
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 FOUNDATION 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.

160
lib/speexdsp/_kiss_fft_guts.h

@ -1,160 +0,0 @@
/*
Copyright (c) 2003-2004, Mark Borgerding
All rights reserved.
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 author nor the names of any 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.
*/
#define MIN(a,b) ((a)<(b) ? (a):(b))
#define MAX(a,b) ((a)>(b) ? (a):(b))
/* kiss_fft.h
defines kiss_fft_scalar as either short or a float type
and defines
typedef struct { kiss_fft_scalar r; kiss_fft_scalar i; }kiss_fft_cpx; */
#include "kiss_fft.h"
#include "math_approx.h"
#define MAXFACTORS 32
/* e.g. an fft of length 128 has 4 factors
as far as kissfft is concerned
4*4*4*2
*/
struct kiss_fft_state{
int nfft;
int inverse;
int factors[2*MAXFACTORS];
kiss_fft_cpx twiddles[1];
};
/*
Explanation of macros dealing with complex math:
C_MUL(m,a,b) : m = a*b
C_FIXDIV( c , div ) : if a fixed point impl., c /= div. noop otherwise
C_SUB( res, a,b) : res = a - b
C_SUBFROM( res , a) : res -= a
C_ADDTO( res , a) : res += a
* */
#ifdef FIXED_POINT
#include "arch.h"
# define FRACBITS 15
# define SAMPPROD spx_int32_t
#define SAMP_MAX 32767
#define SAMP_MIN -SAMP_MAX
#if defined(CHECK_OVERFLOW)
# define CHECK_OVERFLOW_OP(a,op,b) \
if ( (SAMPPROD)(a) op (SAMPPROD)(b) > SAMP_MAX || (SAMPPROD)(a) op (SAMPPROD)(b) < SAMP_MIN ) { \
fprintf(stderr,"WARNING:overflow @ " __FILE__ "(%d): (%d " #op" %d) = %ld\n",__LINE__,(a),(b),(SAMPPROD)(a) op (SAMPPROD)(b) ); }
#endif
# define smul(a,b) ( (SAMPPROD)(a)*(b) )
# define sround( x ) (kiss_fft_scalar)( ( (x) + (1<<(FRACBITS-1)) ) >> FRACBITS )
# define S_MUL(a,b) sround( smul(a,b) )
# define C_MUL(m,a,b) \
do{ (m).r = sround( smul((a).r,(b).r) - smul((a).i,(b).i) ); \
(m).i = sround( smul((a).r,(b).i) + smul((a).i,(b).r) ); }while(0)
# define C_MUL4(m,a,b) \
do{ (m).r = PSHR32( smul((a).r,(b).r) - smul((a).i,(b).i),17 ); \
(m).i = PSHR32( smul((a).r,(b).i) + smul((a).i,(b).r),17 ); }while(0)
# define DIVSCALAR(x,k) \
(x) = sround( smul( x, SAMP_MAX/k ) )
# define C_FIXDIV(c,div) \
do { DIVSCALAR( (c).r , div); \
DIVSCALAR( (c).i , div); }while (0)
# define C_MULBYSCALAR( c, s ) \
do{ (c).r = sround( smul( (c).r , s ) ) ;\
(c).i = sround( smul( (c).i , s ) ) ; }while(0)
#else /* not FIXED_POINT*/
# define S_MUL(a,b) ( (a)*(b) )
#define C_MUL(m,a,b) \
do{ (m).r = (a).r*(b).r - (a).i*(b).i;\
(m).i = (a).r*(b).i + (a).i*(b).r; }while(0)
#define C_MUL4(m,a,b) C_MUL(m,a,b)
# define C_FIXDIV(c,div) /* NOOP */
# define C_MULBYSCALAR( c, s ) \
do{ (c).r *= (s);\
(c).i *= (s); }while(0)
#endif
#ifndef CHECK_OVERFLOW_OP
# define CHECK_OVERFLOW_OP(a,op,b) /* noop */
#endif
#define C_ADD( res, a,b)\
do { \
CHECK_OVERFLOW_OP((a).r,+,(b).r)\
CHECK_OVERFLOW_OP((a).i,+,(b).i)\
(res).r=(a).r+(b).r; (res).i=(a).i+(b).i; \
}while(0)
#define C_SUB( res, a,b)\
do { \
CHECK_OVERFLOW_OP((a).r,-,(b).r)\
CHECK_OVERFLOW_OP((a).i,-,(b).i)\
(res).r=(a).r-(b).r; (res).i=(a).i-(b).i; \
}while(0)
#define C_ADDTO( res , a)\
do { \
CHECK_OVERFLOW_OP((res).r,+,(a).r)\
CHECK_OVERFLOW_OP((res).i,+,(a).i)\
(res).r += (a).r; (res).i += (a).i;\
}while(0)
#define C_SUBFROM( res , a)\
do {\
CHECK_OVERFLOW_OP((res).r,-,(a).r)\
CHECK_OVERFLOW_OP((res).i,-,(a).i)\
(res).r -= (a).r; (res).i -= (a).i; \
}while(0)
#ifdef FIXED_POINT
# define KISS_FFT_COS(phase) floor(MIN(32767,MAX(-32767,.5+32768 * cos (phase))))
# define KISS_FFT_SIN(phase) floor(MIN(32767,MAX(-32767,.5+32768 * sin (phase))))
# define HALF_OF(x) ((x)>>1)
#elif defined(USE_SIMD)
# define KISS_FFT_COS(phase) _mm_set1_ps( cos(phase) )
# define KISS_FFT_SIN(phase) _mm_set1_ps( sin(phase) )
# define HALF_OF(x) ((x)*_mm_set1_ps(.5))
#else
# define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase)
# define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase)
# define HALF_OF(x) ((x)*.5)
#endif
#define kf_cexp(x,phase) \
do{ \
(x)->r = KISS_FFT_COS(phase);\
(x)->i = KISS_FFT_SIN(phase);\
}while(0)
#define kf_cexp2(x,phase) \
do{ \
(x)->r = spx_cos_norm((phase));\
(x)->i = spx_cos_norm((phase)-32768);\
}while(0)
/* a debugging function */
#define pcpx(c)\
fprintf(stderr,"%g + %gi\n",(double)((c)->r),(double)((c)->i) )

232
lib/speexdsp/arch.h

@ -1,232 +0,0 @@
/* Copyright (C) 2003 Jean-Marc Valin */
/**
@file arch.h
@brief Various architecture definitions Speex
*/
/*
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 the Xiph.org Foundation nor the names of its
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 FOUNDATION 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.
*/
#ifndef ARCH_H
#define ARCH_H
/* A couple test to catch stupid option combinations */
#ifdef FIXED_POINT
#ifdef FLOATING_POINT
#error You cannot compile as floating point and fixed point at the same time
#endif
#ifdef USE_SSE
#error SSE is only for floating-point
#endif
#if defined(ARM4_ASM) + defined(ARM5E_ASM) + defined(BFIN_ASM) > 1
#error Make up your mind. What CPU do you have?
#endif
#ifdef VORBIS_PSYCHO
#error Vorbis-psy model currently not implemented in fixed-point
#endif
#else
#ifndef FLOATING_POINT
#error You now need to define either FIXED_POINT or FLOATING_POINT
#endif
#if defined(ARM4_ASM) || defined(ARM5E_ASM) || defined(BFIN_ASM)
#error I suppose you can have a [ARM4/ARM5E/Blackfin] that has float instructions?
#endif
#ifdef FIXED_DEBUG
#error "Don't you think enabling fixed-point is a good thing to do if you want to debug that?"
#endif
#endif
#ifndef OUTSIDE_SPEEX
#include "speex/speexdsp_types.h"
#endif
#define ABS(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute integer value. */
#define ABS16(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute 16-bit value. */
#define MIN16(a,b) ((a) < (b) ? (a) : (b)) /**< Maximum 16-bit value. */
#define MAX16(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 16-bit value. */
#define ABS32(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute 32-bit value. */
#define MIN32(a,b) ((a) < (b) ? (a) : (b)) /**< Maximum 32-bit value. */
#define MAX32(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 32-bit value. */
#ifdef FIXED_POINT
typedef spx_int16_t spx_word16_t;
typedef spx_int32_t spx_word32_t;
typedef spx_word32_t spx_mem_t;
typedef spx_word16_t spx_coef_t;
typedef spx_word16_t spx_lsp_t;
typedef spx_word32_t spx_sig_t;
#define Q15ONE 32767
#define LPC_SCALING 8192
#define SIG_SCALING 16384
#define LSP_SCALING 8192.
#define GAMMA_SCALING 32768.
#define GAIN_SCALING 64
#define GAIN_SCALING_1 0.015625
#define LPC_SHIFT 13
#define LSP_SHIFT 13
#define SIG_SHIFT 14
#define GAIN_SHIFT 6
#define WORD2INT(x) ((x) < -32767 ? -32768 : ((x) > 32766 ? 32767 : (x)))
#define VERY_SMALL 0
#define VERY_LARGE32 ((spx_word32_t)2147483647)
#define VERY_LARGE16 ((spx_word16_t)32767)
#define Q15_ONE ((spx_word16_t)32767)
#ifdef FIXED_DEBUG
#include "fixed_debug.h"
#else
#include "fixed_generic.h"
#ifdef ARM5E_ASM
#include "fixed_arm5e.h"
#elif defined(ARM4_ASM)
#include "fixed_arm4.h"
#elif defined(BFIN_ASM)
#include "fixed_bfin.h"
#endif
#endif
#else
typedef float spx_mem_t;
typedef float spx_coef_t;
typedef float spx_lsp_t;
typedef float spx_sig_t;
typedef float spx_word16_t;
typedef float spx_word32_t;
#define Q15ONE 1.0f
#define LPC_SCALING 1.f
#define SIG_SCALING 1.f
#define LSP_SCALING 1.f
#define GAMMA_SCALING 1.f
#define GAIN_SCALING 1.f
#define GAIN_SCALING_1 1.f
#define VERY_SMALL 1e-15f
#define VERY_LARGE32 1e15f
#define VERY_LARGE16 1e15f
#define Q15_ONE ((spx_word16_t)1.f)
#define QCONST16(x,bits) (x)
#define QCONST32(x,bits) (x)
#define NEG16(x) (-(x))
#define NEG32(x) (-(x))
#define EXTRACT16(x) (x)
#define EXTEND32(x) (x)
#define SHR16(a,shift) (a)
#define SHL16(a,shift) (a)
#define SHR32(a,shift) (a)
#define SHL32(a,shift) (a)
#define PSHR16(a,shift) (a)
#define PSHR32(a,shift) (a)
#define VSHR32(a,shift) (a)
#define SATURATE16(x,a) (x)
#define SATURATE32(x,a) (x)
#define SATURATE32PSHR(x,shift,a) (x)
#define PSHR(a,shift) (a)
#define SHR(a,shift) (a)
#define SHL(a,shift) (a)
#define SATURATE(x,a) (x)
#define ADD16(a,b) ((a)+(b))
#define SUB16(a,b) ((a)-(b))
#define ADD32(a,b) ((a)+(b))
#define SUB32(a,b) ((a)-(b))
#define MULT16_16_16(a,b) ((a)*(b))
#define MULT16_32_32(a,b) ((a)*(b))
#define MULT16_16(a,b) ((spx_word32_t)(a)*(spx_word32_t)(b))
#define MAC16_16(c,a,b) ((c)+(spx_word32_t)(a)*(spx_word32_t)(b))
#define MULT16_32_Q15(a,b) ((a)*(b))
#define MULT16_32_P15(a,b) ((a)*(b))
#define MAC16_32_Q15(c,a,b) ((c)+(a)*(b))
#define MAC16_16_Q11(c,a,b) ((c)+(a)*(b))
#define MAC16_16_Q13(c,a,b) ((c)+(a)*(b))
#define MAC16_16_P13(c,a,b) ((c)+(a)*(b))
#define MULT16_16_Q11_32(a,b) ((a)*(b))
#define MULT16_16_Q13(a,b) ((a)*(b))
#define MULT16_16_Q14(a,b) ((a)*(b))
#define MULT16_16_Q15(a,b) ((a)*(b))
#define MULT16_16_P15(a,b) ((a)*(b))
#define MULT16_16_P13(a,b) ((a)*(b))
#define MULT16_16_P14(a,b) ((a)*(b))
#define DIV32_16(a,b) (((spx_word32_t)(a))/(spx_word16_t)(b))
#define PDIV32_16(a,b) (((spx_word32_t)(a))/(spx_word16_t)(b))
#define DIV32(a,b) (((spx_word32_t)(a))/(spx_word32_t)(b))
#define PDIV32(a,b) (((spx_word32_t)(a))/(spx_word32_t)(b))
#define WORD2INT(x) ((x) < -32767.5f ? -32768 : \
((x) > 32766.5f ? 32767 : (spx_int16_t)floor(.5 + (x))))
#endif
#if defined(CONFIG_TI_C54X) || defined(CONFIG_TI_C55X)
/* 2 on TI C5x DSP */
#define BYTES_PER_CHAR 2
#define BITS_PER_CHAR 16
#define LOG2_BITS_PER_CHAR 4
#else
#define BYTES_PER_CHAR 1
#define BITS_PER_CHAR 8
#define LOG2_BITS_PER_CHAR 3
#endif
#ifdef FIXED_DEBUG
extern long long spx_mips;
#endif
#endif

448
lib/speexdsp/fftwrap.c

@ -1,448 +0,0 @@
/* Copyright (C) 2005-2006 Jean-Marc Valin
File: fftwrap.c
Wrapper for various FFTs
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 the Xiph.org Foundation nor the names of its
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 FOUNDATION 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.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "arch.h"
#include "os_support.h"
#define MAX_FFT_SIZE 2048
#ifdef FIXED_POINT
static int maximize_range(spx_word16_t *in, spx_word16_t *out, spx_word16_t bound, int len)
{
int i, shift;
spx_word16_t max_val = 0;
for (i=0;i<len;i++)
{
if (in[i]>max_val)
max_val = in[i];
if (-in[i]>max_val)
max_val = -in[i];
}
shift=0;
while (max_val <= (bound>>1) && max_val != 0)
{
max_val <<= 1;
shift++;
}
for (i=0;i<len;i++)
{
out[i] = SHL16(in[i], shift);
}
return shift;
}
static void renorm_range(spx_word16_t *in, spx_word16_t *out, int shift, int len)
{
int i;
for (i=0;i<len;i++)
{
out[i] = PSHR16(in[i], shift);
}
}
#endif
#ifdef USE_SMALLFT
#include "smallft.h"
#include <math.h>
void *spx_fft_init(int size)
{
struct drft_lookup *table;
table = speex_alloc(sizeof(struct drft_lookup));
spx_drft_init((struct drft_lookup *)table, size);
return (void*)table;
}
void spx_fft_destroy(void *table)
{
spx_drft_clear(table);
speex_free(table);
}
void spx_fft(void *table, float *in, float *out)
{
if (in==out)
{
int i;
float scale = 1./((struct drft_lookup *)table)->n;
speex_warning("FFT should not be done in-place");
for (i=0;i<((struct drft_lookup *)table)->n;i++)
out[i] = scale*in[i];
} else {
int i;
float scale = 1./((struct drft_lookup *)table)->n;
for (i=0;i<((struct drft_lookup *)table)->n;i++)
out[i] = scale*in[i];
}
spx_drft_forward((struct drft_lookup *)table, out);
}
void spx_ifft(void *table, float *in, float *out)
{
if (in==out)
{
speex_warning("FFT should not be done in-place");
} else {
int i;
for (i=0;i<((struct drft_lookup *)table)->n;i++)
out[i] = in[i];
}
spx_drft_backward((struct drft_lookup *)table, out);
}
#elif defined(USE_INTEL_MKL)
#include <mkl.h>
struct mkl_config {
DFTI_DESCRIPTOR_HANDLE desc;
int N;
};
void *spx_fft_init(int size)
{
struct mkl_config *table = (struct mkl_config *) speex_alloc(sizeof(struct mkl_config));
table->N = size;
DftiCreateDescriptor(&table->desc, DFTI_SINGLE, DFTI_REAL, 1, size);
DftiSetValue(table->desc, DFTI_PACKED_FORMAT, DFTI_PACK_FORMAT);
DftiSetValue(table->desc, DFTI_PLACEMENT, DFTI_NOT_INPLACE);
DftiSetValue(table->desc, DFTI_FORWARD_SCALE, 1.0f / size);
DftiCommitDescriptor(table->desc);
return table;
}
void spx_fft_destroy(void *table)
{
struct mkl_config *t = (struct mkl_config *) table;
DftiFreeDescriptor(t->desc);
speex_free(table);
}
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
{
struct mkl_config *t = (struct mkl_config *) table;
DftiComputeForward(t->desc, in, out);
}
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
{
struct mkl_config *t = (struct mkl_config *) table;
DftiComputeBackward(t->desc, in, out);
}
#elif defined(USE_INTEL_IPP)
#include <ipps.h>
struct ipp_fft_config
{
IppsDFTSpec_R_32f *dftSpec;
Ipp8u *buffer;
};
void *spx_fft_init(int size)
{
int bufferSize = 0;
int hint;
struct ipp_fft_config *table;
table = (struct ipp_fft_config *)speex_alloc(sizeof(struct ipp_fft_config));
/* there appears to be no performance difference between ippAlgHintFast and
ippAlgHintAccurate when using the with the floating point version
of the fft. */
hint = ippAlgHintAccurate;
ippsDFTInitAlloc_R_32f(&table->dftSpec, size, IPP_FFT_DIV_FWD_BY_N, hint);
ippsDFTGetBufSize_R_32f(table->dftSpec, &bufferSize);
table->buffer = ippsMalloc_8u(bufferSize);
return table;
}
void spx_fft_destroy(void *table)
{
struct ipp_fft_config *t = (struct ipp_fft_config *)table;
ippsFree(t->buffer);
ippsDFTFree_R_32f(t->dftSpec);
speex_free(t);
}
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
{
struct ipp_fft_config *t = (struct ipp_fft_config *)table;
ippsDFTFwd_RToPack_32f(in, out, t->dftSpec, t->buffer);
}
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
{
struct ipp_fft_config *t = (struct ipp_fft_config *)table;
ippsDFTInv_PackToR_32f(in, out, t->dftSpec, t->buffer);
}
#elif defined(USE_GPL_FFTW3)
#include <fftw3.h>
struct fftw_config {
float *in;
float *out;
fftwf_plan fft;
fftwf_plan ifft;
int N;
};
void *spx_fft_init(int size)
{
struct fftw_config *table = (struct fftw_config *) speex_alloc(sizeof(struct fftw_config));
table->in = fftwf_malloc(sizeof(float) * (size+2));
table->out = fftwf_malloc(sizeof(float) * (size+2));
table->fft = fftwf_plan_dft_r2c_1d(size, table->in, (fftwf_complex *) table->out, FFTW_PATIENT);
table->ifft = fftwf_plan_dft_c2r_1d(size, (fftwf_complex *) table->in, table->out, FFTW_PATIENT);
table->N = size;
return table;
}
void spx_fft_destroy(void *table)
{
struct fftw_config *t = (struct fftw_config *) table;
fftwf_destroy_plan(t->fft);
fftwf_destroy_plan(t->ifft);
fftwf_free(t->in);
fftwf_free(t->out);
speex_free(table);
}
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
{
int i;
struct fftw_config *t = (struct fftw_config *) table;
const int N = t->N;
float *iptr = t->in;
float *optr = t->out;
const float m = 1.0 / N;
for(i=0;i<N;++i)
iptr[i]=in[i] * m;
fftwf_execute(t->fft);
out[0] = optr[0];
for(i=1;i<N;++i)
out[i] = optr[i+1];
}
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
{
int i;
struct fftw_config *t = (struct fftw_config *) table;
const int N = t->N;
float *iptr = t->in;
float *optr = t->out;
iptr[0] = in[0];
iptr[1] = 0.0f;
for(i=1;i<N;++i)
iptr[i+1] = in[i];
iptr[N+1] = 0.0f;
fftwf_execute(t->ifft);
for(i=0;i<N;++i)
out[i] = optr[i];
}
#elif defined(USE_KISS_FFT)
#include "kiss_fftr.h"
#include "kiss_fft.h"
struct kiss_config {
kiss_fftr_cfg forward;
kiss_fftr_cfg backward;
int N;
};
void *spx_fft_init(int size)
{
struct kiss_config *table;
table = (struct kiss_config*)speex_alloc(sizeof(struct kiss_config));
table->forward = kiss_fftr_alloc(size,0,NULL,NULL);
table->backward = kiss_fftr_alloc(size,1,NULL,NULL);
table->N = size;
return table;
}
void spx_fft_destroy(void *table)
{
struct kiss_config *t = (struct kiss_config *)table;
kiss_fftr_free(t->forward);
kiss_fftr_free(t->backward);
speex_free(table);
}
#ifdef FIXED_POINT
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
{
int shift;
struct kiss_config *t = (struct kiss_config *)table;
shift = maximize_range(in, in, 32000, t->N);
kiss_fftr2(t->forward, in, out);
renorm_range(in, in, shift, t->N);
renorm_range(out, out, shift, t->N);
}
#else
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out)
{
int i;
float scale;
struct kiss_config *t = (struct kiss_config *)table;
scale = 1./t->N;
kiss_fftr2(t->forward, in, out);
for (i=0;i<t->N;i++)
out[i] *= scale;
}
#endif
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out)
{
struct kiss_config *t = (struct kiss_config *)table;
kiss_fftri2(t->backward, in, out);
}
#else
#error No other FFT implemented
#endif
#ifdef FIXED_POINT
/*#include "smallft.h"*/
void spx_fft_float(void *table, float *in, float *out)
{
int i;
#ifdef USE_SMALLFT
int N = ((struct drft_lookup *)table)->n;
#elif defined(USE_KISS_FFT)
int N = ((struct kiss_config *)table)->N;
#else
#endif
#ifdef VAR_ARRAYS
spx_word16_t _in[N];
spx_word16_t _out[N];
#else
spx_word16_t _in[MAX_FFT_SIZE];
spx_word16_t _out[MAX_FFT_SIZE];
#endif
for (i=0;i<N;i++)
_in[i] = (int)floor(.5+in[i]);
spx_fft(table, _in, _out);
for (i=0;i<N;i++)
out[i] = _out[i];
#if 0
if (!fixed_point)
{
float scale;
struct drft_lookup t;
spx_drft_init(&t, ((struct kiss_config *)table)->N);
scale = 1./((struct kiss_config *)table)->N;
for (i=0;i<((struct kiss_config *)table)->N;i++)
out[i] = scale*in[i];
spx_drft_forward(&t, out);
spx_drft_clear(&t);
}
#endif
}
void spx_ifft_float(void *table, float *in, float *out)
{
int i;
#ifdef USE_SMALLFT
int N = ((struct drft_lookup *)table)->n;
#elif defined(USE_KISS_FFT)
int N = ((struct kiss_config *)table)->N;
#else
#endif
#ifdef VAR_ARRAYS
spx_word16_t _in[N];
spx_word16_t _out[N];
#else
spx_word16_t _in[MAX_FFT_SIZE];
spx_word16_t _out[MAX_FFT_SIZE];
#endif
for (i=0;i<N;i++)
_in[i] = (int)floor(.5+in[i]);
spx_ifft(table, _in, _out);
for (i=0;i<N;i++)
out[i] = _out[i];
#if 0
if (!fixed_point)
{
int i;
struct drft_lookup t;
spx_drft_init(&t, ((struct kiss_config *)table)->N);
for (i=0;i<((struct kiss_config *)table)->N;i++)
out[i] = in[i];
spx_drft_backward(&t, out);
spx_drft_clear(&t);
}
#endif
}
#else
void spx_fft_float(void *table, float *in, float *out)
{
spx_fft(table, in, out);
}
void spx_ifft_float(void *table, float *in, float *out)
{
spx_ifft(table, in, out);
}
#endif

58
lib/speexdsp/fftwrap.h

@ -1,58 +0,0 @@
/* Copyright (C) 2005 Jean-Marc Valin
File: fftwrap.h
Wrapper for various FFTs
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 the Xiph.org Foundation nor the names of its
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 FOUNDATION 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.
*/
#ifndef FFTWRAP_H
#define FFTWRAP_H
#include "arch.h"
/** Compute tables for an FFT */
void *spx_fft_init(int size);
/** Destroy tables for an FFT */
void spx_fft_destroy(void *table);
/** Forward (real to half-complex) transform */
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out);
/** Backward (half-complex to real) transform */
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out);
/** Forward (real to half-complex) transform of float data */
void spx_fft_float(void *table, float *in, float *out);
/** Backward (half-complex to real) transform of float data */
void spx_ifft_float(void *table, float *in, float *out);
#endif

170
lib/speexdsp/include/speex/speex_echo.h

@ -1,170 +0,0 @@
/* Copyright (C) Jean-Marc Valin */
/**
@file speex_echo.h
@brief Echo cancellation
*/
/*
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. 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.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
*/
#ifndef SPEEX_ECHO_H
#define SPEEX_ECHO_H
/** @defgroup SpeexEchoState SpeexEchoState: Acoustic echo canceller
* This is the acoustic echo canceller module.
* @{
*/
#include "speexdsp_types.h"
#ifdef __cplusplus
extern "C" {
#endif
/** Obtain frame size used by the AEC */
#define SPEEX_ECHO_GET_FRAME_SIZE 3
/** Set sampling rate */
#define SPEEX_ECHO_SET_SAMPLING_RATE 24
/** Get sampling rate */
#define SPEEX_ECHO_GET_SAMPLING_RATE 25
/* Can't set window sizes */
/** Get size of impulse response (int32) */
#define SPEEX_ECHO_GET_IMPULSE_RESPONSE_SIZE 27
/* Can't set window content */
/** Get impulse response (int32[]) */
#define SPEEX_ECHO_GET_IMPULSE_RESPONSE 29
/** Internal echo canceller state. Should never be accessed directly. */
struct SpeexEchoState_;
/** @class SpeexEchoState
* This holds the state of the echo canceller. You need one per channel.
*/
/** Internal echo canceller state. Should never be accessed directly. */
typedef struct SpeexEchoState_ SpeexEchoState;
/** Creates a new echo canceller state
* @param frame_size Number of samples to process at one time (should correspond to 10-20 ms)
* @param filter_length Number of samples of echo to cancel (should generally correspond to 100-500 ms)
* @return Newly-created echo canceller state
*/
SpeexEchoState *speex_echo_state_init(int frame_size, int filter_length);
/** Creates a new multi-channel echo canceller state
* @param frame_size Number of samples to process at one time (should correspond to 10-20 ms)
* @param filter_length Number of samples of echo to cancel (should generally correspond to 100-500 ms)
* @param nb_mic Number of microphone channels
* @param nb_speakers Number of speaker channels
* @return Newly-created echo canceller state
*/
SpeexEchoState *speex_echo_state_init_mc(int frame_size, int filter_length, int nb_mic, int nb_speakers);
/** Destroys an echo canceller state
* @param st Echo canceller state
*/
void speex_echo_state_destroy(SpeexEchoState *st);
/** Performs echo cancellation a frame, based on the audio sent to the speaker (no delay is added
* to playback in this form)
*
* @param st Echo canceller state
* @param rec Signal from the microphone (near end + far end echo)
* @param play Signal played to the speaker (received from far end)
* @param out Returns near-end signal with echo removed
*/
void speex_echo_cancellation(SpeexEchoState *st, const spx_int16_t *rec, const spx_int16_t *play, spx_int16_t *out);
/** Performs echo cancellation a frame (deprecated) */
void speex_echo_cancel(SpeexEchoState *st, const spx_int16_t *rec, const spx_int16_t *play, spx_int16_t *out, spx_int32_t *Yout);
/** Perform echo cancellation using internal playback buffer, which is delayed by two frames
* to account for the delay introduced by most soundcards (but it could be off!)
* @param st Echo canceller state
* @param rec Signal from the microphone (near end + far end echo)
* @param out Returns near-end signal with echo removed
*/
void speex_echo_capture(SpeexEchoState *st, const spx_int16_t *rec, spx_int16_t *out);
/** Let the echo canceller know that a frame was just queued to the soundcard
* @param st Echo canceller state
* @param play Signal played to the speaker (received from far end)
*/
void speex_echo_playback(SpeexEchoState *st, const spx_int16_t *play);
/** Reset the echo canceller to its original state
* @param st Echo canceller state
*/
void speex_echo_state_reset(SpeexEchoState *st);
/** Used like the ioctl function to control the echo canceller parameters
*
* @param st Echo canceller state
* @param request ioctl-type request (one of the SPEEX_ECHO_* macros)
* @param ptr Data exchanged to-from function
* @return 0 if no error, -1 if request in unknown
*/
int speex_echo_ctl(SpeexEchoState *st, int request, void *ptr);
struct SpeexDecorrState_;
typedef struct SpeexDecorrState_ SpeexDecorrState;
/** Create a state for the channel decorrelation algorithm
This is useful for multi-channel echo cancellation only
* @param rate Sampling rate
* @param channels Number of channels (it's a bit pointless if you don't have at least 2)
* @param frame_size Size of the frame to process at ones (counting samples *per* channel)
*/
SpeexDecorrState *speex_decorrelate_new(int rate, int channels, int frame_size);
/** Remove correlation between the channels by modifying the phase and possibly
adding noise in a way that is not (or little) perceptible.
* @param st Decorrelator state
* @param in Input audio in interleaved format
* @param out Result of the decorrelation (out *may* alias in)
* @param strength How much alteration of the audio to apply from 0 to 100.
*/
void speex_decorrelate(SpeexDecorrState *st, const spx_int16_t *in, spx_int16_t *out, int strength);
/** Destroy a Decorrelation state
* @param st State to destroy
*/
void speex_decorrelate_destroy(SpeexDecorrState *st);
#ifdef __cplusplus
}
#endif
/** @}*/
#endif

14
lib/speexdsp/include/speex/speexdsp_config_types.h

@ -1,14 +0,0 @@
/* speexdsp_config_types.h — статически сгенерировано для POSIX (Linux/FreeBSD/Android).
* В оригинале генерируется configure из speexdsp_config_types.h.in. На Windows ветка
* _WIN32 в speexdsp_types.h использует нативные типы и этот файл не инклудится. */
#ifndef __SPEEX_TYPES_H__
#define __SPEEX_TYPES_H__
#include <stdint.h>
typedef int16_t spx_int16_t;
typedef uint16_t spx_uint16_t;
typedef int32_t spx_int32_t;
typedef uint32_t spx_uint32_t;
#endif

126
lib/speexdsp/include/speex/speexdsp_types.h

@ -1,126 +0,0 @@
/* speexdsp_types.h taken from libogg */
/********************************************************************
* *
* THIS FILE IS PART OF THE OggVorbis SOFTWARE CODEC SOURCE CODE. *
* USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS *
* GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE *
* IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. *
* *
* THE OggVorbis SOURCE CODE IS (C) COPYRIGHT 1994-2002 *
* by the Xiph.Org Foundation http://www.xiph.org/ *
* *
********************************************************************
function: #ifdef jail to whip a few platforms into the UNIX ideal.
last mod: $Id: os_types.h 7524 2004-08-11 04:20:36Z conrad $
********************************************************************/
/**
@file speexdsp_types.h
@brief Speex types
*/
#ifndef _SPEEX_TYPES_H
#define _SPEEX_TYPES_H
#if defined(_WIN32)
# if defined(__CYGWIN__)
# include <_G_config.h>
typedef _G_int32_t spx_int32_t;
typedef _G_uint32_t spx_uint32_t;
typedef _G_int16_t spx_int16_t;
typedef _G_uint16_t spx_uint16_t;
# elif defined(__MINGW32__)
typedef short spx_int16_t;
typedef unsigned short spx_uint16_t;
typedef int spx_int32_t;
typedef unsigned int spx_uint32_t;
# elif defined(__MWERKS__)
typedef int spx_int32_t;
typedef unsigned int spx_uint32_t;
typedef short spx_int16_t;
typedef unsigned short spx_uint16_t;
# else
/* MSVC/Borland */
typedef __int32 spx_int32_t;
typedef unsigned __int32 spx_uint32_t;
typedef __int16 spx_int16_t;
typedef unsigned __int16 spx_uint16_t;
# endif
#elif defined(__MACOS__)
# include <sys/types.h>
typedef SInt16 spx_int16_t;
typedef UInt16 spx_uint16_t;
typedef SInt32 spx_int32_t;
typedef UInt32 spx_uint32_t;
#elif (defined(__APPLE__) && defined(__MACH__)) /* MacOS X Framework build */
# include <sys/types.h>
typedef int16_t spx_int16_t;
typedef u_int16_t spx_uint16_t;
typedef int32_t spx_int32_t;
typedef u_int32_t spx_uint32_t;
#elif defined(__BEOS__)
/* Be */
# include <inttypes.h>
typedef int16_t spx_int16_t;
typedef u_int16_t spx_uint16_t;
typedef int32_t spx_int32_t;
typedef u_int32_t spx_uint32_t;
#elif defined (__EMX__)
/* OS/2 GCC */
typedef short spx_int16_t;
typedef unsigned short spx_uint16_t;
typedef int spx_int32_t;
typedef unsigned int spx_uint32_t;
#elif defined (DJGPP)
/* DJGPP */
typedef short spx_int16_t;
typedef int spx_int32_t;
typedef unsigned int spx_uint32_t;
#elif defined(R5900)
/* PS2 EE */
typedef int spx_int32_t;
typedef unsigned spx_uint32_t;
typedef short spx_int16_t;
#elif defined(__SYMBIAN32__)
/* Symbian GCC */
typedef signed short spx_int16_t;
typedef unsigned short spx_uint16_t;
typedef signed int spx_int32_t;
typedef unsigned int spx_uint32_t;
#elif defined(CONFIG_TI_C54X) || defined (CONFIG_TI_C55X)
typedef short spx_int16_t;
typedef unsigned short spx_uint16_t;
typedef long spx_int32_t;
typedef unsigned long spx_uint32_t;
#elif defined(CONFIG_TI_C6X)
typedef short spx_int16_t;
typedef unsigned short spx_uint16_t;
typedef int spx_int32_t;
typedef unsigned int spx_uint32_t;
#else
#include "speexdsp_config_types.h"
#endif
#endif /* _SPEEX_TYPES_H */

523
lib/speexdsp/kiss_fft.c

@ -1,523 +0,0 @@
/*
Copyright (c) 2003-2004, Mark Borgerding
Copyright (c) 2005-2007, Jean-Marc Valin
All rights reserved.
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 author nor the names of any 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.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "_kiss_fft_guts.h"
#include "arch.h"
#include "os_support.h"
/* The guts header contains all the multiplication and addition macros that are defined for
fixed or floating point complex numbers. It also declares the kf_ internal functions.
*/
static void kf_bfly2(
kiss_fft_cpx * Fout,
const size_t fstride,
const kiss_fft_cfg st,
int m,
int N,
int mm
)
{
kiss_fft_cpx * Fout2;
kiss_fft_cpx * tw1;
kiss_fft_cpx t;
if (!st->inverse) {
int i,j;
kiss_fft_cpx * Fout_beg = Fout;
for (i=0;i<N;i++)
{
Fout = Fout_beg + i*mm;
Fout2 = Fout + m;
tw1 = st->twiddles;
for(j=0;j<m;j++)
{
/* Almost the same as the code path below, except that we divide the input by two
(while keeping the best accuracy possible) */
spx_word32_t tr, ti;
tr = SHR32(SUB32(MULT16_16(Fout2->r , tw1->r),MULT16_16(Fout2->i , tw1->i)), 1);
ti = SHR32(ADD32(MULT16_16(Fout2->i , tw1->r),MULT16_16(Fout2->r , tw1->i)), 1);
tw1 += fstride;
Fout2->r = PSHR32(SUB32(SHL32(EXTEND32(Fout->r), 14), tr), 15);
Fout2->i = PSHR32(SUB32(SHL32(EXTEND32(Fout->i), 14), ti), 15);
Fout->r = PSHR32(ADD32(SHL32(EXTEND32(Fout->r), 14), tr), 15);
Fout->i = PSHR32(ADD32(SHL32(EXTEND32(Fout->i), 14), ti), 15);
++Fout2;
++Fout;
}
}
} else {
int i,j;
kiss_fft_cpx * Fout_beg = Fout;
for (i=0;i<N;i++)
{
Fout = Fout_beg + i*mm;
Fout2 = Fout + m;
tw1 = st->twiddles;
for(j=0;j<m;j++)
{
C_MUL (t, *Fout2 , *tw1);
tw1 += fstride;
C_SUB( *Fout2 , *Fout , t );
C_ADDTO( *Fout , t );
++Fout2;
++Fout;
}
}
}
}
static void kf_bfly4(
kiss_fft_cpx * Fout,
const size_t fstride,
const kiss_fft_cfg st,
int m,
int N,
int mm
)
{
kiss_fft_cpx *tw1,*tw2,*tw3;
kiss_fft_cpx scratch[6];
const size_t m2=2*m;
const size_t m3=3*m;
int i, j;
if (st->inverse)
{
kiss_fft_cpx * Fout_beg = Fout;
for (i=0;i<N;i++)
{
Fout = Fout_beg + i*mm;
tw3 = tw2 = tw1 = st->twiddles;
for (j=0;j<m;j++)
{
C_MUL(scratch[0],Fout[m] , *tw1 );
C_MUL(scratch[1],Fout[m2] , *tw2 );
C_MUL(scratch[2],Fout[m3] , *tw3 );
C_SUB( scratch[5] , *Fout, scratch[1] );
C_ADDTO(*Fout, scratch[1]);
C_ADD( scratch[3] , scratch[0] , scratch[2] );
C_SUB( scratch[4] , scratch[0] , scratch[2] );
C_SUB( Fout[m2], *Fout, scratch[3] );
tw1 += fstride;
tw2 += fstride*2;
tw3 += fstride*3;
C_ADDTO( *Fout , scratch[3] );
Fout[m].r = scratch[5].r - scratch[4].i;
Fout[m].i = scratch[5].i + scratch[4].r;
Fout[m3].r = scratch[5].r + scratch[4].i;
Fout[m3].i = scratch[5].i - scratch[4].r;
++Fout;
}
}
} else
{
kiss_fft_cpx * Fout_beg = Fout;
for (i=0;i<N;i++)
{
Fout = Fout_beg + i*mm;
tw3 = tw2 = tw1 = st->twiddles;
for (j=0;j<m;j++)
{
C_MUL4(scratch[0],Fout[m] , *tw1 );
C_MUL4(scratch[1],Fout[m2] , *tw2 );
C_MUL4(scratch[2],Fout[m3] , *tw3 );
Fout->r = PSHR16(Fout->r, 2);
Fout->i = PSHR16(Fout->i, 2);
C_SUB( scratch[5] , *Fout, scratch[1] );
C_ADDTO(*Fout, scratch[1]);
C_ADD( scratch[3] , scratch[0] , scratch[2] );
C_SUB( scratch[4] , scratch[0] , scratch[2] );
Fout[m2].r = PSHR16(Fout[m2].r, 2);
Fout[m2].i = PSHR16(Fout[m2].i, 2);
C_SUB( Fout[m2], *Fout, scratch[3] );
tw1 += fstride;
tw2 += fstride*2;
tw3 += fstride*3;
C_ADDTO( *Fout , scratch[3] );
Fout[m].r = scratch[5].r + scratch[4].i;
Fout[m].i = scratch[5].i - scratch[4].r;
Fout[m3].r = scratch[5].r - scratch[4].i;
Fout[m3].i = scratch[5].i + scratch[4].r;
++Fout;
}
}
}
}
static void kf_bfly3(
kiss_fft_cpx * Fout,
const size_t fstride,
const kiss_fft_cfg st,
size_t m
)
{
size_t k=m;
const size_t m2 = 2*m;
kiss_fft_cpx *tw1,*tw2;
kiss_fft_cpx scratch[5];
kiss_fft_cpx epi3;
epi3 = st->twiddles[fstride*m];
tw1=tw2=st->twiddles;
do{
if (!st->inverse) {
C_FIXDIV(*Fout,3); C_FIXDIV(Fout[m],3); C_FIXDIV(Fout[m2],3);
}
C_MUL(scratch[1],Fout[m] , *tw1);
C_MUL(scratch[2],Fout[m2] , *tw2);
C_ADD(scratch[3],scratch[1],scratch[2]);
C_SUB(scratch[0],scratch[1],scratch[2]);
tw1 += fstride;
tw2 += fstride*2;
Fout[m].r = Fout->r - HALF_OF(scratch[3].r);
Fout[m].i = Fout->i - HALF_OF(scratch[3].i);
C_MULBYSCALAR( scratch[0] , epi3.i );
C_ADDTO(*Fout,scratch[3]);
Fout[m2].r = Fout[m].r + scratch[0].i;
Fout[m2].i = Fout[m].i - scratch[0].r;
Fout[m].r -= scratch[0].i;
Fout[m].i += scratch[0].r;
++Fout;
}while(--k);
}
static void kf_bfly5(
kiss_fft_cpx * Fout,
const size_t fstride,
const kiss_fft_cfg st,
int m
)
{
kiss_fft_cpx *Fout0,*Fout1,*Fout2,*Fout3,*Fout4;
int u;
kiss_fft_cpx scratch[13];
kiss_fft_cpx * twiddles = st->twiddles;
kiss_fft_cpx *tw;
kiss_fft_cpx ya,yb;
ya = twiddles[fstride*m];
yb = twiddles[fstride*2*m];
Fout0=Fout;
Fout1=Fout0+m;
Fout2=Fout0+2*m;
Fout3=Fout0+3*m;
Fout4=Fout0+4*m;
tw=st->twiddles;
for ( u=0; u<m; ++u ) {
if (!st->inverse) {
C_FIXDIV( *Fout0,5); C_FIXDIV( *Fout1,5); C_FIXDIV( *Fout2,5); C_FIXDIV( *Fout3,5); C_FIXDIV( *Fout4,5);
}
scratch[0] = *Fout0;
C_MUL(scratch[1] ,*Fout1, tw[u*fstride]);
C_MUL(scratch[2] ,*Fout2, tw[2*u*fstride]);
C_MUL(scratch[3] ,*Fout3, tw[3*u*fstride]);
C_MUL(scratch[4] ,*Fout4, tw[4*u*fstride]);
C_ADD( scratch[7],scratch[1],scratch[4]);
C_SUB( scratch[10],scratch[1],scratch[4]);
C_ADD( scratch[8],scratch[2],scratch[3]);
C_SUB( scratch[9],scratch[2],scratch[3]);
Fout0->r += scratch[7].r + scratch[8].r;
Fout0->i += scratch[7].i + scratch[8].i;
scratch[5].r = scratch[0].r + S_MUL(scratch[7].r,ya.r) + S_MUL(scratch[8].r,yb.r);
scratch[5].i = scratch[0].i + S_MUL(scratch[7].i,ya.r) + S_MUL(scratch[8].i,yb.r);
scratch[6].r = S_MUL(scratch[10].i,ya.i) + S_MUL(scratch[9].i,yb.i);
scratch[6].i = -S_MUL(scratch[10].r,ya.i) - S_MUL(scratch[9].r,yb.i);
C_SUB(*Fout1,scratch[5],scratch[6]);
C_ADD(*Fout4,scratch[5],scratch[6]);
scratch[11].r = scratch[0].r + S_MUL(scratch[7].r,yb.r) + S_MUL(scratch[8].r,ya.r);
scratch[11].i = scratch[0].i + S_MUL(scratch[7].i,yb.r) + S_MUL(scratch[8].i,ya.r);
scratch[12].r = - S_MUL(scratch[10].i,yb.i) + S_MUL(scratch[9].i,ya.i);
scratch[12].i = S_MUL(scratch[10].r,yb.i) - S_MUL(scratch[9].r,ya.i);
C_ADD(*Fout2,scratch[11],scratch[12]);
C_SUB(*Fout3,scratch[11],scratch[12]);
++Fout0;++Fout1;++Fout2;++Fout3;++Fout4;
}
}
/* perform the butterfly for one stage of a mixed radix FFT */
static void kf_bfly_generic(
kiss_fft_cpx * Fout,
const size_t fstride,
const kiss_fft_cfg st,
int m,
int p
)
{
int u,k,q1,q;
kiss_fft_cpx * twiddles = st->twiddles;
kiss_fft_cpx t;
kiss_fft_cpx scratchbuf[17];
int Norig = st->nfft;
/*CHECKBUF(scratchbuf,nscratchbuf,p);*/
if (p>17)
speex_fatal("KissFFT: max radix supported is 17");
for ( u=0; u<m; ++u ) {
k=u;
for ( q1=0 ; q1<p ; ++q1 ) {
scratchbuf[q1] = Fout[ k ];
if (!st->inverse) {
C_FIXDIV(scratchbuf[q1],p);
}
k += m;
}
k=u;
for ( q1=0 ; q1<p ; ++q1 ) {
int twidx=0;
Fout[ k ] = scratchbuf[0];
for (q=1;q<p;++q ) {
twidx += fstride * k;
if (twidx>=Norig) twidx-=Norig;
C_MUL(t,scratchbuf[q] , twiddles[twidx] );
C_ADDTO( Fout[ k ] ,t);
}
k += m;
}
}
}
static
void kf_shuffle(
kiss_fft_cpx * Fout,
const kiss_fft_cpx * f,
const size_t fstride,
int in_stride,
int * factors,
const kiss_fft_cfg st
)
{
const int p=*factors++; /* the radix */
const int m=*factors++; /* stage's fft length/p */
/*printf ("fft %d %d %d %d %d %d\n", p*m, m, p, s2, fstride*in_stride, N);*/
if (m==1)
{
int j;
for (j=0;j<p;j++)
{
Fout[j] = *f;
f += fstride*in_stride;
}
} else {
int j;
for (j=0;j<p;j++)
{
kf_shuffle( Fout , f, fstride*p, in_stride, factors,st);
f += fstride*in_stride;
Fout += m;
}
}
}
static
void kf_work(
kiss_fft_cpx * Fout,
const kiss_fft_cpx * f,
const size_t fstride,
int in_stride,
int * factors,
const kiss_fft_cfg st,
int N,
int s2,
int m2
)
{
int i;
kiss_fft_cpx * Fout_beg=Fout;
const int p=*factors++; /* the radix */
const int m=*factors++; /* stage's fft length/p */
#if 0
/*printf ("fft %d %d %d %d %d %d\n", p*m, m, p, s2, fstride*in_stride, N);*/
if (m==1)
{
/* int j;
for (j=0;j<p;j++)
{
Fout[j] = *f;
f += fstride*in_stride;
}*/
} else {
int j;
for (j=0;j<p;j++)
{
kf_work( Fout , f, fstride*p, in_stride, factors,st, N*p, fstride*in_stride, m);
f += fstride*in_stride;
Fout += m;
}
}
Fout=Fout_beg;
switch (p) {
case 2: kf_bfly2(Fout,fstride,st,m); break;
case 3: kf_bfly3(Fout,fstride,st,m); break;
case 4: kf_bfly4(Fout,fstride,st,m); break;
case 5: kf_bfly5(Fout,fstride,st,m); break;
default: kf_bfly_generic(Fout,fstride,st,m,p); break;
}
#else
/*printf ("fft %d %d %d %d %d %d %d\n", p*m, m, p, s2, fstride*in_stride, N, m2);*/
if (m==1)
{
/*for (i=0;i<N;i++)
{
int j;
Fout = Fout_beg+i*m2;
const kiss_fft_cpx * f2 = f+i*s2;
for (j=0;j<p;j++)
{
*Fout++ = *f2;
f2 += fstride*in_stride;
}
}*/
}else{
kf_work( Fout , f, fstride*p, in_stride, factors,st, N*p, fstride*in_stride, m);
}
switch (p) {
case 2: kf_bfly2(Fout,fstride,st,m, N, m2); break;
case 3: for (i=0;i<N;i++){Fout=Fout_beg+i*m2; kf_bfly3(Fout,fstride,st,m);} break;
case 4: kf_bfly4(Fout,fstride,st,m, N, m2); break;
case 5: for (i=0;i<N;i++){Fout=Fout_beg+i*m2; kf_bfly5(Fout,fstride,st,m);} break;
default: for (i=0;i<N;i++){Fout=Fout_beg+i*m2; kf_bfly_generic(Fout,fstride,st,m,p);} break;
}
#endif
}
/* facbuf is populated by p1,m1,p2,m2, ...
where
p[i] * m[i] = m[i-1]
m0 = n */
static
void kf_factor(int n,int * facbuf)
{
int p=4;
/*factor out powers of 4, powers of 2, then any remaining primes */
do {
while (n % p) {
switch (p) {
case 4: p = 2; break;
case 2: p = 3; break;
default: p += 2; break;
}
if (p>32000 || (spx_int32_t)p*(spx_int32_t)p > n)
p = n; /* no more factors, skip to end */
}
n /= p;
*facbuf++ = p;
*facbuf++ = n;
} while (n > 1);
}
/*
*
* User-callable function to allocate all necessary storage space for the fft.
*
* The return value is a contiguous block of memory, allocated with malloc. As such,
* It can be freed with free(), rather than a kiss_fft-specific function.
* */
kiss_fft_cfg kiss_fft_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem )
{
kiss_fft_cfg st=NULL;
size_t memneeded = sizeof(struct kiss_fft_state)
+ sizeof(kiss_fft_cpx)*(nfft-1); /* twiddle factors*/
if ( lenmem==NULL ) {
st = ( kiss_fft_cfg)KISS_FFT_MALLOC( memneeded );
}else{
if (mem != NULL && *lenmem >= memneeded)
st = (kiss_fft_cfg)mem;
*lenmem = memneeded;
}
if (st) {
int i;
st->nfft=nfft;
st->inverse = inverse_fft;
#ifdef FIXED_POINT
for (i=0;i<nfft;++i) {
spx_word32_t phase = i;
if (!st->inverse)
phase = -phase;
kf_cexp2(st->twiddles+i, DIV32(SHL32(phase,17),nfft));
}
#else
for (i=0;i<nfft;++i) {
const double pi=3.14159265358979323846264338327;
double phase = ( -2*pi /nfft ) * i;
if (st->inverse)
phase *= -1;
kf_cexp(st->twiddles+i, phase );
}
#endif
kf_factor(nfft,st->factors);
}
return st;
}
void kiss_fft_stride(kiss_fft_cfg st,const kiss_fft_cpx *fin,kiss_fft_cpx *fout,int in_stride)
{
if (fin == fout)
{
speex_fatal("In-place FFT not supported");
/*CHECKBUF(tmpbuf,ntmpbuf,st->nfft);
kf_work(tmpbuf,fin,1,in_stride, st->factors,st);
SPEEX_MOVE(fout,tmpbuf,st->nfft);*/
} else {
kf_shuffle( fout, fin, 1,in_stride, st->factors,st);
kf_work( fout, fin, 1,in_stride, st->factors,st, 1, in_stride, 1);
}
}
void kiss_fft(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout)
{
kiss_fft_stride(cfg,fin,fout,1);
}

108
lib/speexdsp/kiss_fft.h

@ -1,108 +0,0 @@
#ifndef KISS_FFT_H
#define KISS_FFT_H
#include <stdlib.h>
#include <math.h>
#include "arch.h"
#ifdef __cplusplus
extern "C" {
#endif
/*
ATTENTION!
If you would like a :
-- a utility that will handle the caching of fft objects
-- real-only (no imaginary time component ) FFT
-- a multi-dimensional FFT
-- a command-line utility to perform ffts
-- a command-line utility to perform fast-convolution filtering
Then see kfc.h kiss_fftr.h kiss_fftnd.h fftutil.c kiss_fastfir.c
in the tools/ directory.
*/
#ifdef USE_SIMD
# include <xmmintrin.h>
# define kiss_fft_scalar __m128
#define KISS_FFT_MALLOC(nbytes) memalign(16,nbytes)
#else
#define KISS_FFT_MALLOC speex_alloc
#endif
#ifdef FIXED_POINT
#include "arch.h"
# define kiss_fft_scalar spx_int16_t
#else
# ifndef kiss_fft_scalar
/* default is float */
# define kiss_fft_scalar float
# endif
#endif
typedef struct {
kiss_fft_scalar r;
kiss_fft_scalar i;
}kiss_fft_cpx;
typedef struct kiss_fft_state* kiss_fft_cfg;
/*
* kiss_fft_alloc
*
* Initialize a FFT (or IFFT) algorithm's cfg/state buffer.
*
* typical usage: kiss_fft_cfg mycfg=kiss_fft_alloc(1024,0,NULL,NULL);
*
* The return value from fft_alloc is a cfg buffer used internally
* by the fft routine or NULL.
*
* If lenmem is NULL, then kiss_fft_alloc will allocate a cfg buffer using malloc.
* The returned value should be free()d when done to avoid memory leaks.
*
* The state can be placed in a user supplied buffer 'mem':
* If lenmem is not NULL and mem is not NULL and *lenmem is large enough,
* then the function places the cfg in mem and the size used in *lenmem
* and returns mem.
*
* If lenmem is not NULL and ( mem is NULL or *lenmem is not large enough),
* then the function returns NULL and places the minimum cfg
* buffer size in *lenmem.
* */
kiss_fft_cfg kiss_fft_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem);
/*
* kiss_fft(cfg,in_out_buf)
*
* Perform an FFT on a complex input buffer.
* for a forward FFT,
* fin should be f[0] , f[1] , ... ,f[nfft-1]
* fout will be F[0] , F[1] , ... ,F[nfft-1]
* Note that each element is complex and can be accessed like
f[k].r and f[k].i
* */
void kiss_fft(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout);
/*
A more generic version of the above function. It reads its input from every Nth sample.
* */
void kiss_fft_stride(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout,int fin_stride);
/* If kiss_fft_alloc allocated a buffer, it is one contiguous
buffer and can be simply free()d when no longer needed*/
#define kiss_fft_free speex_free
/*
Cleans up some memory that gets managed internally. Not necessary to call, but it might clean up
your compiler output to call this before you exit.
*/
void kiss_fft_cleanup(void);
#ifdef __cplusplus
}
#endif
#endif

297
lib/speexdsp/kiss_fftr.c

@ -1,297 +0,0 @@
/*
Copyright (c) 2003-2004, Mark Borgerding
All rights reserved.
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 author nor the names of any 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.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "os_support.h"
#include "kiss_fftr.h"
#include "_kiss_fft_guts.h"
struct kiss_fftr_state{
kiss_fft_cfg substate;
kiss_fft_cpx * tmpbuf;
kiss_fft_cpx * super_twiddles;
#ifdef USE_SIMD
long pad;
#endif
};
kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem)
{
int i;
kiss_fftr_cfg st = NULL;
size_t subsize, memneeded;
if (nfft & 1) {
speex_warning("Real FFT optimization must be even.\n");
return NULL;
}
nfft >>= 1;
kiss_fft_alloc (nfft, inverse_fft, NULL, &subsize);
memneeded = sizeof(struct kiss_fftr_state) + subsize + sizeof(kiss_fft_cpx) * ( nfft * 2);
if (lenmem == NULL) {
st = (kiss_fftr_cfg) KISS_FFT_MALLOC (memneeded);
} else {
if (*lenmem >= memneeded)
st = (kiss_fftr_cfg) mem;
*lenmem = memneeded;
}
if (!st)
return NULL;
st->substate = (kiss_fft_cfg) (st + 1); /*just beyond kiss_fftr_state struct */
st->tmpbuf = (kiss_fft_cpx *) (((char *) st->substate) + subsize);
st->super_twiddles = st->tmpbuf + nfft;
kiss_fft_alloc(nfft, inverse_fft, st->substate, &subsize);
#ifdef FIXED_POINT
for (i=0;i<nfft;++i) {
spx_word32_t phase = i+(nfft>>1);
if (!inverse_fft)
phase = -phase;
kf_cexp2(st->super_twiddles+i, DIV32(SHL32(phase,16),nfft));
}
#else
for (i=0;i<nfft;++i) {
const double pi=3.14159265358979323846264338327;
double phase = pi*(((double)i) /nfft + .5);
if (!inverse_fft)
phase = -phase;
kf_cexp(st->super_twiddles+i, phase );
}
#endif
return st;
}
void kiss_fftr(kiss_fftr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata)
{
/* input buffer timedata is stored row-wise */
int k,ncfft;
kiss_fft_cpx fpnk,fpk,f1k,f2k,tw,tdc;
if ( st->substate->inverse) {
speex_fatal("kiss fft usage error: improper alloc\n");
}
ncfft = st->substate->nfft;
/*perform the parallel fft of two real signals packed in real,imag*/
kiss_fft( st->substate , (const kiss_fft_cpx*)timedata, st->tmpbuf );
/* The real part of the DC element of the frequency spectrum in st->tmpbuf
* contains the sum of the even-numbered elements of the input time sequence
* The imag part is the sum of the odd-numbered elements
*
* The sum of tdc.r and tdc.i is the sum of the input time sequence.
* yielding DC of input time sequence
* The difference of tdc.r - tdc.i is the sum of the input (dot product) [1,-1,1,-1...
* yielding Nyquist bin of input time sequence
*/
tdc.r = st->tmpbuf[0].r;
tdc.i = st->tmpbuf[0].i;
C_FIXDIV(tdc,2);
CHECK_OVERFLOW_OP(tdc.r ,+, tdc.i);
CHECK_OVERFLOW_OP(tdc.r ,-, tdc.i);
freqdata[0].r = tdc.r + tdc.i;
freqdata[ncfft].r = tdc.r - tdc.i;
#ifdef USE_SIMD
freqdata[ncfft].i = freqdata[0].i = _mm_set1_ps(0);
#else
freqdata[ncfft].i = freqdata[0].i = 0;
#endif
for ( k=1;k <= ncfft/2 ; ++k ) {
fpk = st->tmpbuf[k];
fpnk.r = st->tmpbuf[ncfft-k].r;
fpnk.i = - st->tmpbuf[ncfft-k].i;
C_FIXDIV(fpk,2);
C_FIXDIV(fpnk,2);
C_ADD( f1k, fpk , fpnk );
C_SUB( f2k, fpk , fpnk );
C_MUL( tw , f2k , st->super_twiddles[k]);
freqdata[k].r = HALF_OF(f1k.r + tw.r);
freqdata[k].i = HALF_OF(f1k.i + tw.i);
freqdata[ncfft-k].r = HALF_OF(f1k.r - tw.r);
freqdata[ncfft-k].i = HALF_OF(tw.i - f1k.i);
}
}
void kiss_fftri(kiss_fftr_cfg st,const kiss_fft_cpx *freqdata, kiss_fft_scalar *timedata)
{
/* input buffer timedata is stored row-wise */
int k, ncfft;
if (st->substate->inverse == 0) {
speex_fatal("kiss fft usage error: improper alloc\n");
}
ncfft = st->substate->nfft;
st->tmpbuf[0].r = freqdata[0].r + freqdata[ncfft].r;
st->tmpbuf[0].i = freqdata[0].r - freqdata[ncfft].r;
/*C_FIXDIV(st->tmpbuf[0],2);*/
for (k = 1; k <= ncfft / 2; ++k) {
kiss_fft_cpx fk, fnkc, fek, fok, tmp;
fk = freqdata[k];
fnkc.r = freqdata[ncfft - k].r;
fnkc.i = -freqdata[ncfft - k].i;
/*C_FIXDIV( fk , 2 );
C_FIXDIV( fnkc , 2 );*/
C_ADD (fek, fk, fnkc);
C_SUB (tmp, fk, fnkc);
C_MUL (fok, tmp, st->super_twiddles[k]);
C_ADD (st->tmpbuf[k], fek, fok);
C_SUB (st->tmpbuf[ncfft - k], fek, fok);
#ifdef USE_SIMD
st->tmpbuf[ncfft - k].i *= _mm_set1_ps(-1.0);
#else
st->tmpbuf[ncfft - k].i *= -1;
#endif
}
kiss_fft (st->substate, st->tmpbuf, (kiss_fft_cpx *) timedata);
}
void kiss_fftr2(kiss_fftr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_scalar *freqdata)
{
/* input buffer timedata is stored row-wise */
int k,ncfft;
kiss_fft_cpx f2k,tdc;
spx_word32_t f1kr, f1ki, twr, twi;
if ( st->substate->inverse) {
speex_fatal("kiss fft usage error: improper alloc\n");
}
ncfft = st->substate->nfft;
/*perform the parallel fft of two real signals packed in real,imag*/
kiss_fft( st->substate , (const kiss_fft_cpx*)timedata, st->tmpbuf );
/* The real part of the DC element of the frequency spectrum in st->tmpbuf
* contains the sum of the even-numbered elements of the input time sequence
* The imag part is the sum of the odd-numbered elements
*
* The sum of tdc.r and tdc.i is the sum of the input time sequence.
* yielding DC of input time sequence
* The difference of tdc.r - tdc.i is the sum of the input (dot product) [1,-1,1,-1...
* yielding Nyquist bin of input time sequence
*/
tdc.r = st->tmpbuf[0].r;
tdc.i = st->tmpbuf[0].i;
C_FIXDIV(tdc,2);
CHECK_OVERFLOW_OP(tdc.r ,+, tdc.i);
CHECK_OVERFLOW_OP(tdc.r ,-, tdc.i);
freqdata[0] = tdc.r + tdc.i;
freqdata[2*ncfft-1] = tdc.r - tdc.i;
for ( k=1;k <= ncfft/2 ; ++k )
{
/*fpk = st->tmpbuf[k];
fpnk.r = st->tmpbuf[ncfft-k].r;
fpnk.i = - st->tmpbuf[ncfft-k].i;
C_FIXDIV(fpk,2);
C_FIXDIV(fpnk,2);
C_ADD( f1k, fpk , fpnk );
C_SUB( f2k, fpk , fpnk );
C_MUL( tw , f2k , st->super_twiddles[k]);
freqdata[2*k-1] = HALF_OF(f1k.r + tw.r);
freqdata[2*k] = HALF_OF(f1k.i + tw.i);
freqdata[2*(ncfft-k)-1] = HALF_OF(f1k.r - tw.r);
freqdata[2*(ncfft-k)] = HALF_OF(tw.i - f1k.i);
*/
/*f1k.r = PSHR32(ADD32(EXTEND32(st->tmpbuf[k].r), EXTEND32(st->tmpbuf[ncfft-k].r)),1);
f1k.i = PSHR32(SUB32(EXTEND32(st->tmpbuf[k].i), EXTEND32(st->tmpbuf[ncfft-k].i)),1);
f2k.r = PSHR32(SUB32(EXTEND32(st->tmpbuf[k].r), EXTEND32(st->tmpbuf[ncfft-k].r)),1);
f2k.i = SHR32(ADD32(EXTEND32(st->tmpbuf[k].i), EXTEND32(st->tmpbuf[ncfft-k].i)),1);
C_MUL( tw , f2k , st->super_twiddles[k]);
freqdata[2*k-1] = HALF_OF(f1k.r + tw.r);
freqdata[2*k] = HALF_OF(f1k.i + tw.i);
freqdata[2*(ncfft-k)-1] = HALF_OF(f1k.r - tw.r);
freqdata[2*(ncfft-k)] = HALF_OF(tw.i - f1k.i);
*/
f2k.r = SHR32(SUB32(EXTEND32(st->tmpbuf[k].r), EXTEND32(st->tmpbuf[ncfft-k].r)),1);
f2k.i = PSHR32(ADD32(EXTEND32(st->tmpbuf[k].i), EXTEND32(st->tmpbuf[ncfft-k].i)),1);
f1kr = SHL32(ADD32(EXTEND32(st->tmpbuf[k].r), EXTEND32(st->tmpbuf[ncfft-k].r)),13);
f1ki = SHL32(SUB32(EXTEND32(st->tmpbuf[k].i), EXTEND32(st->tmpbuf[ncfft-k].i)),13);
twr = SHR32(SUB32(MULT16_16(f2k.r,st->super_twiddles[k].r),MULT16_16(f2k.i,st->super_twiddles[k].i)), 1);
twi = SHR32(ADD32(MULT16_16(f2k.i,st->super_twiddles[k].r),MULT16_16(f2k.r,st->super_twiddles[k].i)), 1);
#ifdef FIXED_POINT
freqdata[2*k-1] = PSHR32(f1kr + twr, 15);
freqdata[2*k] = PSHR32(f1ki + twi, 15);
freqdata[2*(ncfft-k)-1] = PSHR32(f1kr - twr, 15);
freqdata[2*(ncfft-k)] = PSHR32(twi - f1ki, 15);
#else
freqdata[2*k-1] = .5f*(f1kr + twr);
freqdata[2*k] = .5f*(f1ki + twi);
freqdata[2*(ncfft-k)-1] = .5f*(f1kr - twr);
freqdata[2*(ncfft-k)] = .5f*(twi - f1ki);
#endif
}
}
void kiss_fftri2(kiss_fftr_cfg st,const kiss_fft_scalar *freqdata,kiss_fft_scalar *timedata)
{
/* input buffer timedata is stored row-wise */
int k, ncfft;
if (st->substate->inverse == 0) {
speex_fatal ("kiss fft usage error: improper alloc\n");
}
ncfft = st->substate->nfft;
st->tmpbuf[0].r = freqdata[0] + freqdata[2*ncfft-1];
st->tmpbuf[0].i = freqdata[0] - freqdata[2*ncfft-1];
/*C_FIXDIV(st->tmpbuf[0],2);*/
for (k = 1; k <= ncfft / 2; ++k) {
kiss_fft_cpx fk, fnkc, fek, fok, tmp;
fk.r = freqdata[2*k-1];
fk.i = freqdata[2*k];
fnkc.r = freqdata[2*(ncfft - k)-1];
fnkc.i = -freqdata[2*(ncfft - k)];
/*C_FIXDIV( fk , 2 );
C_FIXDIV( fnkc , 2 );*/
C_ADD (fek, fk, fnkc);
C_SUB (tmp, fk, fnkc);
C_MUL (fok, tmp, st->super_twiddles[k]);
C_ADD (st->tmpbuf[k], fek, fok);
C_SUB (st->tmpbuf[ncfft - k], fek, fok);
#ifdef USE_SIMD
st->tmpbuf[ncfft - k].i *= _mm_set1_ps(-1.0);
#else
st->tmpbuf[ncfft - k].i *= -1;
#endif
}
kiss_fft (st->substate, st->tmpbuf, (kiss_fft_cpx *) timedata);
}

51
lib/speexdsp/kiss_fftr.h

@ -1,51 +0,0 @@
#ifndef KISS_FTR_H
#define KISS_FTR_H
#include "kiss_fft.h"
#ifdef __cplusplus
extern "C" {
#endif
/*
Real optimized version can save about 45% cpu time vs. complex fft of a real seq.
*/
typedef struct kiss_fftr_state *kiss_fftr_cfg;
kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem, size_t * lenmem);
/*
nfft must be even
If you don't care to allocate space, use mem = lenmem = NULL
*/
void kiss_fftr(kiss_fftr_cfg cfg,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata);
/*
input timedata has nfft scalar points
output freqdata has nfft/2+1 complex points
*/
void kiss_fftr2(kiss_fftr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_scalar *freqdata);
void kiss_fftri(kiss_fftr_cfg cfg,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata);
void kiss_fftri2(kiss_fftr_cfg st,const kiss_fft_scalar *freqdata, kiss_fft_scalar *timedata);
/*
input freqdata has nfft/2+1 complex points
output timedata has nfft scalar points
*/
#define kiss_fftr_free speex_free
#ifdef __cplusplus
}
#endif
#endif

332
lib/speexdsp/math_approx.h

@ -1,332 +0,0 @@
/* Copyright (C) 2002 Jean-Marc Valin */
/**
@file math_approx.h
@brief Various math approximation functions for Speex
*/
/*
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 the Xiph.org Foundation nor the names of its
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 FOUNDATION 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.
*/
#ifndef MATH_APPROX_H
#define MATH_APPROX_H
#include "arch.h"
#ifndef FIXED_POINT
#define spx_sqrt sqrt
#define spx_acos acos
#define spx_exp exp
#define spx_cos_norm(x) (cos((.5f*M_PI)*(x)))
#define spx_atan atan
/** Generate a pseudo-random number */
static inline spx_word16_t speex_rand(spx_word16_t std, spx_int32_t *seed)
{
const unsigned int jflone = 0x3f800000;
const unsigned int jflmsk = 0x007fffff;
union {int i; float f;} ran;
*seed = 1664525 * *seed + 1013904223;
ran.i = jflone | (jflmsk & *seed);
ran.f -= 1.5;
return 3.4642*std*ran.f;
}
#endif
static inline spx_int16_t spx_ilog2(spx_uint32_t x)
{
int r=0;
if (x>=(spx_int32_t)65536)
{
x >>= 16;
r += 16;
}
if (x>=256)
{
x >>= 8;
r += 8;
}
if (x>=16)
{
x >>= 4;
r += 4;
}
if (x>=4)
{
x >>= 2;
r += 2;
}
if (x>=2)
{
r += 1;
}
return r;
}
static inline spx_int16_t spx_ilog4(spx_uint32_t x)
{
int r=0;
if (x>=(spx_int32_t)65536)
{
x >>= 16;
r += 8;
}
if (x>=256)
{
x >>= 8;
r += 4;
}
if (x>=16)
{
x >>= 4;
r += 2;
}
if (x>=4)
{
r += 1;
}
return r;
}
#ifdef FIXED_POINT
/** Generate a pseudo-random number */
static inline spx_word16_t speex_rand(spx_word16_t std, spx_int32_t *seed)
{
spx_word32_t res;
*seed = 1664525 * *seed + 1013904223;
res = MULT16_16(EXTRACT16(SHR32(*seed,16)),std);
return EXTRACT16(PSHR32(SUB32(res, SHR32(res, 3)),14));
}
/* sqrt(x) ~= 0.22178 + 1.29227*x - 0.77070*x^2 + 0.25723*x^3 (for .25 < x < 1) */
/*#define C0 3634
#define C1 21173
#define C2 -12627
#define C3 4215*/
/* sqrt(x) ~= 0.22178 + 1.29227*x - 0.77070*x^2 + 0.25659*x^3 (for .25 < x < 1) */
#define C0 3634
#define C1 21173
#define C2 -12627
#define C3 4204
static inline spx_word16_t spx_sqrt(spx_word32_t x)
{
int k;
spx_word32_t rt;
k = spx_ilog4(x)-6;
x = VSHR32(x, (k<<1));
rt = ADD16(C0, MULT16_16_Q14(x, ADD16(C1, MULT16_16_Q14(x, ADD16(C2, MULT16_16_Q14(x, (C3)))))));
rt = VSHR32(rt,7-k);
return rt;
}
/* log(x) ~= -2.18151 + 4.20592*x - 2.88938*x^2 + 0.86535*x^3 (for .5 < x < 1) */
#define A1 16469
#define A2 2242
#define A3 1486
static inline spx_word16_t spx_acos(spx_word16_t x)
{
int s=0;
spx_word16_t ret;
spx_word16_t sq;
if (x<0)
{
s=1;
x = NEG16(x);
}
x = SUB16(16384,x);
x = x >> 1;
sq = MULT16_16_Q13(x, ADD16(A1, MULT16_16_Q13(x, ADD16(A2, MULT16_16_Q13(x, (A3))))));
ret = spx_sqrt(SHL32(EXTEND32(sq),13));
/*ret = spx_sqrt(67108864*(-1.6129e-04 + 2.0104e+00*f + 2.7373e-01*f*f + 1.8136e-01*f*f*f));*/
if (s)
ret = SUB16(25736,ret);
return ret;
}
#define K1 8192
#define K2 -4096
#define K3 340
#define K4 -10
static inline spx_word16_t spx_cos(spx_word16_t x)
{
spx_word16_t x2;
if (x<12868)
{
x2 = MULT16_16_P13(x,x);
return ADD32(K1, MULT16_16_P13(x2, ADD32(K2, MULT16_16_P13(x2, ADD32(K3, MULT16_16_P13(K4, x2))))));
} else {
x = SUB16(25736,x);
x2 = MULT16_16_P13(x,x);
return SUB32(-K1, MULT16_16_P13(x2, ADD32(K2, MULT16_16_P13(x2, ADD32(K3, MULT16_16_P13(K4, x2))))));
}
}
#define L1 32767
#define L2 -7651
#define L3 8277
#define L4 -626
static inline spx_word16_t _spx_cos_pi_2(spx_word16_t x)
{
spx_word16_t x2;
x2 = MULT16_16_P15(x,x);
return ADD16(1,MIN16(32766,ADD32(SUB16(L1,x2), MULT16_16_P15(x2, ADD32(L2, MULT16_16_P15(x2, ADD32(L3, MULT16_16_P15(L4, x2))))))));
}
static inline spx_word16_t spx_cos_norm(spx_word32_t x)
{
x = x&0x0001ffff;
if (x>SHL32(EXTEND32(1), 16))
x = SUB32(SHL32(EXTEND32(1), 17),x);
if (x&0x00007fff)
{
if (x<SHL32(EXTEND32(1), 15))
{
return _spx_cos_pi_2(EXTRACT16(x));
} else {
return NEG32(_spx_cos_pi_2(EXTRACT16(65536-x)));
}
} else {
if (x&0x0000ffff)
return 0;
else if (x&0x0001ffff)
return -32767;
else
return 32767;
}
}
/*
K0 = 1
K1 = log(2)
K2 = 3-4*log(2)
K3 = 3*log(2) - 2
*/
#define D0 16384
#define D1 11356
#define D2 3726
#define D3 1301
/* Input in Q11 format, output in Q16 */
static inline spx_word32_t spx_exp2(spx_word16_t x)
{
int integer;
spx_word16_t frac;
integer = SHR16(x,11);
if (integer>14)
return 0x7fffffff;
else if (integer < -15)
return 0;
frac = SHL16(x-SHL16(integer,11),3);
frac = ADD16(D0, MULT16_16_Q14(frac, ADD16(D1, MULT16_16_Q14(frac, ADD16(D2 , MULT16_16_Q14(D3,frac))))));
return VSHR32(EXTEND32(frac), -integer-2);
}
/* Input in Q11 format, output in Q16 */
static inline spx_word32_t spx_exp(spx_word16_t x)
{
if (x>21290)
return 0x7fffffff;
else if (x<-21290)
return 0;
else
return spx_exp2(MULT16_16_P14(23637,x));
}
#define M1 32767
#define M2 -21
#define M3 -11943
#define M4 4936
static inline spx_word16_t spx_atan01(spx_word16_t x)
{
return MULT16_16_P15(x, ADD32(M1, MULT16_16_P15(x, ADD32(M2, MULT16_16_P15(x, ADD32(M3, MULT16_16_P15(M4, x)))))));
}
#undef M1
#undef M2
#undef M3
#undef M4
/* Input in Q15, output in Q14 */
static inline spx_word16_t spx_atan(spx_word32_t x)
{
if (x <= 32767)
{
return SHR16(spx_atan01(x),1);
} else {
int e = spx_ilog2(x);
if (e>=29)
return 25736;
x = DIV32_16(SHL32(EXTEND32(32767),29-e), EXTRACT16(SHR32(x, e-14)));
return SUB16(25736, SHR16(spx_atan01(x),1));
}
}
#else
#ifndef M_PI
#define M_PI 3.14159265358979323846 /* pi */
#endif
#define C1 0.9999932946f
#define C2 -0.4999124376f
#define C3 0.0414877472f
#define C4 -0.0012712095f
#define SPX_PI_2 1.5707963268
static inline spx_word16_t spx_cos(spx_word16_t x)
{
if (x<SPX_PI_2)
{
x *= x;
return C1 + x*(C2+x*(C3+C4*x));
} else {
x = M_PI-x;
x *= x;
return NEG16(C1 + x*(C2+x*(C3+C4*x)));
}
}
#endif
#endif

1279
lib/speexdsp/mdf.c

File diff suppressed because it is too large Load Diff

169
lib/speexdsp/os_support.h

@ -1,169 +0,0 @@
/* Copyright (C) 2007 Jean-Marc Valin
File: os_support.h
This is the (tiny) OS abstraction layer. Aside from math.h, this is the
only place where system headers are allowed.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. 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.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
*/
#ifndef OS_SUPPORT_H
#define OS_SUPPORT_H
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#ifdef OS_SUPPORT_CUSTOM
#include "os_support_custom.h"
#endif
/** Speex wrapper for calloc. To do your own dynamic allocation, all you need to do is replace this function, speex_realloc and speex_free
NOTE: speex_alloc needs to CLEAR THE MEMORY */
#ifndef OVERRIDE_SPEEX_ALLOC
static inline void *speex_alloc (int size)
{
/* WARNING: this is not equivalent to malloc(). If you want to use malloc()
or your own allocator, YOU NEED TO CLEAR THE MEMORY ALLOCATED. Otherwise
you will experience strange bugs */
return calloc(size,1);
}
#endif
/** Same as speex_alloc, except that the area is only needed inside a Speex call (might cause problem with wideband though) */
#ifndef OVERRIDE_SPEEX_ALLOC_SCRATCH
static inline void *speex_alloc_scratch (int size)
{
/* Scratch space doesn't need to be cleared */
return calloc(size,1);
}
#endif
/** Speex wrapper for realloc. To do your own dynamic allocation, all you need to do is replace this function, speex_alloc and speex_free */
#ifndef OVERRIDE_SPEEX_REALLOC
static inline void *speex_realloc (void *ptr, int size)
{
return realloc(ptr, size);
}
#endif
/** Speex wrapper for calloc. To do your own dynamic allocation, all you need to do is replace this function, speex_realloc and speex_alloc */
#ifndef OVERRIDE_SPEEX_FREE
static inline void speex_free (void *ptr)
{
free(ptr);
}
#endif
/** Same as speex_free, except that the area is only needed inside a Speex call (might cause problem with wideband though) */
#ifndef OVERRIDE_SPEEX_FREE_SCRATCH
static inline void speex_free_scratch (void *ptr)
{
free(ptr);
}
#endif
/** Copy n elements from src to dst. The 0* term provides compile-time type checking */
#ifndef OVERRIDE_SPEEX_COPY
#define SPEEX_COPY(dst, src, n) (memcpy((dst), (src), (n)*sizeof(*(dst)) + 0*((dst)-(src)) ))
#endif
/** Copy n elements from src to dst, allowing overlapping regions. The 0* term
provides compile-time type checking */
#ifndef OVERRIDE_SPEEX_MOVE
#define SPEEX_MOVE(dst, src, n) (memmove((dst), (src), (n)*sizeof(*(dst)) + 0*((dst)-(src)) ))
#endif
/** For n elements worth of memory, set every byte to the value of c, starting at address dst */
#ifndef OVERRIDE_SPEEX_MEMSET
#define SPEEX_MEMSET(dst, c, n) (memset((dst), (c), (n)*sizeof(*(dst))))
#endif
#ifndef OVERRIDE_SPEEX_FATAL
static inline void _speex_fatal(const char *str, const char *file, int line)
{
fprintf (stderr, "Fatal (internal) error in %s, line %d: %s\n", file, line, str);
exit(1);
}
#endif
#ifndef OVERRIDE_SPEEX_WARNING
static inline void speex_warning(const char *str)
{
#ifndef DISABLE_WARNINGS
fprintf (stderr, "warning: %s\n", str);
#endif
}
#endif
#ifndef OVERRIDE_SPEEX_WARNING_INT
static inline void speex_warning_int(const char *str, int val)
{
#ifndef DISABLE_WARNINGS
fprintf (stderr, "warning: %s %d\n", str, val);
#endif
}
#endif
#ifndef OVERRIDE_SPEEX_NOTIFY
static inline void speex_notify(const char *str)
{
#ifndef DISABLE_NOTIFICATIONS
fprintf (stderr, "notification: %s\n", str);
#endif
}
#endif
#ifndef OVERRIDE_SPEEX_PUTC
/** Speex wrapper for putc */
static inline void _speex_putc(int ch, void *file)
{
FILE *f = (FILE *)file;
fprintf(f, "%c", ch);
}
#endif
#define speex_fatal(str) _speex_fatal(str, __FILE__, __LINE__);
#define speex_assert(cond) {if (!(cond)) {speex_fatal("assertion failed: " #cond);}}
#ifndef RELEASE
static inline void print_vec(float *vec, int len, char *name)
{
int i;
printf ("%s ", name);
for (i=0;i<len;i++)
printf (" %f", vec[i]);
printf ("\n");
}
#endif
#endif

379
lib/speexdsp/pseudofloat.h

@ -1,379 +0,0 @@
/* Copyright (C) 2005 Jean-Marc Valin */
/**
@file pseudofloat.h
@brief Pseudo-floating point
* This header file provides a lightweight floating point type for
* use on fixed-point platforms when a large dynamic range is
* required. The new type is not compatible with the 32-bit IEEE format,
* it is not even remotely as accurate as 32-bit floats, and is not
* even guaranteed to produce even remotely correct results for code
* other than Speex. It makes all kinds of shortcuts that are acceptable
* for Speex, but may not be acceptable for your application. You're
* quite welcome to reuse this code and improve it, but don't assume
* it works out of the box. Most likely, it doesn't.
*/
/*
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 the Xiph.org Foundation nor the names of its
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 FOUNDATION 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.
*/
#ifndef PSEUDOFLOAT_H
#define PSEUDOFLOAT_H
#include "arch.h"
#include "os_support.h"
#include "math_approx.h"
#include <math.h>
#ifdef FIXED_POINT
typedef struct {
spx_int16_t m;
spx_int16_t e;
} spx_float_t;
static const spx_float_t FLOAT_ZERO = {0,0};
static const spx_float_t FLOAT_ONE = {16384,-14};
static const spx_float_t FLOAT_HALF = {16384,-15};
#define MIN(a,b) ((a)<(b)?(a):(b))
static inline spx_float_t PSEUDOFLOAT(spx_int32_t x)
{
int e=0;
int sign=0;
if (x<0)
{
sign = 1;
x = -x;
}
if (x==0)
{
spx_float_t r = {0,0};
return r;
}
e = spx_ilog2(ABS32(x))-14;
x = VSHR32(x, e);
if (sign)
{
spx_float_t r;
r.m = -x;
r.e = e;
return r;
}
else
{
spx_float_t r;
r.m = x;
r.e = e;
return r;
}
}
static inline spx_float_t FLOAT_ADD(spx_float_t a, spx_float_t b)
{
spx_float_t r;
if (a.m==0)
return b;
else if (b.m==0)
return a;
if ((a).e > (b).e)
{
r.m = ((a).m>>1) + ((b).m>>MIN(15,(a).e-(b).e+1));
r.e = (a).e+1;
}
else
{
r.m = ((b).m>>1) + ((a).m>>MIN(15,(b).e-(a).e+1));
r.e = (b).e+1;
}
if (r.m>0)
{
if (r.m<16384)
{
r.m<<=1;
r.e-=1;
}
} else {
if (r.m>-16384)
{
r.m<<=1;
r.e-=1;
}
}
/*printf ("%f + %f = %f\n", REALFLOAT(a), REALFLOAT(b), REALFLOAT(r));*/
return r;
}
static inline spx_float_t FLOAT_SUB(spx_float_t a, spx_float_t b)
{
spx_float_t r;
if (a.m==0)
return b;
else if (b.m==0)
return a;
if ((a).e > (b).e)
{
r.m = ((a).m>>1) - ((b).m>>MIN(15,(a).e-(b).e+1));
r.e = (a).e+1;
}
else
{
r.m = ((a).m>>MIN(15,(b).e-(a).e+1)) - ((b).m>>1);
r.e = (b).e+1;
}
if (r.m>0)
{
if (r.m<16384)
{
r.m<<=1;
r.e-=1;
}
} else {
if (r.m>-16384)
{
r.m<<=1;
r.e-=1;
}
}
/*printf ("%f + %f = %f\n", REALFLOAT(a), REALFLOAT(b), REALFLOAT(r));*/
return r;
}
static inline int FLOAT_LT(spx_float_t a, spx_float_t b)
{
if (a.m==0)
return b.m>0;
else if (b.m==0)
return a.m<0;
if ((a).e > (b).e)
return ((a).m>>1) < ((b).m>>MIN(15,(a).e-(b).e+1));
else
return ((b).m>>1) > ((a).m>>MIN(15,(b).e-(a).e+1));
}
static inline int FLOAT_GT(spx_float_t a, spx_float_t b)
{
return FLOAT_LT(b,a);
}
static inline spx_float_t FLOAT_MULT(spx_float_t a, spx_float_t b)
{
spx_float_t r;
r.m = (spx_int16_t)((spx_int32_t)(a).m*(b).m>>15);
r.e = (a).e+(b).e+15;
if (r.m>0)
{
if (r.m<16384)
{
r.m<<=1;
r.e-=1;
}
} else {
if (r.m>-16384)
{
r.m<<=1;
r.e-=1;
}
}
/*printf ("%f * %f = %f\n", REALFLOAT(a), REALFLOAT(b), REALFLOAT(r));*/
return r;
}
static inline spx_float_t FLOAT_AMULT(spx_float_t a, spx_float_t b)
{
spx_float_t r;
r.m = (spx_int16_t)((spx_int32_t)(a).m*(b).m>>15);
r.e = (a).e+(b).e+15;
return r;
}
static inline spx_float_t FLOAT_SHL(spx_float_t a, int b)
{
spx_float_t r;
r.m = a.m;
r.e = a.e+b;
return r;
}
static inline spx_int16_t FLOAT_EXTRACT16(spx_float_t a)
{
if (a.e<0)
return EXTRACT16((EXTEND32(a.m)+(EXTEND32(1)<<(-a.e-1)))>>-a.e);
else
return a.m<<a.e;
}
static inline spx_int32_t FLOAT_EXTRACT32(spx_float_t a)
{
if (a.e<0)
return (EXTEND32(a.m)+(EXTEND32(1)<<(-a.e-1)))>>-a.e;
else
return EXTEND32(a.m)<<a.e;
}
static inline spx_int32_t FLOAT_MUL32(spx_float_t a, spx_word32_t b)
{
return VSHR32(MULT16_32_Q15(a.m, b),-a.e-15);
}
static inline spx_float_t FLOAT_MUL32U(spx_word32_t a, spx_word32_t b)
{
int e1, e2;
spx_float_t r;
if (a==0 || b==0)
{
return FLOAT_ZERO;
}
e1 = spx_ilog2(ABS32(a));
a = VSHR32(a, e1-14);
e2 = spx_ilog2(ABS32(b));
b = VSHR32(b, e2-14);
r.m = MULT16_16_Q15(a,b);
r.e = e1+e2-13;
return r;
}
/* Do NOT attempt to divide by a negative number */
static inline spx_float_t FLOAT_DIV32_FLOAT(spx_word32_t a, spx_float_t b)
{
int e=0;
spx_float_t r;
if (a==0)
{
return FLOAT_ZERO;
}
e = spx_ilog2(ABS32(a))-spx_ilog2(b.m-1)-15;
a = VSHR32(a, e);
if (ABS32(a)>=SHL32(EXTEND32(b.m-1),15))
{
a >>= 1;
e++;
}
r.m = DIV32_16(a,b.m);
r.e = e-b.e;
return r;
}
/* Do NOT attempt to divide by a negative number */
static inline spx_float_t FLOAT_DIV32(spx_word32_t a, spx_word32_t b)
{
int e0=0,e=0;
spx_float_t r;
if (a==0)
{
return FLOAT_ZERO;
}
if (b>32767)
{
e0 = spx_ilog2(b)-14;
b = VSHR32(b, e0);
e0 = -e0;
}
e = spx_ilog2(ABS32(a))-spx_ilog2(b-1)-15;
a = VSHR32(a, e);
if (ABS32(a)>=SHL32(EXTEND32(b-1),15))
{
a >>= 1;
e++;
}
e += e0;
r.m = DIV32_16(a,b);
r.e = e;
return r;
}
/* Do NOT attempt to divide by a negative number */
static inline spx_float_t FLOAT_DIVU(spx_float_t a, spx_float_t b)
{
int e=0;
spx_int32_t num;
spx_float_t r;
if (b.m<=0)
{
speex_warning_int("Attempted to divide by", b.m);
return FLOAT_ONE;
}
num = a.m;
a.m = ABS16(a.m);
while (a.m >= b.m)
{
e++;
a.m >>= 1;
}
num = num << (15-e);
r.m = DIV32_16(num,b.m);
r.e = a.e-b.e-15+e;
return r;
}
static inline spx_float_t FLOAT_SQRT(spx_float_t a)
{
spx_float_t r;
spx_int32_t m;
m = SHL32(EXTEND32(a.m), 14);
r.e = a.e - 14;
if (r.e & 1)
{
r.e -= 1;
m <<= 1;
}
r.e >>= 1;
r.m = spx_sqrt(m);
return r;
}
#else
#define spx_float_t float
#define FLOAT_ZERO 0.f
#define FLOAT_ONE 1.f
#define FLOAT_HALF 0.5f
#define PSEUDOFLOAT(x) (x)
#define FLOAT_MULT(a,b) ((a)*(b))
#define FLOAT_AMULT(a,b) ((a)*(b))
#define FLOAT_MUL32(a,b) ((a)*(b))
#define FLOAT_DIV32(a,b) ((a)/(b))
#define FLOAT_EXTRACT16(a) (a)
#define FLOAT_EXTRACT32(a) (a)
#define FLOAT_ADD(a,b) ((a)+(b))
#define FLOAT_SUB(a,b) ((a)-(b))
#define REALFLOAT(x) (x)
#define FLOAT_DIV32_FLOAT(a,b) ((a)/(b))
#define FLOAT_MUL32U(a,b) ((a)*(b))
#define FLOAT_SHL(a,b) (a)
#define FLOAT_LT(a,b) ((a)<(b))
#define FLOAT_GT(a,b) ((a)>(b))
#define FLOAT_DIVU(a,b) ((a)/(b))
#define FLOAT_SQRT(a) (spx_sqrt(a))
#endif
#endif

32
lib/tcp_io.c

@ -79,7 +79,7 @@ struct tcp_conn* tcp_conn_create(
queue_set_callback(tc->write_queue, write_queue_fetch_cb, tc);
queue_set_waiter_defer(tc->write_queue, 1);
tc->socket_id = uasync_add_socket_t(ua, sock, read_cb, write_cb, error_cb, "tcp_io", tc);
tc->socket_id = uasync_add_socket_t(ua, sock, read_cb, write_cb, error_cb, tc);
if (!tc->socket_id) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_conn_create: uasync_add_socket_t failed");
queue_free(tc->read_queue);
@ -282,16 +282,6 @@ static int flush_write_buf(struct tcp_conn* tc) {
return 0;
}
// Последний send может опустошить очередь при уже выключенном POLLOUT.
// Уведомляем потребителя здесь, не рассчитывая на ещё один write event.
static void notify_flushed(struct tcp_conn* tc) {
if (tc->destroyed || tc->write_buf || tc->write_queue->head || !tc->on_flushed) return;
void (*cb)(struct tcp_conn*, void*) = tc->on_flushed;
tc->on_flushed = NULL;
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: flushed fd=%d", (int)tc->sock);
cb(tc, tc->arg);
}
static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
struct tcp_conn* tc = (struct tcp_conn*)arg;
@ -310,7 +300,11 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
tc->write_monitor = 0;
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: wq_fetch POLLOUT OFF fd=%d (queue empty)", (int)tc->sock);
queue_resume_callback(q);
notify_flushed(tc);
if (tc->on_flushed) {
void (*cb)(struct tcp_conn*, void*) = tc->on_flushed;
tc->on_flushed = NULL;
cb(tc, tc->arg);
}
return;
}
@ -318,7 +312,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
if (!e->dgram) {
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: CLOSE fd=%d", (int)tc->sock);
queue_entry_free(e); queue_resume_callback(q);
if (tc->socket_id) uasync_remove_socket_t(tc->ua, tc->sock);
uasync_remove_socket_t(tc->ua, tc->sock);
socket_close_wrapper(tc->sock);
tc->socket_id = NULL; tc->sock = SOCKET_INVALID; tc->closed = 1;
if (tc->on_closed) tc->on_closed(tc, tc->arg);
@ -340,7 +334,6 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
memory_pool_free(tc->data_pool, e->dgram);
queue_entry_free(e);
queue_resume_callback(q);
notify_flushed(tc);
} else if (n > 0) {
size_t rem = e->len - (size_t)n;
tc->write_buf = memory_pool_alloc(tc->data_pool);
@ -396,8 +389,6 @@ static void write_cb(socket_t sock, void* arg) {
tc->connected = 1;
if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, tc->connect_timer); tc->connect_timer = NULL; }
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: connect ok fd=%d", (int)tc->sock);
if (tc->on_connected) tc->on_connected(tc, tc->arg);
if (tc->destroyed) return;
} else {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: connect fail fd=%d err=%d", (int)tc->sock, err);
tcp_conn_handle_error(tc, err ? err : -1);
@ -413,7 +404,6 @@ static void write_cb(socket_t sock, void* arg) {
tc->write_monitor = 0;
}
queue_resume_callback(tc->write_queue);
notify_flushed(tc);
}
}
@ -430,14 +420,6 @@ static void error_cb(socket_t sock, void* arg) {
// Peer полностью закрыл свой сокет (EPOLLHUP) после того, как FIN уже прочитан (fin_remote=1).
// EPOLLHUP — level-triggered и «always reported»: если оставить сокет в epoll, каждая итерация
// epoll_wait мгновенно вернёт HUP и error_cb зациклится на 100% CPU. Закрываем сокет.
if (tc->fin_remote && tc->fin_local) {
// Обе TCP-половины закрыты, но владелец ещё может передавать ранее прочитанные данные.
// Убираем level-triggered HUP из poll; CLOSE остаётся решением владельца.
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: both FINs fd=%d — detach poll, await owner drain", (int)tc->sock);
if (tc->socket_id) uasync_remove_socket_t(tc->ua, tc->sock);
tc->socket_id = NULL; tc->write_monitor = 0;
return;
}
if (tc->fin_remote) {
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: async HUP (graceful) fd=%d — peer fully closed after FIN, closing",
(int)tc->sock);

1
lib/tcp_io.h

@ -70,7 +70,6 @@ struct tcp_conn {
struct queue_waiter_handle read_waiter;
// Коллбэки
void (*on_connected)(struct tcp_conn* tc, void* arg); // исходящий connect завершён
void (*on_fin)(struct tcp_conn* tc, void* arg); // FIN получен от удалённой стороны (fin_remote=1)
void (*on_fin_sent)(struct tcp_conn* tc, void* arg); // FIN отправлен удалённой стороне (fin_local=1)
void (*on_error)(struct tcp_conn* tc, int err, void* arg); // фатальная ошибка (вызывается после close сокета; обязан вызвать tcp_conn_destroy)

824
lib/u_async.c

File diff suppressed because it is too large Load Diff

16
lib/u_async.h

@ -90,7 +90,6 @@ static inline int uasync_is_running(struct UASYNC* ua) {
// Instance API - основной API для работы с uasync
struct UASYNC* uasync_create(void);
// Уничтожение только вне callbacks, после возврата из poll/mainloop.
void uasync_destroy(struct UASYNC* ua, int close_fds);
void uasync_mark_running(struct UASYNC* ua);
void uasync_mark_stopped(struct UASYNC* ua);
@ -114,9 +113,9 @@ void* uasync_call_soon(struct UASYNC* ua, void* user_arg, timeout_callback_t cal
err_t uasync_call_soon_cancel(struct UASYNC* ua, void* t_id);
// Sockets - for regular file descriptors (pipe, file)
void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, const char* name, void* user_arg);
void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, void* user_arg);
// Sockets - for socket_t (cross-platform sockets)
void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t read_cbk, socket_t_callback_t write_cbk, socket_t_callback_t except_cbk, const char* name, void* user_arg);
void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t read_cbk, socket_t_callback_t write_cbk, socket_t_callback_t except_cbk, void* user_arg);
err_t uasync_remove_socket(struct UASYNC* ua, void* s_id);
err_t uasync_remove_socket_t(struct UASYNC* ua, socket_t sock);
@ -128,14 +127,14 @@ err_t uasync_set_socket_write(struct UASYNC* ua, void* s_id, int enable);
void uasync_poll(struct UASYNC* ua, int timeout_tb);
void uasync_drain_immediate(struct UASYNC* ua);
// Цикл до uasync_stop (возвращается после остановки).
// Mainloop (бесконечный цикл, __noreturn)
void uasync_mainloop(struct UASYNC* ua);
void uasync_stop(struct UASYNC* ua);
// Debug statistics
void uasync_get_stats(struct UASYNC* ua, size_t* timer_alloc, size_t* timer_free, size_t* socket_alloc, size_t* socket_free);
// Handle — кодированный индекс; сохраняется при расширении массива сокетов.
// Lookup socket by file descriptor - returns current pointer even after realloc
int uasync_lookup_socket(struct UASYNC* ua, int fd, void** socket_id);
// Print all resources (timers, sockets) for debugging
@ -148,13 +147,6 @@ int uasync_get_wakeup_fd(struct UASYNC* ua); // returns write fd for wakeup pipe
// сообщить async (из другого thread) чтобы он вызвал callback с аргументом
void uasync_post(struct UASYNC* ua, uasync_post_callback_t callback, void* user_arg);
/* Передать заранее выделенный u_malloc task без аллокаций. uasync освобождает
* task после callback. Производитель больше не обращается к task после передачи.
* Отмена — в потоке владельца вне callbacks, после завершения производителя;
* ERR_OK означает, что task удалён из очереди и освобождён. */
void uasync_post_reserved(struct UASYNC* ua, struct posted_task* task);
err_t uasync_cancel_post(struct UASYNC* ua, struct posted_task* task);
// синхронизация памяти (для доступности из других потоков)
void uasync_memsync(struct UASYNC* ua);

8
lib/u_async_doc.md

@ -17,7 +17,7 @@ u_async — центральный планировщик проекта uTun.
- **Один UASYNC на поток.** Нельзя создать несколько экземпляров и вызывать их из разных потоков.
- **Никаких sleep/usleep.** Если поток заблокирован на sleep, цикл событий стоит. Все ожидания — только через таймеры `uasync_set_timeout`.
- **Закрытие из callback.** Можно удалять сокет и освобождать его пользовательский контекст из callback. Перед следующим callback модуль повторно проверяет регистрацию, поколение и разрешённые события. Для завершения цикла вызывайте `uasync_stop`; `uasync_destroy` выполняйте после возврата из `uasync_poll` / `uasync_mainloop`, вне callbacks.
- **Закрытие из callback.** Можно вызывать `uasync_destroy` или `uasync_remove_socket` прямо из callback сокета/таймера. Модуль использует локальные копии указателей перед вызовом callback и генерационные счётчики (gen) для защиты от stale epoll-событий.
- **Указатель `user_arg`** передаётся во все callback и позволяет передать контекстную структуру (например, `UTUN_INSTANCE`).
### Типовой сценарий
@ -141,7 +141,7 @@ uint64_t now_us = get_time_us(); // микросекунды (для burst-и
| `uasync_remove_socket_t(ua, sock)` | Удалить по значению `socket_t`. |
| `uasync_set_socket_read(ua, s_id, enable)` | Динамически включить/отключить мониторинг чтения (EPOLL_CTL_MOD). |
| `uasync_set_socket_write(ua, s_id, enable)` | Динамически включить/отключить мониторинг записи. |
| `uasync_lookup_socket(ua, fd, &s_id)` | Найти handle сокета по fd (кодированный индекс, устойчивый к расширению массива). |
| `uasync_lookup_socket(ua, fd, &s_id)` | Найти дескриптор сокета по fd (возвращает актуальный указатель даже после realloc). |
### Межпоточное взаимодействие
@ -172,7 +172,3 @@ uint64_t now_us = get_time_us(); // микросекунды (для burst-и
- **`struct socket_node`** — узел сокета: fd/sock, тип (FD/SOCK), колбэки, user_data, флаги active/enable_read/enable_write, gen (защита от stale epoll-событий после переиспользования fd).
- **`struct socket_array`** — массив сокетов: O(1) доступ по fd через `fd_to_index[]`, обход активных через `active_indices[]`, динамическое расширение.
- **`struct posted_task`** — задача из другого потока: callback + arg + next, защищена `posted_lock`.
Таймеры используют монотонное время: коррекция календарных часов не меняет срок срабатывания. Положительные интервалы округляются вверх до миллисекунд; нулевой интервал готов немедленно. Каждый проход обрабатывает исходную порцию `call_soon`, затем проверяет таймеры и сокеты; новые callbacks остаются на следующий проход. `uasync_drain_immediate` явно обрабатывает все порции до опустошения очереди.
Для fallback `poll` массив дескрипторов резервируется при создании экземпляра и до регистрации нового сокета. Отказ памяти возвращает `NULL` из `uasync_create` / `uasync_add_socket*`; существующие регистрации сохраняются. Перестроение массива в `uasync_poll` не выделяет память. Расширение из callback сохраняет события текущего прохода, новый сокет опрашивается со следующего прохода.

2
net_emulator/net_emulator.c

@ -425,7 +425,7 @@ int net_emulator_init(net_emulator_t *emulator) {
ep->emulator = emulator;
// Add socket to u_async
uasync_add_socket(emulator->ua, ep->sockfd, socket_callback, NULL, NULL, "emulator", ep);
uasync_add_socket(emulator->ua, ep->sockfd, socket_callback, NULL, NULL, ep);
printf("[INIT] Listening on %s:%u\n",
inet_ntoa(ep->listen_addr.sin_addr), ntohs(ep->listen_addr.sin_port));
}

51
src/Makefile.am

@ -30,8 +30,6 @@ utun_CORE_SOURCES = \
tun_freebsd.c \
tun_windows.c \
transport_layer/etcp.c \
transport_layer/etcp_session.c \
transport_layer/etcp_session.h \
transport_layer/etcp_connections.c \
transport_layer/etcp_keepalive.c \
transport_layer/etcp_bbr.c \
@ -62,20 +60,11 @@ utun_CORE_SOURCES = \
media_delivery/media_delivery.c \
media_delivery/media_download.c \
media_delivery/media_index.c \
media_delivery/file_transfer.c \
media_delivery/file_transfer.h \
media_async/media_async.c \
media_async/attachment.c \
media_async/attachment.h \
media_async/voice_file.c \
media_async/voice_file.h \
media_async/attachment_send.c \
media_async/attachment_send.h \
video/video.c \
transport_layer/dummynet.c \
ntp_time.c \
ntp_node_time.c \
proxy/proxy_protocol.h \
proxy/tcp_proxy_client.c \
routing_layer/etcp_router.c \
routing_layer/route_crypto.c \
@ -100,23 +89,12 @@ utun_CORE_SOURCES = \
chat/chat_join.c \
chat/member_sync.c \
chat/merkle_sync.c \
chat/merkle_tree.c \
chat/merkle_tree.h \
chat/invite_link.c \
chat/invite_build.c \
chat/chat_headless_control.c \
dm/dm_core.c \
dm/dm_crypto.c \
dm/dm_mailbox.c \
dm/dm_media.c \
dm/dm_media.h \
dm/dm_media_priv.h \
dm/dm_mailbox_media.c \
dm/dm_mailbox_media.h \
call/call.c \
call/call_headless.c \
radio/radio.c \
radio/radio_headless.c \
broadcast.c
# libutun: all core sources except main()
@ -147,8 +125,6 @@ libutun_a_SOURCES = \
tun_freebsd.c \
tun_windows.c \
transport_layer/etcp.c \
transport_layer/etcp_session.c \
transport_layer/etcp_session.h \
transport_layer/etcp_connections.c \
transport_layer/etcp_keepalive.c \
transport_layer/etcp_bbr.c \
@ -172,7 +148,6 @@ libutun_a_SOURCES = \
transport_layer/node_conn_direct.c \
transport_layer/socket_monitor.c \
transport_layer/auto_socket.c \
transport_layer/call_ring.c \
control_server.c \
firewall.c \
eim_nat.c \
@ -180,20 +155,11 @@ libutun_a_SOURCES = \
media_delivery/media_delivery.c \
media_delivery/media_download.c \
media_delivery/media_index.c \
media_delivery/file_transfer.c \
media_delivery/file_transfer.h \
media_async/media_async.c \
media_async/attachment.c \
media_async/attachment.h \
media_async/voice_file.c \
media_async/voice_file.h \
media_async/attachment_send.c \
media_async/attachment_send.h \
video/video.c \
transport_layer/dummynet.c \
ntp_time.c \
ntp_node_time.c \
proxy/proxy_protocol.h \
proxy/tcp_proxy_client.c \
routing_layer/etcp_router.c \
routing_layer/route_crypto.c \
@ -218,22 +184,11 @@ libutun_a_SOURCES = \
chat/member_sync.c \
chat/merkle_sync.c \
chat/invite_link.c \
chat/merkle_tree.c \
chat/merkle_tree.h \
chat/invite_build.c \
chat/chat_headless_control.c \
dm/dm_core.c \
dm/dm_crypto.c \
dm/dm_mailbox.c \
dm/dm_media.c \
dm/dm_media.h \
dm/dm_media_priv.h \
dm/dm_mailbox_media.c \
dm/dm_mailbox_media.h \
call/call.c \
call/call_headless.c \
radio/radio.c \
radio/radio_headless.c \
broadcast.c
libutun_a_CFLAGS = $(utun_CFLAGS)
@ -251,7 +206,6 @@ utun_CFLAGS = \
-I$(top_srcdir)/src/media_delivery \
-I$(top_srcdir)/src/media_async \
-I$(top_srcdir)/src/video \
-I$(top_srcdir)/src/call \
-I$(top_srcdir)/src/uip \
-g \
-DUSE_SQLITE \
@ -278,11 +232,6 @@ utun_CFLAGS += -DHAVE_FFMPEG @FFMPEG_CFLAGS@
utun_LDADD += @FFMPEG_LIBS@
endif
if HAVE_SILERO_VAD
utun_CFLAGS += -DHAVE_SILERO_VAD
utun_LDADD += @SILERO_VAD_LIBS@
endif
# Copy binary to project root after building
all-local: copy-to-root

1030
src/call/call.c

File diff suppressed because it is too large Load Diff

100
src/call/call.h

@ -1,100 +0,0 @@
// call.h — P2P аудио-звонок (сигналинг + медиа), per-instance
//
// Модуль оперирует (group_id, peer_node_id): звонок можно инициировать из
// CHAT-группы или из DM (group_id — группа маршрутизации, как в dm_core).
// Сигналинг идёт через etcp_router (ROUTE_CRYPTO_SIGN|ENCRYPT), соединение
// оптимизируется через параллельные пробы conn_mgr (DIRECT/REVERSE/INDIRECT).
//
// Все функции — в uasync-потоке. GUI/headless вызывают через trampoline
// (gui_bridge_post_uasync_fn / uasync_post).
#ifndef CALL_H
#define CALL_H
#include <stdint.h>
#include "call_jitter.h"
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
/* ── жизненный цикл ── */
int call_init(struct UTUN_INSTANCE* inst); /* из utun_instance.c после dm_core_init */
void call_destroy(struct UTUN_INSTANCE* inst); /* перед dm_core_destroy */
/* ── сигналинг (uasync-поток) ── */
/* Caller: ch_id — группа либо "dm:<conv_id>" (BGP-группа выбирается при запуске).
* out_call_id — случайный uint64, генерит вызывающий. */
void call_start (struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id);
/* Callee: ответить/отклонить. hangup — любая сторона в любой момент. */
void call_accept (struct UTUN_INSTANCE* inst, uint64_t call_id);
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id);
void call_hangup (struct UTUN_INSTANCE* inst, uint64_t call_id);
/* Read-only онлайн-проверка (GUI может звать из любого потока). */
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id);
/* ── медиа (релей Opus-кадров, uasync-поток) ── */
/* Отправить закодированный Opus-кадр пиру (оборачивает в CALL_MEDIA).
* Только когда звонок ACTIVE. seq/ts выставляются автоматически. */
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len);
/* Коллбэк приёма медиа: вызывается в uasync-потоке на каждый Opus-кадр пира.
* Используется headless-аудио-сокетом (и в фазе 3 — декодером GUI). */
typedef void (*call_media_recv_fn)(struct UTUN_INSTANCE* inst, uint64_t call_id,
const uint8_t* opus, int len, void* arg);
void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, void* arg);
/* ── аудио-движок (опционально, GUI-сборки) ──
* Сервисный аудио-контур (codec + джиттер-буфер + тоны) регистрируется через
* call_set_audio_ops. call.c зовёт on_media на каждый принятый медиа-кадр (с seq/ts)
* и get_stats для события CALL_STATS. Демон без аудио (headless) — ops не ставит. */
struct call_audio_ops {
/* uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер). */
void (*on_media)(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len);
/* uasync-поток: текущие метрики джиттер-буфера (buffer_ms, tempo_x100,
* dropped/underruns — опционально, NULL допустим). */
int (*get_stats)(uint64_t call_id, int* buffer_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range);
};
void call_set_audio_ops(struct UTUN_INSTANCE* inst, const struct call_audio_ops* ops);
/* Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline. */
struct call_media_arg {
struct UTUN_INSTANCE* inst;
uint64_t call_id;
uint16_t len;
uint8_t opus[256];
};
void call_send_media_trampoline(void* arg);
/* ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ── */
struct call_start_arg {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t peer_node_id;
uint64_t call_id;
};
struct call_id_arg {
struct UTUN_INSTANCE* inst;
uint64_t call_id;
};
void call_start_trampoline (void* arg);
void call_accept_trampoline (void* arg);
void call_decline_trampoline(void* arg);
void call_hangup_trampoline (void* arg);
#ifdef __cplusplus
}
#endif
#endif /* CALL_H */

554
src/call/call_audio.c

@ -1,554 +0,0 @@
// call_audio.c — сервисный аудио-движок P2P-звонка (Opus + джиттер + тоны).
//
// Один активный звонок: encoder (аудио-поток), decoder + адаптивный джиттер-буфер
// vj (кодированные кадры + SoundTouch stretch на pull) и генератор тонов.
//
// RX: call.c → call_audio_on_media (uasync) → call_jitter_push (кодированный кадр);
// аудио-поток call_audio_pull_pcm → call_jitter_pull (декод+stretch) (+ глитч/завершение).
// TX: аудио-поток call_audio_feed_pcm → encode → uasync_post(call_send_media).
#include "call_audio.h"
#include "call.h"
#include "call_jitter.h"
#include "call_tones.h"
#include "../chat/chat_setting.h"
#include "../utun_instance.h"
#include "../../lib/opus_codec.h"
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "../../lib/u_async.h"
#include "../../lib/speex_aec.h"
#include "../../lib/audio_compressor.h"
#include <pthread.h>
#include <string.h>
#define CALL_AUDIO_ID "call_audio"
#define CALL_AUDIO_BITRATE 32000
#define CALL_STALL_SAMPLES (CALL_AUDIO_SAMPLE_RATE / 2) /* 500мс без PCM из jitter → глитч-тон */
#define CALL_AEC_FILTER_SAMPLES 14400 /* хвост эха 300мс @48кГц */
#define CALL_AEC_DELAY_FRAMES 2 /* desktop: минимальная задержка duplex */
static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER;
static struct UTUN_INSTANCE* g_inst = NULL;
static int g_active = 0;
static int g_ending = 0;
static int g_glitch_started = 0; /* только аудиопоток */
static int g_playback_started = 0; /* первый PCM получен; только аудиопоток */
static int g_ended_started = 0; /* только аудио-поток: тон завершения запущен */
static uint64_t g_call_id = 0;
static int g_gap_samples = 0; /* только аудиопоток; насыщение на пороге */
static opus_codec_encoder_t* g_encoder = NULL;
static opus_codec_decoder_t* g_decoder = NULL;
static struct call_jitter* g_vj = NULL;
static struct call_tones* g_tones = NULL;
static speex_aec_t* g_aec = NULL;
static int g_aec_delay_frames = CALL_AEC_DELAY_FRAMES;
static int g_prepared_capture = 0;
static int g_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */
static struct audio_compressor* g_compressor = NULL;
static uint64_t g_tx_generation;
static unsigned int g_tx_pending, g_tx_dropped, g_tx_reported;
static unsigned int g_tx_reasons;
static int g_tx_encode_error;
static uint32_t g_aec_reported_over, g_aec_reported_under, g_pcm_events;
/* Task и данные — одна аллокация; uasync освобождает task после callback и при destroy. */
struct call_audio_tx {
struct posted_task task;
struct UTUN_INSTANCE* inst;
uint64_t call_id, generation, created_tb;
int len;
uint8_t opus[256];
};
static void call_audio_send(void* arg) {
struct call_audio_tx* tx = arg;
pthread_mutex_lock(&g_mtx);
int current = tx->generation == g_tx_generation;
if (g_tx_pending) --g_tx_pending;
int send = current && g_active && !g_ending && g_inst == tx->inst && g_call_id == tx->call_id;
if (get_time_tb() - tx->created_tb > 2000) {
send = 0;
if (current) { ++g_tx_dropped; g_tx_reasons |= 16; }
}
pthread_mutex_unlock(&g_mtx);
if (send) call_send_media(tx->inst, tx->call_id, tx->opus, tx->len);
}
static const struct call_audio_ops g_ops = {
.on_media = call_audio_on_media,
.get_stats = call_audio_get_stats,
};
/* Создать AEC с текущей задержкой (объявление; определение ниже). */
static speex_aec_t* call_audio_aec_create(void);
/* vj decode-коллбэк: дёргается только из аудио-потока (call_jitter_pull). */
static int call_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples) {
opus_codec_decoder_t* dec = (opus_codec_decoder_t*)arg;
return opus_codec_decode(dec, enc, len, pcm, max_samples);
}
int call_audio_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
pthread_mutex_lock(&g_mtx);
if (g_inst == inst) { pthread_mutex_unlock(&g_mtx); return 0; }
g_inst = inst; g_tx_pending = 0;
pthread_mutex_unlock(&g_mtx);
call_set_audio_ops(inst, &g_ops);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized", CALL_AUDIO_ID);
return 0;
}
void call_audio_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || g_inst != inst) return;
call_set_audio_ops(inst, NULL);
pthread_mutex_lock(&g_mtx);
g_inst = NULL;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed", CALL_AUDIO_ID);
}
/* ── uasync-поток: приём медиа (кладём кодированный кадр в jitter-буфер) ── */
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len) {
(void)inst; (void)seq;
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && g_vj && opus && len > 0) {
call_jitter_push(g_vj, opus, len, ts_ms, (int64_t)(get_time_tb() / 10));
}
pthread_mutex_unlock(&g_mtx);
}
/* ── GUI-поток: lifecycle ── */
static int call_audio_start_impl(uint64_t call_id, int prepared) {
if (call_id == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: start bad call_id=0", CALL_AUDIO_ID);
return -1;
}
pthread_mutex_lock(&g_mtx);
if (g_active && g_call_id == call_id) {
int same_mode = g_prepared_capture == prepared;
pthread_mutex_unlock(&g_mtx);
if (!same_mode) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: capture policy cannot change within active call", CALL_AUDIO_ID);
return same_mode ? 0 : -1;
}
if (g_active) {
pthread_mutex_unlock(&g_mtx);
call_audio_release(g_call_id);
pthread_mutex_lock(&g_mtx);
}
opus_codec_encoder_t* enc = opus_codec_encoder_create(CALL_AUDIO_SAMPLE_RATE, 1);
if (!enc) {
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder create failed", CALL_AUDIO_ID);
return -1;
}
opus_codec_encoder_bitrate_set(enc, CALL_AUDIO_BITRATE);
opus_codec_decoder_t* dec = opus_codec_decoder_create(CALL_AUDIO_SAMPLE_RATE, 1);
if (!dec) {
opus_codec_encoder_destroy(enc);
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: decoder create failed", CALL_AUDIO_ID);
return -1;
}
int max_reserve_ms = chat_setting_get_int(g_inst, "call_jitter_max_reserve_ms", 1000);
struct call_jitter* j = call_jitter_create(call_audio_decode_cb, dec, max_reserve_ms);
if (!j) {
opus_codec_decoder_destroy(dec);
opus_codec_encoder_destroy(enc);
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: vj create failed", CALL_AUDIO_ID);
return -1;
}
struct call_tones* tones = call_tones_create();
if (!tones) {
call_jitter_destroy(j);
opus_codec_decoder_destroy(dec);
opus_codec_encoder_destroy(enc);
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: tones create failed", CALL_AUDIO_ID);
return -1;
}
g_aec_enabled = g_inst ? chat_setting_get_int(g_inst, "aec_enabled", 1) : 1;
speex_aec_t* aec = NULL;
if (g_aec_enabled && !prepared) {
aec = call_audio_aec_create();
if (!aec) {
g_aec_enabled = 0;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: AEC create failed — call without echo cancellation", CALL_AUDIO_ID);
}
}
/* компрессор микрофона (AGC) — как в рации: выравнивает тихий проводной микрофон
* (HAL не применяет AGC на проводном входе). lookahead=0 — без задержки. */
struct audio_compressor* ac = audio_compressor_create();
if (ac) {
audio_compressor_config_t accfg = {0};
accfg.sample_rate = CALL_AUDIO_SAMPLE_RATE;
accfg.channels = 1;
accfg.block_duration_ms = CALL_AUDIO_FRAME_MS;
accfg.lookback_ms = 200;
accfg.lookahead_ms = 0;
accfg.max_gain_db = (float)chat_setting_get_int(g_inst, "call_compressor_max_gain_db", 15);
accfg.rise_rate_per_sec = (float)chat_setting_get_int(g_inst, "compressor_rise_rate", 2);
accfg.target_level = 0.25f; /* -12 dBFS — как у голосовых сообщений */
if (audio_compressor_configure(ac, &accfg) != 0) { audio_compressor_destroy(ac); ac = NULL; }
if (ac) audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "call_compressor_enabled", 1));
} else {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: compressor create failed — call without AGC", CALL_AUDIO_ID);
}
g_encoder = enc;
g_decoder = dec;
g_vj = j;
g_tones = tones;
g_aec = aec; g_prepared_capture = prepared;
g_compressor = ac;
g_call_id = call_id;
g_active = 1;
++g_tx_generation; g_tx_dropped = g_tx_reported = 0;
g_tx_reasons = 0; g_tx_encode_error = 0;
g_ending = 0;
g_ended_started = 0;
g_glitch_started = 0;
g_playback_started = 0;
g_gap_samples = 0;
g_aec_reported_over = g_aec_reported_under = g_pcm_events = 0;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx aec=%d delay=%d compressor=%s gain=%d", CALL_AUDIO_ID,
(unsigned long long)call_id, g_aec ? 1 : 0, g_aec_delay_frames,
ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable",
chat_setting_get_int(g_inst, "call_compressor_max_gain_db", 15));
return 0;
}
/* Платформа с собственным голосовым захватом явно передаёт PCM после AEC. */
int call_audio_start_prepared(uint64_t call_id) { return call_audio_start_impl(call_id, 1); }
int call_audio_start(uint64_t call_id) { return call_audio_start_impl(call_id, 0); }
void call_audio_begin_end(uint64_t call_id) {
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && !g_ending) {
g_ending = 1;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: begin_end call=0x%016llx (ended tone)", CALL_AUDIO_ID, (unsigned long long)call_id);
}
pthread_mutex_unlock(&g_mtx);
}
void call_audio_release(uint64_t call_id) {
opus_codec_encoder_t* enc = NULL;
opus_codec_decoder_t* dec = NULL;
struct call_jitter* j = NULL;
struct call_tones* tones = NULL;
speex_aec_t* aec = NULL;
struct audio_compressor* ac = NULL;
int was_active = 0;
pthread_mutex_lock(&g_mtx);
if (g_active && g_call_id == call_id) {
was_active = 1;
g_active = 0;
++g_tx_generation;
g_ending = 0;
g_ended_started = 0;
g_playback_started = 0;
g_call_id = 0;
g_gap_samples = 0;
enc = g_encoder; g_encoder = NULL;
dec = g_decoder; g_decoder = NULL;
j = g_vj; g_vj = NULL;
tones = g_tones; g_tones = NULL;
aec = g_aec; g_aec = NULL;
ac = g_compressor; g_compressor = NULL;
}
pthread_mutex_unlock(&g_mtx);
if (!was_active) return;
/* дальше on_media не тронет объекты (active=0, указатели NULL) */
if (enc) opus_codec_encoder_destroy(enc);
if (dec) opus_codec_decoder_destroy(dec);
if (j) call_jitter_destroy(j);
if (tones) call_tones_destroy(tones);
if (aec) speex_aec_destroy(aec);
if (ac) audio_compressor_destroy(ac);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: released call=0x%016llx", CALL_AUDIO_ID, (unsigned long long)call_id);
}
void call_audio_stop(void) {
uint64_t cid = 0;
pthread_mutex_lock(&g_mtx);
if (g_active) cid = g_call_id;
pthread_mutex_unlock(&g_mtx);
if (cid) call_audio_release(cid);
}
void call_audio_reset_io(uint64_t call_id, int playback) {
pthread_mutex_lock(&g_mtx);
if (g_active && g_call_id == call_id) {
if (g_aec) speex_aec_reset(g_aec);
if (!playback && g_compressor) audio_compressor_reset(g_compressor);
if (playback) {
call_jitter_reset(g_vj);
g_gap_samples = 0;
g_glitch_started = 0;
g_playback_started = 0;
call_tones_glitch_stop(g_tones);
}
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: I/O reset call=%016llx direction=%s", CALL_AUDIO_ID,
(unsigned long long)call_id, playback ? "playback" : "capture");
}
pthread_mutex_unlock(&g_mtx);
}
/* ── AEC (SpeexDSP): рантайм-управление ── */
/* Создать AEC с текущей задержкой. Вызывается под g_mtx. */
static speex_aec_t* call_audio_aec_create(void) {
return speex_aec_create(CALL_AUDIO_SAMPLE_RATE, CALL_AUDIO_FRAME_SAMPLES,
CALL_AEC_FILTER_SAMPLES, g_aec_delay_frames);
}
void call_audio_set_aec_enabled(int enabled) {
pthread_mutex_lock(&g_mtx);
g_aec_enabled = enabled ? 1 : 0;
if (g_active && !g_prepared_capture) {
if (g_aec_enabled && !g_aec) {
g_aec = call_audio_aec_create();
if (!g_aec) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: AEC create failed (enabled)", CALL_AUDIO_ID);
else DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC enabled (delay=%d frames)", CALL_AUDIO_ID, g_aec_delay_frames);
} else if (!g_aec_enabled && g_aec) {
speex_aec_destroy(g_aec);
g_aec = NULL;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC disabled", CALL_AUDIO_ID);
}
}
pthread_mutex_unlock(&g_mtx);
}
void call_audio_set_aec_delay_frames(int frames) {
if (frames < 1) frames = 1;
pthread_mutex_lock(&g_mtx);
g_aec_delay_frames = frames;
if (g_active && !g_prepared_capture && g_aec_enabled && g_aec) {
speex_aec_destroy(g_aec);
g_aec = call_audio_aec_create();
if (!g_aec) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: AEC recreate failed (delay=%d)", CALL_AUDIO_ID, frames);
else DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC delay -> %d frames", CALL_AUDIO_ID, frames);
}
pthread_mutex_unlock(&g_mtx);
}
/* ── аудио-поток: TX / RX ── */
static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count, int muted, const uint8_t* mute_mask, int prepared) {
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES || (prepared && !mute_mask)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: invalid capture frame count=%d prepared=%d", CALL_AUDIO_ID, count, prepared);
return -1;
}
uint8_t opus[256];
int16_t cleaned[CALL_AUDIO_FRAME_SAMPLES];
struct UASYNC* ua = NULL;
struct UTUN_INSTANCE* inst = NULL;
int len = -1;
pthread_mutex_lock(&g_mtx);
if (!g_active || call_id != g_call_id || !g_encoder) {
pthread_mutex_unlock(&g_mtx);
return -1;
}
if (g_prepared_capture != prepared) {
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: capture policy mismatch prepared=%d", CALL_AUDIO_ID, prepared);
return -1;
}
const int16_t* tx = pcm;
if (g_aec) {
if (speex_aec_process_capture(g_aec, pcm, count, cleaned) == count)
tx = cleaned;
}
int16_t amplified[CALL_AUDIO_FRAME_SAMPLES];
if (g_compressor) {
if (audio_compressor_process_frame(g_compressor, tx, amplified, count) != 0) {
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: compressor frame failed", CALL_AUDIO_ID);
return -1;
}
tx = amplified;
}
if (mute_mask) {
if (tx != amplified) memcpy(amplified, tx, sizeof(amplified));
for (int i = 0; i < count; ++i) if (mute_mask[i]) amplified[i] = 0;
tx = amplified;
} else if (muted) { memset(amplified, 0, sizeof(amplified)); tx = amplified; }
len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus));
ua = g_inst ? g_inst->ua : NULL;
inst = g_inst;
if (len <= 0 || !ua || !inst || g_ending || g_tx_pending >= 8) {
++g_tx_dropped;
if (g_tx_pending >= 8) g_tx_reasons |= 1;
if (!ua || !inst) g_tx_reasons |= 4;
if (len <= 0) { g_tx_reasons |= 8; g_tx_encode_error = len; }
if (g_ending) g_tx_reasons |= 32;
pthread_mutex_unlock(&g_mtx);
return -1;
}
struct call_audio_tx* task = u_calloc(1, sizeof(*task));
if (!task) {
++g_tx_dropped;
g_tx_reasons |= 2;
pthread_mutex_unlock(&g_mtx);
return -1;
}
task->inst = inst; task->call_id = call_id;
task->generation = g_tx_generation; task->created_tb = get_time_tb();
task->len = len;
memcpy(task->opus, opus, (size_t)len);
task->task.callback = call_audio_send; task->task.arg = task;
++g_tx_pending;
uasync_post_reserved(ua, &task->task);
pthread_mutex_unlock(&g_mtx);
return 0;
}
/* Маска соответствует границам mute в том же упорядоченном потоке, что и PCM. */
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask) {
return call_audio_feed_impl(call_id, pcm, CALL_AUDIO_FRAME_SAMPLES, 0, mute_mask, 1);
}
int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted) {
return call_audio_feed_impl(call_id, pcm, count, muted, NULL, 0);
}
/* Незамьюченный захват для платформ без программного mute. */
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) {
return call_audio_feed_impl(call_id, pcm, count, 0, NULL, 0);
}
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
if (!out || max_samples <= 0) return 0;
int active = 0, ending = 0;
struct call_jitter* j = NULL;
struct call_tones* tones = NULL;
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id) {
active = 1;
ending = g_ending;
j = g_vj;
tones = g_tones;
}
pthread_mutex_unlock(&g_mtx);
if (!active || !j || !tones) return 0;
int n = 0;
uint32_t events = 0;
/* завершение: доигрываем тон (450мс), затем тишина. Тон стартует лениво
* здесь (аудио-поток) — begin_end лишь выставляет флаг, чтобы не гонять
* генератор тонов с GUI-потоком. */
if (ending) {
if (!g_ended_started) {
call_tones_ended_start(tones);
g_ended_started = 1;
}
if (call_tones_ended_pending(tones))
n = call_tones_render(tones, out, max_samples);
} else {
n = call_jitter_pull(j, out, max_samples);
int previous_gap = g_gap_samples;
if (n > 0) {
if (!g_playback_started) events |= 1;
g_playback_started = 1;
g_gap_samples = 0;
}
int missing = max_samples - n;
/* Подготовка устройств и начальное накопление PCM не являются потерей звука. */
int quiet = g_playback_started ? CALL_STALL_SAMPLES - g_gap_samples : missing;
if (quiet > missing) quiet = missing;
int tone_samples = missing - quiet;
if (n > 0 && previous_gap > 0) events |= 2;
if (missing > 0 && g_playback_started && g_gap_samples == 0) events |= 4;
if (g_playback_started) g_gap_samples += quiet;
/* Новый PCM прерывает предыдущий цикл тона, даже если хвост блока снова пуст. */
if (g_glitch_started && (n > 0 || tone_samples == 0)) {
g_glitch_started = 0;
call_tones_glitch_stop(tones);
events |= 8;
}
memset(out + n, 0, (size_t)quiet * sizeof(*out));
if (tone_samples > 0) {
if (!g_glitch_started) {
g_glitch_started = 1;
events |= 16;
}
call_tones_glitch_start(tones);
call_tones_render(tones, out + n + quiet, tone_samples);
}
n = max_samples;
}
/* рендер-референс для AEC: то, что реально пошло в динамик */
if (n > 0) {
pthread_mutex_lock(&g_mtx);
if (g_aec) speex_aec_feed_playback(g_aec, out, n);
g_pcm_events |= events;
pthread_mutex_unlock(&g_mtx);
}
return n;
}
/* ── uasync-поток: метрики ── */
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range) {
int ok = 0;
struct call_jitter_diagnostics diagnostics = {0};
uint32_t aec_over = 0, aec_under = 0, events = 0;
unsigned int tx_dropped = 0, tx_pending = 0;
unsigned int reasons = 0;
int encode_error = 0;
int tx_changed = 0, aec_changed = 0;
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && g_vj) {
call_jitter_get_stats(g_vj, buffer_ms, tempo_x100, dropped, underruns, range);
call_jitter_take_diagnostics(g_vj, &diagnostics);
speex_aec_get_stats(g_aec, &aec_over, &aec_under);
aec_changed = aec_over != g_aec_reported_over || aec_under != g_aec_reported_under;
g_aec_reported_over = aec_over; g_aec_reported_under = aec_under;
tx_dropped = g_tx_dropped; tx_pending = g_tx_pending;
tx_changed = tx_dropped != g_tx_reported; g_tx_reported = tx_dropped;
reasons = g_tx_reasons; g_tx_reasons = 0; encode_error = g_tx_encode_error;
events = g_pcm_events; g_pcm_events = 0;
ok = 1;
}
pthread_mutex_unlock(&g_mtx);
if (tx_changed) DEBUG_WARN(DEBUG_CATEGORY_CALL,
"%s: TX dropped=%u pending=%u limit=8 max_age=200ms reasons=queue:%d alloc:%d core:%d encode:%d age:%d ending:%d encode_result=%d",
CALL_AUDIO_ID, tx_dropped, tx_pending, !!(reasons & 1), !!(reasons & 2), !!(reasons & 4),
!!(reasons & 8), !!(reasons & 16), !!(reasons & 32), encode_error);
if (aec_changed) DEBUG_WARN(DEBUG_CATEGORY_AEC, "%s: AEC render overflow=%u capture passthrough=%u", CALL_AUDIO_ID, aec_over, aec_under);
if (diagnostics.decode_errors || diagnostics.budget_hits)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: decode_errors=%u last_result=%d len=%d budget_hits=%u depth=%dms",
diagnostics.decode_errors, diagnostics.error_result, diagnostics.error_len, diagnostics.budget_hits, diagnostics.depth_ms);
if (events) DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: PCM first=%d resumed=%d gap=%d tone_started=%d tone_stopped=%d", CALL_AUDIO_ID,
!!(events & 1), !!(events & 2), !!(events & 4), !!(events & 16), !!(events & 8));
if (ok) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: depth=%dms reserve=%dms tempo=%d waiting=%d catching_up=%d resumed=%d",
diagnostics.depth_ms, diagnostics.reserve_ms, diagnostics.tempo_x100, diagnostics.waiting,
diagnostics.catching_up, diagnostics.resumed);
return ok ? 0 : -1;
}

91
src/call/call_audio.h

@ -1,91 +0,0 @@
#ifndef CALL_AUDIO_H
#define CALL_AUDIO_H
// call_audio.h — сервисный аудио-движок P2P-звонка (единый для desktop/Android/headless).
//
// Владеет Opus codec, адаптивным джиттер-буфером (call_jitter: кодированные
// кадры + SoundTouch time-stretch на pull) и тонами (глитч при сталле / завершение).
// Излишек голоса держится до декодирования — декодируется только то, что готово
// к воспроизведению. Потребитель (desktop miniaudio / Android AudioTrack) получает
// финальный PCM через call_audio_pull_pcm и отдаёт захваченный PCM через
// call_audio_feed_pcm.
//
// Потоки:
// - call_audio_start / begin_end / release — GUI-поток (по ACCEPTED/ENDED);
// - call_audio_feed_pcm / pull_pcm — по одному аудио-потоку на направление;
// - call_audio_on_media / get_stats — uasync-поток (из call.c).
// Общие указатели (encoder/decoder/vj/tones) защищены мьютексом; encoder и
// decoder — только аудио-поток (decoder дёргается из call_jitter_pull через коллбэк),
// call_jitter_push — только uasync-поток.
#include <stdint.h>
#include "call_jitter.h"
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
#define CALL_AUDIO_SAMPLE_RATE 48000
#define CALL_AUDIO_FRAME_MS 20
#define CALL_AUDIO_FRAME_SAMPLES (CALL_AUDIO_SAMPLE_RATE * CALL_AUDIO_FRAME_MS / 1000) /* 960 */
/* Зарегистрировать аудио-движок в call.c (идемпотентно). */
int call_audio_init(struct UTUN_INSTANCE* inst);
void call_audio_destroy(struct UTUN_INSTANCE* inst);
/* GUI-поток: создать codec + jitter + tones. Старый API принимает raw PCM и использует core AEC.
* Для desktop нужен start_prepared: AEC и stereo reference принадлежат VoiceAudioIo. 0 = успех. */
int call_audio_start(uint64_t call_id);
/* AEC принадлежит платформенному захвату; pull больше не подаёт собственный референс. */
int call_audio_start_prepared(uint64_t call_id);
/* GUI-поток: по CALL_ENDED — начать тон завершения (pull_pcm выдаст 450мс тона). */
void call_audio_begin_end(uint64_t call_id);
/* GUI-поток (после остановки аудиоустройства): освободить все объекты. */
void call_audio_release(uint64_t call_id);
/* GUI-поток: остановить текущий активный звонок (удобная обёртка release). */
void call_audio_stop(void);
/* аудио-поток: закодировать PCM-кадр (ровно CALL_AUDIO_FRAME_SAMPLES) и отправить пиру. */
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count);
/* Mute после AEC/AGC: фильтр получает настоящий микрофон, Opus — нулевой PCM. */
/* Подготовленный PCM и маска mute (960 элементов); AEC уже выполнен до этого API. */
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask);
int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted);
/* аудио-поток: выдать до max_samples финального PCM (jitter + stretch + тоны).
* Возвращает число сэмплов (может быть < max — андерфлоу/конец тона). */
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
/* uasync-поток (из call.c): принятый Opus-кадр — кладём в джиттер-буфер (call_jitter_push,
* декодирование произойдёт на pull в аудио-потоке). */
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len);
/* uasync-поток: метрики джиттер-буфера для CALL_STATS (dropped/underruns — необязательные). */
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range);
/* После остановки указанного направления: сбросить AEC, для playback также устаревший jitter/PCM.
* Capture и playback могут работать в разных потоках, по одному writer на направление.
* Release допустим только после остановки обоих. */
void call_audio_reset_io(uint64_t call_id, int playback);
/* Core AEC для raw capture: Android — по спикерфону (route == SPEAKER).
* Prepared capture не создаёт core AEC; desktop передаёт настройку в VoiceAudioIo. */
void call_audio_set_aec_enabled(int enabled);
/* Задать задержку рендер→захват AEC в кадрах (20 мс). Android измеряет через
* AudioTrack/AudioRecord getTimestamp. Prepared desktop выравнивает референс по времени. Если core AEC уже
* активен — пересоздаётся с новой задержкой. */
void call_audio_set_aec_delay_frames(int frames);
#ifdef __cplusplus
}
#endif
#endif /* CALL_AUDIO_H */

372
src/call/call_headless.c

@ -1,372 +0,0 @@
// call_headless.c — headless-аудио-сокет для P2P-звонков (PCM, без тактирования)
//
// TCP-сокет с бинарным фреймингом [type:1][call_id:8][len:2][data:len].
// Клиент привязывается к звонку (HELLO) и обменивается ГОТОВЫМ PCM (int16 mono 48k):
// клиент → ядро: PCM-кадр (960 сэмплов) — кодируется в Opus и уходит пиру;
// ядро → клиент: Opus пира декодируется в PCM и отдаётся клиенту.
// Тактирование/джиттер — на стороне клиента (ядро лишь кодирует/декодирует).
// Сигналинг — на control-сокете (chat_headless_control), медиа — здесь.
#include "call_headless.h"
#include "call.h"
#include "../utun_instance.h"
#include "../../lib/opus_codec.h"
#include "../../lib/u_async.h"
#include "../../lib/mem.h"
#include "../../lib/socket_compat.h"
#include "../../lib/debug_config.h"
#include "../../lib/platform_compat.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#define CH_ID "call_audio"
#define AUDIO_RECV_BUF_SIZE 65536
#define AUDIO_SEND_BUF_SIZE 65536
#define AUDIO_MAX_CLIENTS 16
#define AUDIO_MAX_FRAME 2048
#define CA_SAMPLE_RATE 48000
#define CA_FRAME_SAMPLES 960 /* 20мс @48k */
#define CA_PCM_FRAME_BYTES (CA_FRAME_SAMPLES * (int)sizeof(int16_t)) /* 1920 */
#define CA_BITRATE 32000
struct call_audio_client {
socket_t fd;
void* socket_id;
uint64_t call_id; /* 0 = не привязан */
uint8_t recv_buf[AUDIO_RECV_BUF_SIZE];
int recv_len;
uint8_t send_buf[AUDIO_SEND_BUF_SIZE];
int send_len;
int send_offset;
int closing;
struct call_audio* ca;
struct call_audio_client* next;
};
struct call_audio {
struct UASYNC* ua;
struct UTUN_INSTANCE* inst;
struct call_audio_client* clients;
int client_count;
socket_t listen_fd;
void* listen_sock_id;
int running;
void* cleanup_timer;
/* Opus-кодеки (одна активная звонка за раз — busy в call.c) */
uint64_t enc_call_id;
opus_codec_encoder_t* enc;
uint64_t dec_call_id;
opus_codec_decoder_t* dec;
/* диагностика */
uint32_t c_enc_frames;
uint32_t c_dec_frames;
uint32_t c_enc_dropped;
uint32_t c_dec_dropped;
};
static struct call_audio* ca_of(struct UTUN_INSTANCE* inst) {
return inst ? (struct call_audio*)inst->call_audio : NULL;
}
static void ca_close_client(struct call_audio_client* cli);
/* ── отправка фрейма клиенту (с буферизацией как в control-сокете) ── */
static void ca_send_raw(struct call_audio_client* cli, const uint8_t* data, size_t len) {
struct call_audio* ca = cli->ca;
if (!cli || cli->closing || cli->fd == SOCKET_INVALID || !data || len == 0) return;
if (cli->send_len > 0) {
size_t space = sizeof(cli->send_buf) - (size_t)cli->send_len;
if (len > space) return;
memcpy(cli->send_buf + cli->send_len, data, len);
cli->send_len += (int)len;
return;
}
ssize_t r = send(cli->fd, data, (int)len, 0);
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) {
if (len <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data, len); cli->send_len = (int)len; cli->send_offset = 0; }
if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 1);
return;
}
if (r >= 0 && (size_t)r < len) {
size_t remain = len - (size_t)r;
if (remain <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data + r, remain); cli->send_len = (int)remain; cli->send_offset = 0; }
if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 1);
}
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: send failed err=%d, closing client", CH_ID, socket_get_error());
ca_close_client(cli);
}
}
static void ca_write_frame(struct call_audio_client* cli, uint8_t type, uint64_t call_id,
const uint8_t* data, uint16_t len) {
uint8_t hdr[CALL_AUDIO_HDR_SIZE];
hdr[0] = type;
memcpy(hdr + 1, &call_id, 8);
hdr[9] = (uint8_t)(len >> 8);
hdr[10] = (uint8_t)(len & 0xFF);
ca_send_raw(cli, hdr, sizeof(hdr));
if (len) ca_send_raw(cli, data, len);
}
/* ── Opus-кодеки (одна активная звонка за раз) ── */
static opus_codec_encoder_t* ca_encoder(struct call_audio* ca, uint64_t call_id) {
if (ca->enc && ca->enc_call_id == call_id) return ca->enc;
if (ca->enc) { opus_codec_encoder_destroy(ca->enc); ca->enc = NULL; }
ca->enc = opus_codec_encoder_create(CA_SAMPLE_RATE, 1);
if (ca->enc) {
opus_codec_encoder_bitrate_set(ca->enc, CA_BITRATE);
ca->enc_call_id = call_id;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: encoder created for call_id=%016llx", CH_ID, (unsigned long long)call_id);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder create failed", CH_ID);
}
return ca->enc;
}
static opus_codec_decoder_t* ca_decoder(struct call_audio* ca, uint64_t call_id) {
if (ca->dec && ca->dec_call_id == call_id) return ca->dec;
if (ca->dec) { opus_codec_decoder_destroy(ca->dec); ca->dec = NULL; }
ca->dec = opus_codec_decoder_create(CA_SAMPLE_RATE, 1);
if (ca->dec) {
ca->dec_call_id = call_id;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: decoder created for call_id=%016llx", CH_ID, (unsigned long long)call_id);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: decoder create failed", CH_ID);
}
return ca->dec;
}
/* ── приём медиа от пира → PCM клиенту ── */
static void ca_media_recv(struct UTUN_INSTANCE* inst, uint64_t call_id,
const uint8_t* opus, int len, void* arg) {
struct call_audio* ca = (struct call_audio*)arg;
(void)inst;
if (!ca || !opus || len <= 0) return;
opus_codec_decoder_t* dec = ca_decoder(ca, call_id);
if (!dec) return;
int16_t pcm[CA_FRAME_SAMPLES];
int n = opus_codec_decode(dec, opus, len, pcm, CA_FRAME_SAMPLES);
if (n <= 0) {
ca->c_dec_dropped++;
return;
}
ca->c_dec_frames++;
for (struct call_audio_client* c = ca->clients; c; c = c->next) {
if (c->call_id == call_id && !c->closing) {
ca_write_frame(c, CALL_AUDIO_FRAME, call_id, (const uint8_t*)pcm, (uint16_t)(n * (int)sizeof(int16_t)));
break;
}
}
}
/* ── разбор входящих фреймов ── */
static void ca_parse_frames(struct call_audio_client* cli) {
struct call_audio* ca = cli->ca;
uint8_t* p = cli->recv_buf;
int avail = cli->recv_len;
int consumed = 0;
while (avail - consumed >= CALL_AUDIO_HDR_SIZE) {
uint8_t type = p[consumed];
uint64_t call_id; memcpy(&call_id, p + consumed + 1, 8);
uint16_t flen = (uint16_t)((p[consumed + 9] << 8) | p[consumed + 10]);
if (flen > AUDIO_MAX_FRAME) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: frame too big %u — closing", CH_ID, flen);
ca_close_client(cli);
return;
}
if (avail - consumed < (int)(CALL_AUDIO_HDR_SIZE + flen)) break; /* нужен ещё кусок */
const uint8_t* data = p + consumed + CALL_AUDIO_HDR_SIZE;
if (type == CALL_AUDIO_HELLO) {
cli->call_id = call_id;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client bound to call_id=%016llx", CH_ID, (unsigned long long)call_id);
} else if (type == CALL_AUDIO_FRAME) {
if (flen != CA_PCM_FRAME_BYTES) {
ca->c_enc_dropped++;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: bad PCM frame size=%u (expect %d) — dropped",
CH_ID, flen, CA_PCM_FRAME_BYTES);
} else {
opus_codec_encoder_t* enc = ca_encoder(ca, call_id);
uint8_t opus[256];
int olen = enc ? opus_codec_encode(enc, (const int16_t*)data, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
if (olen <= 0) {
ca->c_enc_dropped++;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: encode failed (olen=%d) call_id=%016llx",
CH_ID, olen, (unsigned long long)call_id);
} else {
ca->c_enc_frames++;
if (call_send_media(ca->inst, call_id, opus, olen) != 0)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: send_media failed call_id=%016llx len=%d",
CH_ID, (unsigned long long)call_id, olen);
}
}
} else {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: unknown frame type=0x%02x — closing", CH_ID, type);
ca_close_client(cli);
return;
}
consumed += CALL_AUDIO_HDR_SIZE + flen;
}
if (consumed > 0) {
memmove(cli->recv_buf, cli->recv_buf + consumed, (size_t)(cli->recv_len - consumed));
cli->recv_len -= consumed;
}
}
/* ── socket callbacks ── */
static void ca_write_callback(socket_t fd, void* arg) {
struct call_audio_client* cli = (struct call_audio_client*)arg;
struct call_audio* ca = cli ? cli->ca : NULL;
if (!cli || cli->closing || fd != cli->fd) return;
if (cli->send_len <= 0) { if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 0); return; }
ssize_t r = send(cli->fd, cli->send_buf + cli->send_offset, cli->send_len - cli->send_offset, 0);
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) return;
if (r <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: send failed (r=%zd err=%d) — closing client", CH_ID, r, socket_get_error());
ca_close_client(cli);
return;
}
cli->send_offset += (int)r;
if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 0); }
}
static void ca_close_client(struct call_audio_client* cli) {
struct call_audio* ca = cli ? cli->ca : NULL;
if (!cli || cli->closing) return;
cli->closing = 1;
if (ca && ca->ua && cli->socket_id) uasync_remove_socket_t(ca->ua, cli->fd);
if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; }
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client closed call_id=%016llx", CH_ID, (unsigned long long)cli->call_id);
}
static void ca_read_callback(socket_t fd, void* arg) {
struct call_audio_client* cli = (struct call_audio_client*)arg;
if (!cli || cli->closing) return;
uint8_t tmp[4096];
ssize_t r = recv(fd, tmp, (int)sizeof(tmp), 0);
if (r <= 0) { ca_close_client(cli); return; }
if (cli->recv_len + (int)r > AUDIO_RECV_BUF_SIZE) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: recv buffer overflow — closing", CH_ID);
ca_close_client(cli);
return;
}
memcpy(cli->recv_buf + cli->recv_len, tmp, (size_t)r);
cli->recv_len += (int)r;
ca_parse_frames(cli);
}
static void ca_accept_callback(socket_t fd, void* arg) {
struct call_audio* ca = (struct call_audio*)arg;
struct sockaddr_storage addr; socklen_t alen = sizeof(addr);
socket_t cfd = accept(fd, (struct sockaddr*)&addr, &alen);
if (cfd == SOCKET_INVALID) {
int e = socket_get_error();
if (e != ERR_AGAIN && e != ERR_WOULDBLOCK) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: accept failed: %s", CH_ID, socket_strerror(e));
return;
}
socket_set_nonblocking(cfd);
if (ca->client_count >= AUDIO_MAX_CLIENTS) { socket_close_wrapper(cfd); DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: max clients reached", CH_ID); return; }
struct call_audio_client* cli = u_calloc(1, sizeof(*cli));
if (!cli) { socket_close_wrapper(cfd); return; }
cli->fd = cfd;
cli->ca = ca;
cli->socket_id = uasync_add_socket_t(ca->ua, cfd, ca_read_callback, ca_write_callback, NULL, "call_client", cli);
if (!cli->socket_id) { u_free(cli); socket_close_wrapper(cfd); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: failed to register client fd", CH_ID); return; }
cli->next = ca->clients; ca->clients = cli; ca->client_count++;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client connected fd=%d total=%d", CH_ID, (int)cfd, ca->client_count);
}
/* ── периодическая очистка закрытых клиентов ── */
static void ca_cleanup(void* arg) {
struct call_audio* ca = (struct call_audio*)arg;
ca->cleanup_timer = NULL;
if (!ca->running) return;
struct call_audio_client** prev = &ca->clients;
while (*prev) {
if ((*prev)->closing) {
struct call_audio_client* dead = *prev; *prev = dead->next;
if (dead->fd != SOCKET_INVALID) socket_close_wrapper(dead->fd);
u_free(dead); ca->client_count--;
} else prev = &(*prev)->next;
}
ca->cleanup_timer = uasync_set_timeout(ca->ua, 5000, ca, ca_cleanup, "ca_cleanup");
if (!ca->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID);
}
/* ── init / destroy ── */
int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port) {
if (!ua || !inst || !bind_ip || port <= 0 || port > 65535) return -1;
struct call_audio* ca = ca_of(inst);
if (ca && ca->running) return 0;
if (!ca) {
ca = u_calloc(1, sizeof(*ca));
if (!ca) return -1;
inst->call_audio = ca;
}
ca->ua = ua; ca->inst = inst;
ca->listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (ca->listen_fd == SOCKET_INVALID) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: socket() failed: %s", CH_ID, socket_strerror(socket_get_error())); return -1; }
socket_set_reuseaddr(ca->listen_fd, 1);
socket_set_nonblocking(ca->listen_fd);
struct sockaddr_in sin; memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET; sin.sin_port = htons((uint16_t)port);
if (inet_pton(AF_INET, bind_ip, &sin.sin_addr) != 1) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1; }
if (bind(ca->listen_fd, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: bind(%s:%d) failed: %s", CH_ID, bind_ip, port, socket_strerror(socket_get_error()));
socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1;
}
if (listen(ca->listen_fd, 5) < 0) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1; }
ca->listen_sock_id = uasync_add_socket_t(ua, ca->listen_fd, ca_accept_callback, NULL, NULL, "call_listen", ca);
if (!ca->listen_sock_id) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1; }
call_set_media_recv_cb(inst, ca_media_recv, ca);
ca->cleanup_timer = uasync_set_timeout(ua, 5000, ca, ca_cleanup, "ca_cleanup");
if (!ca->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID);
ca->running = 1;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket listening on %s:%d", CH_ID, bind_ip, port);
return 0;
}
void call_headless_destroy(struct UTUN_INSTANCE* inst) {
struct call_audio* ca = ca_of(inst);
if (!ca || !ca->running) return;
ca->running = 0;
if (ca->cleanup_timer) { uasync_cancel_timeout(ca->ua, ca->cleanup_timer); ca->cleanup_timer = NULL; }
call_set_media_recv_cb(inst, NULL, NULL);
if (ca->listen_sock_id && ca->ua) uasync_remove_socket_t(ca->ua, ca->listen_fd);
if (ca->listen_fd != SOCKET_INVALID) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; }
if (ca->enc) { opus_codec_encoder_destroy(ca->enc); ca->enc = NULL; }
if (ca->dec) { opus_codec_decoder_destroy(ca->dec); ca->dec = NULL; }
struct call_audio_client* c = ca->clients;
while (c) {
struct call_audio_client* n = c->next;
ca_close_client(c);
u_free(c);
c = n;
}
ca->clients = NULL; ca->client_count = 0;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u)",
CH_ID, ca->c_enc_frames, ca->c_dec_frames, ca->c_enc_dropped, ca->c_dec_dropped);
}

38
src/call/call_headless.h

@ -1,38 +0,0 @@
// call_headless.h — headless-аудио-сокет для P2P-звонков (PCM, без тактирования)
//
// Отдельный TCP-сокет, через который headless-клиент (CLI/скрипт) обменивается
// ГОТОВЫМ PCM с ядром звонка. Ядро кодирует PCM→Opus (TX) и декодирует Opus→PCM
// (RX); тактирование и джиттер — на стороне клиента. Сигналинг идёт по
// control-сокету (chat_headless_control), аудио — по этому сокету.
//
// Бинарный фрейм (обе стороны):
// [type:1][call_id:8][len:2][data:len]
// type = 0x01 HELLO (клиент → ядро: привязать соединение к call_id, data пусто)
// type = 0x02 FRAME (PCM int16 mono 48k, 960 сэмплов = 1920 байт)
// клиент → ядро = PCM захвата (кодируется и шлётся пиру);
// ядро → клиент = PCM воспроизведения (декодированный Opus пира)
#ifndef CALL_HEADLESS_H
#define CALL_HEADLESS_H
#include <stdint.h>
struct UASYNC;
struct UTUN_INSTANCE;
#define CALL_AUDIO_HELLO 0x01
#define CALL_AUDIO_FRAME 0x02
#define CALL_AUDIO_HDR_SIZE 11 /* type(1) + call_id(8) + len(2) */
#define CALL_AUDIO_PCM_FRAME_BYTES (960 * (int)sizeof(int16_t)) /* 1920 — PCM int16 mono 48k 20мс */
#ifdef __cplusplus
extern "C" {
#endif
int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port);
void call_headless_destroy(struct UTUN_INSTANCE* inst);
#ifdef __cplusplus
}
#endif
#endif /* CALL_HEADLESS_H */

217
src/call/call_jitter.cpp

@ -1,217 +0,0 @@
// Буфер звонка: храним речь в порядке доставки, ускоряем накопление без изменения высоты голоса.
#include "call_jitter.h"
#include "call_jitter_window.h"
#include <chrono>
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "soundtouch/SoundTouch.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <mutex>
#include <new>
#include <vector>
static constexpr int kSampleRate = 48000;
static constexpr int kFrameSamples = 960;
static constexpr int kMaxEncoded = 512;
static constexpr int kTargetMs = 60;
static constexpr double kMaxTempo = 1.6;
static constexpr int kTempoRampMs = 500;
template<typename T> static T clamp_value(T value, T low, T high) {
return std::max(low, std::min(value, high));
}
struct call_jitter {
std::mutex mtx;
uint8_t ring[CALL_JITTER_MAX_FRAMES][kMaxEncoded];
int lens[CALL_JITTER_MAX_FRAMES];
int head = 0, count = 0;
uint32_t dropped = 0, underruns = 0;
call_jitter_diagnostics diagnostics{};
int processing_ms = 0, tempo_x100 = 100; /* снимок аудиопотока под mtx */
call_jitter_window depth_window, arrival_window;
call_jitter_range range = {0, 0, 60}; /* опубликованный снимок под mtx */
call_jitter_decode_fn decode;
void* arg;
soundtouch::SoundTouch st;
int16_t pcm[kFrameSamples];
float input[kFrameSamples];
std::vector<float> output;
bool primed = false, catching_up = false;
double tempo = 1.0, ema_ms = kTargetMs;
};
extern "C" {
struct call_jitter* call_jitter_create(call_jitter_decode_fn decode, void* arg, int max_reserve_ms) {
if (max_reserve_ms < 500 || max_reserve_ms > 2000) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_jitter: invalid max reserve=%dms", max_reserve_ms);
return nullptr;
}
if (!decode) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_jitter: missing decoder");
return nullptr;
}
void* memory = u_malloc(sizeof(call_jitter));
if (!memory) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_jitter: allocation failed");
return nullptr;
}
auto* j = new (memory) call_jitter;
j->arrival_window.max_reserve = max_reserve_ms;
j->decode = decode; j->arg = arg;
j->st.setSampleRate(kSampleRate); j->st.setChannels(1); j->st.setTempo(1.0);
j->st.setSetting(SETTING_USE_QUICKSEEK, 1);
j->st.setSetting(SETTING_SEQUENCE_MS, 20);
j->st.setSetting(SETTING_SEEKWINDOW_MS, 10);
j->st.setSetting(SETTING_OVERLAP_MS, 4);
j->st.setSetting(SETTING_USE_AA_FILTER, 0);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: created capacity=%dms target=%dms max_reserve=%dms tempo=1.0..%.1f ramp=%dms",
CALL_JITTER_MAX_FRAMES * CALL_JITTER_FRAME_MS, kTargetMs, max_reserve_ms, kMaxTempo, kTempoRampMs);
return j;
}
void call_jitter_destroy(struct call_jitter* j) {
if (!j) return;
j->~call_jitter(); u_free(j);
}
void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32_t sent_ms, int64_t received_ms) {
if (!j || !enc || len <= 0 || len > kMaxEncoded || received_ms < 0) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: invalid frame len=%d received=%lldms", len, (long long)received_ms);
return;
}
std::lock_guard<std::mutex> lock(j->mtx);
if (received_ms < j->arrival_window.last_received_ms) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: receive clock moved backwards previous=%lldms now=%lldms",
(long long)j->arrival_window.last_received_ms, (long long)received_ms);
return;
}
j->arrival_window.observe_arrival(received_ms, sent_ms);
j->range.reserve_ms = j->arrival_window.reserve;
if (j->count == CALL_JITTER_MAX_FRAMES) {
j->head = (j->head + 1) % CALL_JITTER_MAX_FRAMES; j->count--; j->dropped++;
}
int idx = (j->head + j->count) % CALL_JITTER_MAX_FRAMES;
std::memcpy(j->ring[idx], enc, (size_t)len); j->lens[idx] = len; j->count++;
}
int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) {
if (!j || !out || max_samples <= 0) return 0;
int count, reserve;
{ std::lock_guard<std::mutex> lock(j->mtx); count = j->count; reserve = j->arrival_window.reserve; }
// Готовый PCM учитываем в длительности исходной речи.
double depth = count * CALL_JITTER_FRAME_MS
+ (j->st.numUnprocessedSamples() + j->st.numSamples() * j->tempo) * 1000.0 / kSampleRate;
int64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count();
j->depth_window.observe(now_ms, (int)std::lround(depth));
int resumed = 0;
{
std::lock_guard<std::mutex> lock(j->mtx);
j->range.min_ms = (int)j->depth_window.low; j->range.max_ms = (int)j->depth_window.high;
}
if (!j->primed) {
if (depth < reserve) {
std::lock_guard<std::mutex> lock(j->mtx);
j->diagnostics.depth_ms = (int)depth; j->diagnostics.reserve_ms = reserve;
j->diagnostics.tempo_x100 = j->tempo_x100; j->diagnostics.waiting = 1;
return 0;
}
j->primed = true;
resumed = 1;
}
j->ema_ms += 0.2 * (depth - j->ema_ms);
double target = 1.0 + (kMaxTempo - 1.0) * clamp_value((std::min(depth, j->ema_ms) - reserve - 15.0) / 925.0, 0.0, 1.0);
// Линейный предел 1.0→1.6 за 500мс PCM. Снижение следует за глубиной буфера с тем же пределом.
double step = (kMaxTempo - 1.0) * max_samples * 1000.0 / (kSampleRate * kTempoRampMs);
j->tempo += clamp_value(target - j->tempo, -step, step);
j->st.setTempo(j->tempo);
bool catching = j->tempo > 1.05;
j->catching_up = catching;
uint32_t decode_errors = 0;
int error_result = 0, error_len = 0;
uint8_t enc[kMaxEncoded];
// Бюджет учитывает ускорение 1.6 и начальное накопление SoundTouch, включая битые пакеты.
int budget = std::min(CALL_JITTER_MAX_FRAMES, 8 + max_samples / 480);
int attempted = 0;
while (j->st.numSamples() < (uint)max_samples && attempted++ < budget) {
int len;
{
std::lock_guard<std::mutex> lock(j->mtx);
if (!j->count) break;
len = j->lens[j->head]; std::memcpy(enc, j->ring[j->head], (size_t)len);
j->head = (j->head + 1) % CALL_JITTER_MAX_FRAMES; j->count--;
}
int n = j->decode(j->arg, enc, len, j->pcm, kFrameSamples);
if (n <= 0 || n > kFrameSamples) {
++decode_errors;
error_result = n; error_len = len;
continue;
}
for (int i = 0; i < n; i++) j->input[i] = j->pcm[i] / 32768.0f;
j->st.putSamples(j->input, (uint)n);
}
j->output.resize((size_t)max_samples);
int n = (int)j->st.receiveSamples(j->output.data(), (uint)max_samples);
for (int i = 0; i < n; i++) out[i] = (int16_t)std::lrintf(clamp_value(j->output[i] * 32767.0f, -32768.0f, 32767.0f));
int processing_ms = (int)std::lround((j->st.numUnprocessedSamples() + j->st.numSamples() * j->tempo) * 1000.0 / kSampleRate);
{
std::lock_guard<std::mutex> lock(j->mtx);
j->processing_ms = processing_ms; j->tempo_x100 = (int)std::lround(j->tempo * 100);
if (n < max_samples) j->underruns++;
j->diagnostics.depth_ms = (int)depth;
j->diagnostics.reserve_ms = reserve;
j->diagnostics.tempo_x100 = j->tempo_x100;
j->diagnostics.decode_errors += decode_errors;
if (decode_errors) { j->diagnostics.error_result = error_result; j->diagnostics.error_len = error_len; }
if (attempted > budget) ++j->diagnostics.budget_hits;
j->diagnostics.resumed |= resumed;
j->diagnostics.catching_up = catching;
j->diagnostics.waiting = n < max_samples;
}
if (n < max_samples) {
j->primed = false;
}
return n;
}
void call_jitter_take_diagnostics(struct call_jitter* j, struct call_jitter_diagnostics* out) {
std::lock_guard<std::mutex> lock(j->mtx);
*out = j->diagnostics;
j->diagnostics.decode_errors = j->diagnostics.budget_hits = 0;
j->diagnostics.resumed = 0;
}
void call_jitter_reset(struct call_jitter* j) {
if (!j) return;
std::lock_guard<std::mutex> lock(j->mtx);
int keep = std::min(j->count, (j->arrival_window.reserve + CALL_JITTER_FRAME_MS - 1) / CALL_JITTER_FRAME_MS);
int discard = j->count - keep;
j->head = (j->head + discard) % CALL_JITTER_MAX_FRAMES;
j->count = keep;
j->dropped += discard;
j->st.clear(); j->st.setTempo(1.0);
j->primed = false; j->catching_up = false;
j->tempo = 1.0; j->ema_ms = j->arrival_window.reserve;
j->processing_ms = 0; j->tempo_x100 = 100;
j->depth_window = {};
j->diagnostics = {};
j->range.min_ms = j->range.max_ms = keep * CALL_JITTER_FRAME_MS;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: device discontinuity discarded=%dms kept=%dms reserve=%dms",
discard * CALL_JITTER_FRAME_MS, keep * CALL_JITTER_FRAME_MS, j->arrival_window.reserve);
}
void call_jitter_get_stats(struct call_jitter* j, int* depth_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range) {
if (!j) return;
std::lock_guard<std::mutex> lock(j->mtx);
if (depth_ms) *depth_ms = j->count * CALL_JITTER_FRAME_MS + j->processing_ms;
if (tempo_x100) *tempo_x100 = j->tempo_x100;
if (range) *range = j->range;
if (dropped) *dropped = j->dropped;
if (underruns) *underruns = j->underruns;
}
}

39
src/call/call_jitter.h

@ -1,39 +0,0 @@
#ifndef CALL_JITTER_H
#define CALL_JITTER_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Звонок: FIFO Opus, запас 60мс..max_reserve_ms по изменению задержки доставки за 10 секунд и догон после паузы устройства.
* push — сетевой поток, pull/decode/SoundTouch — единственный аудиопоток.
* destroy — после остановки обоих потребителей. get_stats — любой поток. */
#define CALL_JITTER_FRAME_MS 20
#define CALL_JITTER_MAX_FRAMES 750 /* 10с перебоя + 5с запаса на накопление и восстановление */
typedef int (*call_jitter_decode_fn)(void*, const uint8_t*, int, int16_t*, int);
struct call_jitter_range { int min_ms, max_ms, reserve_ms; };
struct call_jitter;
struct call_jitter_diagnostics {
int depth_ms, reserve_ms, tempo_x100, error_result, error_len;
uint32_t decode_errors, budget_hits;
int resumed, catching_up, waiting;
};
/* Снимок и сброс событий под mutex; логировать после выхода из mutex голосового стека. */
void call_jitter_take_diagnostics(struct call_jitter*, struct call_jitter_diagnostics*);
struct call_jitter* call_jitter_create(call_jitter_decode_fn decode, void* arg, int max_reserve_ms);
void call_jitter_destroy(struct call_jitter* j);
/* sent_ms — timestamp кадра на проводе (uint32 с переполнением), received_ms — локальные монотонные часы.
* Абсолютное смещение часов пиров не влияет на оценку джиттера. */
void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len, uint32_t sent_ms, int64_t received_ms);
/* Только после остановки pull: убрать устаревший PCM, сохранить свежий сетевой запас. */
void call_jitter_reset(struct call_jitter* j);
int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples);
void call_jitter_get_stats(struct call_jitter* j, int* depth_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range);
#ifdef __cplusplus
}
#endif
#endif

34
src/call/call_jitter_window.h

@ -1,34 +0,0 @@
#pragma once
#include <algorithm>
#include <cstdint>
// Десять секундных интервалов. Отдельные экземпляры для глубины очереди и задержки доставки.
struct call_jitter_window {
struct bucket { int64_t second = -1, low = 0, high = 0; } buckets[10];
int64_t low = 0, high = 0;
int reserve = 60;
int max_reserve = 1000;
int64_t last_received_ms = -1, transit_ms = 0;
uint32_t last_sent_ms = 0;
bool observe(int64_t now_ms, int64_t depth) {
int64_t second = now_ms / 1000;
auto& b = buckets[second % 10];
bool changed_second = b.second != second;
if (changed_second) { b.second = second; b.low = b.high = depth; }
else { b.low = std::min(b.low, depth); b.high = std::max(b.high, depth); }
low = high = depth;
for (const auto& item : buckets) {
if (item.second < 0 || item.second > second || second - item.second >= 10) continue;
low = std::min(low, item.low); high = std::max(high, item.high);
}
reserve = (int)std::max<int64_t>(60, std::min<int64_t>(max_reserve, high - low));
return changed_second;
}
// Разность интервалов прихода и отправки: смещение часов исключается, uint32 timestamp разворачивается.
bool observe_arrival(int64_t received_ms, uint32_t sent_ms) {
if (last_received_ms >= 0) transit_ms += received_ms - last_received_ms - (int32_t)(sent_ms - last_sent_ms);
last_received_ms = received_ms; last_sent_ms = sent_ms;
return observe(received_ms, transit_ms);
}
};

98
src/call/call_proto.h

@ -1,98 +0,0 @@
// call_proto.h — wire-формат и константы P2P аудио-звонка (svc_id ETCP_RT_ID_CALL 0x36)
//
// Приём/отправка через etcp_router. На проводе первый байт payload = subcmd.
// Сигналинг идёт с ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT, медиа — без подписи
// (уже внутри шифрованного канала etcp_router).
#ifndef CALL_PROTO_H
#define CALL_PROTO_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* ── подкоманды (первый байт payload после svc_id) ── */
enum {
CALL_SUBCMD_INVITE = 0x01, /* {call_id:8, group_id:8, tracks:[]} */
CALL_SUBCMD_RINGING = 0x02, /* {call_id:8} */
CALL_SUBCMD_ACCEPT = 0x03, /* {call_id:8} */
CALL_SUBCMD_DECLINE = 0x04, /* {call_id:8, reason:1} */
CALL_SUBCMD_BUSY = 0x05, /* {call_id:8} */
CALL_SUBCMD_CANCEL = 0x06, /* {call_id:8} */
CALL_SUBCMD_HANGUP = 0x07, /* {call_id:8, reason:1} */
CALL_SUBCMD_MEDIA = 0x10, /* {call_id:8, track_id:1, seq:2, ts_ms:4, payload[]} */
};
/* ── треки и кодеки (модель расширяема под видео) ── */
enum { CALL_TRACK_AUDIO = 0x01, CALL_TRACK_VIDEO = 0x02 };
enum { CALL_CODEC_OPUS = 0x01 };
/* ── причины завершения (ENDED) ── */
enum {
CALL_REASON_LOCAL_HANGUP = 0,
CALL_REASON_REMOTE_HANGUP = 1,
CALL_REASON_DECLINE = 2,
CALL_REASON_BUSY = 3,
CALL_REASON_CANCEL = 4,
CALL_REASON_NO_TRAFFIC = 5,
CALL_REASON_RING_TIMEOUT = 6,
CALL_REASON_CONNECT_FAIL = 7,
};
#pragma pack(push, 1)
struct call_pkt_track {
uint8_t type; /* CALL_TRACK_AUDIO / CALL_TRACK_VIDEO */
uint8_t codec; /* CALL_CODEC_OPUS */
uint32_t sample_rate;
uint8_t channels;
uint32_t bitrate;
};
struct call_pkt_invite {
uint8_t subcmd; /* CALL_SUBCMD_INVITE */
uint64_t call_id;
uint64_t group_id;
uint8_t num_tracks;
struct call_pkt_track tracks[1]; /* tracks[num_tracks] */
};
struct call_pkt_ringing { uint8_t subcmd; uint64_t call_id; };
struct call_pkt_accept { uint8_t subcmd; uint64_t call_id; };
struct call_pkt_decline { uint8_t subcmd; uint64_t call_id; uint8_t reason; };
struct call_pkt_busy { uint8_t subcmd; uint64_t call_id; };
struct call_pkt_cancel { uint8_t subcmd; uint64_t call_id; };
struct call_pkt_hangup { uint8_t subcmd; uint64_t call_id; uint8_t reason; };
struct call_pkt_media {
uint8_t subcmd; /* CALL_SUBCMD_MEDIA */
uint64_t call_id;
uint8_t track_id;
uint16_t seq;
uint32_t ts_ms;
/* payload[] */
};
#pragma pack(pop)
#define CALL_MEDIA_HDR_SIZE sizeof(struct call_pkt_media) /* 16 */
/* ── параметры аудио (Opus 48kHz mono, 20ms) ── */
#define CALL_OPUS_SAMPLE_RATE 48000
#define CALL_OPUS_FRAME_MS 20
#define CALL_OPUS_FRAME_SAMPLES (CALL_OPUS_SAMPLE_RATE * CALL_OPUS_FRAME_MS / 1000) /* 960 */
/* ── таймауты и ёмкости (timebase 0.1ms) ── */
#define CALL_RING_TIMEOUT_TB 450000 /* 45с — вызывающий: нет ответа → CANCEL */
#define CALL_INVITE_RETRY_TB 3000 /* 300мс — кодограмма INVITE (OUTGOING + RINGING) */
#define CALL_RING_DEAD_TB 20000 /* 2с — вызываемый: нет кодограммы → вызов сорван */
#define CALL_NO_ROUTE_MAX_STRIKES 20 /* 20 × 300мс ≈ 6с без маршрута → fail-fast CONNECT_FAIL */
#define CALL_NO_TRAFFIC_TB 200000 /* 20с — нет входящих медиа → HANGUP */
#define CALL_TEARDOWN_DELAY_TB 5000 /* 500мс — задержка разрыва conn_mgr после завершения */
#define CALL_MAX_TX_FRAMES 750 /* 15с: 10с перебоя + 5с запаса */
#ifdef __cplusplus
}
#endif
#endif /* CALL_PROTO_H */

165
src/call/call_tones.c

@ -1,165 +0,0 @@
// call_tones.c — генератор тонов звонка (48kHz mono int16).
//
// Глитч (сталл >500мс): 1000Гц, паттерн из doc/chat_call_arch.md §8 —
// пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс) пауза(200мс) пи(200мс),
// цикл 2с, повторяется пока длится сталл.
// Завершение: 3 нисходящих тона 700/500/400 Гц × 150мс (один раз, 450мс).
//
// Генерация — синус с коротким fade in/out (без кликов). Только аудио-поток.
#include "call_tones.h"
#include <math.h>
#include <string.h>
#include <stdlib.h>
#define TONES_SAMPLE_RATE 48000
#define TONES_AMPLITUDE 0.25f /* от полной шкалы int16 */
#define TONES_FADE_SAMPLES (TONES_SAMPLE_RATE * 5 / 1000) /* 5мс */
enum tones_mode { TONES_NONE = 0, TONES_GLITCH, TONES_ENDED };
struct tone_seg {
int tone; /* 1 = звук, 0 = тишина */
int dur_ms;
int freq_hz;
};
static const struct tone_seg GLITCH_SEGS[] = {
{1, 150, 1000}, {0, 100, 0}, {1, 150, 1000}, {0, 600, 0},
{1, 200, 1000}, {0, 200, 0}, {1, 200, 1000}, {0, 400, 0},
};
#define GLITCH_SEG_COUNT ((int)(sizeof(GLITCH_SEGS) / sizeof(GLITCH_SEGS[0])))
static const struct tone_seg ENDED_SEGS[] = {
{1, 150, 700}, {1, 150, 500}, {1, 150, 400},
};
#define ENDED_SEG_COUNT ((int)(sizeof(ENDED_SEGS) / sizeof(ENDED_SEGS[0])))
struct call_tones {
enum tones_mode mode;
const struct tone_seg* segs;
int seg_count;
int seg_idx;
int seg_pos; /* сэмплов отыграно в текущем сегменте */
int ended_done;
double phase; /* фаза синуса (радианы) */
};
struct call_tones* call_tones_create(void) {
struct call_tones* t = (struct call_tones*)calloc(1, sizeof(*t));
if (!t) return NULL;
t->mode = TONES_NONE;
t->segs = NULL;
t->seg_count = 0;
t->seg_idx = 0;
t->seg_pos = 0;
t->ended_done = 0;
t->phase = 0.0;
return t;
}
void call_tones_destroy(struct call_tones* t) {
free(t);
}
void call_tones_reset(struct call_tones* t) {
if (!t) return;
t->mode = TONES_NONE;
t->segs = NULL;
t->seg_count = 0;
t->seg_idx = 0;
t->seg_pos = 0;
t->ended_done = 0;
t->phase = 0.0;
}
void call_tones_glitch_start(struct call_tones* t) {
if (!t || t->mode == TONES_GLITCH) return;
t->mode = TONES_GLITCH;
t->segs = GLITCH_SEGS;
t->seg_count = GLITCH_SEG_COUNT;
t->seg_idx = 0;
t->seg_pos = 0;
t->phase = 0.0;
}
void call_tones_glitch_stop(struct call_tones* t) {
if (!t || t->mode != TONES_GLITCH) return;
t->mode = TONES_NONE;
t->segs = NULL;
t->seg_count = 0;
t->seg_idx = 0;
t->seg_pos = 0;
t->phase = 0.0;
}
void call_tones_ended_start(struct call_tones* t) {
if (!t) return;
t->mode = TONES_ENDED;
t->segs = ENDED_SEGS;
t->seg_count = ENDED_SEG_COUNT;
t->seg_idx = 0;
t->seg_pos = 0;
t->ended_done = 0;
t->phase = 0.0;
}
int call_tones_ended_pending(struct call_tones* t) {
return t && t->mode == TONES_ENDED && !t->ended_done;
}
/* Перейти к следующему сегменту (или завершить тон). */
static void tones_advance(struct call_tones* t) {
t->seg_idx++;
t->seg_pos = 0;
t->phase = 0.0;
if (t->seg_idx >= t->seg_count) {
if (t->mode == TONES_GLITCH) {
t->seg_idx = 0; /* цикл глитча */
} else if (t->mode == TONES_ENDED) {
t->ended_done = 1;
t->mode = TONES_NONE;
t->segs = NULL;
}
}
}
int call_tones_render(struct call_tones* t, int16_t* out, int max_samples) {
if (!t || !out || max_samples <= 0) return 0;
int written = 0;
while (written < max_samples) {
if (t->mode == TONES_NONE) {
out[written++] = 0; /* тишина до конца запроса */
continue;
}
const struct tone_seg* s = &t->segs[t->seg_idx];
int seg_len = s->dur_ms * TONES_SAMPLE_RATE / 1000;
int remain = seg_len - t->seg_pos;
int n = max_samples - written;
if (n > remain) n = remain;
for (int i = 0; i < n; i++) {
int pos = t->seg_pos + i;
float v = 0.0f;
if (s->tone) {
v = (float)sin(t->phase) * TONES_AMPLITUDE * 32767.0f;
t->phase += 2.0 * M_PI * (double)s->freq_hz / TONES_SAMPLE_RATE;
/* fade in/out по краям сегмента (без кликов) */
int fade = pos < TONES_FADE_SAMPLES ? pos
: (seg_len - pos - 1) < TONES_FADE_SAMPLES ? (seg_len - pos - 1)
: TONES_FADE_SAMPLES;
if (fade < TONES_FADE_SAMPLES)
v *= (float)fade / (float)TONES_FADE_SAMPLES;
}
int iv = (int)v;
out[written + i] = (int16_t)(iv > 32767 ? 32767 : (iv < -32768 ? -32768 : iv));
}
written += n;
t->seg_pos += n;
if (t->seg_pos >= seg_len) tones_advance(t);
}
return written;
}

32
src/call/call_tones.h

@ -1,32 +0,0 @@
#ifndef CALL_TONES_H
#define CALL_TONES_H
// call_tones.h — генератор тонов звонка (48kHz mono int16): глитч при сталле
// и тон завершения. Используется только аудио-потоком (single-writer).
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct call_tones;
struct call_tones* call_tones_create(void);
void call_tones_destroy(struct call_tones* t);
void call_tones_reset(struct call_tones* t); /* в тишину */
void call_tones_glitch_start(struct call_tones* t); /* запустить/продолжить цикл глитча */
void call_tones_glitch_stop(struct call_tones* t); /* прекратить глитч (тишина) */
void call_tones_ended_start(struct call_tones* t); /* 3 нисходящих тона, один раз */
int call_tones_ended_pending(struct call_tones* t); /* 1 = тон завершения ещё звучит */
/* Выдать до max_samples сэмплов (тон или тишина). Возвращает записанное число. */
int call_tones_render(struct call_tones* t, int16_t* out, int max_samples);
#ifdef __cplusplus
}
#endif
#endif /* CALL_TONES_H */

54
src/call/voice_sources.cmake

@ -1,54 +0,0 @@
# voice_sources.cmake — голосовой стек звонка (единый для desktop/Android/headless).
#
# SoundTouch (time-stretch) + call_jitter + radio_jitter (кодированные кадры + decode-on-pull)
# + call_audio (Opus/тоны) + call_tones. C++ внутри, наружу — C-интерфейс
# (call_audio.h). Экспортирует UTUN_VOICE_SOURCES и UTUN_VOICE_INCLUDE_DIRS.
#
# Использование:
# set(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../lib)
# set(CALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/call)
# include(${CALL_DIR}/voice_sources.cmake)
# utun_setup_voice("${LIB_DIR}" "${CALL_DIR}")
# add_library(utun_voice STATIC ${UTUN_VOICE_SOURCES})
# target_include_directories(utun_voice PUBLIC ${UTUN_VOICE_INCLUDE_DIRS})
# target_link_libraries(utun_voice PUBLIC opus) # opus_codec
function(utun_setup_voice LIB_DIR CALL_DIR)
set(_st_dir "${LIB_DIR}/soundtouch-master")
set(_src_dir "${CALL_DIR}/..")
set(UTUN_VOICE_SOURCES
${CALL_DIR}/call_audio.c
${CALL_DIR}/call_tones.c
${CALL_DIR}/call_jitter.cpp
${_src_dir}/radio/radio_jitter.cpp
${_src_dir}/radio/radio_audio.c
${_st_dir}/source/SoundTouch/SoundTouch.cpp
${_st_dir}/source/SoundTouch/TDStretch.cpp
${_st_dir}/source/SoundTouch/RateTransposer.cpp
${_st_dir}/source/SoundTouch/FIFOSampleBuffer.cpp
${_st_dir}/source/SoundTouch/AAFilter.cpp
${_st_dir}/source/SoundTouch/FIRFilter.cpp
${_st_dir}/source/SoundTouch/InterpolateLinear.cpp
${_st_dir}/source/SoundTouch/InterpolateCubic.cpp
${_st_dir}/source/SoundTouch/InterpolateShannon.cpp
${_st_dir}/source/SoundTouch/cpu_detect_x86.cpp
${_st_dir}/source/SoundTouch/mmx_optimized.cpp
${_st_dir}/source/SoundTouch/sse_optimized.cpp
PARENT_SCOPE
)
set(UTUN_VOICE_INCLUDE_DIRS
${CALL_DIR}
${_src_dir}
${_src_dir}/transport_layer
${_src_dir}/routing_layer
${_src_dir}/chat
${_src_dir}/proxy
${_src_dir}/media_delivery
${_src_dir}/media_async
${LIB_DIR}
${LIB_DIR}/libopus/include
${_st_dir}/include
${_st_dir}/include/soundtouch
PARENT_SCOPE
)
endfunction()

6
src/chat/chat_channel.c

@ -72,7 +72,9 @@ void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_i
}
if (si) {
si_register(inst, si, ch_id);
db_sync_set_insert_cb(si, on_msg_inserted, inst);
struct msg_insert_arg* ia = u_malloc(sizeof(*ia));
if (ia) { ia->inst = inst; snprintf(ia->ch_id, sizeof(ia->ch_id), "%s", ch_id); }
db_sync_set_insert_cb(si, on_msg_inserted, ia);
db_sync_instance_set_gated(si, 1);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel ready ch=%s tbl=%s group=0x%016llx",
CC_ID, ch_id, tbl_msg, (unsigned long long)gid);
@ -135,6 +137,8 @@ void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_cr
if (mrc < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d",
CC_ID, req->channel_id, mrc);
} else if (mrc & MS_APPLY_CHANGED) {
member_sync_broadcast_one(inst, req->channel_id, myid);
}
/* адреса + update_ts + подпись + синк — единой функцией */
chat_core_update_my_member(inst);

176
src/chat/chat_core.c

@ -15,7 +15,6 @@
#include "chat_setting.h"
#include "chat_member.h"
#include "member_sync.h"
#include "chat_sync.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
@ -80,7 +79,6 @@ void si_register(struct UTUN_INSTANCE* inst, struct DB_SYNC_INSTANCE* si, const
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
if (!inst || !db_path) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid args inst=%p db_path=%p", CC_ID, (void*)inst, (void*)db_path); return -1; }
if (inst->chat_core) return 0;
struct chat_core_ctx* cc = u_calloc(1, sizeof(*cc));
if (!cc) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: OOM for chat_core_ctx", CC_ID); return -1; }
inst->chat_core = cc;
@ -236,7 +234,6 @@ void chat_core_destroy(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) return;
cc->initialized = 0;
member_sync_destroy(inst); /* Also releases callback lists after partial startup. */
if (cc->db && !cc->shared_db) { sqlite3_close(cc->db); }
inst->chat_core = NULL;
@ -244,8 +241,8 @@ void chat_core_destroy(struct UTUN_INSTANCE* inst) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CC_ID);
}
sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst) { return (inst && CC(inst)) ? CC(inst)->db : NULL; }
int chat_core_is_initialized(struct UTUN_INSTANCE* inst) { return (inst && CC(inst)) ? CC(inst)->initialized : 0; }
sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst) { return CC(inst) ? CC(inst)->db : NULL; }
int chat_core_is_initialized(struct UTUN_INSTANCE* inst) { return CC(inst) ? CC(inst)->initialized : 0; }
/* ─── chat settings (GUI → uasync) ─── */
@ -262,60 +259,12 @@ void chat_core_set_setting_trampoline(void* arg) {
u_free(arg);
}
/* ─── per-channel «play sound on new message» ─── */
int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return 1;
char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch_id);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) != SQLITE_OK) return 1;
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
int on = 1;
if (sqlite3_step(st) == SQLITE_ROW) {
const char* v = (const char*)sqlite3_column_text(st, 0);
if (v) on = atoi(v) != 0;
}
sqlite3_finalize(st);
return on;
}
void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return;
char key[128]; snprintf(key, sizeof(key), "ch_%s_sound_on_message", ch_id);
char val[2]; snprintf(val, sizeof(val), "%d", on ? 1 : 0);
chat_core_save_ui_state(inst, key, val);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: sound_on_message ch=%s = %d", CC_ID, ch_id, on ? 1 : 0);
chat_sync_broadcast_sound_setting(inst, ch_id, on ? 1 : 0);
}
void chat_core_set_sound_on_message_trampoline(void* arg) {
struct chat_sound_arg* a = (struct chat_sound_arg*)arg;
if (!a) return;
chat_core_set_sound_on_message(a->inst, a->ch_id, a->on);
u_free(arg);
}
/* ─── headless API ─── */
static void json_escape(const char* src, char* dst, size_t dst_sz) {
size_t i = 0; const char* s = src;
while (*s && i < dst_sz - 2) {
const char* escape = NULL;
switch (*s) {
case '"': escape = "\\\""; break;
case '\\': escape = "\\\\"; break;
case '\n': escape = "\\n"; break;
case '\r': escape = "\\r"; break;
case '\t': escape = "\\t"; break;
case '\b': escape = "\\b"; break;
case '\f': escape = "\\f"; break;
}
if (escape) {
if (i + 2 >= dst_sz) break;
dst[i++] = escape[0]; dst[i++] = escape[1]; s++; continue;
}
if (*s == '"' || *s == '\\') { if (i + 1 < dst_sz - 2) dst[i++] = '\\'; }
dst[i++] = *s++;
}
dst[i] = '\0';
@ -418,8 +367,7 @@ int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, i
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !buf || !out_len || buf_size < 3) return -1;
*out_len = 0;
if (!cc || !cc->initialized || !ch_id || !buf || !out_len) return -1;
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
char* w = buf; char* end = buf + buf_size;
*w++ = '['; const char* sep = "";
@ -428,12 +376,6 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
"SELECT p.node_id, n.name, n.x25519_pubkey, n.ed25519_pubkey, p.adm_tags "
"FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id", peers_tbl);
sqlite3_stmt* st = NULL;
sqlite3_stmt* as = NULL;
#define MEMBERS_APPEND(...) do { \
int written = snprintf(w, (size_t)(end - w), __VA_ARGS__); \
if (written < 0 || (size_t)written >= (size_t)(end - w)) goto overflow; \
w += written; \
} while (0)
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
@ -444,7 +386,7 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
int x25519_len = sqlite3_column_bytes(st, 2);
int ed25519_len = sqlite3_column_bytes(st, 3);
const char* adm_tags = (const char*)sqlite3_column_text(st, 4);
char esc_name[256], x25519_hex[65], ed25519_hex[65], esc_tags[6 * MS_ADM_TAGS_MAX + 1];
char esc_name[256], x25519_hex[65], ed25519_hex[65], esc_tags[384];
json_escape(name ? name : "", esc_name, sizeof(esc_name));
x25519_hex[0] = ed25519_hex[0] = '\0';
if (x25519 && x25519_len >= 32) for (int i = 0; i < 32; i++) snprintf(x25519_hex + i*2, 3, "%02x", x25519[i]);
@ -459,13 +401,14 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
json_escape(adm_tags ? adm_tags : "", esc_tags, sizeof(esc_tags));
MEMBERS_APPEND(
w += snprintf(w, (size_t)(end - w),
"%s{\"node_id\":\"0x%016llx\",\"name\":\"%s\",\"online\":%d,\"connected\":%d,"
"\"adm_tags\":\"%s\",\"x25519\":\"%s\",\"ed25519\":\"%s\",\"addrs\":[",
sep, (unsigned long long)nid, esc_name, online, connected, esc_tags, x25519_hex, ed25519_hex);
sep = ",";
/* addresses */
sqlite3_stmt* as = NULL;
char asql[200]; snprintf(asql, sizeof(asql),
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=? ORDER BY family, protocol");
if (sqlite3_prepare_v2(cc->db, asql, -1, &as, NULL) == SQLITE_OK) {
@ -486,24 +429,20 @@ int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, ch
addr[0],addr[1],addr[2],addr[3],addr[4],addr[5],addr[6],addr[7],
addr[8],addr[9],addr[10],addr[11],addr[12],addr[13],addr[14],addr[15]);
else { snprintf(ip_str, sizeof(ip_str), "?"); }
MEMBERS_APPEND(
w += snprintf(w, (size_t)(end - w),
"%s{\"ip\":\"%s\",\"port\":%d,\"proto\":\"%s\",\"rtt\":%d}",
asep2, ip_str, port, proto == 1 ? "UDP" : proto == 2 ? "TCP" : "?", rtt);
asep2 = ",";
}
sqlite3_finalize(as); as = NULL;
sqlite3_finalize(as);
}
MEMBERS_APPEND("]}");
w += snprintf(w, (size_t)(end - w), "]}");
}
MEMBERS_APPEND("]");
sqlite3_finalize(st);
*w++ = ']';
*w = '\0';
*out_len = (size_t)(w - buf);
return 0;
overflow:
sqlite3_finalize(as); sqlite3_finalize(st); buf[0] = 0;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: members JSON exceeds output buffer bytes=%zu", CC_ID, buf_size);
return -1;
#undef MEMBERS_APPEND
}
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) {
@ -525,7 +464,7 @@ int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char*
struct chat_member_tags_key {
char key[CHAT_MEMBER_TAGS_KEY_SZ];
char* val;
char val[CHAT_MEMBER_TAGS_VAL_SZ];
char* raw; /* malloc'd: serialized append-only string (admin/moder) or NULL (= val is raw) */
};
@ -555,19 +494,20 @@ static int tags_is_append_key(const char* key) {
return strcmp(key, "admin") == 0 || strcmp(key, "moder") == 0;
}
static void tags_set_effective_val(struct chat_member_tags_key* k, const char* raw) {
uint64_t ts = 0;
const char* eff = tags_append_effective(raw, &ts);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", eff);
(void)ts;
}
static int tags_json_cb(const char* key, const char* json_val, void* arg) {
struct chat_member_tags* t = (struct chat_member_tags*)arg;
if (t->key_count >= CHAT_MEMBER_TAGS_MAX_KEYS || strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON key limit exceeded", CC_ID); return -1;
}
if (t->key_count >= CHAT_MEMBER_TAGS_MAX_KEYS) return -1;
struct chat_member_tags_key* k = &t->keys[t->key_count];
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
k->val = u_strdup(tags_is_append_key(key) ? tags_append_effective(json_val, NULL) : json_val);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", json_val);
k->raw = tags_is_append_key(key) ? u_strdup(json_val) : NULL;
if (!k->val || (tags_is_append_key(key) && !k->raw)) {
u_free(k->val); u_free(k->raw); memset(k, 0, sizeof(*k));
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags JSON allocation failed", CC_ID); return -1;
}
t->key_count++;
return 0;
}
@ -587,9 +527,7 @@ struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(st, 0);
if (tags && tags[0] && chat_member_tags_replace(t, tags) < 0) {
sqlite3_finalize(st); chat_member_tags_free(t); return NULL;
}
if (tags && tags[0]) chat_member_tags_replace(t, tags);
}
sqlite3_finalize(st);
}
@ -601,24 +539,22 @@ struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val) {
if (!t || !key) return -1;
if (strlen(key) >= CHAT_MEMBER_TAGS_KEY_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — key too long", CC_ID); return -1; }
if (val && strlen(val) > MS_ADM_TAGS_MAX) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — val too long", CC_ID); return -1; }
if (val && strlen(val) >= CHAT_MEMBER_TAGS_VAL_SZ) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — val too long", CC_ID); return -1; }
int idx = tags_find(t, key);
if (!val) {
if (idx >= 0) { u_free(t->keys[idx].raw); u_free(t->keys[idx].val); t->keys[idx] = t->keys[--t->key_count]; }
if (idx >= 0) { u_free(t->keys[idx].raw); t->keys[idx] = t->keys[--t->key_count]; }
return 0;
}
char* copy = u_strdup(val);
if (!copy) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set allocation failed", CC_ID); return -1; }
if (idx >= 0) {
u_free(t->keys[idx].val); t->keys[idx].val = copy;
snprintf(t->keys[idx].val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", val);
} else if (t->key_count < CHAT_MEMBER_TAGS_MAX_KEYS) {
struct chat_member_tags_key* k = &t->keys[t->key_count++];
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
k->val = copy;
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", val);
k->raw = NULL;
} else {
u_free(copy); DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — max keys reached", CC_ID); return -1;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_set — max keys reached", CC_ID); return -1;
}
return 0;
}
@ -630,17 +566,11 @@ const char* chat_member_tags_get(struct chat_member_tags* t, const char* key) {
}
int chat_member_tags_replace(struct chat_member_tags* t, const char* json) {
if (!t || !json || strlen(json) > MS_ADM_TAGS_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace invalid input or size", CC_ID); return -1;
}
struct chat_member_tags parsed = {0};
int rc = json_flat_parse(json, tags_json_cb, &parsed);
if (rc != 0) {
for (int i = 0; i < parsed.key_count; i++) { u_free(parsed.keys[i].raw); u_free(parsed.keys[i].val); }
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace — invalid JSON", CC_ID); return -1;
}
for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); }
t->key_count = parsed.key_count; memcpy(t->keys, parsed.keys, sizeof(t->keys));
if (!t || !json) return -1;
for (int i = 0; i < t->key_count; i++) u_free(t->keys[i].raw);
t->key_count = 0;
int rc = json_flat_parse(json, tags_json_cb, t);
if (rc != 0) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace — invalid JSON", CC_ID); return -1; }
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_replace ch=%s nid=0x%016llx keys=%d",
CC_ID, t->ch_id, (unsigned long long)t->node_id, t->key_count);
return 0;
@ -651,14 +581,8 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
if (!t || !cc || !cc->initialized || !cc->inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — invalid state", CC_ID); return -1;
}
char json[MS_ADM_TAGS_MAX + 1]; int off = 0, ver = 1;
#define TAGS_ADVANCE(expr) do { \
int written = (expr); \
if (written < 0 || (size_t)written >= sizeof(json) - (size_t)off) { \
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit JSON exceeds %d bytes", CC_ID, MS_ADM_TAGS_MAX); return -1; \
} \
off += written; \
} while (0)
char json[4096]; int off = 0, ver = 1;
#define TAGS_ADVANCE(n) do { if (off < (int)sizeof(json) - 10) off += (int)(n); else off = (int)sizeof(json); } while(0)
int vi = tags_find(t, "ver");
if (vi >= 0) ver = atoi(t->keys[vi].val) + 1;
TAGS_ADVANCE(snprintf(json, sizeof(json), "{\"ver\":\"%d\"", ver));
@ -687,13 +611,7 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
} else {
out_val = k->val;
}
char escaped_key[6 * CHAT_MEMBER_TAGS_KEY_SZ + 1], escaped_value[6 * MS_ADM_TAGS_MAX + 1];
if (strlen(out_val) > MS_ADM_TAGS_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit value exceeds limit", CC_ID); return -1;
}
json_escape(k->key, escaped_key, sizeof(escaped_key));
json_escape(out_val, escaped_value, sizeof(escaped_value));
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, ",\"%s\":\"%s\"", escaped_key, escaped_value));
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, ",\"%s\":\"%s\"", k->key, out_val));
}
TAGS_ADVANCE(snprintf(json + off, sizeof(json) - (size_t)off, "}"));
#undef TAGS_ADVANCE
@ -702,9 +620,10 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
if (topo_node_sqlite_channel_get_priv(cc->db, t->ch_id, ch_ed_priv) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — no channel privkey for ch=%s (not owner?)", CC_ID, t->ch_id); return -1;
}
uint8_t sign_msg[MS_ADM_TAGS_MAX + 8];
int mlen = member_sync_build_owner_msg(t->node_id, json, sign_msg, sizeof(sign_msg));
if (mlen < 0) return -1;
uint8_t sign_msg[8192]; size_t mlen = (size_t)off;
if (mlen + 8 > sizeof(sign_msg)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — json too big", CC_ID); return -1; }
memcpy(sign_msg, json, mlen);
memcpy(sign_msg + mlen, &t->node_id, 8); mlen += 8;
uint8_t sig[64];
if (sc_ed25519_sign(ch_ed_priv, sign_msg, mlen, sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — sign failed", CC_ID); return -1;
@ -713,7 +632,7 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
char peers_tbl[80]; peers_table_name(t->ch_id, peers_tbl, sizeof(peers_tbl));
sqlite3_stmt* st = NULL;
char sql[384]; snprintf(sql, sizeof(sql),
"SELECT x25519_pubkey, ed25519_pubkey, join_sig, join_ts, update_sig, update_ts, userinfo, storage, source"
"SELECT x25519_pubkey, ed25519_pubkey, join_sig, join_ts, update_sig, update_ts, userinfo, storage"
" FROM \"%s\" WHERE node_id=?", peers_tbl);
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member query failed", CC_ID); return -1;
@ -730,15 +649,9 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
uint64_t uts = (uint64_t)sqlite3_column_int64(st, 5);
const char* uinfo = (const char*)sqlite3_column_text(st, 6);
int stg = sqlite3_column_int(st, 7);
int src = sqlite3_column_int(st, 8);
if (!x25 || !ed) { sqlite3_finalize(st);
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member has no keys ch=%s", CC_ID, t->ch_id); return -1;
}
if (src != 0) { sqlite3_finalize(st);
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — cannot set flags on placeholder member (source=%d) ch=%s nid=0x%016llx",
CC_ID, src, t->ch_id, (unsigned long long)t->node_id);
return -1;
}
const char* stg_val = chat_member_tags_get(t, "storage");
if (stg_val && strcmp(stg_val, "yes") == 0) stg = 1;
@ -746,15 +659,16 @@ int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags*
uinfo ? uinfo : "", json, sig, stg, 0, NULL);
sqlite3_finalize(st);
if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member_sync_put failed rc=%d", CC_ID, rc); return -1; }
if (rc & MS_APPLY_CHANGED) member_sync_broadcast_one(inst, t->ch_id, t->node_id);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d bytes=%d",
CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, off);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d json=%s",
CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, json);
return 0;
}
void chat_member_tags_free(struct chat_member_tags* t) {
if (!t) return;
for (int i = 0; i < t->key_count; i++) { u_free(t->keys[i].raw); u_free(t->keys[i].val); }
for (int i = 0; i < t->key_count; i++) u_free(t->keys[i].raw);
u_free(t);
}

70
src/chat/chat_core.h

@ -1,7 +1,5 @@
/*
* chat_core — API каналов, сообщений, профиля и настроек одного узла.
* chat_service_start/stop управляют всем чат-сервисом поверх работающего ядра.
* Данные хранятся в общей SQLite; события для интерфейса доставляет chat_event.
* chat_core.h — центральный API чата в потоке uasync
*
* Все DB-операции и сетевые функции выполняются в uasync-потоке.
* Все функции per-instance: первым аргументом struct UTUN_INSTANCE*.
@ -21,16 +19,7 @@ struct UTUN_INSTANCE;
/* ── Жизненный цикл ── */
/* Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1. */
int chat_service_start(struct UTUN_INSTANCE* inst);
/* Остановить чат, сохранив ядро, UTUN и БД. Повтор допустим; вызывать вне callbacks чата.
* Отменяет доставку и ждёт native media workers; длительная задача может задержать stop. */
void chat_service_stop(struct UTUN_INSTANCE* inst);
/* Внутренняя часть запуска: открыть/подключить БД и загрузить каналы. 0/-1.
* Для запуска всего чат-сервиса использовать chat_service_start(). */
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path);
/* Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра. */
void chat_core_destroy(struct UTUN_INSTANCE* inst);
/* Backfill: регистрирует уже скачанные медиафайлы (лежат на диске, но отсутствуют
@ -42,36 +31,33 @@ void chat_media_backfill(struct UTUN_INSTANCE* inst);
void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst);
/* Регистрирует триггер: после первого SYNC_DONE (+ дебаунс 2с) анонсирует локальные
* блоки суперузлам и запускает chat_media_autodownload_backfill. Один раз за запуск чата. */
* блоки суперузлам и запускает chat_media_autodownload_backfill. Один раз за процесс. */
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst);
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst);
/* Заимствованная БД текущего chat_core; NULL до init/после destroy. */
struct sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst);
/* 1 — chat_core инициализирован, 0 — нет; не проверяет доступность сети. */
int chat_core_is_initialized(struct UTUN_INSTANCE* inst);
/* ── Отправка сообщения (GUI → uasync) ── */
struct chat_msg_submit {
struct UTUN_INSTANCE* inst;
char channel_id[64]; /* десятичный ID канала */
char content_type[32]; /* тип содержимого сообщения */
char media_src[1024]; /* исходный файл вложения */
char media_dest[1024]; /* путь подготовленного локального файла */
uint8_t media_copy; /* 1 = скопировать исходный файл при регистрации */
char channel_id[64];
char content_type[32];
char media_src[1024];
char media_dest[1024];
uint8_t media_copy;
uint8_t media_video; /* 1 = подготовить как видео (пробинг+транскод) */
int duration_ms; /* видео: длительность (0 = неизвестно) */
int width; /* видео: ширина */
int height; /* видео: высота */
uint8_t* data; /* содержимое сообщения, заимствовано обычным submit */
uint32_t data_len; /* размер data в байтах */
uint64_t timestamp; /* входное время; отправка назначает новый timestamp через db_sync */
uint8_t* data;
uint32_t data_len;
uint64_t timestamp;
uint64_t reply_to_ts; /* 0 = не ответ */
uint64_t reply_to_node_id; /* node_id автора исходного сообщения */
};
/* Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. */
void chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
/* Отправка медиа-сообщения (вложение): src->dest (copy) или видео (transcode). */
@ -79,18 +65,10 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
/* ── DB-операции (оставлены для интроспекции) ── */
/* Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса. */
uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Хеш записи по позиции с нуля в порядке (timestamp, node_id); hash_out — 32 байта.
* 0 = запрос выполнен (нет записи — нулевой хеш), -1 = ошибка. */
int chat_core_chain_hash_at(struct UTUN_INSTANCE* inst, const char* ch_id, uint32_t pos, uint8_t* hash_out);
/* Список ID: count:u16, затем [длина:u8, строка]; buf_size >= 2. 0/-1.
* Списки ограничены буфером; out_len — записанные байты, числа в порядке байтов хоста. */
int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len);
/* Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1. */
int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len);
/* Ключи X25519/Ed25519 (по 32 байта) и список адресов из БД; buf_size >= 65.
* out_len — размер результата; 0 = прочитано, -1 = нет узла или ошибка. */
int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len);
/* ── Создание канала (GUI → uasync) ── */
@ -109,12 +87,9 @@ struct chat_channel_create {
struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; };
/* Подготовить локальные таблицы, группу и синхронизацию канала. Членства на других узлах не даёт. */
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего. */
void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req);
void chat_core_create_channel_trampoline(void* arg);
/* Сгенерировать ключи и создать канал, добавив себя владельцем. */
void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name);
void chat_core_create_channel_auto_trampoline(void* arg);
@ -133,31 +108,19 @@ void chat_core_delete_channel_trampoline(void* arg);
/* ── Утилиты ── */
/* Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет. */
void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* Сохранить имя узла и обновить собственные member-записи каналов. */
void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name);
struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; };
void chat_core_update_my_name_trampoline(void* arg);
/* Обновить адреса topo_node и собственные member-записи каналов. */
void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst);
/* Обновить своё userinfo во всех каналах: увеличить update_ts, подписать и запустить sync.
* Адреса обновляются отдельно через topo_node; подпись member-блока их не содержит. */
/* Единое обновление своего мембера: адреса + bump update_ts + подпись (с адресами) + синк */
void chat_core_update_my_member(struct UTUN_INSTANCE* inst);
/* Сохранение key-value в ui_state (вызывается из uasync-потока) */
void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value);
/* Per-channel «play sound on new message» (ui_state: ch_<id>_sound_on_message, default 1=on).
* set — сохраняет и рассылает пирам через chat_sync (троттлинг db_sync на сервере). */
int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on);
struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; };
void chat_core_set_sound_on_message_trampoline(void* arg);
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; };
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[256]; };
void chat_core_save_ui_state_trampoline(void* arg);
/* Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) */
@ -227,13 +190,13 @@ void chat_core_collect_member_detail_trampoline(void* arg);
/* Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] */
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
/* Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data,local_attrs}].
* От новых к старым; count<=0 — до 100 записей, offset — сколько пропустить. */
/* Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data}]
* count=0 → все, offset=0 → сначала */
int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len);
/* Мемберы в JSON: [{node_id,name,online,connected,adm_tags,x25519,ed25519,addrs}].
* connected означает наличие транспорта в реестре, а не его UP или готовность группы. */
/* Мемберы канала с полным состоянием в JSON:
* [{node_id,name,online,connected,x25519,ed25519,addrs:[{ip,port,proto,rtt}]}] */
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len);
/* Имя узла из таблицы nodes */
@ -243,6 +206,7 @@ int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char*
#define CHAT_MEMBER_TAGS_MAX_KEYS 16
#define CHAT_MEMBER_TAGS_KEY_SZ 64
#define CHAT_MEMBER_TAGS_VAL_SZ 256
struct chat_member_tags;

6
src/chat/chat_core_priv.h

@ -92,9 +92,6 @@ struct chat_core_ctx {
void* media_backfill_timer;/* was chat_msg.c g_media_startup_backfill_timer */
int media_backfill_ran; /* was chat_msg.c g_media_startup_backfill_ran */
struct UTUN_INSTANCE* media_backfill_inst; /* was chat_msg.c g_media_startup_backfill_inst */
void* media_channel_backfill_timer; /* debounce per-channel докачки */
char media_channel_backfill[16][64]; /* pending ch_id */
int media_channel_backfill_pending;
};
/* Per-instance chat context. inst->chat_core обязателен после chat_core_init. */
@ -113,7 +110,8 @@ void si_register(struct UTUN_INSTANCE* inst, struct DB_SYNC_INSTANCE* si, const
/* ── db_sync callback (chat_msg.c) ── */
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, int initial_sync, void* arg);
struct msg_insert_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; };
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg);
/* ── применить настройку group_autoconnect ко всем CHAT-группам (chat_channel.c) ── */

25
src/chat/chat_event.c

@ -9,8 +9,6 @@
#include "chat_event.h"
#include "../utun_instance.h"
#include "../lib/debug_config.h"
#include <stdio.h>
#include <string.h>
#ifdef __ANDROID__
#include <android/log.h>
@ -41,29 +39,8 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data,
[22]="MEMBER_DETAIL", [23]="MEMBER_LIST",
[24]="MEMBER_UPDATED", [25]="MEMBER_REMOVED",
[26]="BGP_INFO", [27]="CONNECTING_NODES",
[28]="ADMIN_KEY_RECEIVED", [29]="INVITE_LINK_READY",
[30]="INVITE_CANDIDATES", [31]="CHANNEL_DELETED",
[32]="DM_MSG_RECEIVED", [33]="DM_CONV_UPDATED",
[34]="CALL_INCOMING", [35]="CALL_RINGING", [36]="CALL_ACCEPTED",
[37]="CALL_ENDED", [38]="CALL_DECLINED", [39]="CALL_STATS",
[40]="CALL_ERROR", [41]="DM_MESSAGES", [42]="CALL_PATH",
[43]="RADIO_TALK",
[44]="CALL_CONNECTION",
[45]="DM_CONVERSATIONS", [46]="ATTACHMENT_PREPARATION", [47]="DM_CALL_ROUTE",
};
const char* n = (type >= 1 && type < (int)(sizeof(names) / sizeof(names[0]))) ? names[type] : "?";
const char* n = (type >= 1 && type <= 27) ? names[type] : "?";
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "chat_event: %s(%d) data=%d bytes", n, type, len);
if (data && len > 0) log_dump(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CHAT, " ", data, len);
}
/* ID задачи не даёт позднему завершению изменить статус другой подготовки. */
void chat_event_attachment(struct UTUN_INSTANCE* inst, int is_dm, const char* target, const uint8_t id[16], int state) {
char name[68];
int n = snprintf(name, sizeof(name), "%s%s", is_dm ? "dm:" : "", target);
if (n <= 0 || (size_t)n >= sizeof(name) || state < 0 || state > 2) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: invalid preparation event target=%s state=%d", target, state); return;
}
uint8_t data[1 + 68 + 16 + 1];
data[0] = (uint8_t)n; memcpy(data + 1, name, (size_t)n); memcpy(data + 1 + n, id, 16); data[17 + n] = (uint8_t)state;
chat_event_post(inst, CHAT_EVT_ATTACHMENT_PREPARATION, data, n + 18);
}

20
src/chat/chat_event.h

@ -36,7 +36,7 @@ extern "C" {
#define CHAT_EVT_SERVICE_STOPPED 13 /* data: none */
#define CHAT_EVT_ATTACHMENT_DOWNLOADED 14 /* [ch_id_len:1][ch_id:var][msg_id:8] */
#define CHAT_EVT_DOWNLOAD_PROGRESS 15 /* [ch_id_len:1][ch_id:var][msg_id:8][blocks_done:4][num_blocks:4] */
#define CHAT_EVT_NODEINFO_UPDATED 16 /* [node_id:8][conn_presence:1][conn_up:1][best_rtt:2][bgp_rtt:2][bgp_nodes:2] */
#define CHAT_EVT_NODEINFO_UPDATED 16 /* [node_id:8][conn_presence:1][conn_up:1][best_rtt:2] */
#define CHAT_EVT_LINK_STATUS_CHANGED 17 /* [peer_id:8][link_id:1][state:1][status:1][links_up:1][initialized:1] */
#define CHAT_EVT_INVITE_RECEIVED 18 /* [ch_id_len:1][ch_id][name_len:1][name][inviter_node_id:8][inviter_name_len:1][inviter_name] */
#define CHAT_EVT_LOCAL_SOCKETS 19 /* JSON: [{"si":id,"nm":"...","ad":"...","po":port,"pt":"TCP|UDP","st":0|1|2,"lk":n},...] */
@ -49,32 +49,16 @@ extern "C" {
#define CHAT_EVT_BGP_INFO 26 /* текст: "rtt:XXms hop:X via bgp: [XXXX] name1 [XXXX] name2" */
#define CHAT_EVT_CONNECTING_NODES 27 /* [ch_id_len:1][ch_id:var][count:2][node_id:8]* */
#define CHAT_EVT_ADMIN_KEY_RECEIVED 28 /* [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ (стал совладельцем) */
#define CHAT_EVT_INVITE_LINK_READY 29 /* [ch_id_len:1][ch_id:var][request_id:8][link_len:2][link:var] — пустой link = ошибка */
#define CHAT_EVT_INVITE_LINK_READY 29 /* [ch_id_len:1][ch_id:var][link_len:2][link:var] — пустой link = ошибка */
#define CHAT_EVT_INVITE_CANDIDATES 30 /* [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */
#define CHAT_EVT_CHANNEL_DELETED 31 /* [ch_id_len:1][ch_id:var] — канал удалён локально */
#define CHAT_EVT_DM_MSG_RECEIVED 32 /* [conv_id_len:1][conv_id:var][author_node_id:8] */
#define CHAT_EVT_DM_CONV_UPDATED 33 /* [conv_id_len:1][conv_id:var] — беседа создана/изменена */
#define CHAT_EVT_CALL_INCOMING 34 /* [ch_id_len:1][ch_id:var][caller_node_id:8][call_id:8] */
#define CHAT_EVT_CALL_RINGING 35 /* [call_id:8] */
#define CHAT_EVT_CALL_ACCEPTED 36 /* [call_id:8] */
#define CHAT_EVT_CALL_ENDED 37 /* [call_id:8][reason:1] */
#define CHAT_EVT_CALL_DECLINED 38 /* [call_id:8][reason:1] */
#define CHAT_EVT_CALL_STATS 39 /* [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2] */
#define CHAT_EVT_CALL_ERROR 40 /* [call_id:8][err:1][text:var] */
#define CHAT_EVT_DM_MESSAGES 41 /* [conv_id_len:1][conv_id:var][JSON:utf8] — сообщения беседы и состояния доставки */
#define CHAT_EVT_CALL_PATH 42 /* [call_id:8][text:var] — BGP-маршрут до пира звонка */
#define CHAT_EVT_RADIO_TALK 43 /* [group_id:8][src_node_id:8][stream_id:2][on:1] — кто-то начал/закончил говорить */
#define CHAT_EVT_CALL_CONNECTION 44 /* [call_id:8][CONN_TYPE:1][next_hop_node_id:8] — текущий путь отправки звонка */
#define CHAT_EVT_DM_CONVERSATIONS 45 /* [JSON:utf8] — последние 200 личных бесед */
#define CHAT_EVT_ATTACHMENT_PREPARATION 46 /* [target_len:1][target:var][job_uuid:16][state:1], 0=start 1=ready 2=failed */
#define CHAT_EVT_DM_CALL_ROUTE 47 /* [conv_id_len:1][conv_id:var][peer_node_id:8][group_id:8], 0 = нет BGP-пути */
typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler);
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
void chat_event_attachment(struct UTUN_INSTANCE* inst, int is_dm, const char* target, const uint8_t id[16], int state);
#ifdef __cplusplus
}

210
src/chat/chat_headless_control.c

@ -19,9 +19,6 @@
#include "invite_build.h"
#include "../dm/dm_core.h"
#include "../dm/dm_crypto.h"
#include "../call/call.h"
#include "../call/call_proto.h"
#include "../radio/radio.h"
#include "../transport_layer/secure_channel.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/etcp_connections.h"
@ -64,18 +61,10 @@ struct headless_client {
int send_offset;
int subscribed;
int closing;
struct headless_invite* invites;
struct headless_control* hc;
struct headless_client* next;
};
struct headless_invite {
struct headless_invite* next;
struct headless_client* client;
struct chat_invite_build* pending;
int id;
};
struct headless_control {
struct UASYNC* ua;
struct UTUN_INSTANCE* inst;
@ -84,7 +73,6 @@ struct headless_control {
socket_t listen_fd;
void* listen_sock_id;
int running;
void* cleanup_timer;
};
static struct headless_control* hc_of(struct UTUN_INSTANCE* inst) {
@ -94,7 +82,6 @@ static struct headless_control* hc_of(struct UTUN_INSTANCE* inst) {
/* ── Forward declarations ── */
static void hc_handle_command(struct headless_client* cli, const char* json);
static void hc_close_client(struct headless_client* cli);
/* ── JSON helpers ── */
@ -156,10 +143,6 @@ static void cli_send(struct headless_client* cli, const char* data, size_t len)
if (remain <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data + r, remain); cli->send_len = (int)remain; cli->send_offset = 0; }
if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 1);
}
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: send failed err=%d, closing client", socket_get_error());
hc_close_client(cli);
}
}
static void cli_send_str(struct headless_client* cli, const char* s) {
@ -218,27 +201,6 @@ static void hc_on_chat_event(struct UTUN_INSTANCE* inst, int type, const uint8_t
} else if (type == 33 && data && len >= 2) { /* DM_CONV_UPDATED */
uint8_t cl = data[0];
snprintf(json, sizeof(json), "{\"event\":\"dm_conv\",\"conv\":\"%.*s\"}", (int)cl, data + 1);
} else if (type == CHAT_EVT_CALL_INCOMING && data && len >= 1 + 8 + 8) { /* CALL_INCOMING */
uint8_t cl = data[0];
if (len >= 1 + (int)cl + 8 + 8) {
uint64_t caller; memcpy(&caller, data + 1 + cl, 8);
uint64_t cid; memcpy(&cid, data + 1 + cl + 8, 8);
snprintf(json, sizeof(json),
"{\"event\":\"call_incoming\",\"ch\":\"%.*s\",\"caller_id\":\"0x%016llx\",\"call_id\":\"0x%016llx\"}",
(int)cl, data + 1, (unsigned long long)caller, (unsigned long long)cid);
}
} else if (type == CHAT_EVT_CALL_RINGING && data && len >= 8) { /* CALL_RINGING */
uint64_t cid; memcpy(&cid, data, 8);
snprintf(json, sizeof(json), "{\"event\":\"call_ringing\",\"call_id\":\"0x%016llx\"}", (unsigned long long)cid);
} else if (type == CHAT_EVT_CALL_ACCEPTED && data && len >= 8) { /* CALL_ACCEPTED */
uint64_t cid; memcpy(&cid, data, 8);
snprintf(json, sizeof(json), "{\"event\":\"call_accepted\",\"call_id\":\"0x%016llx\"}", (unsigned long long)cid);
} else if (type == CHAT_EVT_CALL_ENDED && data && len >= 9) { /* CALL_ENDED */
uint64_t cid; memcpy(&cid, data, 8);
snprintf(json, sizeof(json), "{\"event\":\"call_ended\",\"call_id\":\"0x%016llx\",\"reason\":%d}", (unsigned long long)cid, data[8]);
} else if (type == CHAT_EVT_CALL_DECLINED && data && len >= 9) { /* CALL_DECLINED */
uint64_t cid; memcpy(&cid, data, 8);
snprintf(json, sizeof(json), "{\"event\":\"call_declined\",\"call_id\":\"0x%016llx\",\"reason\":%d}", (unsigned long long)cid, data[8]);
}
if (json[0]) hc_broadcast_event(hc, json);
}
@ -252,11 +214,7 @@ static void client_write_callback(socket_t fd, void* arg) {
if (cli->send_len <= 0) { if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); return; }
ssize_t r = send(cli->fd, cli->send_buf + cli->send_offset, (int)(cli->send_len - cli->send_offset), 0);
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) return;
if (r <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: send failed (r=%zd err=%d) — closing client", r, socket_get_error());
hc_close_client(cli);
return;
}
if (r <= 0) { cli->send_len = 0; cli->send_offset = 0; if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); return; }
cli->send_offset += (int)r;
if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); }
}
@ -267,9 +225,7 @@ static void hc_close_client(struct headless_client* cli) {
struct headless_control* hc = cli ? cli->hc : NULL;
if (!cli || cli->closing) return;
cli->closing = 1;
while (cli->invites) chat_invite_build_cancel(cli->invites->pending);
if (hc && hc->ua && cli->socket_id) uasync_remove_socket_t(hc->ua, cli->fd);
cli->socket_id = NULL;
if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; }
}
@ -284,7 +240,6 @@ static void client_read_callback(socket_t fd, void* arg) {
for (ssize_t i = 0; i < r; i++) {
if (tmp[i] == '\n') {
if (cli->recv_len > 0) { cli->recv_buf[cli->recv_len] = '\0'; hc_handle_command(cli, cli->recv_buf); cli->recv_len = 0; }
if (cli->closing) return;
} else if (cli->recv_len < RECV_BUF_SIZE - 1) {
cli->recv_buf[cli->recv_len++] = tmp[i];
}
@ -460,34 +415,6 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js
send_response(cli, id, resp, NULL);
}
static void hc_invite_ready(void* arg, int result, const char* link) {
struct headless_invite* req = arg;
struct headless_client* cli = req->client;
struct headless_invite** p = &cli->invites;
while (*p && *p != req) p = &(*p)->next;
if (*p) *p = req->next;
int id = req->id;
u_free(req);
if (cli->closing) return;
if (result != CHAT_JOIN_OK) {
send_response(cli, id, NULL, result == CHAT_JOIN_TIMEOUT ? "invite registration timed out" : "invite registration failed");
return;
}
struct InviteData d; char err[128];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cannot decode confirmed invite: %s", err);
send_response(cli, id, NULL, "failed to decode built link");
return;
}
DEBUG_INFO(DEBUG_CATEGORY_HEADLESS, "headless: invite confirmed ch=%llu node=%016llx addrs=%d",
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
char resp[1280];
snprintf(resp, sizeof(resp),
"{\"link\":\"%s\",\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d}",
link, (unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
send_response(cli, id, resp, NULL);
}
static void hc_handle_invite(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char ch[64], node_id_str[32];
@ -502,16 +429,26 @@ static void hc_handle_invite(struct headless_client* cli, int id, const char* js
if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) == 0)
target_node_id = strtoull(node_id_str, NULL, 16);
struct headless_invite* req = u_calloc(1, sizeof(*req));
if (!req) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: invite allocation failed");
send_response(cli, id, NULL, "out of memory"); return;
char link[1024];
if (chat_invite_build_link(hc->inst, channel_id, target_node_id, NULL, link, sizeof(link)) < 0) {
send_response(cli, id, NULL, "failed to build invite link (see log)");
return;
}
req->client = cli; req->id = id;
req->pending = chat_invite_build_link(hc->inst, channel_id, target_node_id, NULL, hc_invite_ready, req);
if (!req->pending) { u_free(req); send_response(cli, id, NULL, "failed to build invite link (see log)"); return; }
req->next = cli->invites;
cli->invites = req;
struct InviteData d; char err[128];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) {
send_response(cli, id, NULL, "failed to decode built link");
return;
}
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite ch=%s target=0x%016llx node=0x%016llx addrs=%d",
ch, (unsigned long long)target_node_id, (unsigned long long)d.nodeId, (int)d.addrCount);
char resp[1280];
snprintf(resp, sizeof(resp),
"{\"link\":\"%s\",\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d}",
link, (unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
send_response(cli, id, resp, NULL);
}
static void hc_handle_invite_nodes(struct headless_client* cli, int id, const char* json) {
@ -713,92 +650,15 @@ static void hc_handle_dm_send(struct headless_client* cli, int id, const char* j
send_response(cli, id, "{\"sent\":true}", NULL);
}
/* ── Звонки (P2P аудио) ── */
static uint64_t hc_gen_call_id(void) {
uint64_t id = ((uint64_t)rand() << 32) ^ (uint64_t)rand();
return id ? id : 1;
}
static void hc_handle_call_start(struct headless_client* cli, int id, const char* json) {
char ch[64], node_id_str[32], call_id_str[32];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; }
uint64_t peer = strtoull(node_id_str, NULL, 16);
if (!peer || !cli->hc->inst) { send_response(cli, id, NULL, "invalid node_id"); return; }
uint64_t call_id = 0;
if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) == 0) call_id = strtoull(call_id_str, NULL, 16);
if (!call_id) call_id = hc_gen_call_id();
call_start(cli->hc->inst, ch, peer, call_id);
char resp[256]; snprintf(resp, sizeof(resp), "{\"call_id\":\"0x%016llx\",\"calling\":true}", (unsigned long long)call_id);
send_response(cli, id, resp, NULL);
}
static void hc_handle_call_accept(struct headless_client* cli, int id, const char* json) {
char call_id_str[32];
if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; }
uint64_t call_id = strtoull(call_id_str, NULL, 16);
call_accept(cli->hc->inst, call_id);
send_response(cli, id, "{\"accepted\":true}", NULL);
}
static void hc_handle_call_decline(struct headless_client* cli, int id, const char* json) {
char call_id_str[32];
if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; }
uint64_t call_id = strtoull(call_id_str, NULL, 16);
call_decline(cli->hc->inst, call_id);
send_response(cli, id, "{\"declined\":true}", NULL);
}
static void hc_handle_call_hangup(struct headless_client* cli, int id, const char* json) {
char call_id_str[32];
if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; }
uint64_t call_id = strtoull(call_id_str, NULL, 16);
call_hangup(cli->hc->inst, call_id);
send_response(cli, id, "{\"hungup\":true}", NULL);
}
/* ── Рация (PTT walkie-talkie) ── */
static void hc_handle_radio_on(struct headless_client* cli, int id, const char* json) {
char ch[64];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
uint64_t group_id = strtoull(ch, NULL, 10);
if (!group_id || !cli->hc->inst) { send_response(cli, id, NULL, "invalid ch"); return; }
radio_set_active(cli->hc->inst, group_id, 1);
char resp[128]; snprintf(resp, sizeof(resp), "{\"radio\":true,\"ch\":\"%llu\"}", (unsigned long long)group_id);
send_response(cli, id, resp, NULL);
}
static void hc_handle_radio_off(struct headless_client* cli, int id, const char* json) {
char ch[64];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
uint64_t group_id = strtoull(ch, NULL, 10);
if (!group_id || !cli->hc->inst) { send_response(cli, id, NULL, "invalid ch"); return; }
radio_set_active(cli->hc->inst, group_id, 0);
char resp[128]; snprintf(resp, sizeof(resp), "{\"radio\":false,\"ch\":\"%llu\"}", (unsigned long long)group_id);
send_response(cli, id, resp, NULL);
}
static void hc_handle_radio_subscribers(struct headless_client* cli, int id, const char* json) {
char ch[64];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
uint64_t group_id = strtoull(ch, NULL, 10);
int n = radio_subscribers(cli->hc->inst, group_id);
char resp[128]; snprintf(resp, sizeof(resp), "{\"subscribers\":%d,\"ch\":\"%llu\"}", n, (unsigned long long)group_id);
send_response(cli, id, resp, NULL);
}
/* ── Command dispatch ── */
typedef void (*hc_cmd_fn)(struct headless_client* cli, int id, const char* json);
static const char* cmd_names[] = { "ping", "status", "channels", "members", "messages", "send", "attach", "invite", "invite_nodes", "connect", "create_channel", "invite_to", "subscribe", "dm_start", "dm_list", "dm_messages", "dm_send", "call_start", "call_accept", "call_decline", "call_hangup", "radio_on", "radio_off", "radio_subscribers", "quit", NULL };
static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_attach, hc_handle_invite, hc_handle_invite_nodes, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_dm_start, hc_handle_dm_list, hc_handle_dm_messages, hc_handle_dm_send, hc_handle_call_start, hc_handle_call_accept, hc_handle_call_decline, hc_handle_call_hangup, hc_handle_radio_on, hc_handle_radio_off, hc_handle_radio_subscribers, hc_handle_quit };
static const char* cmd_names[] = { "ping", "status", "channels", "members", "messages", "send", "attach", "invite", "invite_nodes", "connect", "create_channel", "invite_to", "subscribe", "dm_start", "dm_list", "dm_messages", "dm_send", "quit", NULL };
static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_attach, hc_handle_invite, hc_handle_invite_nodes, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_dm_start, hc_handle_dm_list, hc_handle_dm_messages, hc_handle_dm_send, hc_handle_quit };
static void hc_handle_command(struct headless_client* cli, const char* json) {
if (!cli || cli->closing || !json) return;
if (!cli || !json) return;
int id = json_get_int(json, "id", 0);
const char* cs = json_get_cmd(json);
if (!cs) { send_response(cli, id, NULL, "missing cmd"); return; }
@ -829,7 +689,7 @@ static void accept_callback(socket_t fd, void* arg) {
cli->fd = cfd;
cli->hc = hc;
cli->socket_id = uasync_add_socket_t(hc->ua, cfd, client_read_callback, client_write_callback, NULL, "chatctl_client", cli);
cli->socket_id = uasync_add_socket_t(hc->ua, cfd, client_read_callback, client_write_callback, NULL, cli);
if (!cli->socket_id) { u_free(cli); socket_close_wrapper(cfd); DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: failed to register client fd"); return; }
cli->next = hc->clients; hc->clients = cli; hc->client_count++;
@ -840,7 +700,6 @@ static void accept_callback(socket_t fd, void* arg) {
static void hc_cleanup(void* arg) {
struct headless_control* hc = (struct headless_control*)arg;
hc->cleanup_timer = NULL;
struct headless_client** prev = &hc->clients;
while (*prev) {
if ((*prev)->closing) {
@ -850,8 +709,7 @@ static void hc_cleanup(void* arg) {
u_free(dead); hc->client_count--;
} else prev = &(*prev)->next;
}
hc->cleanup_timer = uasync_set_timeout(hc->ua, 5000, hc, hc_cleanup, "hc_cleanup");
if (!hc->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cleanup timer allocation failed");
if (hc->ua) uasync_set_timeout(hc->ua, 5000, hc, hc_cleanup, "hc_cleanup");
}
/* ── Init / Destroy ── */
@ -885,15 +743,11 @@ int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
}
if (listen(hc->listen_fd, 5) < 0) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; }
hc->listen_sock_id = uasync_add_socket_t(ua, hc->listen_fd, accept_callback, NULL, NULL, "chatctl_listen", hc);
hc->listen_sock_id = uasync_add_socket_t(ua, hc->listen_fd, accept_callback, NULL, NULL, hc);
if (!hc->listen_sock_id) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; }
chat_event_set_handler(inst, hc_on_chat_event);
hc->cleanup_timer = uasync_set_timeout(ua, 5000, hc, hc_cleanup, "hc_cleanup");
if (!hc->cleanup_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cleanup timer allocation failed");
chat_headless_control_destroy(inst); return -1;
}
uasync_set_timeout(ua, 5000, hc, hc_cleanup, "hc_cleanup");
hc->running = 1;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: listening on %s:%d", bind_ip, port);
@ -902,15 +756,13 @@ int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
void chat_headless_control_destroy(struct UTUN_INSTANCE* inst) {
struct headless_control* hc = hc_of(inst);
if (!hc) return;
if (!hc || !hc->running) return;
hc->running = 0;
if (hc->cleanup_timer) uasync_cancel_timeout(hc->ua, hc->cleanup_timer);
if (inst->chat_event_handler == hc_on_chat_event) chat_event_set_handler(inst, NULL);
chat_event_set_handler(inst, NULL);
if (hc->listen_sock_id && hc->ua) uasync_remove_socket_t(hc->ua, hc->listen_fd);
if (hc->listen_fd != SOCKET_INVALID) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; }
struct headless_client* c = hc->clients;
while (c) { struct headless_client* n = c->next; hc_close_client(c); u_free(c); c = n; }
inst->headless = NULL;
u_free(hc);
while (c) { struct headless_client* n = c->next; if (c->fd != SOCKET_INVALID) socket_close_wrapper(c->fd); u_free(c); c = n; }
hc->clients = NULL; hc->client_count = 0;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: destroyed");
}

189
src/chat/chat_join.c

@ -7,7 +7,6 @@
#include "chat_join.h"
#include "member_sync.h"
#include "chat_event.h"
#include "../utun_instance.h"
#include "../routing_layer/topo_node_sqlite.h"
@ -58,24 +57,10 @@ static void _keys_purge_expired(struct UTUN_INSTANCE* inst) {
}
}
static int _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) {
static void _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) {
_keys_purge_expired(inst);
if (!channel_id || !join_key || !inviter_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid key registration", CJ_ID);
return -1;
}
for (struct join_key_entry* e = inst->join_keys; e; e = e->next) {
if (e->channel_id != channel_id || e->join_key != join_key) continue;
if (e->inviter_id != inviter_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration conflicts with existing inviter ch=%llu", CJ_ID,
(unsigned long long)channel_id);
return -1;
}
e->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL;
return 0;
}
struct join_key_entry* e = u_malloc(sizeof(*e));
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key alloc failed", CJ_ID); return -1; }
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key alloc failed", CJ_ID); return; }
e->join_key = join_key; e->channel_id = channel_id; e->inviter_id = inviter_id;
e->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL;
e->next = inst->join_keys;
@ -83,7 +68,6 @@ static int _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t j
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key stored key=%016llx ch=%llu inviter=%016llx ttl=%ds",
CJ_ID, (unsigned long long)join_key, (unsigned long long)channel_id,
(unsigned long long)inviter_id, JOIN_KEY_TTL_SECONDS);
return 0;
}
static struct join_key_entry* _keys_find(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
@ -200,6 +184,7 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
return;
}
if (rc & MS_APPLY_CHANGED) {
member_sync_broadcast_one(inst, ch_id, m.node_id);
/* джойнер авторизован — добавляем в CHAT-группу сразу, чтобы BGP подключился */
uint64_t gid = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid);
@ -211,80 +196,12 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
CJ_ID, (unsigned long long)m.node_id, ch_id);
}
}
/* уведомить GUI инвайтера об успешном присоединении джойнера */
uint8_t cevt[20]; memcpy(cevt, &m.node_id, 8);
int r = 0; memcpy(cevt + 8, &r, 4);
memcpy(cevt + 12, &group_id, 8);
chat_event_post(inst, CHAT_EVT_CONNECT_RESULT, cevt, 20);
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request accepted joiner=0x%016llx ch=%s via=0x%016llx key=%016llx — signed by me",
CJ_ID, (unsigned long long)m.node_id, ch_id, (unsigned long long)from_node, (unsigned long long)join_key);
}
/* Ожидание прикладного ACK: надёжная доставка router сама повторяет потерянные пакеты. */
struct chat_join_registration {
struct chat_join_registration* next;
struct UTUN_INSTANCE* inst;
uint64_t channel_id, target_node_id, join_key;
void* timer;
chat_join_registered_fn callback;
void* arg;
};
static int send_key_control(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target,
uint64_t key, uint8_t subcmd, uint8_t status) {
struct ll_entry* entry = ll_alloc_lldgram(subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK ? 11 : 10);
if (!entry) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: key control allocation failed", CJ_ID);
return -1;
}
entry->dgram[0] = ETCP_RT_ID_JOIN;
entry->len = entry->memlen;
entry->dgram[1] = subcmd;
memcpy(entry->dgram + 2, &key, 8);
if (subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK) entry->dgram[10] = status;
int rc = etcp_route_send(inst, channel_id, target, entry, 0, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: key control send failed cmd=%02x ch=%llu peer=%016llx rc=%d",
CJ_ID, subcmd, (unsigned long long)channel_id, (unsigned long long)target, rc);
}
return rc;
}
static void registration_finish(struct chat_join_registration* req, int result) {
struct chat_join_registration** p = &req->inst->join_registrations;
while (*p && *p != req) p = &(*p)->next;
if (*p) *p = req->next;
if (req->timer) uasync_cancel_timeout(req->inst->ua, req->timer);
if (result == CHAT_JOIN_OK) {
if (chat_join_store_local_key(req->inst, req->channel_id, req->join_key) != 0) result = CHAT_JOIN_ERROR;
}
if (result == CHAT_JOIN_OK || result == CHAT_JOIN_CANCELLED) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration %s ch=%llu peer=%016llx key=%016llx", CJ_ID,
result == CHAT_JOIN_OK ? "confirmed" : "cancelled", (unsigned long long)req->channel_id,
(unsigned long long)req->target_node_id, (unsigned long long)req->join_key);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration failed ch=%llu peer=%016llx key=%016llx result=%d", CJ_ID,
(unsigned long long)req->channel_id, (unsigned long long)req->target_node_id,
(unsigned long long)req->join_key, result);
}
chat_join_registered_fn callback = req->callback;
void* arg = req->arg;
u_free(req);
callback(arg, result);
}
static void registration_timer(void* arg) {
struct chat_join_registration* req = arg;
req->timer = NULL;
registration_finish(req, req->target_node_id == req->inst->node_id ? CHAT_JOIN_OK : CHAT_JOIN_TIMEOUT);
}
void chat_join_cancel_registration(struct chat_join_registration* req) {
if (req) registration_finish(req, CHAT_JOIN_CANCELLED);
}
/* ── etcp_router recv ── */
static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
@ -298,12 +215,6 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
const uint8_t* d = entry->dgram;
uint64_t from_node; memcpy(&from_node, d + ROUTER_SVC_SRC_OFF, 8);
uint64_t group_id; memcpy(&group_id, d + ROUTER_SVC_GROUP_OFF, 8);
uint8_t required = ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED;
if (from_node != inst->node_id && (d[ROUTER_SVC_FLAGS_OFF] & required) != required) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: unprotected join packet rejected from=%016llx ch=%llu", CJ_ID,
(unsigned long long)from_node, (unsigned long long)group_id);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
const uint8_t* payload = d + ROUTER_SVC_PAYLOAD_OFF;
size_t plen = entry->len - ROUTER_SVC_PAYLOAD_OFF;
uint8_t subcmd = payload[0];
@ -313,28 +224,9 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (subcmd == JOIN_SUBCMD_KEY_REGISTER) {
/* payload: [subcmd:1][join_key:8] */
if (plen != 9) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: malformed KEY_REGISTER len=%zu", CJ_ID, plen);
} else {
uint64_t key; memcpy(&key, payload + 1, 8);
int rc = _keys_put(inst, group_id, key, from_node);
send_key_control(inst, group_id, from_node, key, JOIN_SUBCMD_KEY_REGISTER_ACK, rc ? 1 : 0);
}
} else if (subcmd == JOIN_SUBCMD_KEY_REGISTER_ACK) {
if (plen != 10 || payload[9] > 1) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: malformed KEY_REGISTER_ACK len=%zu", CJ_ID, plen);
} else {
uint64_t key; memcpy(&key, payload + 1, 8);
struct chat_join_registration* req = inst->join_registrations;
while (req && (req->channel_id != group_id || req->target_node_id != from_node || req->join_key != key)) req = req->next;
/* Освобождаем пакет до callback: владелец может закрыть instance. */
int result = payload[9] ? CHAT_JOIN_ERROR : CHAT_JOIN_OK;
queue_dgram_free(entry); queue_entry_free(entry);
if (req) registration_finish(req, result);
else DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: late/unmatched registration ACK ch=%llu peer=%016llx", CJ_ID,
(unsigned long long)group_id, (unsigned long long)from_node);
return;
}
if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t join_key; memcpy(&join_key, payload + 1, 8);
_keys_put(inst, group_id, join_key, from_node);
} else if (subcmd == JOIN_SUBCMD_REQUEST_FWD) {
/* payload: [subcmd:1][join_key:8][member...] */
if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
@ -351,7 +243,6 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
int chat_join_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
inst->join_stopping = 0;
if (etcp_router_bind(inst, ETCP_RT_ID_JOIN, join_recv_cb) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: etcp_router_bind failed", CJ_ID);
return -1;
@ -362,52 +253,45 @@ int chat_join_init(struct UTUN_INSTANCE* inst) {
void chat_join_destroy(struct UTUN_INSTANCE* inst) {
if (!inst) return;
inst->join_stopping = 1;
if (inst->router_bindings.callbacks[ETCP_RT_ID_JOIN]) etcp_router_unbind(inst, ETCP_RT_ID_JOIN);
while (inst->join_registrations) chat_join_cancel_registration(inst->join_registrations);
etcp_router_unbind(inst, ETCP_RT_ID_JOIN);
_keys_free_all(inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CJ_ID);
}
/* ── Публичные операции ── */
int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: local registration without instance", CJ_ID); return -1; }
return _keys_put(inst, channel_id, join_key, inst->node_id);
}
void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key) {
if (!inst || channel_id == 0 || join_key == 0) return;
struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key, chat_join_registered_fn callback, void* arg) {
if (!inst || !inst->ua || inst->join_stopping || !channel_id || !join_key || !callback) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid registration arguments", CJ_ID);
return NULL;
}
for (struct chat_join_registration* r = inst->join_registrations; r; r = r->next) {
if (r->channel_id == channel_id && r->join_key == join_key) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration already pending ch=%llu", CJ_ID, (unsigned long long)channel_id);
return NULL;
}
}
struct chat_join_registration* req = u_calloc(1, sizeof(*req));
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration allocation failed", CJ_ID); return NULL; }
req->inst = inst; req->channel_id = channel_id; req->join_key = join_key;
req->target_node_id = target_node_id ? target_node_id : inst->node_id;
req->callback = callback; req->arg = arg;
int local = req->target_node_id == inst->node_id;
req->timer = uasync_set_timeout(inst->ua, local ? 0 : JOIN_REGISTER_TIMEOUT_TB, req, registration_timer, "join_register");
if (!req->timer) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration timer allocation failed", CJ_ID);
u_free(req); return NULL;
/* инвайтер всегда хранит выданный ключ локально — для верификации REQUEST_FWD */
_keys_put(inst, channel_id, join_key, inst->node_id);
if (target_node_id == inst->node_id || target_node_id == 0) {
return;
}
if (!local && send_key_control(inst, channel_id, req->target_node_id, join_key, JOIN_SUBCMD_KEY_REGISTER, 0) != 0) {
uasync_cancel_timeout(inst->ua, req->timer);
u_free(req); return NULL;
uint8_t payload[1 + 8];
payload[0] = JOIN_SUBCMD_KEY_REGISTER;
memcpy(payload + 1, &join_key, 8);
struct ll_entry* entry = queue_entry_new(0);
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: register_key queue_entry_new failed", CJ_ID); return; }
entry->dgram = u_malloc(1 + sizeof(payload));
if (!entry->dgram) { queue_entry_free(entry); return; }
entry->dgram[0] = ETCP_RT_ID_JOIN;
memcpy(entry->dgram + 1, payload, sizeof(payload));
entry->len = 1 + (uint16_t)sizeof(payload);
int rc = etcp_route_send(inst, channel_id, target_node_id, entry, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: register_key etcp_route_send FAILED rc=%d ch=%llu → 0x%016llx",
CJ_ID, rc, (unsigned long long)channel_id, (unsigned long long)target_node_id);
queue_dgram_free(entry); queue_entry_free(entry);
return;
}
req->next = inst->join_registrations;
inst->join_registrations = req;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: registration pending ch=%llu peer=%016llx key=%016llx", CJ_ID,
(unsigned long long)channel_id, (unsigned long long)req->target_node_id, (unsigned long long)join_key);
return req;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key registered ch=%llu → connection=0x%016llx key=%016llx",
CJ_ID, (unsigned long long)channel_id, (unsigned long long)target_node_id, (unsigned long long)join_key);
}
uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
@ -446,6 +330,7 @@ int chat_join_forward_request(struct UTUN_INSTANCE* inst, uint64_t channel_id,
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: forward etcp_route_send FAILED rc=%d ch=%llu → 0x%016llx",
CJ_ID, rc, (unsigned long long)channel_id, (unsigned long long)inviter_id);
queue_dgram_free(entry); queue_entry_free(entry);
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join request forwarded ch=%llu → inviter=0x%016llx joiner=0x%016llx",

89
src/chat/chat_join.h

@ -1,71 +1,20 @@
/*
* chat_join — добавление участника канала через узел подключения C и инвайтера A.
* Модуль регистрирует invite-ключи, пересылает запрос к A и сохраняет подписанного
* участника. Прямой обмен с новым узлом J выполняет chat_sync; Merkle разносит запись.
* chat_join.h — авторизация входа в канал (join) через connection-узел и инвайтера
*
* Протокол (A и C могут быть одним узлом):
* Flow (замена старого CHANNEL_INFO_REQ/CHANNEL_JOIN/WELCOME):
*
* инвайтер A connection-узел C джойнер J
* │ KEY_REGISTER(key,ch) │ │
* ├──────etcp_router─────────▶│ хранит {key→A, 10 мин} │
* │◀── KEY_REGISTER_ACK ──────┤ │
* │ теперь ссылка готова │ │
* │ │◀─── JOIN_INFO_REQ(key) ──────┤ (J подключился к C по ссылке)
* │ │── JOIN_INFO_RESP ───────────▶│ (ch_x25519, ch_ed25519, ch_sig, C-мембер)
* │ │◀─── JOIN_REQUEST ────────────┤ (key + полный набор мембера J)
* │ │ валиден ключ → только pending, БЕЗ BGP new_conn
* │ │ (валиден ключ → BGP new_conn, pending status=sent)
* │◀── REQUEST_FWD ───────────┤ │
* │ verif join_sig, node_id=derive(x25519) │
* │ member_sync_put(J, signed_by=A, signature=sign(A, J_x25519||"group_join"))
* │ ── merkle broadcast ──▶ (рекорд J дошёл по merkle) │
* │ │── JOIN_READY(ch) ────────────▶│ добавление подтверждено у C
*
* КОНТРАКТ ЧЛЕНСТВА (обязателен для всех путей подключения):
* - A и C уже участвуют в канале; A может совпадать с C.
* - NCD даёт транспорт J–C для протокола добавления. Наличие транспорта,
* invite-ключа или локального TOPO_GROUP не даёт участия J в BGP-группе.
* Инициатор удерживает отдельный NCD handle до результата/таймаута добавления;
* закрывать его на транспортном UP, рассчитывая на владельца BGP, запрещено.
* - После проверки JOIN_REQUEST инвайтер сохраняет мембера через member_sync.
* Merkle распространяет запись между существующими участниками.
* - Каждый узел разрешает групповую сессию J после принятия и сохранения
* записи J в СВОЕЙ таблице участников. Подтверждение всех узлов не требуется.
* - Проверка invite-ключа разрешает только обработку запроса добавления.
* Временного членства и раннего BGP new_conn до сохранения мембера нет.
* - JOIN_READY означает наличие записи J у C. Это не подтверждение доставки
* всем узлам и не окончание обмена топологией. BGP JOIN_GROUP / TABLE_COMPLETE
* из topo_group.h относятся к отдельной групповой сессии.
*
* ПОРЯДОК И ПРОВЕРКИ:
* 1. A регистрирует (канал, join_key) -> A на C на JOIN_KEY_TTL_SECONDS.
* Ссылку можно публиковать только после успешного KEY_REGISTER_ACK.
* При A==C регистрация и обработка запроса локальные.
* 2. J открывает NCD к C; C проверяет ключ из JOIN_INFO_REQ и возвращает
* подписанное описание канала и свою запись участника. J проверяет ответ.
* Создание локальной инфраструктуры канала ещё не добавляет J на других узлах.
* 3. C проверяет JOIN_REQUEST: ключ и привязку идентичности J к транспорту.
* Запрос идёт к A по существующему маршруту канала; маршрут через J не нужен.
* 4. A проверяет запись, подписывает приглашение и сохраняет мембера.
* После локального commit или принятия записи по Merkle C отправляет JOIN_READY.
* 5. JOIN_GROUP, пришедший раньше записи мембера, не создаёт сессию у получателя.
* После commit member_sync повторно инициирует групповое согласование
* с этим пиром, если транспорт уже установлен. Новый ETCP-коннект не требуется.
*
* ОШИБКИ, ПОВТОРЫ И ОТМЕНА:
* - Неверный/истёкший ключ, несовпадение идентичности или неверная подпись
* не создают мембера. Ошибка/таймаут не дают временных прав.
* - Повторная запись проходит обычную проверку подписей и версий member_sync;
* повтор не создаёт второго мембера или второго владельца групповой сессии.
* - Ответ относится к ожидаемым каналу и пиру; ответ другой попытки не должен
* завершать текущее ожидание. UP другого пира не меняет адресата приглашения.
* - Отмена ожидания не откатывает уже сохранённую/реплицированную запись.
* Удаление локального канала отменяет его ожидания и подключения.
* - Recovery восстанавливает сессии существующих участников и не использует
* invite-ключ как замену локальной таблицы участников.
*
* J <-> C: ETCP_RT_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload
* описан в chat_sync.h. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью
* и шифрованием; subcommands и payload описаны ниже.
* │ │── JOIN_READY(ch) ────────────▶│ → topo_group_new_conn + ensure_channel_ready
*
* Модуль отвечает только за маршрутизируемую (etcp_router) часть:
* - регистрация ключа (инвайтер → connection),
@ -86,29 +35,24 @@ struct UTUN_INSTANCE;
extern "C" {
#endif
/* etcp_router service id (connection ↔ инвайтер): ETCP_RT_ID_JOIN (см. etcp_api.h) */
/* etcp_router service id (connection ↔ инвайтер) */
#define ETCP_RT_ID_JOIN 0x33
/* subcommands (первый байт payload etcp_router) */
#define JOIN_SUBCMD_KEY_REGISTER 0x01 /* инвайтер → connection: {join_key:8} */
#define JOIN_SUBCMD_REQUEST_FWD 0x02 /* connection → инвайтер: {join_key:8, member...} */
#define JOIN_SUBCMD_KEY_REGISTER_ACK 0x03 /* connection → инвайтер: {join_key:8, status:1}; 0=stored, 1=rejected */
#define JOIN_KEY_TTL_SECONDS 600 /* 10 минут хранения ключа на connection-узле */
#define JOIN_REGISTER_TIMEOUT_TB 50000 /* 5 секунд на подтверждение */
enum chat_join_result { CHAT_JOIN_OK = 0, CHAT_JOIN_ERROR = -1, CHAT_JOIN_TIMEOUT = -2, CHAT_JOIN_CANCELLED = -3 };
struct chat_join_registration;
typedef void (*chat_join_registered_fn)(void* arg, int result);
/* Полный набор мембера джойнера (без подписи дерева — её ставит инвайтер). */
struct join_member_data {
uint64_t node_id; /* derive(x25519), идентичность нового участника */
uint64_t node_id;
const uint8_t* x25519; /* 32 */
const uint8_t* ed25519; /* 32 */
const uint8_t* join_sig; /* 64, может быть NULL */
uint64_t join_ts; /* время подписи вступления, Unix seconds */
uint64_t join_ts;
const uint8_t* update_sig; /* 64, может быть NULL */
uint64_t update_ts; /* версия подписанного блока участника */
uint64_t update_ts;
char userinfo[256]; /* JSON, null-terminated */
};
@ -122,18 +66,13 @@ int chat_join_parse_member(const uint8_t* data, size_t len, struct join_member_
/* Инициализация: etcp_router_bind(ETCP_RT_ID_JOIN). */
int chat_join_init(struct UTUN_INSTANCE* inst);
/* Снять обработчик, освободить ключи; ожидающие регистрации получают CANCELLED. */
/* Деинициализация. */
void chat_join_destroy(struct UTUN_INSTANCE* inst);
/* Локальное приглашение (инвайтер == connection). 0=stored, -1=ошибка. */
int chat_join_store_local_key(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key);
/* Все вызовы в uasync-потоке. NULL=ошибка запуска, callback не вызывается.
* Иначе callback ровно один раз, после возврата register_key (также для self).
* Handle действителен до callback. cancel/destroy вызывают callback синхронно с CANCELLED. */
struct chat_join_registration* chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key, chat_join_registered_fn callback, void* arg);
void chat_join_cancel_registration(struct chat_join_registration* registration);
/* Инвайтер: зарегистрировать join-ключ на connection-узле.
* target==self → храним локально (инвайтер и есть connection-узел). */
void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
uint64_t target_node_id, uint64_t join_key);
/* Connection-узел: найти инвайтера по ключу. 0 = нет/истёк. */
uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key);

4
src/chat/chat_member.c

@ -43,6 +43,10 @@ static uint8_t parse_adm_tags_flags(const char* adm_tags) {
if (json_flat_get(adm_tags, "supernode", buf, sizeof(buf)) == 0
&& strcmp(buf, "yes") == 0) flags |= CHAT_MEMBER_FLAG_SUPERNODE;
/* dm_storage: узел хранит offline-сообщения прямого чата (DM) */
if (json_flat_get(adm_tags, "dm_storage", buf, sizeof(buf)) == 0
&& strcmp(buf, "yes") == 0) flags |= CHAT_MEMBER_FLAG_DM_STORAGE;
/* admin: значение — накопительная строка ed...ed..., последний символ e/d */
if (json_flat_get(adm_tags, "admin", buf, sizeof(buf)) == 0) {
const char* last = buf;

1
src/chat/chat_member.h

@ -30,6 +30,7 @@ extern "C" {
#define CHAT_MEMBER_FLAG_ADMIN 0x02
#define CHAT_MEMBER_FLAG_MODER 0x04
#define CHAT_MEMBER_FLAG_DELETED 0x08
#define CHAT_MEMBER_FLAG_DM_STORAGE 0x10
#define CHAT_MEMBER_DISPLAY_SIZE 79

141
src/chat/chat_msg.c

@ -216,19 +216,16 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
u_free(sig_msg); u_free(json); u_free(full_data); u_free(mctx); return;
}
/* local_attrs готовим заранее и пишем атомарно при вставке — иначе GUI по
MSG_RECEIVED может прочитать сообщение раньше отдельного UPDATE (гонка),
и своё голосовое покажется «без медиа» (серый media bar + попытка скачать) */
const char* base = strrchr(mctx->media_dest, '/');
const char* filename = base ? base + 1 : mctx->media_dest;
char attrs[1100]; snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", filename);
int ret = db_sync_insert_signed(si, json, strlen(json), json_sig, 64, ts, attrs);
int ret = db_sync_insert_signed(si, json, strlen(json), json_sig, 64, ts, NULL);
if (ret != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media insert failed ret=%d", CC_ID, ret);
} else {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: media message inserted ch=%s ts=%llu",
CC_ID, mctx->channel_id, (unsigned long long)ts);
const char* base = strrchr(mctx->media_dest, '/');
const char* filename = base ? base + 1 : mctx->media_dest;
char attrs[1100]; snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", filename);
chat_core_update_local_attrs(mctx->inst, mctx->channel_id, ts, json_sig, attrs);
/* анонсируем блоки суперузлам — иначе первый скачивающий не найдёт держателя */
media_delivery_announce_media(mctx->inst, strtoull(mctx->channel_id, NULL, 10),
@ -251,7 +248,7 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
u_free(sig_msg); u_free(json); u_free(full_data); u_free(mctx);
}
static void on_video_prepared(void* arg, int err, const struct video_info* vi) {
static void on_video_prepared(void* arg, int err) {
struct media_submit_ctx* mctx = (struct media_submit_ctx*)arg;
struct chat_core_ctx* cc = CC(mctx->inst);
@ -263,12 +260,13 @@ static void on_video_prepared(void* arg, int err, const struct video_info* vi) {
}
/* после транскода знаем итоговую длительность и размеры — для тела сообщения */
if (vi && vi->is_video) {
mctx->duration_ms = vi->duration_ms;
mctx->width = vi->width;
mctx->height = vi->height;
struct video_info vi;
if (video_get_info(mctx->media_dest, &vi) == 0 && vi.is_video) {
mctx->duration_ms = vi.duration_ms;
mctx->width = vi.width;
mctx->height = vi.height;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: video prepared %dx%d dur=%lldms",
CC_ID, vi->width, vi->height, (long long)vi->duration_ms);
CC_ID, vi.width, vi.height, (long long)vi.duration_ms);
}
char media_base[1024];
@ -341,8 +339,8 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
}
if (req->media_video) {
if (video_transcode_start(inst->media_async, inst->ua, req->media_src, req->media_dest, on_video_prepared, mctx) < 0)
on_video_prepared(mctx, -1, NULL);
video_transcode_start(inst->ua, req->media_src, req->media_dest,
on_video_prepared, mctx);
return;
}
@ -840,23 +838,19 @@ static int md_auto_download(struct UTUN_INSTANCE* inst,
/* ─── db_sync callback ─── */
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, int initial_sync, void* arg) {
struct UTUN_INSTANCE* inst = arg;
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg) {
(void)si;
struct msg_insert_arg* ia = (struct msg_insert_arg*)arg;
struct UTUN_INSTANCE* inst = ia ? ia->inst : NULL;
struct chat_core_ctx* cc = CC(inst);
if (!cc) return;
char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)db_sync_instance_group_id(si));
if (!ia || !cc) return;
const char* ch_id = ia->ch_id;
uint8_t evt[73]; uint8_t cl = (uint8_t)strlen(ch_id); evt[0] = cl; memcpy(evt + 1, ch_id, cl);
memcpy(evt + 1 + cl, &author, 8);
chat_event_post(inst, CHAT_EVT_MSG_RECEIVED, evt, 1 + cl + 8);
/* auto-download media if message contains media metadata (live PUSH/local only).
initial-sync (SEND_DATA) media докачивается после sync через per-channel backfill. */
if (initial_sync && data && len > 0 && author != cc->my_node_id) {
char dbg[4096]; size_t dbgl = len < sizeof(dbg) - 1 ? len : sizeof(dbg) - 1;
memcpy(dbg, data, dbgl); dbg[dbgl] = '\0';
if (strstr(dbg, "\"ct\":\"") && strstr(dbg, "\"d\":\""))
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "%s: media download deferred (initial sync) ch=%s", CC_ID, ch_id);
} else if (data && len > 0 && author != cc->my_node_id) {
/* auto-download media if message contains media metadata */
if (data && len > 0 && author != cc->my_node_id) {
/* data from db_sync may not be null-terminated; copy to local buffer */
char buf[4096]; size_t blen = len < sizeof(buf) - 1 ? len : sizeof(buf) - 1;
memcpy(buf, data, blen); buf[blen] = '\0';
@ -967,10 +961,6 @@ void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* chann
uint8_t author_sig[64]; memcpy(author_sig, sig_blob, 64);
sqlite3_finalize(st);
/* сбрасываем пометку «файл есть»: файл мог быть вычищен кешем, а local_attrs ещё "fl".
по завершении md_download_done_cb снова запишет st=fl (успех) или st=er (ошибка). */
chat_core_update_local_attrs(inst, channel_id, (uint64_t)db_ts, author_sig, "{}");
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: manual download ch=%s id=%lld ct=%s file=%s",
CC_ID, channel_id, (long long)msg_id, content_type, base_filename);
md_start_download(inst, body, bi, channel_id, base_filename, (uint64_t)db_ts, author_sig, (uint64_t)author_node_id, msg_id, content_type);
@ -1124,15 +1114,14 @@ static int bf_cmp_ts_desc(const void* a, const void* b) {
return 0;
}
/* Собирает медиасообщения (presence + размеры + контекст докачки). only_ch_id != NULL — только этот канал. */
static void bf_collect(struct chat_core_ctx* cc, const char* media_base, const char* only_ch_id, struct bf_list* l) {
/* Собирает все медиасообщения всех каналов (presence + размеры + контекст докачки). */
static void bf_collect(struct chat_core_ctx* cc, const char* media_base, struct bf_list* l) {
sqlite3_stmt* cs = NULL;
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(cs, 0);
if (!ch_id || !ch_id[0]) continue;
if (only_ch_id && strcmp(only_ch_id, ch_id) != 0) continue;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[256];
@ -1232,7 +1221,7 @@ void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst) {
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
struct bf_list list = {0};
bf_collect(cc, media_base, NULL, &list);
bf_collect(cc, media_base, &list);
if (list.count > 1) qsort(list.items, (size_t)list.count, sizeof(struct bf_entry), bf_cmp_ts_desc);
/* инициализация счётчика: сумма всех present-файлов */
@ -1284,51 +1273,6 @@ void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst) {
chat_media_cache_reconcile(inst);
}
/* Докачка медиа одного канала после завершения его chat_sync.
Порядок — от последних сообщений назад (список сортируется desc по ts). */
void chat_media_autodownload_backfill_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst || !ch_id || !ch_id[0]) return;
uint64_t limit = inst->config->global.chatserver_storage_total_size;
int autoload = chat_setting_get_int(inst, "storage_autoload", 1);
int days = chat_setting_get_int(inst, "storage_backfill_days", 7);
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(inst) * 1000ULL;
uint64_t cutoff_ms = now_ms - (uint64_t)days * 86400ULL * 1000ULL;
char media_base[512];
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
struct bf_list list = {0};
bf_collect(cc, media_base, ch_id, &list);
if (list.count > 1) qsort(list.items, (size_t)list.count, sizeof(struct bf_entry), bf_cmp_ts_desc);
uint64_t used = cc->media_cache_bytes;
int downloaded = 0, skipped = 0;
for (int i = 0; i < list.count; i++) {
struct bf_entry* e = &list.items[i];
if (e->actual_size > 0) continue;
if (!autoload || e->author == cc->my_node_id) { skipped++; continue; }
if (e->ts_ms < cutoff_ms) { skipped++; continue; }
if (limit > 0 && used + (uint64_t)e->claimed_size > limit) { skipped++; continue; }
if (md_auto_download(inst, e->body, e->body_len, e->ch_id, e->base_filename,
e->ts_ms, e->author_sig, e->author, e->msg_id, e->content_type)) {
downloaded++;
used += (uint64_t)e->claimed_size;
} else {
skipped++;
}
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: channel backfill done ch=%s downloaded=%d skipped=%d",
CC_ID, ch_id, downloaded, skipped);
for (int i = 0; i < list.count; i++) if (list.items[i].body) u_free(list.items[i].body);
u_free(list.items);
}
/* ─── Кеш медиафайлов: очистка сирот + лимит storage_total_size ─── */
struct media_msg_file {
@ -1615,57 +1559,22 @@ static void media_startup_backfill_done_cb(struct DB_SYNC_INSTANCE* si, uint64_t
media_startup_backfill_timer_cb, "chat_media_bl");
}
/* per-channel: после SYNC_DONE канала — докачка его медиа (дебаунс 2s, от новых к старым) */
static void media_channel_backfill_timer_cb(void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct chat_core_ctx* cc = CC(inst);
cc->media_channel_backfill_timer = NULL;
for (int i = 0; i < cc->media_channel_backfill_pending; i++)
chat_media_autodownload_backfill_channel(inst, cc->media_channel_backfill[i]);
cc->media_channel_backfill_pending = 0;
}
static void media_channel_backfill_done_cb(struct DB_SYNC_INSTANCE* si, uint64_t peer_node_id, void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct chat_core_ctx* cc = CC(inst);
(void)peer_node_id;
if (!cc || !si) return;
char ch_id[32]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)db_sync_instance_group_id(si));
for (int i = 0; i < cc->media_channel_backfill_pending; i++)
if (strcmp(cc->media_channel_backfill[i], ch_id) == 0) return;
if (cc->media_channel_backfill_pending < (int)(sizeof(cc->media_channel_backfill) / sizeof(cc->media_channel_backfill[0])))
snprintf(cc->media_channel_backfill[cc->media_channel_backfill_pending++],
sizeof(cc->media_channel_backfill[0]), "%s", ch_id);
if (cc->media_channel_backfill_timer) uasync_cancel_timeout(inst->ua, cc->media_channel_backfill_timer);
cc->media_channel_backfill_timer = uasync_set_timeout(inst->ua, 20000, inst,
media_channel_backfill_timer_cb, "chat_md_chbl");
}
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!inst || !cc) return;
cc->media_backfill_inst = inst;
cc->media_backfill_ran = 0;
cc->media_channel_backfill_pending = 0;
cc->media_channel_backfill_timer = NULL;
db_sync_add_done_cbk(inst, media_startup_backfill_done_cb, inst);
db_sync_add_done_cbk(inst, media_channel_backfill_done_cb, inst);
}
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!inst || !cc) return;
db_sync_remove_done_cbk(inst, media_startup_backfill_done_cb, inst);
db_sync_remove_done_cbk(inst, media_channel_backfill_done_cb, inst);
if (cc->media_backfill_timer) {
uasync_cancel_timeout(inst->ua, cc->media_backfill_timer);
cc->media_backfill_timer = NULL;
}
if (cc->media_channel_backfill_timer) {
uasync_cancel_timeout(inst->ua, cc->media_channel_backfill_timer);
cc->media_channel_backfill_timer = NULL;
}
cc->media_channel_backfill_pending = 0;
cc->media_backfill_inst = NULL;
cc->media_backfill_ran = 0;
}

1
src/chat/chat_profile.c

@ -99,6 +99,7 @@ void chat_core_update_my_member(struct UTUN_INSTANCE* inst) {
inst->my_ed25519_pubkey, join_sig, join_ts,
update_sig, update_ts, juser, NULL, NULL, 0, 0, NULL);
if (r2 < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d", CC_ID, ch, r2);
else if (r2 & MS_APPLY_CHANGED) member_sync_broadcast_one(inst, ch, myid);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: update my member ch=%s ts=%llu", CC_ID, ch,
(unsigned long long)update_ts);

21
src/chat/chat_setting.c

@ -34,33 +34,18 @@ struct chat_setting_def {
static const struct chat_setting_def g_setting_defs[] = {
{"storage_autoload", CHAT_SETTING_BOOL, 1, 0, 1},
{"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 4},
{"radio_opus_mode", CHAT_SETTING_INT, 0, 0, 1},
{"radio_opus_bitrate", CHAT_SETTING_INT, 32, 16, 128},
{"radio_capture_stereo", CHAT_SETTING_INT, 0, 0, 1},
{"compressor_enabled", CHAT_SETTING_BOOL, 1, 0, 1},
{"compressor_max_gain_db", CHAT_SETTING_INT, 15, 1, 100},
{"compressor_rise_rate", CHAT_SETTING_INT, 2, 1, 100},
{"radio_compressor_enabled", CHAT_SETTING_BOOL, 1, 0, 1},
{"radio_compressor_max_gain_db",CHAT_SETTING_INT, 15, 1, 100},
{"radio_vad_enabled", CHAT_SETTING_BOOL, 0, 0, 1},
{"radio_vad_threshold", CHAT_SETTING_INT, 50, 10, 95}, /* порог вероятности речи, % */
{"radio_vad_hangover_ms", CHAT_SETTING_INT, 200, 50, 2000},/* тишина до авто-отбоя */
{"compressor_enabled", CHAT_SETTING_BOOL, 0, 0, 1},
{"compressor_max_gain_db", CHAT_SETTING_INT, 25, 1, 100},
{"compressor_rise_rate", CHAT_SETTING_INT, 10, 1, 100},
{"media_download_max_peers", CHAT_SETTING_INT, 3, 1, 16},
{"storage_backfill_days", CHAT_SETTING_INT, 7, 1, 365},
{"dm_media_ttl_sec", CHAT_SETTING_INT, 604800, 1, 31536000}, /* TTL custody M: 7 суток */
{"dm_media_storage_mb", CHAT_SETTING_INT, 1024, 1, 1048576}, /* включая зарезервированные загрузки */
{"standby_active_sec", CHAT_SETTING_INT, 2, 1, 3600},
{"standby_sleep_sec", CHAT_SETTING_INT, 60, 1, 86400},
{"standby_min_sleep_sec", CHAT_SETTING_INT, 15, 1, 86400},
{"standby_keepalive_ms", CHAT_SETTING_INT, 30000, 1000, 600000},
{"join_policy", CHAT_SETTING_INT, 1, 0, 2}, /* 0=autojoin, 1=ask, 2=deny */
{"whisper_enabled", CHAT_SETTING_BOOL, 0, 0, 1},
{"whisper_model_path", CHAT_SETTING_STRING, 0, 0, 0},
{"group_autoconnect", CHAT_SETTING_BOOL, 1, 0, 1},
{"aec_enabled", CHAT_SETTING_BOOL, 1, 0, 1}, /* AEC (SpeexDSP) в звонке, desktop GUI */
{"call_jitter_max_reserve_ms", CHAT_SETTING_INT, 1000, 500, 2000},
{"call_compressor_enabled", CHAT_SETTING_BOOL, 1, 0, 1},
{"call_compressor_max_gain_db",CHAT_SETTING_INT, 15, 1, 100},
};
#define CHAT_SETTING_COUNT (sizeof(g_setting_defs) / sizeof(g_setting_defs[0]))

4
src/chat/chat_status.c

@ -218,7 +218,7 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
"=== PEER: %04llX\u2192%04llX [%s] ===\n"
"Role: %s\n"
"Status: %s Links: %d/%d Initialized: %s MTU: %d\n"
"Reinit: %u Reset: %u Tx_state: %d\n"
"Reinit: %u Reset: %u Tx_state: %d Routing_exchange: %d\n"
"\n--- ETCP ---\n"
"RTT last/avg10: %.1f/%.1f ms Jitter: %.1f ms\n"
"Bytes sent: %u Retrans: %u ACKs: %u\n"
@ -230,7 +230,7 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
role_str,
conn->links_up ? "UP" : "DOWN", links_up_count, link_count,
conn->initialized ? "yes" : "no", conn->mtu,
conn->reinit_count, conn->reset_count, conn->tx_state,
conn->reinit_count, conn->reset_count, conn->tx_state, conn->routing_exchange_active,
conn->rtt_last / 10.0f, conn->rtt_avg_10 / 10.0f, conn->jitter / 10.0f,
conn->bytes_sent_total, conn->retransmissions_count, conn->ack_packets_count,
conn->unacked_bytes, conn->max_inflight,

283
src/chat/chat_sync.c

@ -17,7 +17,6 @@
#include "../utun_instance.h"
#include "../transport_layer/etcp_api.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/node_conn_direct.h"
#include "../transport_layer/secure_channel.h"
#include "../ntp_time.h"
#include "../../lib/u_async.h"
@ -32,7 +31,6 @@
#include <string.h>
#include <sqlite3.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#ifdef __ANDROID__
#include <android/log.h>
#endif
@ -74,13 +72,10 @@ struct chat_sync {
uint8_t pending_invite_is_inviter; /* 1 = inviter sends CHANNEL_INVITE on conn_up, 0 = joiner sends JOIN_INFO_REQ */
char pending_password[128]; /* пароль для JOIN_INFO_REQ */
uint64_t pending_join_key; /* join-ключ из invite-ссылки (джойнер) */
struct NODE_CONN_DIRECT* join_handle; /* транспорт протокола добавления, не членство */
void* join_transport_timer;
struct join_pending* join_pending; /* connection-узел: ожидающие ready джойнеры */
/* Pending incoming invites (для ask/deny) */
uint64_t pending_inv_in_ch_id[4];
uint64_t pending_inv_in_node_id[4];
uint64_t pending_inv_in_key[4];
int pending_inv_in_count;
};
@ -90,47 +85,6 @@ struct chat_sync {
#define CS_INVITE_SEND_TIMEOUT_MS 3000 /* inviter waits for joiner to respond */
#define CS_SYNC_INTERVAL_MS 1000
static void cs_info_req_timeout_cb(void* arg);
static void cs_join_connect_timeout(void* arg) {
struct chat_sync* cs = arg;
cs->join_transport_timer = NULL;
cs_info_req_timeout_cb(cs);
}
static void cs_join_release_transport(struct chat_sync* cs) {
if (cs->join_transport_timer) {
uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
}
struct NODE_CONN_DIRECT* handle = cs->join_handle;
cs->join_handle = NULL;
if (handle) {
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "join transport release peer=%016llx",
(unsigned long long)node_conn_direct_node_id(handle));
node_conn_direct_close(handle);
}
}
static int cs_join_hold_transport(struct chat_sync* cs, uint64_t node_id, struct TOPO_NODE* ni) {
struct NODE_CONN_DIRECT* handle = NULL;
int rc = ni ? node_conn_direct_open_node(cs->inst, node_id, NULL, NULL, &handle, ni, NULL)
: node_conn_direct_open(cs->inst, node_id, NULL, NULL, &handle, NULL);
if (rc == NCD_ERR) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "cannot acquire join transport peer=%016llx", (unsigned long long)node_id);
return -1;
}
cs_join_release_transport(cs);
cs->join_handle = handle;
cs->join_transport_timer = uasync_set_timeout(cs->inst->ua, CS_INFO_REQ_TIMEOUT_MS * 10,
cs, cs_join_connect_timeout, "join_transport_timeout");
if (!cs->join_transport_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "cannot arm join transport timeout");
cs_join_release_transport(cs); return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "join owns transport peer=%016llx", (unsigned long long)node_id);
return 0;
}
static const char* cs_msg_name(uint8_t type) {
switch (type) {
case CS_MSG_INIT_SYNC: return "INIT_SYNC";
@ -145,7 +99,6 @@ static const char* cs_msg_name(uint8_t type) {
case CS_MSG_JOIN_INFO_RESP: return "JOIN_INFO_RESP";
case CS_MSG_JOIN_REQUEST: return "JOIN_REQUEST";
case CS_MSG_JOIN_READY: return "JOIN_READY";
case CS_MSG_SOUND_SETTING: return "SOUND_SETTING";
default: return "???";
}
}
@ -197,7 +150,6 @@ static int cs_send(struct chat_sync* cs, const char* ch_id, uint64_t dst,
struct ETCP_CONN* conn = cs_find_conn_for_node(cs->inst, dst);
if (!conn) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: no conn for node %016llx", CS_ID, (unsigned long long)dst); u_free(buf); queue_entry_free(entry); return -1; }
int rc = etcp_send(conn, entry);
if (rc != 0) { queue_dgram_free(entry); queue_entry_free(entry); }
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: etcp_send rc=%d id=0x%02x to=%016llx conn=%s", CS_ID, rc, payload[0], (unsigned long long)dst, conn->log_name);
return rc;
}
@ -251,15 +203,13 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
const uint8_t* d = entry->dgram;
size_t dlen = entry->len;
uint64_t group_id; memcpy(&group_id, d + 1, sizeof(group_id));
group_id = be64toh(group_id);
uint64_t group_id = be64toh(*(const uint64_t*)(d + 1));
uint8_t type = d[9];
char ch_id[64];
if (cs_group_id_to_ch_id(cs->inst, group_id, ch_id, sizeof(ch_id)) != 0) {
/* CHANNEL_INVITE/JOIN_INFO_REQ приходят для ещё не созданного у нас канала:
group_id == channel_id (numeric), поэтому выводим ch_id напрямую. */
if (type == CS_MSG_CHANNEL_INVITE || type == CS_MSG_JOIN_INFO_REQ || type == CS_MSG_JOIN_INFO_RESP ||
type == CS_MSG_JOIN_REQUEST || type == CS_MSG_JOIN_READY || type == CS_MSG_ERROR) {
if (type == CS_MSG_CHANNEL_INVITE || type == CS_MSG_JOIN_INFO_REQ || type == CS_MSG_JOIN_REQUEST) {
snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)group_id);
} else {
u_free(entry->dgram); queue_entry_free(entry); return;
@ -273,13 +223,6 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: RECV %s from=%016llx ch=%s len=%zu",
CS_ID, cs_msg_name(type), (unsigned long long)peer, ch_id, plen);
if ((type == CS_MSG_JOIN_INFO_RESP || type == CS_MSG_JOIN_READY || type == CS_MSG_ERROR) &&
(cs->pending_invite_node_id != peer || cs->pending_invite_ch_id != group_id)) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "ignore stale join response peer=%016llx group=%016llx type=%u",
(unsigned long long)peer, (unsigned long long)group_id, type);
u_free(entry->dgram); queue_entry_free(entry); return;
}
switch (type) {
case CS_MSG_JOIN_INFO_REQ: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_REQ", CS_ID); cs_handle_join_info_req(cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_INFO_RESP: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_RESP", CS_ID); cs_handle_join_info_resp(cs, peer, ch_id, pl, plen); break;
@ -287,18 +230,13 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
case CS_MSG_JOIN_READY: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_READY", CS_ID); cs_handle_join_ready(cs, peer, ch_id, pl, plen); break;
case CS_MSG_ERROR: {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: RECV ERROR from=%016llx ch=%s", CS_ID, (unsigned long long)peer, ch_id);
cs_cancel_proto_timers(cs);
if (cs->info_req_timer) { uasync_cancel_timeout(cs->inst->ua, cs->info_req_timer); cs->info_req_timer = NULL; }
uint8_t err[20]; memcpy(err, &peer, 8); int r = -1; memcpy(err + 8, &r, 4);
memcpy(err + 12, &cs->pending_invite_ch_id, 8); chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0;
cs_join_release_transport(cs);
break;
}
case CS_MSG_CHANNEL_INVITE: cs_handle_channel_invite(cs, peer, ch_id, pl, plen); break;
case CS_MSG_SOUND_SETTING:
if (plen >= 1)
db_sync_set_peer_sound(cs->inst, group_id, peer, pl[0] ? 1 : 0);
break;
default: DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: UNKNOWN msg type=%02x from=%016llx", CS_ID, type, (unsigned long long)peer); break;
}
u_free(entry->dgram); queue_entry_free(entry);
@ -319,7 +257,6 @@ static void cs_info_req_timeout_cb(void* arg) {
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs_join_release_transport(cs);
}
static void cs_join_timeout_cb(void* arg) {
@ -336,7 +273,6 @@ static void cs_join_timeout_cb(void* arg) {
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
cs_join_release_transport(cs);
}
static void cs_invite_send_timeout_cb(void* arg) {
@ -353,7 +289,6 @@ static void cs_invite_send_timeout_cb(void* arg) {
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
cs_join_release_transport(cs);
}
/* ── helper: find active ETCP_CONN for node ── */
@ -457,49 +392,6 @@ static void cs_schedule_sync(struct chat_sync* cs) {
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sync scheduled in %ums", CS_ID, CS_SYNC_INTERVAL_MS);
}
/* ── Peer sleep (keepalive-протокол): спячка → отменить сессии; пробуждение → пересинхронизация ── */
static void cs_on_peer_sleep(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, int sleeping, void* arg) {
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
if (sleeping) {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: peer sleep peer=0x%016llx — cancel member_sync sessions", CS_ID, (unsigned long long)peer_node_id);
merkle_sync_cancel_peer(inst, peer_node_id);
} else {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: peer awake peer=0x%016llx — schedule sync", CS_ID, (unsigned long long)peer_node_id);
cs_schedule_sync(cs);
}
}
/* ── Per-chat «play sound on message» (троттлинг db_sync на сервере) ── */
static void cs_send_sound_setting(struct chat_sync* cs, const char* ch_id, uint64_t dst, int on) {
uint8_t p[2] = { CS_MSG_SOUND_SETTING, (uint8_t)(on ? 1 : 0) };
cs_send(cs, ch_id, dst, p, sizeof(p));
}
void chat_sync_broadcast_sound_setting(struct UTUN_INSTANCE* inst, const char* ch_id, int on) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !cs->initialized || !ch_id || !ch_id[0]) return;
for (int i = 0; i < cs->channel_count; i++) {
if (strcmp(cs->channels[i].channel_id, ch_id) != 0) continue;
for (int j = 0; j < cs->channels[i].peer_count; j++) {
uint64_t pid = cs->channels[i].peer_ids[j];
if (pid == inst->node_id || !cs_is_peer_online(inst, pid)) continue;
cs_send_sound_setting(cs, ch_id, pid, on);
}
break;
}
}
/* На поднятии соединения: отправить пиру свои per-chat настройки звука (начальная синхронизация). */
static void cs_send_all_sound_settings(struct chat_sync* cs, uint64_t peer) {
for (int i = 0; i < cs->channel_count; i++) {
int on = chat_core_get_sound_on_message(cs->inst, cs->channels[i].channel_id);
cs_send_sound_setting(cs, cs->channels[i].channel_id, peer, on);
}
}
/* ── Peer status change (GUI + online-бар + schedule_sync; онлайн в БД не хранится) ── */
static void cs_on_peer_status_changed(struct chat_sync* cs, uint64_t peer, int online) {
@ -573,10 +465,7 @@ static void _on_invite_sync_done(uint64_t peer, const char* ns, int result, void
/* ── send JOIN_INFO_REQ to start join handshake ── */
static void cs_start_channel_join(struct chat_sync* cs, uint64_t peer) {
if (!cs || cs->info_req_timer || cs->join_timer) return;
if (cs->join_transport_timer) {
uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
}
if (!cs || cs->info_req_timer) return;
char ch_id_str[64];
snprintf(ch_id_str, sizeof(ch_id_str), "%llu", (unsigned long long)cs->pending_invite_ch_id);
/* payload: [join_key:8][pass_len:1][password] */
@ -606,10 +495,11 @@ static void cs_on_conn_up(struct ETCP_CONN* conn, int event, void* arg) { (void)
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up peer=%016llx pending_invite=%llu links_up=%d", CS_ID, (unsigned long long)peer, (unsigned long long)cs->pending_invite_ch_id, conn->links_up);
if (cs->pending_invite_ch_id != 0 && cs->pending_invite_node_id == peer) {
if (cs->join_transport_timer) {
uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
}
if (cs->pending_invite_ch_id != 0) {
if (cs->pending_invite_node_id != 0 && cs->pending_invite_node_id != peer)
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite node_id MISMATCH: invite=0x%016llx ETCP_peer=0x%016llx — invite is STALE!",
CS_ID, (unsigned long long)cs->pending_invite_node_id, (unsigned long long)peer);
cs->pending_invite_node_id = peer;
if (cs->pending_invite_is_inviter) {
/* we are the inviter — send CHANNEL_INVITE to the joiner */
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu",
@ -630,7 +520,6 @@ static void cs_on_conn_up(struct ETCP_CONN* conn, int event, void* arg) { (void)
}
cs_on_peer_status_changed(cs, peer, 1);
cs_send_all_sound_settings(cs, peer);
}
static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
@ -641,11 +530,8 @@ static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (voi
cs_on_peer_status_changed(cs, peer, 0);
cs_cancel_proto_timers(cs);
if (cs->pending_invite_node_id == peer) {
cs_cancel_proto_timers(cs);
if (cs->join_transport_timer) uasync_cancel_timeout(cs->inst->ua, cs->join_transport_timer);
cs->join_transport_timer = uasync_set_timeout(cs->inst->ua, CS_INFO_REQ_TIMEOUT_MS * 10,
cs, cs_join_connect_timeout, "join_reconnect_timeout");
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn down while waiting invite resp peer=%016llx, timers cancelled, state kept for retry on reconnect",
CS_ID, (unsigned long long)peer);
/* invite state survives connection flaps: only timeout or explicit protocol end clears it */
@ -662,10 +548,6 @@ static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (voi
static void cs_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) {
switch (status) {
case ETCP_CONN_STATUS_UP: cs_on_conn_up(conn, ETCP_CBK_EVENT_UP, arg); break;
case ETCP_CONN_STATUS_REINIT:
cs_on_conn_down(conn, ETCP_CBK_EVENT_DOWN, arg);
cs_on_conn_up(conn, ETCP_CBK_EVENT_UP, arg);
break;
case ETCP_CONN_STATUS_DOWN:
case ETCP_CONN_STATUS_DELETE: cs_on_conn_down(conn, ETCP_CBK_EVENT_DOWN, arg); break;
default: break;
@ -740,7 +622,6 @@ static void refresh_timer_cb(void* arg) {
int chat_sync_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (inst->chat_sync) return 0;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: init", CS_ID);
struct chat_sync* cs = u_calloc(1, sizeof(*cs));
if (!cs) return -1;
@ -748,11 +629,6 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
cs->initialized = 1;
inst->chat_sync = cs;
if (member_sync_init(inst) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member synchronization initialization failed", CS_ID);
inst->chat_sync = NULL; u_free(cs); return -1;
}
etcp_bind(inst, ETCP_RT_ID_CHAT_SYNC, chat_sync_recv_cb);
etcp_add_conn_status_cbk(inst, cs_on_conn_status, cs);
@ -763,8 +639,8 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
cs->info_req_timer = NULL;
cs->join_timer = NULL;
member_sync_init(inst);
member_sync_add_apply_cbk(inst, cs_on_member_applied, cs);
utun_add_peer_sleep_cbk(inst, cs_on_peer_sleep, cs);
chat_admin_init(inst);
chat_join_init(inst);
@ -780,7 +656,6 @@ void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
if (cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
if (cs->sync_scheduled) { cs->sync_scheduled = 0; cs_flush_sync(cs); }
member_sync_remove_apply_cbk(inst, cs_on_member_applied, cs);
utun_remove_peer_sleep_cbk(inst, cs_on_peer_sleep, cs);
cs_pending_free_all(cs);
member_sync_destroy(inst);
chat_admin_destroy(inst);
@ -794,7 +669,6 @@ void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
if (cs->refresh_wait) { standby_wait_cancel(cs->refresh_wait); cs->refresh_wait = NULL; }
#endif
cs_cancel_proto_timers(cs);
cs_join_release_transport(cs);
for (int i = 0; i < cs->channel_count; i++)
if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids);
@ -809,7 +683,11 @@ void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst) {
uint64_t gid = strtoull(cs->channels[i].channel_id, NULL, 10);
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
if (!group || !group->connect) continue;
topo_group_connect_restart(group);
if (topo_group_connect_active_count(group) == 0) {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: socket change — restarting connect for ch=%s (no active connections)",
CS_ID, cs->channels[i].channel_id);
topo_group_connect_restart(group);
}
}
}
@ -820,11 +698,6 @@ void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !ch_id || !ch_id[0]) return;
if (cs->pending_invite_ch_id == strtoull(ch_id, NULL, 10)) {
cs_cancel_proto_timers(cs);
cs->pending_invite_ch_id = 0; cs->pending_invite_node_id = 0;
cs_join_release_transport(cs);
}
for (int i = 0; i < cs->channel_count; i++) {
if (strcmp(cs->channels[i].channel_id, ch_id) != 0) continue;
if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids);
@ -959,13 +832,13 @@ static void cm_invite_trampoline(void* arg) {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"channel invite: link to 0x%016llx already up — skipping connect, join handshake directly",
(unsigned long long)node_id);
if (cs_join_hold_transport(cs, node_id, NULL) == 0) cs_start_channel_join(cs, node_id);
cs_start_channel_join(cs, node_id);
} else {
/* Транспорт принадлежит join-протоколу до его результата, а не будущей BGP-группе.
JOIN_INFO_REQ → … → JOIN_READY запускается по cs_on_conn_up. */
/* topo_group_invite_to_channel: прямое ncd-подключение к connection-узлу БЕЗ INVITE_INFO.
Реальный join-протокол (JOIN_INFO_REQ → … → JOIN_READY) запускается по cs_on_conn_up. */
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "channel invite: connecting to 0x%016llx via %d addresses",
(unsigned long long)node_id, inv->addr_count);
int r = cs_join_hold_transport(cs, node_id, ni);
int r = topo_group_invite_to_channel(inst, channel_id, ni, node_id);
if (r < 0)
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "channel invite: FAILED — topo_group_invite_to_channel error=%d", r);
}
@ -1018,9 +891,8 @@ void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_
(long long)channel_id, (long long)node_id, addr_count, password ? "yes" : "no");
#endif
#ifndef __ANDROID__
uint64_t pubkey_prefix; memcpy(&pubkey_prefix, pubkey_bin, sizeof(pubkey_prefix));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite start ch=%llu node=0x%016llx pubkey=%016llx... addrs=%d pass=%s",
CS_ID, (unsigned long long)channel_id, (unsigned long long)node_id, (unsigned long long)pubkey_prefix, addr_count, password && password[0] ? "yes" : "no");
CS_ID, channel_id, node_id, *(const uint64_t*)pubkey_bin, addr_count, password && password[0] ? "yes" : "no");
#endif
struct cm_invite_wrap* w = u_malloc(sizeof(struct cm_invite_wrap));
@ -1052,7 +924,6 @@ void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint6
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
if (cs_join_hold_transport(cs, target_node_id, NULL) < 0) return;
cs_start_channel_join(cs, target_node_id);
cs_on_peer_status_changed(cs, target_node_id, 1);
}
@ -1136,7 +1007,28 @@ static void cs_handle_join_info_req(struct chat_sync* cs, uint64_t peer,
return;
}
/* 2. инфо канала */
/* 2. пароль (если настроен на сервере) */
char req_password[128] = "";
if (len >= 10) {
uint8_t plen2 = pl[9];
if (plen2 > 0 && plen2 <= len - 10) {
if (plen2 > 127) plen2 = 127;
memcpy(req_password, pl + 10, plen2); req_password[plen2] = '\0';
}
}
{
struct global_config *gcfg = &cs->inst->config->global;
if (gcfg->chatserver_enabled && gcfg->chatserver_join_password[0]) {
if (strcmp(req_password, gcfg->chatserver_join_password) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_REQ wrong password ch=%s from=%016llx", CS_ID, ch_id, (unsigned long long)peer);
uint8_t err[2] = { CS_MSG_ERROR, 0x01 };
cs_send(cs, ch_id, peer, err, 2);
return;
}
}
}
/* 3. инфо канала */
char name[128]; uint64_t owner; uint8_t ch_x25519[32], ch_ed[32], ch_sig[64];
if (topo_node_sqlite_channel_get(cs->inst->topo_sqlite_db,
ch_id, name, (int)sizeof(name), &owner, ch_x25519, ch_ed, ch_sig) != 0) {
@ -1224,11 +1116,9 @@ static void cs_handle_join_info_resp(struct chat_sync* cs, uint64_t peer,
uint64_t c_signed_by; memcpy(&c_signed_by, p, 8); p += 8;
const uint8_t* c_signature = p; p += 64;
uint64_t ch_x_prefix, ch_ed_prefix;
memcpy(&ch_x_prefix, ch_x25519, sizeof(ch_x_prefix)); memcpy(&ch_ed_prefix, ch_ed, sizeof(ch_ed_prefix));
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_INFO_RESP from=%016llx owner=%016llx ch_x=%016llx ch_ed=%016llx name=%s conn_node=%016llx",
CS_ID, (unsigned long long)peer, (unsigned long long)owner,
(unsigned long long)ch_x_prefix, (unsigned long long)ch_ed_prefix, name, (unsigned long long)c_node_id);
*(const uint64_t*)ch_x25519, *(const uint64_t*)ch_ed, name, (unsigned long long)c_node_id);
/* verify channel signature */
uint8_t vmsg[1024]; size_t vlen = 0;
@ -1392,8 +1282,13 @@ static void cs_handle_join_request(struct chat_sync* cs, uint64_t peer,
return;
}
/* Ключ разрешает протокол добавления. BGP начнётся после сохранения мембера,
локально либо через Merkle; существующий транспорт обслуживает JOIN_REQUEST. */
/* валидный ключ → сразу добавляем узел в группу для BGP sync */
{
uint64_t gid = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(cs->inst->topo_groups, gid);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT)
topo_group_new_conn(g, jconn);
}
/* добавить в pending (ждут JOIN_READY по приходу рекорда через merkle) */
cs_pending_add(cs, m.node_id, ch_id, jconn);
@ -1406,14 +1301,6 @@ static void cs_handle_join_request(struct chat_sync* cs, uint64_t peer,
отправляем JOIN_READY напрямую, если мембер реально добавлен. */
if (topo_node_sqlite_member_in_channel(cs->inst->topo_sqlite_db, ch_id, m.node_id))
cs_send_join_ready(cs, ch_id, m.node_id);
/* join завершён — снять pending-состояние инвайтера и таймер ожидания */
if (cs->pending_invite_ch_id == channel_id && cs->pending_invite_node_id == m.node_id) {
if (cs->invite_send_timer) { uasync_cancel_timeout(cs->inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
cs->pending_invite_ch_id = 0;
cs->pending_invite_node_id = 0;
cs->pending_invite_is_inviter = 0;
cs_join_release_transport(cs);
}
} else {
chat_join_forward_request(cs->inst, channel_id, inviter, join_key, &m);
}
@ -1437,7 +1324,7 @@ static void cs_handle_join_ready(struct chat_sync* cs, uint64_t peer,
struct TOPO_GROUP* g = topo_groups_find(cs->inst->topo_groups, group_id);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) {
struct ETCP_CONN* c = cs_find_conn_for_node(cs->inst, peer);
if (c) topo_group_connect_node_once(g, c->peer_node_id);
if (c) topo_group_new_conn(g, c);
}
}
chat_core_ensure_channel_ready(cs->inst, ch_id);
@ -1454,7 +1341,6 @@ static void cs_handle_join_ready(struct chat_sync* cs, uint64_t peer,
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, cevt, 20);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY invite success ch=%s peer=%016llx", CS_ID, ch_id, (unsigned long long)peer);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0; cs->pending_join_key = 0;
cs_join_release_transport(cs);
}
}
@ -1485,10 +1371,6 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
/* skip inviter_update_sig/ts/userinfo for now — verify channel sig only */
p += 64; uint64_t inviter_update_ts; memcpy(&inviter_update_ts, p, 8); p += 8;
uint8_t userinfo_len = *p++; p += userinfo_len;
/* join-ключ (зарегистрирован инвайтером) — хвостовые 8 байт */
uint64_t join_key = 0;
if (p + 8 > pl + len) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE truncated (no join_key) from=%016llx", CS_ID, (unsigned long long)peer); return; }
memcpy(&join_key, p, 8); p += 8;
/* Verify channel signature */
uint8_t vmsg[1024]; size_t vlen = 0;
@ -1523,30 +1405,16 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
/* Save channel and start join handshake */
int cprc = topo_node_sqlite_channel_put(cs->inst->topo_sqlite_db,
ch_id, name, owner, x25519, NULL, ed_pub, NULL, ch_sig);
uint64_t ch_x_prefix; memcpy(&ch_x_prefix, x25519, sizeof(ch_x_prefix));
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE channel_put rc=%d ch=%s ch_x25519=%016llx",
CS_ID, cprc, ch_id, (unsigned long long)ch_x_prefix);
CS_ID, cprc, ch_id, *(const uint64_t*)x25519);
chat_core_ensure_channel_ready(cs->inst, ch_id);
/* Initiate join: connect back and send JOIN_INFO_REQ с зарегистрированным ключом */
cs->pending_join_key = join_key;
/* Initiate join: connect back and send CHANNEL_INFO_REQ */
chat_sync_join_channel(cs->inst, ch_id, peer);
return;
}
/* policy == ask — post event to GUI */
{
/* сохранить join-ключ, чтобы принять инвайт позже через chat_sync_accept_invite */
uint64_t ch_num = strtoull(ch_id, NULL, 10);
int slot = -1;
for (int i = 0; i < cs->pending_inv_in_count; i++)
if (cs->pending_inv_in_ch_id[i] == ch_num && cs->pending_inv_in_node_id[i] == peer) { slot = i; break; }
if (slot < 0 && cs->pending_inv_in_count < 4) slot = cs->pending_inv_in_count++;
if (slot >= 0) {
cs->pending_inv_in_ch_id[slot] = ch_num;
cs->pending_inv_in_node_id[slot] = peer;
cs->pending_inv_in_key[slot] = join_key;
}
size_t inv_name_len = 0; char inv_name[128] = "";
{ sqlite3* vdb = cs->inst->topo_sqlite_db;
sqlite3_stmt* st = NULL;
@ -1589,15 +1457,6 @@ static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* in
int ulen = member_sync_build_update_msg(join_sig, upd_ts, juser, umsg, (int)sizeof(umsg));
if (ulen >= 0) cs_ed25519_sign(inst->my_ed25519_privkey, umsg, (size_t)ulen, upd_sig);
}
/* join-ключ: генерируем и регистрируем локально (инвайтер == connection-узел),
чтобы джойнер смог пройти JOIN_INFO_REQ с валидным ключом. */
uint64_t join_key = 0;
if (RAND_bytes((uint8_t*)&join_key, sizeof(join_key)) != 1 || join_key == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_key generation failed ch=%s", CS_ID, ch_id);
return -1;
}
if (chat_join_store_local_key(inst, strtoull(ch_id, NULL, 10), join_key) != 0) return -1;
uint8_t buf[1024]; size_t boff = 0;
buf[boff++] = CS_MSG_CHANNEL_INVITE;
uint8_t cnl = (uint8_t)strlen(ch_name);
@ -1617,10 +1476,9 @@ static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* in
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", inst->name[0] ? inst->name : "");
uint8_t il = (uint8_t)strlen(juser);
buf[boff++] = il; memcpy(buf + boff, juser, il); boff += il;
memcpy(buf + boff, &join_key, 8); boff += 8;
int rc = cs_send(cs, ch_id, target_node_id, buf, boff);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sent CHANNEL_INVITE to 0x%016llx ch=%s key=%016llx rc=%d",
CS_ID, (unsigned long long)target_node_id, ch_id, (unsigned long long)join_key, rc);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: sent CHANNEL_INVITE to 0x%016llx ch=%s rc=%d",
CS_ID, (unsigned long long)target_node_id, ch_id, rc);
return rc;
}
@ -1663,7 +1521,7 @@ void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id,
}
}
/* Адреса грузятся из node_addresses в БД внутри node_conn_direct_open */
int r = cs_join_hold_transport(cs, target_node_id, NULL);
int r = topo_group_invite_to_channel(inst, channel_id, NULL, target_node_id);
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel topo_group_invite_to_channel failed rc=%d", CS_ID, r);
}
@ -1679,8 +1537,6 @@ void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id,
}
/* Already connected — send invite now, wait for joiner to respond */
if (cs_join_hold_transport(cs, target_node_id, NULL) < 0) return;
uasync_cancel_timeout(inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
cs_send_channel_invite(cs, inst, ch_id, target_node_id, ch_name, owner, ch_x25519, ch_ed, ch_sig);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
@ -1724,8 +1580,6 @@ static void cs_invite_trampoline(void* arg) {
char ch_name[128]; uint8_t ch_x2[32], ch_ed2[32], ch_sig[64]; uint64_t ch_owner;
if (topo_node_sqlite_channel_get(inst->topo_sqlite_db,
ch_id, ch_name, (int)sizeof(ch_name), &ch_owner, ch_x2, ch_ed2, ch_sig) == 0) {
if (cs_join_hold_transport(cs, target_node_id, NULL) < 0) { u_free(w); return; }
uasync_cancel_timeout(inst->ua, cs->join_transport_timer); cs->join_transport_timer = NULL;
cs_send_channel_invite(cs, inst, ch_id, target_node_id, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
@ -1782,7 +1636,7 @@ static void cs_invite_trampoline(void* arg) {
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
int r = cs_join_hold_transport(cs, target_node_id, ni);
int r = topo_group_invite_to_channel(inst, channel_id, ni, target_node_id);
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs topo_group_invite_to_channel failed rc=%d", CS_ID, r);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0;
@ -1830,31 +1684,16 @@ void chat_sync_invite_to_channel_with_addrs(
/* ─── accept/deny incoming invite ─── */
void chat_sync_accept_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_node_id) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !inst) return;
if (!cs_of(inst) || !inst) return;
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)channel_id);
if (channel_id == 0 || inviter_node_id == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite invalid params ch=%llu node=%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
return;
}
/* достать join-ключ, сохранённый при приёме CHANNEL_INVITE (ask) */
uint64_t join_key = 0;
for (int i = 0; i < cs->pending_inv_in_count; i++) {
if (cs->pending_inv_in_ch_id[i] == channel_id && cs->pending_inv_in_node_id[i] == inviter_node_id) {
join_key = cs->pending_inv_in_key[i];
break;
}
}
if (join_key == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite no join_key for ch=%llu inviter=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
return;
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite ch=%llu inviter=0x%016llx key=%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id, (unsigned long long)join_key);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite ch=%llu inviter=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
chat_core_ensure_channel_ready(inst, ch_id);
cs->pending_join_key = join_key;
chat_sync_join_channel(inst, ch_id, inviter_node_id);
}

53
src/chat/chat_sync.h

@ -1,10 +1,25 @@
/*
* chat_sync — связь чат-сервиса с транспортом и группами.
* Отслеживает пиров, запускает member_sync, поддерживает кеш каналов и статус в GUI.
* Вход по ссылке держит отдельный NCD handle до результата join, а не только до UP.
* JOIN_INFO_REQ → JOIN_INFO_RESP → JOIN_REQUEST → JOIN_READY; правила — в chat_join.h.
* Жизненным циклом управляет chat_service_start/stop. Вызовы — в потоке uasync,
* кроме явно отмеченной обёртки, которая копирует аргументы и постит работу в этот поток.
* chat_sync.h — оркестратор синхронизации чата по ETCP
*
* При поднятии / разрыве ETCP-соединений:
* - обновляет online-статус пиров в БД и GUI
* - запускает member_sync (синхронизацию участников каналов) через merkle_sync
* - управляет кешем каналов (периодический refresh из БД, 30s)
*
* Обслуживает протокол входа в канал (join) между джойнером, connection-узлом и инвайтером:
* JOIN_INFO_REQ → JOIN_INFO_RESP → JOIN_REQUEST → (форвард инвайтеру) → JOIN_READY.
* Полное описание flow и дерева приглашений — в chat_join.h.
*
* Обрабатывает invite-ссылки (chat_sync_connect_from_invite):
* - сохраняет pubkey, join_key и адреса invite-узла в БД (nodes, node_addresses)
* - создаёт TOPO_GROUP, запускает conn_mgr для подключения
* - после поднятия ETCP-соединения запускает join-протокол
*
* Использование:
* 1. chat_sync_init(inst) — вызывается при старте, биндит ETCP_RT_ID_CHAT_SYNC (0x30)
* 2. chat_sync_connect_from_invite(ch_id, node_id, pubkey, addrs, count) — вход по invite
* 3. chat_sync_join_channel(inst, ch_id, target_node_id) — вход через уже подключённый узел
* 4. chat_sync_destroy(inst) — вызывается при завершении
*/
#ifndef CHAT_SYNC_H
@ -20,8 +35,9 @@ extern "C" {
struct UTUN_INSTANCE;
struct UASYNC;
/* etcp service IDs (direct P2P): ETCP_RT_ID_CHAT_SYNC / ETCP_RT_ID_MEMBER_SYNC
* (etcp_bind, см. etcp_api.h) */
/* etcp service ID (direct P2P) */
#define ETCP_RT_ID_CHAT_SYNC 0x30
#define ETCP_RT_ID_MEMBER_SYNC 0x31
/* Message types */
#define CS_MSG_INIT_SYNC 0x01
@ -36,7 +52,6 @@ struct UASYNC;
#define CS_MSG_JOIN_INFO_RESP 0x11 /* connection → joiner: channel info + connection's member record */
#define CS_MSG_JOIN_REQUEST 0x12 /* joiner → connection: {join_key:8, member...} */
#define CS_MSG_JOIN_READY 0x13 /* connection → joiner: {group_id:8} */
#define CS_MSG_SOUND_SETTING 0x14 /* peer → server: {sound_on:1} — per-chat play sound on message */
/* Protocol constants */
#define CS_SEND_DATA_MAX 32
@ -50,17 +65,15 @@ struct UASYNC;
/* ── Public API ── */
/* Создать контекст, обработчики join и member_sync. 0/-1; повтор допустим. */
int chat_sync_init(struct UTUN_INSTANCE* inst);
/* Отменить ожидания join, синхронизацию и таймеры; не удаляет данные каналов. */
void chat_sync_destroy(struct UTUN_INSTANCE* inst);
/* Сейчас пустая функция. Для подключения использовать chat_core_connect_node с каналом. */
/* Прямое ETCP-подключение к узлу (pubkey+адреса из БД) */
void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* Подключиться к пиру по данным invite-ссылки (вызывается из uasync-потока).
join_key — 64-битный ключ входа из ссылки (регистрируется инвайтером на connection-узле).
password может быть NULL — тогда передаётся пустой пароль. */
password может быть NULL — тогда отправляется без пароля (совместимость с v1) */
void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count,
@ -69,7 +82,7 @@ void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_
/* Пригласить target_node_id в канал ch_id с адресами из invite-ссылки (inviter → joiner).
Принимает pubkey и адреса целевого узла. Если соединения ещё нет — строит TOPO_NODE
с адресами, удерживает NCD для join, и по UP отправляет CS_MSG_CHANNEL_INVITE.
с адресами, запускает conn_mgr для подключения, и по conn_up отправляет CS_MSG_CHANNEL_INVITE.
Если соединение уже есть — отправляет CHANNEL_INVITE сразу.
Копирует pubkey_bin и addrs_data (вызывающий может освободить после возврата).
Вызов из любого потока — внутри постит в uasync. */
@ -82,7 +95,7 @@ void chat_sync_invite_to_channel_with_addrs(
Отправляет CS_MSG_CHANNEL_INVITE с полной информацией о канале и своих адресах */
void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
/* Принять входящее приглашение — продолжить протокол из chat_join.h. */
/* Принять входящее приглашение — продолжаем join-протокол (CHANNEL_JOIN → WELCOME) */
void chat_sync_accept_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_node_id);
/* Отклонить входящее приглашение */
@ -90,22 +103,18 @@ void chat_sync_deny_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint
/* Присоединиться к каналу через уже подключённый узел (uasync-поток).
Соединение с target_node_id должно быть установлено (links_up && initialized).
Протокол: JOIN_INFO_REQ → JOIN_INFO_RESP → JOIN_REQUEST → JOIN_READY (chat_join.h).
Канал (DB + TOPO_GROUP) создаётся локально при получении JOIN_INFO_RESP;
это само по себе не даёт членства на других узлах. */
Протокол: CHANNEL_INFO_REQ → CHANNEL_INFO_RESP → CHANNEL_JOIN → WELCOME.
Канал (DB + TOPO_GROUP) создаётся локально при получении CHANNEL_INFO_RESP. */
void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
/* Перезапустить групповые подключения для каналов без активных соединений.
Вызывается из auto_socket при появлении/изменении сетевой связности. */
void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst);
/* Немедленно убрать канал из кеша chat_sync (локальное удаление канала),
/* Немедленно убрать канал из кеша g_cs->channels (локальное удаление канала),
* чтобы cs_on_conn_status не запускал member_sync для удалённого ns до следующего refresh. */
void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Разослать всем подключённым пирам канала смену per-chat настройки «play sound on message». */
void chat_sync_broadcast_sound_setting(struct UTUN_INSTANCE* inst, const char* ch_id, int on);
#ifdef __cplusplus
}
#endif

2
src/chat/chat_whisper.c

@ -345,8 +345,8 @@ static void wh_work_fn(void* raw) {
}
static void wh_done_fn(void* raw, int err) {
(void)err;
struct wh_work_ctx* w = (struct wh_work_ctx*)raw;
if (err) w->err = err;
g_processing = 0;

102
src/chat/db_sync.c

@ -25,7 +25,6 @@
// ---- Forward declarations ----
struct DB_SYNC;
struct DB_SYNC_INSTANCE;
struct SI_PEER;
static void db_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry);
static void db_sync_on_conn_status(struct ETCP_CONN* conn, int status, void* arg);
@ -39,7 +38,6 @@ static int db_sync_send_gid(struct DB_SYNC* db, uint64_t node_id, uint64_t grou
static void db_sync_initiate_sync(struct DB_SYNC_INSTANCE* si, uint64_t peer_node_id);
static int si_send_data_batch(struct DB_SYNC_INSTANCE* si, uint64_t dst, uint32_t from, uint32_t count, uint32_t vp, uint32_t want_from);
static void si_delivery_update(struct DB_SYNC_INSTANCE* si, uint64_t ts, uint64_t author, uint64_t peer_id);
static int db_sync_peer_throttled(struct DB_SYNC* db, struct SI_PEER* p);
// ============================================================
// Structures
@ -53,7 +51,6 @@ struct SI_PEER {
uint8_t sync_state;
uint64_t sync_start_tb;
uint32_t retry_count;
uint8_t sound_on; /* 1 = пиру важен этот чат (play sound on message), не троттлим */
};
struct DB_SYNC_INSTANCE {
@ -240,11 +237,6 @@ struct DB_SYNC_INSTANCE* db_sync_instance_find(struct UTUN_INSTANCE* inst, uint6
return db_instance_find(inst->db_sync, group_id);
}
uint64_t db_sync_instance_group_id(struct DB_SYNC_INSTANCE* si)
{
return si ? si->group_id : 0;
}
// Выделяет новый инстанс в куче (стабильный указатель) и регистрирует его в массиве
static struct DB_SYNC_INSTANCE* db_instance_alloc(struct DB_SYNC* db)
{
@ -314,7 +306,6 @@ static struct SI_PEER* si_peer_add(struct DB_SYNC_INSTANCE* si, uint64_t node_id
p = &si->peers[si->peer_count++];
memset(p, 0, sizeof(*p));
p->node_id = node_id;
p->sound_on = 1;
return p;
}
@ -569,10 +560,9 @@ static int db_verify_author_sig(struct DB_SYNC_INSTANCE* si,
memcpy(msg + off, &ts, 8); off += 8;
if (jlen > 0) { memcpy(msg + off, json, jlen); off += jlen; }
if (sc_ed25519_verify(pubkey, msg, off, sig) != SC_OK) {
uint64_t key_prefix, sig_prefix; memcpy(&key_prefix, pubkey, 8); memcpy(&sig_prefix, sig, 8);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC,
"author_sig VERIFY FAIL author=%016llx ed_pubkey=%016llx... msg_len=%zu sig=%016llx... — discarding as forgery",
(unsigned long long)author_node_id, (unsigned long long)key_prefix, off, (unsigned long long)sig_prefix);
(unsigned long long)author_node_id, *(uint64_t*)pubkey, off, *(uint64_t*)sig);
return -1;
}
return 0;
@ -656,12 +646,12 @@ static int db_record_insert_cascade(struct DB_SYNC_INSTANCE* si,
uint64_t rid, uint64_t rts, uint64_t rauthor,
const char* rdata, uint32_t rdlen,
const uint8_t* rsig, uint64_t source_peer,
const char* local_attrs, int initial_sync)
const char* local_attrs)
{
int ret = db_record_insert(si, rid, rts, rauthor, rdata, rdlen, rsig, local_attrs);
if (ret != 0) return ret;
if (si->on_insert) si->on_insert(si, rts, rdata, rdlen, rauthor, initial_sync, si->on_insert_arg);
if (si->on_insert) si->on_insert(si, rts, rdata, rdlen, rauthor, si->on_insert_arg);
uint8_t pbuf[4096]; uint32_t poff = 1; pbuf[0] = DB_MSG_PUSH;
memcpy(pbuf + poff, &rid, 8); poff += 8;
@ -677,7 +667,6 @@ static int db_record_insert_cascade(struct DB_SYNC_INSTANCE* si,
for (int i = 0; i < si->peer_count; i++) {
uint64_t pid = si->peers[i].node_id;
if (si->peers[i].sync_state >= 1 && pid != source_peer && pid != my_id) {
if (db_sync_peer_throttled(si->db_sync, &si->peers[i])) continue; /* спящий пир в «тихом» чате — не пушим */
if (db_sync_send(si, pid, pbuf, poff) >= 0)
{ si_delivery_update(si, rts, rauthor, pid); push_count++; }
}
@ -711,11 +700,10 @@ static int db_sync_send_gid(struct DB_SYNC* db, uint64_t node_id, uint64_t group
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync: send DROP — no conn to %04llX (group=%016llx, type=%02x)",
(unsigned long long)(node_id >> 16), group_id, plen > 0 ? payload[0] : 0,
(void*)conn, conn ? conn->links_up : -1);
queue_dgram_free(entry); queue_entry_free(entry);
queue_entry_free(entry);
return -1;
}
int ret = etcp_send(conn, entry);
if (ret != 0) { queue_dgram_free(entry); queue_entry_free(entry); }
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "sync: send OK → etcp_send ret=%d conn=%s l_up=%d init=%d q=%p",
ret, conn->log_name, conn->links_up, conn->initialized, (void*)conn->send_input_q);
return ret;
@ -809,7 +797,7 @@ static void db_handle_init_sync(struct DB_SYNC_INSTANCE* si, uint64_t src, const
{
if (len < 4) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "INIT_SYNC too short %zu from %016llx", len, (unsigned long long)src); return; }
db_sync_flush_recalc(si);
uint32_t pc; memcpy(&pc, p, 4);
uint32_t pc = *(uint32_t*)p;
uint32_t mc = db_count(si);
uint32_t tp = (pc < mc ? pc : mc);
if (tp > 0) tp--;
@ -862,7 +850,7 @@ static void db_handle_init_resp(struct DB_SYNC_INSTANCE* si, uint64_t src, const
{
if (len < 14) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "INIT_RESP too short %zu from %016llx", len, (unsigned long long)src); return; }
db_sync_flush_recalc(si);
uint32_t tp; memcpy(&tp, p, 4);
uint32_t tp = *(uint32_t*)p;
uint64_t peer_ch8; memcpy(&peer_ch8, p + 4, 8);
uint8_t has_tail = p[12];
uint8_t sc = p[13];
@ -919,7 +907,7 @@ static void db_handle_init_resp(struct DB_SYNC_INSTANCE* si, uint64_t src, const
// Mismatch: find first divergent position
uint32_t fm = tp;
for (int i = 0; i < sc && spr + 12 <= p + len; i++) {
uint32_t pos; memcpy(&pos, spr, 4); uint64_t pch8; memcpy(&pch8, spr + 4, 8); spr += 12;
uint32_t pos = *(uint32_t*)spr; uint64_t pch8; memcpy(&pch8, spr + 4, 8); spr += 12;
uint64_t mch8;
if (db_chain_hash8_at(si, pos, &mch8) == 0) {
if (mch8 != pch8 && pos < fm) fm = pos;
@ -951,7 +939,7 @@ static int si_send_data_batch(struct DB_SYNC_INSTANCE* si, uint64_t dst, uint32_
buf[off++] = DB_MSG_SEND_DATA;
memcpy(buf + off, &from, 4); off += 4;
uint16_t rc = 0;
uint8_t* rcp = buf + off; off += 2;
uint16_t* rcp = (uint16_t*)(buf + off); off += 2;
memcpy(buf + off, &vp, 4); off += 4;
memcpy(buf + off, &want_from, 4); off += 4; // NEW: request peer's data from this position, DB_WANT_FROM_NONE=none
@ -1000,7 +988,7 @@ static int si_send_data_batch(struct DB_SYNC_INSTANCE* si, uint64_t dst, uint32_
SI_SHRT(si), (unsigned long long)(dst >> 16), from, count);
}
memcpy(rcp, &rc, 2);
*rcp = rc;
db_sync_send(si, dst, buf, off);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] → SEND_DATA: %u recs from=%u want=%u vp=%u",
SI_SHRT(si), (unsigned long long)(dst >> 16), rc, from, want_from, vp);
@ -1017,10 +1005,10 @@ static int si_parse_record(const uint8_t** pp, const uint8_t* end,
const uint8_t** rsig, int* rsiglen)
{
if (*pp + 28 > end) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "si_parse_record: truncated header, need 28 have %td", end - *pp); return -1; }
memcpy(rid, *pp, 8); *pp += 8;
memcpy(rts, *pp, 8); *pp += 8;
memcpy(rauthor, *pp, 8); *pp += 8;
memcpy(rdlen, *pp, 4); *pp += 4;
*rid = *(uint64_t*)*pp; *pp += 8;
*rts = *(uint64_t*)*pp; *pp += 8;
*rauthor = *(uint64_t*)*pp; *pp += 8;
*rdlen = *(uint32_t*)*pp; *pp += 4;
if (*pp + *rdlen > end) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "si_parse_record: data overrun dlen=%u have=%td", *rdlen, end - *pp); return -1; }
*rdata = (*rdlen > 0) ? *pp : NULL;
@ -1045,10 +1033,10 @@ static void db_handle_send_data(struct DB_SYNC_INSTANCE* si, uint64_t src, const
{
if (len < 14) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← SEND_DATA: truncated len=%zu (need >=14)", SI_SHRT(si), (unsigned long long)(src >> 16), len); return; }
db_sync_flush_recalc(si);
uint32_t from; memcpy(&from, p, 4);
uint16_t count; memcpy(&count, p + 4, 2);
uint32_t vp; memcpy(&vp, p + 6, 4);
uint32_t want_from; memcpy(&want_from, p + 10, 4);
uint32_t from = *(uint32_t*)p;
uint16_t count = *(uint16_t*)(p + 4);
uint32_t vp = *(uint32_t*)(p + 6);
uint32_t want_from = *(uint32_t*)(p + 10);
struct SI_PEER* sp = si_peer_find(si, src);
@ -1057,7 +1045,7 @@ static void db_handle_send_data(struct DB_SYNC_INSTANCE* si, uint64_t src, const
for (uint16_t i = 0; i < count && i < 32; i++) {
uint64_t rid, rts, rauthor; uint32_t rdlen; const uint8_t* rdata, *rsig; int rsiglen;
if (si_parse_record(&ptr, p + len, &rid, &rts, &rauthor, &rdlen, &rdata, &rsig, &rsiglen) != 0) break;
int ret = db_record_insert_cascade(si, rid, rts, rauthor, (const char*)rdata, rdlen, rsig, src, NULL, 1);
int ret = db_record_insert_cascade(si, rid, rts, rauthor, (const char*)rdata, rdlen, rsig, src, NULL);
if (ret >= 0) received++;
if (ret == 1) duplicates++;
if (ret == -2) bad_sigs++;
@ -1111,7 +1099,7 @@ static void db_handle_sync_done(struct DB_SYNC_INSTANCE* si, uint64_t src, const
{
if (len < 12) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "SYNC_DONE too short %zu from %016llx", len, (unsigned long long)src); return; }
db_sync_flush_recalc(si);
uint32_t pc; memcpy(&pc, p, 4);
uint32_t pc = *(uint32_t*)p;
uint64_t pch8; memcpy(&pch8, p + 4, 8);
uint32_t mc = db_count(si);
uint64_t mch8 = 0; if (mc > 0) db_chain_hash8_at(si, mc - 1, &mch8);
@ -1153,8 +1141,8 @@ static void db_handle_sync_done(struct DB_SYNC_INSTANCE* si, uint64_t src, const
static void db_handle_ack_push(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint8_t* p, size_t len)
{
if (len < 16) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s:%04llX] ← PUSH ACK: truncated len=%zu (need >=16)", SI_SHRT(si), (unsigned long long)(src >> 16), len); return; }
uint64_t ts; memcpy(&ts, p, 8);
uint64_t author; memcpy(&author, p + 8, 8);
uint64_t ts = *(uint64_t*)p;
uint64_t author = *(uint64_t*)(p + 8);
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "from=%016llx ts=%llu author=%016llx", (unsigned long long)src, (unsigned long long)ts, (unsigned long long)author);
sqlite3_stmt* stmt;
@ -1189,7 +1177,7 @@ static void db_handle_push(struct DB_SYNC_INSTANCE* si, uint64_t src, const uint
if (si_parse_record(&ptr, p + len, &rid, &rts, &rauthor, &rdlen, &rdata, &rsig, &rsiglen) != 0) return;
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "from=%016llx id=%llu author=%016llx ts=%llu len=%u", (unsigned long long)src, (unsigned long long)rid, (unsigned long long)rauthor, (unsigned long long)rts, rdlen);
int ret = db_record_insert_cascade(si, rid, rts, rauthor, (const char*)rdata, rdlen, rsig, src, NULL, 0);
int ret = db_record_insert_cascade(si, rid, rts, rauthor, (const char*)rdata, rdlen, rsig, src, NULL);
if (ret == 0) {
uint32_t ins_pos = si_find_pos(si, rts, rsig);
uint32_t total = db_count(si);
@ -1225,7 +1213,7 @@ static void db_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry)
if (!db || !db->enabled) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t src = conn ? conn->peer_node_id : 0;
uint64_t group_id; memcpy(&group_id, entry->dgram + 1, 8); group_id = be64toh(group_id);
uint64_t group_id = be64toh(*(uint64_t*)(entry->dgram + 1));
uint8_t type = entry->dgram[9];
const uint8_t* payload = entry->dgram + 10;
size_t plen = entry->len - 10;
@ -1481,22 +1469,6 @@ void db_sync_peer_set_state(struct DB_SYNC_INSTANCE* si, uint64_t node_id, uint8
if (p) p->sync_state = state;
}
// Задать per-(chat,peer) флаг «play sound on message» (троттлинг: sound_off + спящий пир → пропуск).
void db_sync_set_peer_sound(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t peer_node_id, int sound_on)
{
if (!inst || !inst->db_sync) return;
struct DB_SYNC* db = inst->db_sync;
struct DB_SYNC_INSTANCE* si = db_instance_find(db, group_id);
if (!si) return; /* чат ещё не создан у нас — настройка не критична */
struct SI_PEER* p = si_peer_add(si, peer_node_id);
if (p && p->sound_on != (uint8_t)(sound_on ? 1 : 0)) {
p->sound_on = sound_on ? 1 : 0;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "sync [%s] peer=%016llx sound_on=%d (throttle=%s)",
SI_SHRT(si), (unsigned long long)peer_node_id, p->sound_on,
p->sound_on ? "off" : "on");
}
}
// Гейт авто-синхронизации: пока gated — инстанс не запускает sync автоматически
// (ждёт member_sync/pubkeys). При снятии гейта запускает sync к пирам в sync_state==0.
void db_sync_instance_set_gated(struct DB_SYNC_INSTANCE* si, int gated)
@ -1574,15 +1546,6 @@ static void db_sync_resume_peer_check(struct DB_SYNC* db)
// Периодическая проверка: запускает синхронизацию для пиров в состоянии sync_state==0,
// детектирует таймауты синхронизации (DB_SYNC_SYNC_TIMEOUT секунд), логирует зависшие.
// Work-driven: перевзводится только пока есть пиры с sync_state != 2.
/* Троттлинг: пир в SLEEP-фазе И чат для него «тихий» (sound_on=0) — пропускаем. */
static int db_sync_peer_throttled(struct DB_SYNC* db, struct SI_PEER* p) {
if (!db || !p) return 0;
if (p->sound_on) return 0; /* чат пиру важен (play sound) — не троттлим */
struct ETCP_CONN* conn = instance_find_conn(db->inst, p->node_id);
return conn && conn->peer_sleep_phase;
}
static void db_sync_peer_check_cb(void* arg)
{
struct DB_SYNC* db = (struct DB_SYNC*)arg;
@ -1597,13 +1560,13 @@ static void db_sync_peer_check_cb(void* arg)
#endif
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "instances=%d", db->instance_count);
struct TOPO_GROUP* g = topo_groups_get_default(db->inst->topo_groups);
int total_synced = 0, total_skipped = 0, any_peers = 0;
char launched_list[256] = "";
{
if (g) {
for (int i = 0; i < db->instance_count; i++) {
struct DB_SYNC_INSTANCE* si = db->instances[i];
struct TOPO_GROUP* g = topo_groups_find(db->inst->topo_groups, si->group_id);
if (!g) continue;
if (!si->enabled) continue;
if (si->sync_gated) continue;
@ -1622,7 +1585,6 @@ static void db_sync_peer_check_cb(void* arg)
uint32_t min_pos = UINT32_MAX;
for (int j = 0; j < si->peer_count; j++) {
if (si->peers[j].sync_state == 0 && si->peers[j].synced_pos < min_pos) {
if (db_sync_peer_throttled(db, &si->peers[j])) continue; /* спящий пир в «тихом» чате — пропускаем */
min_pos = si->peers[j].synced_pos;
best = &si->peers[j];
}
@ -1736,14 +1698,9 @@ int db_sync_init(struct UTUN_INSTANCE* inst)
return 0;
}
db->enabled = 1;
inst->db_sync = db;
return db_sync_enable(inst);
}
int db_sync_enable(struct UTUN_INSTANCE* inst) {
struct DB_SYNC* db = inst ? inst->db_sync : NULL;
if (!db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "db_sync enable requires initialized core"); return -1; }
if (db->enabled) return 0;
const char* dp = inst->config->global.db_path;
if (inst->topo_sqlite_db) {
db->db = inst->topo_sqlite_db; db->shared_db = 1;
@ -1754,11 +1711,10 @@ int db_sync_enable(struct UTUN_INSTANCE* inst) {
else snprintf(sp, sizeof(sp), "/tmp/utun_db_sync");
if (db_sqlite_open(db, sp) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "SQLite open failed, sync disabled");
return -1;
db->enabled = 0;
}
}
db->enabled = 1;
etcp_bind(inst, ETCP_RT_ID_DB_SYNC, db_sync_recv_cb);
etcp_add_conn_status_cbk(inst, db_sync_on_conn_status, NULL);
@ -1924,7 +1880,7 @@ int db_sync_insert_signed(struct DB_SYNC_INSTANCE* si, const char* json_data, si
uint64_t id = si->next_id;
uint64_t author_node_id = si->db_sync->inst->node_id;
return db_record_insert_cascade(si, id, ts, author_node_id, json_data, (uint32_t)len, sig, author_node_id, local_attrs, 0);
return db_record_insert_cascade(si, id, ts, author_node_id, json_data, (uint32_t)len, sig, author_node_id, local_attrs);
}
// Возвращает количество записей в инстансе

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save