Compare commits

..

21 Commits

Author SHA1 Message Date
evgeny 1c51df039f Fix call gap tone to follow 500ms of missing jitter PCM 23 hours ago
evgeny 9e3a417059 Synchronize standby transitions and immediate keepalive phase announcements 23 hours ago
evgeny e914a1bdc1 Improve Android member details contrast 24 hours ago
evgeny b36c685cfa Show ongoing Android calls on the lock screen 1 day ago
evgeny 3c5c9604b3 Keep Android calls active independently of screen and UI 1 day ago
evgeny fb37ca747a 1 1 day ago
evgeny 2d21c53b4b Improve call buffering, CM handle lifecycle and radio PTT controls 1 day ago
evgeny 752c1f140c Add unified Android dark theme and improve UI contrast 1 day ago
evgeny db3cacd33e Rename Android app to vibechat 1 day ago
evgeny 7ebc282eb2 Prioritize supernodes in invites and update chat background and ringtone 1 day ago
evgeny 0d44ada922 Improve Android radio capture lifecycle, VAD and PTT controls 1 day ago
evgeny 52ffb91101 Fix TCP proxy use-after-free during synchronous close 1 day ago
evgeny 745ec3dbd2 android radio: preserve output route on reconfigure, link input/output settings 2 days ago
evgeny 15958a078e android: self-managed Telecom ConnectionService for call audio routing 2 days ago
evgeny 61bf235ef4 radio: PTT double-tap latch + single-tap off (in-chat & overlay); compressor initial gain = mid-range 2 days ago
evgeny 556b0bc7b7 call/radio: mic AGC compressor + selectable input device; proximity lock 2 days ago
evgeny f12f61d7ee call: add SpeexDSP AEC (acoustic echo cancellation) for desktop + Android 2 days ago
evgeny 1176411b79 radio: Silero VAD + auto-PTT; chatgui/android: app icon and background 2 days ago
evgeny 74ca98b38f proxy: rename pcb guard, dedupe abort; tasks: FIN_WAIT_2 leak 2 days ago
evgeny 961a234d7d proxy: detach lwIP pcb on local close, guard abort against freed pcb 2 days ago
evgeny a73ae13df4 lwip/proxy: trace PCB lifetime in debug category, drop old noisy debug 2 days ago
  1. 2
      .gitignore
  2. 33
      BUGS_proxy.txt
  3. 89
      BUG_bgp_reconnect.md
  4. 34
      configure.ac
  5. 96
      doc/chat_call_arch.md
  6. 24
      doc/service_lifecycle.md
  7. 55
      doc/tasks.md
  8. 28
      lib/Makefile.am
  9. 11
      lib/audio_compressor.c
  10. 2
      lib/debug_config.c
  11. 4
      lib/debug_config.h
  12. 301
      lib/silero_vad.c
  13. 76
      lib/silero_vad.h
  14. BIN
      lib/silero_vad.onnx
  15. 136918
      lib/silero_vad_model.inc
  16. 169
      lib/speex_aec.c
  17. 74
      lib/speex_aec.h
  18. 35
      lib/speexdsp/COPYING
  19. 160
      lib/speexdsp/_kiss_fft_guts.h
  20. 232
      lib/speexdsp/arch.h
  21. 448
      lib/speexdsp/fftwrap.c
  22. 58
      lib/speexdsp/fftwrap.h
  23. 170
      lib/speexdsp/include/speex/speex_echo.h
  24. 14
      lib/speexdsp/include/speex/speexdsp_config_types.h
  25. 126
      lib/speexdsp/include/speex/speexdsp_types.h
  26. 523
      lib/speexdsp/kiss_fft.c
  27. 108
      lib/speexdsp/kiss_fft.h
  28. 297
      lib/speexdsp/kiss_fftr.c
  29. 51
      lib/speexdsp/kiss_fftr.h
  30. 332
      lib/speexdsp/math_approx.h
  31. 1279
      lib/speexdsp/mdf.c
  32. 169
      lib/speexdsp/os_support.h
  33. 379
      lib/speexdsp/pseudofloat.h
  34. 5
      src/Makefile.am
  35. 150
      src/call/call.c
  36. 3
      src/call/call.h
  37. 211
      src/call/call_audio.c
  38. 20
      src/call/call_audio.h
  39. 12
      src/call/call_headless.c
  40. 172
      src/call/call_jitter.cpp
  41. 28
      src/call/call_jitter.h
  42. 24
      src/call/call_jitter_window.h
  43. 2
      src/call/call_proto.h
  44. 5
      src/call/voice_sources.cmake
  45. 2
      src/chat/chat_event.h
  46. 7
      src/chat/chat_setting.c
  47. 11
      src/chat/invite_build.c
  48. 4
      src/chat/invite_build.h
  49. 21
      src/lwip_tcp/lwip_tcp.c
  50. 21
      src/lwip_tcp/lwip_tcp_in.c
  51. 1
      src/lwip_tcp/lwip_tcp_priv.h
  52. 4
      src/proxy/proxy_protocol.h
  53. 128
      src/proxy/tcp_proxy_client.c
  54. 2
      src/proxy/tcp_proxy_client.h
  55. 16
      src/proxy/tcp_proxy_server.c
  56. 280
      src/radio/radio_audio.c
  57. 13
      src/radio/radio_audio.h
  58. 79
      src/radio/radio_jitter.cpp
  59. 44
      src/radio/radio_jitter.h
  60. 95
      src/radio/radio_vad.h
  61. 24
      src/routing_layer/conn_mgr.h
  62. 78
      src/routing_layer/conn_mgr_core.c
  63. 2
      src/routing_layer/conn_mgr_priv.h
  64. 4
      src/routing_layer/etcp_router.c
  65. 1
      src/routing_layer/etcp_router.h
  66. 20
      src/transport_layer/etcp_connections.c
  67. 54
      src/transport_layer/etcp_keepalive.c
  68. 9
      src/transport_layer/etcp_keepalive.h
  69. 14
      src/transport_layer/node_conn_direct.h
  70. 57
      src/utun_instance.c
  71. 6
      src/utun_instance.h
  72. 37
      tests/Makefile.am
  73. 2
      tests/tcp_proxy_full/client.conf
  74. 1
      tests/tcp_proxy_full/exit.conf
  75. 1
      tests/tcp_proxy_full/intermediate.conf
  76. 16
      tests/test_call.c
  77. 4
      tests/test_call_headless.c
  78. 42
      tests/test_chat_join_e2e.c
  79. 31
      tests/test_conn_mgr_handles.c
  80. 34
      tests/test_etcp_router_faults.c
  81. 9
      tests/test_proxy_regressions.c
  82. 134
      tests/test_radio_vad.c
  83. 14
      tests/test_services.c
  84. 196
      tests/test_silero_vad.c
  85. 219
      tests/test_speex_aec.c
  86. 180
      tests/test_standby_transport.c
  87. 23
      tools/chatgui-android/app/src/main/AndroidManifest.xml
  88. 21
      tools/chatgui-android/app/src/main/cpp/CMakeLists.txt
  89. 19
      tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/cpu_provider_factory.h
  90. 62
      tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/nnapi_provider_factory.h
  91. 7429
      tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_c_api.h
  92. 3224
      tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_cxx_api.h
  93. 3547
      tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_cxx_inline.h
  94. 988
      tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_ep_c_api.h
  95. 18
      tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_ep_device_ep_metadata_keys.h
  96. 535
      tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_float16.h
  97. 1119
      tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_lite_custom_op.h
  98. 54
      tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_run_options_config_keys.h
  99. 417
      tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_session_options_config_keys.h
  100. 12
      tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt
  101. Some files were not shown because too many files have changed in this diff Show More

2
.gitignore vendored

@ -12,6 +12,8 @@ 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

33
BUGS_proxy.txt

@ -1,33 +0,0 @@
Ниже — подтверждённые по коду проблемы; отдельно указал, что воспроизведено запуском.
[P1] Exit смешивает TCP-потоки разных клиентов. Поиск соединения использует только stream_id, хотя каждый клиент начинает нумерацию заново. После подключения второго клиента с таким же ID данные первого могут уйти в соединение второго; FIN/CLOSE также закрывают чужой поток. Нужен ключ (src_node_id, stream_id) во всех обработчиках, включая проверку повторного CONNECT. src/proxy/tcp_proxy_server.c:342
[P1] Клиент не проверяет отправителя ответов. DATA/FIN/CLOSE принимаются по одному stream_id, без сравнения источника с via_node_id. Обработка CLOSE_ALL читает peer_id, но затем уничтожает вообще все клиентские соединения. Пакеты другого узла, доставленные этому сервису, могут повредить или закрыть существующие потоки. src/proxy/tcp_proxy_client.c:668
[P1, воспроизведено] UDP-ответы в TUN имеют неправильную checksum. Обнуляется только IP-заголовок; байты UDP checksum остаются заполненными аллокатором значением 0xAAAA. Проверка сформированного пакета дала сумму 0xC1E2 вместо 0xFFFF. Такие ответы будут отбрасываться принимающим стеком. Нужно рассчитывать checksum либо явно устанавливать ноль для IPv4. src/proxy/udp_proxy.c:188
[P1, воспроизведено] ICMP с нечётной длиной payload получает неверную checksum. Цикл читает последнее 16-битное слово целиком, захватывая байт за пределами выделенного пользовательского буфера. Ошибка есть и на exit, и при восстановлении ответа клиенту. Для payload длиной 2 проверочная сумма получилась FFFF, длиной 3 — FF10. Отправка (src/proxy/icmp_proxy.c:80), ответ в TUN (src/proxy/icmp_proxy.c:285)
[P1] UDP/ICMP-контексты глобальные, а ядро поддерживает несколько instances. Следующий init() перезаписывает g_udp_ctx/g_icmp_ctx; обработчики старого экземпляра используют новый контекст, а destroy(inst) освобождает его без проверки владельца. Дополнительно UDP различает запрос и ответ только по глобальному is_exit: узел, одновременно работающий клиентом и exit, трактует входящий ответ как новый исходящий запрос. src/proxy/udp_proxy.c:137, инициализация (src/proxy/udp_proxy.c:237), src/proxy/icmp_proxy.c:321
[P1] Нет сквозного ограничения потока при медленном получателе. Принятые ETCP-данные без ограничения добавляются в write_queue сокета или to_lwip. Заполненность этих очередей не останавливает доставку и подтверждение данных маршрутизатором. Медленный destination или локальный клиент приводит к росту памяти; при отказе выделения TCP-данные просто теряются. src/proxy/tcp_proxy_server.c:424, src/proxy/tcp_proxy_client.c:584
[P1] TUN-клиент может уничтожить ещё не отправленные данные при FIN. Если локальный FIN уже получен, но tx_queue остаётся заблокированной, входящий FIN от exit вызывает conn_finish() без проверки этой очереди. Она освобождается вместе с данными. Завершать поток нужно после опустошения очередей обоих направлений. src/proxy/tcp_proxy_client.c:642
[P1] Exit может отправить CLOSE раньше остатка ответа. В on_fin_cb() проверка tc->fin_local имеет приоритет над pend_r. Если клиент уже закрыл свою половину, а ответ destination остался в tx_buf из-за backpressure, сервер отправляет CLOSE и освобождает остаток. Ветка on_flushed_cb() также не проверяет ожидающие отправки данные ответа. src/proxy/tcp_proxy_server.c:108
[P1] HTTP CONNECT зависит от границ TCP-чтений. Парсер запускает туннель после первой \r\n, не дожидаясь \r\n\r\n. Если заголовки приходят следующим чтением, они пересылаются destination как содержимое туннеля — например, перед TLS ClientHello. Если вместе с заголовками уже пришли данные туннеля, они уничтожаются обнулением buf_len. src/proxy/socks_proxy.c:195
[P1] HTTP POST во время DNS может потерять весь накопленный запрос. dns_pending накапливается до 65535 байт, затем отправляется одним DATA. Exit принимает максимум 8192 байта в одном сообщении и отбрасывает превышение. При переполнении самого dns_pending очередные данные также теряются без прекращения потока. Нужны ограниченная очередь и отправка частями. src/proxy/socks_proxy.c:339, накопление (src/proxy/socks_proxy.c:430), ограничение exit (src/proxy/tcp_proxy_server.c:438)
[P2] FIN теряется в двух штатных сценариях. SOCKS/HTTP при непустой очереди ответа вызывает tcp_conn_set_flushed(tc, NULL) — продолжение, которое должно переслать FIN, отсутствует. Exit молча игнорирует FIN, пришедший до завершения TCP connect. Протоколы, ожидающие EOF перед ответом, могут зависнуть. src/proxy/socks_proxy.c:538, src/proxy/tcp_proxy_server.c:492
[P2] SOCKS5-парсер некорректно обрабатывает запросы. После greeting/CONNECT сбрасывается весь буфер, включая следующие байты; всегда выбирается метод NO AUTH, даже если клиент его не предлагал. IPv6 вместо отказа превращается в IPv4 из неправильного смещения buf + 12. Доменный CONNECT длиной менее 10 байт бесконечно ожидает продолжения. src/proxy/socks_proxy.c:132
[P2] SOCKS/HTTP сообщают об успешном соединении до подключения exit. Ответ SOCKS success или HTTP 200 формируется даже до send_connect(). Подтверждения успешного TCP connect от exit в протоколе нет. При отказе подключения клиент сначала получает успех, затем закрытие вместо корректной ошибки. src/proxy/socks_proxy.c:316
[P2] После RST TUN-соединение остаётся в списке. tcp_proxy_client_err_cb() обнуляет pcb и выставляет error, но не освобождает pc. Очистка предусмотрена в poll callback уже уничтоженного PCB, который больше не вызовется. src/proxy/tcp_proxy_client.c:446
[P2] Неправильно разбираются IP options и фрагменты в TUN. UDP/ICMP используют фиксированные смещения от 20-байтового IPv4-заголовка, игнорируя IHL и fragment offset. Пакет с options или последующий IP-фрагмент превращается в запрос с неверными портами/данными. src/proxy/tcp_proxy_client.c:158
[P2] ICMP-ответы разных клиентов могут перепутаться. Exit сохраняет исходные echo_id/echo_seq и ищет ответ только по этой паре, без уникального преобразования ID и проверки адреса отправителя. Совпадающие ping-запросы разных клиентов получают чужие ответы. src/proxy/icmp_proxy.c:56

89
BUG_bgp_reconnect.md

@ -1,89 +0,0 @@
# Задача: 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"`.

34
configure.ac

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

