Compare commits

...

170 Commits

Author SHA1 Message Date
evgeny 35648a19ce Avoid Windows poll macro collision in PTT hotkey manager 5 hours ago
evgeny 23368a5bbe Add Android boot startup, lazy settings, emoji and DM calls 5 hours ago
evgeny 66afcc8625 Add extensible Russian and English interface languages on Android 6 hours ago
evgeny df346fc097 Log standby retry transitions and await the actual wake phase 10 hours ago
evgeny 7ec5f931bb Defer automatic retries until Android standby wakes 10 hours ago
evgeny f12bab8676 Include concrete transport definitions for UP checks 10 hours ago
evgeny f4b273d0be Cover UP-based chat peer selection and fallback transitions 10 hours ago
evgeny 1d189195f9 Advertise desktop clients and log unknown automatic candidates 11 hours ago
evgeny dc34faaada Route chat retries through automatic peer selection 11 hours ago
evgeny a7632cfee1 Select chat auto peers by device class and transport UP 11 hours ago
evgeny 1c499dc230 Classify chat supernodes independently of address type 11 hours ago
evgeny 01bc6e4cf0 Опиши контракты аудиотракта и результаты проверок 12 hours ago
evgeny bd082a26bf Раздели аудиопотоки и привяжи AEC к фактическому общему выходу 12 hours ago
evgeny d7fb0c83f9 Fix bounds and unsigned decoding in ETCP packet dumps 19 hours ago
evgeny ac45554439 Fix ETCP INIT collision scope and backpressure regression tests 20 hours ago
evgeny 217e5795bf Раздели PCM-сессии и исправь частоту и перемотку звука 20 hours ago
evgeny 40b974b29c Исправь потоковый AGC и применяй mute после AEC 20 hours ago
evgeny bdcf8b5634 Сохрани накопленные исправления аудио, async и диагностики 20 hours ago
evgeny b1e3193899 Document typed PM media, shared preparation and completed validation 1 day ago
evgeny 7ae56f03b3 Keep Android video metadata truthful and remove obsolete composer imports 1 day ago
evgeny 203ae61eda Share bounded thumbnail workers and keep PM downloads in DM transport 1 day ago
evgeny c863b80917 Validate Android voice streams and keep playback cancellation off UI waits 1 day ago
evgeny dd8041b30f Retain picker cache ownership on native PM rejection 1 day ago
evgeny fe9441d63b Acknowledge Android PM file ownership only after native enqueue 1 day ago
evgeny 05476fb102 Test Qt voice file validation and decoder cancellation 1 day ago
evgeny 6ccf71b20d Exercise separate conversations and codec format boundaries 1 day ago
evgeny 418329962d Test pinned preparation targets, worker ownership and typed offline media 1 day ago
evgeny f2ff3ee5ab Report preparation rejection and release abandoned owned sources 1 day ago
evgeny 3b0609fe76 Reject corrupt voice containers and remove unused synchronous playback 1 day ago
evgeny 8e108425ae Track Android video probing and clean rejected cache files 1 day ago
evgeny 20322e7ecb Show PM voice and video delivery status in both shared renderers 1 day ago
evgeny 5092bdf9fe Validate capture format and diagnose capture thread failures 1 day ago
evgeny be8dbcf25e Remove duplicate GUI voice encoding and keep shared worker codec 1 day ago
evgeny 72233d2365 Quiesce Android capture before detaching PCM and consume PM cache files 1 day ago
evgeny 79f0f2a07f Keep Android media preparation status scoped to its conversation 1 day ago
evgeny 225e11265f Connect Qt preparation notifications to the conversation view 1 day ago
evgeny 6da6471204 Apply task-scoped media preparation events to the shared Qt composer 1 day ago
evgeny e4a57970f0 Show preparation state for the current Qt conversation only 1 day ago
evgeny 55b21fdcda Forward pinned attachment preparation events to Qt 1 day ago
evgeny 51a820ac82 Track PM encryption preparation and clean cancelled source files 1 day ago
evgeny c900adbc69 Report pinned preparation results and clean abandoned media outputs 1 day ago
evgeny b1ee7857f7 Identify media preparation events by conversation and task UUID 1 day ago
evgeny 318d72708c Preserve voice autoplay ordering while decoding in a worker 1 day ago
evgeny 172cc3a649 Update shared Qt playback controls after background decoding 1 day ago
evgeny d4a27d1975 Decode voice files asynchronously before starting Qt playback 1 day ago
evgeny c77cb9bb0a Adopt decoded voice buffers without large copies on the Qt thread 1 day ago
evgeny add016894b Use bounded independent Android cache files for each attachment 1 day ago
evgeny fd3fc971fa Return Android video recording to its original channel or PM 1 day ago
evgeny fef800ef7a Use the same Android media interface for channels and PM 1 day ago
evgeny 1cd366c0e1 Share Android conversation composition attachments and message lists 1 day ago
evgeny dbec6e75e8 Pin Android recording targets and stop capture outside the main thread 1 day ago
evgeny 80b4e6f3e9 Normalize PM voice and video into the Android message model 1 day ago
evgeny 6185869c6d Use fixed conversation targets for Android media preparation 1 day ago
evgeny 038fd173c2 Detach Android recordings for shared background preparation 1 day ago
evgeny 14935378bb Check queued worker completion cancellation and attachment codec tests 1 day ago
evgeny 0dc2b0135a Test typed PM delivery and strict voice attachment metadata 1 day ago
evgeny c58b92ecd7 Render typed PM media using shared Qt message roles 1 day ago
evgeny a281165450 Keep channel download commands out of the shared message delegate 1 day ago
evgeny 4f76b4d765 Dispatch Qt attachments using the target captured by the composer 1 day ago
evgeny d1f03f2f80 Enable shared Qt voice and video composition for PM 1 day ago
evgeny 0d3f2952c1 Move Qt recording buffers to background preparation without copies 1 day ago
evgeny ba409427c6 Bound media worker concurrency and reject incomplete file copies 1 day ago
evgeny 1b3794e9f9 Include common attachment preparation in both core targets 1 day ago
evgeny 555b6fcf86 Prepare voice and video before dispatching to a fixed conversation 1 day ago
evgeny 1bd310c859 Detach recorder compressors while preserving capture settings 1 day ago
evgeny 20acf975d9 Return final video metadata from the preparation worker 1 day ago
evgeny 984b9e8123 Keep PM attachment kinds scoped to files voice and video 1 day ago
evgeny c7310e1fad Register shared voice encoding in core build targets 1 day ago
evgeny a5f90ec207 Share voice file encoding and final PCM metadata across clients 1 day ago
evgeny 72d1d3f582 Validate PM attachment metadata before committing messages 1 day ago
evgeny 7b8e2cd96c Transfer temporary source ownership before launching PM workers 1 day ago
evgeny c1185b62c1 Carry typed encrypted metadata with PM file transfers 1 day ago
evgeny 7357a63fdf Define bounded typed attachment metadata for PM and GUI 1 day ago
evgeny adff64e483 Deliver media completions without polling or timer failure joins 1 day ago
evgeny 00e890e0c3 Reserve worker completions before starting background tasks 1 day ago
evgeny 142eefaf86 Record completed PM media integration and validation results 1 day ago
evgeny 7e780bb6a7 Document PM file custody, worker ownership and client integration 1 day ago
evgeny ef5cd63d9f Set injected control packet lengths so replay and quota checks execute 1 day ago
evgeny 7bd3e0d4de Test PM media rollback, quota, snapshots and transfer cancellation 1 day ago
evgeny 18ee21cf93 Allow receipt-authorized custody cleanup after role revocation 1 day ago
evgeny 934f1ccd1f Use the existing diagnostic macros for desktop PM errors 1 day ago
evgeny a90274f24c Test event-loop heartbeat during sustained PM file encryption 1 day ago
evgeny dc1110af4d Add Android PM attachment selection, saving and delivery labels 1 day ago
evgeny 1457275139 Update Android PM screens from asynchronous core events 1 day ago
evgeny 52e38ff5d6 Parse PM snapshots and delivery states in the Android repository 1 day ago
evgeny 8403697013 Post Android PM reads and file sends to the core event loop 1 day ago
evgeny d9eee4e520 Bound and safely serialize PM conversation snapshots 1 day ago
evgeny e562896d63 Define asynchronous conversation snapshots for PM clients 1 day ago
evgeny 7fee3ec551 Render asynchronous PM history and submit desktop file sends 1 day ago
evgeny 1ceb09062f Show PM file delivery state without invoking group downloads 1 day ago
evgeny 570f3dfab3 Enable original file attachments in the desktop PM composer 1 day ago
evgeny 4cabbc32f5 Fit file chunks within the signed router packet limit 1 day ago
evgeny 3d6f9c1be4 Sign media test fixtures with the derived Ed25519 seeds 1 day ago
evgeny 76497481f7 Test direct and offline DM media with separate supernode and storage nodes 1 day ago
evgeny 139356bd18 Deliver DM history snapshots asynchronously as JSON events 1 day ago
evgeny 1cdead18de Expose bounded DM history snapshots with text and media delivery state 1 day ago
evgeny b47918fad5 Keep media publication and durable deletion off the event loop 1 day ago
evgeny 55c9d79a01 Integrate atomic media registration and orderly transfer/worker shutdown 1 day ago
evgeny d7a12b24bf Coordinate offline DM media custody and durable deletion on supernodes 1 day ago
evgeny 41fc5902be Implement durable DM downloads, media-node custody, quota and expiry 1 day ago
evgeny e47c7fb2ac Allow file owners to revoke active transfers on custody expiry or deletion 1 day ago
evgeny 84c148acff Prepare and publish target-encrypted DM files with exact media receipts 1 day ago
evgeny d559cbf473 Add configurable seven-day DM custody TTL and reserved storage quota 1 day ago
evgeny 5e9a9059c8 Bind signed file descriptors to atomic DM enqueue and receive 1 day ago
evgeny 27fa3a4812 Define independent DM media metadata, receipts and custody contracts 1 day ago
evgeny 3d0ab35efe Register file transfer and DM media services in the common core 1 day ago
evgeny f39ba840b8 Add bounded signed file transfers with per-operation CM ownership 1 day ago
evgeny 6c86e380a8 Record DM stage validation and pending media delivery work 1 day ago
evgeny c63343cf2b Stop DM test event loops promptly on scenario failure 1 day ago
evgeny bd550159f0 Verify DM queues drain beyond one delivery batch after restart 1 day ago
evgeny 43c89ca1ed Document implemented DM guarantees and remaining media integration 1 day ago
evgeny 1740bec94f Test mailbox admission, exact deletion and late PUT replay 1 day ago
evgeny 0e0f611d3c Check atomic DM enqueue rollback and explain sequence conflicts 1 day ago
evgeny bd05ab6d3a Verify DM media record boundaries and corrupted artifacts 1 day ago
evgeny 467d2e7605 Prepare DM media as authenticated records using target key 1 day ago
evgeny 10de4f1c71 Commit DM activity timestamps together with message state 1 day ago
evgeny 794d7b70f3 Authorize DM custody by signed group role instead of NAT type 1 day ago
evgeny e6c78c17b7 Keep DM test diagnostics focused on delivery state 1 day ago
evgeny 3cdb3e6580 Create DM router streams before checking queue capacity 1 day ago
evgeny febc3419b0 Exercise DM reordering, rollback and signed acknowledgments 1 day ago
evgeny 937ef9fcef Test CCM limits and offline DM custody across restart 1 day ago
evgeny 5800902a42 Deliver offline DM through durable supernode custody 1 day ago
evgeny cc072cceb2 Persist DM outbox and verify exact delivery receipts 1 day ago
evgeny 67bbf80769 Fix DM CCM length setup and separate media nonces 1 day ago
evgeny ce5f5bb7d0 Improve Android onboarding, member UI and PTT permissions 1 day ago
evgeny 577bfb4e6f Record independent audio startup checks and phone updates 2 days ago
evgeny fe1d29f007 Check refill below the 60 ms reserve including SoundTouch 2 days ago
evgeny fbf50b093d Document independent call audio startup and delivery reserve 2 days ago
evgeny 5b0262d53f Diagnose refill expectation with the corrected reserve 2 days ago
evgeny 4a2efe21c7 Start Android capture and playback independently 2 days ago
evgeny 7d9f13b8bc Test startup backlog independently of delivery jitter 2 days ago
evgeny 50e3db81ea Separate playback backlog from adaptive call reserve 2 days ago
evgeny d8745fd303 Measure call jitter from media delivery timestamps 2 days ago
evgeny bc797550d9 docs: record SM startup timings after phone update 2 days ago
evgeny 5ebab7072c docs: record call startup validation results 2 days ago
evgeny 9ff4e68246 docs: describe call audio startup and loss tone gating 2 days ago
evgeny 00666499fe android: start call capture before playback setup 2 days ago
evgeny 5487bc8c20 call: suppress loss tone until first received PCM 2 days ago
evgeny a3f6b6e86e docs: outline simple Android call startup improvements 2 days ago
evgeny 6f66df61f7 docs: record false audio loss tone during call startup 2 days ago
evgeny 72b5a9887f chatgui: measure duplex audio stalls and record SM call evidence 2 days ago
evgeny 076cc3c617 docs: record full recovery checks and root socket test 2 days ago
evgeny 91a69b939c docs: record conn_mgr recovery validation and check timeout limitation 2 days ago
evgeny 969c085085 tests: resend withdrawn relay advertisements during recovery 2 days ago
evgeny 426fc2f4e8 conn_mgr: verify route loss and document handle lifetime recovery 2 days ago
evgeny 464575ef44 conn_mgr: exercise persistent retries with valid signed node updates 2 days ago
evgeny a65ef30d78 conn_mgr: resume recovery when local sockets change 2 days ago
evgeny 8f5f2b089c conn_mgr: keep demand alive across bounded recovery attempts 2 days ago
evgeny 111432aa64 conn_mgr: race connection probes and report call path with correct RTT 2 days ago
evgeny 1c51df039f Fix call gap tone to follow 500ms of missing jitter PCM 2 days ago
evgeny 9e3a417059 Synchronize standby transitions and immediate keepalive phase announcements 2 days ago
evgeny e914a1bdc1 Improve Android member details contrast 2 days ago
evgeny b36c685cfa Show ongoing Android calls on the lock screen 2 days ago
evgeny 3c5c9604b3 Keep Android calls active independently of screen and UI 2 days ago
evgeny fb37ca747a 1 2 days ago
evgeny 2d21c53b4b Improve call buffering, CM handle lifecycle and radio PTT controls 2 days ago
evgeny 752c1f140c Add unified Android dark theme and improve UI contrast 2 days ago
evgeny db3cacd33e Rename Android app to vibechat 2 days ago
evgeny 7ebc282eb2 Prioritize supernodes in invites and update chat background and ringtone 2 days ago
evgeny 0d44ada922 Improve Android radio capture lifecycle, VAD and PTT controls 2 days ago
evgeny 52ffb91101 Fix TCP proxy use-after-free during synchronous close 2 days ago
evgeny 745ec3dbd2 android radio: preserve output route on reconfigure, link input/output settings 3 days ago
evgeny 15958a078e android: self-managed Telecom ConnectionService for call audio routing 3 days ago
evgeny 61bf235ef4 radio: PTT double-tap latch + single-tap off (in-chat & overlay); compressor initial gain = mid-range 3 days ago
evgeny 556b0bc7b7 call/radio: mic AGC compressor + selectable input device; proximity lock 3 days ago
evgeny f12f61d7ee call: add SpeexDSP AEC (acoustic echo cancellation) for desktop + Android 3 days ago
evgeny 1176411b79 radio: Silero VAD + auto-PTT; chatgui/android: app icon and background 3 days ago
evgeny 74ca98b38f proxy: rename pcb guard, dedupe abort; tasks: FIN_WAIT_2 leak 3 days ago
evgeny 961a234d7d proxy: detach lwIP pcb on local close, guard abort against freed pcb 3 days ago
evgeny a73ae13df4 lwip/proxy: trace PCB lifetime in debug category, drop old noisy debug 3 days ago
  1. 2
      .gitignore
  2. 33
      BUGS_proxy.txt
  3. 89
      BUG_bgp_reconnect.md
  4. BIN
      bg.png
  5. 34
      configure.ac
  6. 116
      doc/chat_call_arch.md
  7. 315
      doc/dm_arch.md
  8. 433
      doc/linux_audio_recovery.md
  9. 12
      doc/pm.txt
  10. 27
      doc/service_lifecycle.md
  11. 204
      doc/tasks.md
  12. 28
      lib/Makefile.am
  13. 421
      lib/audio_compressor.c
  14. 10
      lib/audio_compressor.h
  15. 2
      lib/debug_config.c
  16. 4
      lib/debug_config.h
  17. 130
      lib/ll_queue.c
  18. 12
      lib/ll_queue.h
  19. 26
      lib/memory_pool.c
  20. 238
      lib/miniaudio.h
  21. 301
      lib/silero_vad.c
  22. 76
      lib/silero_vad.h
  23. BIN
      lib/silero_vad.onnx
  24. 136918
      lib/silero_vad_model.inc
  25. 178
      lib/speex_aec.c
  26. 80
      lib/speex_aec.h
  27. 35
      lib/speexdsp/COPYING
  28. 160
      lib/speexdsp/_kiss_fft_guts.h
  29. 232
      lib/speexdsp/arch.h
  30. 448
      lib/speexdsp/fftwrap.c
  31. 58
      lib/speexdsp/fftwrap.h
  32. 170
      lib/speexdsp/include/speex/speex_echo.h
  33. 14
      lib/speexdsp/include/speex/speexdsp_config_types.h
  34. 126
      lib/speexdsp/include/speex/speexdsp_types.h
  35. 523
      lib/speexdsp/kiss_fft.c
  36. 108
      lib/speexdsp/kiss_fft.h
  37. 297
      lib/speexdsp/kiss_fftr.c
  38. 51
      lib/speexdsp/kiss_fftr.h
  39. 332
      lib/speexdsp/math_approx.h
  40. 1279
      lib/speexdsp/mdf.c
  41. 169
      lib/speexdsp/os_support.h
  42. 379
      lib/speexdsp/pseudofloat.h
  43. 743
      lib/u_async.c
  44. 12
      lib/u_async.h
  45. 8
      lib/u_async_doc.md
  46. 31
      src/Makefile.am
  47. 233
      src/call/call.c
  48. 8
      src/call/call.h
  49. 376
      src/call/call_audio.c
  50. 36
      src/call/call_audio.h
  51. 12
      src/call/call_headless.c
  52. 217
      src/call/call_jitter.cpp
  53. 39
      src/call/call_jitter.h
  54. 34
      src/call/call_jitter_window.h
  55. 2
      src/call/call_proto.h
  56. 5
      src/call/voice_sources.cmake
  57. 2
      src/chat/chat_core.h
  58. 18
      src/chat/chat_event.c
  59. 10
      src/chat/chat_event.h
  60. 15
      src/chat/chat_msg.c
  61. 9
      src/chat/chat_setting.c
  62. 6
      src/chat/chat_sync.c
  63. 11
      src/chat/invite_build.c
  64. 4
      src/chat/invite_build.h
  65. 9
      src/chat/member_sync.c
  66. 1202
      src/dm/dm_core.c
  67. 42
      src/dm/dm_core.h
  68. 116
      src/dm/dm_crypto.c
  69. 22
      src/dm/dm_crypto.h
  70. 662
      src/dm/dm_mailbox.c
  71. 32
      src/dm/dm_mailbox.h
  72. 318
      src/dm/dm_mailbox_media.c
  73. 23
      src/dm/dm_mailbox_media.h
  74. 1053
      src/dm/dm_media.c
  75. 60
      src/dm/dm_media.h
  76. 16
      src/dm/dm_media_priv.h
  77. 21
      src/lwip_tcp/lwip_tcp.c
  78. 21
      src/lwip_tcp/lwip_tcp_in.c
  79. 1
      src/lwip_tcp/lwip_tcp_priv.h
  80. 86
      src/media_async/attachment.c
  81. 30
      src/media_async/attachment.h
  82. 155
      src/media_async/attachment_send.c
  83. 32
      src/media_async/attachment_send.h
  84. 49
      src/media_async/media_async.c
  85. 7
      src/media_async/media_async.h
  86. 62
      src/media_async/voice_file.c
  87. 15
      src/media_async/voice_file.h
  88. 343
      src/media_delivery/file_transfer.c
  89. 34
      src/media_delivery/file_transfer.h
  90. 4
      src/proxy/proxy_protocol.h
  91. 128
      src/proxy/tcp_proxy_client.c
  92. 2
      src/proxy/tcp_proxy_client.h
  93. 16
      src/proxy/tcp_proxy_server.c
  94. 513
      src/radio/radio_audio.c
  95. 30
      src/radio/radio_audio.h
  96. 79
      src/radio/radio_jitter.cpp
  97. 44
      src/radio/radio_jitter.h
  98. 95
      src/radio/radio_vad.h
  99. 43
      src/routing_layer/conn_mgr.h
  100. 1002
      src/routing_layer/conn_mgr_core.c
  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"`.

BIN
bg.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

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}
"

116
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,61 @@ 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`. Явное завершение звонка прекращает
воспроизведение накопленной речи и включает тон завершения — на обеих платформах.
Android останавливает гудки до настройки звонкового аудиотракта. После выбора маршрута
capture/play потоки независимо создают и запускают AudioRecord/AudioTrack; каждый сразу обслуживает своё направление.
`start()` подтверждает запуск подготовки, ошибки устройств передаются владельцу на main через `onFailure`.
Ошибка любого направления завершает звонок. Общий `stop()` отменяет подготовку, разблокирует I/O и ждёт оба потока
до освобождения Android-устройств и native-аудиостека. Отдельные locks защищают подготовку/остановку каждого устройства.
Замер AEC планируется через 300мс после готовности обоих направлений и не задерживает воспроизведение.
Маршрутизатор использует один снимок входных/выходных устройств; обновляет его при старте,
событиях устройств/BT-профиля и явной переоценке. По завершении снимок освобождается.
INFO-логи Android показывают длительности настройки режима, регистрации callback, BT-профиля, выбора маршрута,
создания/старта устройств, первого чтения микрофона и первой записи в AudioTrack, а также готовность другого направления.
Общий `call_audio` логирует время ожидания первого PCM; запись в AudioTrack может содержать начальную тишину.
## 7. Буфер звонка и восстановление
`call_jitter` независим от `radio_jitter`: рация сохраняет свои правила задержки и сброса.
Буфер звонка хранит до 750 кодированных кадров (15с): 10с перебоя и 5с запаса.
Целевой запас — `clamp(max-min, 60, max_reserve_ms)` мс по изменению задержки доставки:
на входящем кадре накапливается разность интервалов приёма и отправки, десять секундных интервалов хранят её экстремумы.
Отправитель уже передаёт timestamp в CALL_MEDIA; смещение часов пиров исключается, переполнение uint32 учитывается.
Подготовка местного аудиоустройства и ускоренное воспроизведение очереди не изменяют оценку доставки.
Минимум/максимум глубины очереди для GUI собираются отдельно в аудиопотоке за те же десять секунд.
Верхний предел задаётся `call_jitter_max_reserve_ms`: 500–2000 мс, по умолчанию 1000 мс.
Настройка есть в аудиопараметрах desktop и Android и применяется к следующему звонку.
Начальное и повторное накопление ждёт этот запас; увеличение запаса не прерывает текущую речь.
Ускорение зависит от превышения запаса. При отсутствии входящих кадров обычное воспроизведение
может исчерпать запас: это не жёсткий запрет воспроизведения ниже цели.
Статистика раз в секунду отображается в desktop-окне и Android-экране звонка (под кнопками): глубина, запас, минимум/максимум,
темп, RTT, удаления при переполнении и недостачи PCM. Диагностика буфера — категория `call=debug`.
После восстановления кадры последовательно декодируются; темп плавно растёт до 1.6x
без изменения высоты голоса: линейный предел роста 1.0x→1.6x за 500мс воспроизведения.
При уменьшении накопления целевой темп снижается к 1.0x; скорость снижения ограничена тем же пределом.
Пауза аудиоустройства не сбрасывает буфер. Переполнение удаляет старейший кадр с WARN и счётчиком.
Роутер сохраняет последовательность и неподтверждённые данные при смене пути.
Для сервиса звонка срок без прогресса ACK увеличен примерно до 20с; прочие сервисы сохраняют прежний срок.
Перезапуск удалённого процесса/роутерной сессии не является кратковременным перебоем пути и может сбросить данные.
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. Тоны и диагностика
@ -141,12 +160,15 @@ API: `ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy`.
устанавливается) — гудок «соединение» (короткие бипы);
- фаза 2 «звоним» (`RINGING`: получен RINGING, у пира идёт вызов) — длинные гудки (ringback).
Гудки генерируются платформенно-нативно: desktop — цикличные MP3 через `SoundManager`
(`call_connecting`/`call_ringing`), Android — `ToneGenerator`
(`TONE_SUP_DIAL`/`TONE_SUP_RINGTONE`). Переключение — по `CHAT_EVT_CALL_RINGING`,
(`call_connecting`/`call_ringing`), Android — цикличные MP3 через `CallRingback`/`MediaPlayer`.
Переключение — по `CHAT_EVT_CALL_RINGING`,
остановка — по `CALL_ACCEPTED`/`CALL_ENDED`/`CALL_ERROR`/hangup.
- **Сталл >500мс** (нет входящих медиа) → в `call_pull_pcm` микшируется
- **Начальное ожидание первого PCM** → тишина без сигнала потери звука. Подготовка удалённого микрофона
и начальное накопление jitter не считаются перебоем. Существующий таймаут 20с без медиа действует с ACTIVE.
- **Недобор PCM ≥500мс после начала воспроизведения** → в `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 +181,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: текущий маршрут/наличие гарнитуры)
```

315
doc/dm_arch.md

@ -1,93 +1,222 @@
# DM — прямой p2p чат между двумя пользователями группы
## 1. Цель
Возможность начать личный чат с любым пользователем доступных групп.
Пока target offline сообщения временно хранятся на storage-узлах.
## 2. Ключевые решения
- Отдельная DM-подсистема (НЕ канал: без TOPO_GROUP/member_sync/merkle).
- Идентичность детерминированная: одна ветка на пару, согласование без handshake.
- E2E-шифрование содержимого сразу.
- Offline-хранение: dm_mailbox на storage-узлах с флагом dm_storage.
- Доставка: fallback — сперва проверка наличия узла в BGP-группе; доступен →
прямой send; нет связи → storage.
## 3. Идентичность и ключи (детерминированные)
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
shared = X25519(my_x25519_priv, peer_x25519_pub)
content_key = SHA256(shared || "utun_dm_content")
Обе стороны выводят одинаковые значения из своего privkey и pubkey пира
(известен из общей группы). Смена x25519 у пира = новая ветка.
## 4. Модель данных (SQLite, общий chats.db)
dm_conversations(conv_id TEXT PK, peer_node_id, peer_x25519 BLOB, peer_ed25519 BLOB,
peer_name TEXT, last_out_seq INT, last_in_seq INT, created_at INT, last_ts INT)
dm_messages(conv_id TEXT, dir INT, seq INT, ts INT, author INT, ct TEXT,
data BLOB, sig BLOB, PRIMARY KEY(conv_id,dir,seq))
dm_mail(recipient INT, sender INT, conv_id TEXT, seq INT, ts INT, data BLOB, sig BLOB,
ttl INT, PRIMARY KEY(recipient,sender,seq))
Два независимых направленных потока (A→B, B→A) с монотонным seq.
Без chain-хеша: для 2 пиров достаточно seq + catch-up по диапазону.
## 5. Доставка
1. Проверка доступности: peer присутствует в BGP-группе (общая группа).
2. Доступен → прямой send (сервис ETCP_RT_ID_DM = 0x34), ждём DM_ACK{conv_id,seq}.
Нет ACK за таймаут → fallback в mailbox.
3. Нет связи → MAIL_PUT на 1..N storage-узлов.
4. Catch-up при connect: DM_HELLO{conv_id,last_out_seq,last_in_seq} → пир досылает
(in_seq+1 .. out_seq).
5. Dedup: вставка только при seq > last_in_seq.
## 6. E2E-шифрование
data_enc = nonce(13B, счётчик по seq) || AES-CCM_ct || tag
secure_channel уже умеет AES-CCM. Storage/relay видят только шифротекст.
## 7. Storage-узлы (dm_storage)
- Новый флаг dm_storage=yes в adm_tags (блок B владельца), распространяется
существующим member_sync без нового синка.
- DM читает adm_tags мемберов общих групп (peers_<ch>) и кэширует storage-узлы.
- Протокол mailbox (сервис ETCP_RT_ID_DM_MAILBOX = 0x35):
MAIL_PUT {recipient,sender,conv_id,seq,ts,data,sig}
MAIL_PULL {recipient, per-sender cursor}
MAIL_ACK {up_to_seq}
TTL 7 дней + лимит объёма, dedup по (recipient,sender,seq).
## 8. Старт и anti-spam
- dm_start(peer): создать беседу (вывести conv_id/ключи), обеспечить прямое
соединение (переиспользовать chat_sync_connect_node/node_conn_direct).
- Пир автопринимает на первом DM_HELLO/сообщении, только если отправитель в
общей группе.
## 9. Файлы и интеграция
| Файл | Назначение |
|------|-----------|
| src/dm/dm_core.c/h | беседы, send/recv, таблицы, bind 0x34, conn-события, fallback |
| src/dm/dm_crypto.c/h | derive + AES-CCM |
| src/dm/dm_mailbox.c/h | storage-узел, push/pull/ack, bind 0x35 |
Плюс:
- src/chat/chat_member.c — бит CHAT_MEMBER_FLAG_DM_STORAGE + парсинг dm_storage.
- utun_instance.c — dm_core_init/destroy.
- chat_core — API dm_start/send/list + события CHAT_EVT_DM_*.
- headless CLI — dm list/send/messages.
- src/Makefile.am — новые источники.
## 10. Сервисы ETCP
ETCP_RT_ID_DM = 0x34
ETCP_RT_ID_DM_MAILBOX = 0x35
# DM: доставка сообщений и файлов
Личные беседы принадлежат `src/dm/`. Общие CHAT-группы используются для знакомства,
проверки ролей и маршрутизации; группа не становится идентичностью беседы.
## Реализовано
- История и постоянная исходящая очередь сообщений в общей SQLite ядра.
- Доставка через живой маршрут router, включая BGP-транзит.
- При отсутствии маршрута — подписанный PUT доступному суперузлу общей группы сторон.
- Приём после проверки подписи и расшифровки, подтверждение только после commit.
- Доставка с суперузла после возвращения получателя, независимо от отправителя.
- Точное удаление промежуточного сообщения и сохранение квитанции для поздних повторов.
- Подготовка, шифрование, проверка и durable публикация медиа в рабочих потоках.
- Прямая передача файлов либо custody на отдельном M, доставка после restart и подтверждённое удаление.
- TTL и квота M; вложения и статусы доставки в Qt и Android.
- Голосовые Opus и видео MP4 с зашифрованными метаданными; общий ввод и отображение с каналами.
## Идентичность и шифрование
```text
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
content_key = SHA256(X25519(my_private, target_public) || "utun_dm_content")
msg_nonce = SHA256(conv_id || author || seq)[..13]
media_nonce = SHA256("utun_dm_media" || author || media_id || part)[..13]
```
Сообщения и медиа используют один content_key target, без ключей отдельных файлов.
AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная порция plaintext — 65535 байт;
порция медиа — 32768 байт. UUID медиа новый для каждой подготовки. Повтор доставки
использует неизменяемый ciphertext. Повторная подготовка после ошибки требует нового UUID.
Каноническое тело сообщения:
```text
[conv:8][seq:8][ts:8][author:8][ct_len:1][ct][enc_len:2][ciphertext+tag][author_sig:64]
```
Автор подписывает все поля до sig. conv связывает автора с получателем.
Для `ct=file` перед подписью добавляется descriptor `[UUID:16][plaintext_size:8][SHA256(ciphertext):32]`.
В зашифрованном теле находится `attachment_info`; пути хранения строятся только из UUID.
Размер plaintext сообщения ограничен 1024 байтами. seq — положительное int64.
```text
[kind:1][name_len:2][duration_ms:4][width:2][height:2][UTF8_name:name_len][waveform:100, только voice]
```
Поля метаданных многобайтной длины имеют big-endian формат. kind: 1=file,
2=voice, 3=video. Имя — basename до 255 байт, без разделителей, управляющих символов
и некорректного UTF-8. FILE имеет нулевые duration/dimensions; VOICE — положительную
длительность и 100 символов уровней waveform; VIDEO — длительность и размеры.
Длительность ограничена 24 часами, размеры — 16384. Метаданные проверяются до
локального commit и до подтверждения получателем; S/M видят только ciphertext.
Медиа ciphertext — последовательность порций с отдельным tag каждой порции.
Последняя порция короче; пустой файл содержит один tag. Размер plaintext и SHA256
ciphertext должны входить в подписанный manifest. Потоковая расшифровка проверяет все
tags, точный размер и хеш; владелец публикует временный файл только после успеха
и собственного durable сохранения.
## Владение и данные
- `dm_core`: беседы, история, outbox и подтверждение получателя.
- `dm_mailbox`: промежуточные сообщения суперузла и квитанции.
- `dm_crypto`: вывод ключей, nonce, CCM и файловые примитивы без БД/сети.
- `dm_media`: файлы endpoint, загрузки и отдельная custody M.
- `dm_mailbox_media`: durable задания S, выбор M и повтор удаления до DELETE_ACK.
- `file_transfer`: подписанная передача байтов, backpressure и отдельный CM handle каждой операции.
- `attachment`: формат, валидация и тип метаданных, без сети и БД.
- `voice_file`: общий Opus-кодек отправки; `attachment_send` готовит файл в worker
и передаёт результат отдельно в `chat_core` либо `dm_media`.
```text
dm_conversations(conv_id, peer_node_id, peer_x25519, peer_ed25519, peer_name,
group_id, last_out_seq, last_in_seq, created_at, last_ts)
dm_messages(conv_id, dir, seq, ts, author, ct, data, sig)
dm_outbox(conv_id, seq, body, retry_at)
dm_pending_messages(recipient, sender, seq, msg, group_id, retry_at)
dm_mail_receipts(recipient, sender, seq, receipt, group_id)
```
Запись исходящего сообщения, увеличение seq и запись неизменяемого body в outbox —
одна транзакция. При ошибке откатывается всё, сообщение не выходит в сеть.
Приём, создание/обновление беседы и история — также одна транзакция.
Dedup — точный ключ (conv,dir,seq) с проверкой сохранённой подписи. last_in_seq —
статистика максимального номера, не курсор подтверждения: seq=99 после seq=100
сохраняется. Тело с другой подписью для уже сохранённого seq отклоняется.
У каждой локальной очереди таймер 1с; порция обработки до 32 задач, повтор задачи
через 5с. retry_at предотвращает голодание хвоста очереди. Перед отправкой проверяется
backpressure router. Потеря пути сохраняет durable задачу; остановка отменяет таймеры,
но сохраняет БД. При старте очередь продолжает работу.
## Подтверждения и протокол
Сервис `ETCP_RT_ID_DM=0x34`: MSG=1, RECEIPT=2.
```text
["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(message):32][recipient_sig:64]
```
Квитанцию создаёт только получатель после commit. Подпись охватывает первые 72 байта.
Отправитель удаляет outbox только после проверки подписи и точного хеша тела.
История беседы сохраняется. Подтверждение транспорта router не заменяет квитанцию.
Сервис `ETCP_RT_ID_DM_MAILBOX=0x35`:
```text
PUT=1 [recipient:8][message]
DELIVER=2 [message]
RECEIPT=3 [receipt:136]
STORED=4 [conv:8][seq:8]
```
Все пакеты идут с подписью router; unsigned control отклоняется.
PUT принимается только от автора, при роли supernode=yes у локального узла
в общей группе автора и получателя. Роль берётся из подписанного adm_tags;
node_type=4 — производная сетевой классификации, для авторизации не используется.
storage=yes само по себе не даёт права хранить сообщения.
STORED подтверждает durable запись суперузлом, outbox до квитанции получателя остаётся
у автора. Суперузел повторяет DELIVER при появлении живого маршрута. RECEIPT проверяется
по точному сохранённому телу; запись квитанции и удаление копии — одна транзакция.
Квитанция пересылается автору. Поздний PUT возвращает её и не возрождает задачу.
dm_mail_receipts содержит только служебные квитанции; доставленное содержимое
с промежуточного узла удаляется. Ранее использовавшийся dm_mail/PULL/HELLO и
диапазонные ACK не поддерживаются: это devel, совместимость не сохраняется.
TTL текстовых сообщений не введён; недельный TTL из pm.txt относится к медиа.
## Доставка медиа по pm.txt
1. Общая передача байтов: явные peer/group/file/size, backpressure и CM handle
на всю активную операцию. Владение handle сохраняется после DOWN/TIMEOUT.
Используется `ETCP_RT_ID_FILE_TRANSFER=0x37`. Один callback читает/отправляет
не более 8 КиБ, продолжение через `call_soon`; inflight ограничен 16 пакетами.
2. Отдельный owner dm_media: подготовка ciphertext, manifest, отправка/скачивание,
временные файлы, TTL и лимит хранения. Каталоги dm_media/ и dm_pending/ вне
группового обхода очистки orphan-файлов; групповые объявления HAVE_BLOCK не используются.
3. Суперузел сначала durable регистрирует сообщение и media job, затем выбирает M.
M скачивает ciphertext у A и сообщает S подтверждённое хранение, место и expiry.
4. Текстовая квитанция и квитанция медиа независимы. Получатель подтверждает медиа
после проверки, публикации файла и commit. S хранит задачу удаления у M до DELETE_ACK;
поздний STORED не возрождает завершённую доставку.
5. TTL медиа `dm_media_ttl_sec` по умолчанию 604800 с; квота `dm_media_storage_mb`
по умолчанию 1024 МиБ. Настройки на M. Новая custody отклоняется при
недостатке квоты; уже подтверждённые копии не вытесняются молча раньше срока.
6. Stop снимает новые задачи и закрывает transfer/CM handles, затем ждёт
media_async с живыми owner-контекстами. Группы уничтожаются после owners.
Control `ETCP_RT_ID_DM_MEDIA=0x38`: OFFER, STORE, STORED, RECEIPT, DELETE,
DELETE_ACK, EXPIRED, REJECT; точные форматы заданы в `dm_media.h` и реализациях owners.
Подпись router обязательна. Media receipt содержит `DMFILE01`, conv, seq, author,
recipient, UUID и SHA256 полного подписанного сообщения; B подписывает первые 88 байт.
Отдельные таблицы `dm_files`, `dm_custody`, `dm_custody_done`, `dm_media_jobs`,
`dm_media_holders` сохраняют состояния и tombstones. Все выданные grants M
учитываются при удалении; повторный STORE не продлевает TTL.
Qt и Android получают историю через события ядра: 41 — conv + JSON сообщений,
45 — JSON бесед. Снимки ограничены 200 записями. Отправка вложения вызывает
`dm_send_file` в uasync; длительная обработка выполняется в `media_async`.
Android копирует выбранный URI и сохраняет полученный файл через `Dispatchers.IO`;
Qt сохраняет файл из контекстного меню в отдельном потоке. Выбор «как файл» сохраняет
оригинал; «как видео» в Qt использует общий FFmpeg worker и метаданные конечного MP4.
Android записывает MP4 через CameraX; импорт MP4 проходит через платформенный
MediaMetadataRetriever в IO. Другие контейнеры на Android отправляются как файлы.
Голосовая запись отдаёт worker собственный PCM либо компрессор без копирования
в GUI/uasync; worker выполняет flush, Opus-кодирование и вычисление waveform.
Кодирование Qt и Android использует один `voice_file`. Адресат фиксируется до
записи, выбора URI и подготовки; завершение не читает текущую беседу GUI.
Подготовленный PM-файл и Android cache удаляются после копирования, ошибки или отмены;
пользовательский исходник Qt остаётся во владении пользователя.
Событие 46 `[target_len:1][target][job_UUID:16][state:1]` сообщает начало (0),
готовность (1) и ошибку (2). Target канала — числовой ID, PM — `dm:<conv_id>`.
Подготовка, доставка и воспроизведение имеют независимые состояния. UI отображает
только задания текущей беседы. Состояния файла и delivery receipt приходят в JSON
41; `content_type`, duration, dimensions и waveform преобразуются в общие роли
сообщения. Канальные скачивания вызываются контроллером только для каналов;
PM-файлы доставляются автоматически через `dm_media`.
Android использует общие `ConversationComposer`/`ConversationMessages` для обеих
бесед; Qt — `InputBar` и `MessageDelegate`. Превью Qt создаются в общем пуле
с двумя workers; декодирование длинного Opus-файла выполняется вне GUI. Оба плеера
проверяют заголовок, пакеты и end marker; Qt ограничивает PCM буфер 256 МиБ.
## Проверки
`test_dm`: симметрия ключа target, CCM round-trip, пустое тело, пределы длины,
неверный ключ/tag, разделение nonce; потоковые файлы на границах 32 КиБ,
неверные author/UUID/hash, обрезание и лишние байты.
`test_dm_e2e`: реальные A–C–B соединения и BGP; нет физического A–B линка.
Проверяет out-of-order, dedup/conflict, подписи, SQL rollback при приёме и enqueue,
ACK чужого тела, отказ mailbox без роли, точное удаление и поздний PUT.
Первый сценарий передаёт 40 offline-сообщений (больше порции pump) и перезапускает источник с pending outbox; второй перезапускает
суперузел и доставляет первое сообщение при выключенном источнике.
`test_dm_media`: реальные A/B/S/M с раздельными ролями supernode и storage;
прямая передача, custody, restart S/M при выключенном A, точный файл на B,
удаление копии и тела S, поздние STORE/STORED, TTL, отказ по квоте, неверная квитанция,
SQL rollback регистрации, состояния JSON и отмена операции с CM handle.
Голосовое передаётся напрямую; метаданные видео сохраняются после offline custody
и restart S/M при выключенном источнике.
`test_dm_worker`: повторное шифрование 8 МиБ не менее 0,5 с и проверка расшифровки;
work выполняется вне uasync, done в его потоке, heartbeat продолжает работать.
Destroy отменяет уже posted completion, callback вызывается ровно один раз.
`test_attachment`: строгий формат, повреждения и граничные длины, реальное
кодирование/декодирование 50 Opus-кадров, длительность и waveform.
`test_attachment_send`: выбранная беседа после смены текущего target, освобождение
PCM в worker, остановка до completion без регистрации и удаление отклонённого cache.
Qt `test_voice_file`: реальные PCM/Opus, повреждённые файлы и отмена async decoder.
Проверка 01.10.2026: чистые сборки Qt и Android APK; полный `check.sh` —
117 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root), proxy/burst/load PASS.
Qt CTest: 7 PASS, 0 FAIL; Android APK собран после `clean`.
В тесте worker максимальный интервал heartbeat составил 10,3 мс.
Интерактивная проверка GUI на телефоне и десктопе не выполнялась.

433
doc/linux_audio_recovery.md

@ -0,0 +1,433 @@
# Linux: диагностика и восстановление аудио
Проверка 2026-10-02: desktop chatgui, miniaudio/PulseAudio, общий голосовой стек,
звонки, рация и запись сообщений. Исправления обработки PCM, владения устройствами
и автоматическое восстановление реализованы. Ниже сохранены исходные результаты
аудита; раздел «Реализация» описывает действующие контракты и проверки.
## Вывод
До исправлений тракт был некорректен при сбоях. Самое существенное: воспроизведение
звонка зависит от наличия микрофонных кадров в duplex-ring. Его опустошение
останавливает вызов пользовательского callback, оставляя хвост выходного буфера
незаполненным. Восстановление jitter-буфера не исправляет остановившееся устройство.
Отдельного управляющего механизма восстановления устройств не было.
В реализации восстановлены три гарантии: каждый запрос output полностью заполнен; обработка
одного callback всегда ограничена; остановка/пересоздание устройства выполняются
вне аудиопотока и не уничтожают сетевую сессию звонка.
## Что установлено по работающим клиентам
В локальном старом логе звонка `132e4a9698f47bdf` с Samsung SM_A525F наблюдались
паузы callback примерно 0,5–2,6 секунды при зарегистрированной обработке 0–2 мс.
Входящий буфер desktop достигал 15 секунд, росли потери, включалось ускорение 160%.
На Samsung захват продолжал давать около 50 кадров/с, playback регистрировал
underrun и тоны разрыва. RTT порядка 1–4 мс не объясняет эти паузы сам по себе.
Это признаки проблемы обслуживания локального аудио, но ещё не доказательство
конкретной причины зависания PulseAudio. Старое измерение исключало время
синхронных DEBUG-вызовов между концом обработки и обновлением timestamp.
Новая диагностика охватывает весь callback, включая эти логи.
Работающий GUI не перезапускался, поэтому новый инструмент ещё не записал этот
сбой на реальном звонке. Лог Samsung сохранён в `/tmp/sm-audio-current.log`.
## Найденные дефекты и ограничения до исправлений
| Место | Поведение и последствия | Необходимая доработка |
| --- | --- | --- |
| `lib/miniaudio.h`, `ma_device__handle_duplex_callback_playback()` | При пустом capture-ring `acquire_read` возвращает success с нулём кадров. Выполняется `break`, output остаётся частично незаполненным, call callback не вызывается. PulseAudio затем отправляет весь mapped buffer. | Обеспечить полный output и продолжение playback при отсутствии capture. Нельзя делать commit_read для синтетических кадров. |
| `ma_device_on_write__pulse()` | Успешная запись с нулём обработанных кадров не уменьшает остаток цикла. Возможно бесконечное вращение с блокированием того же PA mainloop, включая capture. | Немедленно выходить при отсутствии продвижения, возвращать управление mainloop. Логировать состояние и запрос, не пытаться записывать до успеха в этом callback. |
| `ma_device_on_read__pulse()` | PA hole отбрасывается без подачи соответствующего количества нулевых кадров. Из capture timeline исчезает интервал, duplex-ring может опустеть. | Сохранять длительность hole тишиной либо явно обозначать discontinuity независимому capture-тракту. |
| `lib/speex_aec.c`, `speex_aec_feed_playback()` | Обрабатывает только остаток одного 960-sample кадра, отбрасывая хвост входного блока. Контракт разрешает произвольный count. | Циклом потреблять весь count, складывать все полные кадры, сохранять остаток. |
| `CallAudioEngine::setPlaybackDeviceIndex()` | После неудачного пересоздания `m_active` остаётся true при отсутствии устройства; `start` того же звонка возвращает успех. | Разделить желание продолжать звонок и работоспособность устройства. Ошибка запуска переводит устройство в recovering. |
| `RadioAudioEngine::teardownDevice()` | Если playback init успешен, а start нет, uninit пропускается: он зависит от флага started. Объект удаляется вместе с незавершёнными ресурсами. | Разделить initialized и started; каждый успешный init обязательно завершать uninit. |
| `MainWindow::~MainWindow()` / `SoundManager::shutdown()` | Общий context освобождается вместе с engine, но звонок и рация здесь явно не останавливаются. Их singleton-объекты могут оставлять живые устройства. `m_context` также не обнуляется. | Сначала остановить все зависимые устройства и дождаться callbacks, затем освобождать общий context. |
| `AudioRecorder` | Callback выбирает глобальный `g_recorder`, игнорируя `pUserData`. Второй recorder, в том числе тест микрофона, заменяет получателя. Флаги/уровень/вектор waveform используются из GUI и аудиопотока без согласованной синхронизации. | Использовать `pUserData`; передавать уровни атомарно, waveform — безопасным snapshot; буфер записи должен иметь однозначного владельца. |
До исправлений первые два дефекта воспроизводились стендом без аппаратуры:
duplex оставлял sentinel в output при starvation, а PA-write без продвижения
требовал внешней остановки. AEC из 1200 + 720 samples сохранял один кадр вместо
двух. Теперь эти проверки требуют полного output, самостоятельного возврата
из callback и двух сохранённых AEC-кадров; результаты приведены ниже.
Capture-AEC также обрабатывает не более одного кадра за вызов. В звонке его
вызывают ровно с 960 samples, поэтому текущий call feed этот случай не нарушает.
При расширении API нельзя объявлять произвольный count без согласованного размера
выходного буфера. `speex_aec_reset()` уже существует: новый API для сброса самого
AEC не нужен, но нужен согласованный сброс всего локального voice-тракта.
## Чистота реализации
Этот раздел фиксирует исходный аудит; изменения владения и ограничение работы
callback описаны в разделе «Реализация».
Нормальная структура уже есть: GUI владеет устройствами, сервисный слой кодирует
Opus и обрабатывает jitter, сетевой поток доставляет пакеты. Нет необходимости
переписывать весь стек или заводить новый backend.
Однако границы владения не завершены: context фактически принадлежит engine
звуковых уведомлений, а используется независимыми устройствами звонка/рации.
Флаги active/started/initialized местами смешивают три разных состояния.
Выбор input применяется к recorder, звонок использует default input, рация —
default устройства. Output звонка и SoundManager тоже выбираются отдельно.
Индекс перечисления не является устойчивой идентичностью устройства после hotplug.
В radio capture fallback `useDefault` фактически повторяет ту же попытку:
`pDeviceID` и так NULL в обеих ветках. Callbacks рации запускаются раньше
подготовки capture-буфера и установки VAD; порядок запуска нужно привести к
«сначала состояние и буферы, затем устройства», чтобы исключить гонки при старте.
Callback не является строго realtime-safe. Capture удерживает `g_mtx` во время
AEC/AGC/Opus; отправка выделяет память и вызывает `uasync_post`. Playback вызывает
SoundTouch и код с возможным расширением контейнеров, mutex и синхронными логами.
Приём пакетов тоже может держать mutex при работе с jitter и логировании.
Плохие Opus-кадры могут последовательно выбираться из большой очереди в одном
pull: нужен предел работы на callback. Новая диагностика покажет, где это
действительно нарушает deadline. Перенос всего DSP в ещё один worker заранее
увеличит число очередей и точек синхронизации без установленной необходимости.
AEC получает reference только звонка. Звуки отдельного SoundManager в него не
попадают; delay по умолчанию фиксирован, не вычисляется по фактической задержке
устройств. Это ограничение качества эхоподавления, а не установленная причина
текущего зависания.
Jitter умеет повторно накопить резерв после сетевого underrun, но диапазон tempo
ограничен 1,0–1,6. Ускорение убирает избыток, а замедления для компенсации более
медленных часов отправителя нет. При устойчивом отрицательном дрейфе резерв будет
снова опустошаться. Это отдельная задача длительных звонков: после восстановления
устройств проверить наклон глубины буфера и при необходимости добавить небольшую
компенсацию дрейфа в обе стороны, не смешивая её с быстрым catch-up после паузы.
Принудительные 480 frames задают период 10 мс при 48 кГц. Собственный PA default
miniaudio — 25 мс с комментарием о проблемах PipeWire при периодах меньше ~20 мс.
Период backend и Opus-кадр 20 мс должны оставаться независимыми. Увеличивать
период до исправления AEC нельзя: 1200-sample блоки уже воспроизводят потерю хвоста.
После исправления разумно сначала проверить default backend и измерить задержку.
## Предлагаемое восстановление без большого усложнения
### 1. Сначала восстановить инварианты обработки и владения
Исправить полный output, zero-progress, AEC accumulation и uninit при неудачном
start. Перед освобождением context явно остановить recorder, звонок, рацию и
прочие его устройства. До остановки отменять recovery timer; после join можно
сбрасывать voice-ресурсы. Проверять возвраты feed/start/init и логировать причину.
Самый небольшой вариант — оставить один duplex-device: при нехватке capture
подать в call callback нулевой input, полностью сформировать playback и не
продвигать реальный capture-ring за синтетические samples. Это чинит непосредственный
дефект, но сохраняет общий жизненный цикл input/output и синтетический TX по
playback-clock.
Для устойчивости к исчезновению микрофона предпочтительнее два устройства
capture/playback внутри существующего `CallAudioEngine`, как уже сделано в рации.
Capture только накапливает 960 samples и вызывает feed; playback независимо
вызывает pull и всегда заполняет output. Это два handle с одним владельцем,
без нового DSP-worker или дополнительной PCM-очереди. Потребуется уточнить
потоковый контракт voice-слоя: один capture-writer и один playback-writer,
согласованный доступ к AEC, остановка обоих до release. Текущий `g_mtx` уже
защищает AEC, но остальные общие поля нужно проверить перед разделением.
### 2. Разделить краткий underrun и неисправность устройства
| Событие | Действие |
| --- | --- |
| Нет сетевого PCM, callback продолжает приходить | Тишина/существующий gap tone; jitter автоматически накапливает данные. Устройство не пересоздавать. |
| Один backend underrun, новые кадры принимаются | Подать следующий полностью заполненный буфер; собрать счётчик. Не сбрасывать AEC и не запускать restart на каждый xrun. |
| Hole/переполнение capture | Учесть потерянную длительность; краткую потерю пережить, длительную отметить discontinuity для AEC. |
| Поток failed/stopped или нет продвижения кадров длительное время | Перейти в recovering и пересоздать затронутое устройство вне callback. |
| Suspend/resume ОС | Учитывать suspend как отдельную причину, восстановление проверять после resume; не запускать частый restart-loop во время сна. |
| Потерян общий backend/context | Однократно остановить всех его пользователей, восстановить владельца, затем требуемые устройства. |
Один owner timer и состояния `Stopped / Running / Recovering` достаточны.
`wanted` хранит желание пользователя продолжать; generation меняется при stop,
новом звонке и смене устройства. Каждая отложенная попытка проверяет generation
и wanted — законченный звонок никогда не возрождается.
Callbacks только обновляют атомарные счётчики успешно обработанных кадров и
признак backend failure. Watchdog проверяет фактический прогресс отдельно по
направлениям. Тишина в PCM не означает остановку устройства. Reporter диагностической
очереди не должен становиться watchdog: его события могут теряться.
Начальные значения для проверки: timer 200 мс, подозрение при отсутствии прогресса
500 мс после startup grace; повторные попытки через 250/500/1000 мс с потолком
2 секунды. Это предлагаемые настройки, не доказанные универсальные пределы.
Suspend и фактический согласованный backend period должны учитываться отдельно.
Нельзя обещать bounded recovery, пока init/stop/uninit сами могут зависнуть:
сначала устранить spin-loop и проверить время backend-операций. Если они реально
блокируют GUI, вынести только управление устройствами в один owner-thread,
сохранив тот же автомат состояний.
На пересоздании сбросить capture-accumulator, AEC delay/filter и локальный PCM/
stretch, сохранив call ID и сетевую сессию. После длительной аппаратной паузы
устаревшую речь нужно явно отбросить до актуального резерва jitter, иначе
15 секунд накопленного звука при скорости 160% разгружаются примерно 25 секунд.
Этот сброс применять только при аппаратной discontinuity; обычный сетевой jitter
не должен автоматически приводить к отбрасыванию очереди.
Устройство хранить по backend ID либо как default, не по индексу. При его
исчезновении попробовать default и явно сообщить о смене; выбранное устройство
можно вернуть при появлении. Нужны ошибки с ID/именем/направлением и результатом
попытки. Для PA-ошибок следующий уровень диагностики — stream/context state и
`pa_context_errno`, потому что один `-1` от API не объясняет причину.
### 3. Проверить сценарии, а не только успешный старт
После реализации добавить проверки: capture starvation 20/500 мс и возврат;
hole; AEC блоки, пересекающие границу кадра; zero writable; ошибка записи;
исчезновение выбранного устройства и появление default; suspend/resume;
перезапуск звукового сервера; stop во время backoff; закрытие GUI при активном
звонке/рации. Проверять полный output, bounded очереди и работу callback,
возобновление прогресса, отсутствие callbacks после release и отсутствие
повторного старта после пользовательского stop.
## Как читать добавленную диагностику
В GUI-категориях включить `call=debug` либо задать отдельную строку в INI:
```ini
[debug]
call=debug
```
Обычные сводки идут на DEBUG, аномалии — WARN/ERROR; miniaudio info перенесён
на DEBUG, подробный backend log — TRACE. Все устройства используют категорию
call, включая recorder, radio и sound-engine; role и ID различают их в строках.
Не включать глобальный trace для всего проекта при измерении аудио.
Reporter раз в секунду печатает stage, tid, callbacks/s, requested frames/s,
максимальный интервал между входами, полное wall-time обработки и thread CPU-time,
незавершённый вход и время с последнего входа. `work_max` и `cpu_max` — отдельные
максимумы за окно, они не обязательно относятся к одному вызову.
| Stage | Что измеряется |
| --- | --- |
| loop | Один `pa_mainloop_iterate`, включая ожидание и вложенные callbacks. Долгое время само по себе не доказывает тяжёлый DSP. |
| read / write | Полный backend read/write callback. Frames/s здесь — запрошенные, не гарантированно обработанные кадры. |
| map | `writable_size` и `begin_write`; пустой/ошибочный выход тоже закрывает измерение. |
| submit | Вызов `pa_stream_write`. |
| duplex | Перенос capture-ring в duplex-output; detail содержит глубину capture-ring. |
| callback | Полный miniaudio data callback, включая код приложения и его синхронные логи. |
| feed / pull | Capture DSP/encode/send и playback jitter/stretch/tone в звонке. |
`unfilled_frames` — незаполненный output, `hole_frames` — потеря capture timeline,
`capture_drop_frames` — переполнение capture-ring, `no_progress` — PA-write без
продвижения. `backend_underruns` — отдельный callback самого PA-stream; это не
то же самое, что пустой сетевой jitter. `backend playback started/resumed`
показывает фактическое начало/возобновление playback. Notification содержит
числовые miniaudio type/state.
Первое событие каждого типа за окно имеет tid и CLOCK_MONOTONIC timestamp;
повторения суммируются. Для задержавшихся потоков reporter читает `/proc` wchan
и накопленные schedstat CPU/ready-wait/slices. Сравнивать соседние snapshots:
сами totals не являются длительностью текущей паузы. CPU-time намного меньше
wall-time означает ожидание/вытеснение; wchan помогает отличить poll от mutex/
I/O wait. Это снимок в момент отчёта, не исторический стек зависшего вызова.
Producer не выделяет память, не ждёт mutex reporter и не пишет диагностические
логи: ограниченная MPSC-очередь 8192 события, не более четырёх CAS-попыток.
Reporter не разыменовывает ma_device/pa_stream. Переполнение логируется как
`diagnostic events lost`; такое окно нельзя считать полным. Инструментация
добавляет получение часов/TID и атомарные операции, поэтому overhead ненулевой.
Частые события call jitter/AEC/TX теперь публикуются счётчиками и выводятся из
uasync вне аудиомьютекса. Часть существующих логов рации остаётся синхронной;
тракт не объявляется строго realtime-safe.
После close устройство удаляется из reporter: для устройств короче секунды
доступны lifecycle и первые аномалии, но не полная секундная сводка.
Новый binary собран в `tools/chatgui/build/vibechat`; для нового измерения нужен
его обычный перезапуск. Изменение файла binary не обновляет уже запущенный процесс.
## Реализация (2026-10-02)
`SoundManager` владеет общим контекстом и конечными выходами. На каждый фактический
выход приходится один микшер: PCM сообщений/видео, заранее декодированные сигналы,
готовый голос звонка и рации. После суммирования float PCM ограничивается, и один
и тот же stereo буфер отправляется устройству и AEC соответствующего микрофона.
Звук другого выхода в референс не входит. Default/явный backend ID объединяются;
PulseAudio также публикует идентификатор фактического маршрута. После server move
совпавшие выходы объединяются в GUI только после подтверждения фактического
маршрута; предположение о default не используется вместо PA snapshot. При временной коллизии дополнительный выход
отдаёт тишину до объединения. Изменение списка источников выполняется после stop
callback, с сохранением backend stream и выбранного сервером маршрута.
Устройства сохраняются как backend ID вместо индекса. При init/start failure
явного выбора пробуется default, причина логируется. Выбранный ID сохраняется;
вернуться к нему можно повторным выбором или при следующем recovery. Автоматический
опрос hotplug для возврата на первоначальный выбор не добавлен.
`AudioDevice` — владелец одного miniaudio device в GUI. Успешный init всегда имеет
парный uninit, включая ошибку start. Callback и его состояние готовятся до start.
Close останавливает/join-ит device worker перед освобождением callback. Отдельный
атомарный счётчик кадров контролирует прогресс. Таймер 200 мс проверяет stopped
и отсутствие продвижения 1 с; suspend продлевает grace, отдельный xrun не вызывает
restart. Backoff: 250/500/1000/2000 мс, сброс после продвижения.
Miniaudio не добавляет накопитель фиксированного периода: `noFixedSizedCallback`
включён, hardware chunks могут иметь любую длину. В PulseAudio физическое начало
блока определяется по timing snapshot с AUTO_TIMING_UPDATE/INTERPOLATE_TIMING.
Capture: monotonic now минус latency read index, без повторного вычитания длины
блока. Playback: now плюс latency write index. Учитываются задержка converter и
остающийся input cache. Один backend dispatch имеет один snapshot, позиции
последующих client callbacks продвигаются по числу кадров. В отсутствие snapshot
используется оценка по callback; источник времени и physical route логируются.
PA hole отдельно помечен как отсутствующий capture, а не как реальная тишина.
`VoiceAudioIo` создаётся только для звонка и рации. Capture и RX обрабатывают
независимые потоки. Hardware capture только копирует PCM в фиксированную MPSC-очередь;
RX worker выполняет decode/SoundTouch/тоны и поддерживает до 40 мс готового голоса.
В сервисном слое рации вычисление Silero держит только TX mutex: общий mutex
отпущен, поэтому VAD не блокирует RX; lifetime и порядок PCM/PTT сохранены.
Hardware output смешивает готовый PCM. Opus, AEC, VAD, AGC и растущие буферы записи
не исполняются в аппаратных callbacks. Обработка самого backend, PCM/resampler
узлов miniaudio и атомарные уведомления остаются в callback; строгая hard realtime
гарантия не заявляется.
Capture worker — единственный владелец AEC и порядка PCM/PTT/mute. Кадр AEC —
960 отсчётов на канал; 1/2 канала микрофона и 2 независимых канала референса.
Противофазное стерео не усредняется. История рендера содержит до 1,92 с PCM и
физические timestamps. Reference выбирается по времени микрофона; небольшое
различие часов компенсируется интерполяцией только референса. Реальная тишина
сохраняется как валидный PCM; при отсутствующем reference AEC пропускает микрофон
и не адаптируется. Потеря reference, смена маршрута/поколения и скачок часов
сбрасывают фильтр. Возраст capture-очереди ограничен 100 мс от enqueue, отдельно
от собственной задержки backend. Потери и очередь видны в сводках категории `aec`.
Mute применяется после AEC/AGC: фильтр продолжает получать настоящий микрофон,
а Opus получает нули только в отмеченной части кадра. Desktop запускает core
через `call_audio_start_prepared`: повторного core AEC нет. Raw API с core AEC
сохранён для Android/headless; смешивать политики в активном звонке нельзя.
У рации один автомат передачи: manual PTT или VAD. VAD читает непрерывный очищенный
сигнал до AGC, включая время ручного PTT. Передача использует отдельный накопитель
Opus-кадра; границы PTT внутри AEC-кадра сохраняются. При закрытии серии хвост
дополняется нулями только перед Opus и помещается в очередь перед FIN. Короткий
capture tail при stop проходит без AEC, а не дополняет непрерывный фильтр. Разрыв
захвата завершает старый сетевой хвост и сбрасывает VAD, исключая склейку через
потерянные отсчёты. Capture restart завершает принятую серию; новое прослушивание
того же канала отменяет старые BEGIN/media. FIN проверяет токен своего capture/burst
и поколение экземпляра ядра, поэтому не закрывает более новую серию и не обращается
к освобождённому ядру даже при повторном использовании его адреса.
TX ограничен 8 media-задачами, возраст — 200 мс. У рации дополнительно ограничено
16 control-задач; FIN выделяется заранее вместе с BEGIN. Если control-очередь
занята, новый BEGIN откладывается до освобождения; это логируется. Очередь PCM
резервирует 8 слотов для управляющих сообщений; producer не меняет consumer и
публикует слот только после копирования. Capture callback никогда не ждёт слот.
Отказ постановки GUI-команды завершает capture; таймер запускает recovery.
`AudioRecorder` получает raw microphone без AEC. Его worker обрабатывает AGC,
накапливает запись и публикует атомарный уровень/длительность. Переполнение или
ошибка обработки отклоняют запись с логом. Meter-only использует тот же capture,
но не сохраняет PCM и не создаёт голосовое сообщение. Stop: close device, drain
и join worker, затем передача владельца записи задаче attachment. Настройки
компрессора в ходе записи применяются к следующей.
PCM воспроизводится через независимые handles. Сообщение, видео и тест динамика
не останавливают чужой PCM; sample rate принадлежит источнику. Seek пересоздаёт
узел и очищает его read/resampler cache, сохраняя handle и владельца PCM. Cursor
вычисляется по реально обработанным графом кадрам, а не по prefetched datasource.
Pause отсоединяет узел и ждёт начатое чтение графа; resume подключает тот же узел,
сохраняя его PCM и cache. Соседние проигрыватели продолжают работать.
У асинхронного открытия видео/голоса проверяется generation.
Потоковый AGC использует фиксированные кольца истории и PCM, нулевой lookahead
и непосредственный process_frame без растущего output. У записи lookahead явно
100 мс; растёт только сохраняемый результат. Push/flush/configure возвращают
ошибки; flush проверяется перед кодированием вложения.
Порядок остановки голосового направления: close capture → join capture worker →
detach output → join RX worker → release core. При failed context синхронный сигнал
сначала закрывает всех клиентов, затем освобождает context. Mixer, PCM handles,
call ID и группа переживают recovery. Stop выключает recovery; таймер завершения
звонка проверяет generation. MainWindow останавливает клиентов до SoundManager
и сетевого ядра. Сетевые операции и регистрация audio ops выполняются в uasync.
PulseAudio control-waits ограничены 1 с на операцию; при timeout operation
отменяется до возврата, исключая callback с просроченным stack pointer. Partial
init освобождает контекст/mainloop/события. Failed stream/context содержит PA
errno в логе. Zero writable/zero converter progress возвращают управление.
Последовательность нескольких операций init/uninit всё ещё может задержать GUI
дольше секунды; отдельный device owner-thread не добавлен.
## Проверки реализации
`vibechat` и аудиотесты собираются через CMake. Набор проверки:
```sh
ctest --test-dir tools/chatgui/build --output-on-failure \
-R 'test_(linux_audio|audio_timing|audio_diagnostics|audio_recovery|audio_event_queue|voice_audio_io|voice_tx|call_jitter|radio_audio|radio_jitter|voice_file)$'
make -C tests test_audio_compressor test_speex_aec
./tests/test_audio_compressor
./tests/test_speex_aec
```
Добавлены отдельные проверки MPSC (80000 сообщений, reserve/control, слот ещё не
опубликован), точных PCM/PTT/mute границ, короткого хвоста, независимого RX при
задержке capture worker, противофазного stereo AEC, старого физического timestamp
при свежем enqueue, совпадения реальных Opus-пакетов с правильно дополненным
хвостом, rapid PTT, capture restart, отмены старого прослушивания и bounded control.
Проверяются manual/VAD handover
без второй серии и продвижение настоящего сервисного RX при заблокированном VAD.
Timing test проверяет read-index timestamp, несколько converter callbacks одного
блока и метку hole. Recorder тестирует raw PCM и отсутствие вложения в meter-only.
Speex проверяет подавление эха и сохранение double-talk также для двух микрофонов
с двумя независимыми каналами reference. В детерминированном синтетическом тесте
остаточная энергия эха — около 0,009 исходной; корреляция near-end — около 0,95.
Эти величины относятся только к тесту, а не к реальным акустическим условиям.
В `/tmp/utun-audio-asan` проверяются пять тестов нового тракта с ASAN/UBSAN и
LeakSanitizer; LeakSanitizer запускается вне песочницы. MPSC отдельно проверяется
ThreadSanitizer. Изолированный интеграционный запуск:
```sh
python3 tools/chatgui/tests/run_pulse_recovery.py
```
Итоговый прогон 2026-10-02: сборка `vibechat` успешна, CTest — 11/11,
ASAN/UBSAN/LeakSanitizer — 5/5, компрессор — 479/479, Speex — PASS,
MPSC под ThreadSanitizer — PASS. Timing-стенд прошёл ещё 100 последовательных
повторов. Пауза PCM проверяется также серией pause/resume при работающем соседнем
проигрывателе; она ждёт чтение узла, а не только меняет его флаг.
Тест создаёт собственные PipeWire/PulseAudio/WirePlumber в `/tmp`, отключает
аппаратные monitors и использует два виртуальных sink. Проверяет объединение
voice с общим выходом после `move-sink-input`, сохранение desired выхода звонка
при recovery общего выхода (включая server stream-restore), затем восстановление capture/PCM
после остановки и запуска своего PulseAudio. Звонок и рация сохраняют идентичность;
recorder и медиа продолжают работу. Системный аудиосервер не затрагивается.
Исторические проверки исходного recovery: полный `check.sh` — 119 passed,
0 failed, 1 skipped; proxy/burst/load интеграции прошли. По указанию пользователя
async-тесты исключены из дальнейших запусков. APK ранее собран offline; установка
на телефон не выполнялась.
Физический USB/Bluetooth hotplug, сон ОС, внешние звуки других приложений и качество
AEC в длительном реальном звонке этими проверками не подтверждены. Reference
охватывает звук этого приложения на том же выходе, согласно выбранному контракту.
## Референсы
Первичные исходники и документация просмотрены 2026-10-01; ссылки на master могут
изменяться. Используем принципы, а не копируем целиком архитектуру библиотеки.
- [PipeWire audio-src-ring.c](https://docs.pipewire.org/audio-src-ring_8c-example.html):
callback читает доступные кадры, заполняет остаток тишиной и возвращает полный
буфер; пополнение инициирует через событие управляющего потока. Хороший минимальный
пример поведения при пустой PCM-очереди.
- [PulseAudio stream API](https://github.com/pulseaudio/pulseaudio/blob/master/src/pulse/stream.h):
отдельные underflow/overflow/started/suspended/moved callbacks; peek различает
пустую очередь и hole с ненулевой длиной. Нужно разделять эти состояния в логах.
- [PulseAudio pacat.c](https://github.com/pulseaudio/pulseaudio/blob/master/src/utils/pacat.c):
underflow callback сообщает событие без пересоздания stream. При failed context
утилита завершает работу — это пример диагностики, не готовая self-healing система.
- [Mozilla cubeb PulseAudio backend](https://github.com/mozilla/cubeb/blob/master/src/cubeb_pulse.c):
явные context/stream states, пересоздание ошибочного context при инициализации,
согласованное отключение callbacks и доступ под mainloop lock; minimum latency
25 мс. Это полезный референс владения и границ управления, а не основание
автоматически заимствовать всю библиотеку.
- [miniaudio](https://github.com/mackron/miniaudio/blob/master/miniaudio.h):
init/start/stop/uninit запрещены внутри data callback из-за deadlock. Просмотренный
upstream тоже содержит break при пустом duplex-ring: обновление библиотеки само
по себе не решает этот найденный дефект.

12
doc/pm.txt

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

27
doc/service_lifecycle.md

@ -44,6 +44,9 @@ SQLite открывается ядром один раз: `db_path/chats.db`, л
закрывает медиа и сервисы, синхронизацию и группы, после чего освобождает chat core.
`media_async_destroy()` дожидается завершения native workers и вызывает каждый
ожидающий callback с `MEDIA_ASYNC_CANCELLED`, пока его контекст ещё существует.
Worker резервирует completion до запуска и передаёт его через `uasync_post_reserved`;
периодического polling нет. Destroy выполняется вне callbacks uasync, после join
отменяет pending post и завершает done ровно один раз. Uasync живёт до конца этой очистки.
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное
транскодирование или распознавание речи может задержать stop; принудительного
прерывания этих библиотек нет.
@ -57,3 +60,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 отменяет ожидающие задачи и очищает подписки старого ядра.

204
doc/tasks.md

@ -3,6 +3,205 @@
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
## Текущая задача
[+] **DM: надёжная доставка сообщений, первый этап pm.txt** — 01.10.2026:
атомарные история/seq/outbox, подписанные точные квитанции после commit,
live BGP-маршрут и durable очередь на суперузле с проверкой подписанной роли.
Повторы, out-of-order, rollback БД, неверные ACK и поздний PUT проверены;
40 offline-сообщений доставлены после restart источника, первый offline DM —
после restart суперузла при выключенном источнике. Копии удалены по квитанции.
Подготовка ciphertext медиа использует ключ target и независимые nonce порций;
проверены границы 32 КиБ, пустые файлы, неверные ключи/UUID/hash и повреждения.
Clean-сборка ядра/Qt, test_dm, test_dm_e2e и полный check прошли:
113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load PASS.
Текущие гарантии и оставшаяся интеграция: `doc/dm_arch.md`.
[+] **DM: сетевая доставка и pending-медиа по pm.txt** — 01.10.2026:
отдельные owners dm_media/dm_mailbox_media и файловый transfer с CM handle,
подписанные метаданные и независимая квитанция файла после durable приёма.
Прямая доставка или S→M custody, restart S/M при выключенном A, доставка B,
повтор удаления до DELETE_ACK; поздние STORE/STORED не возрождают копии.
TTL M по умолчанию 7 дней, резервирование квоты, отмена transfer и stop workers.
Копирование, шифрование, хеширование, проверка и durable файловые операции —
в media_async; отправка по 8 КиБ через call_soon. Тест 8 МиБ/0,5 с сохраняет
heartbeat uasync (максимальный интервал 10,3 мс в проверенном прогоне).
Qt/Android: оригинальные файловые вложения, сохранение и статусы доставки;
PM-снимки из ядра асинхронные. Чистые сборки Qt и Android APK прошли.
test_dm_media/test_dm_worker, rollback/quota/cancel и полный check:
115 PASS, 0 FAIL, 1 SKIP; proxy/burst/load PASS. GUI на устройствах не проверялся.
Протокол, владение и проверки: `doc/dm_arch.md`.
[+] **DM: голосовые/видео и общий GUI вложений** — 01.10.2026:
тип FILE/VOICE/VIDEO и метаданные зашифрованы ключом target; в transport остаётся
подписанный descriptor, без новых ключей и канального протокола в DM.
Общие attachment/voice_file/attachment_send: Opus, waveform, копирование и видео
готовятся в workers; completion через reserved post вместо polling. PCM передаётся
без копии; адресат закреплён до записи/выбора. Отмена освобождает собственные файлы.
Android: ConversationComposer/ConversationMessages для каналов и PM, CameraX MP4,
IO для URI/метаданных; Qt: общий InputBar/delegate, FFmpeg, фоновые превью/Opus.
Подготовка адресована беседе и UUID задачи; статусы доставки показаны для всех типов.
test_attachment/test_attachment_send проверяют кодек, формат, владение и отмену;
test_dm_media — voice напрямую и encrypted video metadata после offline restart.
Чистые сборки ядра/Qt/APK, 117 PASS / 0 FAIL / 1 SKIP в check.sh; proxy/burst/load PASS.
Qt CTest: 7 PASS, включая повреждения Opus и отмену decoder. APK не устанавливался,
интерактивная проверка устройств не выполнялась. Подробности: `doc/dm_arch.md`.
[ ] **uasync: ложные ERROR об утечке перед cleanup** — 01.10.2026:
test_dm_e2e и существующий test_conn_mgr проходят, но uasync_destroy()
пишет Timer leak до освобождения отменённых heap/immediate таймеров.
После cleanup счётчики сходятся, финальная проверка проходит. Перенести
предварительные счётчики в DEBUG; ERROR оставить для реального расхождения
после cleanup, отдельно показывать активные и отменённые таймеры.
[+] **Статус подключения звонка в Qt и Android** — фактический путь router:
прямое / reverse / indirect / нет подключения. Учитывается BGP fallback;
события только при изменении, общий таймер 1с, сброс при завершении.
Проверены смена путей настоящего звонка, медиа, ASAN, Qt, Android APK и полный check.
[+] **Единицы RTT в окне звонка** — `call_post_stats()` переводит
`ETCP_CONN.rtt_last` из 0.1мс в миллисекунды до отправки CALL_STATS.
Исправление общее для Qt и Android. DEBUG-категория `debug` раз в секунду
показывает исходный RTT, RTT в мс, транспортного соседа и метрики аудио.
[+] **Восстановление путей conn_mgr на протяжении жизни handle** — начальный
TIMEOUT отделён от срока цикла проб. Поздний обрыв запускает новый цикл;
рабочий резерв и NCD сохраняются, при отсутствии резерва INDIRECT стартует сразу.
Циклы ограничены 15с, паузы 1→2→4→8→15с; новая версия узла или локальные сокеты
обходят паузу. Прямой READY останавливает поиск и получает приоритет.
Исходящие request_id и входящие сроки независимы; устаревшие ответы игнорируются.
UP/DOWN отражают общую доступность, TIMEOUT приходит один раз до первого UP.
Последний close отменяет восстановление, включая закрытие из callback.
Проверены десять сетевых сценариев с реальными данными: поздний обрыв,
повторные циклы, смена адресов/сокетов, потеря маршрута посредника, старый ACK,
начальный TIMEOUT с поздним UP и close. ASAN без ошибок; собраны Qt и Android APK.
Полный check: 113 PASS, 0 FAIL, 1 SKIP; пропущенный auto_socket_dynamic отдельно
прошёл с root в network namespace. Интеграционные proxy, burst и load прошли.
Алгоритм: `src/routing_layer/conn_mgr_doc.md`.
[ ] **Общий лимит check.sh после clean** — 01.10.2026: расчёт по времени предыдущего
прогона дал 205с вместе с полной сборкой и оборвал проверку после успешных тестов
до `test_uasync_socket_race`. Повторный запуск на готовой сборке с большим лимитом
прошёл за 206с, включая proxy/burst/load. Разделить бюджет сборки и запуска тестов
либо учитывать clean при расчёте.
[ ] **Паузы аудио Qt при звонке с SM** — 01.10.2026, звонок
`939a3ea36f3eeac3`, прямой LAN TCP `192.168.40.250:44604 ↔ 192.168.40.247:44173`.
Tcpdump: общие паузы входящего трафика до 645мс; ping до роутера в среднем
100мс, до SM 128мс. ПК на 2.4ГГц, SM на 5ГГц. Одновременно `CallAudio: cb`
падает до 20–40/с вместо 100/с, буфер Qt растёт до 2с, на SM повторяется refill.
В 8с strace аудиопоток продолжает обслуживать PulseAudio без долгих syscall;
причина редких аудиоколлбэков пока не установлена. Добавить измерения интервалов
и длительности callback, кадров capture/playback и сводку CALL_STATS; сравнить
сетевые задержки после подключения ПК к 5ГГц или кабелю.
Повторный звонок `b2a423ed2d36ed83` (01:25:31–01:27:39): DIRECT сохраняется,
RTT 2–3мс; в начале на SM gap tone длится 1.8–2.0с примерно каждые 6с.
На Qt частота callback падает вплоть до 4/с, входящий PCM продолжает приходить:
буфер 65→465→1465→2079мс за 3с, затем растёт до нескольких секунд, dropped=0.
Локализовано прекращение обслуживания duplex-аудио Qt; ожидание backend и
задержка обработки PCM пока не разделены. Часы SM отстают от Qt примерно на 1.24с.
Добавлена диагностика backend/устройств, времени между callback и длительности
capture/playback, фактической частоты callback и PCM кадров за интервал.
Счётчики сбрасываются до старта устройства; нужна повторная запись с новой сборкой.
[+] **Ложный сигнал потери звука при запуске звонка** — 01.10.2026, звонок
`bc3efadcac629947`: Qt запускает аудиоколлбэки в 01:54:56.933, включает gap tone в
01:54:57.415 и выключает в 01:54:58.072 при первом воспроизведении PCM.
На SM между ACCEPT и первым захваченным кадром проходит около 1.15с;
часы SM отстают примерно на 1.23с. Qt стабильно обслуживает 100 callback/с,
RTT около 1мс. Исправлено: до первого PCM начальное ожидание проходит в тишине,
последующий перебой включает тон через 500мс. Стартовый сбой по-прежнему
ограничен существующим watchdog отсутствия медиа. Регрессия проверяет ожидание
1.5с, начальное накопление, настоящий перебой, неполный блок, новый звонок и завершение.
Ускорение запуска Android: router.start занимает около 775мс; система сообщает
длительное удержание AudioManager.updateAudioPortCache. Ещё около 370мс проходит
до запуска capture worker. Гудки теперь останавливаются до startAudio;
router использует снимок устройств с обновлением по событиям и явной переоценке,
capture запускается до подготовки/старта playback. Ошибка playback останавливает capture.
Добавлены длительности отдельных стадий, первого capture и PCM. Ядро, Qt и APK
собраны после clean; три аудиотеста прошли. Выигрыш на SM требует замера в новом звонке.
В звонке Qt `180f046ef22defc2` первый PCM пришёл через 1245мс без стартового gap tone.
Полный check вне песочницы: 113 PASS, 0 FAIL, 1 SKIP; auto_socket_dynamic отдельно
прошёл с root в private netns. Интеграционные proxy, burst и load прошли.
[ ] **Оставшаяся задержка запуска аудио SM** — после обновления 01.10.2026,
звонок `2b9ad4e21a54d8ba`: стартового gap tone нет на обеих сторонах; Qt
стабильно работает около 100 callback/с, max_idle=15.3мс, буфер 55мс, RTT=2мс.
На SM первый capture появляется через 1140мс от startAudio (1164мс от ACCEPT).
router.start=781мс, получение снимка устройств=0мс, AudioRecord create=24мс,
startRecording=7мс; от начала startRecording до первого кадра=328мс.
Capture worker стартует раньше playback, но общий выигрыш пока не виден.
AudioTrack create=21мс, play=327мс; система сообщает AudioPortCache contention=497мс
и восстановление AudioTrack при setPreferredDevice. Разделить замеры регистрации
callback, BT-профиля и применения маршрута; отдельно проверить первый AudioRecord.read.
На SM входящий буфер за время подготовки вырос до 1149мс и к концу звонка снизился
до 570мс; ускорение старта playback также сократит начальное накопление.
Реализован независимый запуск AudioTrack и AudioRecord в play/capture потоках;
частичный старт отменяется до освобождения устройств/native. AEC измеряется после готовности обоих.
Добавлены callbackMs/btProfileMs/applyMs/refreshMs, длительность первого read и время первой записи playback.
Резерв теперь зависит от изменения задержки доставки по timestamps CALL_MEDIA, отдельно от глубины очереди;
стартовое накопление и догон не повышают запас. Сохранность FIFO и uint32 wrap проверяются регрессиями.
Сборки ядра, Qt и Android после clean успешны; три аудиотеста прошли. Полный check:
113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load прошли.
Регрессия refill учитывает остаток SoundTouch: два новых кадра могут уже заполнить запас 60мс.
SM и Q8 обновлены 01.10.2026 в 03:03. Выигрыш по времени требует пробного звонка с новой версией.
[ ] **Гонка invite-файла в test_chat_join_e2e** — `wait_file()` проверяет только
существование файла, а `wf()` создаёт его до записи. Читатель иногда получает
пустую ссылку и ошибку префикса `utun://`. Публиковать готовый файл атомарно.
[ ] **Некорректное время в выводе check-proxy** — успешные `basic_1mb` и
`stress_sessions` иногда печатают огромные положительные/отрицательные миллисекунды,
хотя итоговая скорость и длительность передачи выглядят нормально. Проверить расчёт вывода.
[+] **SpeexDSP AEC (эхоподавление) в lib/** — акустическое эхоподавление звонка.
Вендорен SpeexDSP (mdf/fftwrap/kiss_fft, 3-clause BSD) в `lib/speexdsp/`,
обёртка `lib/speex_aec.c/h` (C-API, FLOATING_POINT + USE_KISS_FFT, без внешних
зависимостей). Собственная линия задержки рендера на `delay_frames` кадров +
синхронный `speex_echo_cancellation` (встроенный буфер SpeexDSP фиксирован в
2 кадра и для Android не годится); дрейф потоков — через переполнение/недозаполнение
линии (drop/dup + passthrough). Категория лога `aec`.
Ядро: `call_audio_set_aec_enabled/delay_frames`, AEC в feed_pcm (захват→encode)
и pull_pcm (рендер-референс), lifecycle в start/release. Desktop — настройка GUI
`aec_enabled` (`[chatserver]`, читается в `call_audio_start`). Android — включается
только на спикерфоне (`route==SPEAKER`, live через JNI `callAudioSetAec`),
задержка меряется `AudioTrack/AudioRecord.getTimestamp`; `CallAudioEngine.playLoop`
переведён на absolute-deadline (устранён систематический дрейф). Тест
`test_speex_aec` (подавление эха 25.5 дБ, сохранность речи 0.956, delay-line, NULL-гарды).
Сборки: autotools + chatgui CMake + Android CMake/APK.
[+] **Silero VAD в lib/** — добавлен детектор голосовой активности (Silero VAD v5,
стриминговая ONNX-модель + обёртка `lib/silero_vad.c/h` над ONNX Runtime C API).
API: `silero_vad_create/create_default/destroy/reset/process/process_pcm16`
(окно 512 сэмплов @16 кГц, вероятность речи [0..1], рекуррентное состояние).
Модель вшита в бинарник (`silero_vad_model.inc`, `create_default`), файл не нужен.
Категория лога `vad`. Модуль под `#ifdef HAVE_SILERO_VAD` — без onnxruntime
компилируется в стабы, поэтому GLOB-сборки (chatgui/Android) не ломаются.
Сборка: `--with-silero-vad` (autotools), `-DHAVE_SILERO_VAD` + libonnxruntime
(chatgui CMake, Android NDK). `make check`: 111/0/1.
[+] **Авто-PTT рации по VAD** — сделано. Отдельный режим `radio_vad_enabled` (ручной
PTT не меняется, при зажатой кнопке — override). Сервисный слой `radio_audio.c`:
ресемплер 48k→16k моно (3-tap box + 3:1, стерео микшируется), накопитель окон
512, фильтр-гистерезис + машина состояний (`src/radio/radio_vad.h`, чистый,
юнит-тестируемый): старт при подтверждении 2 окон (~64мс) выше порога
`radio_vad_threshold`, стоп при тишине `radio_vad_hangover_ms` (200мс) или чужом
разговоре (busy = RX-кадр <200мс). История захвата (pre-roll) удалена; передаётся текущий PCM.
Компрессор в VAD-режиме байпасится. Desktop `radio_audio_engine.cpp` и Android
`RadioAudioEngine.kt` фидят PCM постоянно в VAD-режиме; JNI `radioVadMode()`.
Тест `test_radio_vad` (ресемплер + FSM). Android: onnxruntime AAR 1.23.2 в
`jniLibs/` + `cpp/onnxruntime/include` + `-DHAVE_SILERO_VAD` в CMake.
Android: настройки VAD (включение, порог, задержка после тишины); микрофон в PTT
создаётся только для передачи. PTT запускается при касании, после отпускания
передаёт ещё 250мс; повторное касание отменяет завершение (двойной тап без FIN).
VAD работает 15 минут при горящем экране;
SCREEN_OFF освобождает захват, SCREEN_ON автоматически открывает новое окно.
RX остаётся активен; TX освобождается отдельно через `radio_audio_capture_stop()`.
[+] **chatgui (Qt) не собирался: C++ void* в utun_node.cpp** — сделано. Причина:
`proxy_protocol.h` (C-заголовок, включается в C++ `utun_node.cpp`) делал
`e->dgram = u_malloc(...)` и `struct proxy_flow* f = arg;` без явных приведений —
в C++ нет неявного `void*`→T*. Добавлены `(uint8_t*)`/`(struct proxy_flow*)`
(валидны и в C). `vibechat` собирается (47МБ, с вшитой моделью + onnxruntime).
[+] **test_auto_socket_dynamic: root-регрессия и нагрузка** — изолированный namespace,
корректные маршруты, двусторонний поток через backpressure, строгие seq/payload,
пределы TX/RX, остановка при обрыве и drain после восстановления, баланс ресурсов.
@ -220,6 +419,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:

421
lib/audio_compressor.c

@ -3,229 +3,144 @@
#include "mem.h"
#include <math.h>
#include <string.h>
#include <stdlib.h>
#define BLOCK_LEVELS_CHUNK 64
#define PENDING_CHUNK 32
#include <stdint.h>
/* История и задержанные блоки ограничены окном настройки. Только запись накапливает output. */
struct audio_compressor {
audio_compressor_config_t cfg;
int enabled;
int block_samples;
int lookback_blocks;
int lookahead_blocks;
float rise_factor_per_block;
float release_factor_per_block;
float max_gain;
float gain_smoothed;
float* block_levels;
size_t block_levels_count;
size_t block_levels_cap;
struct pending_block {
int16_t* samples;
size_t count;
size_t level_index;
}* pending;
size_t pending_count;
size_t pending_cap;
int16_t* accumulator;
size_t accum_count;
int enabled, configured, failed, block_samples, lookback_blocks, lookahead_blocks;
float rise_factor_per_block, release_factor_per_block, max_gain, gain_smoothed;
float* block_levels;
size_t levels_cap, blocks_seen;
int16_t* pending;
size_t pending_cap, pending_head, pending_count, accum_count;
int16_t* output;
size_t output_size;
size_t output_cap;
size_t output_size, output_cap;
uint32_t clip_count;
int dbg_counter;
unsigned dbg_counter;
};
/* Начальное усиление в середине диапазона в дБ. */
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) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: OOM");
return NULL;
}
if (!ac) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: state allocation failed"); return NULL; }
ac->enabled = 1;
ac->gain_smoothed = 1.0f;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: ok");
return ac;
}
void audio_compressor_destroy(struct audio_compressor* ac) {
if (!ac) return;
for (size_t i = 0; i < ac->pending_count; i++)
u_free(ac->pending[i].samples);
u_free(ac->pending);
u_free(ac->block_levels);
u_free(ac->accumulator);
u_free(ac->output);
u_free(ac);
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor_destroy");
}
void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg) {
if (!ac || !cfg) return;
ac->cfg = *cfg;
if (ac->cfg.sample_rate <= 0) ac->cfg.sample_rate = 48000;
if (ac->cfg.channels <= 0) ac->cfg.channels = 1;
if (ac->cfg.block_duration_ms <= 0) ac->cfg.block_duration_ms = 20;
if (ac->cfg.lookback_ms <= 0) ac->cfg.lookback_ms = 200;
if (ac->cfg.lookahead_ms <= 0) ac->cfg.lookahead_ms = 100;
if (ac->cfg.target_level <= 0.0f) ac->cfg.target_level = 0.25f;
ac->block_samples = ac->cfg.sample_rate * ac->cfg.block_duration_ms / 1000 * ac->cfg.channels;
ac->lookback_blocks = ac->cfg.lookback_ms / ac->cfg.block_duration_ms;
ac->lookahead_blocks = ac->cfg.lookahead_ms / ac->cfg.block_duration_ms;
ac->rise_factor_per_block = powf(ac->cfg.rise_rate_per_sec, (float)ac->cfg.block_duration_ms / 1000.0f);
if (ac->cfg.release_rate_per_sec <= 0.0f)
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);
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",
ac->cfg.sample_rate, ac->cfg.channels, ac->cfg.block_duration_ms,
ac->cfg.lookback_ms, ac->cfg.lookahead_ms, ac->cfg.max_gain_db,
ac->cfg.rise_rate_per_sec, ac->cfg.release_rate_per_sec, 20.0f * log10f(ac->cfg.target_level));
u_free(ac->pending); u_free(ac->block_levels); u_free(ac->output); u_free(ac);
}
void audio_compressor_reset(struct audio_compressor* ac) {
if (!ac) return;
for (size_t i = 0; i < ac->pending_count; i++)
u_free(ac->pending[i].samples);
ac->pending_count = 0;
ac->block_levels_count = 0;
ac->accum_count = 0;
ac->output_size = 0;
ac->gain_smoothed = 1.0f;
ac->clip_count = 0;
ac->dbg_counter = 0;
}
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) {
if (!ac) return;
ac->enabled = enabled ? 1 : 0;
}
int audio_compressor_is_enabled(const struct audio_compressor* ac) {
return ac ? ac->enabled : 0;
}
ac->pending_head = ac->pending_count = ac->accum_count = ac->blocks_seen = ac->output_size = 0;
ac->gain_smoothed = initial_gain(ac->max_gain);
ac->clip_count = ac->dbg_counter = 0;
ac->failed = 0;
}
/* Ноль lookahead означает отсутствие задержки; defaults остальных полей задаются явно здесь. */
int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* config) {
if (!ac || !config) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: missing configuration"); return -1; }
audio_compressor_config_t cfg = *config;
if (!cfg.sample_rate) cfg.sample_rate = 48000;
if (!cfg.channels) cfg.channels = 1;
if (!cfg.block_duration_ms) cfg.block_duration_ms = 20;
if (!cfg.lookback_ms) cfg.lookback_ms = 200;
if (!cfg.target_level) cfg.target_level = 0.25f;
if (!cfg.rise_rate_per_sec) cfg.rise_rate_per_sec = 4.0f;
if (!cfg.release_rate_per_sec) cfg.release_rate_per_sec = cfg.rise_rate_per_sec * 5.0f;
if (cfg.sample_rate < 8000 || cfg.sample_rate > 192000 || cfg.channels < 1 || cfg.channels > 2 ||
cfg.block_duration_ms < 1 || cfg.block_duration_ms > 1000 || cfg.lookback_ms < 0 || cfg.lookback_ms > 5000 ||
cfg.lookahead_ms < 0 || cfg.lookahead_ms > 5000 || !isfinite(cfg.target_level) || cfg.target_level <= 0 ||
!isfinite(cfg.max_gain_db) || cfg.max_gain_db < 0 || cfg.max_gain_db > 60 ||
!isfinite(cfg.rise_rate_per_sec) || cfg.rise_rate_per_sec <= 0 ||
!isfinite(cfg.release_rate_per_sec) || cfg.release_rate_per_sec <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid rate=%d ch=%d block=%d back=%d ahead=%d",
cfg.sample_rate, cfg.channels, cfg.block_duration_ms, cfg.lookback_ms, cfg.lookahead_ms);
return -1;
}
int block = cfg.sample_rate * cfg.block_duration_ms / 1000 * cfg.channels;
int back = cfg.lookback_ms / cfg.block_duration_ms, ahead = cfg.lookahead_ms / cfg.block_duration_ms;
size_t levels_cap = (size_t)back + ahead + 2, pending_cap = (size_t)ahead + 2;
float* levels = u_calloc(levels_cap, sizeof(float));
int16_t* pending = u_calloc(pending_cap * (size_t)block, sizeof(int16_t));
if (!levels || !pending) {
u_free(levels); u_free(pending);
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: buffers allocation failed");
return -1;
}
u_free(ac->block_levels); u_free(ac->pending);
ac->cfg = cfg; ac->block_samples = block;
ac->lookback_blocks = back; ac->lookahead_blocks = ahead;
ac->block_levels = levels; ac->levels_cap = levels_cap;
ac->pending = pending; ac->pending_cap = pending_cap;
ac->rise_factor_per_block = powf(cfg.rise_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f);
ac->release_factor_per_block = powf(cfg.release_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f);
ac->max_gain = powf(10.0f, cfg.max_gain_db / 20.0f);
ac->configured = 1;
audio_compressor_reset(ac);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "compressor: configured rate=%d ch=%d ahead=%dms history=%zu pending=%zu",
cfg.sample_rate, cfg.channels, cfg.lookahead_ms, levels_cap, pending_cap);
return 0;
}
struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source) {
if (!source || !source->configured) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: cannot clone configuration"); return NULL; }
struct audio_compressor* copy = audio_compressor_create();
if (copy && audio_compressor_configure(copy, &source->cfg) != 0) { audio_compressor_destroy(copy); return NULL; }
if (copy) copy->enabled = source->enabled;
return copy;
}
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) { if (ac) ac->enabled = !!enabled; }
int audio_compressor_is_enabled(const struct audio_compressor* ac) { return ac && ac->enabled; }
/* Одна оценка для всех каналов сохраняет баланс стерео. */
static float compute_block_level(const int16_t* samples, size_t count) {
double sum_sq = 0.0;
float peak = 0.0f;
for (size_t i = 0; i < count; i++) {
float v = (float)samples[i] / 32768.0f;
sum_sq += (double)v * v;
float av = fabsf(v);
if (av > peak) peak = av;
}
float rms = (float)sqrt(sum_sq / (double)count);
return (rms * 2.0f + peak) / 2.0f;
}
static void ensure_output_cap(struct audio_compressor* ac, size_t needed) {
size_t want = ac->output_size + needed;
if (want <= ac->output_cap) return;
size_t new_cap = ac->output_cap ? ac->output_cap * 2 : 4096;
while (new_cap < want) new_cap *= 2;
int16_t* tmp = u_realloc(ac->output, new_cap * sizeof(int16_t));
if (!tmp) return;
ac->output = tmp;
ac->output_cap = new_cap;
}
static void append_output(struct audio_compressor* ac, const int16_t* samples, size_t count) {
ensure_output_cap(ac, count);
memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
ac->output_size += count;
}
static void ensure_block_levels_cap(struct audio_compressor* ac) {
if (ac->block_levels_count < ac->block_levels_cap) return;
size_t new_cap = ac->block_levels_cap ? ac->block_levels_cap * 2 : BLOCK_LEVELS_CHUNK;
float* tmp = u_realloc(ac->block_levels, new_cap * sizeof(float));
if (!tmp) return;
ac->block_levels = tmp;
ac->block_levels_cap = new_cap;
}
static void add_block_level(struct audio_compressor* ac, float level) {
ensure_block_levels_cap(ac);
ac->block_levels[ac->block_levels_count++] = level;
}
static void ensure_pending_cap(struct audio_compressor* ac) {
if (ac->pending_count < ac->pending_cap) return;
size_t new_cap = ac->pending_cap ? ac->pending_cap * 2 : PENDING_CHUNK;
struct pending_block* tmp = u_realloc(ac->pending, new_cap * sizeof(*tmp));
if (!tmp) return;
ac->pending = tmp;
ac->pending_cap = new_cap;
}
static void process_pending_block(struct audio_compressor* ac);
void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) {
if (!ac || !samples || count == 0) return;
if (!ac->enabled) {
append_output(ac, samples, count);
return;
if (fabsf(v) > peak) peak = fabsf(v);
}
size_t remaining = count;
const int16_t* src = samples;
while (remaining > 0) {
size_t need = (size_t)ac->block_samples - ac->accum_count;
size_t take = remaining < need ? remaining : need;
size_t new_cnt = ac->accum_count + take;
int16_t* tmp = u_realloc(ac->accumulator, new_cnt * sizeof(int16_t));
if (!tmp) return;
ac->accumulator = tmp;
memcpy(ac->accumulator + ac->accum_count, src, take * sizeof(int16_t));
ac->accum_count = new_cnt;
src += take;
remaining -= take;
if (ac->accum_count == (size_t)ac->block_samples) {
float level = compute_block_level(ac->accumulator, ac->accum_count);
add_block_level(ac, level);
ensure_pending_cap(ac);
ac->pending[ac->pending_count].samples = ac->accumulator;
ac->pending[ac->pending_count].count = ac->accum_count;
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
ac->pending_count++;
ac->accumulator = NULL;
ac->accum_count = 0;
}
return (2.0f * sqrtf((float)(sum_sq / count)) + peak) / 2.0f;
}
/* Выход записи растёт только вне аппаратного callback; OOM переводит запись в failed. */
static int reserve_output(struct audio_compressor* ac, size_t count) {
if (count > SIZE_MAX / sizeof(int16_t) - ac->output_size) goto failed;
size_t want = ac->output_size + count;
if (want <= ac->output_cap) return 0;
size_t capacity = ac->output_cap ? ac->output_cap : 4096;
while (capacity < want) {
if (capacity > SIZE_MAX / sizeof(int16_t) / 2) { capacity = want; break; }
capacity *= 2;
}
while (ac->pending_count > (size_t)ac->lookahead_blocks)
process_pending_block(ac);
}
static void process_pending_block(struct audio_compressor* ac) {
if (ac->pending_count == 0) return;
size_t idx = ac->pending[0].level_index;
size_t win_start = idx >= (size_t)ac->lookback_blocks ? idx - (size_t)ac->lookback_blocks : 0;
size_t win_end = idx + (size_t)ac->lookahead_blocks;
if (win_end >= ac->block_levels_count) win_end = ac->block_levels_count - 1;
float envelope = 0.0f;
for (size_t i = win_start; i <= win_end; i++) {
if (ac->block_levels[i] > envelope) envelope = ac->block_levels[i];
int16_t* samples = u_realloc(ac->output, capacity * sizeof(int16_t));
if (!samples) goto failed;
ac->output = samples; ac->output_cap = capacity;
return 0;
failed:
ac->failed = 1;
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: recording output allocation failed samples=%zu", count);
return -1;
}
/* Общий алгоритм усиления для записи с lookahead и потокового кадра без lookahead. */
static void apply_gain(struct audio_compressor* ac, int16_t* samples, size_t count, size_t idx) {
size_t begin = idx >= (size_t)ac->lookback_blocks ? idx - ac->lookback_blocks : 0;
size_t end = idx + ac->lookahead_blocks;
if (end >= ac->blocks_seen) end = ac->blocks_seen - 1;
float envelope = 0;
for (size_t i = begin; i <= end; i++) {
float level = ac->block_levels[i % ac->levels_cap];
if (level > envelope) envelope = level;
}
if (envelope < 0.0001f) envelope = 0.0001f;
@ -248,64 +163,88 @@ static void process_pending_block(struct audio_compressor* ac) {
float prev_db = 20.0f * log10f(G_prev > 0.0001f ? G_prev : 0.0001f);
if (ac->dbg_counter % 25 == 0 || fabsf(gain_db - prev_db) > 3.0f) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB clip=%u",
idx, ac->block_levels[idx], envelope, 20.0f * log10f(G_raw), gain_db, (unsigned)ac->clip_count);
idx, ac->block_levels[idx % ac->levels_cap], envelope, 20.0f * log10f(G_raw), gain_db, (unsigned)ac->clip_count);
}
ac->dbg_counter++;
for (size_t i = 0; i < ac->pending[0].count; i++) {
float v = (float)ac->pending[0].samples[i] * ac->gain_smoothed;
for (size_t i = 0; i < count; i++) {
float v = (float)samples[i] * ac->gain_smoothed;
if (v > 32767.0f) { v = 32767.0f; ac->clip_count++; }
if (v < -32768.0f) { v = -32768.0f; ac->clip_count++; }
ac->pending[0].samples[i] = (int16_t)(int)v;
samples[i] = (int16_t)(int)v;
}
append_output(ac, ac->pending[0].samples, ac->pending[0].count);
u_free(ac->pending[0].samples);
/* shift remaining pending blocks down */
if (ac->pending_count > 1)
memmove(ac->pending, ac->pending + 1, (ac->pending_count - 1) * sizeof(*ac->pending));
ac->pending_count--;
}
void audio_compressor_flush(struct audio_compressor* ac) {
if (!ac) return;
if (!ac->enabled) return;
if (ac->accum_count > 0) {
float level = compute_block_level(ac->accumulator, ac->accum_count);
add_block_level(ac, level);
ensure_pending_cap(ac);
ac->pending[ac->pending_count].samples = ac->accumulator;
ac->pending[ac->pending_count].count = ac->accum_count;
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
ac->pending_count++;
ac->accumulator = NULL;
ac->accum_count = 0;
}
if (ac->pending_count > 0)
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_flush: %zu pending blocks (gainSmoothed=%.1fdB)",
ac->pending_count, 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f));
while (ac->pending_count > 0)
process_pending_block(ac);
/* Слить один готовый блок записи. История хранится по абсолютному номеру в кольце. */
static int drain_block(struct audio_compressor* ac, size_t count) {
if (reserve_output(ac, count) != 0) return -1;
int16_t* samples = ac->pending + ac->pending_head * ac->block_samples;
apply_gain(ac, samples, count, ac->blocks_seen - ac->pending_count);
memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
ac->output_size += count;
ac->pending_head = (ac->pending_head + 1) % ac->pending_cap;
--ac->pending_count;
return 0;
}
const int16_t* audio_compressor_output(const struct audio_compressor* ac) {
return ac ? ac->output : NULL;
/* Ровно один кадр, без output-накопителя и выделений памяти. in/out могут совпадать. */
int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count) {
if (!ac || !ac->configured || ac->failed || !in || !out || count != (size_t)ac->block_samples ||
ac->lookahead_blocks || ac->pending_count || ac->accum_count) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid streaming frame count=%zu", count);
return -1;
}
memmove(out, in, count * sizeof(int16_t));
if (ac->enabled) {
size_t idx = ac->blocks_seen++;
ac->block_levels[idx % ac->levels_cap] = compute_block_level(in, count);
apply_gain(ac, out, count, idx);
}
return 0;
}
size_t audio_compressor_output_size(const struct audio_compressor* ac) {
return ac ? ac->output_size : 0;
int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) {
if (!ac || !ac->configured || ac->failed || (!samples && count)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid recording input"); return -1;
}
if (!ac->enabled) {
if (reserve_output(ac, count) != 0) return -1;
if (count) memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
ac->output_size += count;
return 0;
}
while (count) {
size_t take = (size_t)ac->block_samples - ac->accum_count;
if (take > count) take = count;
int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples;
memcpy(block + ac->accum_count, samples, take * sizeof(int16_t));
samples += take; count -= take; ac->accum_count += take;
if (ac->accum_count == (size_t)ac->block_samples) {
ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, ac->accum_count);
ac->accum_count = 0; ++ac->pending_count;
if (ac->pending_count > (size_t)ac->lookahead_blocks && drain_block(ac, ac->block_samples) != 0) return -1;
}
}
return 0;
}
void audio_compressor_clear_output(struct audio_compressor* ac) {
if (!ac) return;
ac->output_size = 0;
int audio_compressor_flush(struct audio_compressor* ac) {
if (!ac || !ac->configured || ac->failed) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid flush"); return -1; }
if (!ac->enabled) return 0;
size_t tail = ac->accum_count;
if (tail) {
int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples;
ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, tail);
ac->accum_count = 0; ++ac->pending_count;
}
while (ac->pending_count) {
size_t count = tail && ac->pending_count == 1 ? tail : (size_t)ac->block_samples;
if (drain_block(ac, count) != 0) return -1;
}
return 0;
}
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac) {
return ac ? ac->clip_count : 0;
}
const int16_t* audio_compressor_output(const struct audio_compressor* ac) { return ac ? ac->output : NULL; }
size_t audio_compressor_output_size(const struct audio_compressor* ac) { return ac ? ac->output_size : 0; }
void audio_compressor_clear_output(struct audio_compressor* ac) { if (ac) ac->output_size = 0; }
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac) { return ac ? ac->clip_count : 0; }

10
lib/audio_compressor.h

@ -23,16 +23,20 @@ typedef struct {
} audio_compressor_config_t;
struct audio_compressor* audio_compressor_create(void);
/* Новый пустой compressor с теми же настройками; позволяет передать запись worker без копии PCM. */
struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source);
void audio_compressor_destroy(struct audio_compressor* ac);
void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg);
int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg);
/* Потоковый кадр без lookahead: результат немедленный, память не выделяется; 0 / -1. */
int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count);
void audio_compressor_reset(struct audio_compressor* ac);
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled);
int audio_compressor_is_enabled(const struct audio_compressor* ac);
void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count);
void audio_compressor_flush(struct audio_compressor* ac);
int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count);
int audio_compressor_flush(struct audio_compressor* ac);
const int16_t* audio_compressor_output(const struct audio_compressor* ac);
size_t audio_compressor_output_size(const struct audio_compressor* ac);

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 */

130
lib/ll_queue.c

@ -52,15 +52,23 @@ static void on_get_trampoline(void* arg);
static void empty_trampoline(void* arg) {
struct ll_queue* q = (struct ll_queue*)arg;
q->empty_call_soon_id = NULL;
if (q->count != 0) return;
queue_callback_fn cb = q->empty_callback;
void* cb_arg = q->empty_callback_arg;
q->empty_callback = NULL;
q->empty_callback_arg = NULL;
if (q->count == 0 && cb) cb(q, cb_arg);
if (cb) cb(q, cb_arg);
}
static void on_get_trampoline(void* arg) {
struct ll_queue* q = (struct ll_queue*)arg;
q->on_get_call_soon_id = NULL;
if (!q->on_get_pending || !q->on_get) return;
q->on_get_pending--;
if (q->on_get_pending) {
q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline);
if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending);
}
if (q->on_get) q->on_get(q, q->on_get_arg);
}
@ -130,6 +138,7 @@ void queue_free(struct ll_queue* q) {
// ВАЖНО: Не освобождаем элементы в очереди - они должны быть извлечены отдельно
// Это упрощает архитектуру и предотвращает double-u_free
while (q->deferred_waiters) queue_waiter_cancel(q, q->deferred_waiters);
// Освободить список waiters (сбросить handle->internal, отменить отложенные вызовы)
struct queue_waiter* w = q->waiter_head;
while (w) {
@ -159,6 +168,10 @@ void queue_free(struct ll_queue* q) {
q->empty_callback_arg = NULL;
q->on_get = NULL;
q->on_get_arg = NULL;
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
if (q->head) DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] freeing nonempty queue: count=%d; entries remain caller-owned", q->name, q->count);
for (struct ll_entry* e = q->head; e; e = e->next) e->owner = NULL;
u_free(q);
}
@ -316,22 +329,42 @@ static uint32_t make_hash(const void* data, uint16_t len) {// алгоритм F
return hash;
}
static void unlink_deferred_waiter(struct ll_queue* q, struct queue_waiter_handle* h) {
struct queue_waiter_handle** p = &q->deferred_waiters;
while (*p && *p != h) p = &(*p)->defer_next;
if (*p) *p = h->defer_next;
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter not found: handle=%p", q->name, h);
h->defer_next = NULL; h->defer_q = NULL;
}
static void waiter_defer_cb(void* arg) {
struct queue_waiter_handle* h = (struct queue_waiter_handle*)arg;
struct ll_queue* q = h->defer_q;
queue_threshold_callback_fn cb = h->defer_cb;
void* cb_arg = h->defer_arg;
h->call_soon_id = NULL;
if (h->defer_cb) h->defer_cb(h->defer_q, h->defer_arg);
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "waiter_defer_cb: NULL callback (stale?) q=%p", (void*)h->defer_q);
unlink_deferred_waiter(q, h);
h->defer_cb = NULL; h->defer_arg = NULL;
if (cb) cb(q, cb_arg);
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter has no callback", q->name);
}
static inline void queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg,
static inline int queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg,
struct queue_waiter_handle* h) {
if (q->waiter_defer) {
if (h->call_soon_id) { DEBUG_WARN(DEBUG_CATEGORY_SYS, "queue_waiter_call: overwriting existing call_soon_id=%p", h->call_soon_id); uasync_call_soon_cancel(q->ua, h->call_soon_id); }
if (h->call_soon_id) queue_waiter_cancel(h->defer_q, h);
h->defer_q = q; h->defer_cb = cb; h->defer_arg = arg;
h->call_soon_id = uasync_call_soon(q->ua, h, waiter_defer_cb);
if (h->call_soon_id) { h->defer_next = q->deferred_waiters; q->deferred_waiters = h; }
else {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter scheduling failed: handle=%p", q->name, h);
h->defer_q = NULL; h->defer_cb = NULL; h->defer_arg = NULL;
return -1;
}
} else {
cb(q, arg);
}
return 0;
}
// Проверить и запустить первый ожидающий коллбэк (FIFO / round-robin)
@ -351,13 +384,18 @@ static void check_waiters(struct ll_queue* q) {
queue_threshold_callback_fn cb = waiter->callback;
void* arg = waiter->callback_arg;
u_free(waiter);
if (cb) {
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "check_waiters: waking head waiter, count=%d<=%d, bytes=%zu<=%zu",
q->count, q->threshold_max_packets, q->total_bytes, q->threshold_max_bytes);
queue_waiter_call(q, cb, arg, h);
if (queue_waiter_call(q, cb, arg, h) < 0) {
// Отказ планирования не теряет ожидание: следующий get/remove повторит попытку.
waiter->next = q->waiter_head; q->waiter_head = waiter;
if (!q->waiter_tail) q->waiter_tail = waiter;
h->internal = waiter;
return;
}
}
u_free(waiter);
}
int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
@ -366,7 +404,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
queue_check_thread(q);
#endif
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put detected! entry=%p prev=%p next=%p len=%u",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next, entry->len);
abort();
@ -380,6 +418,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
}
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry);
queue_entry_free(entry);
return -1;
@ -391,17 +430,18 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
if (q->tail) q->tail->next = entry; else q->head = entry;
q->tail = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
// НЕ вызываем add_to_hash
if (q->count == 1) queue_trigger_callback(q);
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@ -411,7 +451,7 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
queue_check_thread(q);
#endif
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_with_index detected! entry=%p prev=%p next=%p",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
abort();
@ -427,6 +467,7 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size);
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry);
queue_entry_free(entry);
return -1;
@ -438,17 +479,18 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
if (q->tail) q->tail->next = entry; else q->head = entry;
q->tail = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
add_to_hash(q, entry);
if (q->count == 1) queue_trigger_callback(q);
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@ -458,7 +500,7 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) {
queue_check_thread(q);
#endif
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first detected! entry=%p prev=%p next=%p",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
abort();
@ -471,20 +513,25 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) {
return -1;
}
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry); queue_entry_free(entry); return -1;
}
entry->next = q->head;
entry->prev = NULL;
if (q->head) q->head->prev = entry; else q->tail = entry;
q->head = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
if (q->count == 1) queue_trigger_callback(q);
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@ -495,7 +542,7 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
queue_check_thread(q);
#endif
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first_with_index detected! entry=%p prev=%p next=%p",
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
abort();
@ -511,6 +558,7 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size);
if (q->size_limit >= 0 && q->count >= q->size_limit) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
queue_dgram_free(entry);
queue_entry_free(entry);
return -1;
@ -521,17 +569,18 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
if (q->head) q->head->prev = entry; else q->tail = entry;
q->head = entry;
entry->owner = q;
q->count++;
entry->int_len = entry->len;
q->total_bytes += entry->int_len;
add_to_hash(q, entry);
if (q->count == 1) queue_trigger_callback(q);
#ifdef QUEUE_DEBUG
queue_check_consistency(q);
#endif
// Пользовательский callback может освободить очередь.
if (q->count == 1) queue_trigger_callback(q);
return 0;
}
@ -554,6 +603,7 @@ struct ll_entry* queue_data_get(struct ll_queue* q) {
entry->next = NULL;
entry->prev = NULL;
entry->owner = NULL;
remove_from_hash(q, entry);
@ -562,18 +612,21 @@ struct ll_entry* queue_data_get(struct ll_queue* q) {
// Приостановить коллбэки для предотвращения рекурсии
q->callback_suspended = 1;
// Проверить ожидающие коллбэки
check_waiters(q);
if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id)
q->empty_call_soon_id = uasync_call_soon(q->ua, q, (timeout_callback_t)empty_trampoline);
if (q->on_get)
uasync_call_soon(q->ua, q, (timeout_callback_t)on_get_trampoline);
if (q->on_get) {
q->on_get_pending++;
if (!q->on_get_call_soon_id) {
q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline);
if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending);
}
}
#ifdef QUEUE_DEBUG
queue_check_consistency(q);// !!!! for debug
#endif
check_waiters(q);
return entry;
}
@ -672,18 +725,24 @@ void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) {
if (!q) return;
q->on_get = cbk_fn;
q->on_get_arg = arg;
if (!cbk_fn) {
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
}
}
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
queue_threshold_callback_fn callback, void* arg) {
if (!q || !h) return -1;
if (!q || !h || !callback) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "queue_waiter_wait: invalid arguments q=%p handle=%p callback=%p", q, h, (void*)callback);
return -1;
}
if (h->internal) queue_waiter_cancel(q, h);
if (h->internal || h->call_soon_id) queue_waiter_cancel(h->call_soon_id ? h->defer_q : q, h);
if (q->count <= q->threshold_max_packets
&& (q->threshold_max_bytes == 0 || q->total_bytes <= q->threshold_max_bytes)) {
queue_waiter_call(q, callback, arg, h);
return 1;
return queue_waiter_call(q, callback, arg, h) < 0 ? -1 : 1;
}
struct queue_waiter* waiter = u_malloc(sizeof(struct queue_waiter));
@ -709,7 +768,11 @@ void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h) {
* queue_waiter_wait по ветке «очередь свободна» планирует call_soon_id,
* не регистрируя waiter. Ранний выход по !internal оставлял бы висячий
* callback с arg=handle, который после teardown разыменовывал бы freed-память. */
if (h->call_soon_id) { uasync_call_soon_cancel(q->ua, h->call_soon_id); h->call_soon_id = NULL; }
if (h->call_soon_id) {
struct ll_queue* deferred_q = h->defer_q;
uasync_call_soon_cancel(deferred_q->ua, h->call_soon_id); h->call_soon_id = NULL;
unlink_deferred_waiter(deferred_q, h); h->defer_cb = NULL; h->defer_arg = NULL;
}
if (!h->internal) return;
struct queue_waiter* waiter = h->internal;
@ -769,6 +832,10 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q, const void* index_
int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
if (!q || !entry) return -1;
if (entry->owner != q) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] cannot remove foreign entry=%p owner=%p", q->name, entry, entry->owner);
return -1;
}
#ifdef QUEUE_THREAD_CHECK
queue_check_thread(q);
@ -792,6 +859,7 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
entry->next = NULL;
entry->prev = NULL;
entry->owner = NULL;
remove_from_hash(q, entry);
@ -799,6 +867,8 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
#ifdef QUEUE_DEBUG
queue_check_consistency(q);// !!!! for debug
#endif
if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id)
q->empty_call_soon_id = uasync_call_soon(q->ua, q, empty_trampoline);
check_waiters(q);
return 0;
}

12
lib/ll_queue.h

@ -61,6 +61,7 @@ struct queue_waiter_handle;
*/
struct ll_entry {
char* name;
struct ll_queue* owner; // Владелец элемента; NULL после извлечения
struct ll_entry* next; // Следующий элемент в очереди
struct ll_entry* prev; // Предыдущий элемент в очереди
uint16_t size; // Размер пользовательского буфера data[] (байт)
@ -123,6 +124,7 @@ struct queue_waiter_handle {
queue_threshold_callback_fn defer_cb;
void* defer_arg;
void* call_soon_id; ///< хендл отложенного вызова (для отмены)
struct queue_waiter_handle* defer_next; ///< список отложенных уведомлений очереди
};
/**
@ -146,6 +148,7 @@ struct ll_queue {
struct queue_waiter* waiter_head; // Голова списка ожидающих
struct queue_waiter* waiter_tail; // Хвост списка ожидающих
struct queue_waiter_handle* deferred_waiters; // Уже запланированные уведомления
int threshold_max_packets; // Общий порог: макс. кол-во элементов (по умолчанию 0)
size_t threshold_max_bytes; // Общий порог: макс. объём данных (0 = не проверять)
int waiter_defer; // 0=immediate callback, 1=defer via uasync_call_soon
@ -157,6 +160,8 @@ struct ll_queue {
queue_callback_fn on_get; // коллбэк после каждого queue_data_get (deferred)
void* on_get_arg;
void* on_get_call_soon_id;
size_t on_get_pending;
struct ll_entry** hash_table; // Хеш-таблица для поиска по id (если hash_size > 0)
size_t hash_size; // Размер хеш-таблицы
@ -275,12 +280,13 @@ void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
/**
* @brief Регистрирует ожидание освобождения очереди до общего порога.
* @param q очередь
* @param h указатель на handle (создаётся как `struct queue_waiter_handle h = {0}` или `h.internal = NULL`)
* @param h полностью обнулённый handle: `struct queue_waiter_handle h = {0}`
* @param callback функция, вызываемая при достижении порога
* @param arg аргумент коллбэка
* @return 1 — условие уже выполнено (callback вызван немедленно), 0 — зарегистрирован в списке, -1 — ошибка
* @return 1 — условие выполнено (callback вызван или отложен), 0 — ожидает порог, -1 — ошибка
*
* Если handle уже ожидает (h->internal != NULL) — старый waiter отменяется и создаётся новый.
* Если handle уже ожидает или имеет отложенный callback — старое ожидание отменяется.
* При queue_free отменяются также уже запланированные уведомления.
*/
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
queue_threshold_callback_fn callback, void* arg);

26
lib/memory_pool.c

@ -5,11 +5,12 @@
#include "memory_pool.h"
#include "mem.h"
// Layout: [user_data: object_size] [canary: 4 bytes] [alloc_loc: pointer] [counter: 1 byte]
// Layout: [user_data] [canary: 4] [alloc_loc: pointer] [free_loc: pointer] [counter: 1]
#define POOL_CANARY_OFF (pool->object_size)
#define POOL_LOC_OFF (pool->object_size + 4)
#define POOL_COUNTER_OFF (pool->object_size + 4 + sizeof(const char*))
#define POOL_ALLOC_SIZE (pool->object_size + 4 + sizeof(const char*) + 1)
#define POOL_FREE_LOC_OFF (pool->object_size + 4 + sizeof(const char*))
#define POOL_COUNTER_OFF (pool->object_size + 4 + 2 * sizeof(const char*))
#define POOL_ALLOC_SIZE (pool->object_size + 4 + 2 * sizeof(const char*) + 1)
#define POOL_CANARY_VAL 0xDEADBEAF
static void pool_init_tags(struct memory_pool* pool, void* obj, const char* location)
@ -19,35 +20,44 @@ static void pool_init_tags(struct memory_pool* pool, void* obj, const char* loca
/* Метаданные следуют за payload произвольного размера и могут быть невыровненными. */
memcpy((uint8_t*)obj + POOL_CANARY_OFF, &canary, sizeof(canary));
memcpy((uint8_t*)obj + POOL_LOC_OFF, &location, sizeof(location));
const char* free_loc = NULL;
memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &free_loc, sizeof(free_loc));
*counter = 1;
}
static void pool_check_and_clear_tags(struct memory_pool* pool, void* obj, const char* location)
{
uint32_t canary;
const char* loc;
const char *loc, *free_loc;
memcpy(&canary, (uint8_t*)obj + POOL_CANARY_OFF, sizeof(canary));
memcpy(&loc, (uint8_t*)obj + POOL_LOC_OFF, sizeof(loc));
memcpy(&free_loc, (uint8_t*)obj + POOL_FREE_LOC_OFF, sizeof(free_loc));
uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF;
if (canary != POOL_CANARY_VAL) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free BUFFER OVERFLOW pool=%p name=%s sz=%zu allocs=%zu reuse=%zu free=%d",
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count, pool->free_count);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc=%s free=%s canary=0x%08x expected=0x%08x",
obj, loc ? loc : "(null)", location, canary, POOL_CANARY_VAL);
/* Повреждённый хвост может содержать битые указатели; их нельзя читать как строки. */
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc_loc_ptr=%p free_loc_ptr=%p free_attempt=%s canary=0x%08x expected=0x%08x",
obj, (void*)loc, (void*)free_loc, location, canary, POOL_CANARY_VAL);
if (pool->object_size)
log_dump(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_SYS, " user_data", obj, pool->object_size);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj);
volatile int _halt = 1;
while (_halt) {}
}
if (*counter == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu obj=%p alloc=%s free=%s",
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu obj=%p",
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count,
obj, loc ? loc : "(null)", location);
obj);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " alloc=%s first_free=%s repeated_free=%s",
loc ? loc : "(null)", free_loc ? free_loc : "(null)", location);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj);
volatile int _halt = 1;
while (_halt) {}
}
*counter = 0;
memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &location, sizeof(location));
}
static size_t g_total_free = 0;

238
lib/miniaudio.h

@ -7543,11 +7543,13 @@ struct ma_context
ma_proc pa_context_disconnect;
ma_proc pa_context_set_state_callback;
ma_proc pa_context_get_state;
ma_proc pa_context_errno;
ma_proc pa_context_get_sink_info_list;
ma_proc pa_context_get_source_info_list;
ma_proc pa_context_get_sink_info_by_name;
ma_proc pa_context_get_source_info_by_name;
ma_proc pa_operation_unref;
ma_proc pa_operation_cancel;
ma_proc pa_operation_get_state;
ma_proc pa_channel_map_init_extend;
ma_proc pa_channel_map_valid;
@ -7563,6 +7565,7 @@ struct ma_context
ma_proc pa_stream_get_buffer_attr;
ma_proc pa_stream_set_buffer_attr;
ma_proc pa_stream_get_device_name;
ma_proc pa_stream_get_latency;
ma_proc pa_stream_set_write_callback;
ma_proc pa_stream_set_read_callback;
ma_proc pa_stream_set_suspended_callback;
@ -7574,6 +7577,7 @@ struct ma_context
ma_proc pa_stream_cork;
ma_proc pa_stream_trigger;
ma_proc pa_stream_begin_write;
ma_proc pa_stream_cancel_write;
ma_proc pa_stream_write;
ma_proc pa_stream_peek;
ma_proc pa_stream_drop;
@ -11553,6 +11557,17 @@ IMPLEMENTATION
#ifndef miniaudio_c
#define miniaudio_c
/* uTun: необязательные наблюдатели, без изменения ABI и поведения аудиотракта. */
#ifndef MA_DIAGNOSTIC_EVENT
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) ((void)0)
#endif
#ifndef MA_DIAGNOSTIC_DEVICE_INIT
#define MA_DIAGNOSTIC_DEVICE_INIT(device) ((void)0)
#endif
#ifndef MA_DIAGNOSTIC_LOG
#define MA_DIAGNOSTIC_LOG(log, level, message) ((void)0)
#endif
#include <assert.h>
#include <limits.h> /* For INT_MAX */
#include <math.h> /* sin(), etc. */
@ -13752,6 +13767,7 @@ MA_API ma_result ma_log_post(ma_log* pLog, ma_uint32 level, const char* pMessage
return MA_INVALID_ARGS;
}
MA_DIAGNOSTIC_LOG(pLog, level, pMessage);
ma_log_lock(pLog);
{
ma_uint32 iLog;
@ -20200,6 +20216,8 @@ static void ma_device__on_notification(ma_device_notification notification)
{
MA_ASSERT(notification.pDevice != NULL);
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_NOTIFY, -1, notification.type, ma_device_get_state(notification.pDevice));
if (notification.pDevice->onNotification != NULL) {
notification.pDevice->onNotification(&notification);
}
@ -20360,6 +20378,7 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut
{
float masterVolumeFactor;
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_CALLBACK, frameCount, (pFramesIn != NULL) | ((pFramesOut != NULL) << 1));
ma_device_get_master_volume(pDevice, &masterVolumeFactor); /* Use ma_device_get_master_volume() to ensure the volume is loaded atomically. */
if (pDevice->onData) {
@ -20407,6 +20426,7 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut
ma_silence_pcm_frames(pFramesOut, frameCount, pDevice->playback.format, pDevice->playback.channels);
}
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_CALLBACK, frameCount, 0);
}
@ -20590,6 +20610,7 @@ static ma_result ma_device__handle_duplex_callback_capture(ma_device* pDevice, m
if (framesToProcessInClientFormat == 0) {
if (ma_pcm_rb_pointer_distance(pRB) == (ma_int32)ma_pcm_rb_get_subbuffer_size(pRB)) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CAPTURE_DROP, AUDIO_DIAG_READ, framesToProcessInDeviceFormat, ma_pcm_rb_available_read(pRB));
break; /* Overrun. Not enough room in the ring buffer for input frame. Excess frames are dropped. */
}
}
@ -20625,6 +20646,7 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
ma_result result;
ma_uint8 silentInputFrames[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
ma_uint32 totalFramesReadOut = 0;
ma_silence_pcm_frames(pFramesInInternalFormat, frameCount, pDevice->playback.internalFormat, pDevice->playback.internalChannels);
MA_ASSERT(pDevice != NULL);
MA_ASSERT(frameCount > 0);
@ -20636,6 +20658,7 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
Sitting in the ring buffer should be captured data from the capture callback in external format. If there's not enough data in there for
the whole frameCount frames we just use silence instead for the input data.
*/
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_DUPLEX, frameCount, ma_pcm_rb_available_read(pRB));
MA_ZERO_MEMORY(silentInputFrames, sizeof(silentInputFrames));
while (totalFramesReadOut < frameCount && ma_device_is_started(pDevice)) {
@ -20646,8 +20669,12 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
if (pDevice->playback.inputCacheRemaining > 0) {
ma_uint64 framesConvertedIn = pDevice->playback.inputCacheRemaining;
ma_uint64 framesConvertedOut = (frameCount - totalFramesReadOut);
ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut);
result = ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut);
if (result != MA_SUCCESS || (framesConvertedIn == 0 && framesConvertedOut == 0)) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
break;
}
pDevice->playback.inputCacheConsumed += framesConvertedIn;
pDevice->playback.inputCacheRemaining -= framesConvertedIn;
@ -20662,30 +20689,34 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
inputFrameCount = (ma_uint32)pDevice->playback.inputCacheCap;
result = ma_pcm_rb_acquire_read(pRB, &inputFrameCount, &pInputFrames);
if (result == MA_SUCCESS) {
if (inputFrameCount > 0) {
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount);
} else {
if (ma_pcm_rb_pointer_distance(pRB) == 0) {
break; /* Underrun. */
}
if (result != MA_SUCCESS) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
break;
}
if (inputFrameCount > 0) {
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount);
result = ma_pcm_rb_commit_read(pRB, inputFrameCount);
if (result != MA_SUCCESS) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
break;
}
} else {
/* No capture data available. Feed in silence. */
inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap, sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels));
/* Отсутствие микрофонных кадров не останавливает рендер; синтетический ввод не сдвигает capture-ring. */
inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap,
sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels));
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, silentInputFrames, inputFrameCount);
}
pDevice->playback.inputCacheConsumed = 0;
pDevice->playback.inputCacheRemaining = inputFrameCount;
result = ma_pcm_rb_commit_read(pRB, inputFrameCount);
if (result != MA_SUCCESS) {
return result; /* Should never happen. */
}
}
}
if (totalFramesReadOut < frameCount) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_UNFILLED, AUDIO_DIAG_DUPLEX, frameCount - totalFramesReadOut, ma_pcm_rb_available_read(pRB));
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_DUPLEX, totalFramesReadOut, ma_pcm_rb_available_read(pRB));
return MA_SUCCESS;
}
@ -30908,11 +30939,13 @@ typedef int (* ma_pa_context_connect_proc) (
typedef void (* ma_pa_context_disconnect_proc) (ma_pa_context* c);
typedef void (* ma_pa_context_set_state_callback_proc) (ma_pa_context* c, ma_pa_context_notify_cb_t cb, void* userdata);
typedef ma_pa_context_state_t (* ma_pa_context_get_state_proc) (const ma_pa_context* c);
typedef int (* ma_pa_context_errno_proc) (const ma_pa_context* c);
typedef ma_pa_operation* (* ma_pa_context_get_sink_info_list_proc) (ma_pa_context* c, ma_pa_sink_info_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_context_get_source_info_list_proc) (ma_pa_context* c, ma_pa_source_info_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_context_get_sink_info_by_name_proc) (ma_pa_context* c, const char* name, ma_pa_sink_info_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_context_get_source_info_by_name_proc)(ma_pa_context* c, const char* name, ma_pa_source_info_cb_t cb, void* userdata);
typedef void (* ma_pa_operation_unref_proc) (ma_pa_operation* o);
typedef void (* ma_pa_operation_cancel_proc) (ma_pa_operation* o);
typedef ma_pa_operation_state_t (* ma_pa_operation_get_state_proc) (const ma_pa_operation* o);
typedef ma_pa_channel_map* (* ma_pa_channel_map_init_extend_proc) (ma_pa_channel_map* m, unsigned channels, ma_pa_channel_map_def_t def);
typedef int (* ma_pa_channel_map_valid_proc) (const ma_pa_channel_map* m);
@ -30927,6 +30960,7 @@ typedef const ma_pa_sample_spec* (* ma_pa_stream_get_sample_spec_proc) (
typedef const ma_pa_channel_map* (* ma_pa_stream_get_channel_map_proc) (ma_pa_stream* s);
typedef const ma_pa_buffer_attr* (* ma_pa_stream_get_buffer_attr_proc) (ma_pa_stream* s);
typedef ma_pa_operation* (* ma_pa_stream_set_buffer_attr_proc) (ma_pa_stream* s, const ma_pa_buffer_attr* attr, ma_pa_stream_success_cb_t cb, void* userdata);
typedef int (* ma_pa_stream_get_latency_proc)(const ma_pa_stream* s, ma_uint64* usec, int* negative);
typedef const char* (* ma_pa_stream_get_device_name_proc) (const ma_pa_stream* s);
typedef void (* ma_pa_stream_set_write_callback_proc) (ma_pa_stream* s, ma_pa_stream_request_cb_t cb, void* userdata);
typedef void (* ma_pa_stream_set_read_callback_proc) (ma_pa_stream* s, ma_pa_stream_request_cb_t cb, void* userdata);
@ -30939,6 +30973,7 @@ typedef int (* ma_pa_stream_is_corked_proc) (
typedef ma_pa_operation* (* ma_pa_stream_cork_proc) (ma_pa_stream* s, int b, ma_pa_stream_success_cb_t cb, void* userdata);
typedef ma_pa_operation* (* ma_pa_stream_trigger_proc) (ma_pa_stream* s, ma_pa_stream_success_cb_t cb, void* userdata);
typedef int (* ma_pa_stream_begin_write_proc) (ma_pa_stream* s, void** data, size_t* nbytes);
typedef int (* ma_pa_stream_cancel_write_proc) (ma_pa_stream* s);
typedef int (* ma_pa_stream_write_proc) (ma_pa_stream* s, const void* data, size_t nbytes, ma_pa_free_cb_t free_cb, int64_t offset, ma_pa_seek_mode_t seek);
typedef int (* ma_pa_stream_peek_proc) (ma_pa_stream* s, const void** data, size_t* nbytes);
typedef int (* ma_pa_stream_drop_proc) (ma_pa_stream* s);
@ -31126,23 +31161,49 @@ static ma_pa_channel_position_t ma_channel_position_to_pulse(ma_channel position
}
#endif
static ma_result ma_wait_for_operation__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_pa_operation* pOP)
#ifndef MA_PULSE_WAIT_TIMEOUT_MS
#define MA_PULSE_WAIT_TIMEOUT_MS 1000
#endif
static ma_result ma_pulse_wait_step(ma_context* pContext, ma_ptr pMainLoop, ma_timer* pTimer, const char* operation)
{
int resultPA;
if (ma_timer_get_time_in_seconds(pTimer) * 1000 >= MA_PULSE_WAIT_TIMEOUT_MS) {
ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR,
"[PulseAudio] %s timed out after %d ms.\n", operation, MA_PULSE_WAIT_TIMEOUT_MS);
return MA_TIMEOUT;
}
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 0, NULL);
if (resultPA < 0) {
ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] %s iterate failed: %d.\n", operation, resultPA);
return ma_result_from_pulse(resultPA);
}
if (resultPA == 0) ma_sleep(1);
return MA_SUCCESS;
}
static ma_result ma_wait_for_operation__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_pa_operation* pOP)
{
ma_result result;
ma_timer timer;
ma_pa_operation_state_t state;
MA_ASSERT(pContext != NULL);
MA_ASSERT(pOP != NULL);
ma_timer_init(&timer);
for (;;) {
state = ((ma_pa_operation_get_state_proc)pContext->pulse.pa_operation_get_state)(pOP);
if (state == MA_PA_OPERATION_DONE) return MA_SUCCESS;
if (state != MA_PA_OPERATION_RUNNING) {
break; /* Done. */
ma_log_post(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] Operation cancelled.\n");
return MA_ERROR;
}
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
if (resultPA < 0) {
return ma_result_from_pulse(resultPA);
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "wait");
if (result != MA_SUCCESS) {
((ma_pa_operation_cancel_proc)pContext->pulse.pa_operation_cancel)(pOP);
return result;
}
}
@ -31165,9 +31226,11 @@ static ma_result ma_wait_for_operation_and_unref__pulse(ma_context* pContext, ma
static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pPulseContext)
{
int resultPA;
ma_result result;
ma_timer timer;
ma_pa_context_state_t state;
ma_timer_init(&timer);
for (;;) {
state = ((ma_pa_context_get_state_proc)pContext->pulse.pa_context_get_state)((ma_pa_context*)pPulseContext);
if (state == MA_PA_CONTEXT_READY) {
@ -31179,10 +31242,8 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext,
return MA_ERROR;
}
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
if (resultPA < 0) {
return ma_result_from_pulse(resultPA);
}
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "context connect");
if (result != MA_SUCCESS) return result;
}
/* Should never get here. */
@ -31191,9 +31252,11 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext,
static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pStream)
{
int resultPA;
ma_result result;
ma_timer timer;
ma_pa_stream_state_t state;
ma_timer_init(&timer);
for (;;) {
state = ((ma_pa_stream_get_state_proc)pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pStream);
if (state == MA_PA_STREAM_READY) {
@ -31205,10 +31268,8 @@ static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, m
return MA_ERROR;
}
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
if (resultPA < 0) {
return ma_result_from_pulse(resultPA);
}
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "stream connect");
if (result != MA_SUCCESS) return result;
}
return MA_SUCCESS;
@ -31382,10 +31443,13 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext
if (deviceType == ma_device_type_playback) {
ma_pa_sink_info sinkInfo;
MA_ZERO_OBJECT(&sinkInfo);
sinkInfo.index = (ma_uint32)-1;
result = ma_context_get_sink_info__pulse(pContext, NULL, &sinkInfo);
if (result != MA_SUCCESS) {
return result;
}
if (sinkInfo.index == (ma_uint32)-1) return MA_NO_DEVICE;
if (pIndex != NULL) {
*pIndex = sinkInfo.index;
@ -31394,10 +31458,13 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext
if (deviceType == ma_device_type_capture) {
ma_pa_source_info sourceInfo;
MA_ZERO_OBJECT(&sourceInfo);
sourceInfo.index = (ma_uint32)-1;
result = ma_context_get_source_info__pulse(pContext, NULL, &sourceInfo);
if (result != MA_SUCCESS) {
return result;
}
if (sourceInfo.index == (ma_uint32)-1) return MA_NO_DEVICE;
if (pIndex != NULL) {
*pIndex = sourceInfo.index;
@ -31551,7 +31618,7 @@ static void ma_context_get_device_info_sink_callback__pulse(ma_pa_context* pPuls
{
ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData;
if (endOfList > 0) {
if (endOfList != 0 || pInfo == NULL) {
return;
}
@ -31588,7 +31655,7 @@ static void ma_context_get_device_info_source_callback__pulse(ma_pa_context* pPu
{
ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData;
if (endOfList > 0) {
if (endOfList != 0 || pInfo == NULL) {
return;
}
@ -31640,6 +31707,7 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi
}
result = ma_context_get_default_device_index__pulse(pContext, deviceType, &callbackData.defaultDeviceIndex);
if (result != MA_SUCCESS) return result;
if (deviceType == ma_device_type_playback) {
pOP = ((ma_pa_context_get_sink_info_by_name_proc)pContext->pulse.pa_context_get_sink_info_by_name)((ma_pa_context*)(pContext->pulse.pPulseContext), pDeviceName, ma_context_get_device_info_sink_callback__pulse, &callbackData);
@ -31648,7 +31716,8 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi
}
if (pOP != NULL) {
ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP);
result = ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP);
if (result != MA_SUCCESS) goto done;
} else {
result = MA_ERROR;
goto done;
@ -31747,6 +31816,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
MA_ASSERT(bpf > 0);
frameCount = byteCount / bpf;
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_READ, frameCount, deviceState);
framesProcessed = 0;
while (ma_device_get_state(pDevice) == ma_device_state_started && framesProcessed < frameCount) {
@ -31756,6 +31826,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
int pulseResult = ((ma_pa_stream_peek_proc)pDevice->pContext->pulse.pa_stream_peek)(pStream, &pMappedPCMFrames, &bytesMapped);
if (pulseResult < 0) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult);
break; /* Failed to map. Abort. */
}
@ -31764,12 +31835,24 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
if (pMappedPCMFrames != NULL) {
ma_device_handle_backend_data_callback(pDevice, NULL, pMappedPCMFrames, framesMapped);
} else {
/* It's a hole. */
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[PulseAudio] ma_device_on_read__pulse: Hole.\n");
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, framesMapped, 0);
ma_uint8 silence[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
ma_uint64 remaining = framesMapped;
ma_uint32 capacity = sizeof(silence) / bpf;
ma_silence_pcm_frames(silence, capacity, pDevice->capture.internalFormat, pDevice->capture.internalChannels);
while (remaining > 0 && ma_device_get_state(pDevice) == ma_device_state_started) {
ma_uint32 n = (ma_uint32)ma_min(remaining, capacity);
#ifdef MA_AUDIO_CAPTURE_HOLE
MA_AUDIO_CAPTURE_HOLE(framesMapped - remaining);
#endif
ma_device_handle_backend_data_callback(pDevice, NULL, silence, n);
remaining -= n;
}
}
pulseResult = ((ma_pa_stream_drop_proc)pDevice->pContext->pulse.pa_stream_drop)(pStream);
if (pulseResult < 0) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult);
break; /* Failed to drop the buffer. */
}
@ -31780,6 +31863,7 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
break;
}
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_READ, framesProcessed, 0);
}
static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stream* pStream, ma_uint64* pFramesProcessed)
@ -31798,6 +31882,7 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
deviceState = ma_device_get_state(pDevice);
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_MAP, 0, deviceState);
bytesMapped = ((ma_pa_stream_writable_size_proc)pDevice->pContext->pulse.pa_stream_writable_size)(pStream);
if (bytesMapped != (size_t)-1) {
if (bytesMapped > 0) {
@ -31810,15 +31895,23 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
}
framesMapped = bytesMapped / bpf;
if (framesMapped == 0 || pMappedPCMFrames == NULL) {
((ma_pa_stream_cancel_write_proc)pDevice->pContext->pulse.pa_stream_cancel_write)(pStream);
result = MA_ERROR;
goto done;
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesMapped, 0);
if (deviceState == ma_device_state_started || deviceState == ma_device_state_starting) { /* Check for starting state just in case this is being used to do the initial fill. */
ma_device_handle_backend_data_callback(pDevice, pMappedPCMFrames, NULL, framesMapped);
} else {
/* Device is not started. Write silence. */
ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.format, pDevice->playback.channels);
ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.internalFormat, pDevice->playback.internalChannels);
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_SUBMIT, framesMapped, 0);
pulseResult = ((ma_pa_stream_write_proc)pDevice->pContext->pulse.pa_stream_write)(pStream, pMappedPCMFrames, bytesMapped, NULL, 0, MA_PA_SEEK_RELATIVE);
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_SUBMIT, framesMapped, pulseResult);
if (pulseResult < 0) {
result = ma_result_from_pulse(pulseResult);
goto done; /* Failed to write data to stream. */
@ -31835,6 +31928,10 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
}
done:
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesProcessed, result);
if (result != MA_SUCCESS) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_WRITE, 1, result);
}
if (pFramesProcessed != NULL) {
*pFramesProcessed = framesProcessed;
}
@ -31850,6 +31947,7 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
ma_uint64 framesProcessed;
ma_uint32 deviceState;
ma_result result;
ma_bool32 diagnosticNoProgress = MA_FALSE;
MA_ASSERT(pDevice != NULL);
@ -31866,6 +31964,7 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
MA_ASSERT(bpf > 0);
frameCount = byteCount / bpf;
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_WRITE, frameCount, deviceState);
framesProcessed = 0;
while (framesProcessed < frameCount) {
@ -31882,8 +31981,14 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
break;
}
if (framesProcessedThisIteration == 0 && !diagnosticNoProgress) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_NO_PROGRESS, AUDIO_DIAG_WRITE, 1, frameCount - framesProcessed);
diagnosticNoProgress = MA_TRUE;
}
if (framesProcessedThisIteration == 0) break;
framesProcessed += framesProcessedThisIteration;
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_WRITE, framesProcessed, 0);
}
static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData)
@ -31902,10 +32007,10 @@ static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData
if (suspended == 1) {
ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Suspended.\n");
ma_device__on_notification_stopped(pDevice);
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_began));
} else {
ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Resumed.\n");
ma_device__on_notification_started(pDevice);
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_ended));
}
}
@ -32044,7 +32149,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi
if (pDescriptorCapture->sampleRate != 0) {
ss.rate = pDescriptorCapture->sampleRate;
}
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY;
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY | MA_PA_STREAM_AUTO_TIMING_UPDATE | MA_PA_STREAM_INTERPOLATE_TIMING;
if (ma_format_from_pulse(ss.format) == ma_format_unknown) {
if (ma_is_little_endian()) {
@ -32203,7 +32308,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi
ss.rate = pDescriptorPlayback->sampleRate;
}
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY;
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY | MA_PA_STREAM_AUTO_TIMING_UPDATE | MA_PA_STREAM_INTERPOLATE_TIMING;
if (ma_format_from_pulse(ss.format) == ma_format_unknown) {
if (ma_is_little_endian()) {
ss.format = MA_PA_SAMPLE_FLOAT32LE;
@ -32375,6 +32480,10 @@ on_error1:
((ma_pa_stream_unref_proc)pDevice->pContext->pulse.pa_stream_unref)((ma_pa_stream*)pDevice->pulse.pStreamCapture);
}
on_error0:
((ma_pa_context_disconnect_proc)pDevice->pContext->pulse.pa_context_disconnect)((ma_pa_context*)pDevice->pulse.pPulseContext);
((ma_pa_context_unref_proc)pDevice->pContext->pulse.pa_context_unref)((ma_pa_context*)pDevice->pulse.pPulseContext);
((ma_pa_mainloop_free_proc)pDevice->pContext->pulse.pa_mainloop_free)((ma_pa_mainloop*)pDevice->pulse.pMainLoop);
MA_ZERO_OBJECT(&pDevice->pulse);
return result;
}
@ -32504,8 +32613,24 @@ static ma_result ma_device_data_loop__pulse(ma_device* pDevice)
the callbacks deal with it.
*/
while (ma_device_get_state(pDevice) == ma_device_state_started) {
ma_pa_context_state_t contextState = ((ma_pa_context_get_state_proc)pDevice->pContext->pulse.pa_context_get_state)((ma_pa_context*)pDevice->pulse.pPulseContext);
ma_pa_stream_state_t captureState = pDevice->pulse.pStreamCapture ?
((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamCapture) : MA_PA_STREAM_READY;
ma_pa_stream_state_t playbackState = pDevice->pulse.pStreamPlayback ?
((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamPlayback) : MA_PA_STREAM_READY;
if (contextState == MA_PA_CONTEXT_FAILED || contextState == MA_PA_CONTEXT_TERMINATED ||
captureState == MA_PA_STREAM_FAILED || captureState == MA_PA_STREAM_TERMINATED ||
playbackState == MA_PA_STREAM_FAILED || playbackState == MA_PA_STREAM_TERMINATED) {
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR,
"[PulseAudio] Device failed context=%d capture=%d playback=%d errno=%d.\n", contextState, captureState,
playbackState, ((ma_pa_context_errno_proc)pDevice->pContext->pulse.pa_context_errno)((ma_pa_context*)pDevice->pulse.pPulseContext));
return MA_ERROR;
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_LOOP, 0, 0);
resultPA = ((ma_pa_mainloop_iterate_proc)pDevice->pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pDevice->pulse.pMainLoop, 1, NULL);
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_LOOP, 0, resultPA);
if (resultPA < 0) {
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_LOOP, 1, resultPA);
break;
}
}
@ -32588,11 +32713,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_context_disconnect = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_disconnect");
pContext->pulse.pa_context_set_state_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_set_state_callback");
pContext->pulse.pa_context_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_state");
pContext->pulse.pa_context_errno = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_errno");
pContext->pulse.pa_context_get_sink_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_list");
pContext->pulse.pa_context_get_source_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_list");
pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_by_name");
pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_by_name");
pContext->pulse.pa_operation_unref = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_unref");
pContext->pulse.pa_operation_cancel = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_cancel");
pContext->pulse.pa_operation_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_get_state");
pContext->pulse.pa_channel_map_init_extend = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_init_extend");
pContext->pulse.pa_channel_map_valid = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_valid");
@ -32607,6 +32734,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_get_channel_map = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_channel_map");
pContext->pulse.pa_stream_get_buffer_attr = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_buffer_attr");
pContext->pulse.pa_stream_set_buffer_attr = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_buffer_attr");
pContext->pulse.pa_stream_get_latency = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_latency");
pContext->pulse.pa_stream_get_device_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_device_name");
pContext->pulse.pa_stream_set_write_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_write_callback");
pContext->pulse.pa_stream_set_read_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_read_callback");
@ -32619,6 +32747,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_cork = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cork");
pContext->pulse.pa_stream_trigger = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_trigger");
pContext->pulse.pa_stream_begin_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_begin_write");
pContext->pulse.pa_stream_cancel_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cancel_write");
pContext->pulse.pa_stream_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_write");
pContext->pulse.pa_stream_peek = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_peek");
pContext->pulse.pa_stream_drop = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_drop");
@ -32651,11 +32780,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
ma_pa_context_disconnect_proc _pa_context_disconnect = pa_context_disconnect;
ma_pa_context_set_state_callback_proc _pa_context_set_state_callback = pa_context_set_state_callback;
ma_pa_context_get_state_proc _pa_context_get_state = pa_context_get_state;
ma_pa_context_errno_proc _pa_context_errno = pa_context_errno;
ma_pa_context_get_sink_info_list_proc _pa_context_get_sink_info_list = pa_context_get_sink_info_list;
ma_pa_context_get_source_info_list_proc _pa_context_get_source_info_list = pa_context_get_source_info_list;
ma_pa_context_get_sink_info_by_name_proc _pa_context_get_sink_info_by_name = pa_context_get_sink_info_by_name;
ma_pa_context_get_source_info_by_name_proc _pa_context_get_source_info_by_name= pa_context_get_source_info_by_name;
ma_pa_operation_unref_proc _pa_operation_unref = pa_operation_unref;
ma_pa_operation_cancel_proc _pa_operation_cancel = pa_operation_cancel;
ma_pa_operation_get_state_proc _pa_operation_get_state = pa_operation_get_state;
ma_pa_channel_map_init_extend_proc _pa_channel_map_init_extend = pa_channel_map_init_extend;
ma_pa_channel_map_valid_proc _pa_channel_map_valid = pa_channel_map_valid;
@ -32670,6 +32801,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
ma_pa_stream_get_channel_map_proc _pa_stream_get_channel_map = pa_stream_get_channel_map;
ma_pa_stream_get_buffer_attr_proc _pa_stream_get_buffer_attr = pa_stream_get_buffer_attr;
ma_pa_stream_set_buffer_attr_proc _pa_stream_set_buffer_attr = pa_stream_set_buffer_attr;
ma_pa_stream_get_latency_proc _pa_stream_get_latency = pa_stream_get_latency;
ma_pa_stream_get_device_name_proc _pa_stream_get_device_name = pa_stream_get_device_name;
ma_pa_stream_set_write_callback_proc _pa_stream_set_write_callback = pa_stream_set_write_callback;
ma_pa_stream_set_read_callback_proc _pa_stream_set_read_callback = pa_stream_set_read_callback;
@ -32682,6 +32814,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
ma_pa_stream_cork_proc _pa_stream_cork = pa_stream_cork;
ma_pa_stream_trigger_proc _pa_stream_trigger = pa_stream_trigger;
ma_pa_stream_begin_write_proc _pa_stream_begin_write = pa_stream_begin_write;
ma_pa_stream_cancel_write_proc _pa_stream_cancel_write = pa_stream_cancel_write;
ma_pa_stream_write_proc _pa_stream_write = pa_stream_write;
ma_pa_stream_peek_proc _pa_stream_peek = pa_stream_peek;
ma_pa_stream_drop_proc _pa_stream_drop = pa_stream_drop;
@ -32713,11 +32846,13 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_context_disconnect = (ma_proc)_pa_context_disconnect;
pContext->pulse.pa_context_set_state_callback = (ma_proc)_pa_context_set_state_callback;
pContext->pulse.pa_context_get_state = (ma_proc)_pa_context_get_state;
pContext->pulse.pa_context_errno = (ma_proc)_pa_context_errno;
pContext->pulse.pa_context_get_sink_info_list = (ma_proc)_pa_context_get_sink_info_list;
pContext->pulse.pa_context_get_source_info_list = (ma_proc)_pa_context_get_source_info_list;
pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)_pa_context_get_sink_info_by_name;
pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)_pa_context_get_source_info_by_name;
pContext->pulse.pa_operation_unref = (ma_proc)_pa_operation_unref;
pContext->pulse.pa_operation_cancel = (ma_proc)_pa_operation_cancel;
pContext->pulse.pa_operation_get_state = (ma_proc)_pa_operation_get_state;
pContext->pulse.pa_channel_map_init_extend = (ma_proc)_pa_channel_map_init_extend;
pContext->pulse.pa_channel_map_valid = (ma_proc)_pa_channel_map_valid;
@ -32732,6 +32867,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_get_channel_map = (ma_proc)_pa_stream_get_channel_map;
pContext->pulse.pa_stream_get_buffer_attr = (ma_proc)_pa_stream_get_buffer_attr;
pContext->pulse.pa_stream_set_buffer_attr = (ma_proc)_pa_stream_set_buffer_attr;
pContext->pulse.pa_stream_get_latency = (ma_proc)_pa_stream_get_latency;
pContext->pulse.pa_stream_get_device_name = (ma_proc)_pa_stream_get_device_name;
pContext->pulse.pa_stream_set_write_callback = (ma_proc)_pa_stream_set_write_callback;
pContext->pulse.pa_stream_set_read_callback = (ma_proc)_pa_stream_set_read_callback;
@ -32744,6 +32880,7 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
pContext->pulse.pa_stream_cork = (ma_proc)_pa_stream_cork;
pContext->pulse.pa_stream_trigger = (ma_proc)_pa_stream_trigger;
pContext->pulse.pa_stream_begin_write = (ma_proc)_pa_stream_begin_write;
pContext->pulse.pa_stream_cancel_write = (ma_proc)_pa_stream_cancel_write;
pContext->pulse.pa_stream_write = (ma_proc)_pa_stream_write;
pContext->pulse.pa_stream_peek = (ma_proc)_pa_stream_peek;
pContext->pulse.pa_stream_drop = (ma_proc)_pa_stream_drop;
@ -43767,6 +43904,7 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC
result = pContext->callbacks.onDeviceInit(pDevice, pConfig, &descriptorPlayback, &descriptorCapture);
if (result != MA_SUCCESS) {
ma_event_uninit(&pDevice->stopEvent);
ma_event_uninit(&pDevice->startEvent);
ma_event_uninit(&pDevice->wakeupEvent);
ma_mutex_uninit(&pDevice->startStopLock);
@ -44008,6 +44146,8 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC
}
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_OPEN, -1, pDevice->type, pDevice->sampleRate);
MA_DIAGNOSTIC_DEVICE_INIT(pDevice);
MA_ASSERT(ma_device_get_state(pDevice) == ma_device_state_stopped);
return MA_SUCCESS;
}
@ -44175,6 +44315,7 @@ MA_API void ma_device_uninit(ma_device* pDevice)
ma_free(pDevice->pContext, &allocationCallbacks);
}
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CLOSE, -1, 0, 0);
MA_ZERO_OBJECT(pDevice);
}
@ -44506,6 +44647,14 @@ MA_API ma_result ma_device_handle_backend_data_callback(ma_device* pDevice, void
if (frameCount == 0) {
return MA_INVALID_ARGS;
}
if (((pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_loopback) && pInput == NULL) ||
(pDevice->type == ma_device_type_playback && pOutput == NULL)) {
return MA_INVALID_ARGS;
}
#ifdef MA_AUDIO_TIMING_BEGIN
MA_AUDIO_TIMING_BEGIN(pDevice, frameCount, pInput != NULL);
#endif
if (pDevice->type == ma_device_type_duplex) {
if (pInput != NULL) {
@ -44517,22 +44666,17 @@ MA_API ma_result ma_device_handle_backend_data_callback(ma_device* pDevice, void
}
} else {
if (pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_loopback) {
if (pInput == NULL) {
return MA_INVALID_ARGS;
}
ma_device__send_frames_to_client(pDevice, frameCount, pInput);
}
if (pDevice->type == ma_device_type_playback) {
if (pOutput == NULL) {
return MA_INVALID_ARGS;
}
ma_device__read_frames_from_client(pDevice, frameCount, pOutput);
}
}
#ifdef MA_AUDIO_TIMING_END
MA_AUDIO_TIMING_END();
#endif
return MA_SUCCESS;
}

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

178
lib/speex_aec.c

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

80
lib/speex_aec.h

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

35
lib/speexdsp/COPYING

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

743
lib/u_async.c

File diff suppressed because it is too large Load Diff

12
lib/u_async.h

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

8
lib/u_async_doc.md

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

31
src/Makefile.am

@ -62,7 +62,15 @@ utun_CORE_SOURCES = \
media_delivery/media_delivery.c \
media_delivery/media_download.c \
media_delivery/media_index.c \
media_delivery/file_transfer.c \
media_delivery/file_transfer.h \
media_async/media_async.c \
media_async/attachment.c \
media_async/attachment.h \
media_async/voice_file.c \
media_async/voice_file.h \
media_async/attachment_send.c \
media_async/attachment_send.h \
video/video.c \
transport_layer/dummynet.c \
ntp_time.c \
@ -100,6 +108,11 @@ utun_CORE_SOURCES = \
dm/dm_core.c \
dm/dm_crypto.c \
dm/dm_mailbox.c \
dm/dm_media.c \
dm/dm_media.h \
dm/dm_media_priv.h \
dm/dm_mailbox_media.c \
dm/dm_mailbox_media.h \
call/call.c \
call/call_headless.c \
radio/radio.c \
@ -167,7 +180,15 @@ libutun_a_SOURCES = \
media_delivery/media_delivery.c \
media_delivery/media_download.c \
media_delivery/media_index.c \
media_delivery/file_transfer.c \
media_delivery/file_transfer.h \
media_async/media_async.c \
media_async/attachment.c \
media_async/attachment.h \
media_async/voice_file.c \
media_async/voice_file.h \
media_async/attachment_send.c \
media_async/attachment_send.h \
video/video.c \
transport_layer/dummynet.c \
ntp_time.c \
@ -204,6 +225,11 @@ libutun_a_SOURCES = \
dm/dm_core.c \
dm/dm_crypto.c \
dm/dm_mailbox.c \
dm/dm_media.c \
dm/dm_media.h \
dm/dm_media_priv.h \
dm/dm_mailbox_media.c \
dm/dm_mailbox_media.h \
call/call.c \
call/call_headless.c \
radio/radio.c \
@ -252,6 +278,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

233
src/call/call.c

@ -13,6 +13,7 @@
#include "call.h"
#include "call_proto.h"
#include "../dm/dm_core.h"
#include "../utun_instance.h"
#include "../chat/chat_event.h"
#include "../routing_layer/etcp_router.h"
@ -38,8 +39,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,13 +55,14 @@ 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) */
void* traffic_timer; /* ACTIVE: 20с без медиа (запускается по первому медиа) */
void* teardown_timer; /* отложенный разрыв conn_mgr (500мс после завершения) */
void* stats_timer; /* ACTIVE: раз в 1с — CALL_STATS (rtt/buffer/tempo) */
void* stats_timer; /* Раз в 1с — путь подключения; в ACTIVE также CALL_STATS. */
uint8_t connection_reported, connection_type;
uint64_t connection_next_hop;
uint64_t last_media_tb; /* время последнего CALL_MEDIA (0.1ms) */
uint16_t tx_media_seq; /* seq следующего исходящего CALL_MEDIA */
struct ll_queue* tx_q; /* FIFO закодированных CALL_MEDIA (backpressure) */
@ -94,7 +95,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 "?";
@ -258,10 +258,12 @@ static void call_post_bgp_path(struct call_ctx* ctx, struct call_session* s) {
char text[512];
int off = 0;
if (hc <= 1) {
off = snprintf(text, sizeof(text), "маршрут: прямой");
if (!hc) {
off = snprintf(text, sizeof(text), "BGP: нет маршрута");
} else if (hc == 1) {
off = snprintf(text, sizeof(text), "BGP: прямой");
} else {
off = snprintf(text, sizeof(text), "маршрут: через ");
off = snprintf(text, sizeof(text), "BGP: через ");
for (int i = hc - 1; i >= 1 && off < (int)sizeof(text) - 48; i--) {
char hop[48];
struct TOPO_NODE* ni = topo_node_registry_find(ctx->inst->topo_groups, hops[i]);
@ -286,6 +288,31 @@ static void call_post_bgp_path(struct call_ctx* ctx, struct call_session* s) {
CALL_ID, (unsigned long long)s->call_id, hc, cr, text);
}
/* Тот же выбор next hop, что для CALL_MEDIA, включая BGP fallback до готовности CM. */
static void call_post_connection(struct call_session* s) {
struct ETCP_ROUTER_PATH path = {0};
if (s->state != CALL_ST_ENDED)
path = etcp_router_get_path(s->ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL);
if (s->connection_reported && s->connection_type == path.type && s->connection_next_hop == path.next_hop_node_id) return;
s->connection_reported = 1; s->connection_type = path.type; s->connection_next_hop = path.next_hop_node_id;
uint8_t buf[17];
memcpy(buf, &s->call_id, 8); buf[8] = path.type; memcpy(buf + 9, &path.next_hop_node_id, 8);
static const char* names[] = { "NONE", "DIRECT", "REVERSE", "INDIRECT" };
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: connection id=%016llx peer=%016llx type=%s via=%016llx",
CALL_ID, (unsigned long long)s->call_id, (unsigned long long)s->peer_node_id, names[path.type],
(unsigned long long)path.next_hop_node_id);
chat_event_post(s->ctx->inst, CHAT_EVT_CALL_CONNECTION, buf, sizeof(buf));
if (s->state != CALL_ST_ENDED) call_post_bgp_path(s->ctx, s);
}
static void call_watch_connection(struct call_session* s) {
call_post_connection(s);
if (s->state == CALL_ST_ENDED || s->stats_timer) return;
s->stats_timer = uasync_set_timeout(s->ctx->inst->ua, 10000, s, call_stats_timer_cb, "call_stats");
if (!s->stats_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: connection timer failed id=%016llx",
CALL_ID, (unsigned long long)s->call_id);
}
/* ─────────────────── завершение ─────────────────── */
/* Отложенный разрыв: единственная точка, где закрывается conn_mgr (после того,
@ -316,10 +343,10 @@ static void call_end(struct call_ctx* ctx, struct call_session* s, uint8_t reaso
s->role == CALL_ROLE_CALLER ? "caller" : "callee",
s->c_rx_frames, s->c_tx_frames, s->c_tx_dropped);
call_post_connection(s);
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 +356,40 @@ 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);
} 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);
}
call_watch_connection(s);
(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_watch_connection(s);
s->traffic_timer = uasync_set_timeout(ctx->inst->ua, CALL_NO_TRAFFIC_TB, s, call_traffic_timeout_cb, "call_traffic");
}
/* ─────────────────── таймеры ─────────────────── */
@ -392,7 +421,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;
@ -447,24 +476,26 @@ static void call_traffic_timeout_cb(void* arg) {
}
/* Периодическое событие CALL_STATS (раз в 1с): rtt_ms, buffer_ms, tempo_x100. */
static void call_stats_timer_cb(void* arg) {
struct call_session* s = (struct call_session*)arg;
if (!s || !s->ctx) return;
static void call_post_stats(struct call_session* s) {
struct call_ctx* ctx = s->ctx;
s->stats_timer = NULL;
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;
uint16_t rtt_tb = 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];
if (rconn && rconn->recv_conn) rtt_tb = rconn->recv_conn->rtt_last;
uint16_t rtt = rtt_tb / 10; /* ETCP хранит RTT в 0.1мс; CALL_STATS передаёт миллисекунды. */
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "%s: stats id=%016llx recv_peer=%016llx rtt_tb=%u rtt_ms=%u buffer=%dms tempo=%d dropped=%u underruns=%u",
CALL_ID, (unsigned long long)s->call_id,
(unsigned long long)(rconn && rconn->recv_conn ? rconn->recv_conn->peer_node_id : 0),
rtt_tb, rtt, buffer_ms, tempo_x100, dropped, underruns);
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,44 +507,21 @@ 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_stats_timer_cb(void* arg) {
struct call_session* s = arg;
if (!s || !s->ctx) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: invalid stats timer session", CALL_ID); return; }
s->stats_timer = NULL;
if (s->state == CALL_ST_ENDED) return;
if (s->state == CALL_ST_ACTIVE) call_post_stats(s);
call_watch_connection(s);
}
/* ─────────────────── обработчики приёма ─────────────────── */
@ -529,7 +537,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) {
@ -576,7 +584,7 @@ static void call_handle_invite(struct call_ctx* ctx, uint64_t from, uint64_t gro
(unsigned long long)group, (unsigned long long)inv_group);
call_event_incoming(ctx, group, from, call_id);
call_post_bgp_path(ctx, s);
call_watch_connection(s);
/* watchdog: вызывающий шлёт кодограммы INVITE каждые 300мс; 2с тишины → вызов сорван */
s->ring_timer = uasync_set_timeout(ctx->inst->ua, CALL_RING_DEAD_TB, s, call_ring_dead_cb, "call_ring_dead");
@ -606,18 +614,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 +757,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 +784,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 +839,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 +871,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);
@ -887,7 +895,22 @@ void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_nod
return;
}
uint64_t group_id = strtoull(ch_id, NULL, 10);
uint64_t group_id = 0;
char route_ch[64];
if (!strncmp(ch_id, "dm:", 3)) {
uint64_t peer = 0;
int available = dm_call_route(inst, ch_id + 3, &peer, &group_id);
if (!available || peer != peer_node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "PM call rejected conv=%s requested_peer=%llu actual_peer=%llu available=%d",
ch_id + 3, (unsigned long long)peer_node_id, (unsigned long long)peer, available);
uint8_t evt[9];
memcpy(evt, &out_call_id, 8); evt[8] = 1;
chat_event_post(inst, CHAT_EVT_CALL_ERROR, evt, sizeof(evt));
return;
}
snprintf(route_ch, sizeof(route_ch), "%llu", (unsigned long long)group_id);
ch_id = route_ch;
} else group_id = strtoull(ch_id, NULL, 10);
if (!call_is_online(inst, ch_id, peer_node_id)) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: peer 0x%016llx offline in group 0x%016llx — call not started",
CALL_ID, (unsigned long long)peer_node_id, (unsigned long long)group_id);
@ -909,11 +932,12 @@ 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;
}
call_post_bgp_path(ctx, s);
call_watch_connection(s);
s->ring_timer = uasync_set_timeout(inst->ua, CALL_RING_TIMEOUT_TB, s, call_ring_timeout_cb, "call_ring");
s->invite_timer = uasync_set_timeout(inst->ua, CALL_INVITE_RETRY_TB, s, call_invite_retry_cb, "call_invite_retry");
}
@ -922,11 +946,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 +958,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) {

8
src/call/call.h

@ -3,7 +3,7 @@
// Модуль оперирует (group_id, peer_node_id): звонок можно инициировать из
// CHAT-группы или из DM (group_id — группа маршрутизации, как в dm_core).
// Сигналинг идёт через etcp_router (ROUTE_CRYPTO_SIGN|ENCRYPT), соединение
// устанавливается через conn_mgr (3-фазное DIRECT→REVERSE→INDIRECT).
// оптимизируется через параллельные пробы conn_mgr (DIRECT/REVERSE/INDIRECT).
//
// Все функции — в uasync-потоке. GUI/headless вызывают через trampoline
// (gui_bridge_post_uasync_fn / uasync_post).
@ -11,6 +11,7 @@
#define CALL_H
#include <stdint.h>
#include "call_jitter.h"
struct UTUN_INSTANCE;
@ -25,7 +26,8 @@ void call_destroy(struct UTUN_INSTANCE* inst); /* перед dm_core_destroy */
/* ── сигналинг (uasync-поток) ── */
/* Caller: начать звонок. out_call_id — случайный uint64, генерит вызывающий. */
/* Caller: ch_id — группа либо "dm:<conv_id>" (BGP-группа выбирается при запуске).
* out_call_id — случайный uint64, генерит вызывающий. */
void call_start (struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id);
/* Callee: ответить/отклонить. hangup — любая сторона в любой момент. */
@ -60,7 +62,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);

376
src/call/call_audio.c

@ -3,46 +3,88 @@
// Один активный звонок: 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_playback_started = 0; /* первый PCM получен; только аудиопоток */
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_prepared_capture = 0;
static int g_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */
static struct audio_compressor* g_compressor = NULL;
static uint64_t g_tx_generation;
static unsigned int g_tx_pending, g_tx_dropped, g_tx_reported;
static unsigned int g_tx_reasons;
static int g_tx_encode_error;
static uint32_t g_aec_reported_over, g_aec_reported_under, g_pcm_events;
/* Task и данные — одна аллокация; uasync освобождает task после callback и при destroy. */
struct call_audio_tx {
struct posted_task task;
struct UTUN_INSTANCE* inst;
uint64_t call_id, generation, created_tb;
int len;
uint8_t opus[256];
};
static void call_audio_send(void* arg) {
struct call_audio_tx* tx = arg;
pthread_mutex_lock(&g_mtx);
int current = tx->generation == g_tx_generation;
if (g_tx_pending) --g_tx_pending;
int send = current && g_active && !g_ending && g_inst == tx->inst && g_call_id == tx->call_id;
if (get_time_tb() - tx->created_tb > 2000) {
send = 0;
if (current) { ++g_tx_dropped; g_tx_reasons |= 16; }
}
pthread_mutex_unlock(&g_mtx);
if (send) call_send_media(tx->inst, tx->call_id, tx->opus, tx->len);
}
static const struct call_audio_ops g_ops = {
.on_media = call_audio_on_media,
.get_stats = call_audio_get_stats,
};
/* 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);
@ -50,8 +92,10 @@ static int call_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t*
int call_audio_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (g_inst == inst) return 0;
g_inst = inst;
pthread_mutex_lock(&g_mtx);
if (g_inst == inst) { pthread_mutex_unlock(&g_mtx); return 0; }
g_inst = inst; g_tx_pending = 0;
pthread_mutex_unlock(&g_mtx);
call_set_audio_ops(inst, &g_ops);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized", CALL_AUDIO_ID);
return 0;
@ -60,7 +104,9 @@ int call_audio_init(struct UTUN_INSTANCE* inst) {
void call_audio_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || g_inst != inst) return;
call_set_audio_ops(inst, NULL);
pthread_mutex_lock(&g_mtx);
g_inst = NULL;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed", CALL_AUDIO_ID);
}
@ -68,19 +114,18 @@ void call_audio_destroy(struct UTUN_INSTANCE* inst) {
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len) {
(void)inst; (void)seq; (void)ts_ms;
(void)inst; (void)seq;
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, ts_ms, (int64_t)(get_time_tb() / 10));
}
pthread_mutex_unlock(&g_mtx);
}
/* ── GUI-поток: lifecycle ── */
int call_audio_start(uint64_t call_id) {
static int call_audio_start_impl(uint64_t call_id, int prepared) {
if (call_id == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: start bad call_id=0", CALL_AUDIO_ID);
return -1;
@ -88,8 +133,10 @@ int call_audio_start(uint64_t call_id) {
pthread_mutex_lock(&g_mtx);
if (g_active && g_call_id == call_id) {
int same_mode = g_prepared_capture == prepared;
pthread_mutex_unlock(&g_mtx);
return 0;
if (!same_mode) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: capture policy cannot change within active call", CALL_AUDIO_ID);
return same_mode ? 0 : -1;
}
if (g_active) {
pthread_mutex_unlock(&g_mtx);
@ -113,8 +160,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 +172,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,21 +180,64 @@ 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 && !prepared) {
aec = call_audio_aec_create();
if (!aec) {
g_aec_enabled = 0;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: AEC create failed — call without echo cancellation", CALL_AUDIO_ID);
}
}
/* компрессор микрофона (AGC) — как в рации: выравнивает тихий проводной микрофон
* (HAL не применяет AGC на проводном входе). lookahead=0 — без задержки. */
struct audio_compressor* ac = audio_compressor_create();
if (ac) {
audio_compressor_config_t accfg = {0};
accfg.sample_rate = CALL_AUDIO_SAMPLE_RATE;
accfg.channels = 1;
accfg.block_duration_ms = CALL_AUDIO_FRAME_MS;
accfg.lookback_ms = 200;
accfg.lookahead_ms = 0;
accfg.max_gain_db = (float)chat_setting_get_int(g_inst, "call_compressor_max_gain_db", 15);
accfg.rise_rate_per_sec = (float)chat_setting_get_int(g_inst, "compressor_rise_rate", 2);
accfg.target_level = 0.25f; /* -12 dBFS — как у голосовых сообщений */
if (audio_compressor_configure(ac, &accfg) != 0) { audio_compressor_destroy(ac); ac = NULL; }
if (ac) audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "call_compressor_enabled", 1));
} else {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: compressor create failed — call without AGC", CALL_AUDIO_ID);
}
g_encoder = enc;
g_decoder = dec;
g_vj = j;
g_tones = tones;
g_aec = aec; g_prepared_capture = prepared;
g_compressor = ac;
g_call_id = call_id;
g_active = 1;
++g_tx_generation; g_tx_dropped = g_tx_reported = 0;
g_tx_reasons = 0; g_tx_encode_error = 0;
g_ending = 0;
g_ended_started = 0;
g_last_media_tb = 0;
g_glitch_started = 0;
g_playback_started = 0;
g_gap_samples = 0;
g_aec_reported_over = g_aec_reported_under = g_pcm_events = 0;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx", 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;
}
/* Платформа с собственным голосовым захватом явно передаёт PCM после AEC. */
int call_audio_start_prepared(uint64_t call_id) { return call_audio_start_impl(call_id, 1); }
int call_audio_start(uint64_t call_id) { return call_audio_start_impl(call_id, 0); }
void call_audio_begin_end(uint64_t call_id) {
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && !g_ending) {
@ -160,22 +250,28 @@ 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);
if (g_active && g_call_id == call_id) {
was_active = 1;
g_active = 0;
++g_tx_generation;
g_ending = 0;
g_ended_started = 0;
g_playback_started = 0;
g_call_id = 0;
g_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 +280,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 +295,72 @@ void call_audio_stop(void) {
if (cid) call_audio_release(cid);
}
void call_audio_reset_io(uint64_t call_id, int playback) {
pthread_mutex_lock(&g_mtx);
if (g_active && g_call_id == call_id) {
if (g_aec) speex_aec_reset(g_aec);
if (!playback && g_compressor) audio_compressor_reset(g_compressor);
if (playback) {
call_jitter_reset(g_vj);
g_gap_samples = 0;
g_glitch_started = 0;
g_playback_started = 0;
call_tones_glitch_stop(g_tones);
}
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: I/O reset call=%016llx direction=%s", CALL_AUDIO_ID,
(unsigned long long)call_id, playback ? "playback" : "capture");
}
pthread_mutex_unlock(&g_mtx);
}
/* ── AEC (SpeexDSP): рантайм-управление ── */
/* Создать AEC с текущей задержкой. Вызывается под g_mtx. */
static speex_aec_t* call_audio_aec_create(void) {
return speex_aec_create(CALL_AUDIO_SAMPLE_RATE, CALL_AUDIO_FRAME_SAMPLES,
CALL_AEC_FILTER_SAMPLES, g_aec_delay_frames);
}
void call_audio_set_aec_enabled(int enabled) {
pthread_mutex_lock(&g_mtx);
g_aec_enabled = enabled ? 1 : 0;
if (g_active && !g_prepared_capture) {
if (g_aec_enabled && !g_aec) {
g_aec = call_audio_aec_create();
if (!g_aec) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: AEC create failed (enabled)", CALL_AUDIO_ID);
else DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC enabled (delay=%d frames)", CALL_AUDIO_ID, g_aec_delay_frames);
} else if (!g_aec_enabled && g_aec) {
speex_aec_destroy(g_aec);
g_aec = NULL;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC disabled", CALL_AUDIO_ID);
}
}
pthread_mutex_unlock(&g_mtx);
}
void call_audio_set_aec_delay_frames(int frames) {
if (frames < 1) frames = 1;
pthread_mutex_lock(&g_mtx);
g_aec_delay_frames = frames;
if (g_active && !g_prepared_capture && g_aec_enabled && g_aec) {
speex_aec_destroy(g_aec);
g_aec = call_audio_aec_create();
if (!g_aec) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: AEC recreate failed (delay=%d)", CALL_AUDIO_ID, frames);
else DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC delay -> %d frames", CALL_AUDIO_ID, frames);
}
pthread_mutex_unlock(&g_mtx);
}
/* ── аудио-поток: TX / RX ── */
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) {
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES) return -1;
static int call_audio_feed_impl(uint64_t call_id, const int16_t* pcm, int count, int muted, const uint8_t* mute_mask, int prepared) {
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES || (prepared && !mute_mask)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: invalid capture frame count=%d prepared=%d", CALL_AUDIO_ID, count, prepared);
return -1;
}
uint8_t opus[256];
int16_t cleaned[CALL_AUDIO_FRAME_SAMPLES];
struct UASYNC* ua = NULL;
struct UTUN_INSTANCE* inst = NULL;
int len = -1;
@ -212,34 +370,79 @@ 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));
if (g_prepared_capture != prepared) {
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: capture policy mismatch prepared=%d", CALL_AUDIO_ID, prepared);
return -1;
}
const int16_t* tx = pcm;
if (g_aec) {
if (speex_aec_process_capture(g_aec, pcm, count, cleaned) == count)
tx = cleaned;
}
int16_t amplified[CALL_AUDIO_FRAME_SAMPLES];
if (g_compressor) {
if (audio_compressor_process_frame(g_compressor, tx, amplified, count) != 0) {
pthread_mutex_unlock(&g_mtx);
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: compressor frame failed", CALL_AUDIO_ID);
return -1;
}
tx = amplified;
}
if (mute_mask) {
if (tx != amplified) memcpy(amplified, tx, sizeof(amplified));
for (int i = 0; i < count; ++i) if (mute_mask[i]) amplified[i] = 0;
tx = amplified;
} else if (muted) { memset(amplified, 0, sizeof(amplified)); tx = amplified; }
len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus));
ua = g_inst ? g_inst->ua : NULL;
inst = g_inst;
pthread_mutex_unlock(&g_mtx);
if (len <= 0) return -1;
if (!ua || !inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: feed_pcm: instance not set (call_audio_init not called)", CALL_AUDIO_ID);
if (len <= 0 || !ua || !inst || g_ending || g_tx_pending >= 8) {
++g_tx_dropped;
if (g_tx_pending >= 8) g_tx_reasons |= 1;
if (!ua || !inst) g_tx_reasons |= 4;
if (len <= 0) { g_tx_reasons |= 8; g_tx_encode_error = len; }
if (g_ending) g_tx_reasons |= 32;
pthread_mutex_unlock(&g_mtx);
return -1;
}
struct call_media_arg* a = (struct call_media_arg*)u_malloc(sizeof(*a));
if (!a) return -1;
a->inst = inst;
a->call_id = call_id;
a->len = (uint16_t)len;
memcpy(a->opus, opus, (size_t)len);
uasync_post(ua, call_send_media_trampoline, a);
struct call_audio_tx* task = u_calloc(1, sizeof(*task));
if (!task) {
++g_tx_dropped;
g_tx_reasons |= 2;
pthread_mutex_unlock(&g_mtx);
return -1;
}
task->inst = inst; task->call_id = call_id;
task->generation = g_tx_generation; task->created_tb = get_time_tb();
task->len = len;
memcpy(task->opus, opus, (size_t)len);
task->task.callback = call_audio_send; task->task.arg = task;
++g_tx_pending;
uasync_post_reserved(ua, &task->task);
pthread_mutex_unlock(&g_mtx);
return 0;
}
/* Маска соответствует границам mute в том же упорядоченном потоке, что и PCM. */
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask) {
return call_audio_feed_impl(call_id, pcm, CALL_AUDIO_FRAME_SAMPLES, 0, mute_mask, 1);
}
int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted) {
return call_audio_feed_impl(call_id, pcm, count, muted, NULL, 0);
}
/* Незамьюченный захват для платформ без программного mute. */
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) {
return call_audio_feed_impl(call_id, pcm, count, 0, NULL, 0);
}
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
if (!out || max_samples <= 0) return 0;
int active = 0, ending = 0;
struct 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 +450,14 @@ 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;
uint32_t events = 0;
/* завершение: доигрываем тон (450мс), затем тишина. Тон стартует лениво
* здесь (аудио-поток) — begin_end лишь выставляет флаг, чтобы не гонять
* генератор тонов с GUI-потоком. */
@ -262,37 +467,88 @@ 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) {
if (!g_playback_started) events |= 1;
g_playback_started = 1;
g_gap_samples = 0;
}
int missing = max_samples - n;
/* Подготовка устройств и начальное накопление PCM не являются потерей звука. */
int quiet = g_playback_started ? CALL_STALL_SAMPLES - g_gap_samples : missing;
if (quiet > missing) quiet = missing;
int tone_samples = missing - quiet;
if (n > 0 && previous_gap > 0) events |= 2;
if (missing > 0 && g_playback_started && g_gap_samples == 0) events |= 4;
if (g_playback_started) g_gap_samples += quiet;
/* Новый PCM прерывает предыдущий цикл тона, даже если хвост блока снова пуст. */
if (g_glitch_started && (n > 0 || tone_samples == 0)) {
g_glitch_started = 0;
call_tones_glitch_stop(tones);
events |= 8;
}
memset(out + n, 0, (size_t)quiet * sizeof(*out));
if (tone_samples > 0) {
if (!g_glitch_started) {
g_glitch_started = 1;
events |= 16;
}
call_tones_glitch_start(tones);
call_tones_render(tones, out + n + quiet, tone_samples);
}
n = max_samples;
}
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);
g_pcm_events |= events;
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;
struct call_jitter_diagnostics diagnostics = {0};
uint32_t aec_over = 0, aec_under = 0, events = 0;
unsigned int tx_dropped = 0, tx_pending = 0;
unsigned int reasons = 0;
int encode_error = 0;
int tx_changed = 0, aec_changed = 0;
pthread_mutex_lock(&g_mtx);
if (g_active && call_id == g_call_id && g_vj) {
vj_get_stats(g_vj, buffer_ms, tempo_x100, dropped, underruns);
call_jitter_get_stats(g_vj, buffer_ms, tempo_x100, dropped, underruns, range);
call_jitter_take_diagnostics(g_vj, &diagnostics);
speex_aec_get_stats(g_aec, &aec_over, &aec_under);
aec_changed = aec_over != g_aec_reported_over || aec_under != g_aec_reported_under;
g_aec_reported_over = aec_over; g_aec_reported_under = aec_under;
tx_dropped = g_tx_dropped; tx_pending = g_tx_pending;
tx_changed = tx_dropped != g_tx_reported; g_tx_reported = tx_dropped;
reasons = g_tx_reasons; g_tx_reasons = 0; encode_error = g_tx_encode_error;
events = g_pcm_events; g_pcm_events = 0;
ok = 1;
}
pthread_mutex_unlock(&g_mtx);
if (tx_changed) DEBUG_WARN(DEBUG_CATEGORY_CALL,
"%s: TX dropped=%u pending=%u limit=8 max_age=200ms reasons=queue:%d alloc:%d core:%d encode:%d age:%d ending:%d encode_result=%d",
CALL_AUDIO_ID, tx_dropped, tx_pending, !!(reasons & 1), !!(reasons & 2), !!(reasons & 4),
!!(reasons & 8), !!(reasons & 16), !!(reasons & 32), encode_error);
if (aec_changed) DEBUG_WARN(DEBUG_CATEGORY_AEC, "%s: AEC render overflow=%u capture passthrough=%u", CALL_AUDIO_ID, aec_over, aec_under);
if (diagnostics.decode_errors || diagnostics.budget_hits)
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: decode_errors=%u last_result=%d len=%d budget_hits=%u depth=%dms",
diagnostics.decode_errors, diagnostics.error_result, diagnostics.error_len, diagnostics.budget_hits, diagnostics.depth_ms);
if (events) DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: PCM first=%d resumed=%d gap=%d tone_started=%d tone_stopped=%d", CALL_AUDIO_ID,
!!(events & 1), !!(events & 2), !!(events & 4), !!(events & 16), !!(events & 8));
if (ok) DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: depth=%dms reserve=%dms tempo=%d waiting=%d catching_up=%d resumed=%d",
diagnostics.depth_ms, diagnostics.reserve_ms, diagnostics.tempo_x100, diagnostics.waiting,
diagnostics.catching_up, diagnostics.resumed);
return ok ? 0 : -1;
}

36
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) получает
@ -12,13 +12,14 @@
//
// Потоки:
// - call_audio_start / begin_end / release — GUI-поток (по ACCEPTED/ENDED);
// - call_audio_feed_pcm / pull_pcm — аудио-поток (single-writer);
// - call_audio_feed_pcm / pull_pcm — по одному аудио-потоку на направление;
// - 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;
@ -34,8 +35,11 @@ extern "C" {
int call_audio_init(struct UTUN_INSTANCE* inst);
void call_audio_destroy(struct UTUN_INSTANCE* inst);
/* GUI-поток: создать codec + jitter + tones для звонка. 0 = успех. */
/* GUI-поток: создать codec + jitter + tones. Старый API принимает raw PCM и использует core AEC.
* Для desktop нужен start_prepared: AEC и stereo reference принадлежат VoiceAudioIo. 0 = успех. */
int call_audio_start(uint64_t call_id);
/* AEC принадлежит платформенному захвату; pull больше не подаёт собственный референс. */
int call_audio_start_prepared(uint64_t call_id);
/* GUI-поток: по CALL_ENDED — начать тон завершения (pull_pcm выдаст 450мс тона). */
void call_audio_begin_end(uint64_t call_id);
@ -48,19 +52,37 @@ void call_audio_stop(void);
/* аудио-поток: закодировать PCM-кадр (ровно CALL_AUDIO_FRAME_SAMPLES) и отправить пиру. */
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count);
/* Mute после AEC/AGC: фильтр получает настоящий микрофон, Opus — нулевой PCM. */
/* Подготовленный PCM и маска mute (960 элементов); AEC уже выполнен до этого API. */
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask);
int call_audio_feed_pcm_muted(uint64_t call_id, const int16_t* pcm, int count, int muted);
/* аудио-поток: выдать до max_samples финального PCM (jitter + stretch + тоны).
* Возвращает число сэмплов (может быть < max — андерфлоу/конец тона). */
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
/* uasync-поток (из call.c): принятый Opus-кадр — кладём в джиттер-буфер (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, для playback также устаревший jitter/PCM.
* Capture и playback могут работать в разных потоках, по одному writer на направление.
* Release допустим только после остановки обоих. */
void call_audio_reset_io(uint64_t call_id, int playback);
/* Core AEC для raw capture: Android — по спикерфону (route == SPEAKER).
* Prepared capture не создаёт core AEC; desktop передаёт настройку в VoiceAudioIo. */
void call_audio_set_aec_enabled(int enabled);
/* Задать задержку рендер→захват AEC в кадрах (20 мс). Android измеряет через
* AudioTrack/AudioRecord getTimestamp. Prepared desktop выравнивает референс по времени. Если core AEC уже
* активен — пересоздаётся с новой задержкой. */
void call_audio_set_aec_delay_frames(int frames);
#ifdef __cplusplus
}

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

217
src/call/call_jitter.cpp

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

39
src/call/call_jitter.h

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

34
src/call/call_jitter_window.h

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

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_core.h

@ -157,7 +157,7 @@ void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_i
struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; };
void chat_core_set_sound_on_message_trampoline(void* arg);
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[256]; };
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; };
void chat_core_save_ui_state_trampoline(void* arg);
/* Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) */

18
src/chat/chat_event.c

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

10
src/chat/chat_event.h

@ -59,16 +59,22 @@ 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_DM_MESSAGES 41 /* [conv_id_len:1][conv_id:var][JSON:utf8] — сообщения беседы и состояния доставки */
#define CHAT_EVT_CALL_PATH 42 /* [call_id:8][text:var] — BGP-маршрут до пира звонка */
#define CHAT_EVT_RADIO_TALK 43 /* [group_id:8][src_node_id:8][stream_id:2][on:1] — кто-то начал/закончил говорить */
#define CHAT_EVT_CALL_CONNECTION 44 /* [call_id:8][CONN_TYPE:1][next_hop_node_id:8] — текущий путь отправки звонка */
#define CHAT_EVT_DM_CONVERSATIONS 45 /* [JSON:utf8] — последние 200 личных бесед */
#define CHAT_EVT_ATTACHMENT_PREPARATION 46 /* [target_len:1][target:var][job_uuid:16][state:1], 0=start 1=ready 2=failed */
#define CHAT_EVT_DM_CALL_ROUTE 47 /* [conv_id_len:1][conv_id:var][peer_node_id:8][group_id:8], 0 = нет BGP-пути */
typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler);
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
void chat_event_attachment(struct UTUN_INSTANCE* inst, int is_dm, const char* target, const uint8_t id[16], int state);
#ifdef __cplusplus
}

15
src/chat/chat_msg.c

@ -251,7 +251,7 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
u_free(sig_msg); u_free(json); u_free(full_data); u_free(mctx);
}
static void on_video_prepared(void* arg, int err) {
static void on_video_prepared(void* arg, int err, const struct video_info* vi) {
struct media_submit_ctx* mctx = (struct media_submit_ctx*)arg;
struct chat_core_ctx* cc = CC(mctx->inst);
@ -263,13 +263,12 @@ static void on_video_prepared(void* arg, int err) {
}
/* после транскода знаем итоговую длительность и размеры — для тела сообщения */
struct video_info vi;
if (video_get_info(mctx->media_dest, &vi) == 0 && vi.is_video) {
mctx->duration_ms = vi.duration_ms;
mctx->width = vi.width;
mctx->height = vi.height;
if (vi && vi->is_video) {
mctx->duration_ms = vi->duration_ms;
mctx->width = vi->width;
mctx->height = vi->height;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: video prepared %dx%d dur=%lldms",
CC_ID, vi.width, vi.height, (long long)vi.duration_ms);
CC_ID, vi->width, vi->height, (long long)vi->duration_ms);
}
char media_base[1024];
@ -343,7 +342,7 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
if (req->media_video) {
if (video_transcode_start(inst->media_async, inst->ua, req->media_src, req->media_dest, on_video_prepared, mctx) < 0)
on_video_prepared(mctx, -1);
on_video_prepared(mctx, -1, NULL);
return;
}

9
src/chat/chat_setting.c

@ -42,8 +42,13 @@ 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},
{"dm_media_ttl_sec", CHAT_SETTING_INT, 604800, 1, 31536000}, /* TTL custody M: 7 суток */
{"dm_media_storage_mb", CHAT_SETTING_INT, 1024, 1, 1048576}, /* включая зарезервированные загрузки */
{"standby_active_sec", CHAT_SETTING_INT, 2, 1, 3600},
{"standby_sleep_sec", CHAT_SETTING_INT, 60, 1, 86400},
{"standby_min_sleep_sec", CHAT_SETTING_INT, 15, 1, 86400},
@ -52,6 +57,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]))

6
src/chat/chat_sync.c

@ -809,11 +809,7 @@ void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst) {
uint64_t gid = strtoull(cs->channels[i].channel_id, NULL, 10);
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
if (!group || !group->connect) continue;
if (topo_group_connect_active_count(group) == 0) {
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: socket change — restarting connect for ch=%s (no active connections)",
CS_ID, cs->channels[i].channel_id);
topo_group_connect_restart(group);
}
topo_group_connect_restart(group);
}
}

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)).

9
src/chat/member_sync.c

@ -6,6 +6,7 @@
#include "chat_core.h"
#include "chat_core_priv.h"
#include "../utun_instance.h"
#include "../transport_layer/etcp.h"
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
@ -707,7 +708,13 @@ static void member_committed(struct UTUN_INSTANCE* inst, const char* ns, uint64_
merkle_sync_changed(inst, ns);
struct TOPO_GROUP* g = inst->topo_groups ? topo_groups_find(inst->topo_groups, strtoull(ns, NULL, 10)) : NULL;
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT && conn) topo_group_connect_node_once(g, node_id);
if (g && g->group_type == TOPO_GROUP_TYPE_CHAT) {
/* Существующий UP сохраняем; новый транспорт выбирает только автопланировщик. */
if (conn && conn->links_up && !conn->close_requested && topo_group_new_conn(g, conn) < 0)
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cannot attach UP member channel=%s node=%016llx", MS_ID, ns,
(unsigned long long)node_id);
topo_group_connect_changed(g);
}
}
int member_sync_apply_record(struct UTUN_INSTANCE* inst, const char* ch_id,

1202
src/dm/dm_core.c

File diff suppressed because it is too large Load Diff

42
src/dm/dm_core.h

@ -5,7 +5,7 @@
* - conv_id и content_key детерминированно выводятся обеими сторонами
* из своих ключей и pubkey пира (см. dm_crypto.h) — без переговоров.
* - сообщения идут через etcp_router (прямое соединение или релей),
* при недоступности пира — через dm_mailbox (storage-узлы).
* при недоступности пира — через dm_mailbox (суперузлы).
* - содержимое шифруется E2E (AES-256-CCM), запись подписывается Ed25519.
*
* Два независимых направленных потока (я→пир и пир→я) с монотонным seq.
@ -17,6 +17,7 @@
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "dm_media.h"
struct UTUN_INSTANCE;
@ -25,32 +26,51 @@ extern "C" {
#endif
/* ── Каноническое тело DM-сообщения (общий wire-формат dm_core и dm_mailbox) ──
* [conv_id:8][seq:8][ts:8][author:8][ct_len:1][ct:ct_len][data_len:2][data_enc:data_len][sig:64]
* [conv_id:8][seq:8][ts:8][author:8][ct_len:1][ct:ct_len][data_len:2][data_enc:data_len][media?:56][sig:64]
* media присутствует только для ct="file", подписан вместе с сообщением.
* data_enc — AES-256-CCM (шифротекст + tag), sig — Ed25519 автора по всем полям до sig. */
#define DM_MSG_FIXED_HDR 33 /* conv_id+seq+ts+author(32) + ct_len(1) — до ct */
#define DM_MSG_SIG_SIZE 64
#define DM_DATA_MAX 1024
#define DM_RECEIPT_SIZE 136
/* Вычислить полную длину канонического тела из его заголовка.
* Возвращает 1 и *out_len, либо 0 если буфер мал/повреждён. */
static inline int dm_msg_len(const uint8_t* msg, size_t avail, size_t* out_len) {
if (!msg || avail < DM_MSG_FIXED_HDR) return 0;
if (!msg || !out_len || avail < DM_MSG_FIXED_HDR) return 0;
uint8_t ct_len = msg[32];
if (avail < (size_t)DM_MSG_FIXED_HDR + ct_len + 2) return 0;
uint16_t data_len;
memcpy(&data_len, msg + DM_MSG_FIXED_HDR + ct_len, 2);
size_t total = (size_t)DM_MSG_FIXED_HDR + ct_len + 2 + data_len + DM_MSG_SIG_SIZE;
if (data_len < 16 || data_len > DM_DATA_MAX + 16) return 0;
size_t media_len = ct_len == 4 && !memcmp(msg + DM_MSG_FIXED_HDR, "file", 4) ? DM_MEDIA_DESC_SIZE : 0;
size_t total = (size_t)DM_MSG_FIXED_HDR + ct_len + 2 + data_len + media_len + DM_MSG_SIG_SIZE;
if (avail < total) return 0;
*out_len = total;
return 1;
}
/* Общие операции протокола DM; все вызовы в uasync-потоке.
* group_id — только область маршрутизации, идентичность беседы от неё не зависит.
* route_group выбирает общую группу с живым путём, возвращает 1/0.
* verify_message проверяет каноническое тело и подпись автора для recipient.
* accept_message возвращает 0 только после commit (включая точный повтор).
* Квитанция: ["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(msg):32][sig:64].
* verify_receipt проверяет подпись получателя и при msg!=NULL точное соответствие телу.
* accept_receipt удаляет только соответствующее исходящее pending-сообщение. */
int dm_route_group(struct UTUN_INSTANCE* inst, uint64_t peer, uint64_t preferred, uint64_t* group_id);
int dm_verify_message(struct UTUN_INSTANCE* inst, uint64_t recipient, const uint8_t* msg, size_t len);
int dm_accept_message(struct UTUN_INSTANCE* inst, const uint8_t* msg, size_t len, uint8_t receipt[DM_RECEIPT_SIZE]);
int dm_verify_receipt(struct UTUN_INSTANCE* inst, const uint8_t receipt[DM_RECEIPT_SIZE], const uint8_t* msg, size_t len);
int dm_accept_receipt(struct UTUN_INSTANCE* inst, const uint8_t receipt[DM_RECEIPT_SIZE]);
/* Жизненный цикл: инициализация после chat_core_init, destroy перед chat_core_destroy. */
int dm_core_init(struct UTUN_INSTANCE* inst);
void dm_core_destroy(struct UTUN_INSTANCE* inst);
/* Начать беседу с пользователем (peer). source_ch_id — канал, где нашли target
* (общая группа; нужен для маршрутизации и подключения). Создаёт беседу если её
* ещё нет, выводит conv_id/ключи, пытается установить прямое соединение.
* ещё нет, выводит conv_id/ключи. Подключение для передачи медиа принадлежит media.
* Возвращает 0 при успехе, <0 при ошибке. */
int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
const uint8_t peer_x25519[32], const uint8_t peer_ed25519[32],
@ -58,7 +78,8 @@ int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
/* Отправить сообщение в беседу. Шифрует, подписывает, сохраняет локально и
* доставляет (прямое соединение при доступности пира, иначе mailbox).
* conv_id — строковое представление (как в dm_conversations.conv_id). */
* conv_id — строковое представление (как в dm_conversations.conv_id).
* Для ct="file" data=[descriptor:56][basename:utf8], только после подготовки dm_media. */
int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type,
const uint8_t* data, uint32_t data_len);
@ -92,13 +113,18 @@ struct dm_send_req {
};
void dm_send_trampoline(void* arg);
/* Запросить расшифрованные сообщения беседы. Результат — CHAT_EVT_DM_MESSAGES:
* [conv_id_len:1][conv_id:var][count:2]([dir:1][seq:8][ts:8][author:8][ct_len:1][ct:var][data_len:2][data:var])* */
/* Запросить последние 200 сообщений со статусами. CHAT_EVT_DM_MESSAGES:
* [conv_id_len:1][conv_id:var][JSON:utf8]. Все чтения в uasync. */
struct dm_messages_req {
struct UTUN_INSTANCE* inst;
char conv_id[64];
};
void dm_messages_trampoline(void* arg);
/* Живой BGP-маршрут PM; group_id=0 в снимке означает недоступность. Все вызовы в uasync. */
int dm_call_route(struct UTUN_INSTANCE* inst, const char* conv_id, uint64_t* peer, uint64_t* group_id);
void dm_call_route_trampoline(void* arg); /* аргумент: dm_messages_req */
struct dm_conversations_req { struct UTUN_INSTANCE* inst; };
void dm_conversations_trampoline(void* arg);
#ifdef __cplusplus
}

116
src/dm/dm_crypto.c

@ -14,6 +14,19 @@
#define DM_ID "dm_crypto"
/* Медиа использует тот же ключ target, но отдельное пространство nonce. */
void dm_build_media_nonce(uint64_t author, const uint8_t media_id[16], uint64_t part, uint8_t nonce[DM_NONCE_SIZE]) {
SHA256_CTX ctx;
uint8_t hash[32];
SHA256_Init(&ctx);
SHA256_Update(&ctx, "utun_dm_media", 13);
SHA256_Update(&ctx, &author, sizeof(author));
SHA256_Update(&ctx, media_id, 16);
SHA256_Update(&ctx, &part, sizeof(part));
SHA256_Final(hash, &ctx);
memcpy(nonce, hash, DM_NONCE_SIZE);
}
uint64_t dm_derive_conv_id(uint64_t a, uint64_t b) {
uint64_t lo = a < b ? a : b;
uint64_t hi = a < b ? b : a;
@ -80,10 +93,17 @@ void dm_build_nonce(uint64_t conv_id, uint64_t author, uint64_t seq, uint8_t non
static int dm_ccm_crypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE],
const uint8_t* in, size_t in_len, uint8_t* out, size_t* out_len,
int encrypt, const uint8_t* tag_in) {
if (!key || !nonce || !in || !out || !out_len) return -1;
if (!key || !nonce || (!in && in_len) || !out || !out_len ||
(encrypt ? in_len > DM_CCM_PLAIN_MAX : in_len < DM_TAG_SIZE || in_len - DM_TAG_SIZE > DM_CCM_PLAIN_MAX)) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: invalid CCM arguments encrypt=%d bytes=%zu", DM_ID, encrypt, in_len);
return -1;
}
*out_len = 0;
static const uint8_t empty = 0;
if (!in) in = &empty;
EVP_CIPHER_CTX* ectx = EVP_CIPHER_CTX_new();
if (!ectx) return -1;
if (!ectx) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: CCM context allocation failed", DM_ID); return -1; }
const EVP_CIPHER* cipher = EVP_aes_256_ccm();
int rc = -1;
@ -93,6 +113,7 @@ static int dm_ccm_crypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t no
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_TAG, DM_TAG_SIZE, NULL) != 1
|| EVP_EncryptInit_ex(ectx, NULL, NULL, key, nonce) != 1) goto done;
int outl;
if (EVP_EncryptUpdate(ectx, NULL, &outl, NULL, (int)in_len) != 1) goto done;
if (EVP_EncryptUpdate(ectx, out, &outl, in, (int)in_len) != 1 || outl != (int)in_len) goto done;
int tmpl;
if (EVP_EncryptFinal_ex(ectx, out + outl, &tmpl) != 1) goto done;
@ -106,14 +127,14 @@ static int dm_ccm_crypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t no
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_TAG, DM_TAG_SIZE, (void*)(tag_in ? tag_in : in + body)) != 1
|| EVP_DecryptInit_ex(ectx, NULL, NULL, key, nonce) != 1) goto done;
int outl;
if (EVP_DecryptUpdate(ectx, NULL, &outl, NULL, (int)body) != 1) goto done;
if (EVP_DecryptUpdate(ectx, out, &outl, in, (int)body) != 1 || outl != (int)body) goto done;
int tmpl;
if (EVP_DecryptFinal_ex(ectx, out + outl, &tmpl) != 1) goto done;
*out_len = body;
}
rc = 0;
done:
EVP_CIPHER_CTX_free(ectx);
if (rc) DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: CCM %s failed bytes=%zu", DM_ID, encrypt ? "encrypt" : "decrypt", in_len);
return rc;
}
@ -126,3 +147,90 @@ int dm_decrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NO
const uint8_t* ct, size_t ct_len, uint8_t* out, size_t* out_len) {
return dm_ccm_crypt(key, nonce, ct, ct_len, out, out_len, 0, NULL);
}
/* Каждая порция имеет собственный nonce, ciphertext неизменен для всех сетевых повторов. */
int dm_media_encrypt_stream(const uint8_t key[DM_CONTENT_KEY_SIZE], uint64_t author, const uint8_t media_id[16],
FILE* src, FILE* dst, uint64_t* plain_size, uint8_t ciphertext_hash[32]) {
if (!key || !author || !media_id || !src || !dst || src == dst || !plain_size || !ciphertext_hash) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: invalid media encryption arguments", DM_ID);
return -1;
}
*plain_size = 0;
memset(ciphertext_hash, 0, 32);
uint8_t plain[DM_MEDIA_PLAIN_SIZE], cipher[DM_MEDIA_PLAIN_SIZE + DM_TAG_SIZE], nonce[DM_NONCE_SIZE];
uint64_t part = 0, total = 0;
SHA256_CTX hash;
SHA256_Init(&hash);
for (;;) {
size_t n = fread(plain, 1, sizeof(plain), src);
if (ferror(src)) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: media source read failed part=%llu", DM_ID, (unsigned long long)part);
return -1;
}
if (!n && part) break;
size_t clen = 0;
dm_build_media_nonce(author, media_id, part, nonce);
if (dm_encrypt(key, nonce, plain, n, cipher, &clen) != 0) return -1;
if (fwrite(cipher, 1, clen, dst) != clen) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: media ciphertext write failed part=%llu", DM_ID, (unsigned long long)part);
return -1;
}
SHA256_Update(&hash, cipher, clen);
total += n;
part++;
if (n < sizeof(plain)) break;
}
if (fflush(dst) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: media ciphertext flush failed", DM_ID);
return -1;
}
SHA256_Final(ciphertext_hash, &hash);
*plain_size = total;
DEBUG_DEBUG(DEBUG_CATEGORY_DM, "%s: media encrypted author=%llu plain=%llu records=%llu", DM_ID,
(unsigned long long)author, (unsigned long long)total, (unsigned long long)part);
return 0;
}
/* Размер и хеш берутся из подписанного manifest; никакого size-only принятия файла. */
int dm_media_decrypt_stream(const uint8_t key[DM_CONTENT_KEY_SIZE], uint64_t author, const uint8_t media_id[16],
uint64_t plain_size, const uint8_t ciphertext_hash[32], FILE* src, FILE* dst) {
if (!key || !author || !media_id || !ciphertext_hash || !src || !dst || src == dst) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: invalid media decryption arguments", DM_ID);
return -1;
}
uint8_t plain[DM_MEDIA_PLAIN_SIZE], cipher[DM_MEDIA_PLAIN_SIZE + DM_TAG_SIZE], nonce[DM_NONCE_SIZE];
uint64_t remaining = plain_size, part = 0;
SHA256_CTX hash;
SHA256_Init(&hash);
do {
size_t n = remaining > DM_MEDIA_PLAIN_SIZE ? DM_MEDIA_PLAIN_SIZE : (size_t)remaining;
size_t clen = n + DM_TAG_SIZE, plen = 0;
if (fread(cipher, 1, clen, src) != clen) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: media ciphertext short/read failure part=%llu expected=%zu", DM_ID,
(unsigned long long)part, clen);
return -1;
}
SHA256_Update(&hash, cipher, clen);
dm_build_media_nonce(author, media_id, part, nonce);
if (dm_decrypt(key, nonce, cipher, clen, plain, &plen) != 0) return -1;
if (plen != n || fwrite(plain, 1, plen, dst) != plen) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: media plaintext write/length failure part=%llu", DM_ID, (unsigned long long)part);
return -1;
}
remaining -= n;
part++;
} while (remaining);
uint8_t actual_hash[32];
SHA256_Final(actual_hash, &hash);
if (fgetc(src) != EOF || ferror(src) || memcmp(actual_hash, ciphertext_hash, 32)) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: media ciphertext size/hash mismatch plain=%llu", DM_ID, (unsigned long long)plain_size);
return -1;
}
if (fflush(dst) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: media plaintext flush failed", DM_ID);
return -1;
}
DEBUG_DEBUG(DEBUG_CATEGORY_DM, "%s: media verified author=%llu plain=%llu records=%llu", DM_ID,
(unsigned long long)author, (unsigned long long)plain_size, (unsigned long long)part);
return 0;
}

22
src/dm/dm_crypto.h

@ -12,6 +12,7 @@
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
@ -20,6 +21,8 @@ extern "C" {
#define DM_CONTENT_KEY_SIZE 32
#define DM_NONCE_SIZE 13 /* AES-CCM требует ровно 13 байт */
#define DM_TAG_SIZE 16
#define DM_CCM_PLAIN_MAX 65535u
#define DM_MEDIA_PLAIN_SIZE 32768u
/* conv_id = SHA256("utun_dm_v1" || min(a,b) || max(a,b))[..8] & 0x7FFF...,
* 63-битное число, одинаковое на обеих сторонах (одна ветка на пару). */
@ -35,6 +38,9 @@ int dm_derive_content_key(const uint8_t my_priv[32], const uint8_t peer_pub[32],
* не пересекается между направлениями (author различается). */
void dm_build_nonce(uint64_t conv_id, uint64_t author, uint64_t seq, uint8_t nonce[DM_NONCE_SIZE]);
/* Nonce порции медиа; media_id неизменен на всех повторах одной отправки. */
void dm_build_media_nonce(uint64_t author, const uint8_t media_id[16], uint64_t part, uint8_t nonce[DM_NONCE_SIZE]);
/* AES-256-CCM. ciphertext = ciphertext(plain_len) || tag(DM_TAG_SIZE).
* out должен вмещать plain_len + DM_TAG_SIZE. 0=ок, <0=ошибка. */
int dm_encrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE],
@ -44,6 +50,22 @@ int dm_encrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NO
int dm_decrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE],
const uint8_t* ct, size_t ct_len, uint8_t* out, size_t* out_len);
/* Подготовка неизменяемого ciphertext медиа в worker, без БД и сетевых вызовов.
* Используется content_key target, отдельного ключа файла нет.
* Формат: порции по DM_MEDIA_PLAIN_SIZE байт + tag каждой порции; последняя короче.
* Пустой файл содержит один tag. Размер plaintext и SHA256 ciphertext входят в подписанные метаданные.
* src/dst открывает и закрывает владелец; dst — временный файл, при ошибке его нужно отбросить.
* Новый случайный media_id нужен для каждой подготовки, в том числе повторной после ошибки;
* при повторе доставки передаётся уже подготовленный файл, шифрование не повторяется.
* Возвращает 0 после fflush(dst), <0 при ошибке; длительную работу запускать через media_async. */
int dm_media_encrypt_stream(const uint8_t key[DM_CONTENT_KEY_SIZE], uint64_t author, const uint8_t media_id[16],
FILE* src, FILE* dst, uint64_t* plain_size, uint8_t ciphertext_hash[32]);
/* Расшифровать в временный dst, проверив все tags, точный размер и хеш ciphertext.
* Публиковать файл разрешено только после успеха и durable сохранения владельцем. */
int dm_media_decrypt_stream(const uint8_t key[DM_CONTENT_KEY_SIZE], uint64_t author, const uint8_t media_id[16],
uint64_t plain_size, const uint8_t ciphertext_hash[32], FILE* src, FILE* dst);
#ifdef __cplusplus
}
#endif

662
src/dm/dm_mailbox.c

@ -1,353 +1,453 @@
/*
* dm_mailbox.c — offline-хранение DM-сообщений на storage-узлах
*
* Storage-узел хранит зашифрованные блобы в таблице dm_mail и отдаёт их
* получателю по PULL, удаляя после ACK. Отправитель кладёт блобы по PUT.
* Содержимое для storage-узла непрозрачно (E2E-шифрование + подпись автора).
* dm_mailbox.c — durable очередь суперузла. Сообщения и квитанции непрозрачны;
* приём/удаление атомарны, роли и область маршрута проверяются на каждом PUT.
*/
#include "dm_mailbox.h"
#include "dm_core.h"
#include "../chat/chat_core.h"
#include "dm_mailbox_media.h"
#include "dm_media_priv.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../routing_layer/etcp_router.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../transport_layer/etcp_api.h"
#include "../transport_layer/etcp.h"
#include "../../lib/mem.h"
#include "../../lib/ll_queue.h"
#include "../../lib/debug_config.h"
#include "../../lib/json_flat.h"
#include "../../lib/platform_compat.h"
#include <string.h>
#include <stdlib.h>
#include <sqlite3.h>
#include <string.h>
#include <stdio.h>
#define MB_ID "dm_mailbox"
#define MB_PUT 1 /* [recipient:8][каноническое тело] */
#define MB_DELIVER 2 /* [каноническое тело] */
#define MB_RECEIPT 3 /* [подписанная квитанция получателя] */
#define MB_STORED 4 /* [conv:8][seq:8] — durable PUT подтверждён суперузлом */
/* Подкоманды протокола mailbox (первый байт payload после ROUTER_SVC-заголовка). */
#define MB_SUBCMD_PUT 0x01 /* отправитель → storage: [recipient:8][sender:8][msg] */
#define MB_SUBCMD_PULL 0x02 /* получатель → storage: [recipient:8][sender:8][since_seq:8] */
#define MB_SUBCMD_PULL_RESP 0x03 /* storage → получатель: [recipient:8][count:2][msg]* */
#define MB_SUBCMD_ACK 0x04 /* получатель → storage: [recipient:8][sender:8][up_to_seq:8] */
#define MB_TTL_DAYS 7
#define MB_MAX_BATCH 32
/* Per-instance состояние модуля. */
struct dm_mb_state {
struct UTUN_INSTANCE* inst;
sqlite3* db;
int initialized;
dm_mail_deliver_fn deliver_fn;
void* deliver_arg;
sqlite3* db;
void* timer;
struct dm_mb_media* media;
};
static struct dm_mb_state* mb_of(struct UTUN_INSTANCE* inst) {
return inst ? inst->dm_mailbox : NULL;
struct dm_mb_media* dm_mailbox_get_media(struct UTUN_INSTANCE* inst) {
return inst && inst->dm_mailbox ? inst->dm_mailbox->media : NULL;
}
void dm_mailbox_set_deliver_cb(struct UTUN_INSTANCE* inst, dm_mail_deliver_fn fn, void* arg) {
struct dm_mb_state* mb = mb_of(inst);
if (!mb) return;
mb->deliver_fn = fn;
mb->deliver_arg = arg;
/* Проверить роль в конкретной общей группе; storage не заменяет supernode. */
static int mb_scope(struct dm_mb_state* mb, uint64_t gid, uint64_t super, uint64_t a, uint64_t b) {
if (!gid) return 0;
char ch[64];
snprintf(ch, sizeof(ch), "%llu", (unsigned long long)gid);
char sql[256];
snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"peers_%s\" WHERE node_id=? AND deleted=0", ch);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(mb->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: role query failed group=%llu: %s", MB_ID,
(unsigned long long)gid, sqlite3_errmsg(mb->db));
return 0;
}
sqlite3_bind_int64(st, 1, (sqlite3_int64)super);
int allowed = 0;
char role[16];
if (sqlite3_step(st) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(st, 0);
allowed = tags && json_flat_get(tags, "supernode", role, sizeof(role)) == 0 && !strcmp(role, "yes");
}
sqlite3_finalize(st);
return allowed && topo_node_sqlite_member_in_channel(mb->db, ch, a) &&
topo_node_sqlite_member_in_channel(mb->db, ch, b);
}
/* Выполнить SQL без результата. Возвращает 0/ошибку. */
static int mb_exec(struct dm_mb_state* mb, const char* sql) {
char* err = NULL;
if (sqlite3_exec(mb->db, sql, NULL, NULL, &err) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: sql failed: %s (%s)", MB_ID, sql, err ? err : "?");
if (err) sqlite3_free(err);
/* Подписать контроль и передать владение пакетом router, учитывая backpressure. */
static int mb_send(struct dm_mb_state* mb, uint64_t gid, uint64_t dst, uint8_t cmd, const uint8_t* body, size_t len) {
if (!etcp_router_conn_get(mb->inst, gid, dst, ETCP_RT_ID_DM_MAILBOX)) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: router channel allocation failed peer=%llu", MB_ID, (unsigned long long)dst);
return -1;
}
return 0;
if (!etcp_router_send_q_has_room(mb->inst, gid, dst, ETCP_RT_ID_DM_MAILBOX)) {
DEBUG_DEBUG(DEBUG_CATEGORY_DM, "%s: backpressure peer=%llu", MB_ID, (unsigned long long)dst);
return -1;
}
struct ll_entry* e = queue_entry_new(0);
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: packet allocation failed", MB_ID); return -1; }
e->dgram = u_malloc(len + 2);
if (!e->dgram) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: payload allocation failed", MB_ID);
queue_entry_free(e);
return -1;
}
e->dgram[0] = ETCP_RT_ID_DM_MAILBOX;
e->dgram[1] = cmd;
memcpy(e->dgram + 2, body, len);
e->len = (uint16_t)(len + 2);
int rc = etcp_route_send(mb->inst, gid, dst, e, 1, ROUTER_FLAG_SIGNED);
if (rc) DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: route failed peer=%llu group=%llu cmd=%u rc=%d", MB_ID,
(unsigned long long)dst, (unsigned long long)gid, cmd, rc);
return rc;
}
/* Прочитать seq из канонического тела сообщения (смещение 8). */
static uint64_t mb_msg_seq(const uint8_t* msg) {
uint64_t seq; memcpy(&seq, msg + 8, 8); return seq;
/* Проверять каждый SQL результат, в том числе границы транзакций. */
static int mb_exec(struct dm_mb_state* mb, const char* sql) {
int rc = sqlite3_exec(mb->db, sql, NULL, NULL, NULL);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: SQL rc=%d error=%s statement=%s",
MB_ID, rc, sqlite3_errmsg(mb->db), sql);
return rc == SQLITE_OK ? 0 : -1;
}
/* Создать таблицу dm_mail (на любом узле; фактически используется storage-узлами). */
static void mb_create_tables(struct dm_mb_state* mb) {
mb_exec(mb, "CREATE TABLE IF NOT EXISTS dm_mail ("
" recipient INTEGER NOT NULL,"
" sender INTEGER NOT NULL,"
" seq INTEGER NOT NULL,"
" msg BLOB NOT NULL,"
" ttl INTEGER NOT NULL,"
" PRIMARY KEY (recipient, sender, seq))");
/* Принять неизменяемое тело; подтверждать только durable запись или точный повтор. */
static void mb_put_received(struct dm_mb_state* mb, uint64_t gid, uint64_t source, const uint8_t* p, size_t len) {
if (len < 8) { DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: short PUT bytes=%zu", MB_ID, len); return; }
uint64_t recipient, author, seq;
memcpy(&recipient, p, 8);
p += 8;
len -= 8;
if (dm_verify_message(mb->inst, recipient, p, len) != 0) return;
memcpy(&seq, p + 8, 8);
memcpy(&author, p + 24, 8);
if (source != author || !mb_scope(mb, gid, mb->inst->node_id, author, recipient)) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: unauthorized PUT source=%llu recipient=%llu group=%llu", MB_ID,
(unsigned long long)source, (unsigned long long)recipient, (unsigned long long)gid);
return;
}
sqlite3_stmt* st = NULL;
if (mb_exec(mb, "BEGIN IMMEDIATE") != 0) return;
if (dm_mailbox_media_register(mb->media, gid, recipient, p, len) != 0) {
mb_exec(mb, "ROLLBACK");
return;
}
int rc = sqlite3_prepare_v2(mb->db,
"SELECT receipt FROM dm_mail_receipts WHERE recipient=? AND sender=? AND seq=?", -1, &st, NULL);
if (rc != SQLITE_OK) goto fail;
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)author);
sqlite3_bind_int64(st, 3, (sqlite3_int64)seq);
rc = sqlite3_step(st);
if (rc == SQLITE_ROW) {
uint8_t receipt[DM_RECEIPT_SIZE];
if (sqlite3_column_bytes(st, 0) != DM_RECEIPT_SIZE) goto fail;
memcpy(receipt, sqlite3_column_blob(st, 0), sizeof(receipt));
sqlite3_finalize(st);
st = NULL;
if (mb_exec(mb, "COMMIT") != 0) goto fail;
if (dm_verify_receipt(mb->inst, receipt, p, len) == 0)
mb_send(mb, gid, author, MB_RECEIPT, receipt, sizeof(receipt));
return; /* Поздний PUT не возрождает доставленную задачу. */
}
if (rc != SQLITE_DONE) goto fail;
sqlite3_finalize(st);
st = NULL;
rc = sqlite3_prepare_v2(mb->db,
"INSERT INTO dm_pending_messages(recipient,sender,seq,msg,group_id,retry_at) VALUES(?,?,?,?,?,0)"
" ON CONFLICT(recipient,sender,seq) DO NOTHING", -1, &st, NULL);
if (rc != SQLITE_OK) goto fail;
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)author);
sqlite3_bind_int64(st, 3, (sqlite3_int64)seq);
sqlite3_bind_blob(st, 4, p, (int)len, SQLITE_STATIC);
sqlite3_bind_int64(st, 5, (sqlite3_int64)gid);
rc = sqlite3_step(st);
if (rc != SQLITE_DONE) goto fail;
sqlite3_finalize(st);
st = NULL;
rc = sqlite3_prepare_v2(mb->db,
"SELECT msg FROM dm_pending_messages WHERE recipient=? AND sender=? AND seq=?", -1, &st, NULL);
if (rc != SQLITE_OK) goto fail;
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)author);
sqlite3_bind_int64(st, 3, (sqlite3_int64)seq);
rc = sqlite3_step(st);
if (rc != SQLITE_ROW || sqlite3_column_bytes(st, 0) != (int)len ||
memcmp(sqlite3_column_blob(st, 0), p, len)) goto fail;
sqlite3_finalize(st);
st = NULL;
if (mb_exec(mb, "COMMIT") != 0) goto fail;
mb_send(mb, gid, author, MB_STORED, p, 16);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: stored sender=%llu recipient=%llu seq=%llu group=%llu", MB_ID,
(unsigned long long)author, (unsigned long long)recipient, (unsigned long long)seq, (unsigned long long)gid);
return;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: PUT database/conflict rc=%d error=%s", MB_ID, rc, sqlite3_errmsg(mb->db));
sqlite3_finalize(st);
mb_exec(mb, "ROLLBACK");
}
/* Отправить payload подкомандой subcmd в dst через etcp_router (mode=0, своё E2E). */
static int mb_route_send(struct dm_mb_state* mb, uint64_t group_id, uint64_t dst, uint8_t subcmd,
const uint8_t* body, size_t body_len) {
struct ll_entry* entry = queue_entry_new(0);
if (!entry) return -1;
entry->dgram = u_malloc(1 + 1 + body_len);
if (!entry->dgram) { queue_entry_free(entry); return -1; }
entry->dgram[0] = ETCP_RT_ID_DM_MAILBOX;
memcpy(entry->dgram + 1, &subcmd, 1);
if (body_len) memcpy(entry->dgram + 1 + 1, body, body_len);
entry->len = (uint16_t)(1 + 1 + body_len);
int rc = etcp_route_send(mb->inst, group_id, dst, entry, 1, 0);
if (rc != 0) { queue_dgram_free(entry); queue_entry_free(entry); return -1; }
return 0;
/* Сначала durable квитанция, затем удаление точной копии в той же транзакции. */
static void mb_receipt_received(struct dm_mb_state* mb, uint64_t gid, uint64_t source, const uint8_t* r) {
if (dm_verify_receipt(mb->inst, r, NULL, 0) != 0) return;
uint64_t author, recipient, seq;
memcpy(&seq, r + 16, 8);
memcpy(&author, r + 24, 8);
memcpy(&recipient, r + 32, 8);
if (author == mb->inst->node_id) {
dm_accept_receipt(mb->inst, r);
return;
}
if ((source != author && source != recipient) || !mb_scope(mb, gid, mb->inst->node_id, author, recipient)) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: receipt outside authorized scope source=%llu group=%llu", MB_ID,
(unsigned long long)source, (unsigned long long)gid);
return;
}
if (mb_exec(mb, "BEGIN IMMEDIATE") != 0) return;
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(mb->db,
"SELECT msg FROM dm_pending_messages WHERE recipient=? AND sender=? AND seq=?", -1, &st, NULL);
if (rc != SQLITE_OK) goto fail;
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)author);
sqlite3_bind_int64(st, 3, (sqlite3_int64)seq);
rc = sqlite3_step(st);
if (rc == SQLITE_ROW) {
if (dm_verify_receipt(mb->inst, r, sqlite3_column_blob(st, 0), (size_t)sqlite3_column_bytes(st, 0)) != 0) goto fail;
} else if (rc != SQLITE_DONE) goto fail;
sqlite3_finalize(st);
st = NULL;
rc = sqlite3_prepare_v2(mb->db,
"INSERT INTO dm_mail_receipts(recipient,sender,seq,receipt,group_id) VALUES(?,?,?,?,?)"
" ON CONFLICT(recipient,sender,seq) DO NOTHING", -1, &st, NULL);
if (rc != SQLITE_OK) goto fail;
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)author);
sqlite3_bind_int64(st, 3, (sqlite3_int64)seq);
sqlite3_bind_blob(st, 4, r, DM_RECEIPT_SIZE, SQLITE_STATIC);
sqlite3_bind_int64(st, 5, (sqlite3_int64)gid);
rc = sqlite3_step(st);
if (rc != SQLITE_DONE) goto fail;
sqlite3_finalize(st);
st = NULL;
rc = sqlite3_prepare_v2(mb->db, "DELETE FROM dm_pending_messages WHERE recipient=? AND sender=? AND seq=?", -1, &st, NULL);
if (rc != SQLITE_OK) goto fail;
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)author);
sqlite3_bind_int64(st, 3, (sqlite3_int64)seq);
rc = sqlite3_step(st);
if (rc != SQLITE_DONE) goto fail;
int deleted = sqlite3_changes(mb->db);
sqlite3_finalize(st);
st = NULL;
if (mb_exec(mb, "COMMIT") != 0) goto fail;
if (deleted) DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: delivered copy removed sender=%llu recipient=%llu seq=%llu", MB_ID,
(unsigned long long)author, (unsigned long long)recipient, (unsigned long long)seq);
mb_send(mb, gid, author, MB_RECEIPT, r, DM_RECEIPT_SIZE);
return;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: receipt commit failed rc=%d error=%s", MB_ID, rc, sqlite3_errmsg(mb->db));
sqlite3_finalize(st);
mb_exec(mb, "ROLLBACK");
}
/* ── обработка входящих mailbox-пакетов (etcp_router 0x35) ── */
static void mb_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry || !conn || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 1) {
/* Никаких диапазонных ACK: одна квитанция подтверждает одно тело. */
static void mb_recv(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!conn || !entry || !entry->dgram || entry->len <= ROUTER_SVC_PAYLOAD_OFF) {
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
return;
}
struct UTUN_INSTANCE* inst = conn->instance;
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !mb || !mb->db) { queue_dgram_free(entry); queue_entry_free(entry); return; }
const uint8_t* d = entry->dgram;
uint64_t group_id; memcpy(&group_id, d + ROUTER_SVC_GROUP_OFF, 8);
const uint8_t* p = d + ROUTER_SVC_PAYLOAD_OFF;
size_t plen = entry->len - ROUTER_SVC_PAYLOAD_OFF;
uint8_t subcmd = p[0];
if (subcmd == MB_SUBCMD_PUT) {
/* [subcmd][recipient:8][sender:8][msg] — storage: сохранить блоб */
if (plen < 1 + 8 + 8 + 16) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t recipient; memcpy(&recipient, p + 1, 8);
uint64_t sender; memcpy(&sender, p + 9, 8);
const uint8_t* msg = p + 17;
size_t mlen = plen - 17;
uint64_t seq = mb_msg_seq(msg);
int64_t ttl = (int64_t)ntp_time_get_seconds(inst) + MB_TTL_DAYS * 86400;
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(mb->db,
"INSERT OR IGNORE INTO dm_mail(recipient,sender,seq,msg,ttl) VALUES(?,?,?,?,?)",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)sender);
sqlite3_bind_int64(st, 3, (sqlite3_int64)seq);
sqlite3_bind_blob(st, 4, msg, (int)mlen, SQLITE_STATIC);
sqlite3_bind_int64(st, 5, ttl);
int rc = sqlite3_step(st);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: PUT recipient=0x%016llx sender=0x%016llx seq=%llu rc=%d",
MB_ID, (unsigned long long)recipient, (unsigned long long)sender,
(unsigned long long)seq, rc);
struct dm_mb_state* mb = conn->instance->dm_mailbox;
if (!mb) { DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: receive after stop", MB_ID); goto done; }
uint64_t gid, source;
memcpy(&gid, entry->dgram + ROUTER_SVC_GROUP_OFF, 8);
memcpy(&source, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
const uint8_t* p = entry->dgram + ROUTER_SVC_PAYLOAD_OFF;
size_t len = entry->len - ROUTER_SVC_PAYLOAD_OFF - 1;
if (!(entry->dgram[ROUTER_SVC_FLAGS_OFF] & ROUTER_FLAG_SIGNED)) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: unsigned control source=%llu", MB_ID, (unsigned long long)source);
goto done;
}
if (p[0] == MB_PUT) mb_put_received(mb, gid, source, p + 1, len);
else if (p[0] == MB_DELIVER) {
if (len < DM_MSG_FIXED_HDR) { DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: short delivery", MB_ID); goto done; }
uint64_t author;
memcpy(&author, p + 25, 8);
if (!mb_scope(mb, gid, source, author, mb->inst->node_id)) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: delivery from non-supernode source=%llu", MB_ID, (unsigned long long)source);
goto done;
}
if (st) sqlite3_finalize(st);
} else if (subcmd == MB_SUBCMD_PULL) {
/* [subcmd][recipient:8][sender:8][since_seq:8] — storage: вернуть очередь */
if (plen < 1 + 8 + 8 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t recipient; memcpy(&recipient, p + 1, 8);
uint64_t sender; memcpy(&sender, p + 9, 8);
uint64_t since_seq; memcpy(&since_seq, p + 17, 8);
/* прочитать блобы (до MB_MAX_BATCH) в буфер: [recipient:8][sender:8][count:2][msg]* */
size_t buf_cap = 18 + (size_t)MB_MAX_BATCH * 4096;
uint8_t* body = u_malloc(buf_cap);
if (!body) { queue_dgram_free(entry); queue_entry_free(entry); return; }
memcpy(body, &recipient, 8);
memcpy(body + 8, &sender, 8);
uint16_t count = 0;
size_t boff = 18;
uint8_t receipt[DM_RECEIPT_SIZE];
if (dm_accept_message(mb->inst, p + 1, len, receipt) == 0)
mb_send(mb, gid, source, MB_RECEIPT, receipt, sizeof(receipt));
} else if (p[0] == MB_RECEIPT && len == DM_RECEIPT_SIZE) mb_receipt_received(mb, gid, source, p + 1);
else if (p[0] == MB_STORED && len == 16) {
uint64_t conv, seq;
memcpy(&conv, p + 1, 8);
memcpy(&seq, p + 9, 8);
DEBUG_DEBUG(DEBUG_CATEGORY_DM, "%s: custody confirmed super=%llu conv=%llu seq=%llu (await recipient)", MB_ID,
(unsigned long long)source, (unsigned long long)conv, (unsigned long long)seq);
} else DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: invalid command=%u bytes=%zu", MB_ID, p[0], len);
done:
queue_dgram_free(entry);
queue_entry_free(entry);
}
/* Продвигать ограниченную порцию; SQLite statement закрыт до вызова транспорта. */
static void mb_tick(void* arg) {
struct dm_mb_state* mb = arg;
mb->timer = NULL;
int64_t now = (int64_t)ntp_time_get_seconds(mb->inst);
for (int i = 0; i < 32; i++) {
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(mb->db,
"SELECT msg, length(msg) FROM dm_mail WHERE recipient=? AND sender=? AND seq>? ORDER BY seq LIMIT ?",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)sender);
sqlite3_bind_int64(st, 3, (sqlite3_int64)since_seq);
sqlite3_bind_int(st, 4, MB_MAX_BATCH);
while (sqlite3_step(st) == SQLITE_ROW) {
const uint8_t* msg = (const uint8_t*)sqlite3_column_blob(st, 0);
int mlen = sqlite3_column_int(st, 1);
if (!msg || mlen <= 0) continue;
if (boff + (size_t)mlen > buf_cap) break;
memcpy(body + boff, msg, (size_t)mlen);
boff += (size_t)mlen;
count++;
}
}
if (st) sqlite3_finalize(st);
memcpy(body + 16, &count, 2);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: PULL recipient=0x%016llx sender=0x%016llx since=%llu → %u msgs",
MB_ID, (unsigned long long)recipient, (unsigned long long)sender,
(unsigned long long)since_seq, count);
if (count > 0)
mb_route_send(mb, group_id, recipient, MB_SUBCMD_PULL_RESP, body, boff);
u_free(body);
} else if (subcmd == MB_SUBCMD_PULL_RESP) {
/* [subcmd][recipient:8][sender:8][count:2][msg]* — получатель: передать в dm_core + ACK storage */
if (plen < 1 + 8 + 8 + 2) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t storage; memcpy(&storage, d + ROUTER_SVC_SRC_OFF, 8);
uint64_t recipient; memcpy(&recipient, p + 1, 8);
uint64_t sender; memcpy(&sender, p + 9, 8);
uint16_t count; memcpy(&count, p + 17, 2);
const uint8_t* m = p + 19;
size_t remaining = plen - 19;
uint64_t max_seq = 0;
for (int i = 0; i < count; i++) {
size_t mlen = 0;
if (!dm_msg_len(m, remaining, &mlen)) break;
uint64_t seq = mb_msg_seq(m);
if (seq > max_seq) max_seq = seq;
if (mb->deliver_fn) mb->deliver_fn(m, mlen, mb->deliver_arg);
m += mlen; remaining -= mlen;
}
/* подтвердить storage-узлу приём — удалить доставленное (recipient/sender/up_to_seq) */
if (count > 0 && max_seq > 0) {
uint8_t ack[24];
memcpy(ack, &recipient, 8);
memcpy(ack + 8, &sender, 8);
memcpy(ack + 16, &max_seq, 8);
mb_route_send(mb, group_id, storage, MB_SUBCMD_ACK, ack, sizeof(ack));
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: PULL_RESP recipient=0x%016llx sender=0x%016llx count=%u → ACK up_to=%llu",
MB_ID, (unsigned long long)recipient, (unsigned long long)sender, count,
(unsigned long long)max_seq);
int rc = sqlite3_prepare_v2(mb->db,
"SELECT recipient,sender,seq,msg,group_id FROM dm_pending_messages"
" WHERE retry_at<=? ORDER BY retry_at,seq LIMIT 1", -1, &st, NULL);
if (rc != SQLITE_OK) goto fail;
sqlite3_bind_int64(st, 1, now);
rc = sqlite3_step(st);
if (rc == SQLITE_DONE) { sqlite3_finalize(st); break; }
if (rc != SQLITE_ROW) goto fail;
uint64_t recipient = (uint64_t)sqlite3_column_int64(st, 0), author = (uint64_t)sqlite3_column_int64(st, 1);
uint64_t seq = (uint64_t)sqlite3_column_int64(st, 2), gid = (uint64_t)sqlite3_column_int64(st, 4);
size_t len = (size_t)sqlite3_column_bytes(st, 3);
uint8_t* msg = u_malloc(len);
if (!msg) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: delivery allocation failed", MB_ID); sqlite3_finalize(st); break; }
memcpy(msg, sqlite3_column_blob(st, 3), len);
sqlite3_finalize(st);
st = NULL;
rc = sqlite3_prepare_v2(mb->db,
"UPDATE dm_pending_messages SET retry_at=? WHERE recipient=? AND sender=? AND seq=?", -1, &st, NULL);
if (rc == SQLITE_OK) {
sqlite3_bind_int64(st, 1, now + 5);
sqlite3_bind_int64(st, 2, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 3, (sqlite3_int64)author);
sqlite3_bind_int64(st, 4, (sqlite3_int64)seq);
rc = sqlite3_step(st);
}
} else if (subcmd == MB_SUBCMD_ACK) {
/* [subcmd][recipient:8][sender:8][up_to_seq:8] — storage: удалить доставленное */
if (plen < 1 + 8 + 8 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t recipient; memcpy(&recipient, p + 1, 8);
uint64_t sender; memcpy(&sender, p + 9, 8);
uint64_t up_to_seq; memcpy(&up_to_seq, p + 17, 8);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(mb->db,
"DELETE FROM dm_mail WHERE recipient=? AND sender=? AND seq<=?",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient);
sqlite3_bind_int64(st, 2, (sqlite3_int64)sender);
sqlite3_bind_int64(st, 3, (sqlite3_int64)up_to_seq);
sqlite3_step(st);
sqlite3_finalize(st);
st = NULL;
if (rc != SQLITE_DONE) { u_free(msg); goto fail; }
uint64_t route_gid = gid;
if (dm_route_group(mb->inst, recipient, gid, &route_gid) &&
mb_scope(mb, route_gid, mb->inst->node_id, author, recipient)) {
mb_send(mb, route_gid, recipient, MB_DELIVER, msg, len);
DEBUG_DEBUG(DEBUG_CATEGORY_DM, "%s: pending delivery sender=%llu recipient=%llu seq=%llu group=%llu", MB_ID,
(unsigned long long)author, (unsigned long long)recipient, (unsigned long long)seq,
(unsigned long long)route_gid);
}
if (st) sqlite3_finalize(st);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: ACK recipient=0x%016llx sender=0x%016llx up_to=%llu",
MB_ID, (unsigned long long)recipient, (unsigned long long)sender,
(unsigned long long)up_to_seq);
} else {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: unknown subcmd=%02x", MB_ID, subcmd);
u_free(msg);
continue;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: pending scan rc=%d error=%s", MB_ID, rc, sqlite3_errmsg(mb->db));
sqlite3_finalize(st);
break;
}
queue_dgram_free(entry); queue_entry_free(entry);
mb->timer = uasync_set_timeout(mb->inst->ua, 10000, mb, mb_tick, "dm_mailbox");
if (!mb->timer) DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: retry timer allocation failed", MB_ID);
}
/* ── Жизненный цикл ── */
/* Новые таблицы протокола; прежний формат и диапазонные ACK не используются. */
int dm_mailbox_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (inst->dm_mailbox && inst->dm_mailbox->initialized) return 0;
if (!inst || !inst->topo_sqlite_db) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: init without core DB", MB_ID); return -1; }
if (inst->dm_mailbox) return 0;
struct dm_mb_state* mb = u_calloc(1, sizeof(*mb));
if (!mb) return -1;
inst->dm_mailbox = mb;
if (!mb) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: state allocation failed", MB_ID); return -1; }
mb->inst = inst;
mb->db = chat_core_get_db(inst);
if (!mb->db) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no chat db", MB_ID); u_free(mb); inst->dm_mailbox = NULL; return -1; }
mb_create_tables(mb);
if (etcp_router_bind(inst, ETCP_RT_ID_DM_MAILBOX, mb_recv_cb) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: etcp_router_bind failed", MB_ID);
u_free(mb); inst->dm_mailbox = NULL; return -1;
mb->db = inst->topo_sqlite_db;
if (mb_exec(mb, "CREATE TABLE IF NOT EXISTS dm_pending_messages (recipient INTEGER NOT NULL,sender INTEGER NOT NULL,"
"seq INTEGER NOT NULL,msg BLOB NOT NULL,group_id INTEGER NOT NULL,retry_at INTEGER NOT NULL DEFAULT 0,"
"PRIMARY KEY(recipient,sender,seq))") != 0 ||
mb_exec(mb, "CREATE TABLE IF NOT EXISTS dm_mail_receipts (recipient INTEGER NOT NULL,sender INTEGER NOT NULL,"
"seq INTEGER NOT NULL,receipt BLOB NOT NULL,group_id INTEGER NOT NULL,"
"PRIMARY KEY(recipient,sender,seq))") != 0) goto fail;
if (etcp_router_bind(inst, ETCP_RT_ID_DM_MAILBOX, mb_recv) != 0) goto fail;
inst->dm_mailbox = mb;
mb->media = dm_mailbox_media_create(inst);
if (!mb->media) {
etcp_router_unbind(inst, ETCP_RT_ID_DM_MAILBOX);
inst->dm_mailbox = NULL;
goto fail;
}
mb->initialized = 1;
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: initialized", MB_ID);
mb->timer = uasync_set_timeout(inst->ua, 10000, mb, mb_tick, "dm_mailbox");
if (!mb->timer) {
etcp_router_unbind(inst, ETCP_RT_ID_DM_MAILBOX);
inst->dm_mailbox = NULL;
goto fail;
}
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: initialized persistent supernode queue", MB_ID);
return 0;
fail:
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: initialization failed", MB_ID);
dm_mailbox_media_destroy(mb->media);
u_free(mb);
return -1;
}
/* Отменить таймер до освобождения owner, durable копии оставить в БД. */
void dm_mailbox_destroy(struct UTUN_INSTANCE* inst) {
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !mb || !mb->initialized) return;
if (!inst || !inst->dm_mailbox) return;
struct dm_mb_state* mb = inst->dm_mailbox;
if (mb->timer) uasync_cancel_timeout(inst->ua, mb->timer);
dm_mailbox_media_destroy(mb->media);
etcp_router_unbind(inst, ETCP_RT_ID_DM_MAILBOX);
u_free(mb);
inst->dm_mailbox = NULL;
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: destroyed", MB_ID);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: destroyed, pending retained", MB_ID);
u_free(mb);
}
/* ── Публичные операции ── */
/* Собрать storage-узлы (storage=yes в adm_tags) из общих групп без дублей. */
int dm_mailbox_get_storage_nodes(struct UTUN_INSTANCE* inst, uint64_t* out, int max) {
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !out || max <= 0 || !mb || !mb->db) return 0;
uint64_t my_id = inst->node_id;
uint64_t* ch_ids = NULL;
int ch_count = 0;
if (topo_node_sqlite_get_member_channels(mb->db, my_id, &ch_ids, &ch_count) != 0) return 0;
int found = 0;
char buf[256];
for (int c = 0; c < ch_count && found < max; c++) {
char ch[64]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)ch_ids[c]);
char tbl[80]; snprintf(tbl, sizeof(tbl), "peers_%s", ch);
char sql[256]; snprintf(sql, sizeof(sql),
"SELECT node_id, COALESCE(adm_tags,'') FROM \"%s\"", tbl);
/* Найти суперузлы в общей группе обеих сторон; для PUT выбрать только один. */
static int mb_to_supernodes(struct dm_mb_state* mb, uint64_t recipient, uint64_t author,
uint8_t cmd, const uint8_t* body, size_t len) {
uint64_t* ids = NULL;
int count = 0, sent = 0;
if (topo_node_sqlite_get_member_channels(mb->db, mb->inst->node_id, &ids, &count) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: membership lookup failed", MB_ID);
return -1;
}
for (int i = 0; i < count; i++) {
char ch[64], sql[256];
snprintf(ch, sizeof(ch), "%llu", (unsigned long long)ids[i]);
if (!topo_node_sqlite_member_in_channel(mb->db, ch, author) ||
!topo_node_sqlite_member_in_channel(mb->db, ch, recipient)) continue;
snprintf(sql, sizeof(sql), "SELECT node_id,adm_tags FROM \"peers_%s\" WHERE deleted=0 ORDER BY node_id", ch);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(mb->db, sql, -1, &st, NULL) != SQLITE_OK) continue;
while (sqlite3_step(st) == SQLITE_ROW && found < max) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
if (nid == my_id) continue;
if (sqlite3_prepare_v2(mb->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: supernode query failed: %s", MB_ID, sqlite3_errmsg(mb->db));
continue;
}
int rc;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
uint64_t node = (uint64_t)sqlite3_column_int64(st, 0);
const char* tags = (const char*)sqlite3_column_text(st, 1);
if (tags && json_flat_get(tags, "storage", buf, sizeof(buf)) == 0
&& strcmp(buf, "yes") == 0) {
int dup = 0;
for (int i = 0; i < found; i++) if (out[i] == nid) { dup = 1; break; }
if (!dup) out[found++] = nid;
char role[16];
if (!tags || json_flat_get(tags, "supernode", role, sizeof(role)) != 0 || strcmp(role, "yes")) continue;
if (node == mb->inst->node_id || node == recipient || node == author) continue;
struct ETCP_ROUTER_PATH path = etcp_router_get_path(mb->inst, ids[i], node, ETCP_RT_ID_DM_MAILBOX);
if (!path.next_hop_node_id) continue;
if (!mb_send(mb, ids[i], node, cmd, body, len)) {
sent++;
if (cmd == MB_PUT) break;
}
}
if (rc != SQLITE_ROW && rc != SQLITE_DONE)
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: supernode scan failed rc=%d", MB_ID, rc);
sqlite3_finalize(st);
if (sent && cmd == MB_PUT) break;
}
if (ch_ids) u_free(ch_ids);
return found;
u_free(ids);
return sent;
}
/* Отправитель: разослать зашифрованное сообщение на все storage-узлы. */
int dm_mailbox_put(struct UTUN_INSTANCE* inst, uint64_t recipient, uint64_t sender,
const uint8_t* msg, size_t msg_len) {
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !mb || !msg || msg_len == 0) return -1;
uint64_t nodes[8];
int n = dm_mailbox_get_storage_nodes(inst, nodes, 8);
if (n == 0) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: no storage nodes for offline delivery", MB_ID);
/* Исходящее тело остаётся у A до подписанного подтверждения B. */
int dm_mailbox_put(struct UTUN_INSTANCE* inst, uint64_t recipient, uint64_t sender, const uint8_t* msg, size_t len) {
struct dm_mb_state* mb = inst ? inst->dm_mailbox : NULL;
if (!mb || sender != inst->node_id || dm_verify_message(inst, recipient, msg, len) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: invalid local PUT", MB_ID);
return -1;
}
/* body = [recipient:8][sender:8][msg] */
size_t body_len = 8 + 8 + msg_len;
uint8_t* body = u_malloc(body_len);
if (!body) return -1;
uint8_t body[8 + DM_MEDIA_BODY_MAX];
memcpy(body, &recipient, 8);
memcpy(body + 8, &sender, 8);
memcpy(body + 16, msg, msg_len);
int sent = 0;
for (int i = 0; i < n; i++) {
if (mb_route_send(mb, 0, nodes[i], MB_SUBCMD_PUT, body, body_len) == 0) sent++;
}
u_free(body);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: put → %d/%d storage nodes", MB_ID, sent, n);
return sent > 0 ? 0 : -1;
memcpy(body + 8, msg, len);
int n = mb_to_supernodes(mb, recipient, sender, MB_PUT, body, len + 8);
if (n <= 0) DEBUG_DEBUG(DEBUG_CATEGORY_DM, "%s: no reachable supernode; local outbox retained recipient=%llu", MB_ID,
(unsigned long long)recipient);
return n > 0 ? 0 : -1;
}
/* Получатель: запросить почту от отправителя со всех storage-узлов. */
void dm_mailbox_pull(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, uint64_t since_seq) {
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !mb) return;
uint64_t nodes[8];
int n = dm_mailbox_get_storage_nodes(inst, nodes, 8);
if (n == 0) return;
uint8_t body[24];
memcpy(body, &inst->node_id, 8);
memcpy(body + 8, &peer_node_id, 8);
memcpy(body + 16, &since_seq, 8);
for (int i = 0; i < n; i++)
mb_route_send(mb, 0, nodes[i], MB_SUBCMD_PULL, body, sizeof(body));
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: pull from %d storage nodes (peer=0x%016llx since=%llu)",
MB_ID, n, (unsigned long long)peer_node_id, (unsigned long long)since_seq);
/* Копии могут остаться после смены маршрута/суперузла — удалить каждую доступную. */
void dm_mailbox_receipt(struct UTUN_INSTANCE* inst, const uint8_t* receipt) {
struct dm_mb_state* mb = inst ? inst->dm_mailbox : NULL;
if (!mb || dm_verify_receipt(inst, receipt, NULL, 0) != 0) return;
uint64_t author, recipient;
memcpy(&author, receipt + 24, 8);
memcpy(&recipient, receipt + 32, 8);
mb_to_supernodes(mb, recipient, author, MB_RECEIPT, receipt, DM_RECEIPT_SIZE);
}

32
src/dm/dm_mailbox.h

@ -1,12 +1,12 @@
/*
* dm_mailbox.h — offline-хранение DM-сообщений на storage-узлах
* dm_mailbox.h — очередь offline-сообщений на суперузлах
*
* Когда пир недоступен (нет прямой связи), отправитель кладёт зашифрованное
* сообщение на storage-узлы (мемберы общих групп с флагом storage=yes).
* Получатель при выходе в online вытягивает накопившуюся почту, проверяет
* подпись, расшифровывает и вставляет в dm_messages (dedup по seq).
* Суперузел общей группы хранит непрозрачное подписанное тело и повторяет
* доставку при появлении маршрута до получателя, независимо от отправителя.
* Копия удаляется только по квитанции получателя после commit в dm_core.
* Квитанция сохраняется отдельно для повторов PUT и доставки отправителю.
*
* Сервис ETCP_RT_ID_DM_MAILBOX (0x35), подкоманды PUT/PULL/PULL_RESP/ACK.
* Сервис ETCP_RT_ID_DM_MAILBOX (0x35); контрольные пакеты подписаны router.
*/
#ifndef DM_MAILBOX_H
@ -25,23 +25,15 @@ extern "C" {
int dm_mailbox_init(struct UTUN_INSTANCE* inst);
void dm_mailbox_destroy(struct UTUN_INSTANCE* inst);
/* Коллбэк доставки вытянутых сообщений в dm_core (msg = каноническое тело
* DM-сообщения, включая подпись). Вызывается из uasync-потока. */
typedef void (*dm_mail_deliver_fn)(const uint8_t* msg, size_t len, void* arg);
void dm_mailbox_set_deliver_cb(struct UTUN_INSTANCE* inst, dm_mail_deliver_fn fn, void* arg);
/* Отправитель: положить зашифрованное сообщение на storage-узлы. msg — уже
* собранное каноническое тело (зашифровано + подписано). 0=ок, <0=ошибка. */
/* Поставить PUT выбранному доступному суперузлу общей с recipient группы.
* 0 означает принятие router, хранение подтверждается отдельно на проводе.
* До квитанции получателя источник сохраняет задачу в durable outbox. */
int dm_mailbox_put(struct UTUN_INSTANCE* inst, uint64_t recipient, uint64_t sender,
const uint8_t* msg, size_t msg_len);
/* Получатель: вытянуть почту от конкретного отправителя (seq > since_seq) со
* всех известных storage-узлов. Вызывается из uasync-потока. */
void dm_mailbox_pull(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, uint64_t since_seq);
/* Собрать node_id storage-узлов (storage=yes в adm_tags) из общих групп. До max
* штук. Возвращает количество найденных. out — буфер на max*8 байт. */
int dm_mailbox_get_storage_nodes(struct UTUN_INSTANCE* inst, uint64_t* out, int max);
/* Передать точную квитанцию всем доступным суперузлам общей группы сторон.
* receipt — DM_RECEIPT_SIZE байт (см. dm_core.h); вызов только из uasync. */
void dm_mailbox_receipt(struct UTUN_INSTANCE* inst, const uint8_t* receipt);
#ifdef __cplusplus
}

318
src/dm/dm_mailbox_media.c

@ -0,0 +1,318 @@
/* dm_mailbox_media — S хранит координатор отдельно от opaque custody на M. */
#include <stdio.h>
#include <string.h>
#include <openssl/sha.h>
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
#include "../../lib/json_flat.h"
#include "dm_media_priv.h"
#include "dm_mailbox_media.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../routing_layer/etcp_router.h"
#include "../routing_layer/topo_node_sqlite.h"
struct dm_mb_media {
struct UTUN_INSTANCE* inst;
void* timer;
};
/* Регистрация идёт в транзакции PUT, до выдачи какого-либо задания M. */
int dm_mailbox_media_register(struct dm_mb_media* media, uint64_t group, uint64_t recipient,
const uint8_t* body, size_t len) {
const uint8_t* desc = dm_message_media(body, len);
if (!desc) return 0;
if (!media) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: registration without owner"); return -1; }
struct UTUN_INSTANCE* inst = media->inst;
uint64_t author, seq;
memcpy(&author, body + 24, 8); memcpy(&seq, body + 8, 8);
uint8_t hash[32]; SHA256(body, len, hash);
sqlite3_stmt* st = dm_media_prepare(inst, "INSERT INTO dm_media_jobs(id,author,recipient,seq,body,body_hash,group_id)"
" VALUES(?,?,?,?,?,?,?) ON CONFLICT(id) DO NOTHING");
if (!st) return -1;
sqlite3_bind_blob(st, 1, desc, 16, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)author);
sqlite3_bind_int64(st, 3, (sqlite3_int64)recipient); sqlite3_bind_int64(st, 4, (sqlite3_int64)seq);
sqlite3_bind_blob(st, 5, body, (int)len, SQLITE_STATIC); sqlite3_bind_blob(st, 6, hash, 32, SQLITE_STATIC);
sqlite3_bind_int64(st, 7, (sqlite3_int64)group);
if (dm_media_step(inst, st)) return -1;
int inserted = sqlite3_changes(inst->topo_sqlite_db);
st = dm_media_prepare(inst, "SELECT body_hash FROM dm_media_jobs WHERE id=?");
if (!st) return -1;
sqlite3_bind_blob(st, 1, desc, 16, SQLITE_STATIC);
int rc = sqlite3_step(st);
int exact = rc == SQLITE_ROW && sqlite3_column_bytes(st, 0) == 32 && !memcmp(sqlite3_column_blob(st, 0), hash, 32);
sqlite3_finalize(st);
if (!exact) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: conflicting media UUID/body"); return -1; }
if (inserted) DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_mailbox_media: registered author=%llu recipient=%llu seq=%llu group=%llu",
(unsigned long long)author, (unsigned long long)recipient, (unsigned long long)seq, (unsigned long long)group);
return 0;
}
/* Список S используется только для пересылки подписанного подтверждения B. */
void dm_mailbox_media_receipt(struct UTUN_INSTANCE* inst, const uint8_t* receipt) {
if (dm_media_verify_receipt(inst, receipt, NULL, 0)) return;
uint64_t author, recipient;
memcpy(&author, receipt + 24, 8); memcpy(&recipient, receipt + 32, 8);
uint64_t* groups = NULL;
int count = 0;
if (topo_node_sqlite_get_member_channels(inst->topo_sqlite_db, inst->node_id, &groups, &count)) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: common group lookup failed"); return;
}
for (int i = 0; i < count; i++) {
char sql[192];
snprintf(sql, sizeof(sql), "SELECT node_id FROM \"peers_%llu\" WHERE deleted=0", (unsigned long long)groups[i]);
sqlite3_stmt* st = dm_media_prepare(inst, sql);
if (!st) continue;
uint64_t peers[64];
unsigned n = 0;
int rc;
while ((rc = sqlite3_step(st)) == SQLITE_ROW && n < 64) {
uint64_t node = (uint64_t)sqlite3_column_int64(st, 0);
if (node != author && node != recipient && dm_media_scope(inst, groups[i], node, "supernode", author, recipient))
peers[n++] = node;
}
if (rc != SQLITE_ROW && rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: supernode scan failed rc=%d", rc);
sqlite3_finalize(st);
for (unsigned j = 0; j < n; j++)
if (peers[j] == inst->node_id || etcp_router_get_path(inst, groups[i], peers[j], ETCP_RT_ID_DM_MEDIA).next_hop_node_id)
dm_media_send(inst, groups[i], peers[j], DM_MEDIA_RECEIPT, receipt, DM_MEDIA_RECEIPT_SIZE);
}
u_free(groups);
}
/* Подтверждение медиадоставки сохраняется до начала DELETE и никогда не отменяется STORED. */
static void dm_job_receipt(struct dm_mb_media* media, uint64_t group, uint64_t source, const uint8_t* r) {
struct UTUN_INSTANCE* inst = media->inst;
if (dm_media_verify_receipt(inst, r, NULL, 0)) return;
uint64_t author, recipient;
memcpy(&author, r + 24, 8); memcpy(&recipient, r + 32, 8);
if (!dm_media_scope(inst, group, inst->node_id, "supernode", author, recipient)) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_mailbox_media: receipt outside S scope source=%llu", (unsigned long long)source); return;
}
sqlite3_stmt* st = dm_media_prepare(inst, "SELECT body_hash,receipt FROM dm_media_jobs WHERE id=?");
if (!st) return;
sqlite3_bind_blob(st, 1, r + 40, 16, SQLITE_STATIC);
int rc = sqlite3_step(st);
if (rc == SQLITE_DONE) { sqlite3_finalize(st); return; } /* Нет выданных копий на этом S. */
int exact = rc == SQLITE_ROW && sqlite3_column_bytes(st, 0) == 32 && !memcmp(sqlite3_column_blob(st, 0), r + 56, 32);
int already = rc == SQLITE_ROW && sqlite3_column_bytes(st, 1) == DM_MEDIA_RECEIPT_SIZE;
sqlite3_finalize(st);
if (!exact) { DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_mailbox_media: receipt body hash mismatch rc=%d", rc); return; }
st = dm_media_prepare(inst, "UPDATE dm_media_jobs SET receipt=?,retry_at=0 WHERE id=? AND receipt IS NULL");
if (!st) return;
sqlite3_bind_blob(st, 1, r, DM_MEDIA_RECEIPT_SIZE, SQLITE_STATIC); sqlite3_bind_blob(st, 2, r + 40, 16, SQLITE_STATIC);
if (dm_media_step(inst, st)) return;
if (!already) {
DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_mailbox_media: recipient confirmed file; delete jobs activated source=%llu",
(unsigned long long)source);
dm_media_send(inst, group, author, DM_MEDIA_RECEIPT, r, DM_MEDIA_RECEIPT_SIZE);
}
}
/* Control от M принимается только для durable зарегистрированного назначения. */
void dm_mailbox_media_recv(struct dm_mb_media* media, uint64_t group, uint64_t source,
uint8_t cmd, const uint8_t* p, size_t len) {
if (!media) { DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_mailbox_media: control after stop"); return; }
struct UTUN_INSTANCE* inst = media->inst;
if (cmd == DM_MEDIA_RECEIPT && len == DM_MEDIA_RECEIPT_SIZE) { dm_job_receipt(media, group, source, p); return; }
sqlite3_stmt* st = dm_media_prepare(inst, "SELECT j.author,j.recipient,j.receipt,j.holder FROM dm_media_jobs j"
" JOIN dm_media_holders h ON h.id=j.id WHERE j.id=? AND h.node_id=? AND h.group_id=?");
if (!st) return;
sqlite3_bind_blob(st, 1, p, 16, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)source);
sqlite3_bind_int64(st, 3, (sqlite3_int64)group);
int rc = sqlite3_step(st);
if (rc == SQLITE_DONE) { sqlite3_finalize(st); return; }
if (rc != SQLITE_ROW) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: location lookup failed rc=%d", rc); sqlite3_finalize(st); return; }
uint64_t author = (uint64_t)sqlite3_column_int64(st, 0), recipient = (uint64_t)sqlite3_column_int64(st, 1);
uint64_t holder = (uint64_t)sqlite3_column_int64(st, 3);
uint8_t receipt[DM_MEDIA_RECEIPT_SIZE];
int delivered = sqlite3_column_bytes(st, 2) == sizeof(receipt);
if (delivered) memcpy(receipt, sqlite3_column_blob(st, 2), sizeof(receipt));
sqlite3_finalize(st);
if (cmd == DM_MEDIA_STORED && len == 24) {
uint64_t expiry;
memcpy(&expiry, p + 16, 8);
if (delivered) { dm_media_send(inst, group, source, DM_MEDIA_DELETE, receipt, sizeof(receipt)); return; }
if (holder != source || expiry <= ntp_time_get_seconds(inst) ||
!dm_media_scope(inst, group, source, "storage", author, recipient)) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_mailbox_media: stale/unauthorized STORED source=%llu expiry=%llu",
(unsigned long long)source, (unsigned long long)expiry); return;
}
st = dm_media_prepare(inst, "UPDATE dm_media_jobs SET stored=1,expires=?,retry_at=0 WHERE id=? AND receipt IS NULL AND stored=0");
if (!st) return;
sqlite3_bind_int64(st, 1, (sqlite3_int64)expiry); sqlite3_bind_blob(st, 2, p, 16, SQLITE_STATIC);
if (dm_media_step(inst, st)) return;
if (sqlite3_changes(inst->topo_sqlite_db))
DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_mailbox_media: custody committed M=%llu expires=%llu",
(unsigned long long)source, (unsigned long long)expiry);
} else if ((cmd == DM_MEDIA_DELETE_ACK || cmd == DM_MEDIA_EXPIRED) && len == 16) {
st = dm_media_prepare(inst, "UPDATE dm_media_holders SET released=1 WHERE id=? AND node_id=?");
if (!st) return;
sqlite3_bind_blob(st, 1, p, 16, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)source);
if (dm_media_step(inst, st)) return;
if (!delivered && cmd == DM_MEDIA_EXPIRED) {
st = dm_media_prepare(inst, "UPDATE dm_media_jobs SET holder=0,stored=0,expired=1,retry_at=0 WHERE id=? AND holder=?");
if (!st) return;
sqlite3_bind_blob(st, 1, p, 16, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)source);
dm_media_step(inst, st);
}
} else if (cmd == DM_MEDIA_REJECT && len == 17 && !delivered) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_mailbox_media: storage refused M=%llu reason=%u", (unsigned long long)source, p[16]);
st = dm_media_prepare(inst, "UPDATE dm_media_holders SET released=1 WHERE id=? AND node_id=?");
if (!st) return;
sqlite3_bind_blob(st, 1, p, 16, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)source);
if (dm_media_step(inst, st)) return;
st = dm_media_prepare(inst, "UPDATE dm_media_jobs SET holder=0,stored=0,retry_at=0 WHERE id=? AND holder=?");
if (!st) return;
sqlite3_bind_blob(st, 1, p, 16, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)source);
dm_media_step(inst, st);
} else DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_mailbox_media: unexpected control cmd=%u bytes=%zu", cmd, len);
}
/* Выбрать один достижимый storage, исключив A/B и прежние отказы/истёкшие назначения. */
static uint64_t dm_job_storage(struct dm_mb_media* media, uint64_t group, const uint8_t id[16], uint64_t a, uint64_t b) {
struct UTUN_INSTANCE* inst = media->inst;
char sql[256];
snprintf(sql, sizeof(sql), "SELECT node_id,adm_tags FROM \"peers_%llu\" WHERE deleted=0 AND node_id NOT IN"
" (SELECT node_id FROM dm_media_holders WHERE id=?) ORDER BY node_id", (unsigned long long)group);
sqlite3_stmt* st = dm_media_prepare(inst, sql);
if (!st) return 0;
sqlite3_bind_blob(st, 1, id, 16, SQLITE_STATIC);
uint64_t found = 0;
int rc;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
uint64_t node = (uint64_t)sqlite3_column_int64(st, 0);
const char* tags = (const char*)sqlite3_column_text(st, 1);
char role[16];
if (node == a || node == b || !tags || json_flat_get(tags, "storage", role, sizeof(role)) || strcmp(role, "yes")) continue;
if (node == inst->node_id || etcp_router_get_path(inst, group, node, ETCP_RT_ID_DM_MEDIA).next_hop_node_id) { found = node; break; }
}
if (rc != SQLITE_ROW && rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: storage scan failed rc=%d", rc);
sqlite3_finalize(st);
return found;
}
/* Удаление каждого когда-либо выданного назначения повторяется до signed DELETE_ACK. */
static void dm_job_cleanup(struct dm_mb_media* media, const uint8_t id[16], const uint8_t* receipt) {
struct UTUN_INSTANCE* inst = media->inst;
sqlite3_stmt* st = dm_media_prepare(inst, "SELECT node_id,group_id FROM dm_media_holders WHERE id=? AND released=0 LIMIT 16");
if (!st) return;
sqlite3_bind_blob(st, 1, id, 16, SQLITE_STATIC);
uint64_t peers[16], groups[16];
unsigned count = 0;
int rc;
while ((rc = sqlite3_step(st)) == SQLITE_ROW && count < 16) {
peers[count] = (uint64_t)sqlite3_column_int64(st, 0); groups[count++] = (uint64_t)sqlite3_column_int64(st, 1);
}
sqlite3_finalize(st);
if (rc != SQLITE_ROW && rc != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: cleanup scan failed rc=%d", rc); return; }
for (unsigned i = 0; i < count; i++) dm_media_send(inst, groups[i], peers[i], DM_MEDIA_DELETE, receipt, DM_MEDIA_RECEIPT_SIZE);
if (!count) {
st = dm_media_prepare(inst, "UPDATE dm_media_jobs SET body=NULL WHERE id=? AND receipt IS NOT NULL AND body IS NOT NULL");
if (!st) return;
sqlite3_bind_blob(st, 1, id, 16, SQLITE_STATIC);
if (!dm_media_step(inst, st) && sqlite3_changes(inst->topo_sqlite_db))
DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_mailbox_media: all custody copies deleted; message body removed");
}
}
/* Snapshot statement закрывается до control send, включая локальное совмещение S/M. */
static void dm_mailbox_media_tick(void* arg) {
struct dm_mb_media* media = arg;
struct UTUN_INSTANCE* inst = media->inst;
media->timer = NULL;
uint64_t now = ntp_time_get_seconds(inst);
for (int i = 0; i < 8; i++) {
sqlite3_stmt* st = dm_media_prepare(inst, "SELECT id,author,recipient,body,group_id,holder,stored,expires,receipt,expired"
" FROM dm_media_jobs WHERE retry_at<=? ORDER BY retry_at,seq LIMIT 1");
if (!st) break;
sqlite3_bind_int64(st, 1, (sqlite3_int64)now);
int rc = sqlite3_step(st);
if (rc == SQLITE_DONE) { sqlite3_finalize(st); break; }
if (rc != SQLITE_ROW || sqlite3_column_bytes(st, 0) != 16) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: job scan failed rc=%d", rc); sqlite3_finalize(st); break;
}
uint8_t id[16], body[DM_MEDIA_BODY_MAX], receipt[DM_MEDIA_RECEIPT_SIZE];
memcpy(id, sqlite3_column_blob(st, 0), 16);
uint64_t author = (uint64_t)sqlite3_column_int64(st, 1), recipient = (uint64_t)sqlite3_column_int64(st, 2);
size_t len = (size_t)sqlite3_column_bytes(st, 3);
if (len <= sizeof(body) && len) memcpy(body, sqlite3_column_blob(st, 3), len);
uint64_t group = (uint64_t)sqlite3_column_int64(st, 4), holder = (uint64_t)sqlite3_column_int64(st, 5);
int stored = sqlite3_column_int(st, 6), expired = sqlite3_column_int(st, 9);
uint64_t expires = (uint64_t)sqlite3_column_int64(st, 7);
int delivered = sqlite3_column_bytes(st, 8) == sizeof(receipt);
if (delivered) memcpy(receipt, sqlite3_column_blob(st, 8), sizeof(receipt));
sqlite3_finalize(st);
st = dm_media_prepare(inst, "UPDATE dm_media_jobs SET retry_at=? WHERE id=?");
if (!st) break;
sqlite3_bind_int64(st, 1, (sqlite3_int64)(now + 5)); sqlite3_bind_blob(st, 2, id, 16, SQLITE_STATIC);
if (dm_media_step(inst, st)) break;
if (delivered) {
dm_job_cleanup(media, id, receipt);
uint64_t gid = group;
if (dm_route_group(inst, author, group, &gid)) dm_media_send(inst, gid, author, DM_MEDIA_RECEIPT, receipt, sizeof(receipt));
continue;
}
if (!len || len > sizeof(body) || !dm_message_media(body, len)) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: corrupt saved descriptor"); continue;
}
if (!dm_media_scope(inst, group, inst->node_id, "supernode", author, recipient)) continue;
uint64_t gid = group;
if (dm_route_group(inst, recipient, group, &gid)) {
uint8_t offer[24 + DM_MEDIA_BODY_MAX];
uint64_t location = stored && expires > now ? holder : author;
uint64_t expiry = stored ? expires : 0;
memcpy(offer, &recipient, 8); memcpy(offer + 8, &location, 8); memcpy(offer + 16, &expiry, 8);
memcpy(offer + 24, body, len);
dm_media_send(inst, gid, recipient, DM_MEDIA_OFFER, offer, len + 24);
if (expired || (stored && expires <= now)) dm_media_send(inst, gid, recipient, DM_MEDIA_EXPIRED, id, 16);
} else {
if (!holder) {
holder = dm_job_storage(media, group, id, author, recipient);
if (!holder) continue;
if (dm_media_exec(inst, "BEGIN IMMEDIATE")) continue;
st = dm_media_prepare(inst, "INSERT INTO dm_media_holders(id,node_id,group_id) VALUES(?,?,?)");
if (!st) { dm_media_exec(inst, "ROLLBACK"); continue; }
sqlite3_bind_blob(st, 1, id, 16, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)holder);
sqlite3_bind_int64(st, 3, (sqlite3_int64)group);
if (dm_media_step(inst, st)) { dm_media_exec(inst, "ROLLBACK"); continue; }
st = dm_media_prepare(inst, "UPDATE dm_media_jobs SET holder=?,stored=0 WHERE id=? AND receipt IS NULL");
if (!st) { dm_media_exec(inst, "ROLLBACK"); continue; }
sqlite3_bind_int64(st, 1, (sqlite3_int64)holder); sqlite3_bind_blob(st, 2, id, 16, SQLITE_STATIC);
if (dm_media_step(inst, st) || dm_media_exec(inst, "COMMIT")) { dm_media_exec(inst, "ROLLBACK"); continue; }
DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_mailbox_media: assigned storage M=%llu", (unsigned long long)holder);
}
if (!stored) {
uint8_t put[8 + DM_MEDIA_BODY_MAX]; memcpy(put, &recipient, 8); memcpy(put + 8, body, len);
dm_media_send(inst, group, holder, DM_MEDIA_STORE, put, len + 8);
}
}
}
media->timer = uasync_set_timeout(inst->ua, 10000, media, dm_mailbox_media_tick, "dm_mailbox_media");
if (!media->timer) DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: retry timer allocation failed");
}
struct dm_mb_media* dm_mailbox_media_create(struct UTUN_INSTANCE* inst) {
if (dm_media_exec(inst, "CREATE TABLE IF NOT EXISTS dm_media_jobs(id BLOB PRIMARY KEY,author INTEGER NOT NULL,"
"recipient INTEGER NOT NULL,seq INTEGER NOT NULL,body BLOB,body_hash BLOB NOT NULL,group_id INTEGER NOT NULL,"
"holder INTEGER NOT NULL DEFAULT 0,stored INTEGER NOT NULL DEFAULT 0,expires INTEGER NOT NULL DEFAULT 0,"
"expired INTEGER NOT NULL DEFAULT 0,receipt BLOB,retry_at INTEGER NOT NULL DEFAULT 0)") ||
dm_media_exec(inst, "CREATE TABLE IF NOT EXISTS dm_media_holders(id BLOB NOT NULL,node_id INTEGER NOT NULL,"
"group_id INTEGER NOT NULL,released INTEGER NOT NULL DEFAULT 0,PRIMARY KEY(id,node_id))") ||
dm_media_exec(inst, "UPDATE dm_media_jobs SET retry_at=0")) return NULL;
struct dm_mb_media* media = u_calloc(1, sizeof(*media));
if (!media) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: owner allocation failed"); return NULL; }
media->inst = inst;
media->timer = uasync_set_timeout(inst->ua, 10000, media, dm_mailbox_media_tick, "dm_mailbox_media");
if (!media->timer) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: timer allocation failed"); u_free(media); return NULL; }
DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_mailbox_media: initialized persistent coordinator");
return media;
}
void dm_mailbox_media_destroy(struct dm_mb_media* media) {
if (!media) return;
if (media->timer) uasync_cancel_timeout(media->inst->ua, media->timer);
DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_mailbox_media: destroyed, pending custody/delete jobs retained");
u_free(media);
}

23
src/dm/dm_mailbox_media.h

@ -0,0 +1,23 @@
/* dm_mailbox_media — durable задания суперузла: размещение на M, доставка
* метаданных получателю и удаление всех выданных копий до DELETE_ACK.
* Жизненный цикл принадлежит dm_mailbox, control принимает dm_media.
*/
#ifndef DM_MAILBOX_MEDIA_H
#define DM_MAILBOX_MEDIA_H
#include <stdint.h>
#include <stddef.h>
struct UTUN_INSTANCE;
struct dm_mb_media;
struct dm_mb_media* dm_mailbox_media_create(struct UTUN_INSTANCE* inst);
void dm_mailbox_media_destroy(struct dm_mb_media* media);
/* Вызвать в транзакции PUT после проверки роли S и подписи сообщения. */
int dm_mailbox_media_register(struct dm_mb_media* media, uint64_t group, uint64_t recipient,
const uint8_t* msg, size_t len);
void dm_mailbox_media_recv(struct dm_mb_media* media, uint64_t group, uint64_t source,
uint8_t cmd, const uint8_t* body, size_t len);
/* Отправить квитанцию всем доступным S общей группы, включая поздние копии. */
void dm_mailbox_media_receipt(struct UTUN_INSTANCE* inst, const uint8_t* receipt);
#endif

1053
src/dm/dm_media.c

File diff suppressed because it is too large Load Diff

60
src/dm/dm_media.h

@ -0,0 +1,60 @@
/* dm_media — файлы личных бесед и ограниченное ciphertext-хранилище M.
* Шифрование/проверка/публикация файлов в media_async; БД и сеть в uasync.
* dm_core вызывает track внутри своей транзакции. Квитанция файла независима
* от квитанции сообщения; создаётся после проверки, публикации и commit.
* Суперузлом и заданиями удаления владеет dm_mailbox_media.
*/
#ifndef DM_MEDIA_H
#define DM_MEDIA_H
#include <stdint.h>
#include <stddef.h>
#include "../media_async/attachment.h"
struct UTUN_INSTANCE;
#define DM_MEDIA_DESC_SIZE 56 /* UUID:16, plaintext_size:8, SHA256(ciphertext):32 */
#define DM_MEDIA_RECEIPT_SIZE 152
#define DM_MEDIA_MAX_SIZE (1024ULL * 1024 * 1024)
#define DM_MEDIA_OFFER 1
#define DM_MEDIA_STORE 2
#define DM_MEDIA_STORED 3
#define DM_MEDIA_RECEIPT 4
#define DM_MEDIA_DELETE 5
#define DM_MEDIA_DELETE_ACK 6
#define DM_MEDIA_EXPIRED 7
#define DM_MEDIA_REJECT 8
int dm_media_init(struct UTUN_INSTANCE* inst);
/* quiesce до media_async_destroy; destroy после workers, до групп/core/mailbox. */
void dm_media_quiesce(struct UTUN_INSTANCE* inst);
void dm_media_destroy(struct UTUN_INSTANCE* inst);
/* Подготовить копию и enqueue file-сообщение асинхронно. 0=задача принята. */
int dm_send_file(struct UTUN_INSTANCE* inst, const char* conv, const char* path, const struct attachment_info* info,
int remove_source);
struct dm_file_req {
struct UTUN_INSTANCE* inst;
char conv[64];
char path[1024];
struct attachment_info info;
int remove_source; /* Только принадлежащий приложению временный файл. */
};
void dm_send_file_trampoline(void* arg);
/* Внутри транзакции dm_core, 0=записано/точный повтор. Для text — пустая операция. */
int dm_media_track(struct UTUN_INSTANCE* inst, int dir, const uint8_t* msg, size_t len);
const uint8_t* dm_message_media(const uint8_t* msg, size_t len);
uint64_t dm_media_cipher_size(uint64_t plain_size);
int dm_media_verify_receipt(struct UTUN_INSTANCE* inst, const uint8_t* receipt, const uint8_t* msg, size_t len);
int dm_media_accept_receipt(struct UTUN_INSTANCE* inst, const uint8_t* receipt);
/* Общие операции control для endpoint/M и mailbox/S. Все пакеты подписаны. */
int dm_media_send(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer,
uint8_t cmd, const uint8_t* body, size_t len);
int dm_media_scope(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t node,
const char* role, uint64_t author, uint64_t recipient);
void dm_media_notify(struct UTUN_INSTANCE* inst, uint64_t conv);
#endif

16
src/dm/dm_media_priv.h

@ -0,0 +1,16 @@
/* Внутренние SQL/control операции двух владельцев DM media; снаружи не использовать. */
#ifndef DM_MEDIA_PRIV_H
#define DM_MEDIA_PRIV_H
#include "dm_media.h"
#include "dm_core.h"
#include <sqlite3.h>
#define DM_MEDIA_BODY_MAX (DM_MSG_FIXED_HDR + 255 + 2 + DM_DATA_MAX + 16 + DM_MEDIA_DESC_SIZE + DM_MSG_SIG_SIZE)
sqlite3_stmt* dm_media_prepare(struct UTUN_INSTANCE* inst, const char* sql);
int dm_media_step(struct UTUN_INSTANCE* inst, sqlite3_stmt* st);
int dm_media_exec(struct UTUN_INSTANCE* inst, const char* sql);
struct dm_mb_media* dm_mailbox_get_media(struct UTUN_INSTANCE* inst);
#endif

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

86
src/media_async/attachment.c

@ -0,0 +1,86 @@
/* Ограниченный бинарный формат: kind:1, name_len:2, duration_ms:4, width:2,
* height:2, UTF-8 name, waveform:100 для voice. Числа big-endian. */
#include <string.h>
#include "../../lib/debug_config.h"
#include "attachment.h"
/* Имя должно быть корректным UTF-8 basename без управляющих символов. */
static int valid_name(const char* name, size_t n) {
if (!n || n > 255 || (n == 1 && name[0] == '.') || (n == 2 && !memcmp(name, "..", 2))) return 0;
for (size_t i = 0; i < n;) {
unsigned c = (unsigned char)name[i++];
if (c < 0x20 || c == 0x7f || c == '/' || c == '\\') return 0;
if (c < 0x80) continue;
unsigned count, scalar, minimum;
if (c >= 0xc2 && c <= 0xdf) { count = 1; scalar = c & 31; minimum = 0x80; }
else if (c >= 0xe0 && c <= 0xef) { count = 2; scalar = c & 15; minimum = 0x800; }
else if (c >= 0xf0 && c <= 0xf4) { count = 3; scalar = c & 7; minimum = 0x10000; }
else return 0;
if (count > n - i) return 0;
while (count--) {
c = (unsigned char)name[i++];
if ((c & 0xc0) != 0x80) return 0;
scalar = (scalar << 6) | (c & 63);
}
if (scalar < minimum || scalar > 0x10ffff || (scalar >= 0xd800 && scalar <= 0xdfff)) return 0;
}
return 1;
}
/* Один набор правил для отправки, приёма и снимка истории. */
int attachment_validate(const struct attachment_info* info) {
if (!info || !valid_name(info->name, strnlen(info->name, sizeof(info->name)))) goto invalid;
size_t wave = strnlen(info->waveform, sizeof(info->waveform));
if (info->kind == ATTACHMENT_VOICE) {
if (!info->duration_ms || info->duration_ms > 86400000 || info->width || info->height || wave != 100) goto invalid;
if (strspn(info->waveform, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/") != wave) goto invalid;
} else {
if (wave) goto invalid;
if (info->kind == ATTACHMENT_FILE) {
if (info->duration_ms || info->width || info->height) goto invalid;
} else if (info->kind == ATTACHMENT_VIDEO) {
if (!info->width || !info->height || info->width > 16384 || info->height > 16384) goto invalid;
if (!info->duration_ms || info->duration_ms > 86400000) goto invalid;
} else goto invalid;
}
return 0;
invalid:
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment: invalid metadata kind=%d", info ? (int)info->kind : 0);
return -1;
}
int attachment_encode(const struct attachment_info* info, uint8_t* out, size_t capacity) {
if (attachment_validate(info)) return -1;
size_t n = strlen(info->name), length = 11 + n + (info->kind == ATTACHMENT_VOICE ? 100 : 0);
if (!out || capacity < length) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: encode buffer too small"); return -1; }
out[0] = (uint8_t)info->kind; out[1] = (uint8_t)(n >> 8); out[2] = (uint8_t)n;
for (unsigned i = 0; i < 4; i++) out[3 + i] = (uint8_t)(info->duration_ms >> (24 - 8 * i));
out[7] = (uint8_t)(info->width >> 8); out[8] = (uint8_t)info->width;
out[9] = (uint8_t)(info->height >> 8); out[10] = (uint8_t)info->height;
memcpy(out + 11, info->name, n);
if (info->kind == ATTACHMENT_VOICE) memcpy(out + 11 + n, info->waveform, 100);
return (int)length;
}
int attachment_decode(const uint8_t* data, size_t length, struct attachment_info* info) {
if (!data || !info || length < 11) goto invalid;
size_t n = ((size_t)data[1] << 8) | data[2];
if (!n || n > 255 || length != 11 + n + (data[0] == ATTACHMENT_VOICE ? 100 : 0) || memchr(data + 11, 0, n)) goto invalid;
memset(info, 0, sizeof(*info)); info->kind = (enum attachment_kind)data[0];
for (unsigned i = 0; i < 4; i++) info->duration_ms = (info->duration_ms << 8) | data[3 + i];
info->width = (uint16_t)((data[7] << 8) | data[8]); info->height = (uint16_t)((data[9] << 8) | data[10]);
memcpy(info->name, data + 11, n);
if (info->kind == ATTACHMENT_VOICE) memcpy(info->waveform, data + 11 + n, 100);
return attachment_validate(info);
invalid:
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment: malformed wire metadata bytes=%zu", length);
return -1;
}
const char* attachment_content_type(enum attachment_kind kind) {
switch (kind) {
case ATTACHMENT_VOICE: return "audio/opus";
case ATTACHMENT_VIDEO: return "video/mp4";
default: return "application/octet-stream";
}
}

30
src/media_async/attachment.h

@ -0,0 +1,30 @@
/* Общие метаданные готового вложения. Подготовка не знает адресата и транспорта.
* PM шифрует компактный wire-формат; GUI получает разобранные поля.
* encode/decode возвращают длину/0 или -1; неизвестные типы и лишние байты запрещены. */
#ifndef ATTACHMENT_H
#define ATTACHMENT_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
enum attachment_kind { ATTACHMENT_FILE = 1, ATTACHMENT_VOICE, ATTACHMENT_VIDEO };
#define ATTACHMENT_WAVE_SIZE 100
#define ATTACHMENT_WIRE_MAX (11 + 255 + ATTACHMENT_WAVE_SIZE)
struct attachment_info {
enum attachment_kind kind;
char name[256];
uint32_t duration_ms;
uint16_t width, height;
char waveform[ATTACHMENT_WAVE_SIZE + 1]; /* 100 уровней из A-Za-z0-9+/; только voice. */
};
int attachment_validate(const struct attachment_info* info);
int attachment_encode(const struct attachment_info* info, uint8_t* out, size_t capacity);
int attachment_decode(const uint8_t* data, size_t length, struct attachment_info* info);
const char* attachment_content_type(enum attachment_kind kind);
#ifdef __cplusplus
}
#endif
#endif

155
src/media_async/attachment_send.c

@ -0,0 +1,155 @@
/* Подготовка не обращается к БД; публикация и выбор транспорта выполняются в uasync. */
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <openssl/rand.h>
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
#include "../../lib/platform_compat.h"
#include "../../lib/audio_compressor.h"
#include "../utun_instance.h"
#include "../chat/chat_core.h"
#include "../chat/chat_event.h"
#include "../dm/dm_media.h"
#include "../video/video.h"
#include "media_async.h"
#include "voice_file.h"
#include "attachment_send.h"
/* PCM освобождается в worker после кодирования, до callback. */
static void attachment_release_pcm(struct attachment_send_req* req) {
if (req->pcm_release) req->pcm_release(req->pcm_owner);
else u_free((void*)req->pcm);
req->pcm = NULL; req->pcm_owner = NULL; req->pcm_release = NULL;
audio_compressor_destroy(req->compressor); req->compressor = NULL;
}
/* Только пути собственного output; исходный пользовательский файл не удаляется. */
static void attachment_prepare_work(void* arg) {
struct attachment_send_req* req = arg;
req->error = -1;
char parent[1024]; snprintf(parent, sizeof(parent), "%s", req->output);
for (char* p = parent + 1; *p; p++) {
if (*p != '/') continue;
*p = 0;
int rc = utun_mkdir(parent, 0700);
*p = '/';
if (rc && errno != EEXIST) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: mkdir failed path=%s", parent); goto done; }
}
if (req->info.kind == ATTACHMENT_VOICE) {
const int16_t* pcm = req->pcm; size_t count = req->pcm_count;
if (req->compressor) {
if (audio_compressor_flush(req->compressor) != 0) goto done;
pcm = audio_compressor_output(req->compressor); count = audio_compressor_output_size(req->compressor);
}
req->error = voice_file_encode(pcm, count, 48000, 1, req->preset, req->output, &req->info);
} else if (req->transcode) req->error = 0; /* FFmpeg worker стартует после создания каталога. */
else {
req->error = ma_copy_file(req->source, req->output);
}
done:
if (req->temporary_source && !req->transcode && remove(req->source) && errno != ENOENT) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: source cleanup failed path=%s", req->source); req->error = -1;
}
attachment_release_pcm(req);
if (req->error && remove(req->output) && errno != ENOENT)
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment: partial output cleanup failed path=%s", req->output);
}
/* Один выбор транспорта после общей подготовки; групповой индекс и PM crypto не смешиваются. */
static void attachment_publish(struct attachment_send_req* req, int error) {
attachment_release_pcm(req); /* Submit мог завершиться ошибкой до запуска work. */
if (req->temporary_source && remove(req->source) && errno != ENOENT) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: abandoned source cleanup failed path=%s", req->source);
error = -1;
}
if (error || req->error || attachment_validate(&req->info)) {
chat_event_attachment(req->inst, req->is_dm, req->target, req->id, 2);
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: preparation failed target=%s error=%d worker=%d", req->target, error, req->error);
if (remove(req->output) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment: output cleanup failed path=%s", req->output);
u_free(req); return;
}
chat_event_attachment(req->inst, req->is_dm, req->target, req->id, 1);
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment: prepared target=%s pm=%d kind=%d duration=%u dimensions=%ux%u",
req->target, req->is_dm, req->info.kind, req->info.duration_ms, req->info.width, req->info.height);
if (req->is_dm) {
struct dm_file_req* file = u_calloc(1, sizeof(*file));
if (file) {
file->inst = req->inst; file->info = req->info; file->remove_source = 1;
snprintf(file->conv, sizeof(file->conv), "%s", req->target);
snprintf(file->path, sizeof(file->path), "%s", req->output);
dm_send_file_trampoline(file);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: PM dispatch allocation failed");
chat_event_attachment(req->inst, req->is_dm, req->target, req->id, 2);
if (remove(req->output)) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment: rejected output cleanup failed");
}
} else {
char text[384];
if (req->info.kind == ATTACHMENT_VOICE) snprintf(text, sizeof(text), "wf=%s;dur=%u;", req->info.waveform, req->info.duration_ms);
else snprintf(text, sizeof(text), "%s", req->info.name);
size_t length = strlen(text);
struct chat_msg_submit* message = u_calloc(1, sizeof(*message) + length);
if (message) {
message->inst = req->inst;
snprintf(message->channel_id, sizeof(message->channel_id), "%s", req->target);
snprintf(message->content_type, sizeof(message->content_type), "%s", attachment_content_type(req->info.kind));
snprintf(message->media_src, sizeof(message->media_src), "%s", req->output);
snprintf(message->media_dest, sizeof(message->media_dest), "%s", req->output);
message->duration_ms = req->info.duration_ms; message->width = req->info.width; message->height = req->info.height;
message->data = (uint8_t*)(message + 1); message->data_len = (uint32_t)length; memcpy(message->data, text, length);
chat_core_submit_trampoline(message);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: channel dispatch allocation failed");
chat_event_attachment(req->inst, req->is_dm, req->target, req->id, 2);
if (remove(req->output)) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment: rejected output cleanup failed");
}
}
u_free(req);
}
static void attachment_video_done(void* arg, int error, const struct video_info* info) {
struct attachment_send_req* req = arg;
if (!error && info && info->is_video && info->duration_ms > 0 && info->duration_ms <= 86400000 &&
info->width > 0 && info->width <= 16384 && info->height > 0 && info->height <= 16384) {
req->info.duration_ms = (uint32_t)info->duration_ms;
req->info.width = (uint16_t)info->width; req->info.height = (uint16_t)info->height;
} else req->error = -1;
attachment_publish(req, error);
}
static void attachment_prepare_done(void* arg, int error) {
struct attachment_send_req* req = arg;
if (!error && !req->error && req->transcode) {
if (!video_transcode_start(req->inst->media_async, req->inst->ua, req->source, req->output, attachment_video_done, req)) return;
error = -1;
}
attachment_publish(req, error);
}
/* Request содержит адресата, выбранного до записи/выбора файла. */
void attachment_send_trampoline(void* arg) {
struct attachment_send_req* req = arg;
if (!req) return;
if (!req->inst || !req->inst->chat_core || !req->inst->media_async || !req->target[0] ||
strspn(req->target, "0123456789") != strlen(req->target)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: invalid target/stopped service");
attachment_publish(req, -1); return;
}
uint8_t uuid[16]; char hex[33];
if (RAND_bytes(uuid, sizeof(uuid)) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: UUID generation failed"); attachment_publish(req, -1); return;
}
for (unsigned i = 0; i < 16; i++) snprintf(hex + 2 * i, 3, "%02x", uuid[i]);
memcpy(req->id, uuid, sizeof(req->id));
const char* base = req->inst->config->global.db_path;
int n = snprintf(req->output, sizeof(req->output), "%s/media/%s/%s.%s", base[0] ? base : ".",
req->is_dm ? "pm-prepared" : req->target, hex, req->info.kind == ATTACHMENT_VOICE ? "opus" : "mp4");
if (n < 0 || (size_t)n >= sizeof(req->output)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment: output path too long"); req->output[0] = 0; attachment_publish(req, -1); return;
}
if (!req->info.name[0]) snprintf(req->info.name, sizeof(req->info.name), "%s.%s", hex,
req->info.kind == ATTACHMENT_VOICE ? "opus" : "mp4");
chat_event_attachment(req->inst, req->is_dm, req->target, req->id, 0);
media_async_submit(req->inst->media_async, req->inst->ua, attachment_prepare_work, req, attachment_prepare_done, req);
}

32
src/media_async/attachment_send.h

@ -0,0 +1,32 @@
/* Подготовить голос/видео и отправить в зафиксированную беседу. Вызывать trampoline
* через uasync_post. Request и PCM/compressor переходят во владение задачи;
* pcm_release освобождает pcm_owner, NULL означает PCM из u_malloc.
* Qt/Android используют общий результат подготовки, доставка остаётся в chat/dm. */
#ifndef ATTACHMENT_SEND_H
#define ATTACHMENT_SEND_H
#include "attachment.h"
struct UTUN_INSTANCE;
struct audio_compressor;
#ifdef __cplusplus
extern "C" {
#endif
struct attachment_send_req {
struct UTUN_INSTANCE* inst;
char target[64];
int is_dm;
struct attachment_info info;
uint8_t id[16];
char source[1024], output[1024];
int transcode, temporary_source;
const int16_t* pcm;
size_t pcm_count;
void* pcm_owner;
void (*pcm_release)(void* owner);
struct audio_compressor* compressor;
int preset, error;
};
void attachment_send_trampoline(void* arg);
#ifdef __cplusplus
}
#endif
#endif

49
src/media_async/media_async.c

@ -27,12 +27,11 @@ struct ma_thread_ctx {
struct ma_thread_ctx* next;
struct UASYNC* ua;
pthread_t thread;
void* timer;
struct posted_task* completion;
ma_work_fn work;
void* data;
ma_done_fn done;
void* arg;
int finished;
};
static void ma_finish(struct ma_thread_ctx* job, int error) {
@ -44,21 +43,17 @@ static void ma_finish(struct ma_thread_ctx* job, int error) {
u_free(job);
}
static void ma_poll(void* arg) {
struct ma_thread_ctx* job = arg; job->timer = NULL;
if (__atomic_load_n(&job->finished, __ATOMIC_ACQUIRE)) { ma_finish(job, 0); return; }
job->timer = uasync_set_timeout(job->ua, 100, job, ma_poll, "media_worker");
if (!job->timer) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media worker completion timer failed");
ma_finish(job, -1);
}
/* Completion приходит только после работы; ожидать незавершённый worker здесь нельзя. */
static void ma_completed(void* arg) {
struct ma_thread_ctx* job = arg;
job->completion = NULL;
ma_finish(job, 0);
}
static void* ma_thread_entry(void* arg) {
struct ma_thread_ctx* job = arg;
job->work(job->data);
__atomic_store_n(&job->finished, 1, __ATOMIC_RELEASE);
uasync_wakeup(job->ua);
uasync_post_reserved(job->ua, job->completion);
return NULL;
}
@ -75,8 +70,11 @@ void media_async_destroy(struct media_async* ma) {
unsigned cancelled = 0;
while (ma->jobs) {
struct ma_thread_ctx* job = ma->jobs;
if (job->timer) uasync_cancel_timeout(job->ua, job->timer);
ma_finish(job, MEDIA_ASYNC_CANCELLED); cancelled++;
pthread_join(job->thread, NULL);
uasync_cancel_post(job->ua, job->completion);
ma->jobs = job->next;
job->done(job->arg, MEDIA_ASYNC_CANCELLED);
u_free(job); cancelled++;
}
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media_async: destroyed cancelled=%u", cancelled);
u_free(ma);
@ -89,15 +87,20 @@ void media_async_submit(struct media_async* ma, struct UASYNC* ua,
if (done) done(arg, MEDIA_ASYNC_CANCELLED);
return;
}
unsigned active = 0;
for (struct ma_thread_ctx* pending = ma->jobs; pending; pending = pending->next) active++;
if (active >= 16) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media task limit reached active=%u", active); done(arg, -1); return; }
struct ma_thread_ctx* job = u_calloc(1, sizeof(*job));
if (!job) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media task allocation failed"); done(arg, -1); return; }
job->owner = ma; job->ua = ua; job->work = work; job->data = data; job->done = done; job->arg = arg;
job->timer = uasync_set_timeout(ua, 1, job, ma_poll, "media_worker");
if (!job->timer) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media task timer failed"); u_free(job); done(arg, -1); return; }
job->completion = u_calloc(1, sizeof(*job->completion));
if (!job->completion) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media completion allocation failed"); u_free(job); done(arg, -1); return; }
job->completion->callback = ma_completed;
job->completion->arg = job;
int rc = pthread_create(&job->thread, NULL, ma_thread_entry, job);
if (rc) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media task thread failed rc=%d", rc);
uasync_cancel_timeout(ua, job->timer); u_free(job); done(arg, -1); return;
u_free(job->completion); u_free(job); done(arg, -1); return;
}
job->next = ma->jobs; ma->jobs = job;
}
@ -114,8 +117,9 @@ int ma_sha256_file(const char* path, uint8_t hash_out[32]) {
uint8_t buf[65536];
size_t rd;
while ((rd = fread(buf, 1, sizeof(buf), f)) > 0) SHA256_Update(&ctx, buf, rd);
fclose(f);
int error = ferror(f);
if (fclose(f)) error = 1;
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_sha256_file: read/close failed path=%s", path); return -1; }
SHA256_Final(hash_out, &ctx);
return 0;
}
@ -162,8 +166,11 @@ int ma_copy_file(const char* src, const char* dst) {
fclose(s); fclose(d); return -1;
}
}
fclose(s); fclose(d);
return 0;
int error = ferror(s);
if (fclose(s)) error = 1;
if (fclose(d)) error = 1;
if (error) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_copy_file: read/close failed src=%s dst=%s", src, dst);
return error ? -1 : 0;
}
int ma_file_size(const char* path) {

7
src/media_async/media_async.h

@ -17,15 +17,16 @@ struct UASYNC;
struct media_async;
typedef void (*ma_work_fn)(void* data); // рабочий поток; не обращаться к состоянию uasync
typedef void (*ma_done_fn)(void* arg, int err); // 0=work завершён, -1=ошибка запуска/таймера, -2=отмена
typedef void (*ma_done_fn)(void* arg, int err); // 0=work завершён, -1=ошибка запуска, -2=отмена
/* Создать владельца задач; NULL при ошибке выделения памяти. */
struct media_async* media_async_create(void);
#define MEDIA_ASYNC_CANCELLED (-2)
/* Вызывать вне done. Ждёт работы, вызывает ожидающие done и освобождает ma; NULL допустим. */
/* Вызывать вне callbacks uasync. Ждёт работы, отменяет posted completion,
* вызывает ожидающие done и освобождает ma; NULL допустим. */
void media_async_destroy(struct media_async* ma);
/* Передать задачу; data/arg остаются у вызывающего до done. При ошибке done
/* Передать задачу (не более 16 активных); data/arg остаются у вызывающего до done. При ошибке done
* может быть вызван синхронно. Ошибку самой work передают через data, а не err. */
void media_async_submit(struct media_async* ma, struct UASYNC* ua,
ma_work_fn work, void* data,

62
src/media_async/voice_file.c

@ -0,0 +1,62 @@
/* Общий кодек файла и waveform без зависимости от GUI, беседы или сети. */
#include <stdio.h>
#include <string.h>
#include <math.h>
#include "../../lib/debug_config.h"
#include "../../lib/opus_codec.h"
#include "voice_file.h"
int voice_file_encode(const int16_t* pcm, size_t count, int rate, int channels, int preset,
const char* path, struct attachment_info* info) {
static const int bitrates[] = {16000, 32000, 64000}, complexities[] = {2, 5, 10};
static const char levels[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
if (!pcm || !info || !path || (rate != 8000 && rate != 12000 && rate != 16000 && rate != 24000 && rate != 48000) ||
(channels != 1 && channels != 2) || preset < 0 || preset > 2) goto invalid;
size_t frame = (size_t)(rate / 50) * channels, samples = count / frame * frame;
if (samples < (size_t)rate * channels / 2 || samples / channels / rate > 86400) goto invalid;
opus_codec_encoder_t* encoder = opus_codec_encoder_create(rate, channels);
if (!encoder) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice_file: encoder creation failed"); return -1; }
opus_codec_encoder_bitrate_set(encoder, bitrates[preset]);
opus_codec_encoder_complexity_set(encoder, complexities[preset]);
FILE* file = fopen(path, "wb");
int error = -1;
if (!file) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice_file: output open failed path=%s", path); goto done; }
uint32_t magic = 0x5355504f, sample_rate = (uint32_t)rate;
uint16_t channel_count = (uint16_t)channels, frame_samples = (uint16_t)(rate / 50);
if (fwrite(&magic, 4, 1, file) != 1 || fwrite(&sample_rate, 4, 1, file) != 1 ||
fwrite(&channel_count, 2, 1, file) != 1 || fwrite(&frame_samples, 2, 1, file) != 1) goto done;
uint8_t packet[4000];
for (size_t offset = 0; offset < samples; offset += frame) {
int n = opus_codec_encode(encoder, pcm + offset, frame_samples, packet, sizeof(packet));
if (n <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice_file: encode failed offset=%zu result=%d", offset, n); goto done; }
uint16_t length = (uint16_t)n;
if (fwrite(&length, 2, 1, file) != 1 || fwrite(packet, 1, (size_t)n, file) != (size_t)n) goto done;
}
uint16_t end = 0;
if (fwrite(&end, 2, 1, file) != 1 || fflush(file)) goto done;
info->kind = ATTACHMENT_VOICE; info->duration_ms = (uint32_t)(samples / frame * 20);
info->width = info->height = 0;
for (size_t i = 0; i < 100; i++) {
size_t first = i * samples / 100, last = (i + 1) * samples / 100;
double sum = 0;
for (size_t j = first; j < last; j++) { double v = pcm[j] / 32768.0; sum += v * v; }
double rms = sqrt(sum / (last - first)), db = rms > 0.00001 ? 20 * log10(rms) : -100;
int level = (int)((db + 60) * 64 / 60);
if (level < 0) level = 0;
if (level > 63) level = 63;
info->waveform[i] = levels[level];
}
info->waveform[100] = 0;
error = 0;
done:
if (file && fclose(file)) error = -1;
opus_codec_encoder_destroy(encoder);
if (error) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice_file: encoding/write failed path=%s", path);
if (file && remove(path)) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice_file: partial file cleanup failed path=%s", path);
} else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "voice_file: prepared path=%s duration=%ums rate=%d channels=%d", path, info->duration_ms, rate, channels);
return error;
invalid:
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice_file: invalid/short recording count=%zu rate=%d channels=%d preset=%d", count, rate, channels, preset);
return -1;
}

15
src/media_async/voice_file.h

@ -0,0 +1,15 @@
/* Существующий контейнер Opus голосовых Qt/Android. Вызывать только в worker.
* PCM принадлежит вызывающему; encode заполняет duration/waveform по реально
* записанным кадрам. При ошибке неполный файл удаляется. */
#ifndef VOICE_FILE_H
#define VOICE_FILE_H
#include "attachment.h"
#ifdef __cplusplus
extern "C" {
#endif
int voice_file_encode(const int16_t* pcm, size_t count, int rate, int channels, int preset,
const char* path, struct attachment_info* info);
#ifdef __cplusplus
}
#endif
#endif

343
src/media_delivery/file_transfer.c

@ -0,0 +1,343 @@
/* file_transfer — signed router stream с ограниченной очередью и CM-владением. */
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <openssl/rand.h>
#include "../../lib/mem.h"
#include "../../lib/platform_compat.h"
#include "../../lib/debug_config.h"
#include "file_transfer.h"
#include "../utun_instance.h"
#include "../routing_layer/conn_mgr.h"
#include "../routing_layer/etcp_router.h"
#include "../transport_layer/etcp.h"
#define FT_REQUEST 1 /* tid:16, object:16, size:8, offset:8 */
#define FT_DATA 2 /* tid:16, offset:8, bytes */
#define FT_DONE 3 /* tid:16 */
#define FT_REFUSE 4 /* tid:16 */
#define FT_CHUNK 8192u /* Вместе с подписью и полным путём помещается в кодограмму router (16 КиБ). */
#define FT_MAX_OPS 16
struct file_transfer {
struct UTUN_INSTANCE* inst;
file_transfer_lookup_fn lookup;
void* arg;
struct file_transfer_op* ops;
unsigned count;
int closing;
};
struct file_transfer_op {
struct file_transfer_op* next;
struct file_transfer* ft;
uint64_t group, peer, size, offset, progress_tb, request_tb;
uint8_t tid[16], object[16];
int receiving, up;
FILE* file;
struct CONN_MGR_HANDLE* cm;
struct queue_waiter_handle waiter;
void* timer;
void* pump;
file_transfer_done_fn done;
void* arg;
};
/* Очистить все локальные триггеры до callback владельца. */
static void ft_finish(struct file_transfer_op* op, int error) {
struct file_transfer* ft = op->ft;
if (op->timer) uasync_cancel_timeout(ft->inst->ua, op->timer);
if (op->pump) uasync_call_soon_cancel(ft->inst->ua, op->pump);
etcp_router_cancel_send_ready(ft->inst, op->group, op->peer, ETCP_RT_ID_FILE_TRANSFER, &op->waiter);
if (op->cm) conn_mgr_close(op->cm);
if (op->file && fclose(op->file) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: close failed peer=%llu: %s",
(unsigned long long)op->peer, strerror(errno));
error = -1;
}
struct file_transfer_op** p = &ft->ops;
while (*p && *p != op) p = &(*p)->next;
if (*p) *p = op->next;
ft->count--;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file_transfer: finish peer=%llu receive=%d bytes=%llu/%llu error=%d active=%u",
(unsigned long long)op->peer, op->receiving, (unsigned long long)op->offset,
(unsigned long long)op->size, error, ft->count);
file_transfer_done_fn done = op->done;
void* arg = op->arg;
u_free(op);
if (done) done(arg, error);
}
/* Отправить подписанную кодограмму, не превышая порог очереди. */
static int ft_send(struct file_transfer* ft, uint64_t group, uint64_t peer,
uint8_t cmd, const uint8_t* data, size_t size) {
struct ETCP_ROUTER_CONN* rc = etcp_router_conn_get(ft->inst, group, peer, ETCP_RT_ID_FILE_TRANSFER);
if (!rc) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: router allocation failed"); return -1; }
queue_set_threshold(rc->send_q, 0, 0);
etcp_router_set_max_inflight(rc, 16);
if (!etcp_router_send_q_has_room(ft->inst, group, peer, ETCP_RT_ID_FILE_TRANSFER)) return 1;
uint8_t packet[1 + 24 + FT_CHUNK];
packet[0] = cmd;
memcpy(packet + 1, data, size);
int error = etcp_router_conn_send(rc, packet, size + 1, ROUTER_FLAG_SIGNED);
if (error) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file_transfer: send failed peer=%llu cmd=%u rc=%d",
(unsigned long long)peer, cmd, error);
return error ? -1 : 0;
}
/* Повторный REQUEST возобновляет приём со следующего неполученного байта. */
static void ft_request(struct file_transfer_op* op) {
uint8_t body[48];
memcpy(body, op->tid, 16);
memcpy(body + 16, op->object, 16);
memcpy(body + 32, &op->size, 8);
memcpy(body + 40, &op->offset, 8);
if (ft_send(op->ft, op->group, op->peer, FT_REQUEST, body, sizeof(body)) == 0)
op->request_tb = get_time_tb();
}
static void ft_pump(void* arg);
/* Waiter может сработать синхронно; продолжение всегда через call_soon. */
static void ft_ready(struct ll_queue* q, void* arg) {
(void)q;
struct file_transfer_op* op = arg;
if (op->pump) return;
op->pump = uasync_call_soon(op->ft->inst->ua, op, ft_pump);
if (!op->pump) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: pump timer allocation failed");
}
/* Ограничить работу одного callback и количество байтов в router. */
static void ft_pump(void* arg) {
struct file_transfer_op* op = arg;
op->pump = NULL;
if (!op->up || op->receiving) return;
struct file_transfer* ft = op->ft;
if (op->offset < op->size) {
if (!etcp_router_send_q_has_room(ft->inst, op->group, op->peer, ETCP_RT_ID_FILE_TRANSFER)) {
etcp_router_on_send_ready(ft->inst, op->group, op->peer, ETCP_RT_ID_FILE_TRANSFER, &op->waiter, ft_ready, op);
return;
}
uint8_t body[24 + FT_CHUNK];
size_t n = op->size - op->offset < FT_CHUNK ? (size_t)(op->size - op->offset) : FT_CHUNK;
memcpy(body, op->tid, 16);
memcpy(body + 16, &op->offset, 8);
if (fseeko(op->file, (off_t)op->offset, SEEK_SET) != 0 || fread(body + 24, 1, n, op->file) != n) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: read failed peer=%llu offset=%llu: %s",
(unsigned long long)op->peer, (unsigned long long)op->offset, strerror(errno));
ft_finish(op, -1);
return;
}
int error = ft_send(ft, op->group, op->peer, FT_DATA, body, 24 + n);
if (error < 0) { ft_finish(op, -1); return; }
if (error > 0) return;
op->offset += n;
op->progress_tb = get_time_tb();
}
if (op->offset < op->size) ft_ready(NULL, op);
}
/* DOWN/TIMEOUT сохраняет CM handle; таймер проверяет позднее восстановление. */
static void ft_connected(struct CONN_MGR_HANDLE* h, uint64_t peer, uint64_t group,
enum conn_mgr_event event, void* arg) {
(void)h; (void)peer; (void)group;
struct file_transfer_op* op = arg;
op->up = event == CONN_EVENT_UP;
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "file_transfer: CM event=%d peer=%llu receive=%d offset=%llu",
event, (unsigned long long)op->peer, op->receiving, (unsigned long long)op->offset);
if (op->up) {
if (op->receiving) ft_request(op);
else ft_ready(NULL, op);
}
}
/* Неактивная операция завершается; durable владелец решает, когда повторять. */
static void ft_tick(void* arg) {
struct file_transfer_op* op = arg;
op->timer = NULL;
uint64_t now = get_time_tb();
if (now - op->progress_tb >= 300000) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file_transfer: inactivity timeout peer=%llu offset=%llu/%llu",
(unsigned long long)op->peer, (unsigned long long)op->offset, (unsigned long long)op->size);
ft_finish(op, -1);
return;
}
if (op->up) {
if (op->receiving && now - op->request_tb >= 50000 && now - op->progress_tb >= 50000) ft_request(op);
else if (!op->receiving && !op->pump && !op->waiter.internal) ft_ready(NULL, op);
}
op->timer = uasync_set_timeout(op->ft->inst->ua, 10000, op, ft_tick, "file_transfer");
if (!op->timer) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: watchdog allocation failed"); ft_finish(op, -1); }
}
/* Добавить операцию до conn_mgr_open; callbacks CM используют уже живой owner. */
static struct file_transfer_op* ft_open(struct file_transfer* ft, uint64_t group, uint64_t peer,
const uint8_t object[16], uint64_t size, const char* path, int receiving) {
if (!ft || ft->closing || !group || !peer || !object || !path || !size || size > INT64_MAX || ft->count >= FT_MAX_OPS) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file_transfer: invalid request or operation limit size=%llu",
(unsigned long long)size);
return NULL;
}
struct file_transfer_op* op = u_calloc(1, sizeof(*op));
if (!op) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: operation allocation failed"); return NULL; }
op->ft = ft; op->group = group; op->peer = peer; op->size = size; op->receiving = receiving;
memcpy(op->object, object, 16);
op->file = fopen(path, receiving ? "wb" : "rb");
if (!op->file) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: open failed path=%s: %s", path, strerror(errno));
u_free(op);
return NULL;
}
op->next = ft->ops; ft->ops = op; ft->count++;
op->progress_tb = get_time_tb();
op->timer = uasync_set_timeout(ft->inst->ua, 10000, op, ft_tick, "file_transfer");
if (!op->timer || !etcp_router_conn_get(ft->inst, group, peer, ETCP_RT_ID_FILE_TRANSFER) ||
conn_mgr_open(ft->inst, group, peer, ft_connected, op, &op->cm) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: CM/timer launch failed peer=%llu", (unsigned long long)peer);
ft_finish(op, -1);
return NULL;
}
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file_transfer: start peer=%llu receive=%d size=%llu active=%u",
(unsigned long long)peer, receiving, (unsigned long long)size, ft->count);
return op;
}
static struct file_transfer_op* ft_find(struct file_transfer* ft, uint64_t group, uint64_t peer,
const uint8_t tid[16], int receiving) {
for (struct file_transfer_op* op = ft->ops; op; op = op->next)
if (op->group == group && op->peer == peer && op->receiving == receiving && !memcmp(op->tid, tid, 16)) return op;
return NULL;
}
/* REQUEST проверяется владельцем файла при каждом повторе, включая TTL/права. */
static void ft_serve(struct file_transfer* ft, uint64_t group, uint64_t peer, const uint8_t* p) {
uint64_t size, offset;
memcpy(&size, p + 32, 8);
memcpy(&offset, p + 40, 8);
char path[1024];
if (!size || offset >= size || ft->lookup(ft->arg, group, peer, p + 16, size, path, sizeof(path)) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file_transfer: request refused peer=%llu size=%llu offset=%llu",
(unsigned long long)peer, (unsigned long long)size, (unsigned long long)offset);
ft_send(ft, group, peer, FT_REFUSE, p, 16);
return;
}
struct file_transfer_op* op = ft_find(ft, group, peer, p, 0);
if (op && (op->size != size || memcmp(op->object, p + 16, 16))) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file_transfer: conflicting transfer ID peer=%llu", (unsigned long long)peer);
return;
}
if (!op) {
op = ft_open(ft, group, peer, p + 16, size, path, 0);
if (!op) { ft_send(ft, group, peer, FT_REFUSE, p, 16); return; }
memcpy(op->tid, p, 16);
}
op->offset = offset;
op->progress_tb = get_time_tb();
if (op->up) ft_ready(NULL, op);
}
/* Signed source, transfer ID и точные границы отделяют операции и повторы. */
static void ft_recv(struct ETCP_CONN* conn, struct ll_entry* e) {
if (!e || !conn || !e->dgram || e->len <= ROUTER_SVC_PAYLOAD_OFF) goto done;
struct file_transfer* ft = conn->instance->file_transfer;
if (!ft || ft->closing) goto done;
uint64_t group, peer;
memcpy(&group, e->dgram + ROUTER_SVC_GROUP_OFF, 8);
memcpy(&peer, e->dgram + ROUTER_SVC_SRC_OFF, 8);
if (!(e->dgram[ROUTER_SVC_FLAGS_OFF] & ROUTER_FLAG_SIGNED)) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file_transfer: unsigned packet peer=%llu", (unsigned long long)peer);
goto done;
}
const uint8_t* p = e->dgram + ROUTER_SVC_PAYLOAD_OFF;
uint8_t cmd = *p++;
size_t len = e->len - ROUTER_SVC_PAYLOAD_OFF - 1;
if (cmd == FT_REQUEST && len == 48) ft_serve(ft, group, peer, p);
else if (cmd == FT_DONE && len == 16) {
struct file_transfer_op* op = ft_find(ft, group, peer, p, 0);
if (op && op->offset == op->size) ft_finish(op, 0);
} else if (cmd == FT_REFUSE && len == 16) {
struct file_transfer_op* op = ft_find(ft, group, peer, p, 1);
if (op) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file_transfer: source refused peer=%llu", (unsigned long long)peer); ft_finish(op, -1); }
} else if (cmd == FT_DATA && len > 24 && len <= 24 + FT_CHUNK) {
struct file_transfer_op* op = ft_find(ft, group, peer, p, 1);
if (!op) goto done; /* Поздний пакет завершённой/отменённой операции. */
uint64_t offset;
memcpy(&offset, p + 16, 8);
size_t n = len - 24;
if (offset < op->offset && n <= op->offset - offset) goto done;
if (offset != op->offset || n > op->size - op->offset) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file_transfer: wrong range peer=%llu offset=%llu expected=%llu bytes=%zu",
(unsigned long long)peer, (unsigned long long)offset, (unsigned long long)op->offset, n);
ft_request(op);
goto done;
}
if (fwrite(p + 24, 1, n, op->file) != n) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: write failed peer=%llu: %s", (unsigned long long)peer, strerror(errno));
ft_finish(op, -1);
goto done;
}
op->offset += n;
op->progress_tb = get_time_tb();
if (op->offset == op->size) {
ft_send(ft, group, peer, FT_DONE, op->tid, 16);
ft_finish(op, 0);
}
} else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file_transfer: malformed packet cmd=%u bytes=%zu", cmd, len);
done:
if (e) { queue_dgram_free(e); queue_entry_free(e); }
}
struct file_transfer* file_transfer_create(struct UTUN_INSTANCE* inst, file_transfer_lookup_fn lookup, void* arg) {
if (!inst || !lookup || inst->file_transfer) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: invalid initialization");
return NULL;
}
struct file_transfer* ft = u_calloc(1, sizeof(*ft));
if (!ft) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: owner allocation failed"); return NULL; }
ft->inst = inst; ft->lookup = lookup; ft->arg = arg;
if (etcp_router_bind(inst, ETCP_RT_ID_FILE_TRANSFER, ft_recv) != 0) { u_free(ft); return NULL; }
inst->file_transfer = ft;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file_transfer: initialized max_operations=%u", FT_MAX_OPS);
return ft;
}
void file_transfer_destroy(struct file_transfer* ft) {
if (!ft) return;
ft->closing = 1;
etcp_router_unbind(ft->inst, ETCP_RT_ID_FILE_TRANSFER);
while (ft->ops) ft_finish(ft->ops, -2);
ft->inst->file_transfer = NULL;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file_transfer: destroyed");
u_free(ft);
}
struct file_transfer_op* file_transfer_pull(struct file_transfer* ft, uint64_t group, uint64_t peer,
const uint8_t object[16], uint64_t size, const char* path,
file_transfer_done_fn done, void* arg) {
uint8_t tid[16];
if (!done || RAND_bytes(tid, sizeof(tid)) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file_transfer: transfer ID generation/arguments failed");
return NULL;
}
struct file_transfer_op* op = ft_open(ft, group, peer, object, size, path, 1);
if (!op) return NULL;
memcpy(op->tid, tid, sizeof(tid));
op->done = done; op->arg = arg;
return op;
}
void file_transfer_cancel(struct file_transfer_op* op) {
if (op) ft_finish(op, -2);
}
void file_transfer_cancel_object(struct file_transfer* ft, const uint8_t object[16]) {
if (!ft) return;
for (;;) {
struct file_transfer_op* found = NULL;
for (struct file_transfer_op* op = ft->ops; op; op = op->next)
if (!memcmp(op->object, object, 16)) { found = op; break; }
if (!found) break;
ft_finish(found, -2);
}
}

34
src/media_delivery/file_transfer.h

@ -0,0 +1,34 @@
/* file_transfer — передача неизменяемого файла между явно заданными узлами.
* Не знает о чатах, ключах содержимого, custody или БД. lookup авторизует запрос
* и возвращает путь; получатель проверяет содержимое в своём done.
* Каждая операция владеет CM handle до завершения/отмены, включая DOWN/TIMEOUT.
* Все API/callbacks в uasync; destroy вызывается вне callbacks, до удаления групп.
* Файл назначения принадлежит вызывающему; при ошибке его нужно отбросить.
*/
#ifndef FILE_TRANSFER_H
#define FILE_TRANSFER_H
#include <stdint.h>
#include <stddef.h>
struct UTUN_INSTANCE;
struct file_transfer;
struct file_transfer_op;
typedef int (*file_transfer_lookup_fn)(void* arg, uint64_t group, uint64_t peer,
const uint8_t object[16], uint64_t size, char* path, size_t cap);
typedef void (*file_transfer_done_fn)(void* arg, int error);
/* 0 в done означает точный приём size байт и закрытие файла, а не custody ACK.
* Возврат NULL означает отказ запуска, done не вызывается. cancel вызывает done(-2).
* Handle перестаёт существовать перед вызовом done, его нужно обнулить в callback. */
struct file_transfer* file_transfer_create(struct UTUN_INSTANCE* inst, file_transfer_lookup_fn lookup, void* arg);
void file_transfer_destroy(struct file_transfer* ft);
struct file_transfer_op* file_transfer_pull(struct file_transfer* ft, uint64_t group, uint64_t peer,
const uint8_t object[16], uint64_t size, const char* destination,
file_transfer_done_fn done, void* arg);
void file_transfer_cancel(struct file_transfer_op* op);
/* Отозвать все операции объекта (TTL/delete); callbacks приёма вызываются с -2. */
void file_transfer_cancel_object(struct file_transfer* ft, const uint8_t object[16]);
#endif

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

513
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;
@ -67,6 +73,10 @@ struct radio_source {
};
static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER;
/* TX → общий mutex. На время VAD общий mutex отпускается; TX сохраняет lifetime и порядок PCM/PTT. */
static pthread_mutex_t g_tx_mtx = PTHREAD_MUTEX_INITIALIZER;
static void radio_tx_lock(void) { pthread_mutex_lock(&g_tx_mtx); pthread_mutex_lock(&g_mtx); }
static void radio_tx_unlock(void) { pthread_mutex_unlock(&g_mtx); pthread_mutex_unlock(&g_tx_mtx); }
static struct UTUN_INSTANCE* g_inst = NULL;
static int g_active = 0;
static uint64_t g_group_id = 0;
@ -77,13 +87,91 @@ 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 uint64_t g_vad_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; /* дропнутых хвостовых сэмплов за интервал */
static uint32_t g_tx_tail = 0; /* нулевых отсчётов дополнения хвоста за интервал */
/* Один TX-автомат и отдельный накопитель сетевого кадра; AEC выполняется до этого API. */
static int g_transmitting, g_tx_pcm_frames, g_vad_decimation;
static int16_t g_tx_pcm[RADIO_AUDIO_MIX_SAMPLES];
static float g_vad_sum;
static uint64_t g_tx_burst;
/* Только uasync: идентичность открытой сетевой серии, включая поколение capture. */
static uint64_t g_net_generation, g_net_burst, g_net_group;
static uint64_t g_tx_generation;
static uint64_t g_listen_generation;
static uint64_t g_instance_generation;
static unsigned int g_control_pending;
static int g_control_warned;
static struct radio_audio_talk* g_reserved_fin;
static unsigned int g_tx_pending, g_tx_dropped, g_tx_reported;
struct radio_audio_tx {
struct posted_task task;
struct UTUN_INSTANCE* inst;
uint64_t group_id, generation, listen_generation, instance_generation, burst, created_tb;
int len;
uint8_t opus[RADIO_MAX_OPUS];
};
static void radio_audio_send(void* arg) {
struct radio_audio_tx* tx = arg;
pthread_mutex_lock(&g_mtx);
int current = tx->generation == g_tx_generation;
int owner = tx->instance_generation == g_instance_generation && g_inst == tx->inst;
if (owner && g_tx_pending) --g_tx_pending;
int send = owner && tx->listen_generation == g_listen_generation && g_active && g_group_id == tx->group_id &&
g_net_generation == tx->generation && g_net_burst == tx->burst && g_net_group == tx->group_id;
if (get_time_tb() - tx->created_tb > 2000) { send = 0; if (current) ++g_tx_dropped; }
unsigned int dropped = g_tx_dropped, pending = g_tx_pending;
int changed = dropped != g_tx_reported;
g_tx_reported = dropped;
uint32_t frames = 0, bytes = 0, tail = 0;
uint64_t now = get_time_tb();
if (now - g_tx_stats_tb >= RADIO_AUDIO_STATS_TB) {
frames = g_tx_frames; bytes = g_tx_bytes; tail = g_tx_tail;
g_tx_frames = g_tx_bytes = g_tx_tail = 0; g_tx_stats_tb = now;
}
pthread_mutex_unlock(&g_mtx);
if (changed) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: TX dropped=%u pending=%u limit=8 max_age=200ms", RADIO_AUDIO_ID,
dropped, pending);
}
if (frames) DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: TX frames=%u bytes=%u tail_padding_samples=%u", RADIO_AUDIO_ID, frames, bytes, tail);
if (send) radio_talk_send(tx->inst, tx->group_id, tx->opus, tx->len);
}
/* Вызывается под g_mtx; post и FIN упорядочены тем же mutex, задачи не накапливаются бесконечно. */
static int radio_audio_post_frame(struct UTUN_INSTANCE* inst, uint64_t group_id, const uint8_t* opus, int len) {
if (!inst || !inst->ua || g_tx_pending >= 8) { ++g_tx_dropped; return -1; }
struct radio_audio_tx* task = u_calloc(1, sizeof(*task));
if (!task) { ++g_tx_dropped; return -1; }
task->inst = inst; task->group_id = group_id;
task->generation = g_tx_generation; task->burst = g_tx_burst; task->created_tb = get_time_tb();
task->listen_generation = g_listen_generation;
task->instance_generation = g_instance_generation;
task->len = len;
memcpy(task->opus, opus, (size_t)len);
task->task.callback = radio_audio_send; task->task.arg = task;
++g_tx_pending;
uasync_post_reserved(inst->ua, &task->task);
return 0;
}
/* vj decode-коллбэк: дёргается только из аудио-потока (vj_pull). */
/* rj decode-коллбэк: только единственный playback-поток. */
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 +179,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 +192,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 +213,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 +228,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 +272,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 +286,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 +301,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 +313,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,12 +327,18 @@ static struct radio_source* radio_src_acquire(uint64_t src, uint16_t stream) {
return NULL;
}
static int radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, int begin);
static void radio_audio_update_transmission(void);
static void radio_audio_flush_tx(void);
/* ── lifecycle ── */
int radio_audio_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (g_inst == inst) return 0;
g_inst = inst;
pthread_mutex_lock(&g_mtx);
if (g_inst == inst) { pthread_mutex_unlock(&g_mtx); return 0; }
g_inst = inst; g_tx_pending = g_control_pending = 0; ++g_listen_generation; ++g_instance_generation;
pthread_mutex_unlock(&g_mtx);
radio_set_frame_cb(inst, radio_audio_on_frame, NULL);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: initialized", RADIO_AUDIO_ID);
return 0;
@ -253,27 +347,52 @@ int radio_audio_init(struct UTUN_INSTANCE* inst) {
void radio_audio_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || g_inst != inst) return;
radio_set_frame_cb(inst, NULL, NULL);
pthread_mutex_lock(&g_mtx);
g_inst = NULL;
++g_instance_generation;
pthread_mutex_unlock(&g_mtx);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: destroyed", RADIO_AUDIO_ID);
}
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_listen_generation;
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;
}
radio_tx_lock();
if (!g_active || g_group_id != group_id || g_encoder) {
radio_tx_unlock();
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; }
if (!enc) { radio_tx_unlock(); 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);
int opus_bitrate_kbps = chat_setting_get_int(g_inst, "radio_opus_bitrate", RADIO_AUDIO_BITRATE / 1000);
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};
@ -285,27 +404,66 @@ int radio_audio_start(uint64_t group_id, int capture_channels) {
accfg.max_gain_db = (float)chat_setting_get_int(g_inst, "radio_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, "radio_compressor_enabled", 1));
if (audio_compressor_configure(ac, &accfg) != 0) { audio_compressor_destroy(ac); ac = NULL; }
if (ac) audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "radio_compressor_enabled", 1));
}
if (!ac) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: compressor unavailable", RADIO_AUDIO_ID);
/* VAD анализирует текущие кадры без истории захвата. */
g_vad_mode = 0;
if (vad_enabled) {
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);
pthread_mutex_unlock(&g_mtx);
silero_vad_t* vad = silero_vad_create_default();
pthread_mutex_lock(&g_mtx);
g_vad = vad; g_vad_mode = vad != NULL;
if (!g_vad) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: silero_vad_create_default failed — VAD auto-PTT disabled", RADIO_AUDIO_ID);
}
}
if (g_vad_mode) {
g_vad_win_len = 0;
radio_vad_fsm_reset(&g_vad_fsm);
}
g_vad_manual_ptt = g_transmitting = g_tx_pcm_frames = g_vad_decimation = 0;
g_vad_sum = 0; g_tx_burst = 0;
++g_tx_generation; g_tx_dropped = g_tx_reported = 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));
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");
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);
radio_tx_unlock();
return 0;
}
void radio_audio_capture_stop(void) {
radio_tx_lock();
if (g_encoder) {
if (g_transmitting) {
radio_audio_flush_tx();
radio_audio_post_talk(g_inst ? g_inst->ua : NULL, g_group_id, 0);
}
++g_tx_generation;
g_transmitting = g_tx_pcm_frames = 0;
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);
}
radio_tx_unlock();
}
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,20 +471,25 @@ 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);
}
}
void radio_audio_reset_playback(uint64_t group_id) {
pthread_mutex_lock(&g_mtx);
if (g_active && g_group_id == group_id) {
for (int i = 0; i < RADIO_AUDIO_MAX_SOURCES; ++i) radio_src_free(&g_sources[i]);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: playback reset group=%016llx", RADIO_AUDIO_ID, (unsigned long long)group_id);
}
pthread_mutex_unlock(&g_mtx);
}
int radio_audio_active(void) {
int a = 0;
pthread_mutex_lock(&g_mtx);
@ -335,107 +498,210 @@ 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_transmitting;
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*)) {
struct radio_talk_arg* a = (struct radio_talk_arg*)u_malloc(sizeof(*a));
if (!a) return;
a->inst = g_inst;
a->group_id = group_id;
uasync_post(ua, fn, a);
struct radio_audio_talk {
struct posted_task task;
struct UTUN_INSTANCE* inst;
uint64_t group_id, generation, listen_generation, instance_generation, burst, created_tb;
int begin;
};
static void radio_audio_talk(void* arg) {
struct radio_audio_talk* talk = arg;
pthread_mutex_lock(&g_mtx);
int owner = g_inst == talk->inst && g_instance_generation == talk->instance_generation;
if (owner && g_control_pending) --g_control_pending;
int current = owner && talk->listen_generation == g_listen_generation && g_active && g_group_id == talk->group_id;
pthread_mutex_unlock(&g_mtx);
if (talk->begin) {
if (current && get_time_tb() - talk->created_tb <= 2000 && radio_talk_begin(talk->inst, talk->group_id) == 0) {
g_net_generation = talk->generation; g_net_burst = talk->burst; g_net_group = talk->group_id;
}
} else if (g_net_generation == talk->generation && g_net_burst == talk->burst && g_net_group == talk->group_id) {
if (owner) radio_talk_end(talk->inst, talk->group_id);
g_net_generation = g_net_burst = g_net_group = 0;
}
}
/* Не более 16 control-задач, включая заранее выделенный FIN активной серии. */
static int radio_audio_post_talk(struct UASYNC* ua, uint64_t group_id, int begin) {
if (!ua) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: talk post without uasync", RADIO_AUDIO_ID); return -1; }
struct radio_audio_talk* talk;
if (begin) {
if (g_control_pending > 14) {
if (!g_control_warned) DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: TX BEGIN deferred control_pending=%u limit=16",
RADIO_AUDIO_ID, g_control_pending);
g_control_warned = 1; return -1;
}
talk = u_calloc(1, sizeof(*talk));
struct radio_audio_talk* fin = u_calloc(1, sizeof(*fin));
if (!talk || !fin) {
u_free(talk); u_free(fin);
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: BEGIN/FIN allocation failed", RADIO_AUDIO_ID); return -1;
}
g_reserved_fin = fin; g_control_pending += 2; g_control_warned = 0;
} else {
talk = g_reserved_fin; g_reserved_fin = NULL;
if (!talk) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: FIN without reserved task", RADIO_AUDIO_ID); return -1; }
}
talk->inst = g_inst; talk->group_id = group_id; talk->begin = begin;
talk->generation = g_tx_generation; talk->burst = g_tx_burst;
talk->listen_generation = g_listen_generation; talk->created_tb = get_time_tb();
talk->instance_generation = g_instance_generation;
talk->task.callback = radio_audio_talk; talk->task.arg = talk;
uasync_post_reserved(ua, &talk->task);
return 0;
}
void radio_audio_talk_begin(uint64_t group_id) {
if (!g_inst || !g_inst->ua) return;
radio_audio_post_talk(g_inst->ua, group_id, radio_talk_begin_trampoline);
radio_tx_lock();
if (!g_active || group_id != g_group_id || !g_encoder) {
radio_tx_unlock();
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;
radio_audio_update_transmission();
radio_tx_unlock();
}
void radio_audio_talk_end(uint64_t group_id) {
if (!g_inst || !g_inst->ua) return;
radio_audio_post_talk(g_inst->ua, group_id, radio_talk_end_trampoline);
radio_tx_lock();
if (!g_active || group_id != g_group_id || !g_vad_manual_ptt) {
radio_tx_unlock();
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_update_transmission();
radio_tx_unlock();
}
/* ── TX (аудио-поток) ── */
/* ── VAD авто-PTT: helpers (под g_mtx) ── */
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; }
enum { MAX_OUT_FRAMES = 4 };
uint8_t opus[MAX_OUT_FRAMES][RADIO_MAX_OPUS];
int opus_len[MAX_OUT_FRAMES];
int n_frames = 0;
struct UASYNC* ua = NULL;
struct UTUN_INSTANCE* inst = NULL;
if (!g_active || group_id != g_group_id || !g_encoder) {
pthread_mutex_unlock(&g_mtx);
return -1;
}
/* компрессор микрофона: push → слив выходных блоков → encode каждого.
* lookahead=0 ⇒ ровно 1 frame-блок на вход; при disabled — passthrough. */
const int16_t* src = pcm;
size_t src_count = (size_t)count;
/* Закодировать один 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; }
int16_t amplified[RADIO_AUDIO_MIX_SAMPLES];
if (g_compressor) {
audio_compressor_push(g_compressor, pcm, (size_t)count);
src = audio_compressor_output(g_compressor);
src_count = audio_compressor_output_size(g_compressor);
if (audio_compressor_process_frame(g_compressor, frame, amplified,
(size_t)RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: compressor frame failed", RADIO_AUDIO_ID);
return;
}
frame = amplified;
}
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; }
if (radio_audio_post_frame(inst, group_id, opus, l) == 0) { ++g_tx_frames; g_tx_bytes += (uint32_t)l; }
}
size_t off = 0;
while (off + (size_t)frame_total <= src_count && n_frames < MAX_OUT_FRAMES) {
int l = opus_codec_encode(g_encoder, src + off, RADIO_AUDIO_FRAME_SAMPLES,
opus[n_frames], (int)sizeof(opus[n_frames]));
if (l > 0) { opus_len[n_frames] = l; n_frames++; }
off += (size_t)frame_total;
}
if (off != src_count) {
g_tx_tail += (uint32_t)(src_count - off);
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: feed_pcm dropped %zu tail samples (compressor)", RADIO_AUDIO_ID, src_count - off);
}
if (g_compressor) audio_compressor_clear_output(g_compressor);
g_tx_frames += (uint32_t)n_frames;
for (int i = 0; i < n_frames; i++) g_tx_bytes += (uint32_t)opus_len[i];
uint32_t clip = g_compressor ? audio_compressor_clip_count(g_compressor) : 0;
/* Хвост относится только к своей серии. Тишина добавляется исключительно перед Opus. */
static void radio_audio_flush_tx(void) {
if (!g_tx_pcm_frames) return;
int count = g_tx_pcm_frames * g_capture_channels;
g_tx_tail += RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels - count;
memset(g_tx_pcm + count, 0, (size_t)(RADIO_AUDIO_FRAME_SAMPLES * g_capture_channels - count) * sizeof(int16_t));
radio_audio_encode_post(g_group_id, g_tx_pcm, g_inst ? g_inst->ua : NULL, g_inst);
g_tx_pcm_frames = 0;
}
ua = g_inst ? g_inst->ua : NULL;
inst = g_inst;
pthread_mutex_unlock(&g_mtx);
/* Ручной PTT имеет приоритет; смена manual/VAD при открытой серии не создаёт второй BEGIN. */
static void radio_audio_update_transmission(void) {
int desired = g_vad_manual_ptt || (g_vad_mode && g_vad_fsm.talking);
if (desired == g_transmitting) return;
if (!desired) radio_audio_flush_tx();
else { ++g_tx_burst; g_tx_pcm_frames = 0; }
if (radio_audio_post_talk(g_inst ? g_inst->ua : NULL, g_group_id, desired) != 0) return;
g_transmitting = desired;
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: TX %s generation=%llu burst=%llu manual=%d vad=%d", RADIO_AUDIO_ID,
desired ? "BEGIN" : "FIN", (unsigned long long)g_tx_generation, (unsigned long long)g_tx_burst,
g_vad_manual_ptt, g_vad_fsm.talking);
}
if (n_frames == 0) return -1;
if (!ua || !inst) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: feed_pcm: instance not set", RADIO_AUDIO_ID);
return -1;
void radio_audio_capture_discontinuity(uint64_t group_id) {
radio_tx_lock();
if (g_active && g_group_id == group_id && g_encoder) {
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: capture discontinuity tail=%d transmitting=%d", RADIO_AUDIO_ID,
g_tx_pcm_frames, g_transmitting);
radio_audio_flush_tx();
g_vad_win_len = g_vad_decimation = 0; g_vad_sum = 0;
if (g_vad) silero_vad_reset(g_vad);
radio_vad_fsm_reset(&g_vad_fsm);
radio_audio_update_transmission();
}
radio_tx_unlock();
}
/* каденс TX: раз в секунду — кадров/с, средний размер opus-кадра, хвост, клиппинг */
/* VAD читает очищенный сигнал до AGC, накопление 48k→16k непрерывно между PCM-чанками. */
static void radio_audio_vad_sample(const int16_t* frame) {
float mono = g_capture_channels == 2 ? ((float)frame[0] + frame[1]) * 0.5f : frame[0];
g_vad_sum += mono;
if (++g_vad_decimation != 3) return;
g_vad_win[g_vad_win_len++] = g_vad_sum / (3.0f * 32768.0f);
g_vad_decimation = 0; g_vad_sum = 0;
if (g_vad_win_len != RADIO_VAD_WINDOW_16K) return;
float probability = 0;
pthread_mutex_unlock(&g_mtx);
int result = silero_vad_process(g_vad, g_vad_win, &probability);
pthread_mutex_lock(&g_mtx);
if (result != 0)
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "%s: VAD process failed", RADIO_AUDIO_ID);
g_vad_win_len = 0;
uint64_t now = get_time_tb();
if (g_tx_stats_tb == 0 || now - g_tx_stats_tb >= RADIO_AUDIO_STATS_TB) {
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "%s: tx stats frames=%u/s avg=%.0fB/frame tail=%u clip=%u",
RADIO_AUDIO_ID, g_tx_frames,
g_tx_frames ? (double)g_tx_bytes / (double)g_tx_frames : 0.0,
(unsigned)g_tx_tail, (unsigned)clip);
g_tx_stats_tb = now;
g_tx_frames = 0;
g_tx_bytes = 0;
g_tx_tail = 0;
int busy = g_last_rx_tb && now - g_last_rx_tb < RADIO_VAD_BUSY_TB;
radio_vad_fsm_update(&g_vad_fsm, probability, g_vad_threshold, RADIO_VAD_CONFIRM_WINDOWS,
(uint64_t)g_vad_hangover_ms * 10, busy, now);
radio_audio_update_transmission();
if (!g_vad_stats_tb || now - g_vad_stats_tb >= RADIO_AUDIO_STATS_TB) {
DEBUG_DEBUG(DEBUG_CATEGORY_RADIO, "%s: VAD probability=%.3f busy=%d manual=%d transmitting=%d",
RADIO_AUDIO_ID, probability, busy, g_vad_manual_ptt, g_transmitting);
g_vad_stats_tb = now;
}
}
for (int i = 0; i < n_frames; i++) {
struct radio_media_arg* a = (struct radio_media_arg*)u_malloc(sizeof(*a));
if (!a) return -1;
a->inst = inst;
a->group_id = group_id;
a->len = (uint16_t)opus_len[i];
memcpy(a->opus, opus[i], (size_t)opus_len[i]);
uasync_post(ua, radio_talk_send_trampoline, a);
/* Очищенные interleaved отсчёты произвольной длины; границы PTT ставит единственный TX-writer. */
int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count) {
radio_tx_lock();
if (!g_active || group_id != g_group_id || !g_encoder || !pcm || count <= 0 || count % g_capture_channels) {
radio_tx_unlock();
DEBUG_WARN(DEBUG_CATEGORY_RADIO, "%s: invalid capture frame count=%d", RADIO_AUDIO_ID, count);
return -1;
}
radio_audio_update_transmission(); // Повторить BEGIN после освобождения bounded control-очереди.
for (int offset = 0; offset < count; offset += g_capture_channels) {
if (g_vad_mode) radio_audio_vad_sample(pcm + offset);
if (!g_transmitting) continue;
memcpy(g_tx_pcm + g_tx_pcm_frames * g_capture_channels, pcm + offset, g_capture_channels * sizeof(int16_t));
if (++g_tx_pcm_frames == RADIO_AUDIO_FRAME_SAMPLES) radio_audio_flush_tx();
}
radio_tx_unlock();
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 +712,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 +764,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);

30
src/radio/radio_audio.h

@ -1,15 +1,15 @@
// radio_audio.h — сервисный аудио-движок рации (единый для desktop/Android/headless).
//
// Владеет Opus encoder (TX), per-source декодерами и микшером (RX). Приём:
// radio.c → radio_set_frame_cb → radio_audio_on_frame (uasync) декодирует Opus-кадр
// и кладёт PCM в ring-буфер источника; аудио-поток radio_audio_pull_pcm микширует
// radio.c → radio_set_frame_cb → radio_audio_on_frame (uasync) сохраняет Opus-кадр
// в jitter источника; декодирование и time-stretch выполняются на pull; аудио-поток radio_audio_pull_pcm микширует
// PCM всех источников. Передача: аудио-поток radio_audio_feed_pcm кодирует PCM и
// постит radio_talk_send в uasync. Микшер избыточных источников — насыщающее
// суммирование (soft-clip). Полудуплекс: радио включено — слушаем, PTT — говорим.
//
// Потоки (как call_audio):
// - radio_audio_start / talk_begin / talk_end / stop — GUI-поток;
// - radio_audio_feed_pcm / pull_pcm — аудио-поток;
// - radio_audio_start / capture_start / capture_stop / stop — GUI после остановки workers;
// - radio_audio_feed_pcm / talk_begin / talk_end — один TX-writer, pull_pcm — отдельный RX-writer;
// - radio_audio_on_frame — uasync-поток (из radio.c).
// Общее состояние — под мьютексом. Синглтон на один активный group_id.
@ -33,18 +33,29 @@ 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);
/* Единственный TX-writer: завершить старый PCM-хвост, сбросить VAD после потери захвата. */
void radio_audio_capture_discontinuity(uint64_t group_id);
/* GUI-поток: прекратить прослушивание (освободить всё). */
void radio_audio_stop(void);
/* Playback остановлен: удалить устаревшие источники, сохранив TX и подписку. */
void radio_audio_reset_playback(uint64_t group_id);
/* GUI-поток: PTT зажата/отпущена — постит radio_talk_begin/end в uasync. */
/* Единственный TX-writer: PTT упорядочен с PCM; сетевые BEGIN/FIN исполняются в uasync. */
void radio_audio_talk_begin(uint64_t group_id);
void radio_audio_talk_end(uint64_t group_id);
/* аудио-поток: закодировать PCM-кадр (ровно RADIO_AUDIO_FRAME_SAMPLES) и отправить. */
/* Единственный TX-writer: очищенные interleaved отсчёты произвольной длины (кратно числу каналов).
* VAD до AGC; один автомат manual/VAD; хвост каждой серии дополняется тишиной до FIN. */
int radio_audio_feed_pcm(uint64_t group_id, const int16_t* pcm, int count);
/* аудио-поток: выдать до max_samples микшированного PCM (источники микшируются,
@ -54,6 +65,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 */

43
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.
* CM удерживает свой NCD handle для позднего UP и мониторинга прямого пути;
* группа приобретает независимое NCD-владение при согласовании своей сессии.
* CM handle остаётся у медиа-сервиса. Закрытие CM handle отменяет его запрос, а не владение группы.
* Групповые сессии открывают через topo_group_peer_open(). Все операции — в потоке uasync. */
#ifndef CONN_MGR_H
#define CONN_MGR_H
@ -22,7 +31,7 @@ struct TOPO_GROUP;
enum conn_mgr_event {
CONN_EVENT_UP = 0, /* связь есть. handle жив, можно отправлять данные */
CONN_EVENT_DOWN = 1, /* связь потеряна; handle остаётся у владельца, успех восстановления не гарантирован */
CONN_EVENT_DOWN = 1, /* все пути потеряны; восстановление продолжается, пока жив handle */
CONN_EVENT_TIMEOUT = 2, /* подключение не удалось. handle жив, закройте сами */
};
@ -49,7 +58,8 @@ void conn_mgr_router_recv_handler(struct ETCP_CONN* conn, struct ll_entry* entry
/* ═══════ единая точка входа ═══════ */
/**
* Единственная публичная функция подключения — 3-фазное (DIRECT→REVERSE→INDIRECT).
* Параллельные пробы: DIRECT сразу, REVERSE через 0/500мс, INDIRECT через 0/1000мс от общего старта.
* Посредник используется сразу после проверки; поздний прямой путь заменяет его без повторного UP.
* Возврат 0 — запрос принят, -1 — ошибка запуска. cb обязателен.
* Ошибка до запуска может возвращаться без callback. В callback h может быть NULL.
* Успешно выданный handle закрывает вызывающий, в том числе после TIMEOUT.
@ -60,12 +70,15 @@ 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);
/* ═══════ доступ к соединению ═══════ */
/** Заимствованный ETCP_CONN от NCD, если он есть; NULL для INDIRECT или отсоединённого handle. */
/** Заимствованный READY прямой ETCP_CONN; NULL для INDIRECT, ожидания или отсоединённого handle. */
struct ETCP_CONN* conn_mgr_get_conn(struct CONN_MGR_HANDLE* h);
/** Передать e->dgram через маршрутизатор CM. Освобождает entry при ненулевом h;
@ -77,6 +90,18 @@ int conn_mgr_send(struct CONN_MGR_HANDLE* h, struct ll_entry* e);
/** Обновить RTT кандидата-посредника (≤3, сортировка по возрастанию). */
void conn_mgr_update_best_candidates(struct CONN_MGR* mgr, uint64_t node_id, uint16_t rtt);
/** Запланировать перепроверку путей после изменения маршрутов или READY групповых сессий. */
void conn_mgr_routes_changed(struct CONN_MGR* mgr);
/** Изменились локальные сокеты: начать восстановление без ожидания backoff, сохраняя работающие пути. */
void conn_mgr_network_changed(struct CONN_MGR* mgr);
/** Заимствованный next hop: READY прямой путь/подтверждённый посредник; allow_probe — только для управляющего обмена CM. */
struct ETCP_CONN* conn_mgr_route_conn(struct CONN_MGR* mgr, uint64_t node_id, int allow_probe);
/** Тип живого транспорта к node_id: исходящий DIRECT, только входящие REVERSE, другой next hop INDIRECT. */
uint8_t conn_mgr_connection_type(const struct ETCP_CONN* conn, uint64_t node_id);
#ifdef __cplusplus
}
#endif

1002
src/routing_layer/conn_mgr_core.c

File diff suppressed because it is too large Load Diff

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

Loading…
Cancel
Save