96
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 →
установка соединения (conn_mgr) → аудиопоток → HANGUP.
аудиопоток через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
## 2. Разбиение файлов (отдельный каталог `src/call/`)
@ -21,13 +21,12 @@ 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/audio_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
| `src/call/call_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
Новая категория логирования: `DEBUG_CATEGORY_CALL = 31` (добавить в `debug_config.h`,
`DEBUG_CATEGORY_COUNT` → 32).
Диагностика: категория `call` (`DEBUG_CATEGORY_CALL`); транспортная доставка — `etcp_route`.
## 3. Протокол (svc_id 0x34, первый байт payload = subcmd)
@ -55,8 +54,8 @@ CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, payload}
## 4. Машина состояний
```
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
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → ACTIVE(ACCEPT) → ENDED
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACTIVE(accept) → ENDED
```
`ENDED` reasons: `LOCAL_HANGUP / REMOTE_HANGUP / DECLINE / BUSY / CANCEL /
@ -69,18 +68,20 @@ NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
- **watchdog 2с у вызываемого** — нет кодограммы 2с → вызов считается сорванным
(отмена потерялась), сессия закрывается локально (`CALL_REASON_CANCEL`);
- ring-timeout 45с (вызывающий, нет ответа → CANCEL);
- connect: `conn_mgr` `CONN_EVENT_TIMEOUT` → fail;
- `CONN_EVENT_DOWN/TIMEOUT` CM не завершают звонок: роутер использует доступные пути;
- **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP.
Завершающие кодограммы (HANGUP/CANCEL/DECLINE) шлются без ретрансмиссий — потеря
компенсируется watchdog'ом 2с у вызываемого. Запоздалые кодограммы вызова при
идущем разговоре (`CONNECTING`/`ACTIVE`) игнорируются.
идущем разговоре (`ACTIVE`) игнорируются.
Онлайн-проверка: `topo_node_find_by_id(group, peer) == NULL` → «не в сети» (event).
Соединение: на ACCEPT обе стороны `conn_mgr_open(inst, group_id, peer, cb, ...)`;
`CONN_EVENT_UP` → ACTIVE. Медиа идёт `etcp_route_send` (после подъёма прямого линка
роутер идёт напрямую — RTT минимален).
CM handle инициирует оптимальное подключение и удерживается до завершения звонка.
Медиа всегда идёт через `etcp_route_send`: роутер сам выбирает лучший путь и fallback.
Звонок не хранит ETCP_CONN и не завершается при потере отдельного транспорта.
Очистка записи CM отсоединяет handles и отменяет отложенные UP; handle закрывает владелец.
Закрытие CM handle не посылает DISCONNECT удалённым владельцам. Механизм NCD не меняется.
## 5. Отправка медиа — backpressure через локальный буфер
@ -96,43 +97,47 @@ NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
В сессии:
- `tx_q` — локальная FIFO очередь закодированных кадров (ll_entry с dgram), ёмкостью
~1с (≈50 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
15с (750 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
- Алгоритм (`call_tx_flush`): пока `tx_q` не пуст:
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=0)`;
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=1)` (после проверки места);
- иначе — если waiter не зарегистрирован → `etcp_router_on_send_ready(... drain_cb)` и выход.
- `drain_cb(q, arg)` → снова `call_tx_flush(session)` (re-arm при необходимости).
- Весь код — в uasync-потоке (single-threaded, реентерабельность как в
`media_delivery.c:stream_send_chunk_cb`).
## 6. 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**.
## 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`. Явное завершение звонка прекращает
воспроизведение накопленной речи и включает тон завершения — на обеих платформах.
## 7. Буфер звонка и восстановление
`call_jitter` независим от `radio_jitter`: рация сохраняет свои правила задержки и сброса.
Буфер звонка хранит до 750 кодированных кадров (15с): 10с перебоя и 5с запаса.
Целевой запас — `clamp(max-min, 60, max_reserve_ms)` мс: десять секундных интервалов
хранят минимум и максимум глубины, старые интервалы истекают по монотонным часам.
Верхний предел задаётся `call_jitter_max_reserve_ms`: 500–2000 мс, по умолчанию 1000 мс.
Настройка есть в аудиопараметрах desktop и Android и применяется к следующему звонку.
Начальное и повторное накопление ждёт этот запас; увеличение запаса не прерывает текущую речь.
Ускорение зависит от превышения запаса. При отсутствии входящих кадров обычное воспроизведение
может исчерпать запас: это не жёсткий запрет воспроизведения ниже цели.
Статистика раз в секунду отображается в desktop-окне и Android-экране звонка (под кнопками): глубина, запас, минимум/максимум,
темп, RTT, удаления при переполнении и недостачи PCM. Диагностика буфера — категория `call=debug`.
После восстановления кадры последовательно декодируются; темп плавно растёт до 1.6x
без изменения высоты голоса: линейный предел роста 1.0x→1.6x за 500мс воспроизведения.
При уменьшении накопления целевой темп снижается к 1.0x; скорость снижения ограничена тем же пределом.
Пауза аудиоустройства не сбрасывает буфер. Переполнение удаляет старейший кадр с WARN и счётчиком.
Роутер сохраняет последовательность и неподтверждённые данные при смене пути.
Для сервиса звонка срок без прогресса ACK увеличен примерно до 20с; прочие сервисы сохраняют прежний срок.
Перезапуск удалённого процесса/роутерной сессии не является кратковременным перебоем пути и может сбросить данные.
SoundTouch: `USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4`.
API: `ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy`.
## 8. Тоны и диагностика
@ -144,9 +149,10 @@ API: `ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy`.
(`call_connecting`/`call_ringing`), Android — `ToneGenerator`
(`TONE_SUP_DIAL`/`TONE_SUP_RINGTONE`). Переключение — по `CHAT_EVT_CALL_RINGING`,
остановка — по `CALL_ACCEPTED`/`CALL_ENDED`/`CALL_ERROR`/hangup.
- **Сталл >500мс** (нет входящих медиа) → в `call_pull_pcm` микшируется
- **Отсутствие медиа ≥500мс при недоборе PCM** (нет входящих медиа) → в `call_audio_pull_pcm` воспроизводится
`CALL_TONE_GLITCH`: `пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс)
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл.
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл. До порога — тишина при нехватке PCM.
При восстановлении медиа или полном запросе PCM сигнал останавливается.
- **Завершение звонка → всегда** `CALL_TONE_ENDED`: 3 нисходящих 700/500/400 Гц по 150 мс.
- Счётчики: tx/rx кадры, `c_tx_dropped`/`c_rx_dropped`, дропы буфера, сталлы,
tempo-события, причины завершения. Логи `DEBUG_CATEGORY_CALL` (+стата раз в 1с).
@ -159,7 +165,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 37 [call_id:8][rtt_ms:2][buffer_ms:2][tempo_x100:2]
CHAT_EVT_CALL_STATS 39 [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2]
CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
```

24
doc/service_lifecycle.md

@ -57,3 +57,27 @@ SQLite открывается ядром один раз: `db_path/chats.db`, л
сохранение идентичности/сокетов/БД, повторную загрузку каналов, отмену 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 отменяет ожидающие задачи и очищает подписки старого ядра.

55
doc/tasks.md

@ -3,6 +3,56 @@
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
## Текущая задача
[+] **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 после восстановления, баланс ресурсов.
@ -220,6 +270,11 @@
в нескольких прогонах. В `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

28
lib/Makefile.am

@ -40,17 +40,41 @@ libuasync_a_SOURCES = \
dns.c \
dns.h \
async_dns.c \
async_dns.h
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
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/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
# Clean build directory
clean-local:

11
lib/audio_compressor.c

@ -43,6 +43,11 @@ struct audio_compressor {
int dbg_counter;
};
/* Начальный gain — середина диапазона [0, max_gain] в дБ (геометрическое среднее). */
static float initial_gain(float max_gain) {
return max_gain > 0.0f ? sqrtf(max_gain) : 1.0f;
}
struct audio_compressor* audio_compressor_create(void) {
struct audio_compressor* ac = u_calloc(1, sizeof(*ac));
if (!ac) {
@ -85,11 +90,13 @@ void audio_compressor_configure(struct audio_compressor* ac, const audio_compres
ac->cfg.release_rate_per_sec = ac->cfg.rise_rate_per_sec * 5.0f;
ac->release_factor_per_block = powf(ac->cfg.release_rate_per_sec, (float)ac->cfg.block_duration_ms / 1000.0f);
ac->max_gain = powf(10.0f, ac->cfg.max_gain_db / 20.0f);
ac->gain_smoothed = initial_gain(ac->max_gain);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"audio_compressor_configure: rate=%d ch=%d block=%dms lookback=%dms lookahead=%dms maxGain=%.0fdB riseRate=%.1f/sec releaseRate=%.1f/sec target=%.0fdBFS",
"audio_compressor_configure: rate=%d ch=%d block=%dms lookback=%dms lookahead=%dms maxGain=%.0fdB initGain=%.1fdB riseRate=%.1f/sec releaseRate=%.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,
20.0f * log10f(ac->gain_smoothed),
ac->cfg.rise_rate_per_sec, ac->cfg.release_rate_per_sec, 20.0f * log10f(ac->cfg.target_level));
}
@ -101,7 +108,7 @@ void audio_compressor_reset(struct audio_compressor* ac) {
ac->block_levels_count = 0;
ac->accum_count = 0;
ac->output_size = 0;
ac->gain_smoothed = 1.0f;
ac->gain_smoothed = initial_gain(ac->max_gain);
ac->clip_count = 0;
ac->dbg_counter = 0;
}

2
lib/debug_config.c

@ -134,6 +134,8 @@ static const struct {
{"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}
};

4
lib/debug_config.h

@ -72,7 +72,9 @@ typedef int debug_category_t;
#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_COUNT 34 // Total number of categories
#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_ALL (-1) // special value for all categories
/* Debug configuration structure */

301
lib/silero_vad.c

@ -0,0 +1,301 @@
#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

@ -0,0 +1,76 @@
/**
* 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

169
lib/speex_aec.c

@ -0,0 +1,169 @@
// 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 без канселлера.
//
// Захват и рендер приходят чанками разного размера (480/960), поэтому обе
// стороны накапливаются до полного кадра (frame_samples) внутри обёртки.
#include "speex_aec.h"
#include "debug_config.h"
#include "mem.h"
#include <string.h>
#include <speex/speex_echo.h>
#define AEC_ID "speex_aec"
struct speex_aec {
SpeexEchoState* st;
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;
int16_t* cap_acc; /* аккумулятор захвата до кадра */
int cap_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++;
if (a->overruns == 1 || (a->overruns & 0xff) == 0) {
DEBUG_WARN(DEBUG_CATEGORY_AEC,
"%s: delay line overflow (render faster than capture) overruns=%u", AEC_ID, a->overruns);
}
}
memcpy(a->line + (size_t)((a->head + a->count) % a->depth) * a->frame_samples,
frame, (size_t)a->frame_samples * sizeof(int16_t));
a->count++;
}
speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames) {
speex_aec_t* a;
int rate;
if (sample_rate <= 0 || frame_samples <= 0 || filter_samples < frame_samples) {
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(frame_samples, filter_samples);
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->frame_samples = frame_samples;
a->depth = delay_frames > 0 ? delay_frames : 1;
a->line = (int16_t*)u_calloc((uint32_t)(a->depth * frame_samples), sizeof(int16_t));
a->play_acc = (int16_t*)u_calloc((uint32_t)frame_samples, sizeof(int16_t));
a->cap_acc = (int16_t*)u_calloc((uint32_t)frame_samples, sizeof(int16_t));
if (!a->line || !a->play_acc || !a->cap_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);
if (a->cap_acc) u_free(a->cap_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",
AEC_ID, sample_rate, frame_samples, filter_samples, a->depth);
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->cap_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;
a->cap_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;
take = count;
if (a->play_len + count > fs) take = fs - a->play_len;
memcpy(a->play_acc + a->play_len, pcm, (size_t)take * sizeof(int16_t));
a->play_len += take;
if (a->play_len < fs) return;
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, take;
if (!a || !pcm || !out || count <= 0) return 0;
fs = a->frame_samples;
take = count;
if (a->cap_len + count > fs) take = fs - a->cap_len;
memcpy(a->cap_acc + a->cap_len, pcm, (size_t)take * sizeof(int16_t));
a->cap_len += take;
if (a->cap_len < fs) return 0;
if (a->count == a->depth) {
const int16_t* ref = a->line + (size_t)a->head * fs;
speex_echo_cancellation(a->st, a->cap_acc, ref, out);
a->head = (a->head + 1) % a->depth;
a->count--;
} else {
/* линия не наполнена (старт или рендер отстаёт): passthrough без канселлера */
if (a->underruns == 0 || (a->underruns & 0xff) == 0) {
DEBUG_WARN(DEBUG_CATEGORY_AEC,
"%s: delay line underflow (render slower than capture) underruns=%u fill=%d/%d",
AEC_ID, a->underruns, a->count, a->depth);
}
memcpy(out, a->cap_acc, (size_t)fs * sizeof(int16_t));
a->underruns++;
}
a->cap_len = 0;
return fs;
}
int speex_aec_delay_fill(const speex_aec_t* a) {
return a ? a->count : 0;
}

74
lib/speex_aec.h

@ -0,0 +1,74 @@
/*
* speex_aec.h — акустическое эхоподавление (AEC), C-обёртка над SpeexDSP mdf.c.
*
* Подавляет эхо дальнего конца (то, что играем в динамик) в сигнале микрофона
* перед кодированием. Работает на int16 PCM, один канал, частота 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 без канселлера (с подробным логом категории "aec").
*
* Зависимости: 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; 0 = без линии задержки).
* Возвращает NULL при ошибке (лог категории "aec").
*/
speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames);
/* Освободить канселлер. 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);
/**
* Обработать захват (микрофон). Накопление до кадра внутри. Когда кадр собран —
* подавляет эхо (с учётом линии задержки) и пишет результат в out.
* Возвращает число сэмплов, записанных в out (frame_samples, если кадр готов;
* 0 — кадр ещё накапливается). out не должен алиаситься с pcm.
*/
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);
#ifdef __cplusplus
}
#endif
#endif /* SPEEX_AEC_H */

35
lib/speexdsp/COPYING

@ -0,0 +1,35 @@
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

@ -0,0 +1,160 @@
/*
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

@ -0,0 +1,232 @@
/* 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

@ -0,0 +1,448 @@
/* 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

@ -0,0 +1,58 @@
/* 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

@ -0,0 +1,170 @@
/* 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

@ -0,0 +1,14 @@
/* 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

@ -0,0 +1,126 @@
/* 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

@ -0,0 +1,523 @@
/*
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

@ -0,0 +1,108 @@
#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

@ -0,0 +1,297 @@
/*
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

@ -0,0 +1,51 @@
#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

@ -0,0 +1,332 @@
/* 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

@ -0,0 +1,169 @@
/* 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

@ -0,0 +1,379 @@
/* 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

5
src/Makefile.am

@ -252,6 +252,11 @@ 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

150
src/call/call.c

@ -38,8 +38,7 @@ enum call_state {
CALL_ST_IDLE = 0,
CALL_ST_OUTGOING, /* caller: INVITE отправлен */
CALL_ST_RINGING, /* caller: получил RINGING; callee: отправил RINGING */
CALL_ST_CONNECTING, /* обе стороны: conn_mgr_open в процессе */
CALL_ST_ACTIVE, /* обе стороны: соединение поднято */
CALL_ST_ACTIVE, /* вызов принят; доставка через роутер независимо от готовности CM */
CALL_ST_ENDED,
};
@ -55,7 +54,6 @@ struct call_session {
uint8_t reason; /* CALL_REASON_* (при ENDED) */
struct call_ctx* ctx; /* обратная ссылка (для коллбэков conn_mgr/таймеров) */
struct CONN_MGR_HANDLE* cm_handle;
struct ETCP_CONN* data_conn; /* прямой data-коннект звонка (NULL для INDIRECT) */
void* ring_timer; /* caller: 45с ожидания ответа; callee: 2с watchdog кодограмм */
void* invite_timer; /* caller: кодограмма INVITE каждые 300мс (OUTGOING+RINGING) */
uint8_t no_route_strikes; /* caller: подряд ретрансмитов без маршрута (fail-fast) */
@ -94,7 +92,6 @@ static const char* call_state_name(uint8_t st) {
case CALL_ST_IDLE: return "IDLE";
case CALL_ST_OUTGOING: return "OUTGOING";
case CALL_ST_RINGING: return "RINGING";
case CALL_ST_CONNECTING: return "CONNECTING";
case CALL_ST_ACTIVE: return "ACTIVE";
case CALL_ST_ENDED: return "ENDED";
default: return "?";
@ -318,8 +315,7 @@ static void call_end(struct call_ctx* ctx, struct call_session* s, uint8_t reaso
call_event_id(ctx, CHAT_EVT_CALL_ENDED, s->call_id, reason);
/* Разрыв conn_mgr — отложенно (500мс), чтобы HANGUP/DECLINE успел долететь.
* Без cm_handle рвать нечего — освобождаем сразу (отложенно). */
/* Закрытие запроса CM откладываем на 500мс для отправки финального сигналинга. */
if (s->cm_handle) {
s->teardown_timer = uasync_set_timeout(ctx->inst->ua, CALL_TEARDOWN_DELAY_TB, s, call_teardown_timer_cb, "call_teardown");
} else {
@ -329,38 +325,41 @@ static void call_end(struct call_ctx* ctx, struct call_session* s, uint8_t reaso
/* ─────────────────── conn_mgr ─────────────────── */
static void call_traffic_timeout_cb(void* arg);
/* CM готовит оптимальный путь; звонок всегда отправляет через роутер с автоматическим fallback. */
static void call_conn_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id,
enum conn_mgr_event event, void* arg) {
struct call_session* s = (struct call_session*)arg;
if (!s || !s->ctx) return;
struct call_ctx* ctx = s->ctx;
if (s->state == CALL_ST_ENDED) {
/* сессия завершена, ждёт teardown: conn_mgr сообщает об обрыве — handle больше
* не трогаем (entry чистится самим conn_mgr / удалённой стороной) */
if (event == CONN_EVENT_DOWN || event == CONN_EVENT_TIMEOUT) s->cm_handle = NULL;
return;
}
struct call_session* s = arg;
if (!s || s->state == CALL_ST_ENDED) return;
if (event == CONN_EVENT_UP) {
s->cm_handle = h;
s->data_conn = conn_mgr_get_conn(h); /* NULL для INDIRECT */
s->state = CALL_ST_ACTIVE;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: conn UP id=%016llx peer=0x%016llx -> ACTIVE",
CALL_ID, (unsigned long long)s->call_id, (unsigned long long)node_id);
call_event_id(ctx, CHAT_EVT_CALL_ACCEPTED, s->call_id, 0);
call_post_bgp_path(ctx, s);
if (!s->stats_timer)
s->stats_timer = uasync_set_timeout(ctx->inst->ua, 10000, s, call_stats_timer_cb, "call_stats");
} else if (event == CONN_EVENT_DOWN) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: conn DOWN id=%016llx peer=0x%016llx (temporary, keep alive)",
CALL_ID, (unsigned long long)s->call_id, (unsigned long long)node_id);
} else { /* CONN_EVENT_TIMEOUT */
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: conn TIMEOUT id=%016llx peer=0x%016llx -> fail",
CALL_ID, (unsigned long long)s->call_id, (unsigned long long)node_id);
/* entry уже чистится вызывающим (monitor) — handle не закрываем */
s->cm_handle = NULL;
call_end(ctx, s, CALL_REASON_CONNECT_FAIL);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: CM path ready id=%016llx peer=%016llx", CALL_ID,
(unsigned long long)s->call_id, (unsigned long long)node_id);
if (s->state == CALL_ST_ACTIVE) call_post_bgp_path(s->ctx, s);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: CM event=%d id=%016llx peer=%016llx; delivery continues via router",
CALL_ID, event, (unsigned long long)s->call_id, (unsigned long long)node_id);
}
(void)h; (void)group_id;
}
static void call_open_cm(struct call_session* s) {
if (s->cm_handle) return;
if (conn_mgr_open(s->ctx->inst, s->group_id, s->peer_node_id, call_conn_cb, s, &s->cm_handle) != 0)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: CM request failed id=%016llx; using existing routing", CALL_ID,
(unsigned long long)s->call_id);
}
static void call_activate(struct call_session* s) {
if (s->state == CALL_ST_ACTIVE || s->state == CALL_ST_ENDED) return;
struct call_ctx* ctx = s->ctx;
s->state = CALL_ST_ACTIVE;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: accepted id=%016llx -> ACTIVE (router delivery)", CALL_ID,
(unsigned long long)s->call_id);
call_event_id(ctx, CHAT_EVT_CALL_ACCEPTED, s->call_id, 0);
call_post_bgp_path(ctx, s);
s->traffic_timer = uasync_set_timeout(ctx->inst->ua, CALL_NO_TRAFFIC_TB, s, call_traffic_timeout_cb, "call_traffic");
s->stats_timer = uasync_set_timeout(ctx->inst->ua, 10000, s, call_stats_timer_cb, "call_stats");
}
/* ─────────────────── таймеры ─────────────────── */
@ -392,7 +391,7 @@ static void call_invite_retry_cb(void* arg) {
struct call_ctx* ctx = s->ctx;
s->invite_timer = NULL;
if (s->state == CALL_ST_ENDED) return;
if (s->state != CALL_ST_OUTGOING && s->state != CALL_ST_RINGING) return; /* CONNECTING/ACTIVE — звонок идёт */
if (s->state != CALL_ST_OUTGOING && s->state != CALL_ST_RINGING) return; /* ACTIVE — звонок идёт */
if (etcp_router_has_route(ctx->inst, s->group_id, s->peer_node_id)) {
s->no_route_strikes = 0;
@ -455,16 +454,17 @@ static void call_stats_timer_cb(void* arg) {
if (s->state != CALL_ST_ACTIVE) return;
int buffer_ms = 0, tempo_x100 = 100;
struct call_jitter_range range = {0, 0, 60};
uint32_t dropped = 0, underruns = 0;
if (ctx->audio_ops && ctx->audio_ops->get_stats)
ctx->audio_ops->get_stats(s->call_id, &buffer_ms, &tempo_x100, &dropped, &underruns);
ctx->audio_ops->get_stats(s->call_id, &buffer_ms, &tempo_x100, &dropped, &underruns, &range);
uint16_t rtt = 0;
struct ETCP_ROUTER_CONN* rconn =
etcp_router_conn_get(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL);
if (rconn && rconn->recv_conn) rtt = rconn->recv_conn->rtt_last;
uint8_t buf[18];
uint8_t buf[24];
size_t off = 0;
memcpy(buf + off, &s->call_id, 8); off += 8;
buf[off++] = (uint8_t)(rtt >> 8); buf[off++] = (uint8_t)(rtt & 0xFF);
@ -476,46 +476,16 @@ static void call_stats_timer_cb(void* arg) {
buf[off++] = (uint8_t)(dr >> 8); buf[off++] = (uint8_t)(dr & 0xFF);
uint16_t un = (uint16_t)(underruns > 0xFFFF ? 0xFFFF : underruns);
buf[off++] = (uint8_t)(un >> 8); buf[off++] = (uint8_t)(un & 0xFF);
int values[] = {range.min_ms, range.max_ms, range.reserve_ms};
for (size_t i = 0; i < 3; i++) {
uint16_t value = (uint16_t)values[i];
buf[off++] = (uint8_t)(value >> 8); buf[off++] = (uint8_t)value;
}
chat_event_post(ctx->inst, CHAT_EVT_CALL_STATS, buf, (int)off);
s->stats_timer = uasync_set_timeout(ctx->inst->ua, 10000, s, call_stats_timer_cb, "call_stats");
}
/* ─────────────────── потеря соединения ─────────────────── */
/* На обрыв ETCP-соединения с пиром завершаем ACTIVE-звонки (robust fallback:
* HANGUP может не успеть долететь, если закрывающая сторона рвёт conn_mgr). */
static void call_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct call_ctx* ctx = call_of(inst);
if (!ctx || !ctx->initialized || !conn) return;
if (status != ETCP_CONN_STATUS_DOWN && status != ETCP_CONN_STATUS_DELETE) return;
uint64_t peer = conn->peer_node_id;
if (!peer) return;
struct ll_entry* e = ctx->sessions ? ctx->sessions->head : NULL;
while (e) {
struct ll_entry* next = e->next;
struct call_session* s = (struct call_session*)e->data;
if (s->peer_node_id == peer) {
if (s->state == CALL_ST_ACTIVE) {
/* Завершаем только если упал именно data-коннект звонка (DIRECT/REVERSE).
* Для INDIRECT data_conn==NULL — посторонний коннект к тому же пиру
* (topo_group_connect) не должен рвать работающий звонок. */
if (s->data_conn && s->data_conn == conn) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: peer conn lost id=%016llx peer=0x%016llx -> end",
CALL_ID, (unsigned long long)s->call_id, (unsigned long long)peer);
call_end(ctx, s, CALL_REASON_REMOTE_HANGUP);
}
} else if (s->state == CALL_ST_ENDED) {
/* удалённая сторона уже порвала (DISCONNECT чистит entry без коллбэка) */
s->cm_handle = NULL;
}
}
e = next;
}
}
/* ─────────────────── обработчики приёма ─────────────────── */
static void call_handle_invite(struct call_ctx* ctx, uint64_t from, uint64_t group,
@ -529,7 +499,7 @@ static void call_handle_invite(struct call_ctx* ctx, uint64_t from, uint64_t gro
memcpy(&inv_group, b + 9, 8);
/* кодограмма-дубликат (тот же call_id): пока звоним — пере-взводим watchdog и
* идемпотентно отвечаем RINGING; в разговоре (CONNECTING/ACTIVE) — игнорируем. */
* идемпотентно отвечаем RINGING; в разговоре (ACTIVE) — игнорируем. */
struct call_session* s = call_session_find(ctx, call_id);
if (s) {
if (s->role == CALL_ROLE_CALLEE && s->state == CALL_ST_RINGING) {
@ -606,18 +576,14 @@ static void call_handle_accept(struct call_ctx* ctx, uint64_t from, const uint8_
if (n < 9) return;
uint64_t call_id; memcpy(&call_id, b + 1, 8);
struct call_session* s = call_session_find(ctx, call_id);
if (!s || s->role != CALL_ROLE_CALLER || s->state == CALL_ST_ENDED || s->state == CALL_ST_CONNECTING) return;
if (!s || s->role != CALL_ROLE_CALLER || s->state == CALL_ST_ENDED || s->state == CALL_ST_ACTIVE) return;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: ACCEPT id=%016llx -> conn_mgr_open", CALL_ID, (unsigned long long)call_id);
s->state = CALL_ST_CONNECTING;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: ACCEPT id=%016llx", CALL_ID, (unsigned long long)call_id);
if (s->ring_timer) { uasync_cancel_timeout(ctx->inst->ua, s->ring_timer); s->ring_timer = NULL; }
if (s->invite_timer) { uasync_cancel_timeout(ctx->inst->ua, s->invite_timer); s->invite_timer = NULL; }
int rc = conn_mgr_open(ctx->inst, s->group_id, s->peer_node_id, call_conn_cb, s, &s->cm_handle);
if (rc != 0) {
/* либо уже получили TIMEOUT (сессия ENDED), либо ошибка без коллбэка */
if (s->state != CALL_ST_ENDED) call_end(ctx, s, CALL_REASON_CONNECT_FAIL);
}
call_open_cm(s);
call_activate(s);
(void)from;
}
static void call_handle_decline(struct call_ctx* ctx, uint64_t from, const uint8_t* b, size_t n) {
@ -753,7 +719,6 @@ int call_init(struct UTUN_INSTANCE* inst) {
u_free(ctx); inst->call = NULL;
return -1;
}
etcp_add_conn_status_cbk(inst, call_on_conn_status, inst);
ctx->initialized = 1;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized (svc=0x%02x)", CALL_ID, ETCP_RT_ID_CALL);
@ -781,7 +746,6 @@ void call_destroy(struct UTUN_INSTANCE* inst) {
}
queue_free(ctx->sessions);
etcp_router_unbind(inst, ETCP_RT_ID_CALL);
etcp_remove_conn_status_cbk(inst, call_on_conn_status, inst);
inst->call = NULL;
uint32_t started = ctx->c_calls_started, ended = ctx->c_calls_ended;
u_free(ctx);
@ -837,6 +801,7 @@ static void call_tx_flush(struct call_session* s) {
struct ll_entry* e = queue_data_get(s->tx_q);
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, e, 1, 0) == 0)
s->c_tx_frames++;
else DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: media enqueue failed id=%016llx", CALL_ID, (unsigned long long)s->call_id);
}
}
@ -868,7 +833,12 @@ int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t*
if (queue_entry_count(s->tx_q) >= CALL_MAX_TX_FRAMES) {
struct ll_entry* old = queue_data_get(s->tx_q);
if (old) { queue_dgram_free(old); queue_entry_free(old); s->c_tx_dropped++; }
if (old) {
queue_dgram_free(old); queue_entry_free(old); s->c_tx_dropped++;
if (s->c_tx_dropped == 1 || s->c_tx_dropped % 50 == 0)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: TX overflow id=%016llx queued=%zu dropped=%u", CALL_ID,
(unsigned long long)call_id, (size_t)queue_entry_count(s->tx_q), s->c_tx_dropped);
}
}
queue_data_put(s->tx_q, e);
@ -909,6 +879,7 @@ void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_nod
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: call_start id=%016llx peer=0x%016llx group=0x%016llx",
CALL_ID, (unsigned long long)out_call_id, (unsigned long long)peer_node_id, (unsigned long long)group_id);
call_open_cm(s);
if (call_send_invite(ctx, s) != 0) {
call_end(ctx, s, CALL_REASON_CONNECT_FAIL);
return;
@ -922,11 +893,10 @@ void call_accept(struct UTUN_INSTANCE* inst, uint64_t call_id) {
struct call_ctx* ctx = call_of(inst);
if (!ctx || !ctx->initialized) return;
struct call_session* s = call_session_find(ctx, call_id);
if (!s || s->role != CALL_ROLE_CALLEE || s->state == CALL_ST_ENDED || s->state == CALL_ST_CONNECTING) return;
if (!s || s->role != CALL_ROLE_CALLEE || s->state == CALL_ST_ENDED || s->state == CALL_ST_ACTIVE) return;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: call_accept id=%016llx -> ACCEPT + conn_mgr_open",
CALL_ID, (unsigned long long)call_id);
s->state = CALL_ST_CONNECTING;
if (s->ring_timer) { uasync_cancel_timeout(ctx->inst->ua, s->ring_timer); s->ring_timer = NULL; } /* watchdog кодограмм */
uint8_t pkt[sizeof(struct call_pkt_accept)];
@ -935,10 +905,8 @@ void call_accept(struct UTUN_INSTANCE* inst, uint64_t call_id) {
a->call_id = call_id;
call_send(ctx, s->group_id, s->peer_node_id, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT);
int rc = conn_mgr_open(inst, s->group_id, s->peer_node_id, call_conn_cb, s, &s->cm_handle);
if (rc != 0 && s->state != CALL_ST_ENDED) {
call_end(ctx, s, CALL_REASON_CONNECT_FAIL);
}
call_open_cm(s);
call_activate(s);
}
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id) {

3
src/call/call.h

@ -11,6 +11,7 @@
#define CALL_H
#include <stdint.h>
#include "call_jitter.h"
struct UTUN_INSTANCE;
@ -60,7 +61,7 @@ struct call_audio_ops {
/* 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);
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);

211
src/call/call_audio.c

@ -3,46 +3,58 @@
// Один активный звонок: encoder (аудио-поток), decoder + адаптивный джиттер-буфер
// vj (кодированные кадры + SoundTouch stretch на pull) и генератор тонов.
//
// RX: call.c → call_audio_on_media (uasync) → vj_push (кодированный кадр);
// аудио-поток call_audio_pull_pcm → vj_pull (декод+stretch) (+ глитч/завершение).
// 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 "voice_jitter.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_AJ_MAX_FRAMES 50 /* ~1000мс @20мс */
#define CALL_STALL_TB 5000 /* 500мс без медиа → глитч-тон */
#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_ended_started = 0; /* только аудио-поток: тон завершения запущен */
static uint64_t g_call_id = 0;
static uint64_t g_last_media_tb = 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 vj* g_vj = 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_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */
static struct audio_compressor* g_compressor = NULL;
static const struct call_audio_ops g_ops = {
.on_media = call_audio_on_media,
.get_stats = call_audio_get_stats,
};
/* vj decode-коллбэк: дёргается только из аудио-потока (vj_pull). */
/* Создать 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);
@ -72,8 +84,7 @@ void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && g_vj && opus && len > 0) {
vj_push(g_vj, opus, len);
g_last_media_tb = get_time_tb();
call_jitter_push(g_vj, opus, len);
}
pthread_mutex_unlock(&g_mtx);
}
@ -113,8 +124,8 @@ int call_audio_start(uint64_t call_id) {
return -1;
}
struct vj* j = vj_create(CALL_AUDIO_SAMPLE_RATE, 1, CALL_AUDIO_FRAME_SAMPLES, CALL_AUDIO_FRAME_MS,
CALL_AJ_MAX_FRAMES, call_audio_decode_cb, dec);
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);
@ -125,7 +136,7 @@ int call_audio_start(uint64_t call_id) {
struct call_tones* tones = call_tones_create();
if (!tones) {
vj_destroy(j);
call_jitter_destroy(j);
opus_codec_decoder_destroy(dec);
opus_codec_encoder_destroy(enc);
pthread_mutex_unlock(&g_mtx);
@ -133,18 +144,53 @@ int call_audio_start(uint64_t call_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) {
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 — как у голосовых сообщений */
audio_compressor_configure(ac, &accfg);
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_compressor = ac;
g_call_id = call_id;
g_active = 1;
g_ending = 0;
g_ended_started = 0;
g_last_media_tb = 0;
g_glitch_started = 0;
g_gap_samples = 0;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx", CALL_AUDIO_ID, (unsigned long long)call_id);
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;
}
@ -160,8 +206,10 @@ void call_audio_begin_end(uint64_t call_id) {
void call_audio_release(uint64_t call_id) {
opus_codec_encoder_t* enc = NULL;
opus_codec_decoder_t* dec = NULL;
struct vj* j = 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);
@ -171,11 +219,13 @@ void call_audio_release(uint64_t call_id) {
g_ending = 0;
g_ended_started = 0;
g_call_id = 0;
g_last_media_tb = 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);
@ -184,8 +234,10 @@ void call_audio_release(uint64_t call_id) {
/* дальше on_media не тронет объекты (active=0, указатели NULL) */
if (enc) opus_codec_encoder_destroy(enc);
if (dec) opus_codec_decoder_destroy(dec);
if (j) vj_destroy(j);
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);
}
@ -197,12 +249,51 @@ void call_audio_stop(void) {
if (cid) call_audio_release(cid);
}
/* ── 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) {
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_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 ── */
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) {
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES) 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;
@ -212,7 +303,23 @@ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) {
pthread_mutex_unlock(&g_mtx);
return -1;
}
len = opus_codec_encode(g_encoder, pcm, count, opus, (int)sizeof(opus));
const int16_t* tx = pcm;
if (g_aec) {
if (speex_aec_process_capture(g_aec, pcm, count, cleaned) == count)
tx = cleaned;
}
/* компрессор (AGC): push → кодируем прямо из выхода до clear_output.
* lookahead=0 ⇒ выход ровно count сэмплов. */
if (g_compressor) {
audio_compressor_push(g_compressor, tx, (size_t)count);
const int16_t* out = audio_compressor_output(g_compressor);
if (out && audio_compressor_output_size(g_compressor) >= (size_t)count)
tx = out;
len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus));
audio_compressor_clear_output(g_compressor);
} else {
len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus));
}
ua = g_inst ? g_inst->ua : NULL;
inst = g_inst;
pthread_mutex_unlock(&g_mtx);
@ -237,9 +344,8 @@ 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 vj* j = NULL;
struct call_jitter* j = NULL;
struct call_tones* tones = NULL;
uint64_t last_media = 0;
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id) {
@ -247,12 +353,13 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
ending = g_ending;
j = g_vj;
tones = g_tones;
last_media = g_last_media_tb;
}
pthread_mutex_unlock(&g_mtx);
if (!active || !j || !tones) return 0;
int n = 0;
/* завершение: доигрываем тон (450мс), затем тишина. Тон стартует лениво
* здесь (аудио-поток) — begin_end лишь выставляет флаг, чтобы не гонять
* генератор тонов с GUI-потоком. */
@ -262,35 +369,59 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
g_ended_started = 1;
}
if (call_tones_ended_pending(tones))
return call_tones_render(tones, out, max_samples);
return 0;
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) g_gap_samples = 0;
int missing = max_samples - n;
int quiet = CALL_STALL_SAMPLES - g_gap_samples;
if (quiet > missing) quiet = missing;
int tone_samples = missing - quiet;
if (n > 0 && previous_gap > 0)
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: PCM resumed call=%016llx gap=%dms pcm=%d/%d", CALL_AUDIO_ID,
(unsigned long long)call_id, previous_gap * 1000 / CALL_AUDIO_SAMPLE_RATE, n, max_samples);
if (missing > 0 && g_gap_samples == 0)
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: PCM gap started call=%016llx pcm=%d/%d", CALL_AUDIO_ID,
(unsigned long long)call_id, n, max_samples);
g_gap_samples += quiet;
/* Новый PCM прерывает предыдущий цикл тона, даже если хвост блока снова пуст. */
if (g_glitch_started && (n > 0 || tone_samples == 0)) {
g_glitch_started = 0;
call_tones_glitch_stop(tones);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone stopped call=%016llx pcm=%d/%d", CALL_AUDIO_ID,
(unsigned long long)call_id, n, max_samples);
}
memset(out + n, 0, (size_t)quiet * sizeof(*out));
if (tone_samples > 0) {
if (!g_glitch_started) {
g_glitch_started = 1;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone started call=%016llx pcm_gap=500ms pcm=%d/%d", CALL_AUDIO_ID,
(unsigned long long)call_id, n, max_samples);
}
call_tones_glitch_start(tones);
call_tones_render(tones, out + n + quiet, tone_samples);
}
n = max_samples;
}
int n = vj_pull(j, out, max_samples);
if (n >= max_samples) return n;
/* недобор: либо сталь (глитч-тон), либо тишина */
uint64_t now = get_time_tb();
int stalled = (last_media != 0) && (now - last_media > CALL_STALL_TB);
if (stalled) {
call_tones_glitch_start(tones);
int m = call_tones_render(tones, out + n, max_samples - n);
n += m;
} else {
call_tones_glitch_stop(tones);
/* рендер-референс для AEC: то, что реально пошло в динамик */
if (n > 0) {
pthread_mutex_lock(&g_mtx);
if (g_aec) speex_aec_feed_playback(g_aec, out, n);
pthread_mutex_unlock(&g_mtx);
}
for (int i = n; i < max_samples; i++) out[i] = 0;
return max_samples;
return n;
}
/* ── uasync-поток: метрики ── */
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,
uint32_t* dropped, uint32_t* underruns) {
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range) {
int ok = 0;
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && g_vj) {
vj_get_stats(g_vj, buffer_ms, tempo_x100, dropped, underruns);
call_jitter_get_stats(g_vj, buffer_ms, tempo_x100, dropped, underruns, range);
ok = 1;
}
pthread_mutex_unlock(&g_mtx);

20
src/call/call_audio.h

@ -3,7 +3,7 @@
// call_audio.h — сервисный аудио-движок P2P-звонка (единый для desktop/Android/headless).
//
// Владеет Opus codec, адаптивным джиттер-буфером (voice_jitter: кодированные
// Владеет Opus codec, адаптивным джиттер-буфером (call_jitter: кодированные
// кадры + SoundTouch time-stretch на pull) и тонами (глитч при сталле / завершение).
// Излишек голоса держится до декодирования — декодируется только то, что готово
// к воспроизведению. Потребитель (desktop miniaudio / Android AudioTrack) получает
@ -15,10 +15,11 @@
// - call_audio_feed_pcm / pull_pcm — аудио-поток (single-writer);
// - call_audio_on_media / get_stats — uasync-поток (из call.c).
// Общие указатели (encoder/decoder/vj/tones) защищены мьютексом; encoder и
// decoder — только аудио-поток (decoder дёргается из vj_pull через коллбэк),
// vj_push — только uasync-поток.
// decoder — только аудио-поток (decoder дёргается из call_jitter_pull через коллбэк),
// call_jitter_push — только uasync-поток.
#include <stdint.h>
#include "call_jitter.h"
struct UTUN_INSTANCE;
@ -53,14 +54,23 @@ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count);
* Возвращает число сэмплов (может быть < max — андерфлоу/конец тона). */
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
/* uasync-поток (из call.c): принятый Opus-кадр — кладём в джиттер-буфер (vj_push,
/* 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);
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range);
/* Включить/выключить AEC (SpeexDSP) в рантайме. desktop — по настройке GUI,
* Android — по спикерфону (route == SPEAKER). Работает и в активном звонке. */
void call_audio_set_aec_enabled(int enabled);
/* Задать задержку рендер→захват AEC в кадрах (20 мс). Android измеряет через
* AudioTrack/AudioRecord getTimestamp, desktop использует дефолт. Если AEC уже
* активен — пересоздаётся с новой задержкой. */
void call_audio_set_aec_delay_frames(int frames);
#ifdef __cplusplus
}

12
src/call/call_headless.c

@ -55,6 +55,7 @@ struct call_audio {
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;
@ -293,6 +294,8 @@ static void ca_accept_callback(socket_t fd, void* arg) {
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) {
@ -301,7 +304,8 @@ static void ca_cleanup(void* arg) {
u_free(dead); ca->client_count--;
} else prev = &(*prev)->next;
}
if (ca->ua) uasync_set_timeout(ca->ua, 5000, ca, ca_cleanup, "ca_cleanup");
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 ── */
@ -337,7 +341,8 @@ int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
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);
uasync_set_timeout(ua, 5000, ca, ca_cleanup, "ca_cleanup");
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);
@ -348,6 +353,7 @@ 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; }
@ -356,7 +362,7 @@ void call_headless_destroy(struct UTUN_INSTANCE* inst) {
struct call_audio_client* c = ca->clients;
while (c) {
struct call_audio_client* n = c->next;
if (c->fd != SOCKET_INVALID) socket_close_wrapper(c->fd);
ca_close_client(c);
u_free(c);
c = n;
}

172
src/call/call_jitter.cpp

@ -0,0 +1,172 @@
// Буфер звонка: храним речь в порядке доставки, ускоряем накопление без изменения высоты голоса.
#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;
int processing_ms = 0, tempo_x100 = 100; /* снимок аудиопотока под mtx */
call_jitter_window 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->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) {
if (!j || !enc || len <= 0 || len > kMaxEncoded) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: invalid frame len=%d", len);
return;
}
std::lock_guard<std::mutex> lock(j->mtx);
if (j->count == CALL_JITTER_MAX_FRAMES) {
j->head = (j->head + 1) % CALL_JITTER_MAX_FRAMES; j->count--; j->dropped++;
if (j->dropped == 1 || j->dropped % 50 == 0)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: overflow capacity=15000ms dropped=%u", 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;
{ std::lock_guard<std::mutex> lock(j->mtx); count = j->count; }
// Готовый 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();
bool log_stats = j->window.observe(now_ms, (int)std::lround(depth));
{
std::lock_guard<std::mutex> lock(j->mtx);
j->range = {j->window.low, j->window.high, j->window.reserve};
}
if (log_stats)
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: depth=%.0fms min=%dms max=%dms reserve=%dms tempo=%.2f",
depth, j->window.low, j->window.high, j->window.reserve, j->tempo);
if (!j->primed) {
if (depth < j->window.reserve) return 0;
j->primed = true;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: playback resumed depth=%.0fms reserve=%dms", depth, j->window.reserve);
}
j->ema_ms += 0.2 * (depth - j->ema_ms);
double target = 1.0 + (kMaxTempo - 1.0) * clamp_value((std::min(depth, j->ema_ms) - j->window.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;
if (catching != j->catching_up) {
j->catching_up = catching;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: catch-up %s depth=%.0fms tempo=%.2f",
catching ? "started" : "finished", depth, j->tempo);
}
uint8_t enc[kMaxEncoded];
while (j->st.numSamples() < (uint)max_samples) {
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) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: decode failed len=%d result=%d", len, n);
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++;
}
if (n < max_samples) {
j->primed = false;
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: waiting for refill pcm=%d/%d processing=%dms", n, max_samples, processing_ms);
}
return n;
}
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;
}
}

28
src/call/call_jitter.h

@ -0,0 +1,28 @@
#ifndef CALL_JITTER_H
#define CALL_JITTER_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Звонок: FIFO Opus, адаптивный запас 60мс..max_reserve_ms (max-min за 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* call_jitter_create(call_jitter_decode_fn decode, void* arg, int max_reserve_ms);
void call_jitter_destroy(struct call_jitter* j);
void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len);
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

24
src/call/call_jitter_window.h

@ -0,0 +1,24 @@
#pragma once
#include <algorithm>
#include <cstdint>
// Десять секундных интервалов. Время передаёт аудиопоток из монотонных часов.
struct call_jitter_window {
struct bucket { int64_t second = -1; int low = 0, high = 0; } buckets[10];
int low = 0, high = 0, reserve = 60;
int max_reserve = 1000;
bool observe(int64_t now_ms, int 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 = std::max(60, std::min(max_reserve, high - low));
return changed_second;
}
};

2
src/call/call_proto.h

@ -90,7 +90,7 @@ struct call_pkt_media {
#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 50 /* ~1с буфер отправки (50 кадров × 20мс) */
#define CALL_MAX_TX_FRAMES 750 /* 15с: 10с перебоя + 5с запаса */
#ifdef __cplusplus
}

5
src/call/voice_sources.cmake

@ -1,6 +1,6 @@
# voice_sources.cmake — голосовой стек звонка (единый для desktop/Android/headless).
#
# SoundTouch (time-stretch) + voice_jitter (кодированные кадры + decode-on-pull)
# 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.
#
@ -19,7 +19,8 @@ function(utun_setup_voice LIB_DIR CALL_DIR)
set(UTUN_VOICE_SOURCES
${CALL_DIR}/call_audio.c
${CALL_DIR}/call_tones.c
${CALL_DIR}/voice_jitter.cpp
${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

2
src/chat/chat_event.h

@ -59,7 +59,7 @@ extern "C" {
#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_ms:2][buffer_ms:2][tempo_x100:2] */
#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][count:2][msg:var]* — расшифрованные сообщения беседы */
#define CHAT_EVT_CALL_PATH 42 /* [call_id:8][text:var] — BGP-маршрут до пира звонка */

7
src/chat/chat_setting.c

@ -42,6 +42,9 @@ static const struct chat_setting_def g_setting_defs[] = {
{"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},/* тишина до авто-отбоя */
{"media_download_max_peers", CHAT_SETTING_INT, 3, 1, 16},
{"storage_backfill_days", CHAT_SETTING_INT, 7, 1, 365},
{"standby_active_sec", CHAT_SETTING_INT, 2, 1, 3600},
@ -52,6 +55,10 @@ static const struct chat_setting_def g_setting_defs[] = {
{"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]))

11
src/chat/invite_build.c

@ -65,7 +65,7 @@ static int node_has_both_public(struct UTUN_INSTANCE* inst, uint64_t node_id) {
static uint64_t pick_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, int* out_has_both_public, int* out_priority) {
struct chat_core_ctx* cc = CC(inst);
uint64_t best_nid = 0;
int best_priority = 4; /* 1=regular, 2=self, 3=super, 4=invalid */
int best_priority = 4; /* 1=super, 2=regular, 3=self, 4=invalid */
int best_has_both = 0;
struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id);
@ -99,9 +99,9 @@ static uint64_t pick_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id,
int has_both = node_has_both_public(inst, nid);
int priority;
if (nid == cc->my_node_id) priority = 2; /* self */
else if (ntype == 4) priority = 3; /* supernode */
else priority = 1; /* regular */
if (ntype == 4) priority = 1; /* supernode, включая self */
else if (nid == cc->my_node_id) priority = 3;
else priority = 2;
if (priority <= best_priority) {
best_nid = nid;
@ -122,6 +122,9 @@ static uint64_t resolve_auto_node(struct UTUN_INSTANCE* inst, uint64_t channel_i
int has_both = 0, prio = 4;
uint64_t best = pick_best_node(inst, channel_id, &has_both, &prio);
if (best == 0) best = cc->my_node_id;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: auto selection ch=%llu node=0x%016llx role=%s self=%d",
(unsigned long long)channel_id, (unsigned long long)best,
prio == 1 ? "supernode" : prio == 2 ? "regular" : "self", best == cc->my_node_id);
if (out_has_both) *out_has_both = has_both;
if (out_priority) *out_priority = prio;
return best;

4
src/chat/invite_build.h

@ -20,7 +20,7 @@ struct chat_invite_build;
typedef void (*chat_invite_ready_fn)(void* arg, int result, const char* link);
/* Собрать invite-ссылку для канала.
* target_node_id = 0 → auto (лучший достижимый мембер, иначе self).
* target_node_id = 0 → auto (достижимый суперузел → обычный узел → self).
* target_node_id != 0 → конкретный узел (должен быть достижимым мембером канала).
* password = NULL → без пароля.
* NULL=ошибка запуска, callback не вызывается. Иначе callback после возврата,
@ -35,7 +35,7 @@ void chat_invite_build_cancel(struct chat_invite_build* request);
* Возвращает количество (0..max_ids), либо -1. */
int chat_invite_candidate_nodes(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_ids, int max_ids);
/* Авто-узел: лучший достижимый мембер, иначе self. Возвращает 0 и заполняет out_node_id, либо -1. */
/* Авто-узел: достижимый суперузел → обычный узел → self. Возвращает 0 и заполняет out_node_id, либо -1. */
int chat_invite_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_node_id);
/* Трамплин для gui_bridge (arg = u_malloc(struct chat_invite_build_req)).

21
src/lwip_tcp/lwip_tcp.c

@ -189,6 +189,8 @@ void tcp_free(struct tcp_pcb *pcb)
tcp_free_listen(pcb);
return;
}
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "LWIP_FREE pcb=%p state=%s lport=%u rport=%u arg=%p",
(void*)pcb, tcp_debug_state_str(pcb->state), pcb->local_port, pcb->remote_port, pcb->callback_arg);
memory_pool_free(pcb->ctx->pcb_pool, pcb);
}
@ -355,7 +357,11 @@ void tcp_abandon(struct tcp_pcb *pcb, int reset)
if (!pcb) return;
if (pcb->state == LISTEN) return;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "LWIP_ABANDON pcb=%p state=%s reset=%d errf=%p arg=%p",
(void*)pcb, tcp_debug_state_str(pcb->state), reset, (void*)pcb->errf, pcb->callback_arg);
if (pcb->state == TIME_WAIT) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "LWIP_ABANDON_TW pcb=%p lport=%u", (void*)pcb, pcb->local_port);
TCP_RMV(&pcb->ctx->tw_pcbs, pcb);
tcp_free(pcb);
return;
@ -529,11 +535,6 @@ void tcp_recved(struct tcp_pcb *pcb, uint16_t len)
wnd_inflation = tcp_update_rcv_ann_wnd(pcb);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"LWIP_RECVED lport=%u rport=%u len=%u rcv_wnd=%u rcv_ann=%u infl=%u wnd_upd=%d",
pcb->local_port, pcb->remote_port, len, pcb->rcv_wnd, pcb->rcv_ann_wnd,
(unsigned)wnd_inflation, wnd_inflation >= TCP_WND_UPDATE_THRESHOLD ? 1 : 0);
if (wnd_inflation >= TCP_WND_UPDATE_THRESHOLD) {
tcp_ack_now(pcb);
tcp_output(pcb);
@ -677,10 +678,6 @@ tcp_slowtmr_start:
if (pcb->persist_cnt >= backoff_cnt) {
int next_slot = 1;
if (pcb->snd_wnd == 0) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"LWIP_PERSIST lport=%u rport=%u snd_wnd=0 probe=%u unsent=%d snd_buf=%u",
pcb->local_port, pcb->remote_port, pcb->persist_probe,
tcp_seg_count(pcb->unsent), (unsigned)tcp_sndbuf(pcb));
if (tcp_zero_window_probe(pcb) != LERR_OK) next_slot = 0;
} else {
if (tcp_split_unsent_seg(pcb, (uint16_t)pcb->snd_wnd) == LERR_OK) {
@ -701,11 +698,6 @@ tcp_slowtmr_start:
if (pcb->rtime >= pcb->rto) {
lwip_tcp_trace_record(pcb->ctx, 'T', pcb->snd_nxt, 0, pcb->cwnd,
(uint16_t)pcb->rto, pcb->rtime, (uint8_t)pcb->state);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"LWIP_RTO lport=%u rport=%u st=%s rtime=%d rto=%d nrtx=%u cwnd=%u snd_wnd=%u aq=%d",
pcb->local_port, pcb->remote_port, tcp_debug_state_str(pcb->state),
pcb->rtime, pcb->rto, pcb->nrtx, pcb->cwnd, pcb->snd_wnd,
tcp_seg_count(pcb->unacked));
if ((tcp_rexmit_rto_prepare(pcb) == LERR_OK) || ((pcb->unacked == NULL) && (pcb->unsent != NULL))) {
if (pcb->state != SYN_SENT) {
uint8_t backoff_idx = LWIP_MIN(pcb->nrtx, sizeof(tcp_backoff) - 1);
@ -1145,6 +1137,7 @@ struct tcp_pcb *tcp_alloc(struct lwip_tcp_ctx *ctx, uint8_t prio)
pcb->ssthresh = TCP_SND_BUF;
pcb->recv = tcp_recv_null;
pcb->keep_idle = TCP_KEEPIDLE_DEFAULT;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "LWIP_ALLOC pcb=%p prio=%u", (void*)pcb, prio);
}
return pcb;
}

21
src/lwip_tcp/lwip_tcp_in.c

@ -113,13 +113,6 @@ static void pbuf_realloc_local(struct pbuf *p, uint16_t new_len)
p->tot_len = new_len;
}
int tcp_seg_count(const struct tcp_seg *seg)
{
int n = 0;
while (seg) { n++; seg = seg->next; }
return n;
}
// ============================================================
// tcp_input — main entry point
// ============================================================
@ -273,15 +266,6 @@ void lwip_tcp_input(struct lwip_tcp_ctx *ctx, struct pbuf *p,
recv_flags = 0;
recv_acked = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"LWIP_IN lport=%u rport=%u st=%s seq=%u ack=%u flags=%02x len=%u "
"rcv_wnd=%u rcv_ann=%u snd_wnd=%u snd_buf=%u cwnd=%u uq=%d aq=%d oq=%d",
pcb->local_port, pcb->remote_port, tcp_debug_state_str(pcb->state),
(unsigned)seqno, (unsigned)ackno, (unsigned)flags, (unsigned)tcplen,
pcb->rcv_wnd, pcb->rcv_ann_wnd, pcb->snd_wnd, (unsigned)tcp_sndbuf(pcb),
pcb->cwnd, tcp_seg_count(pcb->unsent),
tcp_seg_count(pcb->unacked), tcp_seg_count(pcb->ooseq));
if (flags & TCP_PSH) {
p->flags |= PBUF_FLAG_PUSH;
}
@ -346,11 +330,6 @@ void lwip_tcp_input(struct lwip_tcp_ctx *ctx, struct pbuf *p,
tcp_input_pcb = NULL;
if (tcp_input_delayed_close(pcb)) goto aborted;
tcp_output(pcb);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG,
"LWIP_OUT lport=%u rport=%u st=%s rcv_wnd=%u rcv_ann=%u snd_wnd=%u snd_buf=%u cwnd=%u acked=%u fin=%d",
pcb->local_port, pcb->remote_port, tcp_debug_state_str(pcb->state),
pcb->rcv_wnd, pcb->rcv_ann_wnd, pcb->snd_wnd, (unsigned)tcp_sndbuf(pcb),
pcb->cwnd, (unsigned)recv_acked, (recv_flags & TF_GOT_FIN) ? 1 : 0);
}
}
aborted:

1
src/lwip_tcp/lwip_tcp_priv.h

@ -188,7 +188,6 @@ extern const uint8_t tcp_persist_backoff[7];
// state name (для логов)
const char *tcp_debug_state_str(enum tcp_state s);
int tcp_seg_count(const struct tcp_seg *seg);
// Internal functions
err_t tcp_send_empty_ack(struct tcp_pcb *pcb);

4
src/proxy/proxy_protocol.h

@ -54,7 +54,7 @@ static inline int proxy_send(struct UTUN_INSTANCE* inst, uint64_t peer, uint8_t
}
struct ll_entry* e = queue_entry_new(0);
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: entry allocation failed sid=%08x", sid); return -1; }
e->dgram = u_malloc(TCP_PROXY_HDR_SIZE + len);
e->dgram = (uint8_t*)u_malloc(TCP_PROXY_HDR_SIZE + len);
if (!e->dgram) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: payload allocation failed sid=%08x", sid);
queue_entry_free(e); return -1;
@ -70,7 +70,7 @@ static inline void proxy_flow_flush(struct proxy_flow* f);
// Повторяем только недоставленные control-сообщения; DATA остаётся у владельца потока.
static inline void proxy_flow_retry(void* arg) {
struct proxy_flow* f = arg;
struct proxy_flow* f = (struct proxy_flow*)arg;
f->retry = NULL;
proxy_flow_flush(f);
if (f->wake) f->wake(f->arg);

128
src/proxy/tcp_proxy_client.c

@ -45,13 +45,13 @@ static err_t tcp_proxy_client_output_cb(void *arg, struct pbuf *p, uint32_t src_
static void tcp_proxy_client_feed_from_transport(struct tcp_proxy_client_conn *pc);
static struct ll_entry* tcp_proxy_client_entry_from_data(struct memory_pool* pool, const uint8_t* data, uint16_t len);
static void tcp_proxy_client_conn_free(struct tcp_proxy_client_conn *pc);
static void tcp_proxy_client_conn_finish(struct tcp_proxy_client_conn *pc);
static void tcp_proxy_client_conn_detach(struct tcp_proxy_client_conn *pc);
static void tcp_proxy_client_abort_pcb(struct tcp_proxy_client_conn *pc, const char* why);
static int tcp_proxy_client_send_msg(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst, uint8_t subcmd, uint32_t sid, const uint8_t* data, size_t len, int force);
static int tcp_proxy_client_send_data(struct tcp_proxy_client_conn* pc, const uint8_t* data, uint16_t len, int force);
static void tcp_proxy_client_tx_queue_drain_cb(struct ll_queue* q, void* arg);
static void tcp_proxy_client_pause_resume_cb(struct ll_queue* q, void* arg);
static void tcp_proxy_client_retry_timer_cb(void* arg);
static void tcp_proxy_client_diag_timer_cb(void* arg);
// ====================================================================
// Помощник ll_entry
@ -216,6 +216,7 @@ static err_t tcp_proxy_client_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t
struct tcp_proxy_client_conn *pc = u_calloc(1, sizeof(struct tcp_proxy_client_conn));
if (!pc) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: accept alloc failed"); return LERR_MEM; }
pc->proxy = p; pc->pcb = newpcb; pc->stream_id = ++p->next_stream_id;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "PROXY ACCEPT pcb=%p sid=%08x", (void*)newpcb, pc->stream_id);
proxy_flow_init(&pc->flow, p->inst, p->ua, p->via_node_id, ETCP_RT_ID_TCP_PROXY_SERVER, pc->stream_id, tcp_proxy_client_flow_wake, pc);
int i;
@ -251,7 +252,6 @@ static err_t tcp_proxy_client_accept_cb(void *arg, struct tcp_pcb *newpcb, err_t
if (tcp_proxy_client_send_connect(pc) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: send_connect failed sid=%08x to %d.%d.%d.%d:%d", pc->stream_id, pc->dest_ip[0], pc->dest_ip[1], pc->dest_ip[2], pc->dest_ip[3], ntohs(pc->dest_port)); tcp_arg(newpcb, NULL); tcp_abort(newpcb); queue_free(pc->to_lwip); queue_free(pc->tx_queue); u_free(pc); return LERR_ABRT; }
pc->next = p->conns; p->conns = pc; p->conn_count++;
pc->diag_timer = uasync_set_timeout(p->ua, 5000, pc, tcp_proxy_client_diag_timer_cb, "tpc_diag");
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "TCP proxy client: new conn sid=%08x local_port=%u -> %d.%d.%d.%d:%d total_conns=%d snd_wnd=%u mss=%u",
pc->stream_id, newpcb->local_port, pc->dest_ip[0], pc->dest_ip[1], pc->dest_ip[2], pc->dest_ip[3], ntohs(pc->dest_port), p->conn_count, newpcb->snd_wnd, newpcb->mss);
return LERR_OK;
@ -269,8 +269,14 @@ static int tcp_proxy_client_fin_flush(struct tcp_proxy_client_conn* pc) {
pc->flow.fin_pending = 1;
proxy_flow_flush(&pc->flow);
}
// Локальная сторона (браузер) завершена — отсоединяем pcb сразу, не дожидаясь
// доставки FIN в exit (иначе таймер TIME_WAIT освободит pcb без err_cb, pc->pcb
// останется висячим и последующий tcp_abort даст double free).
if (pc->fin_local && pc->fin_remote && !pc->fin_deferred && !pc->to_lwip->head) {
tcp_proxy_client_conn_detach(pc);
}
if (pc->flow.fin_sent && pc->fin_remote && !pc->fin_deferred && !pc->tx_queue->head && !pc->to_lwip->head) {
tcp_proxy_client_conn_finish(pc);
tcp_proxy_client_conn_free(pc);
return 1;
}
return 0;
@ -344,28 +350,6 @@ static void tcp_proxy_client_retry_timer_cb(void* arg) {
}
}
static void tcp_proxy_client_diag_timer_cb(void* arg) {
struct tcp_proxy_client_conn* pc = (struct tcp_proxy_client_conn*)arg;
if (!pc || !pc->proxy) return;
pc->diag_timer = NULL;
if (pc->rem_closed) return;
struct tcp_pcb* pcb = pc->pcb;
if (!pcb || pcb->state == CLOSED) return;
DEBUG_INFO(DEBUG_CATEGORY_DEBUG,
"PROXY DIAG sid=%08x st=%s fin_l=%d fin_r=%d "
"rcv_wnd=%u rcv_ann=%u snd_wnd=%u snd_buf=%u cwnd=%u "
"unsent=%d unacked=%d ooseq=%d "
"tx_q=%d to_lwip=%d to_exit=%u from_exit=%u bp=%u err=%d rem_closed=%d",
pc->stream_id, tcp_debug_state_str(pcb->state),
pc->fin_local, pc->fin_remote,
pcb->rcv_wnd, pcb->rcv_ann_wnd, pcb->snd_wnd, (unsigned)tcp_sndbuf(pcb), pcb->cwnd,
tcp_seg_count(pcb->unsent), tcp_seg_count(pcb->unacked), tcp_seg_count(pcb->ooseq),
pc->tx_queue ? queue_entry_count(pc->tx_queue) : -1,
pc->to_lwip ? queue_entry_count(pc->to_lwip) : -1,
pc->bytes_to_exit, pc->bytes_from_exit, pc->bp_count, pc->error, pc->rem_closed);
pc->diag_timer = uasync_set_timeout(pc->proxy->ua, 5000, pc, tcp_proxy_client_diag_timer_cb, "tpc_diag");
}
static err_t tcp_proxy_client_recv_cb(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) {
struct tcp_proxy_client_conn *pc = (struct tcp_proxy_client_conn *)arg;
if (!pc || !pc->proxy || pc->rem_closed) { if (p) pbuf_free(p); return LERR_OK; }
@ -426,6 +410,8 @@ static err_t tcp_proxy_client_sent_cb(void *arg, struct tcp_pcb *pcb, uint16_t l
static void tcp_proxy_client_err_cb(void *arg, err_t err) {
struct tcp_proxy_client_conn *pc = (struct tcp_proxy_client_conn *)arg;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "PROXY ERR_CB pc=%p pcb=%p sid=%08x err=%d rem_closed=%d",
(void*)pc, (void*)(pc ? pc->pcb : NULL), pc ? pc->stream_id : 0, err, pc ? pc->rem_closed : 0);
if (!pc || !pc->proxy || pc->rem_closed) { DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "PROXY tcp_proxy_client_err_cb: pc=%p proxy=%p rem_closed=%d err=%d", (void*)pc, pc ? (void*)pc->proxy : NULL, pc ? pc->rem_closed : 0, err); return; }
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "TCP proxy client: error %d sid=%08x fin_local=%d rem_closed=%d",
err, pc->stream_id, pc->fin_local, pc->rem_closed);
@ -443,15 +429,7 @@ static err_t tcp_proxy_client_poll_cb(void *arg, struct tcp_pcb *pcb) {
if (pc->error) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "PROXY CLEANUP error sid=%08x", pc->stream_id);
if (pc->pcb) {
tcp_arg(pc->pcb, NULL);
tcp_recv(pc->pcb, NULL);
tcp_sent(pc->pcb, NULL);
tcp_err(pc->pcb, NULL);
tcp_poll(pc->pcb, NULL, 0);
tcp_abort(pc->pcb);
pc->pcb = NULL;
}
tcp_proxy_client_abort_pcb(pc, "POLL_ABORT");
tcp_proxy_client_conn_free(pc);
return LERR_ABRT;
}
@ -526,6 +504,7 @@ static void tcp_proxy_client_conn_free(struct tcp_proxy_client_conn *pc) {
if (!pc) return;
struct tcp_proxy_client *p = pc->proxy;
proxy_flow_destroy(&pc->flow);
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "PROXY FREE pcb=%p sid=%08x", (void*)pc->pcb, pc->stream_id);
uint32_t pending = pc->to_lwip ? queue_entry_count(pc->to_lwip) : 0;
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "PROXY FREE sid=%08x total_conns=%d to_lwip_q=%u",
pc->stream_id, p->conn_count, pending);
@ -547,25 +526,49 @@ static void tcp_proxy_client_conn_free(struct tcp_proxy_client_conn *pc) {
queue_free(pc->tx_queue); pc->tx_queue = NULL;
}
if (pc->tx_retry_timer) { uasync_cancel_timeout(p->ua, pc->tx_retry_timer); pc->tx_retry_timer = NULL; }
if (pc->diag_timer) { uasync_cancel_timeout(p->ua, pc->diag_timer); pc->diag_timer = NULL; }
if (p && p->inst) etcp_router_cancel_send_ready(p->inst, TOPO_GROUP_UTUN, p->via_node_id, ETCP_RT_ID_TCP_PROXY_SERVER, &pc->tx_waiter);
u_free(pc);
}
// Завершить локальное TCP-соединение: отвязать коллбэки, сбросить pc->pcb
// (lwIP дальше сам освободит pcb — в т.ч. молча по истечении TIME_WAIT) и освободить conn.
// Вызывается, когда обе стороны обменялись FIN (fin_local && fin_remote) — релей завершён.
static void tcp_proxy_client_conn_finish(struct tcp_proxy_client_conn *pc) {
if (!pc) return;
if (pc->pcb) {
tcp_arg(pc->pcb, NULL);
tcp_recv(pc->pcb, NULL);
tcp_sent(pc->pcb, NULL);
tcp_err(pc->pcb, NULL);
tcp_poll(pc->pcb, NULL, 0);
// Отвязать pcb от conn: сбросить коллбэки и pc->pcb. lwIP дальше сам освободит pcb
// (в т.ч. молча по истечении TIME_WAIT). Контракт: после вызова pc->pcb == NULL.
static void tcp_proxy_client_conn_detach(struct tcp_proxy_client_conn *pc) {
if (!pc || !pc->pcb) return;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "PROXY DETACH pcb=%p state=%s sid=%08x",
(void*)pc->pcb, tcp_debug_state_str(pc->pcb->state), pc->stream_id);
tcp_arg(pc->pcb, NULL);
tcp_recv(pc->pcb, NULL);
tcp_sent(pc->pcb, NULL);
tcp_err(pc->pcb, NULL);
tcp_poll(pc->pcb, NULL, 0);
pc->pcb = NULL;
}
// Проверка, что pc->pcb ещё не освобождён пулом (защита от висячего указателя:
// таймер TIME_WAIT мог освободить pcb без err_cb). Возвращает 0 и сбрасывает
// pc->pcb, если pcb освобождён; иначе 1 (pcb валиден).
static int tcp_proxy_client_pcb_guard(struct tcp_proxy_client_conn *pc, const char* why) {
struct tcp_pcb* pcb = pc->pcb;
struct lwip_tcp_ctx* ctx = pc->proxy ? pc->proxy->lwip : NULL;
if (ctx && memory_pool_is_freed(ctx->pcb_pool, pcb)) {
DEBUG_WARN(DEBUG_CATEGORY_PROXY, "PROXY %s: pcb=%p already freed (sid=%08x) — skip",
why, (void*)pcb, pc->stream_id);
pc->pcb = NULL;
return 0;
}
tcp_proxy_client_conn_free(pc);
return 1;
}
// Безопасный abort с защитой от висячего pc->pcb (pcb уже освобождён пулом, например
// таймером TIME_WAIT). Если pcb освобождён — только сбрасываем ссылку, не трогая память.
static void tcp_proxy_client_abort_pcb(struct tcp_proxy_client_conn *pc, const char* why) {
if (!pc || !pc->pcb) return;
struct tcp_pcb* pcb = pc->pcb;
if (!tcp_proxy_client_pcb_guard(pc, why)) return;
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "PROXY %s pcb=%p state=%s sid=%08x",
why, (void*)pcb, tcp_debug_state_str(pcb->state), pc->stream_id);
tcp_proxy_client_conn_detach(pc);
tcp_abort(pcb);
}
// ====================================================================
@ -592,7 +595,7 @@ static void tcp_proxy_client_handle_data(struct tcp_proxy_client* p, struct ETCP
if (e) { queue_dgram_free(e); queue_entry_free(e); }
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy: receive failed sid=%08x", stream_id);
tcp_proxy_client_send_error(pc);
if (pc->pcb) { tcp_arg(pc->pcb, NULL); tcp_err(pc->pcb, NULL); tcp_abort(pc->pcb); pc->pcb = NULL; }
tcp_proxy_client_abort_pcb(pc, "DATA_ABORT");
tcp_proxy_client_conn_free(pc);
} else {
queue_data_put(pc->to_lwip, e);
@ -605,9 +608,11 @@ static void tcp_proxy_client_handle_data(struct tcp_proxy_client* p, struct ETCP
static void tcp_proxy_client_handle_close(struct tcp_proxy_client* p, uint32_t stream_id) {
struct tcp_proxy_client_conn* pc = tcp_proxy_client_find_conn(p, stream_id);
if (!pc) { DEBUG_WARN(DEBUG_CATEGORY_PROXY, "PROXY CLOSE sid=%08x — no conn, dropping", stream_id); return; }
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "PROXY REM_CLOSED sid=%08x fin_local=%d pcb_state=%u", stream_id, pc->fin_local, pc->pcb ? pc->pcb->state : 0);
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "PROXY REM_CLOSED sid=%08x fin_local=%d pcb=%p", stream_id, pc->fin_local, (void*)pc->pcb);
pc->rem_closed = 1;
if (pc->pcb) {
if (pc->pcb && tcp_proxy_client_pcb_guard(pc, "CLOSE")) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "PROXY CLOSE pcb=%p state=%s sid=%08x",
(void*)pc->pcb, tcp_debug_state_str(pc->pcb->state), stream_id);
tcp_arg(pc->pcb, NULL);
tcp_recv(pc->pcb, NULL);
tcp_sent(pc->pcb, NULL);
@ -623,15 +628,7 @@ static void tcp_proxy_client_handle_error(struct tcp_proxy_client* p, uint32_t s
struct tcp_proxy_client_conn* pc = tcp_proxy_client_find_conn(p, stream_id);
if (!pc) { DEBUG_INFO(DEBUG_CATEGORY_PROXY, "PROXY ERROR sid=%08x — no conn, dropping", stream_id); return; }
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "PROXY ERROR from exit sid=%08x fin_local=%d", stream_id, pc->fin_local);
if (pc->pcb) {
tcp_arg(pc->pcb, NULL);
tcp_recv(pc->pcb, NULL);
tcp_sent(pc->pcb, NULL);
tcp_err(pc->pcb, NULL);
tcp_poll(pc->pcb, NULL, 0);
tcp_abort(pc->pcb);
pc->pcb = NULL;
}
tcp_proxy_client_abort_pcb(pc, "ERROR");
pc->rem_closed = 1;
tcp_proxy_client_conn_free(pc);
}
@ -677,15 +674,7 @@ void tcp_proxy_client_router_recv_cb(struct ETCP_CONN* conn, struct ll_entry* en
struct tcp_proxy_client_conn *pc, *next;
for (pc = proxy->conns; pc; pc = next) {
next = pc->next;
if (pc->pcb) {
tcp_arg(pc->pcb, NULL);
tcp_recv(pc->pcb, NULL);
tcp_sent(pc->pcb, NULL);
tcp_err(pc->pcb, NULL);
tcp_poll(pc->pcb, NULL, 0);
tcp_abort(pc->pcb);
pc->pcb = NULL;
}
tcp_proxy_client_abort_pcb(pc, "CLOSE_ALL");
tcp_proxy_client_conn_free(pc);
}
proxy->conns = NULL; proxy->conn_count = 0;
@ -854,7 +843,6 @@ void tcp_proxy_client_destroy(struct tcp_proxy_client* p) {
queue_free(pc->tx_queue); pc->tx_queue = NULL;
}
if (pc->tx_retry_timer) { uasync_cancel_timeout(p->ua, pc->tx_retry_timer); pc->tx_retry_timer = NULL; }
if (pc->diag_timer) { uasync_cancel_timeout(p->ua, pc->diag_timer); pc->diag_timer = NULL; }
if (p->inst) etcp_router_cancel_send_ready(p->inst, TOPO_GROUP_UTUN, p->via_node_id, ETCP_RT_ID_TCP_PROXY_SERVER, &pc->tx_waiter);
u_free(pc); pc = next;
}

2
src/proxy/tcp_proxy_client.h

@ -54,8 +54,6 @@ struct tcp_proxy_client_conn {
uint32_t bytes_from_exit; // байт получено от exit (в lwIP/TUN)
uint32_t bp_count; // счётчик backpressure (send fail)
void* diag_timer; // периодический снимок состояния (DEBUG-категория)
uint8_t dest_ip[4];
uint16_t dest_port;
};

16
src/proxy/tcp_proxy_server.c

@ -55,6 +55,18 @@ static int send_msg(struct UTUN_INSTANCE* inst, uint64_t group_id, uint64_t dst,
return proxy_send(inst, dst, ETCP_RT_ID_TCP_PROXY_CLIENT, subcmd, sid, data, len, force);
}
// CLOSE может синхронно освободить rc через on_closed: резервируем состояние до push.
static void server_queue_close(struct tcp_proxy_server_conn* rc) {
if (rc->close_queued) return;
rc->close_queued = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_PROXY, "proxy exit: queue CLOSE peer=%016llx sid=%08x fd=%d",
(unsigned long long)rc->peer_node_id, rc->stream_id, (int)rc->tc->sock);
if (tcp_conn_push_close(rc->tc) == 0) return;
rc->close_queued = 0;
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "proxy exit: queue CLOSE failed peer=%016llx sid=%08x",
(unsigned long long)rc->peer_node_id, rc->stream_id);
}
// FIN относится только к прочитанному направлению: сначала передаём весь read_queue.
static void server_maybe_finish(struct tcp_proxy_server_conn* rc) {
struct tcp_conn* tc = rc->tc;
@ -64,7 +76,7 @@ static void server_maybe_finish(struct tcp_proxy_server_conn* rc) {
proxy_flow_flush(&rc->flow);
}
if (rc->flow.fin_sent && rc->flow.fin_received && tc->fin_local) {
if (tcp_conn_push_close(tc) == 0) rc->close_queued = 1;
server_queue_close(rc);
}
}
@ -377,7 +389,7 @@ void tcp_proxy_server_handle_close(struct UTUN_INSTANCE* inst, uint64_t peer, ui
}
}
if (rc->tc->connected) {
if (!rc->close_queued && tcp_conn_push_close(rc->tc) == 0) rc->close_queued = 1;
server_queue_close(rc);
} else tcp_proxy_server_conn_free(rc);
}

280
src/radio/radio_audio.c

@ -1,12 +1,12 @@
// radio_audio.c — сервисный аудио-движок рации (Opus TX + per-source jitter + микшер).
//
// Таблица говорящих: per-source инстанс (Opus-декодер + voice_jitter с time-stretch),
// Таблица говорящих: per-source инстанс (Opus-декодер + radio_jitter с time-stretch),
// ключ = src_node_id. Один пир — один поток: новый stream_id сбрасывает инстанс
// (не создаём дубль). Инстанс удаляется по FIN (после дренажа буфера) или по таймауту
// (нет кадров 2с — потерян FIN / пир отвалился).
//
// RX: radio.c → radio_set_frame_cb → radio_audio_on_frame (uasync) → vj_push (кодированный кадр);
// аудио-поток radio_audio_pull_pcm → vj_pull (декод + SoundTouch stretch) → микшер.
// RX: radio.c → radio_set_frame_cb → radio_audio_on_frame (uasync) → rj_push (кодированный кадр);
// аудио-поток radio_audio_pull_pcm → rj_pull (декод + SoundTouch stretch) → микшер.
// TX: аудио-поток radio_audio_feed_pcm → encode → uasync_post(radio_talk_send_trampoline).
// Микшер избыточных источников — насыщающее суммирование (soft-clip). Полудуплекс.
// Синглтон на один активный group_id (GUI слушает один канал), защищён мьютексом.
@ -14,11 +14,13 @@
#include "radio_audio.h"
#include "radio.h"
#include "radio_proto.h"
#include "../call/voice_jitter.h"
#include "radio_vad.h"
#include "radio_jitter.h"
#include "../chat/chat_setting.h"
#include "../utun_instance.h"
#include "../../lib/opus_codec.h"
#include "../../lib/audio_compressor.h"
#include "../../lib/silero_vad.h"
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "../../lib/u_async.h"
@ -43,6 +45,10 @@
#define RADIO_AUDIO_MIN_TEMPO 0.75 /* темп при пустом буфере (замедление) */
#define RADIO_AUDIO_MAX_TEMPO 1.6 /* темп при глубоком буфере (догон) */
/* ── VAD авто-PTT (отдельный режим, ручной PTT не меняется) ── */
#define RADIO_VAD_CONFIRM_WINDOWS 2 /* окон (32мс) выше порога для старта (~64мс) */
#define RADIO_VAD_BUSY_TB 2000 /* 200мс: канал занят после последнего RX-кадра */
struct radio_pending {
uint8_t used;
uint16_t seq;
@ -56,8 +62,8 @@ struct radio_source {
uint8_t used;
uint8_t ending; /* получен FIN — дренируем и удаляем */
uint64_t last_frame_tb;
opus_codec_decoder_t* dec; /* decode-аргумент для vj (stateless per call) */
struct vj* vj; /* jitter-буфер с time-stretch */
opus_codec_decoder_t* dec; /* decode-аргумент для rj (stateless per call) */
struct rj* rj; /* jitter-буфер с time-stretch */
/* reorder по seq (mesh: кадры могут прийти не по порядку через разных соседей) */
uint16_t next_seq; /* ожидаемый следующий seq */
uint8_t have_seq;
@ -77,13 +83,24 @@ static struct radio_source g_sources[RADIO_AUDIO_MAX_SOURCES];
static int32_t g_mix[RADIO_AUDIO_MIX_SAMPLES];
static uint64_t g_last_stats_tb = 0;
/* ── VAD авто-PTT: состояние (под g_mtx) ── */
static int g_vad_mode = 0; /* режим включён для текущей группы */
static silero_vad_t* g_vad = NULL; /* детектор (16кГц), создаётся в start */
static float g_vad_threshold = 0.5f;
static int g_vad_hangover_ms = 200;
static int g_vad_manual_ptt = 0; /* PTT override: зажата — VAD не вмешивается */
static float g_vad_win[RADIO_VAD_WINDOW_16K]; /* накопитель окон 16кГц */
static int g_vad_win_len = 0;
static struct radio_vad_fsm g_vad_fsm;
static uint64_t g_last_rx_tb = 0; /* последний принятый аудио-кадр (busy) */
/* TX-статистика (кадрирование/кодирование), выводится раз в секунду пока идёт PTT. */
static uint64_t g_tx_stats_tb = 0;
static uint32_t g_tx_frames = 0; /* кадров за интервал */
static uint32_t g_tx_bytes = 0; /* суммарный размер opus за интервал */
static uint32_t g_tx_tail = 0; /* дропнутых хвостовых сэмплов за интервал */
/* vj decode-коллбэк: дёргается только из аудио-потока (vj_pull). */
/* rj decode-коллбэк: дёргается только из аудио-потока (rj_pull). */
static int radio_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);
@ -91,8 +108,8 @@ static int radio_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t
/* ── джиттер-буфер рации (адаптивный, симметричный темп + pre-roll) ── */
static struct vj* radio_vj_create(opus_codec_decoder_t* dec) {
struct vj_cfg cfg;
static struct rj* radio_rj_create(opus_codec_decoder_t* dec) {
struct rj_cfg cfg;
cfg.sample_rate = RADIO_AUDIO_SAMPLE_RATE;
cfg.channels = RADIO_AUDIO_MIX_CHANNELS;
cfg.frame_samples = RADIO_AUDIO_FRAME_SAMPLES;
@ -104,7 +121,7 @@ static struct vj* radio_vj_create(opus_codec_decoder_t* dec) {
cfg.pre_roll = 1;
cfg.decode = radio_audio_decode_cb;
cfg.decode_arg = dec;
return vj_create_cfg(&cfg);
return rj_create_cfg(&cfg);
}
/* ── reorder по seq (mesh: кадры одного источника могут прийти не по порядку) ── */
@ -125,7 +142,7 @@ static void radio_src_flush_pending(struct radio_source* s) {
if (!s->pending[i].used) continue;
if (mi < 0 || (int16_t)(s->pending[i].seq - s->pending[mi].seq) < 0) mi = i;
}
vj_push(s->vj, s->pending[mi].opus, s->pending[mi].len);
rj_push(s->rj, s->pending[mi].opus, s->pending[mi].len);
s->next_seq = (uint16_t)(s->pending[mi].seq + 1);
s->pending[mi].used = 0;
s->reorder_count--;
@ -140,7 +157,7 @@ static void radio_src_flush_contiguous(struct radio_source* s) {
progressed = 0;
for (int i = 0; i < RADIO_AUDIO_REORDER_MAX; i++) {
if (!s->pending[i].used || s->pending[i].seq != s->next_seq) continue;
vj_push(s->vj, s->pending[i].opus, s->pending[i].len);
rj_push(s->rj, s->pending[i].opus, s->pending[i].len);
s->pending[i].used = 0;
s->reorder_count--;
s->next_seq++;
@ -184,7 +201,7 @@ static void radio_src_push_frame(struct radio_source* s, uint16_t seq,
return;
}
}
vj_push(s->vj, opus, len);
rj_push(s->rj, opus, len);
s->next_seq++;
radio_src_flush_contiguous(s);
}
@ -198,7 +215,7 @@ static struct radio_source* radio_src_find(uint64_t src) {
}
static void radio_src_free(struct radio_source* s) {
if (s->vj) { vj_destroy(s->vj); s->vj = NULL; }
if (s->rj) { rj_destroy(s->rj); s->rj = NULL; }
if (s->dec) { opus_codec_decoder_destroy(s->dec); s->dec = NULL; }
s->used = 0; s->ending = 0; s->last_frame_tb = 0;
radio_src_reorder_reset(s);
@ -213,9 +230,9 @@ static struct radio_source* radio_src_acquire(uint64_t src, uint16_t stream) {
s->stream_id = stream;
s->ending = 0;
radio_src_reorder_reset(s);
vj_destroy(s->vj);
s->vj = radio_vj_create(s->dec);
if (!s->vj) { radio_src_free(s); return NULL; }
rj_destroy(s->rj);
s->rj = radio_rj_create(s->dec);
if (!s->rj) { radio_src_free(s); return NULL; }
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: source reset src=%016llx stream=%u", RADIO_AUDIO_ID, (unsigned long long)src, stream);
}
return s;
@ -225,8 +242,8 @@ static struct radio_source* radio_src_acquire(uint64_t src, uint16_t stream) {
s = &g_sources[i];
s->dec = opus_codec_decoder_create(RADIO_AUDIO_SAMPLE_RATE, RADIO_AUDIO_MIX_CHANNELS);
if (!s->dec) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: decoder create failed", RADIO_AUDIO_ID); return NULL; }
s->vj = radio_vj_create(s->dec);
if (!s->vj) { opus_codec_decoder_destroy(s->dec); s->dec = NULL; return NULL; }
s->rj = radio_rj_create(s->dec);
if (!s->rj) { opus_codec_decoder_destroy(s->dec); s->dec = NULL; return NULL; }
s->src_node_id = src;
s->stream_id = stream;
s->used = 1; s->ending = 0; s->last_frame_tb = 0;
@ -239,6 +256,8 @@ static struct radio_source* radio_src_acquire(uint64_t src, uint16_t stream) {
return NULL;
}
static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, void (*fn)(void*));
/* ── lifecycle ── */
int radio_audio_init(struct UTUN_INSTANCE* inst) {
@ -258,13 +277,35 @@ void radio_audio_destroy(struct UTUN_INSTANCE* inst) {
}
int radio_audio_start(uint64_t group_id, int capture_channels) {
if (group_id == 0) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: start bad group_id=0", RADIO_AUDIO_ID); return -1; }
if (capture_channels != 1 && capture_channels != 2) capture_channels = 1;
if (!group_id) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: start bad group_id=0", RADIO_AUDIO_ID); return -1; }
pthread_mutex_lock(&g_mtx);
if (g_active && g_group_id == group_id) { pthread_mutex_unlock(&g_mtx); return 0; }
if (g_active) { pthread_mutex_unlock(&g_mtx); radio_audio_stop(); pthread_mutex_lock(&g_mtx); }
pthread_mutex_unlock(&g_mtx);
radio_audio_stop();
pthread_mutex_lock(&g_mtx);
g_group_id = group_id;
g_active = 1;
g_last_rx_tb = 0;
memset(g_sources, 0, sizeof(g_sources));
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: listening grp=%016llx", RADIO_AUDIO_ID, (unsigned long long)group_id);
if (!capture_channels) return 0;
int rc = radio_audio_capture_start(group_id, capture_channels, chat_setting_get_int(g_inst, "radio_vad_enabled", 0));
if (rc != 0) radio_audio_stop();
return rc;
}
int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_enabled) {
if (capture_channels != 1 && capture_channels != 2) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: capture invalid channels=%d", RADIO_AUDIO_ID, capture_channels);
return -1;
}
pthread_mutex_lock(&g_mtx);
if (!g_active || g_group_id != group_id || g_encoder) {
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: capture start invalid state grp=%016llx", RADIO_AUDIO_ID, (unsigned long long)group_id);
return -1;
}
opus_codec_encoder_t* enc = opus_codec_encoder_create(RADIO_AUDIO_SAMPLE_RATE, capture_channels);
if (!enc) { pthread_mutex_unlock(&g_mtx); DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: encoder create failed", RADIO_AUDIO_ID); return -1; }
int opus_mode = chat_setting_get_int(g_inst, "radio_opus_mode", 0);
@ -272,8 +313,7 @@ int radio_audio_start(uint64_t group_id, int capture_channels) {
opus_codec_encoder_application_set(enc, opus_mode ? OPUS_CODEC_APP_AUDIO : OPUS_CODEC_APP_VOIP);
opus_codec_encoder_bitrate_set(enc, opus_bitrate_kbps * 1000);
/* компрессор микрофона (AGC): конфиг из [chatserver], lookahead=0 — без задержки и хвоста.
* gain_smoothed не сбрасывается между PTT — адаптивная громкость запоминается. */
/* Компрессор TX: lookahead=0, без задержки и хвоста. */
struct audio_compressor* ac = audio_compressor_create();
if (ac) {
audio_compressor_config_t accfg = {0};
@ -289,23 +329,54 @@ int radio_audio_start(uint64_t group_id, int capture_channels) {
audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "radio_compressor_enabled", 1));
}
if (!ac) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: compressor unavailable", RADIO_AUDIO_ID);
/* VAD анализирует текущие кадры без истории захвата. */
g_vad_mode = vad_enabled ? 1 : 0;
if (g_vad_mode) {
g_vad_threshold = (float)chat_setting_get_int(g_inst, "radio_vad_threshold", 50) / 100.0f;
g_vad_hangover_ms = chat_setting_get_int(g_inst, "radio_vad_hangover_ms", 200);
g_vad = silero_vad_create_default();
if (!g_vad) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: silero_vad_create_default failed — VAD auto-PTT disabled", RADIO_AUDIO_ID);
g_vad_mode = 0;
}
}
if (g_vad_mode) {
g_vad_win_len = 0;
radio_vad_fsm_reset(&g_vad_fsm);
}
g_vad_manual_ptt = 0;
g_encoder = enc;
g_compressor = ac;
g_capture_channels = capture_channels;
g_group_id = group_id;
g_active = 1;
memset(g_sources, 0, sizeof(g_sources));
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: capture started grp=%016llx ch=%d opus=%s bitrate=%dkbps compressor=%s vad=%d threshold=%.2f hangover=%dms",
RADIO_AUDIO_ID, (unsigned long long)group_id, capture_channels, opus_mode ? "AUDIO" : "VOIP", opus_bitrate_kbps,
ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable", g_vad_mode, g_vad_threshold, g_vad_hangover_ms);
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: started grp=%016llx opus_mode=%s bitrate=%dkbps capture_ch=%d compressor=%s", RADIO_AUDIO_ID,
(unsigned long long)group_id, opus_mode ? "AUDIO" : "VOIP", opus_bitrate_kbps, capture_channels,
ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable");
return 0;
}
void radio_audio_capture_stop(void) {
pthread_mutex_lock(&g_mtx);
if (g_encoder) {
if (g_vad_manual_ptt || g_vad_fsm.talking)
radio_audio_post_talk(g_inst ? g_inst->ua : NULL, g_group_id, radio_talk_end_trampoline);
opus_codec_encoder_destroy(g_encoder); g_encoder = NULL;
if (g_compressor) { audio_compressor_destroy(g_compressor); g_compressor = NULL; }
if (g_vad) { silero_vad_destroy(g_vad); g_vad = NULL; }
g_vad_mode = 0;
g_vad_manual_ptt = 0;
g_vad_win_len = 0;
radio_vad_fsm_reset(&g_vad_fsm);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: capture stopped", RADIO_AUDIO_ID);
}
pthread_mutex_unlock(&g_mtx);
}
void radio_audio_stop(void) {
opus_codec_encoder_t* enc = NULL;
struct audio_compressor* ac = NULL;
radio_audio_capture_stop();
int was_active = 0;
pthread_mutex_lock(&g_mtx);
@ -313,16 +384,12 @@ void radio_audio_stop(void) {
was_active = 1;
g_active = 0;
g_group_id = 0;
enc = g_encoder; g_encoder = NULL;
ac = g_compressor; g_compressor = NULL;
for (int i = 0; i < RADIO_AUDIO_MAX_SOURCES; i++)
if (g_sources[i].used) radio_src_free(&g_sources[i]);
}
pthread_mutex_unlock(&g_mtx);
if (was_active) {
if (enc) opus_codec_encoder_destroy(enc);
if (ac) audio_compressor_destroy(ac);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: stopped", RADIO_AUDIO_ID);
}
}
@ -335,11 +402,27 @@ int radio_audio_active(void) {
return a;
}
int radio_audio_vad_mode(void) {
int m = 0;
pthread_mutex_lock(&g_mtx);
m = g_vad_mode;
pthread_mutex_unlock(&g_mtx);
return m;
}
int radio_audio_transmitting(void) {
pthread_mutex_lock(&g_mtx);
int talking = g_active && (g_vad_manual_ptt || g_vad_fsm.talking);
pthread_mutex_unlock(&g_mtx);
return talking;
}
/* ── PTT (GUI → uasync) ── */
static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, void (*fn)(void*)) {
if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post without uasync", RADIO_AUDIO_ID); return; }
struct radio_talk_arg* a = (struct radio_talk_arg*)u_malloc(sizeof(*a));
if (!a) return;
if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post allocation failed", RADIO_AUDIO_ID); return; }
a->inst = g_inst;
a->group_id = group_id;
uasync_post(ua, fn, a);
@ -347,12 +430,119 @@ static void radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, void (*f
void radio_audio_talk_begin(uint64_t group_id) {
if (!g_inst || !g_inst->ua) return;
pthread_mutex_lock(&g_mtx);
if (!g_active || group_id != g_group_id || !g_encoder) {
pthread_mutex_unlock(&g_mtx);
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: talk_begin without capture grp=%016llx", RADIO_AUDIO_ID, (unsigned long long)group_id);
return;
}
g_vad_manual_ptt = 1;
if (g_vad_fsm.talking) {
/* VAD вела передачу — закрываем её FIN, затем открываем ручной burst */
radio_audio_post_talk(g_inst->ua, group_id, radio_talk_end_trampoline);
radio_vad_fsm_reset(&g_vad_fsm);
}
radio_audio_post_talk(g_inst->ua, group_id, radio_talk_begin_trampoline);
pthread_mutex_unlock(&g_mtx);
}
void radio_audio_talk_end(uint64_t group_id) {
if (!g_inst || !g_inst->ua) return;
pthread_mutex_lock(&g_mtx);
if (!g_active || group_id != g_group_id || !g_vad_manual_ptt) {
pthread_mutex_unlock(&g_mtx);
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: talk_end without manual transmission grp=%016llx", RADIO_AUDIO_ID,
(unsigned long long)group_id);
return;
}
g_vad_manual_ptt = 0;
radio_audio_post_talk(g_inst->ua, group_id, radio_talk_end_trampoline);
pthread_mutex_unlock(&g_mtx);
}
/* ── VAD авто-PTT: helpers (под g_mtx) ── */
/* Закодировать один 20мс кадр и запостить radio_talk_send в uasync.
* Вызывается при удержании g_mtx (использует g_encoder/g_capture_channels). */
static void radio_audio_encode_post(uint64_t group_id, const int16_t* frame,
struct UASYNC* ua, struct UTUN_INSTANCE* inst) {
if (!ua || !inst) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: encode without instance", RADIO_AUDIO_ID); return; }
uint8_t opus[RADIO_MAX_OPUS];
int l = opus_codec_encode(g_encoder, frame, RADIO_AUDIO_FRAME_SAMPLES, opus, (int)sizeof(opus));
if (l <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: opus encode failed rc=%d", RADIO_AUDIO_ID, l); return; }
struct radio_media_arg* a = (struct radio_media_arg*)u_malloc(sizeof(*a));
if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: media allocation failed", RADIO_AUDIO_ID); return; }
a->inst = inst;
a->group_id = group_id;
a->len = (uint16_t)l;
memcpy(a->opus, opus, (size_t)l);
uasync_post(ua, radio_talk_send_trampoline, a);
}
/* VAD-ветка feed_pcm: ресемпл → Silero → фильтр → старт/стоп/передача (держит g_mtx). */
static int radio_vad_feed(uint64_t group_id, const int16_t* pcm, int count) {
float out16k[RADIO_VAD_FRAME_SAMPLES_16K];
struct UASYNC* ua;
struct UTUN_INSTANCE* inst;
pthread_mutex_lock(&g_mtx);
if (!g_active || group_id != g_group_id || !g_encoder || !g_vad) {
pthread_mutex_unlock(&g_mtx);
return -1;
}
ua = g_inst ? g_inst->ua : NULL;
inst = g_inst;
if (g_vad_manual_ptt) {
/* Ручной PTT: передаём текущий кадр, VAD не вмешивается. */
radio_audio_encode_post(group_id, pcm, ua, inst);
pthread_mutex_unlock(&g_mtx);
return 0;
}
radio_vad_resample(pcm, g_capture_channels, out16k);
int off = 0;
while (off < RADIO_VAD_FRAME_SAMPLES_16K) {
int space = RADIO_VAD_WINDOW_16K - g_vad_win_len;
int take = (RADIO_VAD_FRAME_SAMPLES_16K - off < space) ? (RADIO_VAD_FRAME_SAMPLES_16K - off) : space;
memcpy(g_vad_win + g_vad_win_len, out16k + off, (size_t)take * sizeof(float));
g_vad_win_len += take;
off += take;
if (g_vad_win_len == RADIO_VAD_WINDOW_16K) {
float prob = 0.0f;
if (silero_vad_process(g_vad, g_vad_win, &prob) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: VAD processing failed", RADIO_AUDIO_ID);
prob = 0.0f;
}
g_vad_win_len = 0;
uint64_t now = get_time_tb();
int busy = (g_last_rx_tb != 0 && (now - g_last_rx_tb) < RADIO_VAD_BUSY_TB) ? 1 : 0;
if (!g_tx_stats_tb || now - g_tx_stats_tb >= RADIO_AUDIO_STATS_TB) {
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: vad prob=%.3f threshold=%.2f busy=%d talking=%d confirm=%d",
RADIO_AUDIO_ID, prob, g_vad_threshold, busy, g_vad_fsm.talking, g_vad_fsm.confirm_count);
g_tx_stats_tb = now;
}
int action = radio_vad_fsm_update(&g_vad_fsm, prob, g_vad_threshold,
RADIO_VAD_CONFIRM_WINDOWS,
(uint64_t)g_vad_hangover_ms * 10, busy, now);
if (action == 1) {
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: vad START prob=%.2f grp=%016llx",
RADIO_AUDIO_ID, prob, (unsigned long long)group_id);
radio_audio_post_talk(ua, group_id, radio_talk_begin_trampoline);
} else if (action == -1) {
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: vad STOP grp=%016llx",
RADIO_AUDIO_ID, (unsigned long long)group_id);
radio_audio_post_talk(ua, group_id, radio_talk_end_trampoline);
}
}
}
if (g_vad_fsm.talking) {
radio_audio_encode_post(group_id, pcm, ua, inst);
}
pthread_mutex_unlock(&g_mtx);
return 0;
}
/* ── TX (аудио-поток) ── */
@ -361,6 +551,7 @@ int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) {
pthread_mutex_lock(&g_mtx);
int frame_total = RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels;
if (!pcm || count != frame_total) { pthread_mutex_unlock(&g_mtx); return -1; }
if (g_vad_mode) { pthread_mutex_unlock(&g_mtx); return radio_vad_feed(group_id, pcm, count); }
enum { MAX_OUT_FRAMES = 4 };
uint8_t opus[MAX_OUT_FRAMES][RADIO_MAX_OPUS];
@ -435,7 +626,7 @@ int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) {
return 0;
}
/* ── RX (uasync-поток): кодированный кадр → vj_push ── */
/* ── RX (uasync-поток): кодированный кадр → rj_push ── */
void radio_audio_on_frame(struct UTUN_INSTANCE* inst, uint64_t group_id,
uint64_t src_node_id, uint16_t stream_id, uint16_t seq,
@ -446,6 +637,7 @@ void radio_audio_on_frame(struct UTUN_INSTANCE* inst, uint64_t group_id,
uint64_t now = get_time_tb();
pthread_mutex_lock(&g_mtx);
if (g_active && group_id == g_group_id) {
if (len > 0) g_last_rx_tb = now; /* busy-сигнал для VAD: чужой аудио-кадр */
struct radio_source* s = radio_src_acquire(src_node_id, stream_id);
if (s) {
if (len > 0) radio_src_push_frame(s, seq, opus, len, now);
@ -497,20 +689,20 @@ int radio_audio_pull_pcm(uint64_t group_id, int16_t* out, int max_samples) {
radio_src_flush_pending(s);
}
/* ending: снять pre-roll (если ещё не снят) и дренировать накопленный остаток. */
if (s->ending) vj_end(s->vj);
if (s->ending) rj_end(s->rj);
if (do_stats && nst < RADIO_AUDIO_MAX_SOURCES) {
vj_get_stats(s->vj, &st[nst].depth, &st[nst].tempo, &st[nst].dropped, &st[nst].under);
rj_get_stats(s->rj, &st[nst].depth, &st[nst].tempo, &st[nst].dropped, &st[nst].under);
st[nst].src = s->src_node_id;
st[nst].stream = s->stream_id;
st[nst].reorder = s->reorder_count;
nst++;
}
int16_t tmp[RADIO_AUDIO_MIX_SAMPLES];
int n = vj_pull(s->vj, tmp, max_samples);
int n = rj_pull(s->rj, tmp, max_samples);
for (int k = 0; k < n; k++) g_mix[k] += tmp[k];
if (s->ending && n == 0) {
uint32_t dropped = 0, underruns = 0;
vj_get_stats(s->vj, NULL, NULL, &dropped, &underruns);
rj_get_stats(s->rj, NULL, NULL, &dropped, &underruns);
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: source ended src=%016llx dropped=%u underruns=%u",
RADIO_AUDIO_ID, (unsigned long long)s->src_node_id, dropped, underruns);
radio_src_free(s);

13
src/radio/radio_audio.h

@ -33,10 +33,16 @@ int radio_audio_init(struct UTUN_INSTANCE* inst);
void radio_audio_destroy(struct UTUN_INSTANCE* inst);
/* GUI-поток: начать прослушивание канала (encoder + реестр источников).
* capture_channels=0 — только RX; 1/2 сохраняют запуск RX+TX для desktop.
* capture_channels — каналы захвата (1=моно, 2=стерео): задаёт GUI по фактической
* возможности устройства (стерео недоступно → откат на моно). RX всегда стерео. */
int radio_audio_start(uint64_t group_id, int capture_channels);
/* Независимый TX: start(..., 0) включает только RX, захват готовится по фактическому формату микрофона.
* capture_stop отправляет FIN активной передачи и освобождает TX, сохраняя RX. */
int radio_audio_capture_start(uint64_t group_id, int capture_channels, int vad_enabled);
void radio_audio_capture_stop(void);
/* GUI-поток: прекратить прослушивание (освободить всё). */
void radio_audio_stop(void);
@ -54,6 +60,13 @@ int radio_audio_pull_pcm(uint64_t group_id, int16_t* out, int max_samples);
/* 1 = аудио-движок активен (радио канала включено). */
int radio_audio_active(void);
/* 1 = включён VAD-авто-PTT для текущей группы (движки должны фидить PCM постоянно
* и не гасить захват по PTT; ручной PTT остаётся override). */
int radio_audio_vad_mode(void);
/* Текущее состояние передачи: ручной PTT либо сработавший VAD. */
int radio_audio_transmitting(void);
/* uasync-поток: принятый Opus-кадр источника (декодируем + кладём в ring). */
void radio_audio_on_frame(struct UTUN_INSTANCE* inst, uint64_t group_id,
uint64_t src_node_id, uint16_t stream_id, uint16_t seq,

79
src/call/voice_jitter.cpp → src/radio/radio_jitter.cpp

@ -1,6 +1,6 @@
// voice_jitter.cpp — адаптивный джиттер-буфер голоса с time-stretch (SoundTouch).
// radio_jitter.cpp — адаптивный джиттер-буфер голоса с time-stretch (SoundTouch).
//
// Кодированные кадры (Opus, до VJ_MAX_ENC байт) кладутся из uasync-потока в кольцо
// Кодированные кадры (Opus, до RJ_MAX_ENC байт) кладутся из uasync-потока в кольцо
// и вытаскиваются аудио-потоком: кадр декодируется (decode-коллбэк) и подаётся в
// SoundTouch с плавно меняющимся темпом вокруг целевой глубины: мельче target — темп
// падает к min_tempo (накопление), глубже — растёт к max_tempo (догон live). При
@ -8,7 +8,7 @@
// только то, что готово к воспроизведению.
// Кольцо защищено мьютексом (SPSC); decode и SoundTouch трогает только аудио-поток.
#include "voice_jitter.h"
#include "radio_jitter.h"
#include "soundtouch/SoundTouch.h"
@ -19,8 +19,8 @@
#include <mutex>
#include <vector>
static const int VJ_MAX_ENC = 512; /* макс. размер кодированного кадра (Opus 20мс, стерео) */
static const double kDefaultTargetMs = 60.0; /* целевая глубина буфера (звонок) */
static const int RJ_MAX_ENC = 512; /* макс. размер кодированного кадра (Opus 20мс, стерео) */
static const double kDefaultTargetMs = 60.0; /* целевая глубина буфера по умолчанию */
static const double kDefaultMinTempo = 1.0;
static const double kDefaultMaxTempo = 1.6;
static const double kMaxDepthMs = 1000.0; /* при этой глубине tempo == max_tempo */
@ -30,24 +30,25 @@ static const double kPrimeTimeoutMs = 200.0; /* pre-roll: принудите
static const double kStallGapMs = 80.0; /* gap между pull > это → сталь аудио-девайса */
static const double kResyncDepthMs = 400.0; /* бэклог > это после сталла → хард-ресинк к live */
struct vj {
struct rj {
int sr = 0;
int ch = 0;
int frame_samples = 0;
int frame_ms = 0;
int cap = 0;
vj_decode_fn decode = nullptr;
rj_decode_fn decode = nullptr;
void* decode_arg = nullptr;
std::mutex mtx;
std::vector<uint8_t> ring; /* cap * VJ_MAX_ENC — кодированные кадры */
std::vector<uint8_t> ring; /* cap * RJ_MAX_ENC — кодированные кадры */
std::vector<int> lens; /* cap — длины кадров */
int head = 0;
int count = 0;
uint32_t dropped = 0;
uint32_t underruns = 0;
uint32_t resyncs = 0;
std::atomic<uint32_t> underruns{0};
std::atomic<uint32_t> resyncs{0};
std::atomic<int> processing_ms{0};
/* детект сталла аудио-девайса (gap между pull) */
std::chrono::steady_clock::time_point last_pull_tp; /* только аудио-поток */
@ -55,7 +56,7 @@ struct vj {
int stalled = 0; /* только аудио-поток */
soundtouch::SoundTouch st; /* только аудио-поток */
uint8_t enc_buf[VJ_MAX_ENC]; /* scratch: вытащенный кодированный кадр */
uint8_t enc_buf[RJ_MAX_ENC]; /* scratch: вытащенный кодированный кадр */
std::vector<int16_t> pcm_buf; /* scratch: декодированный кадр */
std::vector<float> fin; /* scratch: кадр в float */
std::vector<float> fout; /* scratch: выход float */
@ -76,7 +77,7 @@ struct vj {
/* Симметричный целевой темп с мёртвой зоной ±25% вокруг target: внутри зоны темп=1.0
* (без time-stretch, меньше артефактов), за её пределами — плавно к min/max. */
static double vj_compute_tempo(double depth_ms, double target_ms, double min_tempo, double max_tempo) {
static double rj_compute_tempo(double depth_ms, double target_ms, double min_tempo, double max_tempo) {
double dead = target_ms * 0.25;
double lo = target_ms - dead;
double hi = target_ms + dead;
@ -93,11 +94,11 @@ static double vj_compute_tempo(double depth_ms, double target_ms, double min_tem
}
/* Вытащить один кодированный кадр из кольца (под мьютексом). Возвращает 0 если пусто. */
static int vj_pop_enc(struct vj* j, uint8_t* out, int* len_out) {
static int rj_pop_enc(struct rj* j, uint8_t* out, int* len_out) {
std::lock_guard<std::mutex> lk(j->mtx);
if (j->count == 0) return 0;
*len_out = j->lens[(size_t)j->head];
std::memcpy(out, &j->ring[(size_t)j->head * VJ_MAX_ENC], (size_t)*len_out);
std::memcpy(out, &j->ring[(size_t)j->head * RJ_MAX_ENC], (size_t)*len_out);
j->head = (j->head + 1) % j->cap;
j->count--;
return 1;
@ -105,14 +106,14 @@ static int vj_pop_enc(struct vj* j, uint8_t* out, int* len_out) {
extern "C" {
struct vj* vj_create_cfg(const struct vj_cfg* cfg) {
struct rj* rj_create_cfg(const struct rj_cfg* cfg) {
if (!cfg || cfg->sample_rate <= 0 || cfg->channels <= 0 || cfg->frame_samples <= 0 ||
cfg->frame_ms <= 0 || cfg->max_frames <= 0 || !cfg->decode)
return nullptr;
if (cfg->min_tempo <= 0.0 || cfg->max_tempo < cfg->min_tempo || cfg->target_ms <= 0.0)
return nullptr;
vj* j = new vj();
rj* j = new rj();
j->sr = cfg->sample_rate;
j->ch = cfg->channels;
j->frame_samples = cfg->frame_samples;
@ -124,7 +125,7 @@ struct vj* vj_create_cfg(const struct vj_cfg* cfg) {
j->min_tempo = cfg->min_tempo;
j->max_tempo = cfg->max_tempo;
j->pre_roll = cfg->pre_roll;
j->ring.assign((size_t)cfg->max_frames * VJ_MAX_ENC, 0);
j->ring.assign((size_t)cfg->max_frames * RJ_MAX_ENC, 0);
j->lens.assign(cfg->max_frames, 0);
j->pcm_buf.assign((size_t)cfg->frame_samples * cfg->channels, 0);
j->fin.assign((size_t)cfg->frame_samples * cfg->channels, 0.0f);
@ -140,9 +141,9 @@ struct vj* vj_create_cfg(const struct vj_cfg* cfg) {
return j;
}
struct vj* vj_create(int sample_rate, int channels, int frame_samples, int frame_ms,
int max_frames, vj_decode_fn decode, void* decode_arg) {
struct vj_cfg cfg;
struct rj* rj_create(int sample_rate, int channels, int frame_samples, int frame_ms,
int max_frames, rj_decode_fn decode, void* decode_arg) {
struct rj_cfg cfg;
cfg.sample_rate = sample_rate;
cfg.channels = channels;
cfg.frame_samples = frame_samples;
@ -154,16 +155,16 @@ struct vj* vj_create(int sample_rate, int channels, int frame_samples, int frame
cfg.pre_roll = 0;
cfg.decode = decode;
cfg.decode_arg = decode_arg;
return vj_create_cfg(&cfg);
return rj_create_cfg(&cfg);
}
void vj_destroy(struct vj* j) {
void rj_destroy(struct rj* j) {
if (!j) return;
delete j;
}
void vj_push(struct vj* j, const uint8_t* enc, int len) {
if (!j || !enc || len <= 0 || len > VJ_MAX_ENC) return;
void rj_push(struct rj* j, const uint8_t* enc, int len) {
if (!j || !enc || len <= 0 || len > RJ_MAX_ENC) return;
std::lock_guard<std::mutex> lk(j->mtx);
if (j->count == j->cap) {
j->head = (j->head + 1) % j->cap; /* дроп старейшего */
@ -171,7 +172,7 @@ void vj_push(struct vj* j, const uint8_t* enc, int len) {
j->dropped++;
}
int idx = (j->head + j->count) % j->cap;
std::memcpy(&j->ring[(size_t)idx * VJ_MAX_ENC], enc, (size_t)len);
std::memcpy(&j->ring[(size_t)idx * RJ_MAX_ENC], enc, (size_t)len);
j->lens[(size_t)idx] = len;
j->count++;
}
@ -179,16 +180,17 @@ void vj_push(struct vj* j, const uint8_t* enc, int len) {
/* Хард-ресинк к live: сбросить бэклог (кольцо + SoundTouch) и темп к 1.0.
* Применяется после сталла аудио-девайса, когда бэклог > kResyncDepthMs — один
* чистый срез вместо долгого догона темпом (chipmunk) + дропов по одному. */
static void vj_hard_resync(struct vj* j) {
static void rj_hard_resync(struct rj* j) {
{ std::lock_guard<std::mutex> lk(j->mtx); j->count = 0; j->head = 0; }
j->st.clear();
j->processing_ms.store(0);
j->tempo = 1.0;
j->tempo_x100.store(100);
j->ema_depth_ms = j->target_ms;
j->resyncs++;
}
int vj_pull(struct vj* j, int16_t* out, int max_samples) {
int rj_pull(struct rj* j, int16_t* out, int max_samples) {
if (!j || !out || max_samples <= 0) return 0;
/* ── детект сталла аудио-девайса (gap между pull) + ресинк по его окончании ── */
@ -208,7 +210,7 @@ int vj_pull(struct vj* j, int16_t* out, int max_samples) {
{ std::lock_guard<std::mutex> lk(j->mtx); c = j->count; }
double depth_ms = (double)c * j->frame_ms
+ (double)j->st.numSamples() * 1000.0 / (double)j->sr;
if (depth_ms > kResyncDepthMs) vj_hard_resync(j);
if (depth_ms > kResyncDepthMs) rj_hard_resync(j);
}
}
}
@ -241,7 +243,7 @@ int vj_pull(struct vj* j, int16_t* out, int max_samples) {
/* 1) глубина (кольцо + внутренний буфер SoundTouch, сглаженная) → целевой темп (плавно) */
double depth_ms = (double)cnt * j->frame_ms + (double)j->st.numSamples() * 1000.0 / (double)j->sr;
j->ema_depth_ms = kEmaAlpha * depth_ms + (1.0 - kEmaAlpha) * j->ema_depth_ms;
double target = vj_compute_tempo(j->ema_depth_ms, j->target_ms, j->min_tempo, j->max_tempo);
double target = rj_compute_tempo(j->ema_depth_ms, j->target_ms, j->min_tempo, j->max_tempo);
double d = target - j->tempo;
if (d > kTempoStep) d = kTempoStep;
else if (d < -kTempoStep) d = -kTempoStep;
@ -257,7 +259,7 @@ int vj_pull(struct vj* j, int16_t* out, int max_samples) {
int guard = 0;
while (j->st.numSamples() < max_slots && guard++ < j->cap + 4) {
int elen = 0;
if (!vj_pop_enc(j, j->enc_buf, &elen)) break;
if (!rj_pop_enc(j, j->enc_buf, &elen)) break;
int n = j->decode(j->decode_arg, j->enc_buf, elen, j->pcm_buf.data(), j->frame_samples);
if (n <= 0) continue; /* битый/пустой кадр — пропускаем */
if (n > j->frame_samples) n = j->frame_samples;
@ -275,37 +277,38 @@ int vj_pull(struct vj* j, int16_t* out, int max_samples) {
else if (v < -32768.0f) v = -32768.0f;
out[i] = (int16_t)std::lrintf(v);
}
j->processing_ms.store((int)std::lround((double)j->st.numSamples() * 1000.0 / j->sr));
if ((int)(n * (uint)ch) < max_samples) j->underruns++;
return (int)(n * (uint)ch);
}
void vj_end(struct vj* j) {
void rj_end(struct rj* j) {
if (!j) return;
j->primed = 1; /* только аудио-поток: снять pre-roll и дренировать остаток */
}
void vj_get_stats(struct vj* j, int* depth_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns) { if (!j) return;
void rj_get_stats(struct rj* j, int* depth_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns) { if (!j) return;
std::lock_guard<std::mutex> lk(j->mtx);
if (depth_ms) {
/* полная глубина = кодированные кадры в кольце + невыданный PCM внутри SoundTouch */
std::lock_guard<std::mutex> lk(j->mtx);
double st_ms = (double)j->st.numSamples() * 1000.0 / (double)j->sr;
double st_ms = j->processing_ms.load();
*depth_ms = (int)((double)j->count * j->frame_ms + st_ms + 0.5);
}
if (tempo_x100) *tempo_x100 = j->tempo_x100.load();
if (dropped) *dropped = j->dropped;
if (underruns) *underruns = j->underruns;
if (underruns) *underruns = j->underruns.load();
}
uint32_t vj_dropped(struct vj* j) {
uint32_t rj_dropped(struct rj* j) {
if (!j) return 0;
std::lock_guard<std::mutex> lk(j->mtx);
return j->dropped;
}
uint32_t vj_resyncs(struct vj* j) {
uint32_t rj_resyncs(struct rj* j) {
if (!j) return 0;
std::lock_guard<std::mutex> lk(j->mtx);
return j->resyncs;
return j->resyncs.load();
}
} // extern "C"

44
src/call/voice_jitter.h → src/radio/radio_jitter.h

@ -1,7 +1,7 @@
#ifndef VOICE_JITTER_H
#define VOICE_JITTER_H
#ifndef RADIO_JITTER_H
#define RADIO_JITTER_H
// voice_jitter.h — адаптивный джиттер-буфер голоса с time-stretch (SoundTouch), C API.
// radio_jitter.h — адаптивный джиттер-буфер голоса с time-stretch (SoundTouch), C API.
//
// Хранит КОДИРОВАННЫЕ (Opus) кадры: uasync-поток кладёт кодированные кадры,
// аудио-поток на pull декодирует их (через коллбэк decode) и пропускает через
@ -10,9 +10,9 @@
// разгрузка uasync-потока).
//
// Потоки:
// - vj_push — uasync-поток (producer);
// - vj_pull — аудио-поток (consumer; единственный владелец decode/SoundTouch);
// - vj_get_stats — любой поток.
// - rj_push — uasync-поток (producer);
// - rj_pull — аудио-поток (consumer; единственный владелец decode/SoundTouch);
// - rj_get_stats — любой поток.
// Кольцо кодированных кадров защищено мьютексом (SPSC, дёшево: ~50 кадров/с).
#include <stdint.h>
@ -21,13 +21,13 @@
extern "C" {
#endif
struct vj;
struct rj;
/* decode: закодированный кадр → PCM. Возвращает число декодированных сэмплов. */
typedef int (*vj_decode_fn)(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples);
typedef int (*rj_decode_fn)(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples);
/* Полная конфигурация адаптивного джиттер-буфера. */
struct vj_cfg {
struct rj_cfg {
int sample_rate;
int channels;
int frame_samples; /* сэмплов в кадре (960 @20мс/48k) */
@ -37,43 +37,43 @@ struct vj_cfg {
double min_tempo; /* темп при пустом буфере (<1.0 = замедление, накопление) */
double max_tempo; /* темп при глубоком буфере (>1.0 = догон live) */
int pre_roll; /* 1 = не отдавать звук, пока глубина < target_ms */
vj_decode_fn decode;
rj_decode_fn decode;
void* decode_arg;
};
/* Создать буфер с полной конфигурацией (pre-roll, симметричный темп). */
struct vj* vj_create_cfg(const struct vj_cfg* cfg);
struct rj* rj_create_cfg(const struct rj_cfg* cfg);
/* Создать буфер с конфигурацией по умолчанию (совместимо со звонком: без pre-roll,
/* Создать буфер с конфигурацией по умолчанию (без pre-roll,
* темп 1.0..1.6, target 60мс). frame_samples — сэмплов в кадре (960 @20мс/48k). */
struct vj* vj_create(int sample_rate, int channels, int frame_samples, int frame_ms,
int max_frames, vj_decode_fn decode, void* decode_arg);
struct rj* rj_create(int sample_rate, int channels, int frame_samples, int frame_ms,
int max_frames, rj_decode_fn decode, void* decode_arg);
void vj_destroy(struct vj* j);
void rj_destroy(struct rj* j);
/* producer: положить кодированный кадр (копируется). При переполнении дропает старейший. */
void vj_push(struct vj* j, const uint8_t* enc, int len);
void rj_push(struct rj* j, const uint8_t* enc, int len);
/* consumer: выдать до max_samples декодированного+растянутого PCM. Может вернуть
* < max_samples при нехватке данных (андерфлоу). */
int vj_pull(struct vj* j, int16_t* out, int max_samples);
int rj_pull(struct rj* j, int16_t* out, int max_samples);
/* audio-поток: вход закончен (FIN) — принудительно снять pre-roll, чтобы дренировать
* накопленный остаток до конца. Безопасно вызывать многократно. */
void vj_end(struct vj* j);
void rj_end(struct rj* j);
/* Диагностика: глубина буфера (мс), темп (×100, 100 = 1.0x), дропнуто кадров
* при переполнении, число underrun-ов (pull вернул меньше запрошенного). */
void vj_get_stats(struct vj* j, int* depth_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns);
void rj_get_stats(struct rj* j, int* depth_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns);
/* Число дропнутых кадров при переполнении (для диагностики/тестов). */
uint32_t vj_dropped(struct vj* j);
uint32_t rj_dropped(struct rj* j);
/* Число хард-ресинков к live (после сталла аудио-девайса) — диагностика. */
uint32_t vj_resyncs(struct vj* j);
uint32_t rj_resyncs(struct rj* j);
#ifdef __cplusplus
}
#endif
#endif /* VOICE_JITTER_H */
#endif /* RADIO_JITTER_H */

95
src/radio/radio_vad.h

@ -0,0 +1,95 @@
// radio_vad.h — авто-PTT рации по VAD: чистая логика без I/O (юнит-тестируется).
//
// Содержит:
// - ресемплер 48кГц→16кГц моно (3-tap box + децимация 3:1, стерео микшируется);
// - фильтр-гистерезис вероятности речи и машину состояний «передаю/молчу».
//
// Всё — static inline, без состояния (кроме struct radio_vad_fsm, который владеет
// вызывающий). Используется сервисным слоем radio_audio.c и тестом test_radio_vad.
//
// Время — в timebase-единицах (0.1мс), как get_time_tb() в ядре.
#ifndef RADIO_VAD_H
#define RADIO_VAD_H
#include <stdint.h>
/* Выходов ресемплера на один 20мс кадр 48кГц: 960 сэмплов / 3 = 320 @ 16кГц. */
#define RADIO_VAD_FRAME_SAMPLES_16K 320
/* Число 16кГц-сэмплов в окне Silero: 512 = 32мс. */
#define RADIO_VAD_WINDOW_16K 512
/* Ресемпл 20мс кадра 48кГц (frame_total int16, channels=1|2) → 320 float @16кГц
* моно в диапазоне [-1;1]. Стерео миксируется (среднее L/R), затем децимация 3:1. */
static inline void radio_vad_resample(const int16_t* pcm, int channels, float* out16k) {
for (int j = 0; j < RADIO_VAD_FRAME_SAMPLES_16K; j++) {
float s = 0.0f;
for (int t = 0; t < 3; t++) {
int idx = j * 3 + t;
float v = (channels == 2)
? ((float)pcm[2 * idx] + (float)pcm[2 * idx + 1]) * 0.5f
: (float)pcm[idx];
s += v;
}
out16k[j] = (s / 3.0f) * (1.0f / 32768.0f);
}
}
/* ── машина состояний авто-PTT ── */
struct radio_vad_fsm {
int talking; /* идёт VAD-передача */
int confirm_count; /* подряд окон с prob >= threshold */
uint64_t silence_since_tb; /* начало текущей тишины (0 — не в тишине) */
};
static inline void radio_vad_fsm_reset(struct radio_vad_fsm* f) {
f->talking = 0;
f->confirm_count = 0;
f->silence_since_tb = 0;
}
/* Обработать очередную вероятность речи (окно 32мс).
* prob — вероятность [0..1];
* threshold — порог срабатывания [0..1];
* confirm_windows — сколько окон подряд выше порога нужно для старта;
* hangover_tb — сколько тишины (tb) держать передачу перед стопом;
* busy — 1 если сейчас говорит кто-то другой (канал занят);
* now — текущее время (tb).
* Возврат: 1 = начать передачу, -1 = закончить передачу, 0 = без изменений. */
static inline int radio_vad_fsm_update(struct radio_vad_fsm* f, float prob, float threshold,
int confirm_windows, uint64_t hangover_tb,
int busy, uint64_t now) {
if (busy) {
/* чужой разговор: старт запрещён, свою передачу гасим сразу */
f->confirm_count = 0;
f->silence_since_tb = 0;
if (f->talking) { f->talking = 0; return -1; }
return 0;
}
if (prob >= threshold) {
f->silence_since_tb = 0;
if (!f->talking) {
f->confirm_count++;
if (f->confirm_count >= confirm_windows) {
f->confirm_count = 0;
f->talking = 1;
return 1; /* старт: подтверждение набрано */
}
}
return 0;
}
/* prob < threshold */
f->confirm_count = 0;
if (f->talking) {
if (f->silence_since_tb == 0) f->silence_since_tb = now;
else if (now - f->silence_since_tb >= hangover_tb) {
f->talking = 0;
f->silence_since_tb = 0;
return -1; /* стоп: hangover истёк */
}
}
return 0;
}
#endif /* RADIO_VAD_H */

24
src/routing_layer/conn_mgr.h

@ -1,8 +1,17 @@
/* conn_mgr — соединение с узлом группы напрямую, в обратную сторону или через посредника.
* Один менеджер принадлежит одной группе; open выдаёт отдельный handle владельцу.
* DIRECT использует NCD, REVERSE просит пира подключиться, INDIRECT выбирает посредника.
* Используется сервисами для доставки данных. Групповые сессии открывают через
* topo_group_peer_open(). Все операции — в потоке uasync; handle закрывает вызывающий. */
/* conn_mgr — оптимальная P2P-доставка больших объёмов данных: медиа, файлы, звонки.
* Один менеджер принадлежит одной группе; DIRECT/REVERSE/INDIRECT подбирают путь к получателю.
*
* Архитектурная роль CONN_MGR_HANDLE:
* - принадлежит сервису и выражает его потребность в доставке данных конкретному узлу;
* - существует, пока идёт звонок или передача файла; владелец закрывает его по окончании/отмене;
* - потеря пути и TIMEOUT попытки не освобождают handle и не отменяют потребность владельца;
* - не является NCD handle и не привязан временем жизни к одному ETCP_CONN.
*
* NCD обеспечивает общий прямой мультиадресный транспорт для BGP/topo_group.
* NCD handle попытки CM после установления связи должен перейти группе либо быть закрыт;
* CM handle остаётся у медиа-сервиса. Закрытие CM handle отменяет его запрос, а не владение группы.
* Это контракт разделения ролей; механизм передачи транспорта группе относится к NCD/topo_group.
* Групповые сессии открывают через topo_group_peer_open(). Все операции — в потоке uasync. */
#ifndef CONN_MGR_H
#define CONN_MGR_H
@ -60,7 +69,10 @@ int conn_mgr_open(struct UTUN_INSTANCE* inst,
conn_mgr_cb_t cb, void* cb_arg,
struct CONN_MGR_HANDLE** out_handle);
/** Закрыть свой handle. Последний освобождает ресурсы CM; чужие NCD-владельцы сохраняют транспорт. */
/** Закрыть свой handle и отменить его callbacks, в том числе отложенный UP.
* Допустимо из callback (также закрытие другого handle). После close указатель больше не использовать.
* Очистка внутренней записи CM отсоединяет handles, но владельцы всё равно обязаны вызвать close.
* Последний handle освобождает ресурсы CM; чужие NCD-владельцы сохраняют транспорт. */
void conn_mgr_close(struct CONN_MGR_HANDLE* h);
/* ═══════ доступ к соединению ═══════ */

78
src/routing_layer/conn_mgr_core.c

@ -40,6 +40,7 @@ static struct CONN_MGR_HANDLE* cm_handle_new(struct CONN_MGR_ENTRY* entry, uint6
struct CONN_MGR_HANDLE* h = u_calloc(1, sizeof(*h));
if (!h) return NULL;
h->node_id = entry->node_id; h->group_id = group_id; h->cb = cb; h->cb_arg = cb_arg;
h->ua = entry->mgr->instance->ua;
h->entry = entry; h->next = entry->handles; entry->handles = h;
return h;
}
@ -47,10 +48,12 @@ static struct CONN_MGR_HANDLE* cm_handle_new(struct CONN_MGR_ENTRY* entry, uint6
static void cm_deliver_up_cb(void* arg) {
struct CONN_MGR_HANDLE* h = (struct CONN_MGR_HANDLE*)arg;
h->deliver_up_token = NULL;
if (h->cb) h->cb(h, h->node_id, h->group_id, CONN_EVENT_UP, h->cb_arg);
if (!h->closed && h->entry && h->entry->state == CONN_MGR_STATE_CONNECTED && h->cb)
h->cb(h, h->node_id, h->group_id, CONN_EVENT_UP, h->cb_arg);
}
/* Рассылает событие ВСЕМ handle'ам entry. Одно соединение — много слушателей:
/* Закрытые handles освобождаются через call_soon, поэтому next переживает callbacks.
* Рассылает событие ВСЕМ handle'ам entry. Одно соединение — много слушателей:
* каждый conn_mgr_open создал свой handle, все получают UP/DOWN/TIMEOUT.
* Все поля handle'а читаются до вызова коллбэка — коллбэк может закрыть
* handle (conn_mgr_close), после вызова к handle не обращаемся. */
@ -66,7 +69,10 @@ void cm_deliver_event(struct CONN_MGR_ENTRY* entry, enum conn_mgr_event event) {
uint64_t node_id = h->node_id;
conn_mgr_cb_t cb = h->cb;
void* cb_arg = h->cb_arg;
if (cb) cb(h, node_id, gid, event, cb_arg);
if (event != CONN_EVENT_UP && h->deliver_up_token) {
uasync_call_soon_cancel(h->ua, h->deliver_up_token); h->deliver_up_token = NULL;
}
if (!h->closed && h->entry == entry && cb) cb(h, node_id, gid, event, cb_arg);
h = next;
}
}
@ -180,8 +186,25 @@ struct CONN_MGR_ENTRY* cm_ensure_entry(struct CONN_MGR* mgr, uint64_t node_id) {
return new_e;
}
static void cm_entry_free_cb(void* arg) { queue_entry_free(arg); }
static void cm_handle_free_cb(void* arg) { u_free(arg); }
void cm_entry_cleanup(struct CONN_MGR_ENTRY* entry) {
if (!entry || entry->state == CONN_MGR_STATE_DISCONNECTED) return;
if (!entry) return;
// Сначала исключаем из поиска и запрещаем NCD callbacks; освобождение памяти — вне текущего стека.
struct CONN_MGR* mgr = entry->mgr;
if (cm_find_entry(mgr, entry->node_id) != entry) return;
entry->state = CONN_MGR_STATE_DISCONNECTED;
queue_remove_data(mgr->entries, &entry->ll);
for (struct CONN_MGR_HANDLE* h = entry->handles; h; h = h->next) {
if (h->deliver_up_token) {
uasync_call_soon_cancel(h->ua, h->deliver_up_token); h->deliver_up_token = NULL;
}
h->entry = NULL; // handle остаётся у владельца до close
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: detached handle=%p node=%016llx", (void*)h,
(unsigned long long)h->node_id);
}
entry->handles = NULL;
if (entry->main.timer) { uasync_cancel_timeout(entry->mgr->instance->ua, entry->main.timer); entry->main.timer = NULL; }
if (entry->reverse_timer) { uasync_cancel_timeout(entry->mgr->instance->ua, entry->reverse_timer); entry->reverse_timer = NULL; }
entry->reverse_active = 0; entry->indirect_active = 0; entry->indirect_ready = 0;
@ -200,13 +223,11 @@ void cm_entry_cleanup(struct CONN_MGR_ENTRY* entry) {
} else pp = &(*pp)->next;
}
}
if (entry->ncd_handle) { node_conn_direct_force_close(entry->ncd_handle); entry->ncd_handle = NULL; }
entry->handles = NULL;
struct NODE_CONN_DIRECT* ncd = entry->ncd_handle; entry->ncd_handle = NULL;
if (ncd) node_conn_direct_force_close(ncd);
cm_clear_nodeinfo(entry->mgr, entry->node_id);
entry->state = CONN_MGR_STATE_DISCONNECTED; entry->conn_type = CONN_TYPE_NONE;
{ struct CONN_MGR* m = entry->mgr;
queue_remove_data(m->entries, &entry->ll);
queue_entry_free(&entry->ll); }
uasync_call_soon(mgr->instance->ua, entry, cm_entry_free_cb);
}
void cm_clear_nodeinfo(struct CONN_MGR* mgr, uint64_t node_id) {
@ -458,33 +479,24 @@ int cm_open(struct CONN_MGR* mgr, uint64_t node_id,
}
void conn_mgr_close(struct CONN_MGR_HANDLE* h) {
if (!h) return;
struct CONN_MGR_ENTRY* entry = h->entry;
if (!entry) { u_free(h); return; }
/* отменяем отложенный UP-коллбэк (cm_deliver_up_cb), иначе он дёрнется
* с освобождённым handle (use-after-free при быстром teardown соединения) */
if (h->deliver_up_token && entry->mgr && entry->mgr->instance) {
uasync_call_soon_cancel(entry->mgr->instance->ua, h->deliver_up_token);
h->deliver_up_token = NULL;
}
{ struct CONN_MGR_HANDLE** pp = &entry->handles;
while (*pp) { if (*pp == h) { *pp = h->next; break; } pp = &(*pp)->next; }
}
if (entry->handles) { u_free(h); return; }
if (entry->state == CONN_MGR_STATE_CONNECTED && (entry->conn_type == CONN_TYPE_DIRECT || entry->conn_type == CONN_TYPE_REVERSE)) {
/* DISCONNECT через etcp_router — conn_mgr уровень */
struct CM_DISCONNECT pkt; memset(&pkt, 0, sizeof(pkt));
pkt.cmd = ETCP_RT_ID_CONN_MGR; pkt.subcmd = CM_SUBCMD_DISCONNECT; pkt.node_id = entry->node_id;
struct ll_entry* qe = queue_entry_new(0);
if (qe) { qe->dgram = u_malloc(1+sizeof(pkt)); qe->dgram[0] = ETCP_RT_ID_CONN_MGR; memcpy(qe->dgram+1, &pkt, sizeof(pkt)); qe->len = (uint16_t)(1+sizeof(pkt));
etcp_route_send(entry->mgr->instance, entry->mgr->group->group_id, entry->node_id, qe, 1, 0); }
/* Транспортный уровень рвётся ниже в cm_entry_cleanup (node_conn_direct_force_close) */
}
cm_entry_cleanup(entry); u_free(h);
if (!h || h->closed) return;
h->closed = 1;
if (h->deliver_up_token) { uasync_call_soon_cancel(h->ua, h->deliver_up_token); h->deliver_up_token = NULL; }
struct CONN_MGR_ENTRY* entry = h->entry; h->entry = NULL;
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "conn_mgr: close handle=%p node=%016llx attached=%d", (void*)h,
(unsigned long long)h->node_id, entry != NULL);
if (entry) {
struct CONN_MGR_HANDLE** pp = &entry->handles;
while (*pp && *pp != h) pp = &(*pp)->next;
if (*pp) *pp = h->next;
if (!entry->handles) cm_entry_cleanup(entry);
}
// Не посылаем DISCONNECT: удалённые владельцы и групповой транспорт живут независимо.
uasync_call_soon(h->ua, h, cm_handle_free_cb);
}
struct ETCP_CONN* conn_mgr_get_conn(struct CONN_MGR_HANDLE* h) {
if (!h || !h->entry || !h->entry->ncd_handle) return NULL;
if (!h || h->closed || !h->entry || !h->entry->ncd_handle) return NULL;
return node_conn_direct_get_conn(h->entry->ncd_handle);
}

2
src/routing_layer/conn_mgr_priv.h

@ -95,6 +95,8 @@ struct cm_exchange_pending {
struct CONN_MGR_HANDLE {
uint64_t node_id; uint64_t group_id; conn_mgr_cb_t cb; void* cb_arg;
struct CONN_MGR_ENTRY* entry; struct CONN_MGR_HANDLE* next;
struct UASYNC* ua;
uint8_t closed; /* close отменяет callbacks; память освобождается вне текущего стека */
void* deliver_up_token; /* токен отложенного cm_deliver_up_cb (uasync_call_soon), NULL если не постился/уже сработал */
};

4
src/routing_layer/etcp_router.c

@ -756,7 +756,9 @@ static void router_retrans_timer_cb(void* arg) {
if (elapsed >= ROUTER_RETRANS_TIMEOUT_TB) {
rconn->no_ack_count++;
if (rconn->no_ack_count >= ROUTER_NO_ACK_MAX_RETRANS) {
// Звонок сохраняет надёжную сессию во время перебоя; остальные сервисы используют прежний срок.
unsigned max_retrans = rconn->svc_id == ETCP_RT_ID_CALL ? ROUTER_CALL_NO_ACK_MAX_RETRANS : ROUTER_NO_ACK_MAX_RETRANS;
if (rconn->no_ack_count >= max_retrans) {
DEBUG_WARN(DEBUG_CATEGORY_ETCPROUTE, "router: no ACK progress for %d retrans cycles, closing svc_id=%u remote=%016llx",
rconn->no_ack_count, rconn->svc_id, (unsigned long long)rconn->remote_node_id);
router_close_and_notify(rconn);

1
src/routing_layer/etcp_router.h

@ -189,6 +189,7 @@ struct ETCP_ROUTER_CONN {
#define ROUTER_RETRANS_TIMEOUT_TB 3000 // 300ms в timebase (0.1ms)
#define ROUTER_INFLIGHT_HASH_SIZE 1024
#define ROUTER_NO_ROUTE_RETRY_TB 200 // 20ms проверка появления маршрута
#define ROUTER_CALL_NO_ACK_MAX_RETRANS 67 // 20с: запас для 10с перебоя звонка и восстановления
#define ROUTER_NO_ACK_MAX_RETRANS 17 // 17 × 300ms ≈ 5s таймаут без ACK
// minRTT measurement

20
src/transport_layer/etcp_connections.c

@ -1256,11 +1256,7 @@ void etcp_link_enter_ready_tcp(struct ETCP_LINK *link) {
if (!etcp->peer_reset_id) etcp_session_start(etcp); // сервер создаёт ETCP_CONN после STCP handshake
if (etcp->initialized == 0) etcp_conn_ready(etcp);
if (etcp->state == 2) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "enter_ready_tcp: conn DELETED after conn_ready, link=%p", (void*)link); return; }
/* в standby анонс спячки вместо обычного flagless-keepalive */
if (!etcp_keepalive_announce_mode(link)) {
etcp_link_send_keepalive(link);
start_keepalive_timer(link);
}
etcp_keepalive_link_ready(link);
loadbalancer_link_ready(link);
etcp_fire_link_status_cbk(link, old_state, link->link_status);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] TCP link %d UP (mtu=%d init=%d up=%d tcp_links=%d)",
@ -1955,7 +1951,7 @@ static void send_init_response(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pk
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] Connection established (socket=%s, link=%d, addr=%s)", link->etcp->log_name, e_sock->name, link->local_link_id, sockaddr_storage_to_str(&link->remote_addr).str);
}
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] Link %d UP (server, mtu=%d, addr=%s)", link->etcp->log_name, link->local_link_id, link->mtu_local, sockaddr_storage_to_str(&link->remote_addr).str);
start_keepalive_timer(link);
etcp_keepalive_link_ready(link);
loadbalancer_link_ready(link);
// Restart NAT check after link is up (e.g. after address change or reinit)
if (link->etcp->instance->nat_det) nat_detection_link_ready(link->etcp->instance->nat_det, link);
@ -2061,11 +2057,7 @@ static int handle_init_response_client(struct ETCP_SOCKET* e_sock, struct ETCP_D
loadbalancer_link_ready(link);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] Link %d UP (client, mtu=%d, addr=%s)", link->etcp->log_name, link->local_link_id, link->mtu, sockaddr_storage_to_str(&link->remote_addr).str);
/* в standby анонс спячки вместо обычного flagless-keepalive */
if (!etcp_keepalive_announce_mode(link)) {
etcp_link_send_keepalive(link);
start_keepalive_timer(link);
}
etcp_keepalive_link_ready(link);
loadbalancer_link_ready(link);
@ -2601,11 +2593,7 @@ int etcp_packet_decrypted(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt,
return 0;
}
if (link->link_state == LINK_STATE_TRY_RECONNECT) {// 0 - just init, 1 - handshake, 2 - try reconnect, 3 - connected
/* в standby анонс спячки вместо обычного flagless-keepalive */
if (!etcp_keepalive_announce_mode(link)) {
start_keepalive_timer(link);
etcp_link_send_keepalive(link);
}
etcp_keepalive_link_ready(link);
int old_state = link->link_state; link->link_state = LINK_STATE_CONNECTED;
etcp_fire_link_status_cbk(link, old_state, link->link_status);
}

54
src/transport_layer/etcp_keepalive.c

@ -386,13 +386,14 @@ static void ka_wake(struct ETCP_LINK* link) {
typedef void (*ka_link_fn)(struct ETCP_LINK* link);
static void ka_for_each_link(struct UTUN_INSTANCE* inst, ka_link_fn fn) {
static int ka_for_each_link(struct UTUN_INSTANCE* inst, ka_link_fn fn) {
int count = 0;
if (inst->connections) {
for (struct ll_entry* e = inst->connections->head; e; e = e->next) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
if (!ce || !ce->conn) continue;
for (struct ETCP_LINK* l = ce->conn->links; l; l = l->next)
if (l->initialized) fn(l);
if (l->initialized) { fn(l); count++; }
}
}
if (inst->tcp_connections) {
@ -400,12 +401,22 @@ static void ka_for_each_link(struct UTUN_INSTANCE* inst, ka_link_fn fn) {
struct tcp_conn_entry* te = (struct tcp_conn_entry*)e->data;
if (!te || !te->etcp_conn) continue;
for (struct ETCP_LINK* l = te->etcp_conn->links; l; l = l->next)
if (l->initialized) fn(l);
if (l->initialized) { fn(l); count++; }
}
}
return count;
}
#ifdef UTUN_HAVE_STANDBY
// Фактический режим standby меняется после подготовки его фазы и таймера.
static void ka_standby_switch(int enabled, void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
if (!inst || inst->client_type != CLIENT_TYPE_MOBILE) return;
int links = ka_for_each_link(inst, enabled ? ka_enter_sleep : ka_wake);
DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "standby mode=%s applied to %d links node=%016llx",
enabled ? "sleep" : "normal", links, (unsigned long long)inst->node_id);
}
// Вход в ACTIVE-окно — анонс-серия с ретраями до ACK (active_remaining>0).
static void ka_active_announce(struct ETCP_LINK* link) {
if (!link->ka_my_sleeping) return;
@ -540,6 +551,7 @@ int etcp_keepalive_on_recv(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt,
return 0;
}
#ifndef UTUN_HAVE_STANDBY
static void etcp_keepalive_on_activity(struct UTUN_INSTANCE* inst, int active, void* arg) {
(void)arg;
if (!inst || inst->client_type != CLIENT_TYPE_MOBILE) return;
@ -547,23 +559,37 @@ static void etcp_keepalive_on_activity(struct UTUN_INSTANCE* inst, int active, v
DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "keepalive activity change: %s -> applied to links (node=%016llx)",
active ? "ACTIVE(wake)" : "STANDBY(sleep)", (unsigned long long)inst->node_id);
}
#endif
// На только что (пере)инициализированном линке: если я в standby — анонсировать спячку
// (возвращает 1, обычный flagless в этом случае слать НЕ надо); иначе 0.
int etcp_keepalive_announce_mode(struct ETCP_LINK* link) {
if (!link || !link->etcp || !link->etcp->instance) return 0;
struct UTUN_INSTANCE* inst = link->etcp->instance;
if (inst->client_type != CLIENT_TYPE_MOBILE) return 0;
if (!inst->standby_enabled) return 0; /* не в standby — обычный режим */
ka_enter_sleep(link);
return 1;
// Сразу сообщает текущий режим/фазу нового или восстановленного линка и запускает нужный механизм keepalive.
void etcp_keepalive_link_ready(struct ETCP_LINK* link) {
if (!link || !link->etcp || !link->etcp->instance) {
DEBUG_ERROR(DEBUG_CATEGORY_KEEPALIVE, "link_ready: missing link/connection/instance link=%p", (void*)link);
return;
}
int sleeping = 0;
#ifdef UTUN_HAVE_STANDBY
sleeping = link->etcp->instance->client_type == CLIENT_TYPE_MOBILE && standby_is_enabled();
#endif
DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "[%s] link ready id=%u tcp=%u server=%u mode=%s",
link->etcp->log_name, link->local_link_id, link->is_tcp, link->is_server, sleeping ? "standby" : "normal");
if (sleeping) {
ka_enter_sleep(link);
} else if (link->ka_my_sleeping) {
ka_wake(link);
} else {
etcp_link_send_keepalive(link);
if (!link->ka_peer_sleeping) start_keepalive_timer(link);
}
}
void etcp_keepalive_register(struct UTUN_INSTANCE* inst) {
if (!inst) return;
utun_add_activity_cbk(inst, etcp_keepalive_on_activity, NULL);
#ifdef UTUN_HAVE_STANDBY
standby_set_switch_callback(ka_standby_switch, inst);
standby_set_phase_callback(ka_phase_cb, inst);
#else
utun_add_activity_cbk(inst, etcp_keepalive_on_activity, NULL);
#endif
DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "keepalive activity + phase callbacks registered");
DEBUG_DEBUG(DEBUG_CATEGORY_KEEPALIVE, "keepalive mode/phase callbacks registered node=%016llx", (unsigned long long)inst->node_id);
}

9
src/transport_layer/etcp_keepalive.h

@ -64,14 +64,13 @@ void start_keepalive_timer(struct ETCP_LINK* link);
int etcp_keepalive_on_recv(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt,
struct ETCP_LINK* link, size_t pkt_len);
// Подписка на смену active mode (foreground/background) и фазы спячки duty-cycle.
// Подписка на фактический режим/фазу standby; без duty-cycle — на активность клиента.
// Вызывается из init_connections.
void etcp_keepalive_register(struct UTUN_INSTANCE* inst);
// На только что (пере)инициализированном линке: если я в standby — анонсировать
// спячку (возвращает 1, обычный flagless в этом случае слать НЕ надо); иначе 0.
// Вызывается из etcp_link_enter_ready_tcp / handle_init_response_client / TRY_RECONNECT.
int etcp_keepalive_announce_mode(struct ETCP_LINK* link);
// После INIT/INIT_RESPONSE, TCP handshake и reconnect сразу анонсирует текущий режим/фазу.
// В standby запускает sleep-анонс с ретраями UDP; в normal — обычный keepalive.
void etcp_keepalive_link_ready(struct ETCP_LINK* link);
#ifdef __cplusplus
}

14
src/transport_layer/node_conn_direct.h

@ -1,5 +1,15 @@
/*
* node_conn_direct — общий прямой транспорт к узлу по node_id.
* node_conn_direct — общий прямой мультиадресный транспорт к узлу по node_id для BGP/topo_group.
* Адреса узла используются для создания и обновления линков общего ETCP-соединения.
*
* Архитектурная роль NODE_CONN_DIRECT (NCD handle):
* - выражает владение прямым транспортом, в том числе во время установления связи;
* - группа удерживает его для своей BGP/topo_group-сессии;
* - временный handle попытки подключения после установления связи должен перейти группе
* либо быть закрыт; передачу владения выполняют через node_conn_direct_transfer();
* - не выражает длительность звонка или передачи файла: эту потребность хранит CONN_MGR_HANDLE.
* CM подбирает оптимальный P2P-путь для больших объёмов данных (медиа, файлы, звонки),
* а NCD управляет прямым транспортом и его адресами независимо от конкретной медиа-операции.
*
* Модуль управляет ETCP-соединениями через непрозрачные handle'ы (NODE_CONN_DIRECT).
* Каждый владелец открывает свой handle; несколько handle разделяют ETCP_CONN.
@ -7,7 +17,7 @@
* сразу; для UP начинает CLOSE/KEEP_ALIVE (до 5 s), пир может сохранить соединение.
*
* Кто использует:
* - conn_mgr (Connection Manager) — создаёт handle'ы для DIR/REV/IND-соединений
* - conn_mgr (Connection Manager) — временные handle'ы попыток установления прямого транспорта
* - topo_group — владеет транспортом групповой сессии с начала CONNECTING
* - протокол join — временный транспорт до добавления участника
* - любой модуль, кому нужно надёжное ETCP-соединение с конкретным node_id

57
src/utun_instance.c

@ -31,6 +31,9 @@
#include "transport_layer/node_conn_direct.h"
#include "transport_layer/socket_monitor.h"
#include "transport_layer/auto_socket.h"
#ifdef UTUN_HAVE_STANDBY
#include "standby.h"
#endif
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
@ -113,7 +116,6 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u
instance->client_type = config->global.client_type;
instance->keepalive_interval = (uint16_t)(config->global.keepalive_interval > 0 ? config->global.keepalive_interval : 200);
instance->client_activity = (instance->client_type == CLIENT_TYPE_MOBILE) ? CLIENT_ACTIVITY_STANDBY : CLIENT_ACTIVITY_ACTIVE;
instance->client_activity_timer = NULL;
// Set my keys
if (sc_init_local_keys(&instance->my_keys, config->global.my_public_key_hex, config->global.my_private_key_hex) != SC_OK) {
@ -397,10 +399,6 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) {
if (instance->ua) uasync_print_resources(instance->ua, "INSTANCE_DESTROY_BEFORE");
instance->running = 0;
if (instance->client_activity_timer) {
uasync_cancel_timeout(instance->ua, instance->client_activity_timer);
instance->client_activity_timer = NULL;
}
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] A done — diagnose complete");
/* Phase B: NTP */
@ -1133,32 +1131,27 @@ void utun_fire_peer_sleep_cbk(struct UTUN_INSTANCE* instance, uint64_t peer_node
}
}
static void client_activity_timeout_cb(void* arg) {
struct UTUN_INSTANCE* instance = (struct UTUN_INSTANCE*)arg;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "client_activity_timer fired: node=%016llx type=%u -> standby",
(unsigned long long)instance->node_id, instance->client_type);
instance->client_activity_timer = NULL;
if (instance->client_type == CLIENT_TYPE_SERVER) return;
instance->client_activity = CLIENT_ACTIVITY_STANDBY;
utun_fire_activity_cbk(instance, CLIENT_ACTIVITY_STANDBY);
}
void utun_set_client_activity(struct UTUN_INSTANCE* instance, int active) {
if (!instance) return;
if (instance->client_type == CLIENT_TYPE_SERVER) return;
if (instance->client_activity_timer) {
uasync_cancel_timeout(instance->ua, instance->client_activity_timer);
instance->client_activity_timer = NULL;
}
if (active) {
instance->client_activity = CLIENT_ACTIVITY_ACTIVE;
instance->client_activity_timer = uasync_set_timeout(instance->ua, 600000, instance, client_activity_timeout_cb, "activity_timeout");
DEBUG_INFO(DEBUG_CATEGORY_BGP, "client_activity set ACTIVE, timer restarted 60s node=%016llx type=%u",
(unsigned long long)instance->node_id, instance->client_type);
} else {
instance->client_activity = CLIENT_ACTIVITY_STANDBY;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "client_activity set STANDBY node=%016llx type=%u",
(unsigned long long)instance->node_id, instance->client_type);
/* Явная активность UI/звонка не истекает по таймеру: владелец сообщает завершение сам. */
int utun_set_client_activity(struct UTUN_INSTANCE* instance, int active) {
if (!instance) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "client_activity: missing instance active=%d", active);
return -1;
}
if (instance->client_type == CLIENT_TYPE_SERVER) return 0;
int state = active ? CLIENT_ACTIVITY_ACTIVE : CLIENT_ACTIVITY_STANDBY;
int previous = instance->client_activity;
instance->client_activity = state;
#ifdef UTUN_HAVE_STANDBY
if (instance->client_type == CLIENT_TYPE_MOBILE && standby_set_enabled(instance, !active) < 0) {
instance->client_activity = previous;
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "client_activity: standby transition failed node=%016llx requested=%d previous=%d",
(unsigned long long)instance->node_id, state, previous);
return -1;
}
utun_fire_activity_cbk(instance, active);
#endif
if (previous == state) return 0;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "client_activity set %s until explicit change node=%016llx type=%u",
active ? "ACTIVE" : "STANDBY", (unsigned long long)instance->node_id, instance->client_type);
utun_fire_activity_cbk(instance, state);
return 0;
}

6
src/utun_instance.h

@ -241,10 +241,8 @@ struct UTUN_INSTANCE {
uint8_t client_type; // CLIENT_TYPE_SERVER/DESKTOP/MOBILE (from config)
uint16_t keepalive_interval; // желаемый keepalive (ms), из конфига. для handshake
uint8_t client_activity; // CLIENT_ACTIVITY_STANDBY/ACTIVE
void* client_activity_timer; // uasync timer handle for inactivity timeout
struct utun_activity_cbk_entry* activity_cbks; // подписки на смену client_activity
struct peer_sleep_cbk_entry* peer_sleep_cbks; // подписки на смену спячки пира (keepalive)
uint8_t standby_enabled; // 1 = standby duty-cycle активен (chatgui-android)
// TCP proxy server (exit node)
struct tcp_proxy_server tcp_proxy_server;
@ -295,7 +293,9 @@ void utun_instance_stop(struct UTUN_INSTANCE *instance);
/* Глобальные переключатели создания TUN/топологии для тестов и встраивания. */
void utun_instance_set_tun_init_enabled(int enabled);
void utun_instance_set_topo_group_enabled(int enabled);
void utun_set_client_activity(struct UTUN_INSTANCE* instance, int active);
/* Применяет активность и мобильный standby до уведомления подписчиков; выполнять в uasync-потоке.
* Повторная команда согласует фактический режим без повторного BGP-события. 0 — успех, <0 — ошибка. */
int utun_set_client_activity(struct UTUN_INSTANCE* instance, int active);
void utun_add_activity_cbk(struct UTUN_INSTANCE* instance, utun_activity_cbk_fn fn, void* arg);
void utun_remove_activity_cbk(struct UTUN_INSTANCE* instance, utun_activity_cbk_fn fn, void* arg);
void utun_add_peer_sleep_cbk(struct UTUN_INSTANCE* instance, peer_sleep_cbk_fn fn, void* arg);

37
tests/Makefile.am

@ -89,6 +89,7 @@ check_PROGRAMS = \
test_stcp_traffic \
test_bbr_integration \
test_audio_compressor \
test_radio_vad \
test_intensive_memory_pool \
test_tcp_io \
test_uasync_socket_race \
@ -111,11 +112,19 @@ check_PROGRAMS = \
bench_timeout_heap \
bench_uasync_timeouts
# Linux-only тест: требует root + dummy-интерфейсы + iproute2/netlink
# Linux-only: test_auto_socket_dynamic требует root + dummy-интерфейсы + iproute2/netlink.
if OS_LINUX
check_PROGRAMS += test_auto_socket_dynamic
check_PROGRAMS += test_auto_socket_dynamic test_standby test_standby_transport
endif
# Silero VAD: только при включённом ONNX Runtime (--with-silero-vad)
if HAVE_SILERO_VAD
check_PROGRAMS += test_silero_vad
endif
# AEC (SpeexDSP): всегда (без внешних зависимостей)
check_PROGRAMS += test_speex_aec
# Долгие тесты: запускаются только вручную
noinst_PROGRAMS =
@ -137,6 +146,19 @@ CRYPTO_LIBS = -lcrypto
# Common libraries (lib/dns.c собирается с -DDNS_RANDOM=RAND_bytes → нужен -lcrypto)
COMMON_LIBS = $(top_builddir)/lib/libuasync.a $(CRYPTO_LIBS) -lpthread $(WIN_LIBS) @FFMPEG_LIBS@ $(top_builddir)/lib/libopus/libopus_internal.a -lm
# Вариант общего ядра с Android duty-cycle, без JNI и устройства.
if OS_LINUX
standby_test_sources = ../tools/chatgui-android/libutun_lite/standby.c ../src/utun_instance.c ../src/transport_layer/etcp_keepalive.c
standby_test_cppflags = $(AM_CPPFLAGS) -I$(top_srcdir)/src/chat -I$(top_srcdir)/tools/chatgui-android/libutun_lite -DUTUN_HAVE_STANDBY
test_standby_SOURCES = ../tools/chatgui-android/libutun_lite/tests/test_standby.c $(standby_test_sources)
test_standby_CPPFLAGS = $(standby_test_cppflags)
test_standby_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_standby_transport_SOURCES = test_standby_transport.c $(standby_test_sources)
test_standby_transport_CPPFLAGS = $(standby_test_cppflags)
test_standby_transport_LDADD = $(LIBUTUN) $(COMMON_LIBS)
test_standby_transport_LDFLAGS = -Wl,--wrap=etcp_encrypt_send -Wl,--wrap=uasync_set_timeout
endif
# Test definitions
test_etcp_bbr_SOURCES = test_etcp_bbr.c
test_etcp_bbr_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
@ -493,6 +515,17 @@ test_opus_codec_LDADD = $(COMMON_LIBS) $(top_builddir)/lib/libopus/libopus_inter
test_audio_compressor_SOURCES = test_audio_compressor.c
test_audio_compressor_LDADD = $(COMMON_LIBS) -lm
test_radio_vad_SOURCES = test_radio_vad.c
test_radio_vad_LDADD = -lm
test_silero_vad_SOURCES = test_silero_vad.c
test_silero_vad_CFLAGS = -I$(top_srcdir)/lib
test_silero_vad_LDADD = $(COMMON_LIBS) @SILERO_VAD_LIBS@
test_speex_aec_SOURCES = test_speex_aec.c
test_speex_aec_CFLAGS = -I$(top_srcdir)/lib
test_speex_aec_LDADD = $(COMMON_LIBS)
test_media_async_SOURCES = test_media_async.c
test_media_async_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/media_async -I$(top_srcdir)/lib
test_media_async_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)

2
tests/tcp_proxy_full/client.conf

@ -29,4 +29,4 @@ traffic=info
tun=info
proxy=debug
etcp_route=debug
debug=info
debug=debug

1
tests/tcp_proxy_full/exit.conf

@ -22,3 +22,4 @@ socket=info
traffic=info
proxy=debug
etcp_route=debug
debug=debug

1
tests/tcp_proxy_full/intermediate.conf

@ -25,4 +25,5 @@ general=info
traffic=info
etcp_route=debug
proxy=debug
debug=debug

16
tests/test_call.c

@ -17,6 +17,7 @@
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/topo_group.h"
#include "../routing_layer/topo_node.h"
#include "../routing_layer/conn_mgr_priv.h"
#include "../utun_instance.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/secure_channel.h"
@ -335,6 +336,20 @@ static void call_tick(void* arg) {
if (ea->accepted && eb->accepted && ea->accepted_call_id == t->call_id) t->phase = P_SEND_MEDIA;
break;
case P_SEND_MEDIA: {
// Потеря CM-пути не завершает звонок; роутер продолжает доставку.
struct TOPO_GROUP* g = topo_groups_find(A->topo_groups, strtoull(CH_ID, NULL, 10));
struct CONN_MGR_ENTRY* ce = cm_find_entry(g->conn_mgr, nb);
if (ce) {
cm_deliver_event(ce, CONN_EVENT_DOWN);
cm_deliver_event(ce, CONN_EVENT_TIMEOUT);
}
struct ETCP_CONN* conn = instance_find_conn(A, nb);
if (conn) {
etcp_fire_conn_status(conn, ETCP_CONN_STATUS_DOWN);
etcp_fire_conn_status(conn, ETCP_CONN_STATUS_UP);
}
if (ea->ended || eb->ended) { fprintf(stderr, "temporary path loss ended call\n"); t->result = 2; break; }
/* тестовый «Opus-кадр» — релей произвольных байт, не валидируем кодек */
static const uint8_t opus[16] = { 0xf8, 0xff, 0xfe, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d };
if (call_send_media(A, t->call_id, opus, sizeof(opus)) != 0) {
@ -454,6 +469,7 @@ int main(int argc, char** argv) {
if (t.inst[IDX_A]) utun_instance_destroy(t.inst[IDX_A]);
if (t.inst[IDX_B]) utun_instance_destroy(t.inst[IDX_B]);
uasync_poll(ua, 0);
uasync_destroy(ua, 0);
if (ok) OK(); else FAIL("result=%d phase=%d", t.result, (int)t.phase);

4
tests/test_call_headless.c

@ -502,6 +502,10 @@ int main(int argc, char** argv) {
if (t.inst[IDX_A]) utun_instance_destroy(t.inst[IDX_A]);
if (t.inst[IDX_B]) utun_instance_destroy(t.inst[IDX_B]);
uasync_poll(ua, 0); // завершить отложенное освобождение CM и роутера
if (ua->timer_alloc_count != ua->timer_free_count || ua->socket_alloc_count != ua->socket_free_count) {
fprintf(stderr, "headless teardown left live timers or sockets\n"); ok = 0;
}
uasync_destroy(ua, 0);
if (ok) OK(); else FAIL("result=%d phase=%d", t.result, (int)t.phase);

42
tests/test_chat_join_e2e.c

@ -12,6 +12,7 @@
// invalid неверный ключ: J отклонён, мембера J нет.
#include "chat_core.h"
#include "chat_core_priv.h"
#include "chat_sync.h"
#include "chat_join.h"
#include "chat_headless_control.h"
@ -432,6 +433,46 @@ out:
/* ── child_main ── */
/* Проверяем выбор на реальных мемберах канала; изменения производных полей откатываем до следующего poll. */
static int check_invite_priority(struct UTUN_INSTANCE* inst) {
uint64_t channel_id = strtoull(g_sh.ch_id, NULL, 10), selected = 0;
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, channel_id);
struct TOPO_GROUP_NODE* peer = topo_node_find_by_id(group, g_sh.nid[IDX_C]);
if (!peer) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: missing peer"); return 0; }
uint64_t deadline = get_time_tb() + 10000;
while (!peer->conn_presence && get_time_tb() < deadline) poll_checked(inst);
if (!peer->conn_presence) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: peer unreachable"); return 0; }
char table[80], sql[256];
peers_table_name(g_sh.ch_id, table, sizeof(table));
sqlite3* db = inst->topo_sqlite_db;
if (sqlite3_exec(db, "SAVEPOINT invite_priority", NULL, NULL, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: savepoint failed: %s", sqlite3_errmsg(db)); return 0;
}
int ok = 0;
uint8_t presence = peer->conn_presence;
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=1", table);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto out;
if (chat_invite_best_node(inst, channel_id, &selected) || selected != g_sh.nid[IDX_C]) goto out;
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=4 WHERE node_id=%lld", table, (long long)inst->node_id);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto out;
if (chat_invite_best_node(inst, channel_id, &selected) || selected != inst->node_id) goto out;
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=1 WHERE node_id=%lld", table, (long long)inst->node_id);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto out;
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET node_type=4 WHERE node_id=%lld", table, (long long)g_sh.nid[IDX_C]);
if (sqlite3_exec(db, sql, NULL, NULL, NULL) != SQLITE_OK) goto out;
if (chat_invite_best_node(inst, channel_id, &selected) || selected != g_sh.nid[IDX_C]) goto out;
peer->conn_presence = 0;
if (chat_invite_best_node(inst, channel_id, &selected) || selected != inst->node_id) goto out;
ok = 1;
out:
peer->conn_presence = presence;
if (sqlite3_exec(db, "ROLLBACK TO invite_priority; RELEASE invite_priority", NULL, NULL, NULL) != SQLITE_OK) ok = 0;
if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority failed: selected=%016llx err=%s",
(unsigned long long)selected, sqlite3_errmsg(db));
else DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "test invite priority: regular, self-supernode, supernode, unreachable PASS");
return ok;
}
static int child_main(const char* role, const char* dir, int invalid_key) {
struct UASYNC* ua = uasync_create();
if (!ua) { fprintf(stderr, "%s: uasync_create failed\n", role); return 1; }
@ -477,6 +518,7 @@ static int child_main(const char* role, const char* dir, int invalid_key) {
if (!g_sh.degraded && !wait_bgp(inst, g_sh.nid[IDX_C], 8000)) {
fprintf(stderr, "A: no BGP to C\n"); goto out;
}
if (!g_sh.degraded && !check_invite_priority(inst)) goto out;
uint64_t target = g_sh.degraded ? g_sh.nid[IDX_A] : g_sh.nid[IDX_C];
struct invite_result result = {0};

31
tests/test_conn_mgr_handles.c

@ -20,6 +20,7 @@
#include "../src/utun_instance.h"
#include "topo_group.h"
#include "conn_mgr.h"
#include "../src/routing_layer/conn_mgr_priv.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
@ -36,6 +37,7 @@ static int pa = 0, pb = 0;
static uint64_t g_nid_a = 0, g_nid_b = 0;
static volatile int cb1_ok = 0, cb2_ok = 0;
static int close_in_callback, unexpected_callback;
static struct CONN_MGR_HANDLE *gh1 = NULL, *gh2 = NULL;
static int wf(const char* p, const char* f, ...) {
@ -53,11 +55,18 @@ static void to_cb(void* arg) { (void)arg; fail("timeout"); done(); }
static void ccb1(struct CONN_MGR_HANDLE* h, uint64_t id, uint64_t gid, enum conn_mgr_event ev, void* arg) {
(void)h; (void)arg; (void)gid;
if (close_in_callback) {
conn_mgr_close(gh1); gh1 = NULL;
conn_mgr_close(gh2); gh2 = NULL;
close_in_callback = 0;
return;
}
cb1_ok = (ev == CONN_EVENT_UP);
fprintf(stderr, "ccb1: event=%d node=0x%llx ok=%d\n", ev, (unsigned long long)id, cb1_ok); fflush(stderr);
}
static void ccb2(struct CONN_MGR_HANDLE* h, uint64_t id, uint64_t gid, enum conn_mgr_event ev, void* arg) {
(void)h; (void)arg; (void)gid;
if (close_in_callback) unexpected_callback++;
cb2_ok = (ev == CONN_EVENT_UP);
fprintf(stderr, "ccb2: event=%d node=0x%llx ok=%d\n", ev, (unsigned long long)id, cb2_ok); fflush(stderr);
}
@ -119,6 +128,26 @@ static void test3_verify(void* arg) {
(void)arg;
if (entry_type() != CONN_TYPE_NONE) { fail("test3: entry should be cleaned up"); done(); return; }
fprintf(stderr, " OK: entry cleaned up (conn_type=NONE)\n"); fflush(stderr);
struct CONN_MGR* mgr = topo_groups_get_default(g_a->topo_groups)->conn_mgr;
fprintf(stderr, "Test 4: callback closes itself and next handle\n");
// Новые handles добавляются в голову: gh1 должен получить событие раньше gh2.
if (conn_mgr_open(g_a, TOPO_GROUP_UTUN, g_nid_b, ccb2, NULL, &gh2) != 0
|| conn_mgr_open(g_a, TOPO_GROUP_UTUN, g_nid_b, ccb1, NULL, &gh1) != 0) {
fail("test4: open failed"); return;
}
cb2_ok = 0; close_in_callback = 1;
cm_deliver_event(cm_find_entry(mgr, g_nid_b), CONN_EVENT_UP);
if (gh1 || gh2 || cb2_ok || unexpected_callback) { fail("test4: closed handle received callback"); return; }
fprintf(stderr, "Test 5: remote cleanup detaches handles and cancels pending UP\n");
if (conn_mgr_open(g_a, TOPO_GROUP_UTUN, g_nid_b, ccb1, NULL, &gh1) != 0) {
fail("test5: open failed"); return;
}
cb1_ok = 0;
cm_handle_disconnect(mgr, g_nid_b);
if (gh1->entry || gh1->deliver_up_token || conn_mgr_get_conn(gh1)) { fail("test5: handle not detached"); return; }
conn_mgr_close(gh1); gh1 = NULL;
uasync_poll(ua, 0);
if (cb1_ok) { fail("test5: stale UP delivered"); return; }
fprintf(stderr, "=== ALL DONE ===\n"); fflush(stderr);
done();
}
@ -154,7 +183,7 @@ done:
if (gh2) { conn_mgr_close(gh2); gh2 = NULL; }
if (g_a) { g_a->running = 0; utun_instance_destroy(g_a); g_a = NULL; }
if (g_b) { g_b->running = 0; utun_instance_destroy(g_b); g_b = NULL; }
if (ua) { uasync_destroy(ua, 0); ua = NULL; }
if (ua) { uasync_poll(ua, 0); uasync_destroy(ua, 0); ua = NULL; }
test_unlink(ca); test_unlink(cb); test_rmdir(tdir);
return (result == 1) ? 0 : 1;
}

34
tests/test_etcp_router_faults.c

@ -11,6 +11,8 @@
#include "../lib/mem.h"
#define SVC 0x42
static uint8_t svc_base = SVC;
static int drop_wire;
#define COUNT 160
#define REQUIRE(x) do { if (!(x)) { fprintf(stderr, "FAIL %s:%d: %s\n", __func__, __LINE__, #x); return -1; } } while (0)
@ -35,11 +37,11 @@ static struct ll_entry* copy_packet(const struct ll_entry* e) {
return copy;
}
static struct ETCP_ROUTER_CONN* rc(int side, int svc) {
return etcp_router_conn_get(&ep[side].inst, 0, ep[1-side].inst.node_id, SVC + svc);
return etcp_router_conn_get(&ep[side].inst, 0, ep[1-side].inst.node_id, svc_base + svc);
}
static void receive(struct ETCP_CONN* conn, struct ll_entry* e) {
int side = conn->instance == &ep[0].inst ? 0 : 1;
unsigned svc = e->dgram[0] - SVC;
unsigned svc = e->dgram[0] - svc_base;
uint64_t src, dst, group;
memcpy(&src, e->dgram + ROUTER_SVC_SRC_OFF, 8); memcpy(&dst, e->dgram + ROUTER_SVC_DST_OFF, 8);
memcpy(&group, e->dgram + ROUTER_SVC_GROUP_OFF, 8);
@ -71,6 +73,7 @@ static void wire(int from, struct ll_queue* q) {
if (e->len < SVC_ROUTE_HDR_SIZE) { release(e); return; }
struct SVC_ROUTE_HDR* h = (struct SVC_ROUTE_HDR*)e->dgram;
if (e->dgram[e->len - 1] != 1) { failed = 1; release(e); return; } /* every source retry starts afresh */
if (drop_wire) { release(e); return; }
int data = e->len > SVC_ROUTE_HDR_SIZE + 9;
if (!data && h->flags == ROUTER_FLAG_START && !saved_hello && from == 0) saved_hello = copy_packet(e);
if (!data && h->flags == (ROUTER_FLAG_START | ROUTER_FLAG_RST) && !saved_confirm && from == 0) saved_confirm = copy_packet(e);
@ -79,7 +82,7 @@ static void wire(int from, struct ll_queue* q) {
if (!data && h->flags == ROUTER_FLAG_RST) drop = &drop_challenge[from];
if (!data && h->flags == (ROUTER_FLAG_START | ROUTER_FLAG_RST)) drop = &drop_confirm[from];
if (drop && *drop) { --*drop; handshake_drops++; release(e); return; }
if (data && from == 0 && h->svc_id == SVC) {
if (data && from == 0 && h->svc_id == svc_base) {
if (!saved_data) saved_data = copy_packet(e);
if (drop_data && h->seq == 2) { drop_data--; dropped_data++; release(e); return; }
if (reorder && h->seq == 3 && !held) { held = e; return; }
@ -103,12 +106,13 @@ static int setup(void) {
memset(ep, 0, sizeof(ep)); failed = 0; ua = uasync_create(); REQUIRE(ua);
drop_data = drop_ack = drop_all_ack = reorder = duplicated = dropped_data = dropped_ack = handshake_drops = closed_in_handler = 0;
memset(drop_hello, 0, sizeof(drop_hello)); memset(drop_challenge, 0, sizeof(drop_challenge)); memset(drop_confirm, 0, sizeof(drop_confirm));
drop_wire = 0;
max_wire = 0; held = saved_data = saved_ack = saved_hello = saved_confirm = NULL;
for (int i = 0; i < 2; i++) {
ep[i].inst.ua = ua; ep[i].inst.node_id = i + 1;
REQUIRE(etcp_router_init(&ep[i].inst) == 0);
REQUIRE(etcp_router_bind(&ep[i].inst, SVC, receive) == 0);
REQUIRE(etcp_router_bind(&ep[i].inst, SVC+1, receive) == 0);
REQUIRE(etcp_router_bind(&ep[i].inst, svc_base, receive) == 0);
REQUIRE(etcp_router_bind(&ep[i].inst, svc_base+1, receive) == 0);
ep[i].conn.instance = ep[i].alternate.instance = &ep[i].inst;
ep[i].conn.peer_node_id = ep[i].alternate.peer_node_id = 2-i;
ep[i].conn.send_input_q = queue_new(ua, 0, 0, 0, "wire");
@ -273,7 +277,7 @@ static int close_from_producer(void) {
struct ETCP_ROUTER_CONN* c = rc(0,0);
for (int i=0; i<ROUTER_MAX_SEND_Q_PACKETS; i++) REQUIRE(send_one(0,0) == 0);
struct queue_waiter_handle waiter = {0};
etcp_router_on_send_ready(&ep[0].inst, 0, ep[1].inst.node_id, SVC, &waiter, producer_closes, c);
etcp_router_on_send_ready(&ep[0].inst, 0, ep[1].inst.node_id, svc_base, &waiter, producer_closes, c);
REQUIRE(waiter.internal);
step();
REQUIRE(ep[0].notified[0] == 1 && ep[0].inst.router_conns->count == 0);
@ -327,6 +331,22 @@ static int simultaneous_restart(void) {
REQUIRE(ep[0].notified[0] == 1 && ep[1].notified[0] == 1);
cleanup(); return 0;
}
static int call_ten_second_gap(void) {
svc_base = ETCP_RT_ID_CALL;
REQUIRE(setup() == 0); REQUIRE(send_one(0,0) == 0); REQUIRE(wait_settled(0,0) == 0);
uint64_t epoch = rc(0,0)->reset_id;
drop_wire = 1;
REQUIRE(send_one(0,0) == 0);
uint64_t deadline = get_time_tb() + 100000;
while (get_time_tb() < deadline && !failed) step();
REQUIRE(!failed); REQUIRE(ep[0].notified[0] == 0);
REQUIRE(rc(0,0)->reset_id == epoch && rc(0,0)->inflight_q->count == 1);
drop_wire = 0;
REQUIRE(wait_settled(0,0) == 0);
REQUIRE(ep[1].received[0] == 2 && ep[1].notified[0] == 0);
REQUIRE(rc(0,0)->reset_id == epoch && rc(0,0)->c_retrans_done > 0);
cleanup(); svc_base = SVC; return 0;
}
static int size_bounds(void) {
REQUIRE(setup() == 0);
uint8_t* data = u_calloc(1, PKTNORM_MAX_DGRAM_SIZE);
@ -352,7 +372,7 @@ int main(void) {
{"restart rejects old DATA/ACK/CLOSE/HELLO/confirm", stale_session}, {"sequence wraparound", wraparound},
{"waiter lifetime after route switch", waiter_lifetime}, {"service closes during delivery", close_from_handler}, {"route switch while old next hop blocked", route_switch},
{"no route then restore without data loss", no_route_restore}, {"duplicate ACK cannot prevent timeout", timeout_with_duplicate_ack},
{"simultaneous endpoint restart", simultaneous_restart}, {"payload size boundaries", size_bounds}, {"producer closes during send queue drain", close_from_producer}
{"simultaneous endpoint restart", simultaneous_restart}, {"call: 10s outage retains session and pending audio", call_ten_second_gap}, {"payload size boundaries", size_bounds}, {"producer closes during send queue drain", close_from_producer}
};
for (unsigned i=0; i<sizeof(tests)/sizeof(tests[0]); i++) {
printf("TEST %u: %s\n", i+1, tests[i].name); fflush(stdout);

9
tests/test_proxy_regressions.c

@ -204,6 +204,15 @@ static void server_tests(struct UTUN_INSTANCE* inst) {
CHECK(inst->tcp_proxy_server.conn_count == 1);
deliver(inst, 22, ETCP_RT_ID_TCP_PROXY_SERVER, TCP_PROXY_SUBCMD_CLOSE, 2, NULL, 0);
CHECK(inst->tcp_proxy_server.conn_count == 0);
// Сначала FIN от ETCP, затем FIN из OS: on_fin закрывает и освобождает exit-поток.
deliver(inst, 11, ETCP_RT_ID_TCP_PROXY_SERVER, TCP_PROXY_SUBCMD_CONNECT, 3, connect_data, 6);
socket_t c = accept(listener, NULL, NULL); CHECK(c != SOCKET_INVALID); socket_set_nonblocking(c);
deliver(inst, 11, ETCP_RT_ID_TCP_PROXY_SERVER, TCP_PROXY_SUBCMD_FIN, 3, NULL, 0);
CHECK(inst->tcp_proxy_server.conn_count == 1 && recv(c, buf, sizeof(buf), 0) == 0);
message_count = 0;
CHECK(shutdown(c, SHUT_WR) == 0); pump();
CHECK(inst->tcp_proxy_server.conn_count == 0 && count_cmd(TCP_PROXY_SUBCMD_FIN) == 1);
socket_close_wrapper(c);
socket_close_wrapper(a); socket_close_wrapper(b); socket_close_wrapper(listener);
inst->tcp_proxy_server.enabled = 0; pump();
puts("[PASS] exit streams isolated by node + stream ID, duplicate CONNECT rejected");

134
tests/test_radio_vad.c

@ -0,0 +1,134 @@
/* Тест чистой логики VAD авто-PTT рации (radio_vad.h): ресемплер 48k→16k и
* фильтр-гистерезис + машина состояний. Без ONNX Runtime и без аудио-устройств —
* детерминированные синтетические последовательности.
*/
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "../src/radio/radio_vad.h"
#define PI 3.14159265358979323846
static int g_failures = 0;
#define CHECK(cond, ...) do { \
if (!(cond)) { printf(" FAIL: " __VA_ARGS__); printf("\n"); g_failures++; } \
} while (0)
static int near(float a, float b, float eps) {
return fabsf(a - b) <= eps;
}
/* ── ресемплер ── */
static void test_resample(void) {
printf("[radio_vad] resampler 48k -> 16k\n");
/* тишина → тишина */
{
int16_t in[960] = {0};
float out[RADIO_VAD_FRAME_SAMPLES_16K];
radio_vad_resample(in, 1, out);
for (int i = 0; i < RADIO_VAD_FRAME_SAMPLES_16K; i++) CHECK(out[i] == 0.0f, "silence out[%d]=%f", i, out[i]);
}
/* DC 0.5*32768 → выход ~0.5 (box-фильтр сохраняет DC) */
{
int16_t in[960];
float out[RADIO_VAD_FRAME_SAMPLES_16K];
for (int i = 0; i < 960; i++) in[i] = 16384;
radio_vad_resample(in, 1, out);
for (int i = 0; i < RADIO_VAD_FRAME_SAMPLES_16K; i++) CHECK(near(out[i], 0.5f, 0.001f), "DC out[%d]=%f", i, out[i]);
}
/* стерео L=R → тот же результат, что моно */
{
int16_t mono[960];
int16_t stereo[1920];
float om[RADIO_VAD_FRAME_SAMPLES_16K], os[RADIO_VAD_FRAME_SAMPLES_16K];
for (int i = 0; i < 960; i++) { mono[i] = (int16_t)(sin(2.0 * PI * 1000.0 * i / 48000.0) * 20000.0); }
for (int i = 0; i < 960; i++) { stereo[2*i] = mono[i]; stereo[2*i+1] = mono[i]; }
radio_vad_resample(mono, 1, om);
radio_vad_resample(stereo, 2, os);
for (int i = 0; i < RADIO_VAD_FRAME_SAMPLES_16K; i++) CHECK(near(om[i], os[i], 1e-4f), "stereo!=mono out[%d] %f vs %f", i, om[i], os[i]);
}
/* синус 1кГц: амплитуда сохраняется (box-фильтр даёт ~0.6% ослабления) */
{
int16_t in[960];
float out[RADIO_VAD_FRAME_SAMPLES_16K];
float peak = 0.0f;
for (int i = 0; i < 960; i++) in[i] = (int16_t)(sin(2.0 * PI * 1000.0 * i / 48000.0) * 30000.0);
radio_vad_resample(in, 1, out);
for (int i = 0; i < RADIO_VAD_FRAME_SAMPLES_16K; i++) { float a = fabsf(out[i]); if (a > peak) peak = a; }
CHECK(peak > 0.85f && peak <= 1.0f, "1kHz peak=%.3f (ожидали ~0.91)", peak);
}
}
/* ── машина состояний ── */
static void test_fsm(void) {
printf("[radio_vad] fsm (confirm / hangover / busy)\n");
struct radio_vad_fsm f;
const float thr = 0.5f;
const int confirm = 2;
const uint64_t hangover = 2000; /* 200мс */
uint64_t now = 0;
/* подтверждение: 2 окна выше порога → старт */
radio_vad_fsm_reset(&f);
now += 320;
CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now) == 0, "confirm#1 должен молчать");
now += 320;
CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now) == 1, "confirm#2 должен дать START");
CHECK(f.talking == 1, "после старта talking=1");
/* одиночный пик не запускает передачу */
radio_vad_fsm_reset(&f);
now += 320;
CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now) == 0, "одиночный пик не стартует");
now += 320;
CHECK(radio_vad_fsm_update(&f, 0.1f, thr, confirm, hangover, 0, now) == 0, "сброс подтверждения");
/* hangover: после старта тишина ~200мс → STOP */
radio_vad_fsm_reset(&f);
now += 320; radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now);
now += 320; CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now) == 1, "старт");
{
int stops = 0;
for (int i = 0; i < 10; i++) {
now += 320;
int a = radio_vad_fsm_update(&f, 0.1f, thr, confirm, hangover, 0, now);
if (a == -1) { stops++; break; }
CHECK(a == 0, "hangover: до ~200мс не стопим");
}
CHECK(stops == 1, "hangover должен дать STOP");
CHECK(f.talking == 0, "после STOP talking=0");
}
/* busy: чужой разговор гасит нашу передачу сразу */
radio_vad_fsm_reset(&f);
now += 320; radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now);
now += 320; radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 0, now);
CHECK(f.talking == 1, "старт перед busy");
now += 320;
CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 1, now) == -1, "busy должен дать STOP");
/* busy блокирует старт */
radio_vad_fsm_reset(&f);
for (int i = 0; i < 5; i++) {
now += 320;
CHECK(radio_vad_fsm_update(&f, 0.9f, thr, confirm, hangover, 1, now) == 0, "busy блокирует старт");
}
CHECK(f.talking == 0, "busy: не стартуем");
}
int main(void) {
test_resample();
test_fsm();
if (g_failures == 0) { printf("TEST PASSED\n"); return 0; }
printf("TEST FAILED (%d failures)\n", g_failures);
return 1;
}

14
tests/test_services.c

@ -22,6 +22,19 @@ static void work(void* arg) { ((struct job*)arg)->worked = 1; }
static void done(void* arg, int err) { struct job* job = arg; job->done++; job->error = err; }
static void ready(struct ll_queue* queue, void* arg) { (void)queue; (*(int*)arg)++; }
/* Активность звонка не создаёт таймер, который сам переведёт мобильный узел в standby. */
static void check_client_activity(struct UASYNC* ua) {
struct UTUN_INSTANCE mobile = { .ua = ua, .client_type = CLIENT_TYPE_MOBILE, .client_activity = CLIENT_ACTIVITY_STANDBY };
uint64_t timers = ua->timer_alloc_count;
utun_set_client_activity(&mobile, 1);
utun_set_client_activity(&mobile, 1);
uasync_poll(ua, 0);
assert(mobile.client_activity == CLIENT_ACTIVITY_ACTIVE && ua->timer_alloc_count == timers);
utun_set_client_activity(&mobile, 0);
assert(mobile.client_activity == CLIENT_ACTIVITY_STANDBY);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "[PASS] explicit mobile activity persists without timeout and returns to standby");
}
/* Pending transit shares the physical connection but belongs to its group. */
static void pending_transit(struct ETCP_CONN* conn, uint64_t group_id, int* calls) {
if (!conn->transit_queues) conn->transit_queues = queue_new(conn->instance->ua, 16, 0, 24, "test_transit");
@ -47,6 +60,7 @@ static struct TOPO_GROUP* chat_group(struct UTUN_INSTANCE* inst) {
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
struct UASYNC* ua = uasync_create(); assert(ua);
check_client_activity(ua);
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua,
"[global]\nmy_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n"
"my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n"

196
tests/test_silero_vad.c

@ -0,0 +1,196 @@
/* Тест Silero VAD-обёртки (lib/silero_vad) поверх ONNX Runtime.
*
* Проверяет:
* - загрузку модели и жизненный цикл (create/destroy/reset);
* - обработку ошибок (пустой/несуществующий путь, NULL-аргументы);
* - что на тишине вероятность речи близка к нулю (реальный инференс, не мусор);
* - что путь pcm16 даёт тот же результат, что и float (конвертация /32768);
* - что вероятность всегда лежит в [0;1].
*
* Модель ищется в argv[1], по умолчанию "../lib/silero_vad.onnx" (запуск из tests/).
*/
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "../lib/silero_vad.h"
#include "../lib/debug_config.h"
#define PI 3.14159265358979323846
static int g_failures = 0;
#define CHECK(cond, ...) do { \
if (!(cond)) { \
printf(" FAIL: " __VA_ARGS__); printf("\n"); \
g_failures++; \
} \
} while (0)
/* Детерминированный «гласный»: модулированный гармонический комплекс (f0 ~120 Гц). */
static void gen_vowel(float* out, int n, int offset) {
int i;
for (i = 0; i < n; i++) {
double t = (double)(offset + i) / 16000.0;
double f0 = 120.0 + 20.0 * sin(2.0 * PI * 3.0 * t);
double env = 0.5 + 0.5 * sin(2.0 * PI * 2.0 * t);
double s = sin(2.0 * PI * f0 * t)
+ 0.5 * sin(2.0 * PI * 2.0 * f0 * t)
+ 0.3 * sin(2.0 * PI * 3.0 * f0 * t)
+ 0.2 * sin(2.0 * PI * 4.0 * f0 * t);
out[i] = (float)(s * env * 0.25);
}
}
static int test_lifecycle_and_errors(const char* model_path) {
silero_vad_t* vad;
printf("[silero_vad] lifecycle + error paths\n");
vad = silero_vad_create(NULL);
CHECK(vad == NULL, "create(NULL) should fail");
if (vad) silero_vad_destroy(vad);
vad = silero_vad_create("");
CHECK(vad == NULL, "create(\"\") should fail");
vad = silero_vad_create("/nonexistent/silero_vad.onnx");
CHECK(vad == NULL, "create(bad path) should fail");
vad = silero_vad_create(model_path);
CHECK(vad != NULL, "create(%s) should succeed", model_path);
if (!vad) return 1;
silero_vad_reset(vad);
silero_vad_reset(vad); /* повторный reset безопасен */
silero_vad_destroy(vad);
silero_vad_destroy(NULL); /* destroy(NULL) безопасен */
return 0;
}
static int test_silence(const char* model_path) {
silero_vad_t* vad;
float zeros[SILERO_VAD_WINDOW_SAMPLES];
float prob = -1.0f;
float max_prob = 0.0f;
int i, rc;
printf("[silero_vad] silence -> prob ~ 0\n");
memset(zeros, 0, sizeof(zeros));
vad = silero_vad_create(model_path);
CHECK(vad != NULL, "create failed");
if (!vad) return 1;
for (i = 0; i < 32; i++) {
rc = silero_vad_process(vad, zeros, &prob);
CHECK(rc == 0, "process(%d) rc=%d", i, rc);
CHECK(prob >= 0.0f && prob <= 1.0f, "prob out of range: %f", prob);
if (prob > max_prob) max_prob = prob;
}
printf(" silence max_prob=%.4f\n", max_prob);
CHECK(max_prob < 0.2f, "silence probability too high: %f", max_prob);
silero_vad_destroy(vad);
return 0;
}
static int test_pcm16_matches_float(const char* model_path) {
silero_vad_t* vad;
int16_t pcm[SILERO_VAD_WINDOW_SAMPLES];
float samples[SILERO_VAD_WINDOW_SAMPLES];
float prob_f = -1.0f, prob_i = -1.0f;
int rc;
printf("[silero_vad] pcm16 path == float path\n");
vad = silero_vad_create(model_path);
CHECK(vad != NULL, "create failed");
if (!vad) return 1;
gen_vowel(samples, SILERO_VAD_WINDOW_SAMPLES, 0);
for (int i = 0; i < SILERO_VAD_WINDOW_SAMPLES; i++) {
pcm[i] = (int16_t)(samples[i] * 32768.0f);
}
rc = silero_vad_process(vad, samples, &prob_f);
CHECK(rc == 0, "process(float) rc=%d", rc);
silero_vad_reset(vad); /* одинаковое состояние перед вторым прогоном */
rc = silero_vad_process_pcm16(vad, pcm, &prob_i);
CHECK(rc == 0, "process_pcm16 rc=%d", rc);
printf(" float prob=%.5f pcm16 prob=%.5f\n", prob_f, prob_i);
CHECK(fabsf(prob_f - prob_i) < 1e-4f, "pcm16 != float: %f vs %f", prob_i, prob_f);
silero_vad_destroy(vad);
return 0;
}
static int test_default_model(void) {
silero_vad_t* vad;
float zeros[SILERO_VAD_WINDOW_SAMPLES];
float prob = -1.0f;
int rc;
printf("[silero_vad] create_default (embedded model)\n");
memset(zeros, 0, sizeof(zeros));
vad = silero_vad_create_default();
CHECK(vad != NULL, "create_default failed");
if (!vad) return 1;
rc = silero_vad_process(vad, zeros, &prob);
CHECK(rc == 0, "process rc=%d", rc);
CHECK(prob >= 0.0f && prob <= 1.0f, "prob out of range: %f", prob);
CHECK(prob < 0.2f, "silence prob too high: %f", prob);
printf(" embedded silence prob=%.4f\n", prob);
silero_vad_destroy(vad);
return 0;
}
static int test_null_args(const char* model_path) {
silero_vad_t* vad;
float samples[SILERO_VAD_WINDOW_SAMPLES];
float prob;
printf("[silero_vad] NULL-arg guards\n");
memset(samples, 0, sizeof(samples));
vad = silero_vad_create(model_path);
CHECK(vad != NULL, "create failed");
if (!vad) return 1;
CHECK(silero_vad_process(NULL, samples, &prob) != 0, "process(NULL vad) should fail");
CHECK(silero_vad_process(vad, NULL, &prob) != 0, "process(NULL samples) should fail");
CHECK(silero_vad_process(vad, samples, NULL) != 0, "process(NULL prob) should fail");
CHECK(silero_vad_process_pcm16(vad, NULL, &prob) != 0, "process_pcm16(NULL) should fail");
silero_vad_destroy(vad);
return 0;
}
int main(int argc, char** argv) {
const char* model_path = (argc > 1) ? argv[1] : "../lib/silero_vad.onnx";
debug_config_init();
debug_set_level(DEBUG_LEVEL_INFO);
debug_set_category_level(DEBUG_CATEGORY_VAD, DEBUG_LEVEL_INFO);
printf("Silero VAD test, model: %s\n", model_path);
test_lifecycle_and_errors(model_path);
test_silence(model_path);
test_pcm16_matches_float(model_path);
test_null_args(model_path);
test_default_model();
if (g_failures == 0) {
printf("TEST PASSED\n");
return 0;
}
printf("TEST FAILED (%d failures)\n", g_failures);
return 1;
}

219
tests/test_speex_aec.c

@ -0,0 +1,219 @@
/* Тест обёртки эхоподавления lib/speex_aec (SpeexDSP mdf).
*
* Проверяет:
* - жизненный цикл и NULL/невалидные аргументы (create/destroy/reset/process);
* - линию задержки: passthrough на старте, наполнение до delay_frames, учёт дрейфа;
* - накопление захвата/рендера чанками произвольного размера (480/960);
* - собственно подавление эха: синтетический far-end (шум) + near-end (тон),
* после адаптации остаток эха должен быть заметно меньше исходного эха.
*/
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "../lib/speex_aec.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#define RATE 48000
#define FS 960 /* 20 мс */
#define FILTER 14400 /* хвост 300 мс */
#define DELAY_FRAMES 2
#define ECHO_GAIN 0.5
#define N_FRAMES 160
#define ADAPT_FRAMES 100 /* фаза адаптации: только far-end */
#define DT_START (ADAPT_FRAMES)
#define DT_FRAMES 20 /* double-talk: речь + эхо */
#define MEAS_START (DT_START + DT_FRAMES)
static int g_failures = 0;
#define CHECK(cond, ...) do { \
if (!(cond)) { \
printf(" FAIL: " __VA_ARGS__); printf("\n"); \
g_failures++; \
} \
} while (0)
/* Детерминированный «шум» far-end (LCG). */
static uint32_t g_rng = 0x12345678;
static int16_t aec_noise(void) {
g_rng = g_rng * 1664525u + 1013904223u;
return (int16_t)(((g_rng >> 16) & 0x3fff) - 0x1fff);
}
static int test_lifecycle_and_null(void) {
speex_aec_t* a;
int16_t pcm[FS], out[FS];
printf("[speex_aec] lifecycle + NULL guards\n");
memset(pcm, 0, sizeof(pcm));
a = speex_aec_create(RATE, FS, FILTER, DELAY_FRAMES);
CHECK(a != NULL, "create failed");
if (!a) return 1;
CHECK(speex_aec_create(0, FS, FILTER, DELAY_FRAMES) == NULL, "create(rate=0) should fail");
CHECK(speex_aec_create(RATE, 0, FILTER, DELAY_FRAMES) == NULL, "create(frame=0) should fail");
CHECK(speex_aec_create(RATE, FS, FS - 1, DELAY_FRAMES) == NULL, "create(filter<frame) should fail");
CHECK(speex_aec_create(RATE, FS, FILTER, -3) != NULL, "create(delay=-3) should clamp, not fail");
CHECK(speex_aec_process_capture(NULL, pcm, FS, out) == 0, "process(NULL vad) should return 0");
CHECK(speex_aec_process_capture(a, NULL, FS, out) == 0, "process(NULL pcm) should return 0");
CHECK(speex_aec_process_capture(a, pcm, FS, NULL) == 0, "process(NULL out) should return 0");
CHECK(speex_aec_delay_fill(NULL) == 0, "delay_fill(NULL) should be 0");
speex_aec_feed_playback(NULL, pcm, FS); /* не должно падать */
speex_aec_reset(NULL); /* не должно падать */
speex_aec_destroy(NULL); /* не должно падать */
speex_aec_destroy(a);
return 0;
}
static int test_delay_line(void) {
speex_aec_t* a;
int16_t in[FS], out[FS];
int16_t pcm[FS];
int i, n;
printf("[speex_aec] delay line + chunk accumulation\n");
for (i = 0; i < FS; i++) pcm[i] = (int16_t)(i & 0x7ff);
a = speex_aec_create(RATE, FS, FILTER, 3);
CHECK(a != NULL, "create failed");
if (!a) return 1;
CHECK(speex_aec_delay_fill(a) == 0, "initial fill=%d (expect 0)", speex_aec_delay_fill(a));
/* захват до рендера: passthrough (линия пуста) */
memset(in, 0x11, sizeof(in));
n = speex_aec_process_capture(a, in, FS, out);
CHECK(n == FS, "process_capture returned %d (expect %d)", n, FS);
CHECK(memcmp(in, out, sizeof(in)) == 0, "passthrough expected at empty line");
/* рендер чанками по 480 → накопление до кадра */
speex_aec_feed_playback(a, pcm, 480);
CHECK(speex_aec_delay_fill(a) == 0, "fill=%d after 480 (expect 0)", speex_aec_delay_fill(a));
speex_aec_feed_playback(a, pcm + 480, 480);
CHECK(speex_aec_delay_fill(a) == 1, "fill=%d after 960 (expect 1)", speex_aec_delay_fill(a));
/* захват чанками по 480 → 0, потом кадр */
n = speex_aec_process_capture(a, in, 480, out);
CHECK(n == 0, "capture 480 should accumulate (returned %d)", n);
n = speex_aec_process_capture(a, in + 480, 480, out);
CHECK(n == FS, "capture 960 should emit frame (returned %d)", n);
/* докормить рендер до глубины 3 */
speex_aec_feed_playback(a, pcm, FS);
speex_aec_feed_playback(a, pcm, FS);
CHECK(speex_aec_delay_fill(a) == 3, "fill=%d (expect 3)", speex_aec_delay_fill(a));
/* обработка кадра с полной линией уменьшает её на 1 */
n = speex_aec_process_capture(a, in, FS, out);
CHECK(n == FS, "process_capture returned %d", n);
CHECK(speex_aec_delay_fill(a) == 2, "fill=%d after capture (expect 2)", speex_aec_delay_fill(a));
speex_aec_reset(a);
CHECK(speex_aec_delay_fill(a) == 0, "fill=%d after reset (expect 0)", speex_aec_delay_fill(a));
speex_aec_destroy(a);
return 0;
}
static int test_echo_suppression(void) {
speex_aec_t* a;
int16_t* render_hist;
int16_t speech[FS], cap[FS], out[FS];
int k, i, n;
double echo_pow = 0.0, resid_pow = 0.0;
double dt_speech_pow = 0.0, dt_out_pow = 0.0, dt_cross = 0.0;
int measured = 0, dt_measured = 0;
printf("[speex_aec] echo suppression (far-end noise, near-end tone)\n");
render_hist = (int16_t*)u_malloc((uint32_t)(N_FRAMES * FS) * sizeof(int16_t));
CHECK(render_hist != NULL, "OOM render_hist");
if (!render_hist) return 1;
for (k = 0; k < N_FRAMES; k++)
for (i = 0; i < FS; i++) render_hist[k * FS + i] = aec_noise();
a = speex_aec_create(RATE, FS, FILTER, DELAY_FRAMES);
CHECK(a != NULL, "create failed");
if (!a) { u_free(render_hist); return 1; }
for (k = 0; k < N_FRAMES; k++) {
for (i = 0; i < FS; i++) {
double t = (double)(k * FS + i) / RATE;
speech[i] = (int16_t)(4000.0 * sin(2.0 * M_PI * 440.0 * t));
}
/* фазы: [0,ADAPT) — только эхо (адаптация); [DT_START,MEAS_START) — речь+эхо;
* [MEAS_START,N) — только эхо (замер чистого подавления). */
int double_talk = (k >= DT_START && k < MEAS_START);
for (i = 0; i < FS; i++) {
int echo_idx = k - DELAY_FRAMES;
int16_t echo = echo_idx >= 0 ? (int16_t)(ECHO_GAIN * render_hist[echo_idx * FS + i]) : 0;
cap[i] = double_talk ? (int16_t)(speech[i] + echo) : echo;
}
n = speex_aec_process_capture(a, cap, FS, out);
CHECK(n == FS, "process_capture returned %d (expect %d)", n, FS);
speex_aec_feed_playback(a, render_hist + k * FS, FS);
if (double_talk) {
for (i = 0; i < FS; i++) {
dt_speech_pow += (double)speech[i] * speech[i];
dt_out_pow += (double)out[i] * out[i];
dt_cross += (double)speech[i] * out[i];
}
dt_measured += FS;
} else if (k >= MEAS_START) {
for (i = 0; i < FS; i++) {
int echo_idx = k - DELAY_FRAMES;
int16_t echo = echo_idx >= 0 ? (int16_t)(ECHO_GAIN * render_hist[echo_idx * FS + i]) : 0;
echo_pow += (double)echo * echo;
resid_pow += (double)out[i] * out[i];
}
measured += FS;
}
}
{
double echo_rms = sqrt(echo_pow / measured);
double resid_rms = sqrt(resid_pow / measured);
double db = echo_rms > 0 ? 20.0 * log10(echo_rms / (resid_rms > 1.0 ? resid_rms : 1.0)) : 0.0;
printf(" echo-only suppression: echo_rms=%.1f resid_rms=%.1f => %.1f dB\n", echo_rms, resid_rms, db);
CHECK(echo_rms > 100.0, "echo too weak to measure (echo_rms=%.1f)", echo_rms);
CHECK(resid_pow < 0.05 * echo_pow, "echo not suppressed: resid=%.0f echo=%.0f", resid_pow, echo_pow);
}
{
double corr = sqrt(dt_speech_pow * dt_out_pow);
double sim = corr > 0.0 ? dt_cross / corr : 0.0;
printf(" double-talk speech preservation: correlation=%.3f\n", sim);
CHECK(sim > 0.7, "near-end speech distorted: correlation=%.3f", sim);
}
speex_aec_destroy(a);
u_free(render_hist);
return 0;
}
int main(void) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_INFO);
debug_set_category_level(DEBUG_CATEGORY_AEC, DEBUG_LEVEL_INFO);
printf("SpeexDSP AEC test\n");
test_lifecycle_and_null();
test_delay_line();
test_echo_suppression();
if (g_failures == 0) {
printf("TEST PASSED\n");
return 0;
}
printf("TEST FAILED (%d failures)\n", g_failures);
return 1;
}

180
tests/test_standby_transport.c

@ -0,0 +1,180 @@
/* Проверка режима и фаз standby на реальных keepalive-пакетах до шифрования.
* Управляемая отправка исключает сеть; отказ таймера проверяет сохранение рабочего режима. */
#include <assert.h>
#include <string.h>
#include "utun_instance.h"
#include "etcp.h"
#include "etcp_keepalive.h"
#include "standby.h"
#include "debug_config.h"
static struct UASYNC* ua;
static struct UTUN_INSTANCE inst;
static struct ETCP_CONN udp, tcp;
static struct ETCP_LINK links[3];
static struct ll_queue udp_queue, tcp_queue;
static struct ll_entry *udp_entry, *tcp_entry;
static struct { unsigned count; uint8_t flags; uint16_t active, sleep; } sent[3];
static int fail_phase_timer, activity_events, wait_calls;
static unsigned wait_after_packets;
/* Перехватывает сформированный keepalive, сохраняя его реальные флаги и тайминг. */
int __wrap_etcp_encrypt_send(struct ETCP_DGRAM* pkt) {
assert(pkt && pkt->data[0] == ETCP_KEEPALIVE && pkt->data_len >= 4);
int i;
for (i = 0; i < 3 && pkt->link != &links[i]; i++);
assert(i < 3);
sent[i].count++;
sent[i].flags = pkt->data[1];
sent[i].active = pkt->data_len >= 6 ? pkt->data[4] | ((uint16_t)pkt->data[5] << 8) : 0;
sent[i].sleep = pkt->data_len >= 8 ? pkt->data[6] | ((uint16_t)pkt->data[7] << 8) : 0;
return pkt->data_len;
}
void* __real_uasync_set_timeout(struct UASYNC* ua, int tb, void* arg, timeout_callback_t cb, const char* name);
/* Отказывает только фазовому таймеру, остальные таймеры выполняются настоящим uasync. */
void* __wrap_uasync_set_timeout(struct UASYNC* loop, int tb, void* arg, timeout_callback_t cb, const char* name) {
if (fail_phase_timer && name && !strcmp(name, "standby_phase")) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "[TEST] injected standby phase timer failure");
return NULL;
}
return __real_uasync_set_timeout(loop, tb, arg, cb, name);
}
/* Подписчики активности видят уже согласованные режимы, без промежуточного состояния. */
static void activity_changed(struct UTUN_INSTANCE* owner, int active, void* arg) {
(void)arg;
assert(owner == &inst && inst.client_activity == active);
assert(standby_is_enabled() == !active);
for (int i = 0; i < 3; i++) assert(links[i].ka_my_sleeping == !active);
activity_events++;
}
/* Работа возобновляется после отправки новой фазы пирам или выхода из standby. */
static void waited(void* arg) {
(void)arg;
assert(sent[0].count > wait_after_packets);
if (standby_is_enabled()) {
assert(standby_get_active_remaining_tb() > 0 && sent[0].flags == KA_FLAG_SLEEP && sent[0].active > 0);
} else {
assert(inst.client_activity == CLIENT_ACTIVITY_ACTIVE && !links[0].ka_my_sleeping && sent[0].flags == 0);
}
wait_calls++;
}
/* Мобильный узел с исходящим UDP, входящим UDP и TCP линками. */
static void setup(void) {
ua = uasync_create(); assert(ua);
memset(&inst, 0, sizeof(inst)); memset(&udp, 0, sizeof(udp)); memset(&tcp, 0, sizeof(tcp));
memset(links, 0, sizeof(links)); memset(sent, 0, sizeof(sent));
activity_events = wait_calls = fail_phase_timer = 0;
inst.ua = ua; inst.client_type = CLIENT_TYPE_MOBILE; inst.client_activity = CLIENT_ACTIVITY_STANDBY;
udp.instance = tcp.instance = &inst;
strcpy(udp.log_name, "standby-test-udp"); strcpy(tcp.log_name, "standby-test-tcp");
for (int i = 0; i < 3; i++) {
links[i].etcp = i == 2 ? &tcp : &udp;
links[i].local_link_id = i + 1; links[i].initialized = links[i].link_status = 1;
links[i].keepalive_interval = links[i].ka_period_ms = 2000;
links[i].keepalive_timeout = 20000; links[i].last_recv_local_time = get_time_tb();
}
links[0].next = &links[1]; links[1].is_server = 1; links[2].is_tcp = 1;
udp.links = &links[0]; tcp.links = &links[2];
udp_entry = queue_entry_new(sizeof(struct conn_queue_entry)); assert(udp_entry);
tcp_entry = queue_entry_new(sizeof(struct tcp_conn_entry)); assert(tcp_entry);
((struct conn_queue_entry*)udp_entry->data)->conn = &udp;
((struct tcp_conn_entry*)tcp_entry->data)->etcp_conn = &tcp;
memset(&udp_queue, 0, sizeof(udp_queue)); memset(&tcp_queue, 0, sizeof(tcp_queue));
udp_queue.head = udp_entry; tcp_queue.head = tcp_entry;
inst.connections = &udp_queue; inst.tcp_connections = &tcp_queue;
standby_set_intervals_ms(200, 300, 0);
standby_init(ua); etcp_keepalive_register(&inst);
utun_add_activity_cbk(&inst, activity_changed, NULL);
for (int i = 0; i < 3; i++) etcp_keepalive_link_ready(&links[i]);
}
/* Отменяет таймеры и подписки перед повторной инициализацией ядра. */
static void teardown(void) {
standby_deinit();
for (int i = 0; i < 3; i++) {
if (links[i].keepalive_timer) uasync_cancel_timeout(ua, links[i].keepalive_timer);
if (links[i].ka_sleep_announce_timer) uasync_cancel_timeout(ua, links[i].ka_sleep_announce_timer);
}
while (inst.activity_cbks) utun_remove_activity_cbk(&inst, inst.activity_cbks->fn, inst.activity_cbks->arg);
queue_entry_free(udp_entry); queue_entry_free(tcp_entry);
uasync_poll(ua, 0);
assert(ua->timer_alloc_count == ua->timer_free_count);
uasync_destroy(ua, 0);
}
/* Первый фон при начальном STANDBY, повторы, новые линки, обе причины wake и выход в ACTIVE. */
static void check_transitions(void) {
setup();
assert(!standby_is_enabled());
for (int i = 0; i < 3; i++) assert(sent[i].count == 1 && sent[i].flags == 0 && links[i].keepalive_timer);
assert(utun_set_client_activity(&inst, 0) == 0 && standby_is_enabled() && activity_events == 0);
for (int i = 0; i < 3; i++) {
assert(links[i].ka_my_sleeping && !links[i].keepalive_timer);
assert(sent[i].count == 2 && sent[i].flags == KA_FLAG_SLEEP && sent[i].active > 0 && sent[i].sleep == 3);
}
uint64_t timers = ua->timer_alloc_count;
assert(utun_set_client_activity(&inst, 0) == 0 && ua->timer_alloc_count == timers);
assert(sent[0].count == 2 && sent[1].count == 2 && sent[2].count == 2);
uasync_poll(ua, -1); assert(standby_get_sleep_tb() > 0);
for (int i = 0; i < 3; i++) {
etcp_keepalive_link_ready(&links[i]);
assert(sent[i].flags == KA_FLAG_SLEEP && sent[i].active == 0 && sent[i].sleep > 0 && !links[i].keepalive_timer);
}
assert(links[0].ka_sleep_announce_timer && links[1].ka_sleep_announce_timer && !links[2].ka_sleep_announce_timer);
timers = ua->timer_alloc_count; wait_after_packets = sent[0].count;
assert(standby_wait(NULL, waited)); assert(ua->timer_alloc_count == timers);
fail_phase_timer = 1;
standby_notify_network_activity();
assert(standby_get_sleep_tb() > 0 && wait_calls == 0 && sent[0].count == wait_after_packets);
fail_phase_timer = 0;
standby_notify_network_activity(); assert(wait_calls == 1);
uasync_poll(ua, -1); assert(standby_get_sleep_tb() > 0);
wait_after_packets = sent[0].count;
void* cancelled = standby_wait(NULL, waited); assert(cancelled);
standby_wait_cancel(cancelled);
assert(standby_wait(NULL, waited));
uasync_poll(ua, -1); assert(wait_calls == 2 && standby_get_active_remaining_tb() > 0);
uasync_poll(ua, -1); assert(standby_get_sleep_tb() > 0);
wait_after_packets = sent[0].count; assert(standby_wait(NULL, waited));
links[0].ka_peer_sleeping = 1;
assert(utun_set_client_activity(&inst, 1) == 0 && activity_events == 1 && wait_calls == 3);
for (int i = 0; i < 3; i++) {
assert(!links[i].ka_my_sleeping && sent[i].flags == 0 && !links[i].ka_sleep_announce_timer);
}
assert(!links[0].keepalive_timer && links[1].keepalive_timer && links[2].keepalive_timer);
timers = ua->timer_alloc_count; unsigned packets = sent[0].count;
assert(utun_set_client_activity(&inst, 1) == 0 && ua->timer_alloc_count == timers);
assert(activity_events == 1 && sent[0].count == packets);
assert(utun_set_client_activity(&inst, 0) == 0 && activity_events == 2);
teardown();
DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[PASS] first background, repeated commands, UDP/TCP ready, phase-before-work, peer sleep and cleanup");
}
/* Ошибка ENTER сохраняет normal; потеря фазового таймера возвращает normal, без зависания сна. */
static void check_timer_failures(void) {
setup(); assert(utun_set_client_activity(&inst, 1) == 0);
fail_phase_timer = 1;
assert(utun_set_client_activity(&inst, 0) < 0);
assert(inst.client_activity == CLIENT_ACTIVITY_ACTIVE && !standby_is_enabled() && activity_events == 1);
for (int i = 0; i < 3; i++) assert(!links[i].ka_my_sleeping && links[i].keepalive_timer);
fail_phase_timer = 0;
assert(utun_set_client_activity(&inst, 0) == 0);
fail_phase_timer = 1;
uasync_poll(ua, -1);
assert(!standby_is_enabled() && inst.client_activity == CLIENT_ACTIVITY_ACTIVE && activity_events == 3);
for (int i = 0; i < 3; i++) assert(!links[i].ka_my_sleeping && links[i].keepalive_timer);
fail_phase_timer = 0; teardown();
DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "[PASS] timer failure rollback and safe normal fallback");
}
int main(void) {
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
check_transitions(); check_transitions(); check_timer_failures();
DEBUG_INFO(DEBUG_CATEGORY_KEEPALIVE, "PASS: standby transport regression tests");
return 0;
}

23
tools/chatgui-android/app/src/main/AndroidManifest.xml

@ -6,11 +6,13 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
@ -24,7 +26,9 @@
<application
android:name=".ChatApplication"
android:allowBackup="true"
android:label="uTun Chat"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:label="vibechat"
android:supportsRtl="true"
android:theme="@style/Theme.UTun">
@ -58,6 +62,23 @@
android:value="ptt_overlay_service" />
</service>
<service
android:name=".data.CallService"
android:exported="false"
android:foregroundServiceType="phoneCall|microphone" />
<service
android:name=".data.UtunConnectionService"
android:exported="true"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
<meta-data
android:name="android.telecom.INCLUDE_SELF_MANAGED_CALLS"
android:value="true" />
</service>
<receiver
android:name=".data.IncomingCallActionReceiver"
android:exported="false" />

21
tools/chatgui-android/app/src/main/cpp/CMakeLists.txt

@ -28,6 +28,12 @@ list(APPEND UTUN_INCLUDE_DIRS "${LIB_DIR}/libopus" "${LIB_DIR}/libopus/include"
# Opus requires these to be defined (or HAVE_CONFIG_H + config.h with VAR_ARRAYS)
list(APPEND UTUN_DEFINES OPUS_BUILD VAR_ARRAYS USE_ALLOCA)
# SpeexDSP (акустическое эхоподавление AEC) — обёртка speex_aec.c уже в lib/*.c GLOB.
file(GLOB _SPEEXDSP_SRC "${LIB_DIR}/speexdsp/*.c")
list(APPEND UTUN_SOURCES ${_SPEEXDSP_SRC})
list(APPEND UTUN_INCLUDE_DIRS "${LIB_DIR}/speexdsp" "${LIB_DIR}/speexdsp/include")
list(APPEND UTUN_DEFINES FLOATING_POINT USE_KISS_FFT "EXPORT=")
# Голосовой стек звонка (SoundTouch + voice_jitter + call_audio + call_tones),
# C++ внутри, C-интерфейс наружу (call_audio.h).
set(CALL_DIR ${SRC_DIR}/call)
@ -50,12 +56,19 @@ set_target_properties(openssl_crypto PROPERTIES
IMPORTED_LOCATION "${OPENSSL_DIR}/lib/libcrypto.a"
)
# ONNX Runtime (для Silero VAD) — prebuilt .so в jniLibs/<abi>, заголовки в cpp/onnxruntime/include
set(ORT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/onnxruntime)
add_library(onnxruntime SHARED IMPORTED)
set_target_properties(onnxruntime PROPERTIES
IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libonnxruntime.so"
)
# OBJECT library: compile .c once, link into SHARED
add_library(utun_core OBJECT ${UTUN_SOURCES})
target_include_directories(utun_core PRIVATE
${UTUN_INCLUDE_DIRS} ${JNI_DIR} ${OPENSSL_DIR}/include
${UTUN_INCLUDE_DIRS} ${JNI_DIR} ${OPENSSL_DIR}/include ${ORT_DIR}/include
)
target_compile_definitions(utun_core PRIVATE ${UTUN_DEFINES} __ANDROID__)
target_compile_definitions(utun_core PRIVATE ${UTUN_DEFINES} __ANDROID__ HAVE_SILERO_VAD)
set_target_properties(utun_core PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON)
# JNI bridge + link into shared library
@ -66,10 +79,10 @@ add_library(utun SHARED
)
target_include_directories(utun PRIVATE
${UTUN_INCLUDE_DIRS} ${JNI_DIR} ${OPENSSL_DIR}/include
${UTUN_INCLUDE_DIRS} ${JNI_DIR} ${OPENSSL_DIR}/include ${ORT_DIR}/include
)
target_compile_definitions(utun PRIVATE ${UTUN_DEFINES} __ANDROID__)
set_target_properties(utun PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON)
find_library(log-lib log)
target_link_libraries(utun ${log-lib} openssl_crypto utun_voice)
target_link_libraries(utun ${log-lib} openssl_crypto utun_voice onnxruntime)

19
tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/cpu_provider_factory.h

@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include "onnxruntime_c_api.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* \param use_arena zero: false. non-zero: true.
*/
ORT_EXPORT
ORT_API_STATUS(OrtSessionOptionsAppendExecutionProvider_CPU, _In_ OrtSessionOptions* options, int use_arena)
ORT_ALL_ARGS_NONNULL;
#ifdef __cplusplus
}
#endif

62
tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/nnapi_provider_factory.h

@ -0,0 +1,62 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "onnxruntime_c_api.h"
// NNAPIFlags are bool options we want to set for NNAPI EP
// This enum is defined as bit flags, and cannot have negative value
// To generate an uint32_t nnapi_flags for using with OrtSessionOptionsAppendExecutionProvider_Nnapi below,
// uint32_t nnapi_flags = 0;
// nnapi_flags |= NNAPI_FLAG_USE_FP16;
enum NNAPIFlags {
NNAPI_FLAG_USE_NONE = 0x000,
// Using fp16 relaxation in NNAPI EP, this may improve perf but may also reduce precision
NNAPI_FLAG_USE_FP16 = 0x001,
// Use NCHW layout in NNAPI EP, this is only available after Android API level 29
// Please note for now, NNAPI perform worse using NCHW compare to using NHWC
NNAPI_FLAG_USE_NCHW = 0x002,
// Prevent NNAPI from using CPU devices.
//
// NNAPI is more efficient using GPU or NPU for execution, and NNAPI might fall back to its own CPU implementation
// for operations not supported by GPU/NPU. The CPU implementation of NNAPI (which is called nnapi-reference)
// might be less efficient than the optimized versions of the operation of ORT. It might be advantageous to disable
// the NNAPI CPU fallback and handle execution using ORT kernels.
//
// For some models, if NNAPI would use CPU to execute an operation, and this flag is set, the execution of the
// model may fall back to ORT kernels.
//
// This option is only available after Android API level 29, and will be ignored for Android API level 28-
//
// For NNAPI device assignments, see https://developer.android.com/ndk/guides/neuralnetworks#device-assignment
// For NNAPI CPU fallback, see https://developer.android.com/ndk/guides/neuralnetworks#cpu-fallback
//
// Please note, the NNAPI EP will return error status if both NNAPI_FLAG_CPU_DISABLED
// and NNAPI_FLAG_CPU_ONLY flags are set
NNAPI_FLAG_CPU_DISABLED = 0x004,
// Using CPU only in NNAPI EP, this may decrease the perf but will provide
// reference output value without precision loss, which is useful for validation
//
// Please note, the NNAPI EP will return error status if both NNAPI_FLAG_CPU_DISABLED
// and NNAPI_FLAG_CPU_ONLY flags are set
NNAPI_FLAG_CPU_ONLY = 0x008,
// Keep NNAPI_FLAG_LAST at the end of the enum definition
// And assign the last NNAPIFlag to it
NNAPI_FLAG_LAST = NNAPI_FLAG_CPU_ONLY,
};
#ifdef __cplusplus
extern "C" {
#endif
ORT_EXPORT ORT_API_STATUS(OrtSessionOptionsAppendExecutionProvider_Nnapi,
_In_ OrtSessionOptions* options, uint32_t nnapi_flags);
#ifdef __cplusplus
}
#endif

7429
tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_c_api.h

File diff suppressed because it is too large Load Diff

3224
tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_cxx_api.h

File diff suppressed because it is too large Load Diff

3547
tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_cxx_inline.h

File diff suppressed because it is too large Load Diff

988
tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_ep_c_api.h

@ -0,0 +1,988 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
// Do not include this file directly. Please include "onnxruntime_c_api.h" instead.
#ifdef __cplusplus
extern "C" {
#endif
ORT_RUNTIME_CLASS(Ep);
ORT_RUNTIME_CLASS(EpFactory);
ORT_RUNTIME_CLASS(EpGraphSupportInfo);
ORT_RUNTIME_CLASS(MemoryDevice); // opaque class to wrap onnxruntime::OrtDevice
ORT_RUNTIME_CLASS(NodeComputeContext);
ORT_RUNTIME_CLASS(DataTransferImpl);
ORT_RUNTIME_CLASS(SyncNotificationImpl);
ORT_RUNTIME_CLASS(SyncStreamImpl);
// struct that an EP implements for IDataTransfer to copy between devices it uses and CPU
struct OrtDataTransferImpl {
uint32_t ort_version_supported; ///< Must be initialized to ORT_API_VERSION
/** \brief Release the OrtDataTransferImpl instance.
*
* This is called by ORT when the OrtDataTransferImpl instance is no longer needed.
* The implementation should release any resources held by the instance.
*
* \param[in] this_ptr Pointer to the OrtDataTransferImpl instance.
*
* \since Version 1.23.
*/
ORT_API_T(void, Release, _In_ OrtDataTransferImpl* this_ptr);
/** \brief Check if the implementation can copy between the source and destination memory devices.
*
* \param[in] this_ptr Pointer to the OrtDataTransferImpl instance.
* \param[in] src_memory_device Source OrtMemoryDevice to copy from.
* \param[in] dst_memory_device Destination OrtMemoryDevice to copy to.
* \return True if the implementation can copy between the devices.
*
* \since Version 1.23.
*/
ORT_API_T(bool, CanCopy, _In_ const OrtDataTransferImpl* this_ptr,
_In_ const OrtMemoryDevice* src_memory_device, _In_ const OrtMemoryDevice* dst_memory_device);
/** \brief Copy tensors from src_tensors to dst_tensors using the provided streams.
*
* The implementation can use the provided streams to perform asynchronous copies if supported.
* If a stream is not available, the copy is performed synchronously.
*
* \param[in] this_ptr Pointer to the OrtDataTransferImpl instance.
* \param[in] src_tensors Array of source OrtValue pointers to copy from.
* \param[in] dst_tensors Array of destination OrtValue pointers to copy to.
* \param[in] streams Array of OrtSyncStream pointers for the copy operations, if the execution provider is stream
* aware. nullptr if it is not.
* \param[in] num_tensors Number of tensors to copy.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(CopyTensors, _In_ OrtDataTransferImpl* this_ptr,
_In_reads_(num_tensors) const OrtValue** src_tensors,
_In_reads_(num_tensors) OrtValue** dst_tensors,
_In_reads_(num_tensors) OrtSyncStream** streams,
_In_ size_t num_tensors);
};
/** \brief Struct that an EP implements for Stream Notifications.
*
* \since Version 1.23.
*/
struct OrtSyncNotificationImpl {
uint32_t ort_version_supported; ///< Must be initialized to ORT_API_VERSION
/** \brief Release the OrtSyncNotificationImpl instance.
*
* This is called by ORT when the OrtSyncNotificationImpl instance is no longer needed.
* The implementation should release any resources held by the instance.
*
* \param[in] this_ptr Pointer to the OrtSyncNotificationImpl instance.
*
* \since Version 1.23.
*/
ORT_API_T(void, Release, _In_ OrtSyncNotificationImpl* this_ptr);
/** \brief Called by ORT to activate the notification.
*
* \param[in] this_ptr Pointer to the OrtSyncNotificationImpl instance.
*
* \since Version 1.23.
*/
ORT_API2_STATUS(Activate, _In_ OrtSyncNotificationImpl* this_ptr);
/** \brief Wait for a device to device operation to complete.
*
* \param[in] this_ptr Pointer to the OrtSyncNotificationImpl instance.
* \param[in] stream The OrtSyncStream instance that will wait on this notification to be activated.
*
* \since Version 1.23.
*/
ORT_API2_STATUS(WaitOnDevice, _In_ OrtSyncNotificationImpl* this_ptr, _In_ OrtSyncStream* consumer_stream);
/** \brief Wait for a device to host operation to complete.
*
* \param[in] this_ptr Pointer to the OrtSyncNotificationImpl instance.
*
* \since Version 1.23.
*/
ORT_API2_STATUS(WaitOnHost, _In_ OrtSyncNotificationImpl* this_ptr);
};
/** \brief Struct that an EP implements if it wishes to implement Stream support.
*
* This struct provides the overrides for onnxruntime::Stream's virtual methods.
*
* \since Version 1.23.
*/
struct OrtSyncStreamImpl {
uint32_t ort_version_supported; ///< Must be initialized to ORT_API_VERSION
/** \brief Release the OrtSyncStreamImpl instance.
*
* This is called by ORT when the OrtSyncStreamImpl instance is no longer needed.
* The implementation should release any resources held by the instance.
*
* \param[in] this_ptr Pointer to the OrtSyncStreamImpl instance.
*
* \since Version 1.23.
*/
ORT_API_T(void, Release, _In_ OrtSyncStreamImpl* this_ptr);
/** \brief Get the handle of the stream.
*
* This returns the native handle for the stream. e.g. cudaStream_t for CUDA streams.
*
* \param[in] this_ptr Pointer to the OrtSyncStreamImpl instance.
* \return The handle of the stream.
*
* \since Version 1.23.
*/
ORT_API_T(void*, GetHandle, _In_ OrtSyncStreamImpl* this_ptr);
/** \brief Create an OrtSyncNotificationImpl for the OrtSyncStreamImpl instance.
*
* \param[in] this_ptr Pointer to the OrtSyncStreamImpl instance
* \param[out] notification The new OrtSyncNotificationImpl instance.
*
* \since Version 1.23.
*/
ORT_API2_STATUS(CreateNotification, _In_ OrtSyncStreamImpl* this_ptr,
_Outptr_ OrtSyncNotificationImpl** notification);
/** \brief Flush the stream.
*
* This is called by ORT to flush the stream, ensuring that all operations submitted to the stream are completed.
*
* \param[in] this_ptr Pointer to the OrtSyncStreamImpl instance.
*
* \since Version 1.23.
*/
ORT_API2_STATUS(Flush, _In_ OrtSyncStreamImpl* this_ptr);
/** \brief Notify the stream that a session run has ended.
*
* This is called by ORT to notify the stream that a session run has ended, allowing the stream to perform any
* necessary cleanup or finalization.
*
* \param[in] this_ptr Pointer to the OrtSyncStreamImpl instance.
*
* \since Version 1.23.
*/
ORT_API2_STATUS(OnSessionRunEnd, _In_ OrtSyncStreamImpl* this_ptr);
};
struct OrtNodeFusionOptions;
typedef struct OrtNodeFusionOptions OrtNodeFusionOptions;
struct OrtNodeComputeInfo;
typedef struct OrtNodeComputeInfo OrtNodeComputeInfo;
/**
* \brief The OrtNodeFusionOptions struct specifies options for fusing nodes supported by an execution provider.
*
* Refer to OrtEpApi::EpGraphSupportInfo_AddNodesToFuse.
*
* \since Version 1.23.
*/
struct OrtNodeFusionOptions {
/** \brief The ONNX Runtime version the OrtNodeFusionOptions was compiled with.
*
* Implementation should set to ORT_API_VERSION.
* ORT will use this to ensure it does not use members that were not available when the EP library was compiled.
*
* \since Version 1.23.
*/
uint32_t ort_version_supported;
/** \brief If set to true, specify that the execution provider does not require ONNX Runtime to provide constant
* initializers as inputs to the fused node during model inference. This is used when the execution
* provider saves a copy of constant initializers, and allows ONNX Runtime to release constant initializers that
* are not used by any execution provider.
*
* If not specified, defaults to false. That is, ONNX Runtime provides constant initializers as inputs to
* the fused node by default.
*
* \since Version 1.23.
*/
bool drop_constant_initializers;
// const OrtNode* fused_node_schema;
};
/**
* \brief The OrtNodeComputeInfo struct provides functions that an OrtEp implements to specify the compute
* function for a compiled OrtGraph instance.
* \since Version 1.23.
*/
struct OrtNodeComputeInfo {
/** \brief The ONNX Runtime version the OrtNodeComputeInfo was compiled with.
*
* Implementation should set to ORT_API_VERSION.
* ORT will use this to ensure it does not call functions that were not available when the EP library was compiled.
*
* \since Version 1.23.
*/
uint32_t ort_version_supported;
/** \brief Creates an opaque compute state object that is then passed to the Compute() function during inference.
* \param[in] this_ptr The OrtNodeComputeInfo instance.
* \param[in] compute_context OrtNodeComputeContext instance that contains compiled/fused node's name and host
* memory allocation functions. Can optionally be used to build the compute state.
* \param[out] compute_state Output parameter that is assigned the opaque computation state. ONNX Runtime calls
* ReleaseState() (after calling Compute()) to allow the implementer to release the
* compute state.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
OrtStatus*(ORT_API_CALL* CreateState)(_In_ OrtNodeComputeInfo* this_ptr,
_In_ OrtNodeComputeContext* compute_context,
_Outptr_ void** compute_state);
/** \brief Computation function called to execute the fused node compiled by an OrtEp instance.
* \param[in] this_ptr The OrtNodeComputeInfo instance.
* \param[in] compute_state The opaque computation state returned by CreateState().
* \param[in] kernel_context The OrtKernelContext instance used to access inputs/outputs.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
OrtStatus*(ORT_API_CALL* Compute)(_In_ OrtNodeComputeInfo* this_ptr, _In_ void* compute_state,
_In_ OrtKernelContext* kernel_context);
/** \brief Releases the compute state returned by CreateState().
* \param[in] this_ptr The OrtNodeComputeInfo instance.
* \param[inout] compute_state The opaque compute state returned by CreateState().
*
* \since Version 1.23.
*/
void(ORT_API_CALL* ReleaseState)(_In_ OrtNodeComputeInfo* this_ptr, _Frees_ptr_opt_ void* compute_state);
};
struct OrtEpApi {
/** \brief Create an OrtEpDevice for the EP and an OrtHardwareDevice.
* \param[in] ep_factory Execution provider factory that is creating the instance.
* \param[in] hardware_device Hardware device that the EP can utilize.
* \param[in] ep_metadata Optional OrtKeyValuePairs instance for execution provider metadata that may be used
* during execution provider selection and passed to CreateEp.
* ep_device will copy this instance and the user should call ReleaseKeyValuePairs.
* \param[in] ep_options Optional OrtKeyValuePairs instance for execution provider options that will be added
* to the Session configuration options if the execution provider is selected.
* ep_device will copy this instance and the user should call ReleaseKeyValuePairs.
* \param ep_device OrtExecutionDevice that is created.
*
* \since Version 1.22.
*/
ORT_API2_STATUS(CreateEpDevice, _In_ OrtEpFactory* ep_factory,
_In_ const OrtHardwareDevice* hardware_device,
_In_opt_ const OrtKeyValuePairs* ep_metadata,
_In_opt_ const OrtKeyValuePairs* ep_options,
_Out_ OrtEpDevice** ep_device);
ORT_CLASS_RELEASE(EpDevice);
/** \brief Specify nodes that are supported by an OrtEp and should be fused into one node.
*
* Because the nodes will be fused into one "fused node", there must not exist an unsupported node in
* a path between two of the provided nodes. Otherwise, the graph will become invalid.
*
* This function can be called multiple times. A subsequent call to this function will force the next set of
* nodes to be fused into a different node.
*
* \param[in] graph_support_info OrtEpGraphSupportInfo instance to which to add the supported nodes.
* \param[in] nodes Array of nodes supported by the EP that should be fused/compiled.
* \param[in] num_nodes The number of supported nodes.
* \param[in] node_fusion_options Optional node fusion options. Ignored if set to NULL.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(EpGraphSupportInfo_AddNodesToFuse, _In_ OrtEpGraphSupportInfo* graph_support_info,
_In_reads_(num_nodes) const OrtNode* const* nodes, _In_ size_t num_nodes,
_In_opt_ const OrtNodeFusionOptions* node_fusion_options);
/** \brief Specify a node that is supported by an OrtEp and should be run with a registered EP kernel.
*
* \param[in] graph_support_info OrtEpGraphSupportInfo instance to which to add the supported node.
* \param[in] node The supported OrtNode instance.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(EpGraphSupportInfo_AddSingleNode, _In_ OrtEpGraphSupportInfo* graph_support_info,
_In_ const OrtNode* node);
/** \brief Query a OrtNodeComputeContext for the name of the node that encapsulates the compiled/fused node.
*
* Used in OrtNodeComputeInfo::CreateComputeState().
*
* \param[in] context The OrtNodeComputeContext instance to query.
* \return The node's name.
*
* \note Returned string is owned by ORT and valid only while OrtNodeComputeInfo::CreateComputeState() is called.
*
* \since Version 1.23.
*/
ORT_API_T(const char*, NodeComputeContext_NodeName, _In_ const OrtNodeComputeContext* context);
/** \brief Register an allocator with the OrtEpDevice.
*
* This allows an EP to provide OrtMemoryInfo for DEFAULT and HOST_ACCESSIBLE memory type as needed.
* The registered values will be used in calls to OrtEpFactory::CreateAllocator to ensure the required allocator/s
* are available for EP usage.
*
* Multiple calls for the same entry type will replace a previous entry.
*
* Available entries:
* - OrtDeviceAllocator with type of OrtDeviceMemoryType_DEFAULT
* - OrtDeviceAllocator with type of OrtDeviceMemoryType_HOST_ACCESSIBLE
* - OrtReadOnlyAllocator with type of OrtDeviceMemoryType_DEFAULT
* - if provided this allocator will only be used to copy initializers to the device the EP uses.
* ORT will use the OrtDeviceAllocator if not provided.
*
* \param[in] ep_device The OrtEpDevice instance to register the OrtMemoryInfo with.
* \param[in] allocator_memory_info The OrtMemoryInfo information for the allocator.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(EpDevice_AddAllocatorInfo, _In_ OrtEpDevice* ep_device,
_In_ const OrtMemoryInfo* allocator_memory_info);
/** \brief Get the OrtMemoryDevice from an OrtMemoryInfo instance.
*
* This is required for OrtDataTransferImpl (which implements onnxruntime::IDataTransfer) where the OrtMemoryDevice
* is used in the CanCopy and CopyTensors functions.
*
* \param[in] memory_info The OrtMemoryInfo instance to get the memory device from.
* \return The OrtMemoryDevice associated with the OrtMemoryInfo instance.
*
* \since Version 1.23.
*/
ORT_API_T(const OrtMemoryDevice*, MemoryInfo_GetMemoryDevice, _In_ const OrtMemoryInfo* memory_info);
/** \brief Get the OrtMemoryDevice from an OrtValue instance if it contains a Tensor.
*
* \param[in] value The OrtValue instance to get the memory device from.
* \return Memory device if OrtValue contains a Tensor, nullptr otherwise.
*
* \since Version 1.23.
*/
ORT_API_T(const OrtMemoryDevice*, Value_GetMemoryDevice, _In_ const OrtValue* value);
/** \brief Compare two OrtMemoryDevice instances for equality.
*
* This is used to check if two memory devices are the same.
* Used to implement DataTransferImpl::CanCopy.
*
* \param[in] a The first OrtMemoryDevice instance to compare.
* \param[in] b The second OrtMemoryDevice instance to compare.
* \return True if the two OrtMemoryDevice instances are equal, false otherwise.
*
* \since Version 1.23.
*/
ORT_API_T(bool, MemoryDevice_AreEqual, _In_ const OrtMemoryDevice* a, _In_ const OrtMemoryDevice* b);
/** \brief Get the OrtMemoryInfoDeviceType value from an OrtMemoryDevice instance.
*
* \param[in] memory_device OrtMemoryDevice instance.
* \return The OrtMemoryInfoDeviceType value.
*
* \since Version 1.23.
*/
ORT_API_T(OrtMemoryInfoDeviceType, MemoryDevice_GetDeviceType, _In_ const OrtMemoryDevice* memory_device);
/** \brief Get the OrtDeviceMemoryType value from an OrtMemoryDevice instance.
*
* \param[in] memory_device OrtMemoryDevice instance.
* \return The OrtDeviceMemoryType value.
*
* \since Version 1.23.
*/
ORT_API_T(OrtDeviceMemoryType, MemoryDevice_GetMemoryType, _In_ const OrtMemoryDevice* memory_device);
/** \brief Get the vendor ID from an OrtMemoryDevice instance.
*
* The vendor ID is used to identify the vendor of the device, and is typically set to the PCI vendor ID.
*
* If the device is not vendor specific (e.g. CPU memory) the vendor ID is set to 0.
*
* \param[in] memory_device OrtMemoryDevice instance.
* \return The vendor ID value.
*
* \since Version 1.23.
*/
ORT_API_T(uint32_t, MemoryDevice_GetVendorId, _In_ const OrtMemoryDevice* memory_device);
/** \brief Get the device ID from an OrtMemoryDevice instance.
*
* \param[in] memory_device OrtMemoryDevice instance.
* \return The device ID.
*
* \since Version 1.23.
*/
ORT_API_T(uint32_t, MemoryDevice_GetDeviceId, _In_ const OrtMemoryDevice* memory_device);
/** \brief Get the OrtSyncStreamImpl associated with an OrtSyncStream instance.
*
* This allows an the plugin library to connect its OrtSyncStreamImpl instance with an OrtSyncStream if needed.
*
* \param[in] stream The OrtSyncStream instance to find an OrtSyncStreamImpl for.
* \return The associated OrtSyncStreamImpl if found. nullptr otherwise.
*
* \since Version 1.23.
*
* \remarks There should always be an OrtSyncStreamImpl associated with an OrtSyncStream instance that the EP gets.
*/
ORT_API_T(const OrtSyncStreamImpl*, SyncStream_GetImpl, _In_ const OrtSyncStream* stream);
/** \brief Get the current sync ID for a stream.
*
* \param[in] stream The OrtSyncStream to get the sync ID for.
* \return Current sync ID.
*
* \since Version 1.23.
*/
ORT_API_T(uint64_t, SyncStream_GetSyncId, _In_ const OrtSyncStream* stream);
/** \brief Get the sync ID for the last time the consumer_stream waited on the producer_stream.
*
* When two streams are synchronized, the sync id represents the event used in that synchronization.
*
* \param[in] producer_stream The OrtSyncStream that produced the data.
* \param[in] consumer_stream The OrtSyncStream that waited on the producer_stream.
* \return ID for last sync. 0 if no sync has occurred between the two streams.
*
* \since Version 1.23.
*/
ORT_API_T(uint64_t, GetSyncIdForLastWaitOnSyncStream,
_In_ const OrtSyncStream* producer_stream, _In_ const OrtSyncStream* consumer_stream);
};
/**
* \brief The data layout type.
*
* EPs may specify a preferred data layout type. ORT's default layout type is OrtEpDataLayout_NCHW, or
* OrtEpDataLayout_Default.
*
* \since Version 1.23.
*/
typedef enum OrtEpDataLayout {
OrtEpDataLayout_NCHW = 0,
OrtEpDataLayout_NHWC,
OrtEpDataLayout_Default = OrtEpDataLayout_NCHW,
} OrtEpDataLayout;
/**
* \brief The OrtEp struct provides functions to implement for an execution provider.
* \since Version 1.22.
*/
struct OrtEp {
/** \brief The ONNX Runtime version the execution provider was compiled with.
*
* Implementation should set to ORT_API_VERSION.
* ORT will use this to ensure it does not call functions that were not available when the library was compiled.
*
* \since Version 1.22.
*/
uint32_t ort_version_supported;
/** \brief Get the execution provider name.
*
* The returned string should be a null-terminated, UTF-8 encoded string. ORT will copy it.
*
* \param[in] this_ptr The OrtEp instance.
* \return The execution provider name.
*
* \since Version 1.22.
*/
ORT_API_T(const char*, GetName, _In_ const OrtEp* this_ptr);
/** \brief Get information about the nodes supported by the OrtEp instance.
*
* IMPORTANT: This is not the final version of this API function. This is currently experimental but will
* be stabilized by the ONNX Runtime 1.23 release.
*
* \param[in] this_ptr The OrtEp instance.
* \param[in] graph The OrtGraph instance for which to populate node support. The OrtGraph could be a nested subgraph
* contained by a node (e.g., an If or Loop node). ONNX Runtime calls this function separately
* for each nested subgraph.
* \param[inout] graph_support_info OrtEpGraphSupportInfo instance that the implementer must fill out in order to
* specify the supported nodes.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(GetCapability, _In_ OrtEp* this_ptr, _In_ const OrtGraph* graph,
_Inout_ OrtEpGraphSupportInfo* graph_support_info);
/** \brief Compile OrtGraph instances assigned to the OrtEp. Implementer must set a OrtNodeComputeInfo instance
* for each OrtGraph in order to define its computation function.
*
* If the session is configured to generate a pre-compiled model, the execution provider must return EPContext nodes,
* as OrtNode instances, that ONNX Runtime uses to create a pre-compiled model, known as an "EPContext model".
* An EPContext model contains EPContext nodes. Each EPContext node encapsulates the pre-compiled binary data for a
* OrtGraph compiled for a specific execution provider. For more details about the EPContext design, refer to:
* \htmlonly
* <a href="https://onnxruntime.ai/docs/execution-providers/EP-Context-Design.html">EPContext design document.</a>
* \endhtmlonly
*
* \param[in] this_ptr The OrtEp instance.
* \param[in] graphs Array of `count` OrtGraph instances to compile. Each graph contains only the nodes for
* which the execution provider indicated support. Nested subgraphs contained by a
* node, such as an If or Loop, have separate OrtGraph instances.
* \param[in] fused_nodes Array of `count` fused nodes that will replace the compiled graphs.
* Each fused node is an OrtNode initialized with the intended fused node name and
* input/output information.
* \param[in] count The number of OrtGraph instances to compile.
* \param[out] node_compute_infos Array of `count` OrtNodeComputeInfo instances that define each OrtGraph instance's
* computation function. The implementer allocates the OrtNodeComputeInfo instances.
* ORT calls ReleaseNodeComputeInfos() to release multiple instances in a batch.
* \param[out] ep_context_nodes Output array of `count` OrtNode instances, each representing an EPContext
* node for a compiled OrtGraph. The execution provider must use
* OrtModelEditorApi::CreateNode to create the OrtNode instances. ONNX Runtime takes
* ownership of the OrtNode instances, so the execution provider must NOT call
* OrtApi::ReleaseNode. Should be ignored if the session is not configured to generate an
* EPContext model.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \note Do NOT cache the provided OrtGraph instances in any of the OrtNodeComputeInfo functions because the
* graphs are only valid for the duration of the call to Compile. Any graph/node/input/output
* names that are needed by the OrtNodeComputeInfo functions must be copied and stored by the OrtEp.
*
* \since Version 1.23.
*/
ORT_API2_STATUS(Compile, _In_ OrtEp* this_ptr, _In_ const OrtGraph** graphs,
_In_ const OrtNode** fused_nodes, _In_ size_t count,
_Out_writes_all_(count) OrtNodeComputeInfo** node_compute_infos,
_Out_writes_(count) OrtNode** ep_context_nodes);
/** \brief Release OrtNodeComputeInfo instances.
*
* \param[in] this_ptr The OrtEp instance.
* \param[inout] node_compute_infos The OrtNodeComputeInfo instances to release.
* \param[in] num_node_compute_infos The number of OrtNodeComputeInfo instances.
*
* \since Version 1.23.
*/
ORT_API_T(void, ReleaseNodeComputeInfos, _In_ OrtEp* this_ptr,
OrtNodeComputeInfo** node_compute_infos,
_In_ size_t num_node_compute_infos);
/** \brief Get the EP's preferred data layout.
*
* \note Implementation of this function is optional.
* If not implemented, ORT will assume that this EP prefers the data layout `OrtEpDataLayout::NCHW`.
*
* \param[in] this_ptr The OrtEp instance.
* \param[out] preferred_data_layout The EP's preferred data layout.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(GetPreferredDataLayout, _In_ OrtEp* this_ptr, _Out_ OrtEpDataLayout* preferred_data_layout);
/** \brief Given an op with domain `domain` and type `op_type`, determine whether an associated node's data layout
* should be converted to `target_data_layout`.
* If the EP prefers a non-default data layout (see `GetPreferredDataLayout()`), this function will be called
* during layout transformation with `target_data_layout` set to the EP's preferred data layout.
*
* \note Implementation of this function is optional.
* If an EP prefers a non-default data layout, it may implement this to customize the specific op data layout
* preferences at a finer granularity.
*
* \param[in] this_ptr The OrtEp instance.
* \param[in] domain The op domain. An empty string means the ONNX domain.
* \param[in] op_type The op type.
* \param[in] target_data_layout The target data layout.
* \param[out] should_convert Whether the associated node's data layout should be converted to `target_data_layout`.
* If greater than 0, convert.
* If 0, don't convert.
* Otherwise, if less than 0, leave the decision to ORT.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(ShouldConvertDataLayoutForOp, _In_ OrtEp* this_ptr,
_In_z_ const char* domain, _In_z_ const char* op_type,
_In_ OrtEpDataLayout target_data_layout,
_Outptr_ int* should_convert);
/** \brief Set dynamic options on this EP.
*
* Dynamic options can be set by the user at any time after session creation with `OrtApi::SetEpDynamicOptions()`.
*
* \param[in] this_ptr The OrtEp instance.
* \param[in] option_keys The dynamic option keys.
* \param[in] option_values The dynamic option values.
* \param[in] num_options The number of dynamic options.
*
* \note Implementation of this function is optional.
* An EP should only implement this if it needs to handle any dynamic options.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(SetDynamicOptions, _In_ OrtEp* this_ptr,
_In_reads_(num_options) const char* const* option_keys,
_In_reads_(num_options) const char* const* option_values,
_In_ size_t num_options);
/** \brief Called by ORT to notify the EP of the start of a run.
*
* \param[in] this_ptr The OrtEp instance.
* \param[in] run_options The run options for this run.
*
* \note Implementation of this function is optional.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(OnRunStart, _In_ OrtEp* this_ptr, _In_ const OrtRunOptions* run_options);
/** \brief Called by ORT to notify the EP of the end of a run.
*
* \param[in] this_ptr The OrtEp instance.
* \param[in] run_options The run options for this run.
* \param[in] sync_stream Whether any associated stream should be synchronized during this call.
* Only applicable if there is such a stream.
*
* \note Implementation of this function is optional.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(OnRunEnd, _In_ OrtEp* this_ptr, _In_ const OrtRunOptions* run_options, _In_ bool sync_stream);
/** \brief Create an OrtAllocator for the given OrtMemoryInfo for an OrtSession.
*
* The OrtMemoryInfo instance will match one of the values set in the OrtEpDevice using EpDevice_AddAllocatorInfo.
* Any allocator specific options should be read from the session options.
*
* If nullptr OrtEpFactory::CreateAllocator will be used.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \param[in] memory_info The OrtMemoryInfo to create the allocator for. May be nullptr.
* \param[out] allocator The created OrtAllocator instance. Set to nullptr if the default CPU allocator is used.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(CreateAllocator, _In_ OrtEp* this_ptr,
_In_ const OrtMemoryInfo* memory_info,
_Outptr_result_maybenull_ OrtAllocator** allocator);
/** \brief Create a synchronization stream for the given memory device for an OrtSession.
*
* This is used to create a synchronization stream for the execution provider and is used to synchronize
* operations on the device during model execution.
* Any stream specific options should be read from the session options.
*
* If nullptr OrtEpFactory::CreateSyncStreamForDevice will be used.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \param[in] memory_device The OrtMemoryDevice to create the synchronization stream for.
* \param[out] stream The created OrtSyncStreamImpl instance. nullptr if the execution provider is not stream aware.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(CreateSyncStreamForDevice, _In_ OrtEp* this_ptr,
_In_ const OrtMemoryDevice* memory_device,
_Outptr_ OrtSyncStreamImpl** stream);
/** \brief Get a string with details about the EP stack used to produce a compiled model.
*
* This function gets a compatibility information string that contains details about the execution provider
* used to compile a given model. This string can later be used with ValidateCompiledModelCompatibilityInfo
* to determine if a compiled model is compatible with the EP.
*
* The returned string should be a null-terminated, UTF-8 encoded string. ORT will copy it.
*
* \param[in] this_ptr The OrtEp instance.
* \param[in] graph The OrtGraph instance for which to generate compatibility information.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API_T(const char*, GetCompiledModelCompatibilityInfo, _In_ OrtEp* this_ptr,
_In_ const OrtGraph* graph);
};
/** \brief The function signature that ORT will call to create OrtEpFactory instances.
*
* This must be available in a function called 'CreateEpFactories' in the execution provider library.
*
* \param[in] registered_name The name the execution library is registered with by RegisterExecutionProviderLibrary
* \param[in] ort_api_base The OrtApiBase instance that is used by the factory to get the OrtApi instance for the
* version of ORT that the library was compiled against.
* \param[in] default_logger The default ORT logger that can be used for logging outside of an inference session.
* \param[in,out] factories The implementation should create and add OrtEpFactory instances to this
* pre-allocated array.
* i.e. usage is `factories[0] = new MyEpFactory();`
* \param[in] max_factories The maximum number of OrtEpFactory instances that can be added to `factories`.
* Current default is to allow 4 factories. This can be increased in the future if needed.
* \param[out] num_factories The number of OrtEpFactory instances created by the factory and added to `factories`.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.22.
*/
typedef OrtStatus* (*CreateEpApiFactoriesFn)(_In_ const char* registered_name, _In_ const OrtApiBase* ort_api_base,
_In_ const OrtLogger* default_logger,
_Inout_ OrtEpFactory** factories, _In_ size_t max_factories,
_Out_ size_t* num_factories);
/** \brief The function signature that ORT will call to release an OrtEpFactory instance.
*
* This must be available in a function called 'ReleaseEpFactory' in the execution provider library.
*
* \param[in] factory The OrtEpFactory instance to release.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.22.
*/
typedef OrtStatus* (*ReleaseEpApiFactoryFn)(_In_ OrtEpFactory* factory);
/**
* \brief The OrtEpFactory provides functions to create and manage execution providers.
* \since Version 1.22.
*/
struct OrtEpFactory {
/** \brief The ONNX Runtime version the execution provider was compiled with.
*
* Implementation should set to ORT_API_VERSION.
* ORT will use this to ensure it does not call functions that were not available when the library was compiled.
*
* \since Version 1.22.
*/
uint32_t ort_version_supported;
/** \brief Get the name of the execution provider that the factory creates.
*
* The returned string should be a null-terminated, UTF-8 encoded string. ORT will copy it.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \return The name of the execution provider the factory creates.
*
* \since Version 1.22.
*/
ORT_API_T(const char*, GetName, const OrtEpFactory* this_ptr);
/** \brief Get the name of vendor who owns the execution provider that the factory creates.
*
* The returned string should be a null-terminated, UTF-8 encoded string. ORT will copy it.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \return vendor The vendor name of the execution provider the factory creates.
*
* \since Version 1.22.
*/
ORT_API_T(const char*, GetVendor, const OrtEpFactory* this_ptr); // return EP vendor
/** \brief Get information from the execution provider about OrtHardwareDevice support.
*
* \param[in] this_ptr The OrtEpFactory instance.
* Non-const as the factory is passed through to the CreateEp call via the OrtEpDevice.
* \param[in] devices The OrtHardwareDevice instances that are available.
* \param[in] num_devices The number of OrtHardwareDevice instances.
* \param[out] ep_devices OrtEpDevice instances for each OrtHardwareDevice that the EP can use.
* The implementation should call OrtEpApi::CreateEpDevice to create, and add the OrtEpDevice
* instances to this pre-allocated array. ORT will take ownership of the values returned.
* i.e. usage is `ep_devices[0] = <ptr to OrtEpDevice created with OrtEpApi::CreateEpDevice>;`
* \param[in] max_ep_devices The maximum number of OrtEpDevices that can be added to ep_devices.
* Current default is 8. This can be increased if needed.
* \param[out] num_ep_devices The number of EP devices added to ep_devices.
* \return true if the factory can create an execution provider that uses `device`.
*
* \since Version 1.22.
*/
ORT_API2_STATUS(GetSupportedDevices, _In_ OrtEpFactory* this_ptr,
_In_reads_(num_devices) const OrtHardwareDevice* const* devices,
_In_ size_t num_devices,
_Inout_ OrtEpDevice** ep_devices,
_In_ size_t max_ep_devices,
_Out_ size_t* num_ep_devices);
/** \brief Function to create an OrtEp instance for use in a Session.
*
* ORT will call ReleaseEp to release the instance when it is no longer needed.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \param[in] devices The OrtHardwareDevice instances that the execution provider was selected to use.
* May be a subset of the OrtHardwareDevice instances that the execution provider's factory
* set as supported in the call to OrtEpFactory::GetSupportedDevices.
* \param[in] ep_metadata_pairs Execution provider metadata that was provided to OrtEpApi::CreateEpDevice, for each
* device.
* \param[in] num_devices The number of devices the execution provider was selected for.
* \param[in] session_options The OrtSessionOptions instance that contains the configuration options for the
* session. This will include ep_options from GetSupportedDevices as well as any
* user provided overrides.
* Execution provider options will have been added with a prefix of 'ep.[ep name].'.
* The OrtSessionOptions instance will NOT be valid after this call and should not be
* stored for later use.
* \param[in] logger The OrtLogger instance for the session that the execution provider should use for logging.
* \param[out] ep The OrtEp instance created by the factory.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.22.
*/
ORT_API2_STATUS(CreateEp, _In_ OrtEpFactory* this_ptr,
_In_reads_(num_devices) const OrtHardwareDevice* const* devices,
_In_reads_(num_devices) const OrtKeyValuePairs* const* ep_metadata_pairs,
_In_ size_t num_devices,
_In_ const OrtSessionOptions* session_options,
_In_ const OrtLogger* logger, _Outptr_ OrtEp** ep);
/** \brief Release the OrtEp instance.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \param[in] ep The OrtEp instance to release.
*
* \since Version 1.22.
*/
ORT_API_T(void, ReleaseEp, OrtEpFactory* this_ptr, struct OrtEp* ep);
/** \brief Get the vendor id who owns the execution provider that the factory creates.
*
* This is typically the PCI vendor ID. See https://pcisig.com/membership/member-companies
*
* \param[in] this_ptr The OrtEpFactory instance.
* \return vendor_id The vendor ID of the execution provider the factory creates.
*
* \since Version 1.23.
*/
ORT_API_T(uint32_t, GetVendorId, const OrtEpFactory* this_ptr);
/** \brief Get the version of the execution provider that the factory creates.
*
* The version string should adhere to the Semantic Versioning 2.0 specification
* (https://github.com/semver/semver/blob/v2.0.0/semver.md).
*
* The returned string should be a null-terminated, UTF-8 encoded string. ORT will copy it.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \return The execution provider version string.
*
* \since Version 1.23.
*/
ORT_API_T(const char*, GetVersion, _In_ const OrtEpFactory* this_ptr);
/** \brief Validate the compatibility of a compiled model with the execution provider factory for one or more devices.
*
* Given a compatibility info string produced during model compilation, the EP factory should determine whether the
* compiled model is compatible with the EP factory when targeting the provided hardware devices. All devices provided
* must belong to the same execution provider instance that this factory creates.
*
* The EP factory implementation should consider the set of devices (e.g., multi-adapter or multi-GPU scenarios) when
* evaluating compatibility and set `model_compatibility` accordingly.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \param[in] devices Array of OrtHardwareDevice pointers that the EP would run on. All must map to this EP.
* \param[in] num_devices Number of entries in `devices`.
* \param[in] compatibility_info The compatibility information string produced when the model was compiled.
* \param[out] model_compatibility OrtCompiledModelCompatibility value describing the compatibility of the model with the EP.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(ValidateCompiledModelCompatibilityInfo, _In_ OrtEpFactory* this_ptr,
_In_reads_(num_devices) const OrtHardwareDevice* const* devices,
_In_ size_t num_devices,
_In_ const char* compatibility_info,
_Out_ OrtCompiledModelCompatibility* model_compatibility);
/** \brief Create an OrtAllocator that can be shared across sessions for the given OrtMemoryInfo.
*
* The factory that creates the EP is responsible for providing the allocators required by the EP.
* The OrtMemoryInfo instance will match one of the values set in the OrtEpDevice using EpDevice_AddAllocatorInfo.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \param[in] memory_info The OrtMemoryInfo to create the allocator for. May be nullptr.
* \param[in] allocator_options Optional key-value pairs for allocator options, can be nullptr.
* \param[out] allocator The created OrtAllocator instance. Set to nullptr if the default CPU allocator is used.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(CreateAllocator, _In_ OrtEpFactory* this_ptr,
_In_ const OrtMemoryInfo* memory_info,
_In_opt_ const OrtKeyValuePairs* allocator_options,
_Outptr_result_maybenull_ OrtAllocator** allocator);
/** \brief Release an OrtAllocator created by the factory.
*
* \since Version 1.23.
*/
ORT_API_T(void, ReleaseAllocator, _In_ OrtEpFactory* this_ptr, _In_ OrtAllocator* allocator);
/** \brief Create an OrtDataTransferImpl instance for the factory.
*
* This is used to create an IDataTransfer implementation that can be used to copy data between devices
* that the execution provider supports.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \param[out] data_transfer The created OrtDataTransferImpl instance. Set to nullptr if not required.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(CreateDataTransfer, _In_ OrtEpFactory* this_ptr,
_Outptr_result_maybenull_ OrtDataTransferImpl** data_transfer);
/** \brief Check if execution providers created by the factory are stream aware.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \return True if the factory creates execution providers that are stream aware and it implements CreateSyncStreamForDevice.
*
* \since Version 1.23.
*/
ORT_API_T(bool, IsStreamAware, _In_ const OrtEpFactory* this_ptr);
/** \brief Create a synchronization stream for the given memory device.
*
* This is used to create a synchronization stream for the memory device that can be used for operations outside of
* a session.
*
* \param[in] this_ptr The OrtEpFactory instance.
* \param[in] memory_device The OrtMemoryDevice to create the synchronization stream for.
* \param[in] stream_options Options for stream creation. May be nullptr.
* \param[out] stream The created OrtSyncStreamImpl instance. nullptr if the execution provider is not stream aware.
*
* \snippet{doc} snippets.dox OrtStatus Return Value
*
* \since Version 1.23.
*/
ORT_API2_STATUS(CreateSyncStreamForDevice, _In_ OrtEpFactory* this_ptr,
_In_ const OrtMemoryDevice* memory_device,
_In_opt_ const OrtKeyValuePairs* stream_options,
_Outptr_ OrtSyncStreamImpl** stream);
};
#ifdef __cplusplus
}
#endif

18
tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_ep_device_ep_metadata_keys.h

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
// This file contains well-known keys for OrtEpDevice EP metadata entries.
// It does NOT specify all available metadata keys.
// Key for the execution provider version string. This should be available for all plugin EPs.
static const char* const kOrtEpDevice_EpMetadataKey_Version = "version";
// Prefix for execution provider compatibility information stored in model metadata.
// Used when generating EP context models to store compatibility strings for each EP.
// Full key format: "ep_compatibility_info.<EP_TYPE>"
static const char* const kOrtModelMetadata_EpCompatibilityInfoPrefix = "ep_compatibility_info.";
// Key for the execution provider library path (for dynamically loaded EPs)
static const char* const kOrtEpDevice_EpMetadataKey_LibraryPath = "library_path";

535
tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_float16.h

@ -0,0 +1,535 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include <stdint.h>
#include <cmath>
#include <cstring>
#include <limits>
namespace onnxruntime_float16 {
namespace detail {
enum class endian {
#if defined(_WIN32)
little = 0,
big = 1,
native = little,
#elif defined(__GNUC__) || defined(__clang__)
little = __ORDER_LITTLE_ENDIAN__,
big = __ORDER_BIG_ENDIAN__,
native = __BYTE_ORDER__,
#else
#error onnxruntime_float16::detail::endian is not implemented in this environment.
#endif
};
static_assert(
endian::native == endian::little || endian::native == endian::big,
"Only little-endian or big-endian native byte orders are supported.");
} // namespace detail
/// <summary>
/// Shared implementation between public and internal classes. CRTP pattern.
/// </summary>
template <class Derived>
struct Float16Impl {
protected:
/// <summary>
/// Converts from float to uint16_t float16 representation
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
constexpr static uint16_t ToUint16Impl(float v) noexcept;
/// <summary>
/// Converts float16 to float
/// </summary>
/// <returns>float representation of float16 value</returns>
float ToFloatImpl() const noexcept;
/// <summary>
/// Creates an instance that represents absolute value.
/// </summary>
/// <returns>Absolute value</returns>
uint16_t AbsImpl() const noexcept {
return static_cast<uint16_t>(val & ~kSignMask);
}
/// <summary>
/// Creates a new instance with the sign flipped.
/// </summary>
/// <returns>Flipped sign instance</returns>
uint16_t NegateImpl() const noexcept {
return IsNaN() ? val : static_cast<uint16_t>(val ^ kSignMask);
}
public:
// uint16_t special values
static constexpr uint16_t kSignMask = 0x8000U;
static constexpr uint16_t kBiasedExponentMask = 0x7C00U;
static constexpr uint16_t kPositiveInfinityBits = 0x7C00U;
static constexpr uint16_t kNegativeInfinityBits = 0xFC00U;
static constexpr uint16_t kPositiveQNaNBits = 0x7E00U;
static constexpr uint16_t kNegativeQNaNBits = 0xFE00U;
static constexpr uint16_t kMaxValueBits = 0x7BFFU; // Largest normal number
static constexpr uint16_t kOneBits = 0x3C00U;
static constexpr uint16_t kMinusOneBits = 0xBC00U;
uint16_t val{0};
Float16Impl() = default;
/// <summary>
/// Checks if the value is negative
/// </summary>
/// <returns>true if negative</returns>
bool IsNegative() const noexcept {
return static_cast<int16_t>(val) < 0;
}
/// <summary>
/// Tests if the value is NaN
/// </summary>
/// <returns>true if NaN</returns>
bool IsNaN() const noexcept {
return AbsImpl() > kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value is finite
/// </summary>
/// <returns>true if finite</returns>
bool IsFinite() const noexcept {
return AbsImpl() < kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value represents positive infinity.
/// </summary>
/// <returns>true if positive infinity</returns>
bool IsPositiveInfinity() const noexcept {
return val == kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value represents negative infinity
/// </summary>
/// <returns>true if negative infinity</returns>
bool IsNegativeInfinity() const noexcept {
return val == kNegativeInfinityBits;
}
/// <summary>
/// Tests if the value is either positive or negative infinity.
/// </summary>
/// <returns>True if absolute value is infinity</returns>
bool IsInfinity() const noexcept {
return AbsImpl() == kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value is NaN or zero. Useful for comparisons.
/// </summary>
/// <returns>True if NaN or zero.</returns>
bool IsNaNOrZero() const noexcept {
auto abs = AbsImpl();
return (abs == 0 || abs > kPositiveInfinityBits);
}
/// <summary>
/// Tests if the value is normal (not zero, subnormal, infinite, or NaN).
/// </summary>
/// <returns>True if so</returns>
bool IsNormal() const noexcept {
auto abs = AbsImpl();
return (abs < kPositiveInfinityBits) // is finite
&& (abs != 0) // is not zero
&& ((abs & kBiasedExponentMask) != 0); // is not subnormal (has a non-zero exponent)
}
/// <summary>
/// Tests if the value is subnormal (denormal).
/// </summary>
/// <returns>True if so</returns>
bool IsSubnormal() const noexcept {
auto abs = AbsImpl();
return (abs < kPositiveInfinityBits) // is finite
&& (abs != 0) // is not zero
&& ((abs & kBiasedExponentMask) == 0); // is subnormal (has a zero exponent)
}
/// <summary>
/// Creates an instance that represents absolute value.
/// </summary>
/// <returns>Absolute value</returns>
Derived Abs() const noexcept { return Derived::FromBits(AbsImpl()); }
/// <summary>
/// Creates a new instance with the sign flipped.
/// </summary>
/// <returns>Flipped sign instance</returns>
Derived Negate() const noexcept { return Derived::FromBits(NegateImpl()); }
/// <summary>
/// IEEE defines that positive and negative zero are equal, this gives us a quick equality check
/// for two values by or'ing the private bits together and stripping the sign. They are both zero,
/// and therefore equivalent, if the resulting value is still zero.
/// </summary>
/// <param name="lhs">first value</param>
/// <param name="rhs">second value</param>
/// <returns>True if both arguments represent zero</returns>
static bool AreZero(const Float16Impl& lhs, const Float16Impl& rhs) noexcept {
return static_cast<uint16_t>((lhs.val | rhs.val) & ~kSignMask) == 0;
}
bool operator==(const Float16Impl& rhs) const noexcept {
if (IsNaN() || rhs.IsNaN()) {
// IEEE defines that NaN is not equal to anything, including itself.
return false;
}
return val == rhs.val;
}
bool operator!=(const Float16Impl& rhs) const noexcept { return !(*this == rhs); }
bool operator<(const Float16Impl& rhs) const noexcept {
if (IsNaN() || rhs.IsNaN()) {
// IEEE defines that NaN is unordered with respect to everything, including itself.
return false;
}
const bool left_is_negative = IsNegative();
if (left_is_negative != rhs.IsNegative()) {
// When the signs of left and right differ, we know that left is less than right if it is
// the negative value. The exception to this is if both values are zero, in which case IEEE
// says they should be equal, even if the signs differ.
return left_is_negative && !AreZero(*this, rhs);
}
return (val != rhs.val) && ((val < rhs.val) ^ left_is_negative);
}
};
// The following Float16_t conversions are based on the code from
// Eigen library.
// The conversion routines are Copyright (c) Fabian Giesen, 2016.
// The original license follows:
//
// Copyright (c) Fabian Giesen, 2016
// All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted.
// 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
// HOLDER 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.
namespace detail {
union float32_bits {
unsigned int u;
float f;
};
} // namespace detail
template <class Derived>
inline constexpr uint16_t Float16Impl<Derived>::ToUint16Impl(float v) noexcept {
detail::float32_bits f{};
f.f = v;
constexpr detail::float32_bits f32infty = {255 << 23};
constexpr detail::float32_bits f16max = {(127 + 16) << 23};
constexpr detail::float32_bits denorm_magic = {((127 - 15) + (23 - 10) + 1) << 23};
constexpr unsigned int sign_mask = 0x80000000u;
uint16_t val = static_cast<uint16_t>(0x0u);
unsigned int sign = f.u & sign_mask;
f.u ^= sign;
// NOTE all the integer compares in this function can be safely
// compiled into signed compares since all operands are below
// 0x80000000. Important if you want fast straight SSE2 code
// (since there's no unsigned PCMPGTD).
if (f.u >= f16max.u) { // result is Inf or NaN (all exponent bits set)
val = (f.u > f32infty.u) ? 0x7e00 : 0x7c00; // NaN->qNaN and Inf->Inf
} else { // (De)normalized number or zero
if (f.u < (113 << 23)) { // resulting FP16 is subnormal or zero
// use a magic value to align our 10 mantissa bits at the bottom of
// the float. as long as FP addition is round-to-nearest-even this
// just works.
f.f += denorm_magic.f;
// and one integer subtract of the bias later, we have our final float!
val = static_cast<uint16_t>(f.u - denorm_magic.u);
} else {
unsigned int mant_odd = (f.u >> 13) & 1; // resulting mantissa is odd
// update exponent, rounding bias part 1
// Equivalent to `f.u += ((unsigned int)(15 - 127) << 23) + 0xfff`, but
// without arithmetic overflow.
f.u += 0xc8000fffU;
// rounding bias part 2
f.u += mant_odd;
// take the bits!
val = static_cast<uint16_t>(f.u >> 13);
}
}
val |= static_cast<uint16_t>(sign >> 16);
return val;
}
template <class Derived>
inline float Float16Impl<Derived>::ToFloatImpl() const noexcept {
constexpr detail::float32_bits magic = {113 << 23};
constexpr unsigned int shifted_exp = 0x7c00 << 13; // exponent mask after shift
detail::float32_bits o{};
o.u = (val & 0x7fff) << 13; // exponent/mantissa bits
unsigned int exp = shifted_exp & o.u; // just the exponent
o.u += (127 - 15) << 23; // exponent adjust
// handle exponent special cases
if (exp == shifted_exp) { // Inf/NaN?
o.u += (128 - 16) << 23; // extra exp adjust
} else if (exp == 0) { // Zero/Denormal?
o.u += 1 << 23; // extra exp adjust
o.f -= magic.f; // re-normalize
}
// Attempt to workaround the Internal Compiler Error on ARM64
// for bitwise | operator, including std::bitset
#if (defined _MSC_VER) && (defined _M_ARM || defined _M_ARM64 || defined _M_ARM64EC)
if (IsNegative()) {
return -o.f;
}
#else
// original code:
o.u |= (val & 0x8000U) << 16U; // sign bit
#endif
return o.f;
}
/// Shared implementation between public and internal classes. CRTP pattern.
template <class Derived>
struct BFloat16Impl {
protected:
/// <summary>
/// Converts from float to uint16_t float16 representation
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
static uint16_t ToUint16Impl(float v) noexcept;
/// <summary>
/// Converts bfloat16 to float
/// </summary>
/// <returns>float representation of bfloat16 value</returns>
float ToFloatImpl() const noexcept;
/// <summary>
/// Creates an instance that represents absolute value.
/// </summary>
/// <returns>Absolute value</returns>
uint16_t AbsImpl() const noexcept {
return static_cast<uint16_t>(val & ~kSignMask);
}
/// <summary>
/// Creates a new instance with the sign flipped.
/// </summary>
/// <returns>Flipped sign instance</returns>
uint16_t NegateImpl() const noexcept {
return IsNaN() ? val : static_cast<uint16_t>(val ^ kSignMask);
}
public:
// uint16_t special values
static constexpr uint16_t kSignMask = 0x8000U;
static constexpr uint16_t kBiasedExponentMask = 0x7F80U;
static constexpr uint16_t kPositiveInfinityBits = 0x7F80U;
static constexpr uint16_t kNegativeInfinityBits = 0xFF80U;
static constexpr uint16_t kPositiveQNaNBits = 0x7FC1U;
static constexpr uint16_t kNegativeQNaNBits = 0xFFC1U;
static constexpr uint16_t kMaxValueBits = 0x7F7FU;
static constexpr uint16_t kRoundToNearest = 0x7FFFU;
static constexpr uint16_t kOneBits = 0x3F80U;
static constexpr uint16_t kMinusOneBits = 0xBF80U;
uint16_t val{0};
BFloat16Impl() = default;
/// <summary>
/// Checks if the value is negative
/// </summary>
/// <returns>true if negative</returns>
bool IsNegative() const noexcept {
return static_cast<int16_t>(val) < 0;
}
/// <summary>
/// Tests if the value is NaN
/// </summary>
/// <returns>true if NaN</returns>
bool IsNaN() const noexcept {
return AbsImpl() > kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value is finite
/// </summary>
/// <returns>true if finite</returns>
bool IsFinite() const noexcept {
return AbsImpl() < kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value represents positive infinity.
/// </summary>
/// <returns>true if positive infinity</returns>
bool IsPositiveInfinity() const noexcept {
return val == kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value represents negative infinity
/// </summary>
/// <returns>true if negative infinity</returns>
bool IsNegativeInfinity() const noexcept {
return val == kNegativeInfinityBits;
}
/// <summary>
/// Tests if the value is either positive or negative infinity.
/// </summary>
/// <returns>True if absolute value is infinity</returns>
bool IsInfinity() const noexcept {
return AbsImpl() == kPositiveInfinityBits;
}
/// <summary>
/// Tests if the value is NaN or zero. Useful for comparisons.
/// </summary>
/// <returns>True if NaN or zero.</returns>
bool IsNaNOrZero() const noexcept {
auto abs = AbsImpl();
return (abs == 0 || abs > kPositiveInfinityBits);
}
/// <summary>
/// Tests if the value is normal (not zero, subnormal, infinite, or NaN).
/// </summary>
/// <returns>True if so</returns>
bool IsNormal() const noexcept {
auto abs = AbsImpl();
return (abs < kPositiveInfinityBits) // is finite
&& (abs != 0) // is not zero
&& ((abs & kBiasedExponentMask) != 0); // is not subnormal (has a non-zero exponent)
}
/// <summary>
/// Tests if the value is subnormal (denormal).
/// </summary>
/// <returns>True if so</returns>
bool IsSubnormal() const noexcept {
auto abs = AbsImpl();
return (abs < kPositiveInfinityBits) // is finite
&& (abs != 0) // is not zero
&& ((abs & kBiasedExponentMask) == 0); // is subnormal (has a zero exponent)
}
/// <summary>
/// Creates an instance that represents absolute value.
/// </summary>
/// <returns>Absolute value</returns>
Derived Abs() const noexcept { return Derived::FromBits(AbsImpl()); }
/// <summary>
/// Creates a new instance with the sign flipped.
/// </summary>
/// <returns>Flipped sign instance</returns>
Derived Negate() const noexcept { return Derived::FromBits(NegateImpl()); }
/// <summary>
/// IEEE defines that positive and negative zero are equal, this gives us a quick equality check
/// for two values by or'ing the private bits together and stripping the sign. They are both zero,
/// and therefore equivalent, if the resulting value is still zero.
/// </summary>
/// <param name="lhs">first value</param>
/// <param name="rhs">second value</param>
/// <returns>True if both arguments represent zero</returns>
static bool AreZero(const BFloat16Impl& lhs, const BFloat16Impl& rhs) noexcept {
// IEEE defines that positive and negative zero are equal, this gives us a quick equality check
// for two values by or'ing the private bits together and stripping the sign. They are both zero,
// and therefore equivalent, if the resulting value is still zero.
return static_cast<uint16_t>((lhs.val | rhs.val) & ~kSignMask) == 0;
}
};
template <class Derived>
inline uint16_t BFloat16Impl<Derived>::ToUint16Impl(float v) noexcept {
uint16_t result;
if (std::isnan(v)) {
result = kPositiveQNaNBits;
} else {
auto get_msb_half = [](float fl) {
uint16_t result;
#ifdef __cpp_if_constexpr
if constexpr (detail::endian::native == detail::endian::little) {
#else
if (detail::endian::native == detail::endian::little) {
#endif
std::memcpy(&result, reinterpret_cast<char*>(&fl) + sizeof(uint16_t), sizeof(uint16_t));
} else {
std::memcpy(&result, &fl, sizeof(uint16_t));
}
return result;
};
uint16_t upper_bits = get_msb_half(v);
union {
uint32_t U32;
float F32;
};
F32 = v;
U32 += (upper_bits & 1) + kRoundToNearest;
result = get_msb_half(F32);
}
return result;
}
template <class Derived>
inline float BFloat16Impl<Derived>::ToFloatImpl() const noexcept {
if (IsNaN()) {
return std::numeric_limits<float>::quiet_NaN();
}
float result;
char* const first = reinterpret_cast<char*>(&result);
char* const second = first + sizeof(uint16_t);
#ifdef __cpp_if_constexpr
if constexpr (detail::endian::native == detail::endian::little) {
#else
if (detail::endian::native == detail::endian::little) {
#endif
std::memset(first, 0, sizeof(uint16_t));
std::memcpy(second, &val, sizeof(uint16_t));
} else {
std::memcpy(first, &val, sizeof(uint16_t));
std::memset(second, 0, sizeof(uint16_t));
}
return result;
}
} // namespace onnxruntime_float16

1119
tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_lite_custom_op.h

File diff suppressed because it is too large Load Diff

54
tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_run_options_config_keys.h

@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
/*
* This file defines RunOptions Config Keys and format of the Config Values.
*
* The Naming Convention for a RunOptions Config Key,
* "[Area][.[SubArea1].[SubArea2]...].[Keyname]"
* Such as "ep.cuda.use_arena"
* The Config Key cannot be empty
* The maximum length of the Config Key is 128
*
* The string format of a RunOptions Config Value is defined individually for each Config.
* The maximum length of the Config Value is 1024
*/
// Key for enabling shrinkages of user listed device memory arenas.
// Expects a list of semi-colon separated key value pairs separated by colon in the following format:
// "device_0:device_id_0;device_1:device_id_1"
// No white-spaces allowed in the provided list string.
// Currently, the only supported devices are : "cpu", "gpu" (case sensitive).
// If "cpu" is included in the list, DisableCpuMemArena() API must not be called (i.e.) arena for cpu should be enabled.
// Example usage: "cpu:0;gpu:0" (or) "gpu:0"
// By default, the value for this key is empty (i.e.) no memory arenas are shrunk
static const char* const kOrtRunOptionsConfigEnableMemoryArenaShrinkage = "memory.enable_memory_arena_shrinkage";
// Set to '1' to not synchronize execution providers with CPU at the end of session run.
// Per default it will be set to '0'
// Taking CUDA EP as an example, it omit triggering cudaStreamSynchronize on the compute stream.
static const char* const kOrtRunOptionsConfigDisableSynchronizeExecutionProviders = "disable_synchronize_execution_providers";
// Set HTP performance mode for QNN HTP backend before session run.
// options for HTP performance mode: "burst", "balanced", "default", "high_performance",
// "high_power_saver", "low_balanced", "extreme_power_saver", "low_power_saver", "power_saver",
// "sustained_high_performance". Default to "default".
static const char* const kOrtRunOptionsConfigQnnPerfMode = "qnn.htp_perf_mode";
// Set HTP performance mode for QNN HTP backend post session run.
static const char* const kOrtRunOptionsConfigQnnPerfModePostRun = "qnn.htp_perf_mode_post_run";
// Set RPC control latency for QNN HTP backend
static const char* const kOrtRunOptionsConfigQnnRpcControlLatency = "qnn.rpc_control_latency";
// Set QNN Lora Config File for apply Lora in QNN context binary
static const char* const kOrtRunOptionsConfigQnnLoraConfig = "qnn.lora_config";
// Set graph annotation id for CUDA EP. Use with enable_cuda_graph=true.
// The value should be an integer. If the value is not set, the default value is 0 and
// ORT session only captures one cuda graph before another capture is requested.
// If the value is set to -1, cuda graph capture/replay is disabled in that run.
// User are not expected to set the value to 0 as it is reserved for internal use.
static const char* const kOrtRunOptionsConfigCudaGraphAnnotation = "gpu_graph_id";

417
tools/chatgui-android/app/src/main/cpp/onnxruntime/include/onnxruntime/onnxruntime_session_options_config_keys.h

@ -0,0 +1,417 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
/*
* This file defines SessionOptions Config Keys and format of the Config Values.
*
* The Naming Convention for a SessionOptions Config Key,
* "[Area][.[SubArea1].[SubArea2]...].[Keyname]"
* Such as "ep.cuda.use_arena"
* The Config Key cannot be empty
* The maximum length of the Config Key is 1024
*
* The string format of a SessionOptions Config Value is defined individually for each Config.
* The maximum length of the Config Value is 2048
*/
// Key for disable PrePacking,
// If the config value is set to "1" then the prepacking is disabled, otherwise prepacking is enabled (default value)
static const char* const kOrtSessionOptionsConfigDisablePrepacking = "session.disable_prepacking";
// A value of "1" means allocators registered in the env will be used. "0" means the allocators created in the session
// will be used. Use this to override the usage of env allocators on a per session level.
static const char* const kOrtSessionOptionsConfigUseEnvAllocators = "session.use_env_allocators";
// Set to 'ORT' (case sensitive) to load an ORT format model.
// If unset, model type will default to ONNX unless inferred from filename ('.ort' == ORT format) or bytes to be ORT
static const char* const kOrtSessionOptionsConfigLoadModelFormat = "session.load_model_format";
// Set to 'ORT' (case sensitive) to save optimized model in ORT format when SessionOptions.optimized_model_path is set.
// If unset, format will default to ONNX unless optimized_model_filepath ends in '.ort'.
static const char* const kOrtSessionOptionsConfigSaveModelFormat = "session.save_model_format";
// If a value is "1", flush-to-zero and denormal-as-zero are applied. The default is "0".
// When multiple sessions are created, a main thread doesn't override changes from succeeding session options,
// but threads in session thread pools follow option changes.
// When ORT runs with OpenMP, the same rule is applied, i.e. the first session option to flush-to-zero and
// denormal-as-zero is only applied to global OpenMP thread pool, which doesn't support per-session thread pool.
// Note that an alternative way not using this option at runtime is to train and export a model without denormals
// and that's recommended because turning this option on may hurt model accuracy.
static const char* const kOrtSessionOptionsConfigSetDenormalAsZero = "session.set_denormal_as_zero";
// It controls to run quantization model in QDQ (QuantizelinearDeQuantizelinear) format or not.
// "0": enable. ORT does fusion logic for QDQ format.
// "1": disable. ORT doesn't do fusion logic for QDQ format.
// Its default value is "0" unless the DirectML execution provider is registered, in which case it defaults to "1".
static const char* const kOrtSessionOptionsDisableQuantQDQ = "session.disable_quant_qdq";
// It controls whether to enable Double QDQ remover and Identical Children Consolidation
// "0": not to disable. ORT does remove the middle 2 Nodes from a Q->(QD->Q)->QD pairs
// "1": disable. ORT doesn't remove the middle 2 Nodes from a Q->(QD->Q)->QD pairs
// Its default value is "0"
static const char* const kOrtSessionOptionsDisableDoubleQDQRemover = "session.disable_double_qdq_remover";
// If set to "1", enables the removal of QuantizeLinear/DequantizeLinear node pairs once all QDQ handling has been
// completed. e.g. If after all QDQ handling has completed and we have -> FloatOp -> Q -> DQ -> FloatOp -> the
// Q -> DQ could potentially be removed. This will provide a performance benefit by avoiding going from float to
// 8-bit and back to float, but could impact accuracy. The impact on accuracy will be model specific and depend on
// other factors like whether the model was created using Quantization Aware Training or Post Training Quantization.
// As such, it's best to test to determine if enabling this works well for your scenario.
// The default value is "0"
// Available since version 1.11.
static const char* const kOrtSessionOptionsEnableQuantQDQCleanup = "session.enable_quant_qdq_cleanup";
// Enable or disable gelu approximation in graph optimization. "0": disable; "1": enable. The default is "0".
// GeluApproximation has side effects which may change the inference results. It is disabled by default due to this.
static const char* const kOrtSessionOptionsEnableGeluApproximation = "optimization.enable_gelu_approximation";
// Enable or disable Cast chain elimination in graph optimization. "0": disable; "1": enable. The default is "0".
// CastElimination with chain elimination has side effects which may change the inference results. It is disabled by default due to this.
static const char* const kOrtSessionOptionsEnableCastChainElimination = "optimization.enable_cast_chain_elimination";
// This setting controls whether to enable AheadOfTime function inlining.
// AOT function inlining examines the graph and attempts to inline as many locally defined functions in the model
// as possible with the help of enabled execution providers.
// This can reduce the number of function calls and improve performance because it is done before
// Level1 optimizers and constant folding. However, under some circumstances, when the EPs are not available,
// one can disable the AOT inlining, produce an optimized model and postpone AOT until run time.
// "0": enable; "1": disable.
// Its default value is "0".
static const char* const kOrtSessionOptionsDisableAheadOfTimeFunctionInlining = "session.disable_aot_function_inlining";
#ifdef ENABLE_TRAINING
// Specifies a path of the file containing a list of memory optimization configurations.
// The value should be a string indicating the file path of the config file.
// The content of the config file is a JSON struct like this:
// [
// "Gelu+Cast+:1:0",
// "Dropout+:1:1"
// ]
// Taking the example of "Gelu+Cast+:1:0",
// > "Gelu+Cast+" is the subgraph string, a valid "subgraph string" should be one subgraph representation
// output by ORT graph transformations.
// > "1" is "optimization strategy", valid values: 0 - disabled, 1 - recompute.
// > "0" is "number of subgraph to apply" which is used to control how many subgraphs to apply optimization,
// to avoid "oversaving" the memory.
static const char* const kOrtSessionOptionsMemoryOptimizerApplyConfig = "optimization.memory_optimizer_config";
// Specifies the config for detecting subgraphs for memory footprint reduction.
// The value should be a string contains int separated using commas. The default value is "0:0".
static const char* const kOrtSessionOptionsMemoryOptimizerProbeConfig = "optimization.enable_memory_probe_recompute_config";
#endif
// This setting if set should contain a comma separated list of optimizers names that should be disabled.
// Optimizers may take time to execute and affect model loading time. If you feel that a specific optimizer
// does not provider runtime benefits, but affects your model loading time you may disable it using this config
// entry. This option is not enabled in ORT_MINIMAL_BUILD build.
// A list of optimizes is available in onnxruntime/core/optimizer/graph_transformer_utils.cc
//
// Default is an empty string which means no optimizers are disabled.
static const char* const kOrtSessionOptionsDisableSpecifiedOptimizers = "optimization.disable_specified_optimizers";
// It controls whether to run graph optimizations in loop or not.
//
// "0": disable. Graph Optimization Loop is disabled.
// ```
// Level 2 --> Level 3 --> InsertCastTransforms --> Level 4
// ^ |
// | "No Loop" |
// | |
// X xxxxxxxxxxx X
// ```
// "1": enable. Graph Optimization Loop is enabled, such that, if optimizations at Level 4 are applied then
// the loop will check for any other valid optimization that can happen.
// ```
// Level 2 --> Level 3 --> InsertCastTransforms --> Level 4
// ^ |
// | "Loop only depending on Level 4" |
// | |
// ---------------------------------------------------
// ```
// "2": enable. Graph Optimization Loop is enabled, such that, if optimizations at Level 2 or above are applied then
// The loop will check for any other valid optimization that can happen.
// ```
// Level 2 --> Level 3 --> InsertCastTransforms --> Level 4
// ^ |
// | "Loop" |
// | |
// ---------------------------------------------------
// ```
// Default value is set to "1".
static const char* const kOrtSessionOptionsGraphOptimizationsLoopLevel = "session.graph_optimizations_loop_level";
// Enable or disable using device allocator for allocating initialized tensor memory. "1": enable; "0": disable. The default is "0".
// Using device allocators means the memory allocation is made using malloc/new.
static const char* const kOrtSessionOptionsUseDeviceAllocatorForInitializers = "session.use_device_allocator_for_initializers";
// Configure whether to allow the inter_op/intra_op threads spinning a number of times before blocking
// "0": thread will block if found no job to run
// "1": thread will spin a number of times before blocking
// The default is "0" when ORT is built with "ORT_CLIENT_PACKAGE_BUILD" and "1" otherwise.
// Thread spinning is disabled by default for client/on-device workloads to reduce cpu utilization and improve power efficiency.
static const char* const kOrtSessionOptionsConfigAllowInterOpSpinning = "session.inter_op.allow_spinning";
static const char* const kOrtSessionOptionsConfigAllowIntraOpSpinning = "session.intra_op.allow_spinning";
// Key for using model bytes directly for ORT format
// If a session is created using an input byte array contains the ORT format model data,
// By default we will copy the model bytes at the time of session creation to ensure the model bytes
// buffer is valid.
// Setting this option to "1" will disable copy the model bytes, and use the model bytes directly. The caller
// has to guarantee that the model bytes are valid until the ORT session using the model bytes is destroyed.
static const char* const kOrtSessionOptionsConfigUseORTModelBytesDirectly = "session.use_ort_model_bytes_directly";
/// <summary>
/// Key for using the ORT format model flatbuffer bytes directly for initializers.
/// This avoids copying the bytes and reduces peak memory usage during model loading and initialization.
/// Requires `session.use_ort_model_bytes_directly` to be true.
/// If set, the flatbuffer bytes provided when creating the InferenceSession MUST remain valid for the entire
/// duration of the InferenceSession.
/// </summary>
static const char* const kOrtSessionOptionsConfigUseORTModelBytesForInitializers =
"session.use_ort_model_bytes_for_initializers";
// This should only be specified when exporting an ORT format model for use on a different platform.
// If the ORT format model will be used on ARM platforms set to "1". For other platforms set to "0"
// Available since version 1.11.
static const char* const kOrtSessionOptionsQDQIsInt8Allowed = "session.qdqisint8allowed";
// x64 SSE4.1/AVX2/AVX512(with no VNNI) has overflow problem with quantizied matrix multiplication with U8S8.
// To avoid this we need to use slower U8U8 matrix multiplication instead. This option, if
// turned on, use slower U8U8 matrix multiplications. Only effective with AVX2 or AVX512
// platforms.
static const char* const kOrtSessionOptionsAvx2PrecisionMode = "session.x64quantprecision";
// Specifies how minimal build graph optimizations are handled in a full build.
// These optimizations are at the extended level or higher.
// Possible values and their effects are:
// "save": Save runtime optimizations when saving an ORT format model.
// "apply": Only apply optimizations available in a minimal build.
// ""/<unspecified>: Apply optimizations available in a full build.
// Available since version 1.11.
static const char* const kOrtSessionOptionsConfigMinimalBuildOptimizations =
"optimization.minimal_build_optimizations";
// Note: The options specific to an EP should be specified prior to appending that EP to the session options object in
// order for them to take effect.
// Specifies a list of stop op types. Nodes of a type in the stop op types and nodes downstream from them will not be
// run by the NNAPI EP.
// The value should be a ","-delimited list of op types. For example, "Add,Sub".
// If not specified, the default set of stop ops is used. To specify an empty stop ops types list and disable stop op
// exclusion, set the value to "".
static const char* const kOrtSessionOptionsConfigNnapiEpPartitioningStopOps = "ep.nnapi.partitioning_stop_ops";
// Enabling dynamic block-sizing for multithreading.
// With a positive value, thread pool will split a task of N iterations to blocks of size starting from:
// N / (num_of_threads * dynamic_block_base)
// As execution progresses, the size will decrease according to the diminishing residual of N,
// meaning the task will be distributed in smaller granularity for better parallelism.
// For some models, it helps to reduce the variance of E2E inference latency and boost performance.
// The feature will not function by default, specify any positive integer, e.g. "4", to enable it.
// Available since version 1.11.
static const char* const kOrtSessionOptionsConfigDynamicBlockBase = "session.dynamic_block_base";
// This option allows to decrease CPU usage between infrequent
// requests and forces any TP threads spinning stop immediately when the last of
// concurrent Run() call returns.
// Spinning is restarted on the next Run() call.
// Applies only to internal thread-pools
static const char* const kOrtSessionOptionsConfigForceSpinningStop = "session.force_spinning_stop";
// "1": all inconsistencies encountered during shape and type inference
// will result in failures.
// "0": in some cases warnings will be logged but processing will continue. The default.
// May be useful to expose bugs in models.
static const char* const kOrtSessionOptionsConfigStrictShapeTypeInference = "session.strict_shape_type_inference";
// "1": every model using a more recent opset than the latest released one will fail
// "0": the model may or may not work if onnxruntime cannot find an implementation, this option
// is used for development purpose.
static const char* const kOrtSessionOptionsConfigStrictAllowReleasedOpsetsOnly = "session.allow_released_opsets_only";
// The file saves configuration for partitioning node among logic streams
static const char* const kNodePartitionConfigFile = "session.node_partition_config_file";
// This Option allows setting affinities for intra op threads.
// Affinity string follows format:
// logical_processor_id,logical_processor_id;logical_processor_id,logical_processor_id
// Semicolon isolates configurations among threads, while comma split processors where ith thread expected to attach to.
// e.g.1,2,3;4,5
// specifies affinities for two threads, with the 1st thread attach to the 1st, 2nd, and 3rd processor, and 2nd thread to the 4th and 5th.
// To ease the configuration, an "interval" is also allowed:
// e.g. 1-8;8-16;17-24
// orders that the 1st thread runs on first eight processors, 2nd thread runs on next eight processors, and so forth.
// Note:
// 1. Once set, the number of thread affinities must equal to intra_op_num_threads - 1, since ort does not set affinity on the main thread which
// is started and managed by the calling app;
// 2. For windows, ort will infer the group id from a logical processor id, for example, assuming there are two groups with each has 64 logical processors,
// an id of 64 will be inferred as the last processor of the 1st group, while 65 will be interpreted as the 1st processor of the second group.
// Hence 64-65 is an invalid configuration, because a windows thread cannot be attached to processors across group boundary.
static const char* const kOrtSessionOptionsConfigIntraOpThreadAffinities = "session.intra_op_thread_affinities";
// This option will dump out the model to assist debugging any issues with layout transformation,
// and is primarily intended for developer usage. It is only relevant if an execution provider that requests
// NHWC layout is enabled such as NNAPI, XNNPACK or QNN.
//
// Default is off. Set to "1" to enable.
//
// If modified by layout transformation the model will be dumped after these steps:
// 1) insertion of the layout transformation Transpose nodes
// 2) after those are optimized using the transpose optimizer,
// 3) after the L1 transformers are applied to the updated graph.
// The model will be saved to filename post_layout_transform_step_<step_number>.onnx.
static const char* const kDebugLayoutTransformation = "session.debug_layout_transformation";
// Graph nodes that are not supported by the execution providers (EPs) explicitly added to the session are
// assigned (i.e., "fallback") to the CPU EP by default.
//
// This option allows the user to disable the fallback of unsupported graph nodes to the CPU EP.
// If this option is set to "1", session creation will fail if the execution providers other than the CPU EP cannot
// fully support all of the nodes in the graph.
//
// It is invalid to set this option and explicitly add the CPU EP to the session. In this case, session creation
// will also fail with an error.
//
// Option values:
// - "0": CPU EP fallback is not disabled. [DEFAULT]
// - "1": CPU EP fallback is disabled.
static const char* const kOrtSessionOptionsDisableCPUEPFallback = "session.disable_cpu_ep_fallback";
// Use this config when serializing a large model after optimization to specify an external initializers file
static const char* const kOrtSessionOptionsOptimizedModelExternalInitializersFileName =
"session.optimized_model_external_initializers_file_name";
// Use this config to control the minimum size of the initializer when externalizing it during serialization
static const char* const kOrtSessionOptionsOptimizedModelExternalInitializersMinSizeInBytes =
"session.optimized_model_external_initializers_min_size_in_bytes";
// When loading model from memory buffer and the model has external initializers
// Use this config to set the external data file folder path
// All external data files should be in the same folder
static const char* const kOrtSessionOptionsModelExternalInitializersFileFolderPath =
"session.model_external_initializers_file_folder_path";
// Use this config when saving pre-packed constant initializers to an external data file.
// This allows you to memory map pre-packed initializers on model load and leave it to
// to the OS the amount of memory consumed by the pre-packed initializers. Otherwise,
// pre-packed data resides on the heap.
//
// - "0": Default is not save pre-packed initializers to a data file.
// - "1": Save pre-packed constant initializers to an external data file.
// Sample usage: sess_options.add_session_config_entry(kOrtSessionOptionsSavePrePackedConstantInitializers, "1")
static const char* const kOrtSessionOptionsSavePrePackedConstantInitializers =
"session.save_external_prepacked_constant_initializers";
// Use this config when you want to collect memory stats for each node in the graph.
// The file format is a CSV file with the following columns:
// The file will be created if it does not exist, and will be overwritten if it does.
//
// The content of the file can be used to estimate memory requirements at run time including
// the temporary allocations. This operation is preferably done on a CPU device, as the model may exceed
// device memory limits in constrained environments. When enabling this option, it is important to disable
// memory patterns, as they tend to allocate large blocks to avoid fragmentation and accommodate needs of multiple
// kernels. Memory patterns may make it difficult to allocate on a device with limited memory.
//
// The collected stats then can be used to partition the graph among the devices in a way that only the
// required memory is allocated on each device.
//
// node_name, initializers_memory, dynamic_outputs_sizes, temp_allocations_size
//
// - "full path to file": there is not a default for this option. If the file can not be opened for writing, an error will be returned.
static const char* const kOrtSessionOptionsCollectNodeMemoryStatsToFile = "session.collect_node_memory_stats_to_file";
/// This is a composite CSV setting formatted as "memory limit in kb,file name for collected stats"
/// "limit > 0": enables Capacity Aware Partitioning for Cuda EP. `limit` is optional and when absent
/// the provider may attempt to figure out the memory available automatically.
/// The setting with no limit is expected to look like: ",file name for collected stats"
/// The EP will place nodes on device "file name" :
/// this file is expected to be found at the same folder with the model. The file contains
/// pre-recorded stats collected when running with kOrtSessionOptionsCollectNodeMemoryStatsToFile enforce (see above)
static const char* const kOrtSessionOptionsResourceCudaPartitioningSettings =
"session.resource_cuda_partitioning_settings";
// Enable EP context feature to dump the partitioned graph which includes the EP context into Onnx file.
// The dumped Onnx model with EP context can be used for future inference to avoid the EP graph partitioning/compile overhead.
// "0": disable. (default)
// "1": enable.
static const char* const kOrtSessionOptionEpContextEnable = "ep.context_enable";
// Specify the file path for the Onnx model which has EP context.
// Default to original_file_name_ctx.onnx if not specified
// Folder is not a valid option
static const char* const kOrtSessionOptionEpContextFilePath = "ep.context_file_path";
// Flag to specify whether to dump the EP context into the Onnx model.
// "0": dump the EP context into separate file, keep the file name in the Onnx model. (default).
// "1": dump the EP context into the Onnx model.
static const char* const kOrtSessionOptionEpContextEmbedMode = "ep.context_embed_mode";
// Specify the EPContext node name prefix to make it unique
// in case user need to merge/connect multiple EPContext nodes in one model
static const char* const kOrtSessionOptionEpContextNodeNamePrefix = "ep.context_node_name_prefix";
// Share EP related resources across sessions
static const char* const kOrtSessionOptionShareEpContexts = "ep.share_ep_contexts";
// Stop to share EP related resources across sessions from then on
static const char* const kOrtSessionOptionStopShareEpContexts = "ep.stop_share_ep_contexts";
// Used only for context model generation.
// This configuration is used when some nodes are partitioned on the CPU EP and those nodes have external initializers.
// When generating the EP context model, the new model should not rely on the old external data file used by the source ONNX model.
// Use this setting when dumping the EP context model with an external initializers file.
// If specified, all initializers will be placed inside the external data file.
// Otherwise, all initializers will be embedded inside the generated ONNX file.
// By default, this option is not set, meaning all initializers will be included within the ONNX file.
static const char* const kOrtSessionOptionsEpContextModelExternalInitializersFileName =
"ep.context_model_external_initializers_file_name";
// Gemm fastmath mode provides fp32 gemm acceleration with bfloat16 based matmul.
// Option values:
// - "0": Gemm FastMath mode is not enabled. [DEFAULT]
// - "1": Gemm FastMath mode is enabled.
static const char* const kOrtSessionOptionsMlasGemmFastMathArm64Bfloat16 = "mlas.enable_gemm_fastmath_arm64_bfloat16";
// When converting DQ + MatMul -> MatMulNBits, the accuracy level of the MatMulNBits is controlled by this option.
// Refer to MatMulNBits op schema for more details.
// If not provided, default is 4.
static const char* const kOrtSessionOptionsQDQMatMulNBitsAccuracyLevel = "session.qdq_matmulnbits_accuracy_level";
// THIS OPTION IS NOT A REGULAR SESSION OPTION SINCE IT CAN BE MODIFIED AT ANY TIME
// Meant to be used with SetEpDynamicOptions
// Specify the type of workload for this session.
// "Default": OS determines the scheduling priority and processor performance to service this workload. [Default]
// "Efficient": OS treats this workload is efficiency oriented with low scheduling priority and efficient processor performance.
static const char* const kOrtEpDynamicOptionsWorkloadType = "ep.dynamic.workload_type";
// Disables model compilation during session initialization.
//
// If this option is set to "1", inference session creation will fail with error code ORT_MODEL_REQUIRES_COMPILATION
// if compilation is required to run the model on any Execution Provider added to the session.
// Only the following kinds of models are valid when this option is set to "1":
// - Pre-compiled models that have EPContext nodes for the compiling Execution Providers in the session.
// - Non-compiled models that run only on non-compiling Execution Providers, like CPU EP.
//
// See \href https://onnxruntime.ai/docs/execution-providers/EP-Context-Design.html for details about
// compiled models with EPContext nodes.
//
// Option values:
// - "0": EP compile is not disabled. [DEFAULT]
// - "1": EP compile is disabled.
static const char* const kOrtSessionOptionsDisableModelCompile = "session.disable_model_compile";
// Controls behavior when compiled model compatibility is SUPPORTED_PREFER_RECOMPILATION.
// "0": Allow execution with suboptimal performance. [DEFAULT]
// "1": Fail session creation to require recompilation for optimal performance.
// Note: UNSUPPORTED models always fail regardless of this setting.
static const char* const kOrtSessionOptionsFailOnSuboptimalCompiledModel =
"session.fail_on_suboptimal_compiled_model";
// THIS OPTION IS NOT A REGULAR SESSION OPTION SINCE IT CAN BE MODIFIED AT ANY TIME
// Meant to be used with SetEpDynamicOptions
// options for HTP performance mode: "burst", "balanced", "default", "high_performance",
// "high_power_saver", "low_balanced", "extreme_power_saver", "low_power_saver", "power_saver",
// "sustained_high_performance". Default to "default".
static const char* const kOrtEpDynamicOptionsQnnHtpPerformanceMode = "ep.dynamic.qnn_htp_performance_mode";

12
tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt

@ -2,20 +2,28 @@ package com.utun.chat
import android.app.Application
import android.util.Log
import android.os.Handler
import android.os.Looper
import com.utun.chat.data.LogManager
import com.utun.chat.data.ConfigProvider
import com.utun.chat.data.NativeLib
import com.utun.chat.data.NotificationSound
import com.utun.chat.data.IncomingCallManager
import com.utun.chat.data.TelecomCallManager
import com.utun.chat.data.ChatEventCallback
import com.utun.chat.data.CallController
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
object AppEventHandler : ChatEventCallback {
private val mainHandler = Handler(Looper.getMainLooper())
private val _events = MutableSharedFlow<Pair<Int, ByteArray?>>(replay = 16, extraBufferCapacity = 64)
val events: SharedFlow<Pair<Int, ByteArray?>> = _events
override fun onEvent(type: Int, data: ByteArray?) {
if (type == 12 || type == 13 || type in 34..40 || type == 42) {
mainHandler.post { CallController.onEvent(type, data) }
}
Log.d("utun-gui", "[EVENT] type=$type len=${data?.size ?: 0}")
_events.tryEmit(type to data)
}
@ -41,6 +49,10 @@ class ChatApplication : Application() {
/* Incoming-call system effects (wake + ringtone + notification) — app-scoped */
IncomingCallManager.init()
/* Telecom self-managed call integration (system audio routing for calls) */
TelecomCallManager.init()
CallController.init()
/* First launch config */
if (configProvider.isFirstLaunch()) {
kotlinx.coroutines.runBlocking {

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

Loading…
Cancel
Save