Browse Source

dm: прямой обмен сообщениями (DM) + soundtouch + архитектура звонков

- src/dm/: dm_core/dm_crypto/dm_mailbox — прямой обмен сообщениями (DM)
- lib/soundtouch-master/: vendored SoundTouch (изменение темпа/высоты аудио)
- doc/dm_arch.md, doc/chat_call_arch.md: архитектура DM и звонков
- tests: test_dm.c, test_dm_e2e.c
- chat: рефакторинг chat_* модулей, member_sync, invite_build
- chatgui/chatgui-android: синхронизация с новым API чата
- lib/u_async, debug_config, utun_instance: сопутствующие изменения
v2
evgeny 2 weeks ago
parent
commit
73fe99b33f
  1. 199
      doc/chat_call_arch.md
  2. 93
      doc/dm_arch.md
  3. 137
      doc/tasks.md
  4. 1
      lib/debug_config.c
  5. 3
      lib/debug_config.h
  6. 55
      lib/soundtouch-master/.gitignore
  7. 131
      lib/soundtouch-master/CMakeLists.txt
  8. 458
      lib/soundtouch-master/COPYING.TXT
  9. 1020
      lib/soundtouch-master/README.html
  10. 11
      lib/soundtouch-master/SoundTouchConfig.cmake.in
  11. 205
      lib/soundtouch-master/include/soundtouch/BPMDetect.h
  12. 180
      lib/soundtouch-master/include/soundtouch/FIFOSampleBuffer.h
  13. 230
      lib/soundtouch-master/include/soundtouch/FIFOSamplePipe.h
  14. 176
      lib/soundtouch-master/include/soundtouch/STTypes.h
  15. 348
      lib/soundtouch-master/include/soundtouch/SoundTouch.h
  16. 177
      lib/soundtouch-master/include/soundtouch/SoundTouchDLL.h
  17. 66
      lib/soundtouch-master/readme.md
  18. 222
      lib/soundtouch-master/source/SoundTouch/AAFilter.cpp
  19. 93
      lib/soundtouch-master/source/SoundTouch/AAFilter.h
  20. 569
      lib/soundtouch-master/source/SoundTouch/BPMDetect.cpp
  21. 275
      lib/soundtouch-master/source/SoundTouch/FIFOSampleBuffer.cpp
  22. 312
      lib/soundtouch-master/source/SoundTouch/FIRFilter.cpp
  23. 140
      lib/soundtouch-master/source/SoundTouch/FIRFilter.h
  24. 196
      lib/soundtouch-master/source/SoundTouch/InterpolateCubic.cpp
  25. 69
      lib/soundtouch-master/source/SoundTouch/InterpolateCubic.h
  26. 296
      lib/soundtouch-master/source/SoundTouch/InterpolateLinear.cpp
  27. 98
      lib/soundtouch-master/source/SoundTouch/InterpolateLinear.h
  28. 181
      lib/soundtouch-master/source/SoundTouch/InterpolateShannon.cpp
  29. 74
      lib/soundtouch-master/source/SoundTouch/InterpolateShannon.h
  30. 277
      lib/soundtouch-master/source/SoundTouch/PeakFinder.cpp
  31. 90
      lib/soundtouch-master/source/SoundTouch/PeakFinder.h
  32. 313
      lib/soundtouch-master/source/SoundTouch/RateTransposer.cpp
  33. 164
      lib/soundtouch-master/source/SoundTouch/RateTransposer.h
  34. 538
      lib/soundtouch-master/source/SoundTouch/SoundTouch.cpp
  35. 29
      lib/soundtouch-master/source/SoundTouch/SoundTouch.sln
  36. 342
      lib/soundtouch-master/source/SoundTouch/SoundTouch.vcxproj
  37. 1085
      lib/soundtouch-master/source/SoundTouch/TDStretch.cpp
  38. 279
      lib/soundtouch-master/source/SoundTouch/TDStretch.h
  39. 55
      lib/soundtouch-master/source/SoundTouch/cpu_detect.h
  40. 130
      lib/soundtouch-master/source/SoundTouch/cpu_detect_x86.cpp
  41. 392
      lib/soundtouch-master/source/SoundTouch/mmx_optimized.cpp
  42. 364
      lib/soundtouch-master/source/SoundTouch/sse_optimized.cpp
  43. 250
      lib/soundtouch-master/source/SoundTouchDLL/SoundTouchDLL.cpp
  44. 100
      lib/soundtouch-master/source/SoundTouchDLL/SoundTouchDLL.rc
  45. 15
      lib/soundtouch-master/source/SoundTouchDLL/resource.h
  46. 11
      lib/u_async.c
  47. 6
      src/Makefile.am
  48. 20
      src/chat/chat_admin.c
  49. 145
      src/chat/chat_channel.c
  50. 183
      src/chat/chat_core.c
  51. 122
      src/chat/chat_core.h
  52. 21
      src/chat/chat_core_priv.h
  53. 15
      src/chat/chat_event.c
  54. 10
      src/chat/chat_event.h
  55. 312
      src/chat/chat_headless_control.c
  56. 2
      src/chat/chat_headless_control.h
  57. 37
      src/chat/chat_join.c
  58. 77
      src/chat/chat_member.c
  59. 14
      src/chat/chat_member.h
  60. 434
      src/chat/chat_msg.c
  61. 71
      src/chat/chat_profile.c
  62. 111
      src/chat/chat_setting.c
  63. 37
      src/chat/chat_setting.h
  64. 90
      src/chat/chat_status.c
  65. 385
      src/chat/chat_sync.c
  66. 2
      src/chat/chat_sync.h
  67. 28
      src/chat/chat_whisper.c
  68. 6
      src/chat/chat_whisper.h
  69. 136
      src/chat/invite_build.c
  70. 12
      src/chat/invite_build.h
  71. 50
      src/chat/member_sync.c
  72. 8
      src/chat/member_sync.h
  73. 4
      src/config_parser.c
  74. 2
      src/config_parser.h
  75. 785
      src/dm/dm_core.c
  76. 76
      src/dm/dm_core.h
  77. 128
      src/dm/dm_crypto.c
  78. 50
      src/dm/dm_crypto.h
  79. 353
      src/dm/dm_mailbox.c
  80. 49
      src/dm/dm_mailbox.h
  81. 4
      src/media_delivery/media_delivery.c
  82. 4
      src/routing_layer/etcp_router.c
  83. 2
      src/routing_layer/topo_group.c
  84. 2
      src/routing_layer/topo_group_connect.c
  85. 6
      src/routing_layer/topo_group_invite.c
  86. 2
      src/transport_layer/auto_socket.c
  87. 2
      src/transport_layer/etcp_api.h
  88. 11
      src/utun_instance.c
  89. 17
      src/utun_instance.h
  90. 79
      task.txt
  91. 10
      tests/Makefile.am
  92. 6
      tests/test_chat_join_e2e.c
  93. 109
      tests/test_dm.c
  94. 452
      tests/test_dm_e2e.c
  95. 30
      tools/chatcli
  96. 13
      tools/chatgui-android/headless/headless_control.c
  97. 114
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  98. 1
      tools/chatgui-android/libutun_lite/attachment_sender.c
  99. 54
      tools/chatgui-android/libutun_lite/instance_lite.c
  100. 4
      tools/chatgui-android/libutun_lite/instance_lite.h
  101. Some files were not shown because too many files have changed in this diff Show More

199
doc/chat_call_arch.md

@ -0,0 +1,199 @@
# P2P аудио-звонок пользователю — архитектура
Статус: спроектировано, ждёт реализации.
## 1. Область
Голос 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)** — модуль
не привязан к типу группы: он оперирует `group_id` + `peer_node_id` (для DM `group_id` —
DM-группа, маршрутизация через `etcp_router` идентична). Протокол расширяем под видео
(track-модель), реализуется только аудио. Работает desktop↔desktop и desktop↔android
(обе стороны компилируют общий стек `etcp_router` / `topo_group` (BGP) / `conn_mgr`).
Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE →
установка соединения (conn_mgr) → аудиопоток → HANGUP.
## 2. Разбиение файлов (отдельный каталог `src/call/`)
| Файл | Язык | Сборки | Назначение |
|---|---|---|---|
| `src/call/call_proto.h` | C | везде | wire-формат, subcmd, svc_id `ETCP_RT_ID_CALL 0x34` |
| `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 |
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
Новая категория логирования: `DEBUG_CATEGORY_CALL = 31` (добавить в `debug_config.h`,
`DEBUG_CATEGORY_COUNT` → 32).
## 3. Протокол (svc_id 0x34, первый байт payload = subcmd)
Формат приёма (как media_delivery, одноуровневый): `entry->dgram[0]=svc_id`,
`entry->dgram[1]=subcmd`, `entry->dgram[2..]=payload`.
```
CALL_INVITE 0x01 {call_id:8, group_id:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
CALL_RINGING 0x02 {call_id:8}
CALL_ACCEPT 0x03 {call_id:8, agreed_tracks}
CALL_DECLINE 0x04 {call_id:8, reason:1}
CALL_BUSY 0x05 {call_id:8}
CALL_CANCEL 0x06 {call_id:8}
CALL_HANGUP 0x07 {call_id:8, reason:1}
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, payload}
```
- `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах.
- `group_id` — CHAT-группа или DM-группа (общая маршрутизация).
- Сигналинг: `etcp_route_send(..., ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)`.
- Медиа: без подписи (уже внутри шифрованного канала etcp_router).
- `CALL_MEDIA` — общий контейнер; `track_id` оставляет место видео (`type=VIDEO`).
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с.
## 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
```
`ENDED` reasons: `LOCAL_HANGUP / REMOTE_HANGUP / DECLINE / BUSY / CANCEL /
NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
Таймауты:
- ring-timeout 45с (нет ответа → CANCEL);
- connect: `conn_mgr` `CONN_EVENT_TIMEOUT` → fail;
- **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP.
Онлайн-проверка: `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 минимален).
## 5. Отправка медиа — backpressure через локальный буфер
Требование: не дропать аудио при переполнении роутера — держим в локальном буфере
отправки, дренируем при появлении места.
Механика (`etcp_router` уже предоставляет):
- `etcp_route_send(force=0)` возвращает `-1`, когда `send_q` роутера полон
(`count >= ROUTER_MAX_SEND_Q_PACKETS`).
- `etcp_router_send_q_has_room(...)` — есть ли место (порог `ROUTER_MAX_SEND_Q_PACKETS-1`).
- `etcp_router_on_send_ready(..., &waiter, drain_cb, arg)` — одноразовый waiter:
колбэк вызывается, когда `send_q->count <= threshold` (освободилось место).
В сессии:
- `tx_q` — локальная FIFO очередь закодированных кадров (ll_entry с dgram), ёмкостью
~1с (≈50 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
- Алгоритм (`call_tx_flush`): пока `tx_q` не пуст:
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=0)`;
- иначе — если waiter не зарегистрирован → `etcp_router_on_send_ready(... drain_cb)` и выход.
- `drain_cb(q, arg)` → снова `call_tx_flush(session)` (re-arm при необходимости).
- Весь код — в uasync-потоке (single-threaded, реентерабельность как в
`media_delivery.c:stream_send_chunk_cb`).
## 6. Thread-модель (single-writer)
| Объект | Владелец |
|---|---|
| `call_session` (состояние, encoder, decoder, tx_q, таймеры) | uasync-поток |
| Opus encoder (send) | аудио-поток (encode) |
| Opus decoder (recv) | uasync-поток (`decode → ajb_push`) |
| `ajb` push | uasync-поток |
| `ajb` pull + SoundTouch | аудио-поток |
| `call_tones` | аудио-поток |
Поток данных:
- TX: аудио-поток кодирует → `call_send_encoded(call_id, opus, len)` → `uasync_post` →
uasync: `tx_q` + `call_tx_flush` (backpressure).
- RX: uasync принимает `CALL_MEDIA` → decode → `ajb_push`; аудио-поток `call_pull_pcm` → `ajb_pull` (+ тоны).
`ajb` = mutex-кольцо PCM (гранулярность 20ms/50 ops в сек — мьютекс бесплатен);
SoundTouch инстанс заперт на pull-стороне.
**Контракт жизненного цикла:** GUI стартует аудио-устройство по `CALL_ACCEPTED`,
останавливает по `CALL_ENDED` и только после остановки (join аудио-потока) вызывает
`call_release(call_id)` (post в uasync) → uasync освобождает session/encoder/decoder/jb.
Пока устройство работает — `call_pull_pcm`/`call_send_encoded` не гоняются с освобождением.
## 7. Адаптивный джиттер-буфер (SoundTouch)
Буфер ≤ **1000 мс**, target playout ~60 мс; при росте буфера плавно tempo → **1.6x**.
SoundTouch: `USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4`.
API: `ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy`.
## 8. Тоны и диагностика
- **Сталл >500мс** (нет входящих медиа) → в `call_pull_pcm` микшируется
`CALL_TONE_GLITCH`: `пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс)
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл.
- **Завершение звонка → всегда** `CALL_TONE_ENDED`: 3 нисходящих 700/500/400 Гц по 150 мс.
- Счётчики: tx/rx кадры, `c_tx_dropped`/`c_rx_dropped`, дропы буфера, сталлы,
tempo-события, причины завершения. Логи `DEBUG_CATEGORY_CALL` (+стата раз в 1с).
## 9. GUI-события (extend `chat_event.h` + `gui_bridge.h` + android)
```
CHAT_EVT_CALL_INCOMING 32 [ch_id_len][ch_id][caller_node_id:8][call_id:8]
CHAT_EVT_CALL_RINGING 33 [call_id:8]
CHAT_EVT_CALL_ACCEPTED 34 [call_id:8]
CHAT_EVT_CALL_ENDED 35 [call_id:8][reason:1]
CHAT_EVT_CALL_DECLINED 36 [call_id:8][reason:1]
CHAT_EVT_CALL_STATS 37 [call_id:8][rtt_ms:2][buffer_ms:2][tempo_x100:2]
CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
```
## 10. Публичный API (call.h)
```
int call_start(ch_id, peer_node_id, out_call_id); // caller (post в uasync)
void call_accept(call_id); // callee
void call_decline(call_id);
void call_hangup(call_id);
int call_is_online(ch_id, peer_node_id); // BGP-проверка для UI
// аудио-поток (общий процессинг в C-ядре):
int call_encode_pcm(call_id, pcm, n, opus_out, cap); // single-writer: audio thread
int call_send_encoded(call_id, opus, len); // post → uasync → tx_q + flush
int call_pull_pcm(call_id, pcm_out, max); // single-writer: audio thread (ajb+tones)
void call_release(call_id); // post → uasync: освободить аудио-объекты
```
`call_init / call_destroy` — вызываются из `chat_core_init / chat_core_destroy`.
## 11. Платформенный I/O
- **Desktop:** `tools/chatgui/src/call_audio_engine.cpp` — miniaudio full-duplex
(capture → `call_encode_pcm`, `call_pull_pcm` → playback) + окно звонка (UI)
+ события в `gui_bridge_impl.cpp`.
- **Android:** JNI `nativeCall*` + Kotlin `AudioRecord`/`AudioTrack`.
**Переключение источника:** `AudioManager` (API31+ `setCommunicationDevice` +
`availableCommunicationDevices`; fallback `setSpeakerphoneOn` + `startBluetoothSco`),
детект гарнитуры (`ACTION_HEADSET_PLUG`, BT-профиль, `ACTION_SCO_AUDIO_STATE_UPDATED`),
кнопка-цикл трубка/громкая/гарнитура, авто-выбор гарнитуры. `nativeCallSetAudioRoute(route)`.
## 12. Сборка
- `src/Makefile.am` + `tests/Makefile.am`: `call.c` (C, без `.cpp`). `src/call/` в include path.
- `tools/chatgui/libutun/CMakeLists.txt`: +`call_audio.c`, `call_tones.c`,
`audio_jitter.cpp`, soundtouch sources (AAFilter, FIFOSampleBuffer, FIRFilter,
InterpolateCubic/Linear/Shannon, RateTransposer, SoundTouch, TDStretch, cpu_detect_x86,
mmx_optimized, sse_optimized) как C++. `call.c` попадёт через GLOB `src/*.c`.
- `tools/chatgui-android/libutun_lite/CMakeLists.txt` + `utun_sources.cmake`:
`LANGUAGES C CXX`, те же файлы.
- `src/transport_layer/etcp_api.h`: `#define ETCP_RT_ID_CALL 0x34`.
- `lib/debug_config.h`: `DEBUG_CATEGORY_CALL 31`, `DEBUG_CATEGORY_COUNT` → 32.
## 13. Порядок задач
1. `call_proto.h` + svc_id + `DEBUG_CATEGORY_CALL`.
2. `call.c` (машина состояний, backpressure-media, watchdog, события, API) + `chat_core` хуки.
3. `audio_jitter.cpp` (SoundTouch) + `test_audio_jitter.c`.
4. `call_tones.c` + `call_audio.c` (Opus + фрейминг + тон-обвязка).
5. Desktop: `call_audio_engine.cpp` + UI + события.
6. Android: JNI + Kotlin AudioRecord/Track + переключение источника + UI.
7. Сборка (Makefile.am / CMake обе).
8. `test_call.c` (два инстанса), интеграция desktop↔desktop и desktop↔android.

93
doc/dm_arch.md

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

137
doc/tasks.md

@ -1,6 +1,137 @@
# Задачи по проекту
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.
## Текущая задача
[ ] **DM: прямой p2p чат с любым пользователем группы** — отдельная DM-подсистема.
ТЗ и архитектура: `/doc/dm_arch.md`. Статус: реализовано (dm_core/dm_mailbox/dm_crypto),
тесты `test_dm` (крипто) и `test_dm_e2e` (интеграция). Остались задачи ниже.
[ ] **Звонок (P2P аудио)** — голос 1:1, из CHAT-группы или p2p-чата (DM). Отдельный
каталог `src/call/`. Сигналинг + conn_mgr + аудиопоток (Opus) с backpressure
(локальный буфер отправки при переполнении роутера) + адаптивный джиттер-буфер
(SoundTouch, ускорение до 1.6x) + тоны (глитч/завершение) + watchdog 20с.
Android: переключение источника (трубка/громкая/гарнитура).
Архитектура: `/doc/chat_call_arch.md`. Статус: спроектировано, ждёт реализации.
## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e)
Сделано (глобальные синглтоны переведены на `UTUN_INSTANCE`):
- `chat_setting` → `inst->chat_settings` (`struct chat_setting_state`, state-level API для парсера конфига + instance-level для runtime).
- `chat_event` → `inst->chat_event_handler` (обработчик теперь получает `inst`).
- `chat_core` (`g_cc`) → `inst->chat_core` (`struct chat_core_ctx*`).
- `chat_sync` (`g_cs`) → `inst->chat_sync`.
- `member_sync` callback-списки (`g_props_cbks`/`g_apply_cbks`) → в `chat_core_ctx`.
- `chat_join` (`g_keys`) → в `chat_core_ctx`.
- `chat_msg` media-счётчики/backfill → в `chat_core_ctx`.
- `dm_core` (`g_dm`) → `inst->dm`; `dm_mailbox` (`g_mb`) → `inst->dm_mailbox`.
- `chat_headless_control` (`g_hc`) → `inst->headless`.
- `chat_whisper` — сигнатуры `init/available/trigger` принимают `inst`.
- API протащено через `inst` (chat_core_*/dm_*), обновлены call-сайты в src/ (headless, topo, media_delivery, auto_socket, config_parser).
- `test_dm_e2e.c` переписан на однопоточный: один `uasync`, три `utun_instance` (A,B,C), master state-machine, без fork. Фаза 2 (B offline) — `utun_instance_destroy(B1)` + пересоздание B2.
- Попутно исправлен 1-байтовый overflow в `dm_mailbox.c: mb_route_send` (`u_malloc(1+body_len)` → `1+1+body_len`).
Статус сборки: `make -C src` (libutun.a + бинарник utun) и `make -C tests` — OK.
### Осталось (по порядку)
[+] **test_dm_e2e: зависание в utun_instance_destroy(B1)** (фаза P_B_OFFLINE) — сделано.
Причин было несколько (не одна):
1. `lib/u_async.c`: если ближайший таймер уже истёк на входе в `uasync_poll`,
`get_next_timeout` возвращал {0,0}, `timeout_ms=-1` → `epoll_wait(-1)` блокировался
навсегда, а `process_timeouts` (вызывается только после epoll_wait) не запускался.
Фикс: `if (timeout_ms == -1 && heap не пуст) timeout_ms = 0`.
2. `routing_layer/etcp_router.c`: `router_send_conn` с group_id=0 (глобальная
маршрутизация DM/mailbox) не находил маршрут (`topo_groups_find(0)==NULL`).
Фикс: fallback на `instance_find_conn(inst, remote)` при group==NULL.
3. `dm/dm_core.c`: `dm_on_conn_status` PULL-ил только беседы с подключившимся пиром;
B2 подключается к storage (C), а беседа — с A. Фикс: на подъём любого соединения
PULL для всех бесед, чей пир не подключён напрямую.
4. `dm/dm_mailbox.c`: не было ACK после PULL_RESP → storage не чистил dm_mail.
Фикс: PULL_RESP теперь несёт sender, получатель шлёт MB_SUBCMD_ACK.
5. `utun_instance.c`: `dm_core_init` ставил deliver-cb до `dm_mailbox_init` →
mailbox не находился, deliver-cb не регистрировался. Фикс: mailbox init раньше core.
`test_dm_e2e` проходит (несколько прогонов), `test_dm` тоже.
[+] **chatgui-android: переезд на per-instance API** — сделано.
`instance_lite.c/h` (+`instance_lite_get_instance()`), `jni_bridge.c`, `standby.c`,
`headless_control.c`, `voice_recorder.c`, `attachment_sender.c`, `video_sender.c`,
`photo_sender.c` — все call-сайты переведены на `inst`. `libutun_lite` компилируется,
`jni_bridge.c` — синтакс-чисто. (headless-линковка сломана ПРЕДЫДУЩЕ — instance_lite.c
ссылается на jni_bridge-функции, не включённые в headless-сборку; отдельная задача.)
[+] **test_chat_join: segfault** (регресс рефакторинга) — сделано.
`chat_join` хранил `join_keys` в `chat_core_ctx` (CC(inst)), а тест использует chat_join
без chat_core → NULL-deref. Фикс: `join_keys` вынесен в `UTUN_INSTANCE.join_keys`
(у chat_join собственный init/destroy).
[ ] **BGP-гонка: дропнутый REQUEST_TABLE** (отдельная задача, как договорились).
**Место:** `topo_group_new_conn()` в `src/routing_layer/topo_group.c:571`. Дедуп-ветка
«conn already in senders_list» (строки 579–587) делает `return` БЕЗ повторной отправки
`topo_group_send_table_request()`.
**Зачем дедуп:** тот же `ETCP_CONN` стреляет `ETCP_CONN_STATUS_UP` дважды (UDP-линк,
затем TCP-линк). Повторная обработка задваивает `active_conn_count`, из-за чего
переподключение не стартует. Поэтому `return` в дедупе — правильный.
**В чём гонка:** REQUEST_TABLE — это то, что заставляет пира отдать свою таблицу
(NODEINFO/узлы группы). Сценарий «разнесённого старта»:
1. A↔C соединение поднято. A шлёт C `REQUEST_TABLE` для группы G (`topo_group_new_conn`
→ `topo_group_send_table_request`, `topo_group.c:602`).
2. У C группа G ещё **не создана** (создаётся позже, по мере загрузки каналов/синка).
C получает `REQUEST_TABLE`, но `topo_group_handle_request_table` не находит G →
дропает запрос.
3. C наконец создаёт G (`topo_groups_create_group` → обход connections →
`topo_group_new_conn(G, conn_A)` на стороне C) → C шлёт A свой `REQUEST_TABLE`.
A отвечает своей таблицей → **C узнаёт A**.
4. Но A больше не шлёт C `REQUEST_TABLE` → **A так и не узнаёт C** в группе G.
Итог: при разнесённом старте A не видит C в CHAT-группе (односторонняя видимость).
**Фикс (предполагаемый):** в дедуп-ветке не просто `return`, а предварительно
идемпотентно `topo_group_send_table_request(group, conn)`:
```c
if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) {
topo_group_send_table_request(group, conn); /* повторно, идемпотентно */
DEBUG_INFO(..., "conn already in senders_list, re-request table (%s)", conn->log_name);
return;
}
```
`topo_group_send_table_request` не ведёт состояния — просто шлёт REQUEST_TABLE,
пир отвечает снимком таблицы (идемпотентно). Лишний дубль таблицы безвреден.
**Замечание:** в однопоточном `test_dm_e2e` (и `test_chat_join_e2e`) из-за
детерминированного порядка группа успевает создаться до прихода REQUEST_TABLE —
поэтому тест проходит. Реальная гонка остаётся для асинхронных сетей. Отдельный
воспроизводимый тест для этой гонки пока не написан.
[ ] **«packet undecryptable» на линках C** (вторичное). A, узнав B через BGP, через
`topo_group_connect` пытается поднять прямой A↔B линк (`seskey 4777737b`), но B не
настроен принимать A → crypto-шум каждые ~1с. Разобраться с авто-подключением
(не пытаться соединяться с узлами, для которых нет `[client]`/ncd-конфигурации,
либо обрабатывать неудачу без спама).
[+] **Обновить GUI call-сайты `tools/chatgui` (Qt, desktop)** — сделано.
- В `chat_core.h` вынесены trampoline-структуры из `.c`: `update_my_name_arg`,
`save_ui_state_arg`, `chat_setting_arg` (были в chat_profile.c/chat_core.c).
- `gui_bridge.h/impl`: `gui_bridge_set_inst()`/`gui_bridge_get_inst()` (+`g_inst`).
- `utun_node.cpp`: `gui_bridge_set_inst(m_instance)`, `chat_event_set_handler(m_instance,…)`,
`chat_core_sync_my_addresses(m_instance)`.
- Обновлены ~45 call-сайтов в 16 файлах (`mainwindow`, `messagelist`, `messagedelegate`,
`memberlistmodel`, `memberpropsdialog`, `accountlist`, `invitedialog`, `inviteby`,
`joindialog`, `settingsdialog`, `channelsettingsdialog`, `soundsettingspage`,
`audiodevicesettingspage`, `storagesettingspage`, `statuspage`, `connmonitorwindow`).
- Сборка `cmake --build . --target vibechat` — OK.
Попутно: `chat_sync_connect_from_invite` писал `cs->pending_*` на вызывающем (GUI) потоке —
перенёс в `cm_invite_trampoline` (uasync-поток), убрав гонку; заодно исправил утечку
`inv->addrs_data` и добавил null-check `cs` в трамплине.
[+] **Прогон всех тестов** — сделано. `make clean && make -j4` OK; все тесты проходят
(кроме `test_auto_socket_dynamic` — пропуск «requires root»). `test_dm_e2e` стабилен
в нескольких прогонах. В `test_dm_e2e` при teardown остаётся timer-leak (~68 узлов,
`router_no_route`), не влияет на результат — стоит разобрать отдельно.
## Открытые флаки
- **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`:
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает.
[ ] **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`:
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает.

1
lib/debug_config.c

@ -131,6 +131,7 @@ static const struct {
{"proxy", DEBUG_CATEGORY_PROXY},
{"video", DEBUG_CATEGORY_VIDEO},
{"reality", DEBUG_CATEGORY_REALITY},
{"dm", DEBUG_CATEGORY_DM},
{"all", DEBUG_CATEGORY_ALL},
{NULL, DEBUG_CATEGORY_NONE}
};

3
lib/debug_config.h

@ -69,7 +69,8 @@ typedef int debug_category_t;
#define DEBUG_CATEGORY_PROXY 28 // Proxy modules (SOCKS5, TCP, UDP, ICMP)
#define DEBUG_CATEGORY_VIDEO 29 // Video module (probe/transcode)
#define DEBUG_CATEGORY_REALITY 30 // REALITY TLS camouflage
#define DEBUG_CATEGORY_COUNT 31 // Total number of categories
#define DEBUG_CATEGORY_DM 31 // DM — direct p2p chat between two users
#define DEBUG_CATEGORY_COUNT 32 // Total number of categories
#define DEBUG_CATEGORY_ALL (-1) // special value for all categories
/* Debug configuration structure */

55
lib/soundtouch-master/.gitignore vendored

@ -0,0 +1,55 @@
bin
lib
# Win build files
*.dll
*.exe
*.lib
# GNU build files
*.o
*.so*
*.lo
*.P*
*.la*
*.a
*.pc
*config*
Makefile
Makefile.in
.libs
aclocal.m4
autom4te.cache
stamp-*
libtool
soundstretch
# Files generated by MSVC
*.bsc
*.suo
*.sdf
*.filters
*.user
source/SoundTouch/Win32/
source/SoundTouch/x64/
source/SoundStretch/Win32/
source/SoundStretch/x64/
source/SoundTouchDll/Win32/
source/SoundTouchDll/x64/
source/SoundTouchDll/DllTest/Win32/
source/SoundTouchDll/DllTest/x64/
.vs
# Files generated by Android Studio
source/android-lib/.gradle
source/android-lib/.idea
**/*.iml
source/android-lib/local.properties
source/android-lib/build
source/android-lib/.externalNativeBuild
# CMake build directory
build*
CMakeFiles
CMakeCache.txt
*.cmake

131
lib/soundtouch-master/CMakeLists.txt

@ -0,0 +1,131 @@
cmake_minimum_required(VERSION 3.16)
project(SoundTouch VERSION 2.3.3 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
include(GNUInstallDirs)
set(COMPILE_OPTIONS)
if(MSVC)
list(APPEND COMPILE_OPTIONS /O2 /fp:fast)
else()
list(APPEND COMPILE_OPTIONS -Wall -Wextra -Wzero-as-null-pointer-constant -Wno-unknown-pragmas)
if(EMSCRIPTEN)
list(APPEND COMPILE_OPTIONS -O3)
else()
list(APPEND COMPILE_OPTIONS -Ofast)
endif()
endif()
set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
#####################
# SoundTouch library
add_library(SoundTouch STATIC
source/SoundTouch/AAFilter.cpp
source/SoundTouch/BPMDetect.cpp
source/SoundTouch/cpu_detect_x86.cpp
source/SoundTouch/FIFOSampleBuffer.cpp
source/SoundTouch/FIRFilter.cpp
source/SoundTouch/InterpolateCubic.cpp
source/SoundTouch/InterpolateLinear.cpp
source/SoundTouch/InterpolateShannon.cpp
source/SoundTouch/mmx_optimized.cpp
source/SoundTouch/PeakFinder.cpp
source/SoundTouch/RateTransposer.cpp
source/SoundTouch/SoundTouch.cpp
source/SoundTouch/sse_optimized.cpp
source/SoundTouch/TDStretch.cpp
)
target_include_directories(SoundTouch PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_include_directories(SoundTouch PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include/soundtouch
)
target_compile_definitions(SoundTouch PRIVATE ${COMPILE_DEFINITIONS})
target_compile_options(SoundTouch PRIVATE ${COMPILE_OPTIONS})
set_target_properties(SoundTouch PROPERTIES OUTPUT_NAME "soundtouch-static")
option(INTEGER_SAMPLES "Use integers instead of floats for samples" OFF)
if(INTEGER_SAMPLES)
target_compile_definitions(SoundTouch PRIVATE SOUNDTOUCH_INTEGER_SAMPLES)
set(SOUNDTOUCH_INTEGER_SAMPLES TRUE)
else()
target_compile_definitions(SoundTouch PRIVATE SOUNDTOUCH_FLOAT_SAMPLES)
set(SOUNDTOUCH_FLOAT_SAMPLES TRUE)
endif()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv7.*)$")
target_compile_options(SoundTouch PRIVATE -mfpu=neon)
endif()
install(
FILES
include/soundtouch/BPMDetect.h
include/soundtouch/FIFOSampleBuffer.h
include/soundtouch/FIFOSamplePipe.h
include/soundtouch/STTypes.h
include/soundtouch/SoundTouch.h
DESTINATION
"${CMAKE_INSTALL_INCLUDEDIR}/soundtouch"
)
########################
# SoundTouchDll library
option(SOUNDTOUCH_DLL "Build SoundTouchDLL C wrapper library" ON)
if(SOUNDTOUCH_DLL)
add_library(SoundTouchDLL SHARED
source/SoundTouchDLL/SoundTouchDLL.cpp
source/SoundTouchDLL/SoundTouchDLL.rc
)
set_target_properties(SoundTouch PROPERTIES POSITION_INDEPENDENT_CODE TRUE)
target_compile_options(SoundTouchDLL PRIVATE ${COMPILE_OPTIONS})
set_target_properties(SoundTouchDLL PROPERTIES CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(SoundTouchDLL PRIVATE DLL_EXPORTS)
target_include_directories(SoundTouchDLL INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
target_include_directories(SoundTouchDLL PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include/soundtouch")
target_link_libraries(SoundTouchDLL PRIVATE SoundTouch)
set_target_properties(SoundTouchDLL PROPERTIES OUTPUT_NAME "soundtouch")
if(NOT ANDROID)
set_target_properties(SoundTouchDLL PROPERTIES VERSION ${CMAKE_PROJECT_VERSION})
if(NOT WIN32)
set_target_properties(SoundTouchDLL PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR})
endif()
endif()
install(FILES include/soundtouch/SoundTouchDLL.h DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/soundtouch")
install(TARGETS SoundTouchDLL EXPORT SoundTouchTargets)
endif()
########################
# CMake config
include(CMakePackageConfigHelpers)
set(SOUNDTOUCH_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/SoundTouch")
install(
EXPORT SoundTouchTargets
FILE SoundTouchTargets.cmake
NAMESPACE SoundTouch::
DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}"
)
configure_package_config_file(SoundTouchConfig.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfig.cmake"
INSTALL_DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}"
)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfigVersion.cmake"
VERSION "${CMAKE_PROJECT_VERSION}"
COMPATIBILITY SameMajorVersion
)
install(
FILES
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfigVersion.cmake"
DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}"
)

458
lib/soundtouch-master/COPYING.TXT

@ -0,0 +1,458 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS

1020
lib/soundtouch-master/README.html

File diff suppressed because it is too large Load Diff

11
lib/soundtouch-master/SoundTouchConfig.cmake.in

@ -0,0 +1,11 @@
@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/SoundTouchTargets.cmake")
check_required_components(SoundTouch)
if(@SOUNDTOUCH_DLL@)
check_required_components(SoundTouchDLL)
get_target_property(SoundTouchDLL_LOCATION SoundTouch::SoundTouchDLL LOCATION)
message(STATUS "Found SoundTouchDLL: ${SoundTouchDLL_LOCATION}")
endif()

205
lib/soundtouch-master/include/soundtouch/BPMDetect.h

@ -0,0 +1,205 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Beats-per-minute (BPM) detection routine.
///
/// The beat detection algorithm works as follows:
/// - Use function 'inputSamples' to input a chunks of samples to the class for
/// analysis. It's a good idea to enter a large sound file or stream in smallish
/// chunks of around few kilosamples in order not to extinguish too much RAM memory.
/// - Input sound data is decimated to approx 500 Hz to reduce calculation burden,
/// which is basically ok as low (bass) frequencies mostly determine the beat rate.
/// Simple averaging is used for anti-alias filtering because the resulting signal
/// quality isn't of that high importance.
/// - Decimated sound data is enveloped, i.e. the amplitude shape is detected by
/// taking absolute value that's smoothed by sliding average. Signal levels that
/// are below a couple of times the general RMS amplitude level are cut away to
/// leave only notable peaks there.
/// - Repeating sound patterns (e.g. beats) are detected by calculating short-term
/// autocorrelation function of the enveloped signal.
/// - After whole sound data file has been analyzed as above, the bpm level is
/// detected by function 'getBpm' that finds the highest peak of the autocorrelation
/// function, calculates it's precise location and converts this reading to bpm's.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef _BPMDetect_H_
#define _BPMDetect_H_
#include <vector>
#include "STTypes.h"
#include "FIFOSampleBuffer.h"
namespace soundtouch
{
/// Minimum allowed BPM rate. Used to restrict accepted result above a reasonable limit.
#define MIN_BPM 45
/// Maximum allowed BPM rate range. Used for calculating algorithm parametrs
#define MAX_BPM_RANGE 200
/// Maximum allowed BPM rate range. Used to restrict accepted result below a reasonable limit.
#define MAX_BPM_VALID 190
////////////////////////////////////////////////////////////////////////////////
typedef struct
{
float pos;
float strength;
} BEAT;
class IIR2_filter
{
double coeffs[5];
double prev[5];
public:
IIR2_filter(const double *lpf_coeffs);
float update(float x);
};
/// Class for calculating BPM rate for audio data.
class BPMDetect
{
protected:
/// Auto-correlation accumulator bins.
float *xcorr;
/// Sample average counter.
int decimateCount;
/// Sample average accumulator for FIFO-like decimation.
soundtouch::LONG_SAMPLETYPE decimateSum;
/// Decimate sound by this coefficient to reach approx. 500 Hz.
int decimateBy;
/// Auto-correlation window length
int windowLen;
/// Number of channels (1 = mono, 2 = stereo)
int channels;
/// sample rate
int sampleRate;
/// Beginning of auto-correlation window: Autocorrelation isn't being updated for
/// the first these many correlation bins.
int windowStart;
/// window functions for data preconditioning
float *hamw;
float *hamw2;
// beat detection variables
int pos;
int peakPos;
int beatcorr_ringbuffpos;
int init_scaler;
float peakVal;
float *beatcorr_ringbuff;
/// FIFO-buffer for decimated processing samples.
soundtouch::FIFOSampleBuffer *buffer;
/// Collection of detected beat positions
//BeatCollection beats;
std::vector<BEAT> beats;
// 2nd order low-pass-filter
IIR2_filter beat_lpf;
/// Updates auto-correlation function for given number of decimated samples that
/// are read from the internal 'buffer' pipe (samples aren't removed from the pipe
/// though).
void updateXCorr(int process_samples /// How many samples are processed.
);
/// Decimates samples to approx. 500 Hz.
///
/// \return Number of output samples.
int decimate(soundtouch::SAMPLETYPE *dest, ///< Destination buffer
const soundtouch::SAMPLETYPE *src, ///< Source sample buffer
int numsamples ///< Number of source samples.
);
/// Calculates amplitude envelope for the buffer of samples.
/// Result is output to 'samples'.
void calcEnvelope(soundtouch::SAMPLETYPE *samples, ///< Pointer to input/output data buffer
int numsamples ///< Number of samples in buffer
);
/// remove constant bias from xcorr data
void removeBias();
// Detect individual beat positions
void updateBeatPos(int process_samples);
public:
/// Constructor.
BPMDetect(int numChannels, ///< Number of channels in sample data.
int sampleRate ///< Sample rate in Hz.
);
/// Destructor.
virtual ~BPMDetect();
/// Inputs a block of samples for analyzing: Envelopes the samples and then
/// updates the autocorrelation estimation. When whole song data has been input
/// in smaller blocks using this function, read the resulting bpm with 'getBpm'
/// function.
///
/// Notice that data in 'samples' array can be disrupted in processing.
void inputSamples(const soundtouch::SAMPLETYPE *samples, ///< Pointer to input/working data buffer
int numSamples ///< Number of samples in buffer
);
/// Analyzes the results and returns the BPM rate. Use this function to read result
/// after whole song data has been input to the class by consecutive calls of
/// 'inputSamples' function.
///
/// \return Beats-per-minute rate, or zero if detection failed.
float getBpm();
/// Get beat position arrays. Note: The array includes also really low beat detection values
/// in absence of clear strong beats. Consumer may wish to filter low values away.
/// - "pos" receive array of beat positions
/// - "values" receive array of beat detection strengths
/// - max_num indicates max.size of "pos" and "values" array.
///
/// You can query a suitable array sized by calling this with nullptr in "pos" & "values".
///
/// \return number of beats in the arrays.
int getBeats(float *pos, float *strength, int max_num);
};
}
#endif // _BPMDetect_H_

180
lib/soundtouch-master/include/soundtouch/FIFOSampleBuffer.h

@ -0,0 +1,180 @@
////////////////////////////////////////////////////////////////////////////////
///
/// A buffer class for temporarily storaging sound samples, operates as a
/// first-in-first-out pipe.
///
/// Samples are added to the end of the sample buffer with the 'putSamples'
/// function, and are received from the beginning of the buffer by calling
/// the 'receiveSamples' function. The class automatically removes the
/// output samples from the buffer as well as grows the storage size
/// whenever necessary.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef FIFOSampleBuffer_H
#define FIFOSampleBuffer_H
#include "FIFOSamplePipe.h"
namespace soundtouch
{
/// Sample buffer working in FIFO (first-in-first-out) principle. The class takes
/// care of storage size adjustment and data moving during input/output operations.
///
/// Notice that in case of stereo audio, one sample is considered to consist of
/// both channel data.
class FIFOSampleBuffer : public FIFOSamplePipe
{
private:
/// Sample buffer.
SAMPLETYPE *buffer;
// Raw unaligned buffer memory. 'buffer' is made aligned by pointing it to first
// 16-byte aligned location of this buffer
SAMPLETYPE *bufferUnaligned;
/// Sample buffer size in bytes
uint sizeInBytes;
/// How many samples are currently in buffer.
uint samplesInBuffer;
/// Channels, 1=mono, 2=stereo.
uint channels;
/// Current position pointer to the buffer. This pointer is increased when samples are
/// removed from the pipe so that it's necessary to actually rewind buffer (move data)
/// only new data when is put to the pipe.
uint bufferPos;
/// Rewind the buffer by moving data from position pointed by 'bufferPos' to real
/// beginning of the buffer.
void rewind();
/// Ensures that the buffer has capacity for at least this many samples.
void ensureCapacity(uint capacityRequirement);
/// Returns current capacity.
uint getCapacity() const;
public:
/// Constructor
FIFOSampleBuffer(int numChannels = 2 ///< Number of channels, 1=mono, 2=stereo.
///< Default is stereo.
);
/// destructor
~FIFOSampleBuffer() override;
/// Returns a pointer to the beginning of the output samples.
/// This function is provided for accessing the output samples directly.
/// Please be careful for not to corrupt the book-keeping!
///
/// When using this function to output samples, also remember to 'remove' the
/// output samples from the buffer by calling the
/// 'receiveSamples(numSamples)' function
virtual SAMPLETYPE *ptrBegin() override;
/// Returns a pointer to the end of the used part of the sample buffer (i.e.
/// where the new samples are to be inserted). This function may be used for
/// inserting new samples into the sample buffer directly. Please be careful
/// not corrupt the book-keeping!
///
/// When using this function as means for inserting new samples, also remember
/// to increase the sample count afterwards, by calling the
/// 'putSamples(numSamples)' function.
SAMPLETYPE *ptrEnd(
uint slackCapacity ///< How much free capacity (in samples) there _at least_
///< should be so that the caller can successfully insert the
///< desired samples to the buffer. If necessary, the function
///< grows the buffer size to comply with this requirement.
);
/// Adds 'numSamples' pcs of samples from the 'samples' memory position to
/// the sample buffer.
virtual void putSamples(const SAMPLETYPE *samples, ///< Pointer to samples.
uint numSamples ///< Number of samples to insert.
) override;
/// Adjusts the book-keeping to increase number of samples in the buffer without
/// copying any actual samples.
///
/// This function is used to update the number of samples in the sample buffer
/// when accessing the buffer directly with 'ptrEnd' function. Please be
/// careful though!
virtual void putSamples(uint numSamples ///< Number of samples been inserted.
);
/// Output samples from beginning of the sample buffer. Copies requested samples to
/// output buffer and removes them from the sample buffer. If there are less than
/// 'numsample' samples in the buffer, returns all that available.
///
/// \return Number of samples returned.
virtual uint receiveSamples(SAMPLETYPE *output, ///< Buffer where to copy output samples.
uint maxSamples ///< How many samples to receive at max.
) override;
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
/// sample buffer without copying them anywhere.
///
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
/// with 'ptrBegin' function.
virtual uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
) override;
/// Returns number of samples currently available.
virtual uint numSamples() const override;
/// Sets number of channels, 1 = mono, 2 = stereo.
void setChannels(int numChannels);
/// Get number of channels
int getChannels()
{
return channels;
}
/// Returns nonzero if there aren't any samples available for outputting.
virtual int isEmpty() const override;
/// Clears all the samples.
virtual void clear() override;
/// allow trimming (downwards) amount of samples in pipeline.
/// Returns adjusted amount of samples
uint adjustAmountOfSamples(uint numSamples) override;
/// Add silence to end of buffer
void addSilent(uint nSamples);
};
}
#endif

230
lib/soundtouch-master/include/soundtouch/FIFOSamplePipe.h

@ -0,0 +1,230 @@
////////////////////////////////////////////////////////////////////////////////
///
/// 'FIFOSamplePipe' : An abstract base class for classes that manipulate sound
/// samples by operating like a first-in-first-out pipe: New samples are fed
/// into one end of the pipe with the 'putSamples' function, and the processed
/// samples are received from the other end with the 'receiveSamples' function.
///
/// 'FIFOProcessor' : A base class for classes the do signal processing with
/// the samples while operating like a first-in-first-out pipe. When samples
/// are input with the 'putSamples' function, the class processes them
/// and moves the processed samples to the given 'output' pipe object, which
/// may be either another processing stage, or a fifo sample buffer object.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef FIFOSamplePipe_H
#define FIFOSamplePipe_H
#include <assert.h>
#include <stdlib.h>
#include "STTypes.h"
namespace soundtouch
{
/// Abstract base class for FIFO (first-in-first-out) sample processing classes.
class FIFOSamplePipe
{
protected:
bool verifyNumberOfChannels(int nChannels) const
{
if ((nChannels > 0) && (nChannels <= SOUNDTOUCH_MAX_CHANNELS))
{
return true;
}
ST_THROW_RT_ERROR("Error: Illegal number of channels");
return false;
}
public:
// virtual default destructor
virtual ~FIFOSamplePipe() {}
/// Returns a pointer to the beginning of the output samples.
/// This function is provided for accessing the output samples directly.
/// Please be careful for not to corrupt the book-keeping!
///
/// When using this function to output samples, also remember to 'remove' the
/// output samples from the buffer by calling the
/// 'receiveSamples(numSamples)' function
virtual SAMPLETYPE *ptrBegin() = 0;
/// Adds 'numSamples' pcs of samples from the 'samples' memory position to
/// the sample buffer.
virtual void putSamples(const SAMPLETYPE *samples, ///< Pointer to samples.
uint numSamples ///< Number of samples to insert.
) = 0;
// Moves samples from the 'other' pipe instance to this instance.
void moveSamples(FIFOSamplePipe &other ///< Other pipe instance where from the receive the data.
)
{
const uint oNumSamples = other.numSamples();
putSamples(other.ptrBegin(), oNumSamples);
other.receiveSamples(oNumSamples);
}
/// Output samples from beginning of the sample buffer. Copies requested samples to
/// output buffer and removes them from the sample buffer. If there are less than
/// 'numsample' samples in the buffer, returns all that available.
///
/// \return Number of samples returned.
virtual uint receiveSamples(SAMPLETYPE *output, ///< Buffer where to copy output samples.
uint maxSamples ///< How many samples to receive at max.
) = 0;
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
/// sample buffer without copying them anywhere.
///
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
/// with 'ptrBegin' function.
virtual uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
) = 0;
/// Returns number of samples currently available.
virtual uint numSamples() const = 0;
// Returns nonzero if there aren't any samples available for outputting.
virtual int isEmpty() const = 0;
/// Clears all the samples.
virtual void clear() = 0;
/// allow trimming (downwards) amount of samples in pipeline.
/// Returns adjusted amount of samples
virtual uint adjustAmountOfSamples(uint numSamples) = 0;
};
/// Base-class for sound processing routines working in FIFO principle. With this base
/// class it's easy to implement sound processing stages that can be chained together,
/// so that samples that are fed into beginning of the pipe automatically go through
/// all the processing stages.
///
/// When samples are input to this class, they're first processed and then put to
/// the FIFO pipe that's defined as output of this class. This output pipe can be
/// either other processing stage or a FIFO sample buffer.
class FIFOProcessor :public FIFOSamplePipe
{
protected:
/// Internal pipe where processed samples are put.
FIFOSamplePipe *output;
/// Sets output pipe.
void setOutPipe(FIFOSamplePipe *pOutput)
{
assert(output == nullptr);
assert(pOutput != nullptr);
output = pOutput;
}
/// Constructor. Doesn't define output pipe; it has to be set be
/// 'setOutPipe' function.
FIFOProcessor()
{
output = nullptr;
}
/// Constructor. Configures output pipe.
FIFOProcessor(FIFOSamplePipe *pOutput ///< Output pipe.
)
{
output = pOutput;
}
/// Destructor.
virtual ~FIFOProcessor() override
{
}
/// Returns a pointer to the beginning of the output samples.
/// This function is provided for accessing the output samples directly.
/// Please be careful for not to corrupt the book-keeping!
///
/// When using this function to output samples, also remember to 'remove' the
/// output samples from the buffer by calling the
/// 'receiveSamples(numSamples)' function
virtual SAMPLETYPE *ptrBegin() override
{
return output->ptrBegin();
}
public:
/// Output samples from beginning of the sample buffer. Copies requested samples to
/// output buffer and removes them from the sample buffer. If there are less than
/// 'numsample' samples in the buffer, returns all that available.
///
/// \return Number of samples returned.
virtual uint receiveSamples(SAMPLETYPE *outBuffer, ///< Buffer where to copy output samples.
uint maxSamples ///< How many samples to receive at max.
) override
{
return output->receiveSamples(outBuffer, maxSamples);
}
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
/// sample buffer without copying them anywhere.
///
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
/// with 'ptrBegin' function.
virtual uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
) override
{
return output->receiveSamples(maxSamples);
}
/// Returns number of samples currently available.
virtual uint numSamples() const override
{
return output->numSamples();
}
/// Returns nonzero if there aren't any samples available for outputting.
virtual int isEmpty() const override
{
return output->isEmpty();
}
/// allow trimming (downwards) amount of samples in pipeline.
/// Returns adjusted amount of samples
virtual uint adjustAmountOfSamples(uint numSamples) override
{
return output->adjustAmountOfSamples(numSamples);
}
};
}
#endif

176
lib/soundtouch-master/include/soundtouch/STTypes.h

@ -0,0 +1,176 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Common type definitions for SoundTouch audio processing library.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef STTypes_H
#define STTypes_H
typedef unsigned int uint;
typedef unsigned long ulong;
// Patch for MinGW: on Win64 long is 32-bit
#ifdef _WIN64
typedef unsigned long long ulongptr;
#else
typedef ulong ulongptr;
#endif
// Helper macro for aligning pointer up to next 16-byte boundary
#define SOUNDTOUCH_ALIGN_POINTER_16(x) ( ( (ulongptr)(x) + 15 ) & ~(ulongptr)15 )
namespace soundtouch
{
/// Max allowed number of channels
#define SOUNDTOUCH_MAX_CHANNELS 16
/// Activate these undef's to overrule the possible sampletype
/// setting inherited from some other header file:
//#undef SOUNDTOUCH_INTEGER_SAMPLES
//#undef SOUNDTOUCH_FLOAT_SAMPLES
/// If following flag is defined, always uses multichannel processing
/// routines also for mono and stero sound. This is for routine testing
/// purposes; output should be same with either routines, yet disabling
/// the dedicated mono/stereo processing routines will result in slower
/// runtime performance so recommendation is to keep this off.
// #define USE_MULTICH_ALWAYS
#if !(SOUNDTOUCH_INTEGER_SAMPLES || SOUNDTOUCH_FLOAT_SAMPLES)
/// Choose either 32bit floating point or 16bit integer sampletype
/// by choosing one of the following defines, unless this selection
/// has already been done in some other file.
////
/// Notes:
/// - In Windows environment, choose the sample format with the
/// following defines.
/// - In GNU environment, the floating point samples are used by
/// default, but integer samples can be chosen by giving the
/// following switch to the configure script:
/// ./configure --enable-integer-samples
/// However, if you still prefer to select the sample format here
/// also in GNU environment, then please #undef the INTEGER_SAMPLE
/// and FLOAT_SAMPLE defines first as in comments above.
//#define SOUNDTOUCH_INTEGER_SAMPLES 1 //< 16bit integer samples
#define SOUNDTOUCH_FLOAT_SAMPLES 1 //< 32bit float samples
#endif
#if (_M_IX86 || __i386__ || __x86_64__ || _M_X64)
/// Define this to allow X86-specific assembler/intrinsic optimizations.
/// Notice that library contains also usual C++ versions of each of these
/// these routines, so if you're having difficulties getting the optimized
/// routines compiled for whatever reason, you may disable these optimizations
/// to make the library compile.
#define SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS 1
#undef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS
#elif defined(__arm__) || defined(__aarch64__)
#undef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
#define SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS 1
#else
/// Always disable optimizations when not using a x86 systems.
#undef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
#undef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS
#endif
// If defined, allows the SIMD-optimized routines to skip unevenly aligned
// memory offsets that can cause performance penalty in some SIMD implementations.
// Causes slight compromise in sound quality.
// #define SOUNDTOUCH_ALLOW_NONEXACT_SIMD_OPTIMIZATION 1
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
// 16bit integer sample type
typedef short SAMPLETYPE;
// data type for sample accumulation: Use 32bit integer to prevent overflows
typedef long LONG_SAMPLETYPE;
#ifdef SOUNDTOUCH_FLOAT_SAMPLES
// check that only one sample type is defined
#error "conflicting sample types defined"
#endif // SOUNDTOUCH_FLOAT_SAMPLES
#ifdef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
// Allow MMX optimizations (not available in X64 mode)
#if (!_M_X64)
#define SOUNDTOUCH_ALLOW_MMX 1
#endif
#endif
#else
// floating point samples
typedef float SAMPLETYPE;
// data type for sample accumulation: Use float also here to enable
// efficient autovectorization
typedef float LONG_SAMPLETYPE;
#ifdef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
// Allow SSE optimizations
#define SOUNDTOUCH_ALLOW_SSE 1
#endif
#ifdef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS
#define SOUNDTOUCH_USE_NEON 1
#endif
#endif // SOUNDTOUCH_INTEGER_SAMPLES
#if ((SOUNDTOUCH_ALLOW_SSE) || (__SSE__) || (SOUNDTOUCH_USE_NEON))
#if SOUNDTOUCH_ALLOW_NONEXACT_SIMD_OPTIMIZATION
#define ST_SIMD_AVOID_UNALIGNED
#endif
#endif
}
// define ST_NO_EXCEPTION_HANDLING switch to disable throwing std exceptions:
#define ST_NO_EXCEPTION_HANDLING 1
#ifdef ST_NO_EXCEPTION_HANDLING
// Exceptions disabled. Throw asserts instead if enabled.
#include <assert.h>
#define ST_THROW_RT_ERROR(x) {assert((const char *)x);}
#else
// use c++ standard exceptions
#include <stdexcept>
#include <string>
#define ST_THROW_RT_ERROR(x) {throw std::runtime_error(x);}
#endif
// When this #define is active, eliminates a clicking sound when the "rate" or "pitch"
// parameter setting crosses from value <1 to >=1 or vice versa during processing.
// Default is off as such crossover is untypical case and involves a slight sound
// quality compromise.
//#define SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER 1
#endif

348
lib/soundtouch-master/include/soundtouch/SoundTouch.h

@ -0,0 +1,348 @@
//////////////////////////////////////////////////////////////////////////////
///
/// SoundTouch - main class for tempo/pitch/rate adjusting routines.
///
/// Notes:
/// - Initialize the SoundTouch object instance by setting up the sound stream
/// parameters with functions 'setSampleRate' and 'setChannels', then set
/// desired tempo/pitch/rate settings with the corresponding functions.
///
/// - The SoundTouch class behaves like a first-in-first-out pipeline: The
/// samples that are to be processed are fed into one of the pipe by calling
/// function 'putSamples', while the ready processed samples can be read
/// from the other end of the pipeline with function 'receiveSamples'.
///
/// - The SoundTouch processing classes require certain sized 'batches' of
/// samples in order to process the sound. For this reason the classes buffer
/// incoming samples until there are enough of samples available for
/// processing, then they carry out the processing step and consequently
/// make the processed samples available for outputting.
///
/// - For the above reason, the processing routines introduce a certain
/// 'latency' between the input and output, so that the samples input to
/// SoundTouch may not be immediately available in the output, and neither
/// the amount of outputtable samples may not immediately be in direct
/// relationship with the amount of previously input samples.
///
/// - The tempo/pitch/rate control parameters can be altered during processing.
/// Please notice though that they aren't currently protected by semaphores,
/// so in multi-thread application external semaphore protection may be
/// required.
///
/// - This class utilizes classes 'TDStretch' for tempo change (without modifying
/// pitch) and 'RateTransposer' for changing the playback rate (that is, both
/// tempo and pitch in the same ratio) of the sound. The third available control
/// 'pitch' (change pitch but maintain tempo) is produced by a combination of
/// combining the two other controls.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef SoundTouch_H
#define SoundTouch_H
#include "FIFOSamplePipe.h"
#include "STTypes.h"
namespace soundtouch
{
/// Soundtouch library version string
#define SOUNDTOUCH_VERSION "2.3.3"
/// SoundTouch library version id
#define SOUNDTOUCH_VERSION_ID (20303)
//
// Available setting IDs for the 'setSetting' & 'get_setting' functions:
/// Enable/disable anti-alias filter in pitch transposer (0 = disable)
#define SETTING_USE_AA_FILTER 0
/// Pitch transposer anti-alias filter length (8 .. 128 taps, default = 32)
#define SETTING_AA_FILTER_LENGTH 1
/// Enable/disable quick seeking algorithm in tempo changer routine
/// (enabling quick seeking lowers CPU utilization but causes a minor sound
/// quality compromising)
#define SETTING_USE_QUICKSEEK 2
/// Time-stretch algorithm single processing sequence length in milliseconds. This determines
/// to how long sequences the original sound is chopped in the time-stretch algorithm.
/// See "STTypes.h" or README for more information.
#define SETTING_SEQUENCE_MS 3
/// Time-stretch algorithm seeking window length in milliseconds for algorithm that finds the
/// best possible overlapping location. This determines from how wide window the algorithm
/// may look for an optimal joining location when mixing the sound sequences back together.
/// See "STTypes.h" or README for more information.
#define SETTING_SEEKWINDOW_MS 4
/// Time-stretch algorithm overlap length in milliseconds. When the chopped sound sequences
/// are mixed back together, to form a continuous sound stream, this parameter defines over
/// how long period the two consecutive sequences are let to overlap each other.
/// See "STTypes.h" or README for more information.
#define SETTING_OVERLAP_MS 5
/// Call "getSetting" with this ID to query processing sequence size in samples.
/// This value gives approximate value of how many input samples you'll need to
/// feed into SoundTouch after initial buffering to get out a new batch of
/// output samples.
///
/// This value does not include initial buffering at beginning of a new processing
/// stream, use SETTING_INITIAL_LATENCY to get the initial buffering size.
///
/// Notices:
/// - This is read-only parameter, i.e. setSetting ignores this parameter
/// - This parameter value is not constant but change depending on
/// tempo/pitch/rate/samplerate settings.
#define SETTING_NOMINAL_INPUT_SEQUENCE 6
/// Call "getSetting" with this ID to query nominal average processing output
/// size in samples. This value tells approcimate value how many output samples
/// SoundTouch outputs once it does DSP processing run for a batch of input samples.
///
/// Notices:
/// - This is read-only parameter, i.e. setSetting ignores this parameter
/// - This parameter value is not constant but change depending on
/// tempo/pitch/rate/samplerate settings.
#define SETTING_NOMINAL_OUTPUT_SEQUENCE 7
/// Call "getSetting" with this ID to query initial processing latency, i.e.
/// approx. how many samples you'll need to enter to SoundTouch pipeline before
/// you can expect to get first batch of ready output samples out.
///
/// After the first output batch, you can then expect to get approx.
/// SETTING_NOMINAL_OUTPUT_SEQUENCE ready samples out for every
/// SETTING_NOMINAL_INPUT_SEQUENCE samples that you enter into SoundTouch.
///
/// Example:
/// processing with parameter -tempo=5
/// => initial latency = 5509 samples
/// input sequence = 4167 samples
/// output sequence = 3969 samples
///
/// Accordingly, you can expect to feed in approx. 5509 samples at beginning of
/// the stream, and then you'll get out the first 3969 samples. After that, for
/// every approx. 4167 samples that you'll put in, you'll receive again approx.
/// 3969 samples out.
///
/// This also means that average latency during stream processing is
/// INITIAL_LATENCY-OUTPUT_SEQUENCE/2, in the above example case 5509-3969/2
/// = 3524 samples
///
/// Notices:
/// - This is read-only parameter, i.e. setSetting ignores this parameter
/// - This parameter value is not constant but change depending on
/// tempo/pitch/rate/samplerate settings.
#define SETTING_INITIAL_LATENCY 8
class SoundTouch final : public FIFOProcessor
{
private:
/// Rate transposer class instance
class RateTransposer *pRateTransposer;
/// Time-stretch class instance
class TDStretch *pTDStretch;
/// Virtual pitch parameter. Effective rate & tempo are calculated from these parameters.
double virtualRate;
/// Virtual pitch parameter. Effective rate & tempo are calculated from these parameters.
double virtualTempo;
/// Virtual pitch parameter. Effective rate & tempo are calculated from these parameters.
double virtualPitch;
/// Flag: Has sample rate been set?
bool bSrateSet;
/// Accumulator for how many samples in total will be expected as output vs. samples put in,
/// considering current processing settings.
double samplesExpectedOut;
/// Accumulator for how many samples in total have been read out from the processing so far
long samplesOutput;
/// Calculates effective rate & tempo valuescfrom 'virtualRate', 'virtualTempo' and
/// 'virtualPitch' parameters.
void calcEffectiveRateAndTempo();
protected :
/// Number of channels
uint channels;
/// Effective 'rate' value calculated from 'virtualRate', 'virtualTempo' and 'virtualPitch'
double rate;
/// Effective 'tempo' value calculated from 'virtualRate', 'virtualTempo' and 'virtualPitch'
double tempo;
public:
SoundTouch();
~SoundTouch() override;
/// Get SoundTouch library version string
static const char *getVersionString();
/// Get SoundTouch library version Id
static uint getVersionId();
/// Sets new rate control value. Normal rate = 1.0, smaller values
/// represent slower rate, larger faster rates.
void setRate(double newRate);
/// Sets new tempo control value. Normal tempo = 1.0, smaller values
/// represent slower tempo, larger faster tempo.
void setTempo(double newTempo);
/// Sets new rate control value as a difference in percents compared
/// to the original rate (-50 .. +100 %)
void setRateChange(double newRate);
/// Sets new tempo control value as a difference in percents compared
/// to the original tempo (-50 .. +100 %)
void setTempoChange(double newTempo);
/// Sets new pitch control value. Original pitch = 1.0, smaller values
/// represent lower pitches, larger values higher pitch.
void setPitch(double newPitch);
/// Sets pitch change in octaves compared to the original pitch
/// (-1.00 .. +1.00)
void setPitchOctaves(double newPitch);
/// Sets pitch change in semi-tones compared to the original pitch
/// (-12 .. +12)
void setPitchSemiTones(int newPitch);
void setPitchSemiTones(double newPitch);
/// Sets the number of channels, 1 = mono, 2 = stereo
void setChannels(uint numChannels);
/// Sets sample rate.
void setSampleRate(uint srate);
/// Get ratio between input and output audio durations, useful for calculating
/// processed output duration: if you'll process a stream of N samples, then
/// you can expect to get out N * getInputOutputSampleRatio() samples.
///
/// This ratio will give accurate target duration ratio for a full audio track,
/// given that the the whole track is processed with same processing parameters.
///
/// If this ratio is applied to calculate intermediate offsets inside a processing
/// stream, then this ratio is approximate and can deviate +- some tens of milliseconds
/// from ideal offset, yet by end of the audio stream the duration ratio will become
/// exact.
///
/// Example: if processing with parameters "-tempo=15 -pitch=-3", the function
/// will return value 0.8695652... Now, if processing an audio stream whose duration
/// is exactly one million audio samples, then you can expect the processed
/// output duration be 0.869565 * 1000000 = 869565 samples.
double getInputOutputSampleRatio();
/// Flushes the last samples from the processing pipeline to the output.
/// Clears also the internal processing buffers.
//
/// Note: This function is meant for extracting the last samples of a sound
/// stream. This function may introduce additional blank samples in the end
/// of the sound stream, and thus it's not recommended to call this function
/// in the middle of a sound stream.
void flush();
/// Adds 'numSamples' pcs of samples from the 'samples' memory position into
/// the input of the object. Notice that sample rate _has_to_ be set before
/// calling this function, otherwise throws a runtime_error exception.
void putSamples(
const SAMPLETYPE *samples, ///< Pointer to sample buffer.
uint numSamples ///< Number of samples in buffer. Notice
///< that in case of stereo-sound a single sample
///< contains data for both channels.
) override;
/// Output samples from beginning of the sample buffer. Copies requested samples to
/// output buffer and removes them from the sample buffer. If there are less than
/// 'numsample' samples in the buffer, returns all that available.
///
/// \return Number of samples returned.
uint receiveSamples(SAMPLETYPE *output, ///< Buffer where to copy output samples.
uint maxSamples ///< How many samples to receive at max.
) override;
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
/// sample buffer without copying them anywhere.
///
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
/// with 'ptrBegin' function.
uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
) override;
/// Clears all the samples in the object's output and internal processing
/// buffers.
void clear() override;
/// Changes a setting controlling the processing system behaviour. See the
/// 'SETTING_...' defines for available setting ID's.
///
/// \return 'true' if the setting was successfully changed
bool setSetting(int settingId, ///< Setting ID number. see SETTING_... defines.
int value ///< New setting value.
);
/// Reads a setting controlling the processing system behaviour. See the
/// 'SETTING_...' defines for available setting ID's.
///
/// \return the setting value.
int getSetting(int settingId ///< Setting ID number, see SETTING_... defines.
) const;
/// Returns number of samples currently unprocessed.
uint numUnprocessedSamples() const;
/// Return number of channels
uint numChannels() const
{
return channels;
}
/// Other handy functions that are implemented in the ancestor classes (see
/// classes 'FIFOProcessor' and 'FIFOSamplePipe')
///
/// - receiveSamples() : Use this function to receive 'ready' processed samples from SoundTouch.
/// - numSamples() : Get number of 'ready' samples that can be received with
/// function 'receiveSamples()'
/// - isEmpty() : Returns nonzero if there aren't any 'ready' samples.
/// - clear() : Clears all samples from ready/processing buffers.
};
}
#endif

177
lib/soundtouch-master/include/soundtouch/SoundTouchDLL.h

@ -0,0 +1,177 @@
//////////////////////////////////////////////////////////////////////////////
///
/// SoundTouch DLL wrapper - wraps SoundTouch routines into a Dynamic Load
/// Library interface.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef _SoundTouchDLL_h_
#define _SoundTouchDLL_h_
#if defined(_WIN32) || defined(WIN32)
// Windows
#ifndef __cplusplus
#error "Expected g++"
#endif
#ifdef DLL_EXPORTS
#define SOUNDTOUCHDLL_API extern "C" __declspec(dllexport)
#else
#define SOUNDTOUCHDLL_API extern "C" __declspec(dllimport)
#endif
#else
// GNU version
#if defined(DLL_EXPORTS) || defined(SoundTouchDLL_EXPORTS)
// GCC declaration for exporting functions
#define SOUNDTOUCHDLL_API extern "C" __attribute__((__visibility__("default")))
#else
// import function
#define SOUNDTOUCHDLL_API extern "C"
#endif
// Linux-replacements for Windows declarations:
#define __cdecl
typedef unsigned int DWORD;
#define FALSE 0
#define TRUE 1
#endif
typedef void * ST_HANDLE;
/// Create a new instance of SoundTouch processor.
SOUNDTOUCHDLL_API ST_HANDLE __cdecl soundtouch_createInstance();
/// Destroys a SoundTouch processor instance.
SOUNDTOUCHDLL_API void __cdecl soundtouch_destroyInstance(ST_HANDLE h);
/// Get SoundTouch library version string
SOUNDTOUCHDLL_API const char *__cdecl soundtouch_getVersionString();
/// Get SoundTouch library version Id
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_getVersionId();
/// Sets new rate control value. Normal rate = 1.0, smaller values
/// represent slower rate, larger faster rates.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRate(ST_HANDLE h, float newRate);
/// Sets new tempo control value. Normal tempo = 1.0, smaller values
/// represent slower tempo, larger faster tempo.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setTempo(ST_HANDLE h, float newTempo);
/// Sets new rate control value as a difference in percents compared
/// to the original rate (-50 .. +100 %);
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRateChange(ST_HANDLE h, float newRate);
/// Sets new tempo control value as a difference in percents compared
/// to the original tempo (-50 .. +100 %);
SOUNDTOUCHDLL_API void __cdecl soundtouch_setTempoChange(ST_HANDLE h, float newTempo);
/// Sets new pitch control value. Original pitch = 1.0, smaller values
/// represent lower pitches, larger values higher pitch.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitch(ST_HANDLE h, float newPitch);
/// Sets pitch change in octaves compared to the original pitch
/// (-1.00 .. +1.00);
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitchOctaves(ST_HANDLE h, float newPitch);
/// Sets pitch change in semi-tones compared to the original pitch
/// (-12 .. +12);
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitchSemiTones(ST_HANDLE h, float newPitch);
/// Sets the number of channels, 1 = mono, 2 = stereo, n = multichannel
SOUNDTOUCHDLL_API void __cdecl soundtouch_setChannels(ST_HANDLE h, unsigned int numChannels);
/// Sets sample rate.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setSampleRate(ST_HANDLE h, unsigned int srate);
/// Flushes the last samples from the processing pipeline to the output.
/// Clears also the internal processing buffers.
//
/// Note: This function is meant for extracting the last samples of a sound
/// stream. This function may introduce additional blank samples in the end
/// of the sound stream, and thus it's not recommended to call this function
/// in the middle of a sound stream.
SOUNDTOUCHDLL_API void __cdecl soundtouch_flush(ST_HANDLE h);
/// Adds 'numSamples' pcs of samples from the 'samples' memory position into
/// the input of the object. Notice that sample rate _has_to_ be set before
/// calling this function, otherwise throws a runtime_error exception.
SOUNDTOUCHDLL_API void __cdecl soundtouch_putSamples(ST_HANDLE h,
const float *samples, ///< Pointer to sample buffer.
unsigned int numSamples ///< Number of sample frames in buffer. Notice
///< that in case of multi-channel sound a single
///< sample frame contains data for all channels.
);
/// Clears all the samples in the object's output and internal processing
/// buffers.
SOUNDTOUCHDLL_API void __cdecl soundtouch_clear(ST_HANDLE h);
/// Changes a setting controlling the processing system behaviour. See the
/// 'SETTING_...' defines for available setting ID's.
///
/// \return 'nonzero' if the setting was successfully changed, otherwise zero
SOUNDTOUCHDLL_API int __cdecl soundtouch_setSetting(ST_HANDLE h,
int settingId, ///< Setting ID number. see SETTING_... defines.
int value ///< New setting value.
);
/// Reads a setting controlling the processing system behaviour. See the
/// 'SETTING_...' defines for available setting ID's.
///
/// \return the setting value.
SOUNDTOUCHDLL_API int __cdecl soundtouch_getSetting(ST_HANDLE h,
int settingId ///< Setting ID number, see SETTING_... defines.
);
/// Returns number of samples currently unprocessed.
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_numUnprocessedSamples(ST_HANDLE h);
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
/// sample buffer without copying them anywhere.
///
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
/// with 'ptrBegin' function.
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_receiveSamples(ST_HANDLE h,
float *outBuffer, ///< Buffer where to copy output samples.
unsigned int maxSamples ///< How many samples to receive at max.
);
/// Returns number of samples currently available.
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_numSamples(ST_HANDLE h);
/// Returns nonzero if there aren't any samples available for outputting.
SOUNDTOUCHDLL_API int __cdecl soundtouch_isEmpty(ST_HANDLE h);
#endif // _SoundTouchDLL_h_

66
lib/soundtouch-master/readme.md

@ -0,0 +1,66 @@
# SoundTouch library
## About
SoundTouch is an open-source audio processing library that allows changing the sound tempo, pitch and playback rate parameters independently from each other:
* Change **tempo** while maintaining the original pitch
* Change **pitch** while maintaining the original tempo
* Change **playback rate** that affects both tempo and pitch at the
same time
* Change any combination of tempo/pitch/rate
Visit [SoundTouch website](https://www.surina.net/soundtouch) and see the [README file](https://www.surina.net/soundtouch/readme.html) for more information and audio examples.
### The latest stable release is 2.3.3
## Example
Use SoundStretch example app for modifying wav audio files, for example as follows:
```
soundstretch my_original_file.wav output_file.wav -tempo=+15 -pitch=-3
```
See the [README file](http://soundtouch.surina.net/README.html) for more usage examples and instructions how to build SoundTouch + SoundStretch.
Ready [SoundStretch application executables](https://www.surina.net/soundtouch/download.html) are available for download for Windows and Mac OS.
## Language & Platforms
SoundTouch is written in C++ and compiles in virtually any platform:
* Windows
* Mac OS
* Linux & Unices (including also Raspberry, Beaglebone, Yocto etc embedded Linux flavors)
* Android
* iOS
* embedded systems
The source code package includes dynamic library import modules for C#, Java and Pascal/Delphi languages.
## Tarballs
Source code release tarballs:
* https://www.surina.net/soundtouch/soundtouch-2.3.3.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.3.2.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.3.1.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.3.0.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.2.0.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.1.2.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.1.1.tar.gz
* https://www.surina.net/soundtouch/soundtouch-2.0.0.tar.gz
## License
SoundTouch is released under LGPL v2.1:
This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation.
This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
See [LGPL v2.1 full license text ](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) for details.
--
Also commercial license free of GPL limitations available upon request

222
lib/soundtouch-master/source/SoundTouch/AAFilter.cpp

@ -0,0 +1,222 @@
////////////////////////////////////////////////////////////////////////////////
///
/// FIR low-pass (anti-alias) filter with filter coefficient design routine and
/// MMX optimization.
///
/// Anti-alias filter is used to prevent folding of high frequencies when
/// transposing the sample rate with interpolation.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <memory.h>
#include <assert.h>
#include <math.h>
#include <stdlib.h>
#include "AAFilter.h"
#include "FIRFilter.h"
using namespace soundtouch;
#define PI 3.14159265358979323846
#define TWOPI (2 * PI)
// define this to save AA filter coefficients to a file
// #define _DEBUG_SAVE_AAFILTER_COEFFICIENTS 1
#ifdef _DEBUG_SAVE_AAFILTER_COEFFICIENTS
#include <stdio.h>
static void _DEBUG_SAVE_AAFIR_COEFFS(SAMPLETYPE *coeffs, int len)
{
FILE *fptr = fopen("aa_filter_coeffs.txt", "wt");
if (fptr == nullptr) return;
for (int i = 0; i < len; i ++)
{
double temp = coeffs[i];
fprintf(fptr, "%lf\n", temp);
}
fclose(fptr);
}
#else
#define _DEBUG_SAVE_AAFIR_COEFFS(x, y)
#endif
/*****************************************************************************
*
* Implementation of the class 'AAFilter'
*
*****************************************************************************/
AAFilter::AAFilter(uint len)
{
pFIR = FIRFilter::newInstance();
cutoffFreq = 0.5;
setLength(len);
}
AAFilter::~AAFilter()
{
delete pFIR;
}
// Sets new anti-alias filter cut-off edge frequency, scaled to
// sampling frequency (nyquist frequency = 0.5).
// The filter will cut frequencies higher than the given frequency.
void AAFilter::setCutoffFreq(double newCutoffFreq)
{
cutoffFreq = newCutoffFreq;
calculateCoeffs();
}
// Sets number of FIR filter taps
void AAFilter::setLength(uint newLength)
{
length = newLength;
calculateCoeffs();
}
// Calculates coefficients for a low-pass FIR filter using Hamming window
void AAFilter::calculateCoeffs()
{
uint i;
double cntTemp, temp, tempCoeff,h, w;
double wc;
double scaleCoeff, sum;
double *work;
SAMPLETYPE *coeffs;
assert(length >= 2);
assert(length % 4 == 0);
assert(cutoffFreq >= 0);
assert(cutoffFreq <= 0.5);
work = new double[length];
coeffs = new SAMPLETYPE[length];
wc = 2.0 * PI * cutoffFreq;
tempCoeff = TWOPI / (double)length;
sum = 0;
for (i = 0; i < length; i ++)
{
cntTemp = (double)i - (double)(length / 2);
temp = cntTemp * wc;
if (temp != 0)
{
h = sin(temp) / temp; // sinc function
}
else
{
h = 1.0;
}
w = 0.54 + 0.46 * cos(tempCoeff * cntTemp); // hamming window
temp = w * h;
work[i] = temp;
// calc net sum of coefficients
sum += temp;
}
// ensure the sum of coefficients is larger than zero
assert(sum > 0);
// ensure we've really designed a lowpass filter...
assert(work[length/2] > 0);
assert(work[length/2 + 1] > -1e-6);
assert(work[length/2 - 1] > -1e-6);
// Calculate a scaling coefficient in such a way that the result can be
// divided by 16384
scaleCoeff = 16384.0f / sum;
for (i = 0; i < length; i ++)
{
temp = work[i] * scaleCoeff;
// scale & round to nearest integer
temp += (temp >= 0) ? 0.5 : -0.5;
// ensure no overfloods
assert(temp >= -32768 && temp <= 32767);
coeffs[i] = (SAMPLETYPE)temp;
}
// Set coefficients. Use divide factor 14 => divide result by 2^14 = 16384
pFIR->setCoefficients(coeffs, length, 14);
_DEBUG_SAVE_AAFIR_COEFFS(coeffs, length);
delete[] work;
delete[] coeffs;
}
// Applies the filter to the given sequence of samples.
// Note : The amount of outputted samples is by value of 'filter length'
// smaller than the amount of input samples.
uint AAFilter::evaluate(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels) const
{
return pFIR->evaluate(dest, src, numSamples, numChannels);
}
/// Applies the filter to the given src & dest pipes, so that processed amount of
/// samples get removed from src, and produced amount added to dest
/// Note : The amount of outputted samples is by value of 'filter length'
/// smaller than the amount of input samples.
uint AAFilter::evaluate(FIFOSampleBuffer &dest, FIFOSampleBuffer &src) const
{
SAMPLETYPE *pdest;
const SAMPLETYPE *psrc;
uint numSrcSamples;
uint result;
int numChannels = src.getChannels();
assert(numChannels == dest.getChannels());
numSrcSamples = src.numSamples();
psrc = src.ptrBegin();
pdest = dest.ptrEnd(numSrcSamples);
result = pFIR->evaluate(pdest, psrc, numSrcSamples, numChannels);
src.receiveSamples(result);
dest.putSamples(result);
return result;
}
uint AAFilter::getLength() const
{
return pFIR->getLength();
}

93
lib/soundtouch-master/source/SoundTouch/AAFilter.h

@ -0,0 +1,93 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Sampled sound tempo changer/time stretch algorithm. Changes the sound tempo
/// while maintaining the original pitch by using a time domain WSOLA-like method
/// with several performance-increasing tweaks.
///
/// Anti-alias filter is used to prevent folding of high frequencies when
/// transposing the sample rate with interpolation.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef AAFilter_H
#define AAFilter_H
#include "STTypes.h"
#include "FIFOSampleBuffer.h"
namespace soundtouch
{
class AAFilter
{
protected:
class FIRFilter *pFIR;
/// Low-pass filter cut-off frequency, negative = invalid
double cutoffFreq;
/// num of filter taps
uint length;
/// Calculate the FIR coefficients realizing the given cutoff-frequency
void calculateCoeffs();
public:
AAFilter(uint length);
~AAFilter();
/// Sets new anti-alias filter cut-off edge frequency, scaled to sampling
/// frequency (nyquist frequency = 0.5). The filter will cut off the
/// frequencies than that.
void setCutoffFreq(double newCutoffFreq);
/// Sets number of FIR filter taps, i.e. ~filter complexity
void setLength(uint newLength);
uint getLength() const;
/// Applies the filter to the given sequence of samples.
/// Note : The amount of outputted samples is by value of 'filter length'
/// smaller than the amount of input samples.
uint evaluate(SAMPLETYPE *dest,
const SAMPLETYPE *src,
uint numSamples,
uint numChannels) const;
/// Applies the filter to the given src & dest pipes, so that processed amount of
/// samples get removed from src, and produced amount added to dest
/// Note : The amount of outputted samples is by value of 'filter length'
/// smaller than the amount of input samples.
uint evaluate(FIFOSampleBuffer &dest,
FIFOSampleBuffer &src) const;
};
}
#endif

569
lib/soundtouch-master/source/SoundTouch/BPMDetect.cpp

@ -0,0 +1,569 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Beats-per-minute (BPM) detection routine.
///
/// The beat detection algorithm works as follows:
/// - Use function 'inputSamples' to input a chunks of samples to the class for
/// analysis. It's a good idea to enter a large sound file or stream in smallish
/// chunks of around few kilosamples in order not to extinguish too much RAM memory.
/// - Inputted sound data is decimated to approx 500 Hz to reduce calculation burden,
/// which is basically ok as low (bass) frequencies mostly determine the beat rate.
/// Simple averaging is used for anti-alias filtering because the resulting signal
/// quality isn't of that high importance.
/// - Decimated sound data is enveloped, i.e. the amplitude shape is detected by
/// taking absolute value that's smoothed by sliding average. Signal levels that
/// are below a couple of times the general RMS amplitude level are cut away to
/// leave only notable peaks there.
/// - Repeating sound patterns (e.g. beats) are detected by calculating short-term
/// autocorrelation function of the enveloped signal.
/// - After whole sound data file has been analyzed as above, the bpm level is
/// detected by function 'getBpm' that finds the highest peak of the autocorrelation
/// function, calculates it's precise location and converts this reading to bpm's.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#define _USE_MATH_DEFINES
#include <math.h>
#include <assert.h>
#include <string.h>
#include <stdio.h>
#include <cfloat>
#include "FIFOSampleBuffer.h"
#include "PeakFinder.h"
#include "BPMDetect.h"
using namespace soundtouch;
// algorithm input sample block size
static const int INPUT_BLOCK_SIZE = 2048;
// decimated sample block size
static const int DECIMATED_BLOCK_SIZE = 256;
/// Target sample rate after decimation
static const int TARGET_SRATE = 1000;
/// XCorr update sequence size, update in about 200msec chunks
static const int XCORR_UPDATE_SEQUENCE = (int)(TARGET_SRATE / 5);
/// Moving average N size
static const int MOVING_AVERAGE_N = 15;
/// XCorr decay time constant, decay to half in 30 seconds
/// If it's desired to have the system adapt quicker to beat rate
/// changes within a continuing music stream, then the
/// 'xcorr_decay_time_constant' value can be reduced, yet that
/// can increase possibility of glitches in bpm detection.
static const double XCORR_DECAY_TIME_CONSTANT = 30.0;
/// Data overlap factor for beat detection algorithm
static const int OVERLAP_FACTOR = 4;
static const double TWOPI = (2 * M_PI);
////////////////////////////////////////////////////////////////////////////////
// Enable following define to create bpm analysis file:
//#define _CREATE_BPM_DEBUG_FILE
#ifdef _CREATE_BPM_DEBUG_FILE
static void _SaveDebugData(const char *name, const float *data, int minpos, int maxpos, double coeff)
{
FILE *fptr = fopen(name, "wt");
int i;
if (fptr)
{
printf("\nWriting BPM debug data into file %s\n", name);
for (i = minpos; i < maxpos; i ++)
{
fprintf(fptr, "%d\t%.1lf\t%f\n", i, coeff / (double)i, data[i]);
}
fclose(fptr);
}
}
void _SaveDebugBeatPos(const char *name, const std::vector<BEAT> &beats)
{
printf("\nWriting beat detections data into file %s\n", name);
FILE *fptr = fopen(name, "wt");
if (fptr)
{
for (uint i = 0; i < beats.size(); i++)
{
BEAT b = beats[i];
fprintf(fptr, "%lf\t%lf\n", b.pos, b.strength);
}
fclose(fptr);
}
}
#else
#define _SaveDebugData(name, a,b,c,d)
#define _SaveDebugBeatPos(name, b)
#endif
// Hamming window
void hamming(float *w, int N)
{
for (int i = 0; i < N; i++)
{
w[i] = (float)(0.54 - 0.46 * cos(TWOPI * i / (N - 1)));
}
}
////////////////////////////////////////////////////////////////////////////////
//
// IIR2_filter - 2nd order IIR filter
IIR2_filter::IIR2_filter(const double *lpf_coeffs)
{
memcpy(coeffs, lpf_coeffs, 5 * sizeof(double));
memset(prev, 0, sizeof(prev));
}
float IIR2_filter::update(float x)
{
prev[0] = x;
double y = x * coeffs[0];
for (int i = 4; i >= 1; i--)
{
y += coeffs[i] * prev[i];
prev[i] = prev[i - 1];
}
prev[3] = y;
return (float)y;
}
// IIR low-pass filter coefficients, calculated with matlab/octave cheby2(2,40,0.05)
const double _LPF_coeffs[5] = { 0.00996655391939, -0.01944529148401, 0.00996655391939, 1.96867605796247, -0.96916387431724 };
////////////////////////////////////////////////////////////////////////////////
BPMDetect::BPMDetect(int numChannels, int aSampleRate) :
beat_lpf(_LPF_coeffs)
{
beats.reserve(250); // initial reservation to prevent frequent reallocation
this->sampleRate = aSampleRate;
this->channels = numChannels;
decimateSum = 0;
decimateCount = 0;
// choose decimation factor so that result is approx. 1000 Hz
decimateBy = sampleRate / TARGET_SRATE;
if ((decimateBy <= 0) || (decimateBy * DECIMATED_BLOCK_SIZE < INPUT_BLOCK_SIZE))
{
ST_THROW_RT_ERROR("Too small samplerate");
}
// Calculate window length & starting item according to desired min & max bpms
windowLen = (60 * sampleRate) / (decimateBy * MIN_BPM);
windowStart = (60 * sampleRate) / (decimateBy * MAX_BPM_RANGE);
assert(windowLen > windowStart);
// allocate new working objects
xcorr = new float[windowLen];
memset(xcorr, 0, windowLen * sizeof(float));
pos = 0;
peakPos = 0;
peakVal = 0;
init_scaler = 1;
beatcorr_ringbuffpos = 0;
beatcorr_ringbuff = new float[windowLen];
memset(beatcorr_ringbuff, 0, windowLen * sizeof(float));
// allocate processing buffer
buffer = new FIFOSampleBuffer();
// we do processing in mono mode
buffer->setChannels(1);
buffer->clear();
// calculate hamming windows
hamw = new float[XCORR_UPDATE_SEQUENCE];
hamming(hamw, XCORR_UPDATE_SEQUENCE);
hamw2 = new float[XCORR_UPDATE_SEQUENCE / 2];
hamming(hamw2, XCORR_UPDATE_SEQUENCE / 2);
}
BPMDetect::~BPMDetect()
{
delete[] xcorr;
delete[] beatcorr_ringbuff;
delete[] hamw;
delete[] hamw2;
delete buffer;
}
/// convert to mono, low-pass filter & decimate to about 500 Hz.
/// return number of outputted samples.
///
/// Decimation is used to remove the unnecessary frequencies and thus to reduce
/// the amount of data needed to be processed as calculating autocorrelation
/// function is a very-very heavy operation.
///
/// Anti-alias filtering is done simply by averaging the samples. This is really a
/// poor-man's anti-alias filtering, but it's not so critical in this kind of application
/// (it'd also be difficult to design a high-quality filter with steep cut-off at very
/// narrow band)
int BPMDetect::decimate(SAMPLETYPE *dest, const SAMPLETYPE *src, int numsamples)
{
int count, outcount;
LONG_SAMPLETYPE out;
assert(channels > 0);
assert(decimateBy > 0);
outcount = 0;
for (count = 0; count < numsamples; count ++)
{
int j;
// convert to mono and accumulate
for (j = 0; j < channels; j ++)
{
decimateSum += src[j];
}
src += j;
decimateCount ++;
if (decimateCount >= decimateBy)
{
// Store every Nth sample only
out = (LONG_SAMPLETYPE)(decimateSum / (decimateBy * channels));
decimateSum = 0;
decimateCount = 0;
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
// check ranges for sure (shouldn't actually be necessary)
if (out > 32767)
{
out = 32767;
}
else if (out < -32768)
{
out = -32768;
}
#endif // SOUNDTOUCH_INTEGER_SAMPLES
dest[outcount] = (SAMPLETYPE)out;
outcount ++;
}
}
return outcount;
}
// Calculates autocorrelation function of the sample history buffer
void BPMDetect::updateXCorr(int process_samples)
{
int offs;
SAMPLETYPE *pBuffer;
assert(buffer->numSamples() >= (uint)(process_samples + windowLen));
assert(process_samples == XCORR_UPDATE_SEQUENCE);
pBuffer = buffer->ptrBegin();
// calculate decay factor for xcorr filtering
float xcorr_decay = (float)pow(0.5, 1.0 / (XCORR_DECAY_TIME_CONSTANT * TARGET_SRATE / process_samples));
// prescale pbuffer
float tmp[XCORR_UPDATE_SEQUENCE];
for (int i = 0; i < process_samples; i++)
{
tmp[i] = hamw[i] * hamw[i] * pBuffer[i];
}
for (offs = windowStart; offs < windowLen; offs ++)
{
float sum;
int i;
sum = 0;
for (i = 0; i < process_samples; i ++)
{
sum += tmp[i] * pBuffer[i + offs]; // scaling the sub-result shouldn't be necessary
}
xcorr[offs] *= xcorr_decay; // decay 'xcorr' here with suitable time constant.
xcorr[offs] += (float)fabs(sum);
}
}
// Detect individual beat positions
void BPMDetect::updateBeatPos(int process_samples)
{
SAMPLETYPE *pBuffer;
assert(buffer->numSamples() >= (uint)(process_samples + windowLen));
pBuffer = buffer->ptrBegin();
assert(process_samples == XCORR_UPDATE_SEQUENCE / 2);
// static double thr = 0.0003;
double posScale = (double)this->decimateBy / (double)this->sampleRate;
int resetDur = (int)(0.12 / posScale + 0.5);
// prescale pbuffer
float tmp[XCORR_UPDATE_SEQUENCE / 2];
for (int i = 0; i < process_samples; i++)
{
tmp[i] = hamw2[i] * hamw2[i] * pBuffer[i];
}
for (int offs = windowStart; offs < windowLen; offs++)
{
float sum = 0;
for (int i = 0; i < process_samples; i++)
{
sum += tmp[i] * pBuffer[offs + i];
}
beatcorr_ringbuff[(beatcorr_ringbuffpos + offs) % windowLen] += (float)((sum > 0) ? sum : 0); // accumulate only positive correlations
}
int skipstep = XCORR_UPDATE_SEQUENCE / OVERLAP_FACTOR;
// compensate empty buffer at beginning by scaling coefficient
float scale = (float)windowLen / (float)(skipstep * init_scaler);
if (scale > 1.0f)
{
init_scaler++;
}
else
{
scale = 1.0f;
}
// detect beats
for (int i = 0; i < skipstep; i++)
{
float sum = beatcorr_ringbuff[beatcorr_ringbuffpos];
sum -= beat_lpf.update(sum);
if (sum > peakVal)
{
// found new local largest value
peakVal = sum;
peakPos = pos;
}
if (pos > peakPos + resetDur)
{
// largest value not updated for 200msec => accept as beat
peakPos += skipstep;
if (peakVal > 0)
{
// add detected beat to end of "beats" vector
BEAT temp = { (float)(peakPos * posScale), (float)(peakVal * scale) };
beats.push_back(temp);
}
peakVal = 0;
peakPos = pos;
}
beatcorr_ringbuff[beatcorr_ringbuffpos] = 0;
pos++;
beatcorr_ringbuffpos = (beatcorr_ringbuffpos + 1) % windowLen;
}
}
#define max(x,y) ((x) > (y) ? (x) : (y))
void BPMDetect::inputSamples(const SAMPLETYPE *samples, int numSamples)
{
SAMPLETYPE decimated[DECIMATED_BLOCK_SIZE];
// iterate so that max INPUT_BLOCK_SAMPLES processed per iteration
while (numSamples > 0)
{
int block;
int decSamples;
block = (numSamples > INPUT_BLOCK_SIZE) ? INPUT_BLOCK_SIZE : numSamples;
// decimate. note that converts to mono at the same time
decSamples = decimate(decimated, samples, block);
samples += block * channels;
numSamples -= block;
buffer->putSamples(decimated, decSamples);
}
// when the buffer has enough samples for processing...
int req = max(windowLen + XCORR_UPDATE_SEQUENCE, 2 * XCORR_UPDATE_SEQUENCE);
while ((int)buffer->numSamples() >= req)
{
// ... update autocorrelations...
updateXCorr(XCORR_UPDATE_SEQUENCE);
// ...update beat position calculation...
updateBeatPos(XCORR_UPDATE_SEQUENCE / 2);
// ... and remove proceessed samples from the buffer
int n = XCORR_UPDATE_SEQUENCE / OVERLAP_FACTOR;
buffer->receiveSamples(n);
}
}
void BPMDetect::removeBias()
{
int i;
// Remove linear bias: calculate linear regression coefficient
// 1. calc mean of 'xcorr' and 'i'
double mean_i = 0;
double mean_x = 0;
for (i = windowStart; i < windowLen; i++)
{
mean_x += xcorr[i];
}
mean_x /= (windowLen - windowStart);
mean_i = 0.5 * (windowLen - 1 + windowStart);
// 2. calculate linear regression coefficient
double b = 0;
double div = 0;
for (i = windowStart; i < windowLen; i++)
{
double xt = xcorr[i] - mean_x;
double xi = i - mean_i;
b += xt * xi;
div += xi * xi;
}
b /= div;
// subtract linear regression and resolve min. value bias
float minval = FLT_MAX; // arbitrary large number
for (i = windowStart; i < windowLen; i ++)
{
xcorr[i] -= (float)(b * i);
if (xcorr[i] < minval)
{
minval = xcorr[i];
}
}
// subtract min.value
for (i = windowStart; i < windowLen; i ++)
{
xcorr[i] -= minval;
}
}
// Calculate N-point moving average for "source" values
void MAFilter(float *dest, const float *source, int start, int end, int N)
{
for (int i = start; i < end; i++)
{
int i1 = i - N / 2;
int i2 = i + N / 2 + 1;
if (i1 < start) i1 = start;
if (i2 > end) i2 = end;
double sum = 0;
for (int j = i1; j < i2; j ++)
{
sum += source[j];
}
dest[i] = (float)(sum / (i2 - i1));
}
}
float BPMDetect::getBpm()
{
double peakPos;
double coeff;
PeakFinder peakFinder;
// remove bias from xcorr data
removeBias();
coeff = 60.0 * ((double)sampleRate / (double)decimateBy);
// save bpm debug data if debug data writing enabled
_SaveDebugData("soundtouch-bpm-xcorr.txt", xcorr, windowStart, windowLen, coeff);
// Smoothen by N-point moving-average
float *data = new float[windowLen];
memset(data, 0, sizeof(float) * windowLen);
MAFilter(data, xcorr, windowStart, windowLen, MOVING_AVERAGE_N);
// find peak position
peakPos = peakFinder.detectPeak(data, windowStart, windowLen);
// save bpm debug data if debug data writing enabled
_SaveDebugData("soundtouch-bpm-smoothed.txt", data, windowStart, windowLen, coeff);
delete[] data;
assert(decimateBy != 0);
if (peakPos < 1e-9) return 0.0; // detection failed.
_SaveDebugBeatPos("soundtouch-detected-beats.txt", beats);
// calculate BPM
float bpm = (float)(coeff / peakPos);
return (bpm >= MIN_BPM && bpm <= MAX_BPM_VALID) ? bpm : 0;
}
/// Get beat position arrays. Note: The array includes also really low beat detection values
/// in absence of clear strong beats. Consumer may wish to filter low values away.
/// - "pos" receive array of beat positions
/// - "values" receive array of beat detection strengths
/// - max_num indicates max.size of "pos" and "values" array.
///
/// You can query a suitable array sized by calling this with nullptr in "pos" & "values".
///
/// \return number of beats in the arrays.
int BPMDetect::getBeats(float *pos, float *values, int max_num)
{
int num = (int)beats.size();
if ((!pos) || (!values)) return num; // pos or values nullptr, return just size
for (int i = 0; (i < num) && (i < max_num); i++)
{
pos[i] = beats[i].pos;
values[i] = beats[i].strength;
}
return num;
}

275
lib/soundtouch-master/source/SoundTouch/FIFOSampleBuffer.cpp

@ -0,0 +1,275 @@
////////////////////////////////////////////////////////////////////////////////
///
/// A buffer class for temporarily storaging sound samples, operates as a
/// first-in-first-out pipe.
///
/// Samples are added to the end of the sample buffer with the 'putSamples'
/// function, and are received from the beginning of the buffer by calling
/// the 'receiveSamples' function. The class automatically removes the
/// outputted samples from the buffer, as well as grows the buffer size
/// whenever necessary.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <stdlib.h>
#include <memory.h>
#include <string.h>
#include <assert.h>
#include "FIFOSampleBuffer.h"
using namespace soundtouch;
// Constructor
FIFOSampleBuffer::FIFOSampleBuffer(int numChannels)
{
assert(numChannels > 0);
sizeInBytes = 0; // reasonable initial value
buffer = nullptr;
bufferUnaligned = nullptr;
samplesInBuffer = 0;
bufferPos = 0;
channels = (uint)numChannels;
ensureCapacity(32); // allocate initial capacity
}
// destructor
FIFOSampleBuffer::~FIFOSampleBuffer()
{
delete[] bufferUnaligned;
bufferUnaligned = nullptr;
buffer = nullptr;
}
// Sets number of channels, 1 = mono, 2 = stereo
void FIFOSampleBuffer::setChannels(int numChannels)
{
uint usedBytes;
if (!verifyNumberOfChannels(numChannels)) return;
usedBytes = channels * samplesInBuffer;
channels = (uint)numChannels;
samplesInBuffer = usedBytes / channels;
}
// if output location pointer 'bufferPos' isn't zero, 'rewinds' the buffer and
// zeroes this pointer by copying samples from the 'bufferPos' pointer
// location on to the beginning of the buffer.
void FIFOSampleBuffer::rewind()
{
if (buffer && bufferPos)
{
memmove(buffer, ptrBegin(), sizeof(SAMPLETYPE) * channels * samplesInBuffer);
bufferPos = 0;
}
}
// Adds 'numSamples' pcs of samples from the 'samples' memory position to
// the sample buffer.
void FIFOSampleBuffer::putSamples(const SAMPLETYPE *samples, uint nSamples)
{
memcpy(ptrEnd(nSamples), samples, sizeof(SAMPLETYPE) * nSamples * channels);
samplesInBuffer += nSamples;
}
// Increases the number of samples in the buffer without copying any actual
// samples.
//
// This function is used to update the number of samples in the sample buffer
// when accessing the buffer directly with 'ptrEnd' function. Please be
// careful though!
void FIFOSampleBuffer::putSamples(uint nSamples)
{
uint req;
req = samplesInBuffer + nSamples;
ensureCapacity(req);
samplesInBuffer += nSamples;
}
// Returns a pointer to the end of the used part of the sample buffer (i.e.
// where the new samples are to be inserted). This function may be used for
// inserting new samples into the sample buffer directly. Please be careful!
//
// Parameter 'slackCapacity' tells the function how much free capacity (in
// terms of samples) there _at least_ should be, in order to the caller to
// successfully insert all the required samples to the buffer. When necessary,
// the function grows the buffer size to comply with this requirement.
//
// When using this function as means for inserting new samples, also remember
// to increase the sample count afterwards, by calling the
// 'putSamples(numSamples)' function.
SAMPLETYPE *FIFOSampleBuffer::ptrEnd(uint slackCapacity)
{
ensureCapacity(samplesInBuffer + slackCapacity);
return buffer + samplesInBuffer * channels;
}
// Returns a pointer to the beginning of the currently non-outputted samples.
// This function is provided for accessing the output samples directly.
// Please be careful!
//
// When using this function to output samples, also remember to 'remove' the
// outputted samples from the buffer by calling the
// 'receiveSamples(numSamples)' function
SAMPLETYPE *FIFOSampleBuffer::ptrBegin()
{
assert(buffer);
return buffer + bufferPos * channels;
}
// Ensures that the buffer has enough capacity, i.e. space for _at least_
// 'capacityRequirement' number of samples. The buffer is grown in steps of
// 4 kilobytes to eliminate the need for frequently growing up the buffer,
// as well as to round the buffer size up to the virtual memory page size.
void FIFOSampleBuffer::ensureCapacity(uint capacityRequirement)
{
SAMPLETYPE *tempUnaligned, *temp;
if (capacityRequirement > getCapacity())
{
// enlarge the buffer in 4kbyte steps (round up to next 4k boundary)
sizeInBytes = (capacityRequirement * channels * sizeof(SAMPLETYPE) + 4095) & (uint)-4096;
assert(sizeInBytes % 2 == 0);
tempUnaligned = new SAMPLETYPE[sizeInBytes / sizeof(SAMPLETYPE) + 16 / sizeof(SAMPLETYPE)];
if (tempUnaligned == nullptr)
{
ST_THROW_RT_ERROR("Couldn't allocate memory!\n");
}
// Align the buffer to begin at 16byte cache line boundary for optimal performance
temp = (SAMPLETYPE *)SOUNDTOUCH_ALIGN_POINTER_16(tempUnaligned);
if (samplesInBuffer)
{
memcpy(temp, ptrBegin(), samplesInBuffer * channels * sizeof(SAMPLETYPE));
}
delete[] bufferUnaligned;
buffer = temp;
bufferUnaligned = tempUnaligned;
bufferPos = 0;
}
else
{
// simply rewind the buffer (if necessary)
rewind();
}
}
// Returns the current buffer capacity in terms of samples
uint FIFOSampleBuffer::getCapacity() const
{
return sizeInBytes / (channels * sizeof(SAMPLETYPE));
}
// Returns the number of samples currently in the buffer
uint FIFOSampleBuffer::numSamples() const
{
return samplesInBuffer;
}
// Output samples from beginning of the sample buffer. Copies demanded number
// of samples to output and removes them from the sample buffer. If there
// are less than 'numsample' samples in the buffer, returns all available.
//
// Returns number of samples copied.
uint FIFOSampleBuffer::receiveSamples(SAMPLETYPE *output, uint maxSamples)
{
uint num;
num = (maxSamples > samplesInBuffer) ? samplesInBuffer : maxSamples;
memcpy(output, ptrBegin(), channels * sizeof(SAMPLETYPE) * num);
return receiveSamples(num);
}
// Removes samples from the beginning of the sample buffer without copying them
// anywhere. Used to reduce the number of samples in the buffer, when accessing
// the sample buffer with the 'ptrBegin' function.
uint FIFOSampleBuffer::receiveSamples(uint maxSamples)
{
if (maxSamples >= samplesInBuffer)
{
uint temp;
temp = samplesInBuffer;
samplesInBuffer = 0;
return temp;
}
samplesInBuffer -= maxSamples;
bufferPos += maxSamples;
return maxSamples;
}
// Returns nonzero if the sample buffer is empty
int FIFOSampleBuffer::isEmpty() const
{
return (samplesInBuffer == 0) ? 1 : 0;
}
// Clears the sample buffer
void FIFOSampleBuffer::clear()
{
samplesInBuffer = 0;
bufferPos = 0;
}
/// allow trimming (downwards) amount of samples in pipeline.
/// Returns adjusted amount of samples
uint FIFOSampleBuffer::adjustAmountOfSamples(uint numSamples)
{
if (numSamples < samplesInBuffer)
{
samplesInBuffer = numSamples;
}
return samplesInBuffer;
}
/// Add silence to end of buffer
void FIFOSampleBuffer::addSilent(uint nSamples)
{
memset(ptrEnd(nSamples), 0, sizeof(SAMPLETYPE) * nSamples * channels);
samplesInBuffer += nSamples;
}

312
lib/soundtouch-master/source/SoundTouch/FIRFilter.cpp

@ -0,0 +1,312 @@
////////////////////////////////////////////////////////////////////////////////
///
/// General FIR digital filter routines with MMX optimization.
///
/// Notes : MMX optimized functions reside in a separate, platform-specific file,
/// e.g. 'mmx_win.cpp' or 'mmx_gcc.cpp'
///
/// This source file contains OpenMP optimizations that allow speeding up the
/// corss-correlation algorithm by executing it in several threads / CPU cores
/// in parallel. See the following article link for more detailed discussion
/// about SoundTouch OpenMP optimizations:
/// http://www.softwarecoven.com/parallel-computing-in-embedded-mobile-devices
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <memory.h>
#include <assert.h>
#include <math.h>
#include <stdlib.h>
#include "FIRFilter.h"
#include "cpu_detect.h"
using namespace soundtouch;
/*****************************************************************************
*
* Implementation of the class 'FIRFilter'
*
*****************************************************************************/
FIRFilter::FIRFilter()
{
resultDivFactor = 0;
resultDivider = 0;
length = 0;
lengthDiv8 = 0;
filterCoeffs = nullptr;
filterCoeffsStereo = nullptr;
}
FIRFilter::~FIRFilter()
{
delete[] filterCoeffs;
delete[] filterCoeffsStereo;
}
// Usual C-version of the filter routine for stereo sound
uint FIRFilter::evaluateFilterStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples) const
{
int j, end;
// hint compiler autovectorization that loop length is divisible by 8
uint ilength = length & -8;
assert((length != 0) && (length == ilength) && (src != nullptr) && (dest != nullptr) && (filterCoeffs != nullptr));
assert(numSamples > ilength);
end = 2 * (numSamples - ilength);
for (j = 0; j < end; j += 2)
{
const SAMPLETYPE *ptr;
LONG_SAMPLETYPE suml, sumr;
suml = sumr = 0;
ptr = src + j;
for (uint i = 0; i < ilength; i ++)
{
suml += ptr[2 * i] * filterCoeffsStereo[2 * i];
sumr += ptr[2 * i + 1] * filterCoeffsStereo[2 * i + 1];
}
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
suml >>= resultDivFactor;
sumr >>= resultDivFactor;
// saturate to 16 bit integer limits
suml = (suml < -32768) ? -32768 : (suml > 32767) ? 32767 : suml;
// saturate to 16 bit integer limits
sumr = (sumr < -32768) ? -32768 : (sumr > 32767) ? 32767 : sumr;
#endif // SOUNDTOUCH_INTEGER_SAMPLES
dest[j] = (SAMPLETYPE)suml;
dest[j + 1] = (SAMPLETYPE)sumr;
}
return numSamples - ilength;
}
// Usual C-version of the filter routine for mono sound
uint FIRFilter::evaluateFilterMono(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples) const
{
int j, end;
// hint compiler autovectorization that loop length is divisible by 8
int ilength = length & -8;
assert(ilength != 0);
end = numSamples - ilength;
for (j = 0; j < end; j ++)
{
const SAMPLETYPE *pSrc = src + j;
LONG_SAMPLETYPE sum;
int i;
sum = 0;
for (i = 0; i < ilength; i ++)
{
sum += pSrc[i] * filterCoeffs[i];
}
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
sum >>= resultDivFactor;
// saturate to 16 bit integer limits
sum = (sum < -32768) ? -32768 : (sum > 32767) ? 32767 : sum;
#endif // SOUNDTOUCH_INTEGER_SAMPLES
dest[j] = (SAMPLETYPE)sum;
}
return end;
}
uint FIRFilter::evaluateFilterMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels)
{
int j, end;
assert(length != 0);
assert(src != nullptr);
assert(dest != nullptr);
assert(filterCoeffs != nullptr);
assert(numChannels < 16);
// hint compiler autovectorization that loop length is divisible by 8
int ilength = length & -8;
end = numChannels * (numSamples - ilength);
for (j = 0; j < end; j += numChannels)
{
const SAMPLETYPE *ptr;
LONG_SAMPLETYPE sums[16];
uint c;
int i;
for (c = 0; c < numChannels; c ++)
{
sums[c] = 0;
}
ptr = src + j;
for (i = 0; i < ilength; i ++)
{
SAMPLETYPE coef=filterCoeffs[i];
for (c = 0; c < numChannels; c ++)
{
sums[c] += ptr[0] * coef;
ptr ++;
}
}
for (c = 0; c < numChannels; c ++)
{
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
sums[c] >>= resultDivFactor;
#endif // SOUNDTOUCH_INTEGER_SAMPLES
dest[j+c] = (SAMPLETYPE)sums[c];
}
}
return numSamples - ilength;
}
// Set filter coeffiecients and length.
//
// Throws an exception if filter length isn't divisible by 8
void FIRFilter::setCoefficients(const SAMPLETYPE *coeffs, uint newLength, uint uResultDivFactor)
{
assert(newLength > 0);
if (newLength % 8) ST_THROW_RT_ERROR("FIR filter length not divisible by 8");
#ifdef SOUNDTOUCH_FLOAT_SAMPLES
// scale coefficients already here if using floating samples
double scale = 1.0 / resultDivider;
#else
short scale = 1;
#endif
lengthDiv8 = newLength / 8;
length = lengthDiv8 * 8;
assert(length == newLength);
resultDivFactor = uResultDivFactor;
resultDivider = (SAMPLETYPE)::pow(2.0, (int)resultDivFactor);
delete[] filterCoeffs;
filterCoeffs = new SAMPLETYPE[length];
delete[] filterCoeffsStereo;
filterCoeffsStereo = new SAMPLETYPE[length*2];
for (uint i = 0; i < length; i ++)
{
filterCoeffs[i] = (SAMPLETYPE)(coeffs[i] * scale);
// create also stereo set of filter coefficients: this allows compiler
// to autovectorize filter evaluation much more efficiently
filterCoeffsStereo[2 * i] = (SAMPLETYPE)(coeffs[i] * scale);
filterCoeffsStereo[2 * i + 1] = (SAMPLETYPE)(coeffs[i] * scale);
}
}
uint FIRFilter::getLength() const
{
return length;
}
// Applies the filter to the given sequence of samples.
//
// Note : The amount of outputted samples is by value of 'filter_length'
// smaller than the amount of input samples.
uint FIRFilter::evaluate(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels)
{
assert(length > 0);
assert(lengthDiv8 * 8 == length);
if (numSamples < length) return 0;
#ifndef USE_MULTICH_ALWAYS
if (numChannels == 1)
{
return evaluateFilterMono(dest, src, numSamples);
}
else if (numChannels == 2)
{
return evaluateFilterStereo(dest, src, numSamples);
}
else
#endif // USE_MULTICH_ALWAYS
{
assert(numChannels > 0);
return evaluateFilterMulti(dest, src, numSamples, numChannels);
}
}
// Operator 'new' is overloaded so that it automatically creates a suitable instance
// depending on if we've a MMX-capable CPU available or not.
void * FIRFilter::operator new(size_t)
{
// Notice! don't use "new FIRFilter" directly, use "newInstance" to create a new instance instead!
ST_THROW_RT_ERROR("Error in FIRFilter::new: Don't use 'new FIRFilter', use 'newInstance' member instead!");
return newInstance();
}
FIRFilter * FIRFilter::newInstance()
{
uint uExtensions;
uExtensions = detectCPUextensions();
(void)uExtensions;
// Check if MMX/SSE instruction set extensions supported by CPU
#ifdef SOUNDTOUCH_ALLOW_MMX
// MMX routines available only with integer sample types
if (uExtensions & SUPPORT_MMX)
{
return ::new FIRFilterMMX;
}
else
#endif // SOUNDTOUCH_ALLOW_MMX
#ifdef SOUNDTOUCH_ALLOW_SSE
if (uExtensions & SUPPORT_SSE)
{
// SSE support
return ::new FIRFilterSSE;
}
else
#endif // SOUNDTOUCH_ALLOW_SSE
{
// ISA optimizations not supported, use plain C version
return ::new FIRFilter;
}
}

140
lib/soundtouch-master/source/SoundTouch/FIRFilter.h

@ -0,0 +1,140 @@
////////////////////////////////////////////////////////////////////////////////
///
/// General FIR digital filter routines with MMX optimization.
///
/// Note : MMX optimized functions reside in a separate, platform-specific file,
/// e.g. 'mmx_win.cpp' or 'mmx_gcc.cpp'
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef FIRFilter_H
#define FIRFilter_H
#include <stddef.h>
#include "STTypes.h"
namespace soundtouch
{
class FIRFilter
{
protected:
// Number of FIR filter taps
uint length;
// Number of FIR filter taps divided by 8
uint lengthDiv8;
// Result divider factor in 2^k format
uint resultDivFactor;
// Result divider value.
SAMPLETYPE resultDivider;
// Memory for filter coefficients
SAMPLETYPE *filterCoeffs;
SAMPLETYPE *filterCoeffsStereo;
virtual uint evaluateFilterStereo(SAMPLETYPE *dest,
const SAMPLETYPE *src,
uint numSamples) const;
virtual uint evaluateFilterMono(SAMPLETYPE *dest,
const SAMPLETYPE *src,
uint numSamples) const;
virtual uint evaluateFilterMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels);
public:
FIRFilter();
virtual ~FIRFilter();
/// Operator 'new' is overloaded so that it automatically creates a suitable instance
/// depending on if we've a MMX-capable CPU available or not.
static void * operator new(size_t s);
static FIRFilter *newInstance();
/// Applies the filter to the given sequence of samples.
/// Note : The amount of outputted samples is by value of 'filter_length'
/// smaller than the amount of input samples.
///
/// \return Number of samples copied to 'dest'.
uint evaluate(SAMPLETYPE *dest,
const SAMPLETYPE *src,
uint numSamples,
uint numChannels);
uint getLength() const;
virtual void setCoefficients(const SAMPLETYPE *coeffs,
uint newLength,
uint uResultDivFactor);
};
// Optional subclasses that implement CPU-specific optimizations:
#ifdef SOUNDTOUCH_ALLOW_MMX
/// Class that implements MMX optimized functions exclusive for 16bit integer samples type.
class FIRFilterMMX : public FIRFilter
{
protected:
short *filterCoeffsUnalign;
short *filterCoeffsAlign;
virtual uint evaluateFilterStereo(short *dest, const short *src, uint numSamples) const override;
public:
FIRFilterMMX();
~FIRFilterMMX();
virtual void setCoefficients(const short *coeffs, uint newLength, uint uResultDivFactor) override;
};
#endif // SOUNDTOUCH_ALLOW_MMX
#ifdef SOUNDTOUCH_ALLOW_SSE
/// Class that implements SSE optimized functions exclusive for floating point samples type.
class FIRFilterSSE : public FIRFilter
{
protected:
float *filterCoeffsUnalign;
float *filterCoeffsAlign;
virtual uint evaluateFilterStereo(float *dest, const float *src, uint numSamples) const override;
public:
FIRFilterSSE();
~FIRFilterSSE();
virtual void setCoefficients(const float *coeffs, uint newLength, uint uResultDivFactor) override;
};
#endif // SOUNDTOUCH_ALLOW_SSE
}
#endif // FIRFilter_H

196
lib/soundtouch-master/source/SoundTouch/InterpolateCubic.cpp

@ -0,0 +1,196 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Cubic interpolation routine.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <stddef.h>
#include <math.h>
#include "InterpolateCubic.h"
#include "STTypes.h"
using namespace soundtouch;
// cubic interpolation coefficients
static const float _coeffs[]=
{ -0.5f, 1.0f, -0.5f, 0.0f,
1.5f, -2.5f, 0.0f, 1.0f,
-1.5f, 2.0f, 0.5f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f};
InterpolateCubic::InterpolateCubic()
{
fract = 0;
}
void InterpolateCubic::resetRegisters()
{
fract = 0;
}
/// Transpose mono audio. Returns number of produced output samples, and
/// updates "srcSamples" to amount of consumed source samples
int InterpolateCubic::transposeMono(SAMPLETYPE *pdest,
const SAMPLETYPE *psrc,
int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 4;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
float out;
const float x3 = 1.0f;
const float x2 = (float)fract; // x
const float x1 = x2*x2; // x^2
const float x0 = x1*x2; // x^3
float y0, y1, y2, y3;
assert(fract < 1.0);
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3;
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3;
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3;
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3;
out = y0 * psrc[0] + y1 * psrc[1] + y2 * psrc[2] + y3 * psrc[3];
pdest[i] = (SAMPLETYPE)out;
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
psrc += whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}
/// Transpose stereo audio. Returns number of produced output samples, and
/// updates "srcSamples" to amount of consumed source samples
int InterpolateCubic::transposeStereo(SAMPLETYPE *pdest,
const SAMPLETYPE *psrc,
int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 4;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
const float x3 = 1.0f;
const float x2 = (float)fract; // x
const float x1 = x2*x2; // x^2
const float x0 = x1*x2; // x^3
float y0, y1, y2, y3;
float out0, out1;
assert(fract < 1.0);
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3;
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3;
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3;
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3;
out0 = y0 * psrc[0] + y1 * psrc[2] + y2 * psrc[4] + y3 * psrc[6];
out1 = y0 * psrc[1] + y1 * psrc[3] + y2 * psrc[5] + y3 * psrc[7];
pdest[2*i] = (SAMPLETYPE)out0;
pdest[2*i+1] = (SAMPLETYPE)out1;
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
psrc += 2*whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}
/// Transpose multi-channel audio. Returns number of produced output samples, and
/// updates "srcSamples" to amount of consumed source samples
int InterpolateCubic::transposeMulti(SAMPLETYPE *pdest,
const SAMPLETYPE *psrc,
int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 4;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
const float x3 = 1.0f;
const float x2 = (float)fract; // x
const float x1 = x2*x2; // x^2
const float x0 = x1*x2; // x^3
float y0, y1, y2, y3;
assert(fract < 1.0);
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3;
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3;
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3;
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3;
for (int c = 0; c < numChannels; c ++)
{
float out;
out = y0 * psrc[c] + y1 * psrc[c + numChannels] + y2 * psrc[c + 2 * numChannels] + y3 * psrc[c + 3 * numChannels];
pdest[0] = (SAMPLETYPE)out;
pdest ++;
}
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
psrc += numChannels*whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}

69
lib/soundtouch-master/source/SoundTouch/InterpolateCubic.h

@ -0,0 +1,69 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Cubic interpolation routine.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef _InterpolateCubic_H_
#define _InterpolateCubic_H_
#include "RateTransposer.h"
#include "STTypes.h"
namespace soundtouch
{
class InterpolateCubic : public TransposerBase
{
protected:
virtual int transposeMono(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) override;
virtual int transposeStereo(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) override;
virtual int transposeMulti(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) override;
double fract;
public:
InterpolateCubic();
virtual void resetRegisters() override;
virtual int getLatency() const override
{
return 1;
}
};
}
#endif

296
lib/soundtouch-master/source/SoundTouch/InterpolateLinear.cpp

@ -0,0 +1,296 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Linear interpolation algorithm.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <assert.h>
#include <stdlib.h>
#include "InterpolateLinear.h"
using namespace soundtouch;
//////////////////////////////////////////////////////////////////////////////
//
// InterpolateLinearInteger - integer arithmetic implementation
//
/// fixed-point interpolation routine precision
#define SCALE 65536
// Constructor
InterpolateLinearInteger::InterpolateLinearInteger() : TransposerBase()
{
// Notice: use local function calling syntax for sake of clarity,
// to indicate the fact that C++ constructor can't call virtual functions.
resetRegisters();
setRate(1.0f);
}
void InterpolateLinearInteger::resetRegisters()
{
iFract = 0;
}
// Transposes the sample rate of the given samples using linear interpolation.
// 'Mono' version of the routine. Returns the number of samples returned in
// the "dest" buffer
int InterpolateLinearInteger::transposeMono(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
LONG_SAMPLETYPE temp;
assert(iFract < SCALE);
temp = (SCALE - iFract) * src[0] + iFract * src[1];
dest[i] = (SAMPLETYPE)(temp / SCALE);
i++;
iFract += iRate;
int iWhole = iFract / SCALE;
iFract -= iWhole * SCALE;
srcCount += iWhole;
src += iWhole;
}
srcSamples = srcCount;
return i;
}
// Transposes the sample rate of the given samples using linear interpolation.
// 'Stereo' version of the routine. Returns the number of samples returned in
// the "dest" buffer
int InterpolateLinearInteger::transposeStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
LONG_SAMPLETYPE temp0;
LONG_SAMPLETYPE temp1;
assert(iFract < SCALE);
temp0 = (SCALE - iFract) * src[0] + iFract * src[2];
temp1 = (SCALE - iFract) * src[1] + iFract * src[3];
dest[0] = (SAMPLETYPE)(temp0 / SCALE);
dest[1] = (SAMPLETYPE)(temp1 / SCALE);
dest += 2;
i++;
iFract += iRate;
int iWhole = iFract / SCALE;
iFract -= iWhole * SCALE;
srcCount += iWhole;
src += 2*iWhole;
}
srcSamples = srcCount;
return i;
}
int InterpolateLinearInteger::transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
LONG_SAMPLETYPE temp, vol1;
assert(iFract < SCALE);
vol1 = (LONG_SAMPLETYPE)(SCALE - iFract);
for (int c = 0; c < numChannels; c ++)
{
temp = vol1 * src[c] + iFract * src[c + numChannels];
dest[0] = (SAMPLETYPE)(temp / SCALE);
dest ++;
}
i++;
iFract += iRate;
int iWhole = iFract / SCALE;
iFract -= iWhole * SCALE;
srcCount += iWhole;
src += iWhole * numChannels;
}
srcSamples = srcCount;
return i;
}
// Sets new target iRate. Normal iRate = 1.0, smaller values represent slower
// iRate, larger faster iRates.
void InterpolateLinearInteger::setRate(double newRate)
{
iRate = (int)(newRate * SCALE + 0.5);
TransposerBase::setRate(newRate);
}
//////////////////////////////////////////////////////////////////////////////
//
// InterpolateLinearFloat - floating point arithmetic implementation
//
//////////////////////////////////////////////////////////////////////////////
// Constructor
InterpolateLinearFloat::InterpolateLinearFloat() : TransposerBase()
{
// Notice: use local function calling syntax for sake of clarity,
// to indicate the fact that C++ constructor can't call virtual functions.
resetRegisters();
setRate(1.0);
}
void InterpolateLinearFloat::resetRegisters()
{
fract = 0;
}
// Transposes the sample rate of the given samples using linear interpolation.
// 'Mono' version of the routine. Returns the number of samples returned in
// the "dest" buffer
int InterpolateLinearFloat::transposeMono(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
double out;
assert(fract < 1.0);
out = (1.0 - fract) * src[0] + fract * src[1];
dest[i] = (SAMPLETYPE)out;
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
src += whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}
// Transposes the sample rate of the given samples using linear interpolation.
// 'Mono' version of the routine. Returns the number of samples returned in
// the "dest" buffer
int InterpolateLinearFloat::transposeStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
double out0, out1;
assert(fract < 1.0);
out0 = (1.0 - fract) * src[0] + fract * src[2];
out1 = (1.0 - fract) * src[1] + fract * src[3];
dest[2*i] = (SAMPLETYPE)out0;
dest[2*i+1] = (SAMPLETYPE)out1;
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
src += 2*whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}
int InterpolateLinearFloat::transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 1;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
float temp, vol1, fract_float;
vol1 = (float)(1.0 - fract);
fract_float = (float)fract;
for (int c = 0; c < numChannels; c ++)
{
temp = vol1 * src[c] + fract_float * src[c + numChannels];
*dest = (SAMPLETYPE)temp;
dest ++;
}
i++;
fract += rate;
int iWhole = (int)fract;
fract -= iWhole;
srcCount += iWhole;
src += iWhole * numChannels;
}
srcSamples = srcCount;
return i;
}

98
lib/soundtouch-master/source/SoundTouch/InterpolateLinear.h

@ -0,0 +1,98 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Linear interpolation routine.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef _InterpolateLinear_H_
#define _InterpolateLinear_H_
#include "RateTransposer.h"
#include "STTypes.h"
namespace soundtouch
{
/// Linear transposer class that uses integer arithmetic
class InterpolateLinearInteger final : public TransposerBase
{
protected:
int iFract;
int iRate;
virtual int transposeMono(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) override;
virtual int transposeStereo(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) override;
virtual int transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples) override;
public:
InterpolateLinearInteger();
/// Sets new target rate. Normal rate = 1.0, smaller values represent slower
/// rate, larger faster rates.
virtual void setRate(double newRate) override;
virtual void resetRegisters() override;
virtual int getLatency() const override
{
return 0;
}
};
/// Linear transposer class that uses floating point arithmetic
class InterpolateLinearFloat final : public TransposerBase
{
protected:
double fract;
virtual int transposeMono(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples);
virtual int transposeStereo(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples);
virtual int transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples);
public:
InterpolateLinearFloat();
virtual void resetRegisters();
int getLatency() const
{
return 0;
}
};
}
#endif

181
lib/soundtouch-master/source/SoundTouch/InterpolateShannon.cpp

@ -0,0 +1,181 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Sample interpolation routine using 8-tap band-limited Shannon interpolation
/// with kaiser window.
///
/// Notice. This algorithm is remarkably much heavier than linear or cubic
/// interpolation, and not remarkably better than cubic algorithm. Thus mostly
/// for experimental purposes
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <math.h>
#include "InterpolateShannon.h"
#include "STTypes.h"
using namespace soundtouch;
/// Kaiser window with beta = 2.0
/// Values scaled down by 5% to avoid overflows
static const double _kaiser8[8] =
{
0.41778693317814,
0.64888025049173,
0.83508562409944,
0.93887857733412,
0.93887857733412,
0.83508562409944,
0.64888025049173,
0.41778693317814
};
InterpolateShannon::InterpolateShannon()
{
fract = 0;
}
void InterpolateShannon::resetRegisters()
{
fract = 0;
}
#define PI 3.1415926536
#define sinc(x) (sin(PI * (x)) / (PI * (x)))
/// Transpose mono audio. Returns number of produced output samples, and
/// updates "srcSamples" to amount of consumed source samples
int InterpolateShannon::transposeMono(SAMPLETYPE *pdest,
const SAMPLETYPE *psrc,
int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 8;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
double out;
assert(fract < 1.0);
out = psrc[0] * sinc(-3.0 - fract) * _kaiser8[0];
out += psrc[1] * sinc(-2.0 - fract) * _kaiser8[1];
out += psrc[2] * sinc(-1.0 - fract) * _kaiser8[2];
if (fract < 1e-6)
{
out += psrc[3] * _kaiser8[3]; // sinc(0) = 1
}
else
{
out += psrc[3] * sinc(- fract) * _kaiser8[3];
}
out += psrc[4] * sinc( 1.0 - fract) * _kaiser8[4];
out += psrc[5] * sinc( 2.0 - fract) * _kaiser8[5];
out += psrc[6] * sinc( 3.0 - fract) * _kaiser8[6];
out += psrc[7] * sinc( 4.0 - fract) * _kaiser8[7];
pdest[i] = (SAMPLETYPE)out;
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
psrc += whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}
/// Transpose stereo audio. Returns number of produced output samples, and
/// updates "srcSamples" to amount of consumed source samples
int InterpolateShannon::transposeStereo(SAMPLETYPE *pdest,
const SAMPLETYPE *psrc,
int &srcSamples)
{
int i;
int srcSampleEnd = srcSamples - 8;
int srcCount = 0;
i = 0;
while (srcCount < srcSampleEnd)
{
double out0, out1, w;
assert(fract < 1.0);
w = sinc(-3.0 - fract) * _kaiser8[0];
out0 = psrc[0] * w; out1 = psrc[1] * w;
w = sinc(-2.0 - fract) * _kaiser8[1];
out0 += psrc[2] * w; out1 += psrc[3] * w;
w = sinc(-1.0 - fract) * _kaiser8[2];
out0 += psrc[4] * w; out1 += psrc[5] * w;
w = _kaiser8[3] * ((fract < 1e-5) ? 1.0 : sinc(- fract)); // sinc(0) = 1
out0 += psrc[6] * w; out1 += psrc[7] * w;
w = sinc( 1.0 - fract) * _kaiser8[4];
out0 += psrc[8] * w; out1 += psrc[9] * w;
w = sinc( 2.0 - fract) * _kaiser8[5];
out0 += psrc[10] * w; out1 += psrc[11] * w;
w = sinc( 3.0 - fract) * _kaiser8[6];
out0 += psrc[12] * w; out1 += psrc[13] * w;
w = sinc( 4.0 - fract) * _kaiser8[7];
out0 += psrc[14] * w; out1 += psrc[15] * w;
pdest[2*i] = (SAMPLETYPE)out0;
pdest[2*i+1] = (SAMPLETYPE)out1;
i ++;
// update position fraction
fract += rate;
// update whole positions
int whole = (int)fract;
fract -= whole;
psrc += 2*whole;
srcCount += whole;
}
srcSamples = srcCount;
return i;
}
/// Transpose stereo audio. Returns number of produced output samples, and
/// updates "srcSamples" to amount of consumed source samples
int InterpolateShannon::transposeMulti(SAMPLETYPE *,
const SAMPLETYPE *,
int &)
{
// not implemented
assert(false);
return 0;
}

74
lib/soundtouch-master/source/SoundTouch/InterpolateShannon.h

@ -0,0 +1,74 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Sample interpolation routine using 8-tap band-limited Shannon interpolation
/// with kaiser window.
///
/// Notice. This algorithm is remarkably much heavier than linear or cubic
/// interpolation, and not remarkably better than cubic algorithm. Thus mostly
/// for experimental purposes
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef _InterpolateShannon_H_
#define _InterpolateShannon_H_
#include "RateTransposer.h"
#include "STTypes.h"
namespace soundtouch
{
class InterpolateShannon final : public TransposerBase
{
protected:
int transposeMono(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) override;
int transposeStereo(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) override;
int transposeMulti(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) override;
double fract;
public:
InterpolateShannon();
void resetRegisters() override;
virtual int getLatency() const override
{
return 3;
}
};
}
#endif

277
lib/soundtouch-master/source/SoundTouch/PeakFinder.cpp

@ -0,0 +1,277 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Peak detection routine.
///
/// The routine detects highest value on an array of values and calculates the
/// precise peak location as a mass-center of the 'hump' around the peak value.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <math.h>
#include <assert.h>
#include "PeakFinder.h"
using namespace soundtouch;
#define max(x, y) (((x) > (y)) ? (x) : (y))
PeakFinder::PeakFinder()
{
minPos = maxPos = 0;
}
// Finds real 'top' of a peak hump from neighnourhood of the given 'peakpos'.
int PeakFinder::findTop(const float *data, int peakpos) const
{
int i;
int start, end;
float refvalue;
refvalue = data[peakpos];
// seek within ±10 points
start = peakpos - 10;
if (start < minPos) start = minPos;
end = peakpos + 10;
if (end > maxPos) end = maxPos;
for (i = start; i <= end; i ++)
{
if (data[i] > refvalue)
{
peakpos = i;
refvalue = data[i];
}
}
// failure if max value is at edges of seek range => it's not peak, it's at slope.
if ((peakpos == start) || (peakpos == end)) return 0;
return peakpos;
}
// Finds 'ground level' of a peak hump by starting from 'peakpos' and proceeding
// to direction defined by 'direction' until next 'hump' after minimum value will
// begin
int PeakFinder::findGround(const float *data, int peakpos, int direction) const
{
int lowpos;
int pos;
int climb_count;
float refvalue;
float delta;
climb_count = 0;
refvalue = data[peakpos];
lowpos = peakpos;
pos = peakpos;
while ((pos > minPos+1) && (pos < maxPos-1))
{
int prevpos;
prevpos = pos;
pos += direction;
// calculate derivate
delta = data[pos] - data[prevpos];
if (delta <= 0)
{
// going downhill, ok
if (climb_count)
{
climb_count --; // decrease climb count
}
// check if new minimum found
if (data[pos] < refvalue)
{
// new minimum found
lowpos = pos;
refvalue = data[pos];
}
}
else
{
// going uphill, increase climbing counter
climb_count ++;
if (climb_count > 5) break; // we've been climbing too long => it's next uphill => quit
}
}
return lowpos;
}
// Find offset where the value crosses the given level, when starting from 'peakpos' and
// proceeds to direction defined in 'direction'
int PeakFinder::findCrossingLevel(const float *data, float level, int peakpos, int direction) const
{
float peaklevel;
int pos;
peaklevel = data[peakpos];
assert(peaklevel >= level);
pos = peakpos;
while ((pos >= minPos) && (pos + direction < maxPos))
{
if (data[pos + direction] < level) return pos; // crossing found
pos += direction;
}
return -1; // not found
}
// Calculates the center of mass location of 'data' array items between 'firstPos' and 'lastPos'
double PeakFinder::calcMassCenter(const float *data, int firstPos, int lastPos) const
{
int i;
float sum;
float wsum;
sum = 0;
wsum = 0;
for (i = firstPos; i <= lastPos; i ++)
{
sum += (float)i * data[i];
wsum += data[i];
}
if (wsum < 1e-6) return 0;
return sum / wsum;
}
/// get exact center of peak near given position by calculating local mass of center
double PeakFinder::getPeakCenter(const float *data, int peakpos) const
{
float peakLevel; // peak level
int crosspos1, crosspos2; // position where the peak 'hump' crosses cutting level
float cutLevel; // cutting value
float groundLevel; // ground level of the peak
int gp1, gp2; // bottom positions of the peak 'hump'
// find ground positions.
gp1 = findGround(data, peakpos, -1);
gp2 = findGround(data, peakpos, 1);
peakLevel = data[peakpos];
if (gp1 == gp2)
{
// avoid rounding errors when all are equal
assert(gp1 == peakpos);
cutLevel = groundLevel = peakLevel;
} else {
// get average of the ground levels
groundLevel = 0.5f * (data[gp1] + data[gp2]);
// calculate 70%-level of the peak
cutLevel = 0.70f * peakLevel + 0.30f * groundLevel;
}
// find mid-level crossings
crosspos1 = findCrossingLevel(data, cutLevel, peakpos, -1);
crosspos2 = findCrossingLevel(data, cutLevel, peakpos, 1);
if ((crosspos1 < 0) || (crosspos2 < 0)) return 0; // no crossing, no peak..
// calculate mass center of the peak surroundings
return calcMassCenter(data, crosspos1, crosspos2);
}
double PeakFinder::detectPeak(const float *data, int aminPos, int amaxPos)
{
int i;
int peakpos; // position of peak level
double highPeak, peak;
this->minPos = aminPos;
this->maxPos = amaxPos;
// find absolute peak
peakpos = minPos;
peak = data[minPos];
for (i = minPos + 1; i < maxPos; i ++)
{
if (data[i] > peak)
{
peak = data[i];
peakpos = i;
}
}
// Calculate exact location of the highest peak mass center
highPeak = getPeakCenter(data, peakpos);
peak = highPeak;
// Now check if the highest peak were in fact harmonic of the true base beat peak
// - sometimes the highest peak can be Nth harmonic of the true base peak yet
// just a slightly higher than the true base
for (i = 1; i < 3; i ++)
{
double peaktmp, harmonic;
int i1,i2;
harmonic = (double)pow(2.0, i);
peakpos = (int)(highPeak / harmonic + 0.5f);
if (peakpos < minPos) break;
peakpos = findTop(data, peakpos); // seek true local maximum index
if (peakpos == 0) continue; // no local max here
// calculate mass-center of possible harmonic peak
peaktmp = getPeakCenter(data, peakpos);
// accept harmonic peak if
// (a) it is found
// (b) is within ±4% of the expected harmonic interval
// (c) has at least half x-corr value of the max. peak
double diff = harmonic * peaktmp / highPeak;
if ((diff < 0.96) || (diff > 1.04)) continue; // peak too afar from expected
// now compare to highest detected peak
i1 = (int)(highPeak + 0.5);
i2 = (int)(peaktmp + 0.5);
if (data[i2] >= 0.4*data[i1])
{
// The harmonic is at least half as high primary peak,
// thus use the harmonic peak instead
peak = peaktmp;
}
}
return peak;
}

90
lib/soundtouch-master/source/SoundTouch/PeakFinder.h

@ -0,0 +1,90 @@
////////////////////////////////////////////////////////////////////////////////
///
/// The routine detects highest value on an array of values and calculates the
/// precise peak location as a mass-center of the 'hump' around the peak value.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef _PeakFinder_H_
#define _PeakFinder_H_
namespace soundtouch
{
class PeakFinder
{
protected:
/// Min, max allowed peak positions within the data vector
int minPos, maxPos;
/// Calculates the mass center between given vector items.
double calcMassCenter(const float *data, ///< Data vector.
int firstPos, ///< Index of first vector item belonging to the peak.
int lastPos ///< Index of last vector item belonging to the peak.
) const;
/// Finds the data vector index where the monotoniously decreasing signal crosses the
/// given level.
int findCrossingLevel(const float *data, ///< Data vector.
float level, ///< Goal crossing level.
int peakpos, ///< Peak position index within the data vector.
int direction /// Direction where to proceed from the peak: 1 = right, -1 = left.
) const;
// Finds real 'top' of a peak hump from neighnourhood of the given 'peakpos'.
int findTop(const float *data, int peakpos) const;
/// Finds the 'ground' level, i.e. smallest level between two neighbouring peaks, to right-
/// or left-hand side of the given peak position.
int findGround(const float *data, /// Data vector.
int peakpos, /// Peak position index within the data vector.
int direction /// Direction where to proceed from the peak: 1 = right, -1 = left.
) const;
/// get exact center of peak near given position by calculating local mass of center
double getPeakCenter(const float *data, int peakpos) const;
public:
/// Constructor.
PeakFinder();
/// Detect exact peak position of the data vector by finding the largest peak 'hump'
/// and calculating the mass-center location of the peak hump.
///
/// \return The location of the largest base harmonic peak hump.
double detectPeak(const float *data, /// Data vector to be analyzed. The data vector has
/// to be at least 'maxPos' items long.
int minPos, ///< Min allowed peak location within the vector data.
int maxPos ///< Max allowed peak location within the vector data.
);
};
}
#endif // _PeakFinder_H_

313
lib/soundtouch-master/source/SoundTouch/RateTransposer.cpp

@ -0,0 +1,313 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Sample rate transposer. Changes sample rate by using linear interpolation
/// together with anti-alias filtering (first order interpolation with anti-
/// alias filtering should be quite adequate for this application)
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <memory.h>
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include "RateTransposer.h"
#include "InterpolateLinear.h"
#include "InterpolateCubic.h"
#include "InterpolateShannon.h"
#include "AAFilter.h"
using namespace soundtouch;
// Define default interpolation algorithm here
TransposerBase::ALGORITHM TransposerBase::algorithm = TransposerBase::CUBIC;
// Constructor
RateTransposer::RateTransposer() : FIFOProcessor(&outputBuffer)
{
bUseAAFilter =
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER
true;
#else
// Disable Anti-alias filter if desirable to avoid click at rate change zero value crossover
false;
#endif
// Instantiates the anti-alias filter
pAAFilter = new AAFilter(64);
pTransposer = TransposerBase::newInstance();
clear();
}
RateTransposer::~RateTransposer()
{
delete pAAFilter;
delete pTransposer;
}
/// Enables/disables the anti-alias filter. Zero to disable, nonzero to enable
void RateTransposer::enableAAFilter(bool newMode)
{
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER
// Disable Anti-alias filter if desirable to avoid click at rate change zero value crossover
bUseAAFilter = newMode;
clear();
#endif
}
/// Returns nonzero if anti-alias filter is enabled.
bool RateTransposer::isAAFilterEnabled() const
{
return bUseAAFilter;
}
AAFilter *RateTransposer::getAAFilter()
{
return pAAFilter;
}
// Sets new target iRate. Normal iRate = 1.0, smaller values represent slower
// iRate, larger faster iRates.
void RateTransposer::setRate(double newRate)
{
double fCutoff;
pTransposer->setRate(newRate);
// design a new anti-alias filter
if (newRate > 1.0)
{
fCutoff = 0.5 / newRate;
}
else
{
fCutoff = 0.5 * newRate;
}
pAAFilter->setCutoffFreq(fCutoff);
}
// Adds 'nSamples' pcs of samples from the 'samples' memory position into
// the input of the object.
void RateTransposer::putSamples(const SAMPLETYPE *samples, uint nSamples)
{
processSamples(samples, nSamples);
}
// Transposes sample rate by applying anti-alias filter to prevent folding.
// Returns amount of samples returned in the "dest" buffer.
// The maximum amount of samples that can be returned at a time is set by
// the 'set_returnBuffer_size' function.
void RateTransposer::processSamples(const SAMPLETYPE *src, uint nSamples)
{
if (nSamples == 0) return;
// Store samples to input buffer
inputBuffer.putSamples(src, nSamples);
// If anti-alias filter is turned off, simply transpose without applying
// the filter
if (bUseAAFilter == false)
{
(void)pTransposer->transpose(outputBuffer, inputBuffer);
return;
}
assert(pAAFilter);
// Transpose with anti-alias filter
if (pTransposer->rate < 1.0f)
{
// If the parameter 'Rate' value is smaller than 1, first transpose
// the samples and then apply the anti-alias filter to remove aliasing.
// Transpose the samples, store the result to end of "midBuffer"
pTransposer->transpose(midBuffer, inputBuffer);
// Apply the anti-alias filter for transposed samples in midBuffer
pAAFilter->evaluate(outputBuffer, midBuffer);
}
else
{
// If the parameter 'Rate' value is larger than 1, first apply the
// anti-alias filter to remove high frequencies (prevent them from folding
// over the lover frequencies), then transpose.
// Apply the anti-alias filter for samples in inputBuffer
pAAFilter->evaluate(midBuffer, inputBuffer);
// Transpose the AA-filtered samples in "midBuffer"
pTransposer->transpose(outputBuffer, midBuffer);
}
}
// Sets the number of channels, 1 = mono, 2 = stereo
void RateTransposer::setChannels(int nChannels)
{
if (!verifyNumberOfChannels(nChannels) ||
(pTransposer->numChannels == nChannels)) return;
pTransposer->setChannels(nChannels);
inputBuffer.setChannels(nChannels);
midBuffer.setChannels(nChannels);
outputBuffer.setChannels(nChannels);
}
// Clears all the samples in the object
void RateTransposer::clear()
{
outputBuffer.clear();
midBuffer.clear();
inputBuffer.clear();
pTransposer->resetRegisters();
// prefill buffer to avoid losing first samples at beginning of stream
int prefill = getLatency();
inputBuffer.addSilent(prefill);
}
// Returns nonzero if there aren't any samples available for outputting.
int RateTransposer::isEmpty() const
{
int res;
res = FIFOProcessor::isEmpty();
if (res == 0) return 0;
return inputBuffer.isEmpty();
}
/// Return approximate initial input-output latency
int RateTransposer::getLatency() const
{
return pTransposer->getLatency() +
((bUseAAFilter) ? (pAAFilter->getLength() / 2) : 0);
}
//////////////////////////////////////////////////////////////////////////////
//
// TransposerBase - Base class for interpolation
//
// static function to set interpolation algorithm
void TransposerBase::setAlgorithm(TransposerBase::ALGORITHM a)
{
TransposerBase::algorithm = a;
}
// Transposes the sample rate of the given samples using linear interpolation.
// Returns the number of samples returned in the "dest" buffer
int TransposerBase::transpose(FIFOSampleBuffer &dest, FIFOSampleBuffer &src)
{
int numSrcSamples = src.numSamples();
int sizeDemand = (int)((double)numSrcSamples / rate) + 8;
int numOutput;
SAMPLETYPE *psrc = src.ptrBegin();
SAMPLETYPE *pdest = dest.ptrEnd(sizeDemand);
#ifndef USE_MULTICH_ALWAYS
if (numChannels == 1)
{
numOutput = transposeMono(pdest, psrc, numSrcSamples);
}
else if (numChannels == 2)
{
numOutput = transposeStereo(pdest, psrc, numSrcSamples);
}
else
#endif // USE_MULTICH_ALWAYS
{
assert(numChannels > 0);
numOutput = transposeMulti(pdest, psrc, numSrcSamples);
}
dest.putSamples(numOutput);
src.receiveSamples(numSrcSamples);
return numOutput;
}
TransposerBase::TransposerBase()
{
numChannels = 0;
rate = 1.0f;
}
TransposerBase::~TransposerBase()
{
}
void TransposerBase::setChannels(int channels)
{
numChannels = channels;
resetRegisters();
}
void TransposerBase::setRate(double newRate)
{
rate = newRate;
}
// static factory function
TransposerBase *TransposerBase::newInstance()
{
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
// Notice: For integer arithmetic support only linear algorithm (due to simplest calculus)
return ::new InterpolateLinearInteger;
#else
switch (algorithm)
{
case LINEAR:
return new InterpolateLinearFloat;
case CUBIC:
return new InterpolateCubic;
case SHANNON:
return new InterpolateShannon;
default:
assert(false);
return nullptr;
}
#endif
}

164
lib/soundtouch-master/source/SoundTouch/RateTransposer.h

@ -0,0 +1,164 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Sample rate transposer. Changes sample rate by using linear interpolation
/// together with anti-alias filtering (first order interpolation with anti-
/// alias filtering should be quite adequate for this application).
///
/// Use either of the derived classes of 'RateTransposerInteger' or
/// 'RateTransposerFloat' for corresponding integer/floating point tranposing
/// algorithm implementation.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef RateTransposer_H
#define RateTransposer_H
#include <stddef.h>
#include "AAFilter.h"
#include "FIFOSamplePipe.h"
#include "FIFOSampleBuffer.h"
#include "STTypes.h"
namespace soundtouch
{
/// Abstract base class for transposer implementations (linear, advanced vs integer, float etc)
class TransposerBase
{
public:
enum ALGORITHM {
LINEAR = 0,
CUBIC,
SHANNON
};
protected:
virtual int transposeMono(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) = 0;
virtual int transposeStereo(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) = 0;
virtual int transposeMulti(SAMPLETYPE *dest,
const SAMPLETYPE *src,
int &srcSamples) = 0;
static ALGORITHM algorithm;
public:
double rate;
int numChannels;
TransposerBase();
virtual ~TransposerBase();
virtual int transpose(FIFOSampleBuffer &dest, FIFOSampleBuffer &src);
virtual void setRate(double newRate);
virtual void setChannels(int channels);
virtual int getLatency() const = 0;
virtual void resetRegisters() = 0;
// static factory function
static TransposerBase *newInstance();
// static function to set interpolation algorithm
static void setAlgorithm(ALGORITHM a);
};
/// A common linear samplerate transposer class.
///
class RateTransposer final : public FIFOProcessor
{
protected:
/// Anti-alias filter object
AAFilter *pAAFilter;
TransposerBase *pTransposer;
/// Buffer for collecting samples to feed the anti-alias filter between
/// two batches
FIFOSampleBuffer inputBuffer;
/// Buffer for keeping samples between transposing & anti-alias filter
FIFOSampleBuffer midBuffer;
/// Output sample buffer
FIFOSampleBuffer outputBuffer;
bool bUseAAFilter;
/// Transposes sample rate by applying anti-alias filter to prevent folding.
/// Returns amount of samples returned in the "dest" buffer.
/// The maximum amount of samples that can be returned at a time is set by
/// the 'set_returnBuffer_size' function.
void processSamples(const SAMPLETYPE *src,
uint numSamples);
public:
RateTransposer();
virtual ~RateTransposer() override;
/// Returns the output buffer object
FIFOSamplePipe *getOutput() { return &outputBuffer; };
/// Return anti-alias filter object
AAFilter *getAAFilter();
/// Enables/disables the anti-alias filter. Zero to disable, nonzero to enable
void enableAAFilter(bool newMode);
/// Returns nonzero if anti-alias filter is enabled.
bool isAAFilterEnabled() const;
/// Sets new target rate. Normal rate = 1.0, smaller values represent slower
/// rate, larger faster rates.
virtual void setRate(double newRate);
/// Sets the number of channels, 1 = mono, 2 = stereo
void setChannels(int channels);
/// Adds 'numSamples' pcs of samples from the 'samples' memory position into
/// the input of the object.
void putSamples(const SAMPLETYPE *samples, uint numSamples) override;
/// Clears all the samples in the object
void clear() override;
/// Returns nonzero if there aren't any samples available for outputting.
int isEmpty() const override;
/// Return approximate initial input-output latency
int getLatency() const;
};
}
#endif

538
lib/soundtouch-master/source/SoundTouch/SoundTouch.cpp

@ -0,0 +1,538 @@
//////////////////////////////////////////////////////////////////////////////
///
/// SoundTouch - main class for tempo/pitch/rate adjusting routines.
///
/// Notes:
/// - Initialize the SoundTouch object instance by setting up the sound stream
/// parameters with functions 'setSampleRate' and 'setChannels', then set
/// desired tempo/pitch/rate settings with the corresponding functions.
///
/// - The SoundTouch class behaves like a first-in-first-out pipeline: The
/// samples that are to be processed are fed into one of the pipe by calling
/// function 'putSamples', while the ready processed samples can be read
/// from the other end of the pipeline with function 'receiveSamples'.
///
/// - The SoundTouch processing classes require certain sized 'batches' of
/// samples in order to process the sound. For this reason the classes buffer
/// incoming samples until there are enough of samples available for
/// processing, then they carry out the processing step and consequently
/// make the processed samples available for outputting.
///
/// - For the above reason, the processing routines introduce a certain
/// 'latency' between the input and output, so that the samples input to
/// SoundTouch may not be immediately available in the output, and neither
/// the amount of outputtable samples may not immediately be in direct
/// relationship with the amount of previously input samples.
///
/// - The tempo/pitch/rate control parameters can be altered during processing.
/// Please notice though that they aren't currently protected by semaphores,
/// so in multi-thread application external semaphore protection may be
/// required.
///
/// - This class utilizes classes 'TDStretch' for tempo change (without modifying
/// pitch) and 'RateTransposer' for changing the playback rate (that is, both
/// tempo and pitch in the same ratio) of the sound. The third available control
/// 'pitch' (change pitch but maintain tempo) is produced by a combination of
/// combining the two other controls.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include <assert.h>
#include <stdlib.h>
#include <memory.h>
#include <math.h>
#include <stdio.h>
#include "SoundTouch.h"
#include "TDStretch.h"
#include "RateTransposer.h"
#include "cpu_detect.h"
using namespace soundtouch;
/// test if two floating point numbers are equal
#define TEST_FLOAT_EQUAL(a, b) (fabs(a - b) < 1e-10)
/// Print library version string for autoconf
extern "C" void soundtouch_ac_test()
{
printf("SoundTouch Version: %s\n",SOUNDTOUCH_VERSION);
}
SoundTouch::SoundTouch()
{
// Initialize rate transposer and tempo changer instances
pRateTransposer = new RateTransposer();
pTDStretch = TDStretch::newInstance();
setOutPipe(pTDStretch);
rate = tempo = 0;
virtualPitch =
virtualRate =
virtualTempo = 1.0;
calcEffectiveRateAndTempo();
samplesExpectedOut = 0;
samplesOutput = 0;
channels = 0;
bSrateSet = false;
}
SoundTouch::~SoundTouch()
{
delete pRateTransposer;
delete pTDStretch;
}
/// Get SoundTouch library version string
const char *SoundTouch::getVersionString()
{
static const char *_version = SOUNDTOUCH_VERSION;
return _version;
}
/// Get SoundTouch library version Id
uint SoundTouch::getVersionId()
{
return SOUNDTOUCH_VERSION_ID;
}
// Sets the number of channels, 1 = mono, 2 = stereo
void SoundTouch::setChannels(uint numChannels)
{
if (!verifyNumberOfChannels(numChannels)) return;
channels = numChannels;
pRateTransposer->setChannels((int)numChannels);
pTDStretch->setChannels((int)numChannels);
}
// Sets new rate control value. Normal rate = 1.0, smaller values
// represent slower rate, larger faster rates.
void SoundTouch::setRate(double newRate)
{
virtualRate = newRate;
calcEffectiveRateAndTempo();
}
// Sets new rate control value as a difference in percents compared
// to the original rate (-50 .. +100 %)
void SoundTouch::setRateChange(double newRate)
{
virtualRate = 1.0 + 0.01 * newRate;
calcEffectiveRateAndTempo();
}
// Sets new tempo control value. Normal tempo = 1.0, smaller values
// represent slower tempo, larger faster tempo.
void SoundTouch::setTempo(double newTempo)
{
virtualTempo = newTempo;
calcEffectiveRateAndTempo();
}
// Sets new tempo control value as a difference in percents compared
// to the original tempo (-50 .. +100 %)
void SoundTouch::setTempoChange(double newTempo)
{
virtualTempo = 1.0 + 0.01 * newTempo;
calcEffectiveRateAndTempo();
}
// Sets new pitch control value. Original pitch = 1.0, smaller values
// represent lower pitches, larger values higher pitch.
void SoundTouch::setPitch(double newPitch)
{
virtualPitch = newPitch;
calcEffectiveRateAndTempo();
}
// Sets pitch change in octaves compared to the original pitch
// (-1.00 .. +1.00)
void SoundTouch::setPitchOctaves(double newPitch)
{
virtualPitch = exp(0.69314718056 * newPitch);
calcEffectiveRateAndTempo();
}
// Sets pitch change in semi-tones compared to the original pitch
// (-12 .. +12)
void SoundTouch::setPitchSemiTones(int newPitch)
{
setPitchOctaves((double)newPitch / 12.0);
}
void SoundTouch::setPitchSemiTones(double newPitch)
{
setPitchOctaves(newPitch / 12.0);
}
// Calculates 'effective' rate and tempo values from the
// nominal control values.
void SoundTouch::calcEffectiveRateAndTempo()
{
double oldTempo = tempo;
double oldRate = rate;
tempo = virtualTempo / virtualPitch;
rate = virtualPitch * virtualRate;
if (!TEST_FLOAT_EQUAL(rate,oldRate)) pRateTransposer->setRate(rate);
if (!TEST_FLOAT_EQUAL(tempo, oldTempo)) pTDStretch->setTempo(tempo);
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER
if (rate <= 1.0f)
{
if (output != pTDStretch)
{
FIFOSamplePipe *tempoOut;
assert(output == pRateTransposer);
// move samples in the current output buffer to the output of pTDStretch
tempoOut = pTDStretch->getOutput();
tempoOut->moveSamples(*output);
// move samples in pitch transposer's store buffer to tempo changer's input
// deprecated : pTDStretch->moveSamples(*pRateTransposer->getStore());
output = pTDStretch;
}
}
else
#endif
{
if (output != pRateTransposer)
{
FIFOSamplePipe *transOut;
assert(output == pTDStretch);
// move samples in the current output buffer to the output of pRateTransposer
transOut = pRateTransposer->getOutput();
transOut->moveSamples(*output);
// move samples in tempo changer's input to pitch transposer's input
pRateTransposer->moveSamples(*pTDStretch->getInput());
output = pRateTransposer;
}
}
}
// Sets sample rate.
void SoundTouch::setSampleRate(uint srate)
{
// set sample rate, leave other tempo changer parameters as they are.
pTDStretch->setParameters((int)srate);
bSrateSet = true;
}
// Adds 'numSamples' pcs of samples from the 'samples' memory position into
// the input of the object.
void SoundTouch::putSamples(const SAMPLETYPE *samples, uint nSamples)
{
if (bSrateSet == false)
{
ST_THROW_RT_ERROR("SoundTouch : Sample rate not defined");
}
else if (channels == 0)
{
ST_THROW_RT_ERROR("SoundTouch : Number of channels not defined");
}
// accumulate how many samples are expected out from processing, given the current
// processing setting
samplesExpectedOut += (double)nSamples / ((double)rate * (double)tempo);
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER
if (rate <= 1.0f)
{
// transpose the rate down, output the transposed sound to tempo changer buffer
assert(output == pTDStretch);
pRateTransposer->putSamples(samples, nSamples);
pTDStretch->moveSamples(*pRateTransposer);
}
else
#endif
{
// evaluate the tempo changer, then transpose the rate up,
assert(output == pRateTransposer);
pTDStretch->putSamples(samples, nSamples);
pRateTransposer->moveSamples(*pTDStretch);
}
}
// Flushes the last samples from the processing pipeline to the output.
// Clears also the internal processing buffers.
//
// Note: This function is meant for extracting the last samples of a sound
// stream. This function may introduce additional blank samples in the end
// of the sound stream, and thus it's not recommended to call this function
// in the middle of a sound stream.
void SoundTouch::flush()
{
int i;
int numStillExpected;
SAMPLETYPE *buff = new SAMPLETYPE[128 * channels];
// how many samples are still expected to output
numStillExpected = (int)((long)(samplesExpectedOut + 0.5) - samplesOutput);
if (numStillExpected < 0) numStillExpected = 0;
memset(buff, 0, 128 * channels * sizeof(SAMPLETYPE));
// "Push" the last active samples out from the processing pipeline by
// feeding blank samples into the processing pipeline until new,
// processed samples appear in the output (not however, more than
// 24ksamples in any case)
for (i = 0; (numStillExpected > (int)numSamples()) && (i < 200); i ++)
{
putSamples(buff, 128);
}
adjustAmountOfSamples(numStillExpected);
delete[] buff;
// Clear input buffers
pTDStretch->clearInput();
// yet leave the output intouched as that's where the
// flushed samples are!
}
// Changes a setting controlling the processing system behaviour. See the
// 'SETTING_...' defines for available setting ID's.
bool SoundTouch::setSetting(int settingId, int value)
{
int sampleRate, sequenceMs, seekWindowMs, overlapMs;
// read current tdstretch routine parameters
pTDStretch->getParameters(&sampleRate, &sequenceMs, &seekWindowMs, &overlapMs);
switch (settingId)
{
case SETTING_USE_AA_FILTER :
// enables / disabless anti-alias filter
pRateTransposer->enableAAFilter((value != 0) ? true : false);
return true;
case SETTING_AA_FILTER_LENGTH :
// sets anti-alias filter length
pRateTransposer->getAAFilter()->setLength(value);
return true;
case SETTING_USE_QUICKSEEK :
// enables / disables tempo routine quick seeking algorithm
pTDStretch->enableQuickSeek((value != 0) ? true : false);
return true;
case SETTING_SEQUENCE_MS:
// change time-stretch sequence duration parameter
pTDStretch->setParameters(sampleRate, value, seekWindowMs, overlapMs);
return true;
case SETTING_SEEKWINDOW_MS:
// change time-stretch seek window length parameter
pTDStretch->setParameters(sampleRate, sequenceMs, value, overlapMs);
return true;
case SETTING_OVERLAP_MS:
// change time-stretch overlap length parameter
pTDStretch->setParameters(sampleRate, sequenceMs, seekWindowMs, value);
return true;
default :
return false;
}
}
// Reads a setting controlling the processing system behaviour. See the
// 'SETTING_...' defines for available setting ID's.
//
// Returns the setting value.
int SoundTouch::getSetting(int settingId) const
{
int temp;
switch (settingId)
{
case SETTING_USE_AA_FILTER :
return (uint)pRateTransposer->isAAFilterEnabled();
case SETTING_AA_FILTER_LENGTH :
return pRateTransposer->getAAFilter()->getLength();
case SETTING_USE_QUICKSEEK :
return (uint)pTDStretch->isQuickSeekEnabled();
case SETTING_SEQUENCE_MS:
pTDStretch->getParameters(nullptr, &temp, nullptr, nullptr);
return temp;
case SETTING_SEEKWINDOW_MS:
pTDStretch->getParameters(nullptr, nullptr, &temp, nullptr);
return temp;
case SETTING_OVERLAP_MS:
pTDStretch->getParameters(nullptr, nullptr, nullptr, &temp);
return temp;
case SETTING_NOMINAL_INPUT_SEQUENCE :
{
int size = pTDStretch->getInputSampleReq();
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER
if (rate <= 1.0)
{
// transposing done before timestretch, which impacts latency
return (int)(size * rate + 0.5);
}
#endif
return size;
}
case SETTING_NOMINAL_OUTPUT_SEQUENCE :
{
int size = pTDStretch->getOutputBatchSize();
if (rate > 1.0)
{
// transposing done after timestretch, which impacts latency
return (int)(size / rate + 0.5);
}
return size;
}
case SETTING_INITIAL_LATENCY:
{
double latency = pTDStretch->getLatency();
int latency_tr = pRateTransposer->getLatency();
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER
if (rate <= 1.0)
{
// transposing done before timestretch, which impacts latency
latency = (latency + latency_tr) * rate;
}
else
#endif
{
latency += (double)latency_tr / rate;
}
return (int)(latency + 0.5);
}
default :
return 0;
}
}
// Clears all the samples in the object's output and internal processing
// buffers.
void SoundTouch::clear()
{
samplesExpectedOut = 0;
samplesOutput = 0;
pRateTransposer->clear();
pTDStretch->clear();
}
/// Returns number of samples currently unprocessed.
uint SoundTouch::numUnprocessedSamples() const
{
FIFOSamplePipe * psp;
if (pTDStretch)
{
psp = pTDStretch->getInput();
if (psp)
{
return psp->numSamples();
}
}
return 0;
}
/// Output samples from beginning of the sample buffer. Copies requested samples to
/// output buffer and removes them from the sample buffer. If there are less than
/// 'numsample' samples in the buffer, returns all that available.
///
/// \return Number of samples returned.
uint SoundTouch::receiveSamples(SAMPLETYPE *output, uint maxSamples)
{
uint ret = FIFOProcessor::receiveSamples(output, maxSamples);
samplesOutput += (long)ret;
return ret;
}
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
/// sample buffer without copying them anywhere.
///
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
/// with 'ptrBegin' function.
uint SoundTouch::receiveSamples(uint maxSamples)
{
uint ret = FIFOProcessor::receiveSamples(maxSamples);
samplesOutput += (long)ret;
return ret;
}
/// Get ratio between input and output audio durations, useful for calculating
/// processed output duration: if you'll process a stream of N samples, then
/// you can expect to get out N * getInputOutputSampleRatio() samples.
double SoundTouch::getInputOutputSampleRatio()
{
return 1.0 / (tempo * rate);
}

29
lib/soundtouch-master/source/SoundTouch/SoundTouch.sln

@ -0,0 +1,29 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.23107.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SoundTouch", "SoundTouch.vcxproj", "{68A5DD20-7057-448B-8FE0-B6AC8D205509}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Win32 = Debug|Win32
Debug|x64 = Debug|x64
Release|Win32 = Release|Win32
Release|x64 = Release|x64
ReleaseX64|Win32 = ReleaseX64|Win32
ReleaseX64|x64 = ReleaseX64|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.Debug|Win32.ActiveCfg = Debug|Win32
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.Debug|x64.ActiveCfg = Debug|x64
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.Release|Win32.ActiveCfg = Release|Win32
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.Release|x64.ActiveCfg = Release|x64
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.ReleaseX64|Win32.ActiveCfg = ReleaseX64|Win32
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.ReleaseX64|Win32.Build.0 = ReleaseX64|Win32
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.ReleaseX64|x64.ActiveCfg = ReleaseX64|x64
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.ReleaseX64|x64.Build.0 = ReleaseX64|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

342
lib/soundtouch-master/source/SoundTouch/SoundTouch.vcxproj

@ -0,0 +1,342 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{68A5DD20-7057-448B-8FE0-B6AC8D205509}</ProjectGuid>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v142</PlatformToolset>
<UseOfMfc>false</UseOfMfc>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v142</PlatformToolset>
<UseOfMfc>false</UseOfMfc>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v142</PlatformToolset>
<UseOfMfc>false</UseOfMfc>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<PlatformToolset>v142</PlatformToolset>
<UseOfMfc>false</UseOfMfc>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC71.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC71.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC71.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC71.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<_ProjectFileVersion>14.0.23107.0</_ProjectFileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>$(ProjectName)_x64</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>$(ProjectName)D</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<TargetName>$(ProjectName)D_x64</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<Optimization>Full</Optimization>
<InlineFunctionExpansion>AnySuitable</InlineFunctionExpansion>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>..\..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<StringPooling>true</StringPooling>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<FloatingPointModel>Fast</FloatingPointModel>
<PrecompiledHeader />
<PrecompiledHeaderOutputFile>$(OutDir)$(TargetName).pch</PrecompiledHeaderOutputFile>
<AssemblerListingLocation>$(OutDir)</AssemblerListingLocation>
<ObjectFileName>$(OutDir)</ObjectFileName>
<ProgramDataBaseFileName>$(OutDir)</ProgramDataBaseFileName>
<WarningLevel>Level3</WarningLevel>
<SuppressStartupBanner>true</SuppressStartupBanner>
<DebugInformationFormat />
<CompileAs>Default</CompileAs>
<EnableEnhancedInstructionSet>StreamingSIMDExtensions2</EnableEnhancedInstructionSet>
<XMLDocumentationFileName>$(IntDir)</XMLDocumentationFileName>
<BrowseInformationFile>$(IntDir)</BrowseInformationFile>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Culture>0x040b</Culture>
</ResourceCompile>
<Lib>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<SuppressStartupBanner>true</SuppressStartupBanner>
</Lib>
<PostBuildEvent>
<Command>if not exist ..\..\lib mkdir ..\..\lib
copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Midl>
<TargetEnvironment>X64</TargetEnvironment>
</Midl>
<ClCompile>
<Optimization>Full</Optimization>
<InlineFunctionExpansion>AnySuitable</InlineFunctionExpansion>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>..\..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<StringPooling>true</StringPooling>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<FloatingPointModel>Fast</FloatingPointModel>
<PrecompiledHeader />
<PrecompiledHeaderOutputFile>$(OutDir)$(TargetName).pch</PrecompiledHeaderOutputFile>
<AssemblerListingLocation>$(OutDir)</AssemblerListingLocation>
<ObjectFileName>$(OutDir)</ObjectFileName>
<ProgramDataBaseFileName>$(OutDir)</ProgramDataBaseFileName>
<WarningLevel>Level3</WarningLevel>
<SuppressStartupBanner>true</SuppressStartupBanner>
<DebugInformationFormat />
<CompileAs>Default</CompileAs>
<EnableEnhancedInstructionSet>
</EnableEnhancedInstructionSet>
<XMLDocumentationFileName>$(IntDir)</XMLDocumentationFileName>
<BrowseInformationFile>$(IntDir)</BrowseInformationFile>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Culture>0x040b</Culture>
</ResourceCompile>
<Lib>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<SuppressStartupBanner>true</SuppressStartupBanner>
</Lib>
<PostBuildEvent>
<Command>if not exist ..\..\lib mkdir ..\..\lib
copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>..\..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<FloatingPointModel>Fast</FloatingPointModel>
<PrecompiledHeader />
<PrecompiledHeaderOutputFile>$(OutDir)$(TargetName).pch</PrecompiledHeaderOutputFile>
<AssemblerListingLocation>$(OutDir)</AssemblerListingLocation>
<ObjectFileName>$(OutDir)</ObjectFileName>
<ProgramDataBaseFileName>$(OutDir)</ProgramDataBaseFileName>
<BrowseInformation>true</BrowseInformation>
<WarningLevel>Level3</WarningLevel>
<SuppressStartupBanner>true</SuppressStartupBanner>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<CompileAs>Default</CompileAs>
<EnableEnhancedInstructionSet>StreamingSIMDExtensions2</EnableEnhancedInstructionSet>
<XMLDocumentationFileName>$(IntDir)</XMLDocumentationFileName>
<BrowseInformationFile>$(IntDir)</BrowseInformationFile>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Culture>0x040b</Culture>
</ResourceCompile>
<Lib>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<SuppressStartupBanner>true</SuppressStartupBanner>
</Lib>
<PostBuildEvent>
<Command>if not exist ..\..\lib mkdir ..\..\lib
copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Midl>
<TargetEnvironment>X64</TargetEnvironment>
</Midl>
<ClCompile>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>..\..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<FloatingPointModel>Fast</FloatingPointModel>
<PrecompiledHeader />
<PrecompiledHeaderOutputFile>$(OutDir)$(TargetName).pch</PrecompiledHeaderOutputFile>
<AssemblerListingLocation>$(OutDir)</AssemblerListingLocation>
<ObjectFileName>$(OutDir)</ObjectFileName>
<ProgramDataBaseFileName>$(OutDir)</ProgramDataBaseFileName>
<BrowseInformation>true</BrowseInformation>
<WarningLevel>Level3</WarningLevel>
<SuppressStartupBanner>true</SuppressStartupBanner>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<CompileAs>Default</CompileAs>
<EnableEnhancedInstructionSet>
</EnableEnhancedInstructionSet>
<XMLDocumentationFileName>$(IntDir)</XMLDocumentationFileName>
<BrowseInformationFile>$(IntDir)</BrowseInformationFile>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<ResourceCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<Culture>0x040b</Culture>
</ResourceCompile>
<Lib>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<SuppressStartupBanner>true</SuppressStartupBanner>
</Lib>
<PostBuildEvent>
<Command>if not exist ..\..\lib mkdir ..\..\lib
copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="AAFilter.cpp">
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization>
</ClCompile>
<ClCompile Include="BPMDetect.cpp">
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">4996</DisableSpecificWarnings>
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">4996</DisableSpecificWarnings>
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">4996</DisableSpecificWarnings>
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Release|x64'">4996</DisableSpecificWarnings>
</ClCompile>
<ClCompile Include="cpu_detect_x86.cpp" />
<ClCompile Include="FIFOSampleBuffer.cpp">
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization>
</ClCompile>
<ClCompile Include="FIRFilter.cpp">
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization>
</ClCompile>
<ClCompile Include="InterpolateCubic.cpp" />
<ClCompile Include="InterpolateLinear.cpp" />
<ClCompile Include="InterpolateShannon.cpp" />
<ClCompile Include="mmx_optimized.cpp" />
<ClCompile Include="PeakFinder.cpp" />
<ClCompile Include="RateTransposer.cpp">
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization>
</ClCompile>
<ClCompile Include="SoundTouch.cpp">
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization>
</ClCompile>
<ClCompile Include="sse_optimized.cpp" />
<ClCompile Include="TDStretch.cpp">
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization>
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks>
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization>
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\..\include\BPMDetect.h" />
<ClInclude Include="..\..\include\FIFOSampleBuffer.h" />
<ClInclude Include="..\..\include\FIFOSamplePipe.h" />
<ClInclude Include="..\..\include\SoundTouch.h" />
<ClInclude Include="..\..\include\STTypes.h" />
<ClInclude Include="AAFilter.h" />
<ClInclude Include="cpu_detect.h" />
<ClInclude Include="FIRFilter.h" />
<ClInclude Include="InterpolateCubic.h" />
<ClInclude Include="InterpolateLinear.h" />
<ClInclude Include="InterpolateShannon.h" />
<ClInclude Include="PeakFinder.h" />
<ClInclude Include="RateTransposer.h" />
<ClInclude Include="TDStretch.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

1085
lib/soundtouch-master/source/SoundTouch/TDStretch.cpp

File diff suppressed because it is too large Load Diff

279
lib/soundtouch-master/source/SoundTouch/TDStretch.h

@ -0,0 +1,279 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Sampled sound tempo changer/time stretch algorithm. Changes the sound tempo
/// while maintaining the original pitch by using a time domain WSOLA-like method
/// with several performance-increasing tweaks.
///
/// Note : MMX/SSE optimized functions reside in separate, platform-specific files
/// 'mmx_optimized.cpp' and 'sse_optimized.cpp'
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef TDStretch_H
#define TDStretch_H
#include <stddef.h>
#include "STTypes.h"
#include "RateTransposer.h"
#include "FIFOSamplePipe.h"
namespace soundtouch
{
/// Default values for sound processing parameters:
/// Notice that the default parameters are tuned for contemporary popular music
/// processing. For speech processing applications these parameters suit better:
/// #define DEFAULT_SEQUENCE_MS 40
/// #define DEFAULT_SEEKWINDOW_MS 15
/// #define DEFAULT_OVERLAP_MS 8
///
/// Default length of a single processing sequence, in milliseconds. This determines to how
/// long sequences the original sound is chopped in the time-stretch algorithm.
///
/// The larger this value is, the lesser sequences are used in processing. In principle
/// a bigger value sounds better when slowing down tempo, but worse when increasing tempo
/// and vice versa.
///
/// Increasing this value reduces computational burden & vice versa.
//#define DEFAULT_SEQUENCE_MS 40
#define DEFAULT_SEQUENCE_MS USE_AUTO_SEQUENCE_LEN
/// Giving this value for the sequence length sets automatic parameter value
/// according to tempo setting (recommended)
#define USE_AUTO_SEQUENCE_LEN 0
/// Seeking window default length in milliseconds for algorithm that finds the best possible
/// overlapping location. This determines from how wide window the algorithm may look for an
/// optimal joining location when mixing the sound sequences back together.
///
/// The bigger this window setting is, the higher the possibility to find a better mixing
/// position will become, but at the same time large values may cause a "drifting" artifact
/// because consequent sequences will be taken at more uneven intervals.
///
/// If there's a disturbing artifact that sounds as if a constant frequency was drifting
/// around, try reducing this setting.
///
/// Increasing this value increases computational burden & vice versa.
//#define DEFAULT_SEEKWINDOW_MS 15
#define DEFAULT_SEEKWINDOW_MS USE_AUTO_SEEKWINDOW_LEN
/// Giving this value for the seek window length sets automatic parameter value
/// according to tempo setting (recommended)
#define USE_AUTO_SEEKWINDOW_LEN 0
/// Overlap length in milliseconds. When the chopped sound sequences are mixed back together,
/// to form a continuous sound stream, this parameter defines over how long period the two
/// consecutive sequences are let to overlap each other.
///
/// This shouldn't be that critical parameter. If you reduce the DEFAULT_SEQUENCE_MS setting
/// by a large amount, you might wish to try a smaller value on this.
///
/// Increasing this value increases computational burden & vice versa.
#define DEFAULT_OVERLAP_MS 8
/// Class that does the time-stretch (tempo change) effect for the processed
/// sound.
class TDStretch : public FIFOProcessor
{
protected:
int channels;
int sampleReq;
int overlapLength;
int seekLength;
int seekWindowLength;
int overlapDividerBitsNorm;
int overlapDividerBitsPure;
int slopingDivider;
int sampleRate;
int sequenceMs;
int seekWindowMs;
int overlapMs;
unsigned long maxnorm;
float maxnormf;
double tempo;
double nominalSkip;
double skipFract;
bool bQuickSeek;
bool bAutoSeqSetting;
bool bAutoSeekSetting;
bool isBeginning;
SAMPLETYPE *pMidBuffer;
SAMPLETYPE *pMidBufferUnaligned;
FIFOSampleBuffer outputBuffer;
FIFOSampleBuffer inputBuffer;
void acceptNewOverlapLength(int newOverlapLength);
virtual void clearCrossCorrState();
void calculateOverlapLength(int overlapMs);
virtual double calcCrossCorr(const SAMPLETYPE *mixingPos, const SAMPLETYPE *compare, double &norm);
virtual double calcCrossCorrAccumulate(const SAMPLETYPE *mixingPos, const SAMPLETYPE *compare, double &norm);
virtual int seekBestOverlapPositionFull(const SAMPLETYPE *refPos);
virtual int seekBestOverlapPositionQuick(const SAMPLETYPE *refPos);
virtual int seekBestOverlapPosition(const SAMPLETYPE *refPos);
virtual void overlapStereo(SAMPLETYPE *output, const SAMPLETYPE *input) const;
virtual void overlapMono(SAMPLETYPE *output, const SAMPLETYPE *input) const;
virtual void overlapMulti(SAMPLETYPE *output, const SAMPLETYPE *input) const;
void clearMidBuffer();
void overlap(SAMPLETYPE *output, const SAMPLETYPE *input, uint ovlPos) const;
void calcSeqParameters();
void adaptNormalizer();
/// Changes the tempo of the given sound samples.
/// Returns amount of samples returned in the "output" buffer.
/// The maximum amount of samples that can be returned at a time is set by
/// the 'set_returnBuffer_size' function.
void processSamples();
public:
TDStretch();
virtual ~TDStretch() override;
/// Operator 'new' is overloaded so that it automatically creates a suitable instance
/// depending on if we've a MMX/SSE/etc-capable CPU available or not.
static void *operator new(size_t s);
/// Use this function instead of "new" operator to create a new instance of this class.
/// This function automatically chooses a correct feature set depending on if the CPU
/// supports MMX/SSE/etc extensions.
static TDStretch *newInstance();
/// Returns the output buffer object
FIFOSamplePipe *getOutput() { return &outputBuffer; };
/// Returns the input buffer object
FIFOSamplePipe *getInput() { return &inputBuffer; };
/// Sets new target tempo. Normal tempo = 'SCALE', smaller values represent slower
/// tempo, larger faster tempo.
void setTempo(double newTempo);
/// Returns nonzero if there aren't any samples available for outputting.
virtual void clear() override;
/// Clears the input buffer
void clearInput();
/// Sets the number of channels, 1 = mono, 2 = stereo
void setChannels(int numChannels);
/// Enables/disables the quick position seeking algorithm. Zero to disable,
/// nonzero to enable
void enableQuickSeek(bool enable);
/// Returns nonzero if the quick seeking algorithm is enabled.
bool isQuickSeekEnabled() const;
/// Sets routine control parameters. These control are certain time constants
/// defining how the sound is stretched to the desired duration.
//
/// 'sampleRate' = sample rate of the sound
/// 'sequenceMS' = one processing sequence length in milliseconds
/// 'seekwindowMS' = seeking window length for scanning the best overlapping
/// position
/// 'overlapMS' = overlapping length
void setParameters(int sampleRate, ///< Samplerate of sound being processed (Hz)
int sequenceMS = -1, ///< Single processing sequence length (ms)
int seekwindowMS = -1, ///< Offset seeking window length (ms)
int overlapMS = -1 ///< Sequence overlapping length (ms)
);
/// Get routine control parameters, see setParameters() function.
/// Any of the parameters to this function can be nullptr, in such case corresponding parameter
/// value isn't returned.
void getParameters(int *pSampleRate, int *pSequenceMs, int *pSeekWindowMs, int *pOverlapMs) const;
/// Adds 'numsamples' pcs of samples from the 'samples' memory position into
/// the input of the object.
virtual void putSamples(
const SAMPLETYPE *samples, ///< Input sample data
uint numSamples ///< Number of samples in 'samples' so that one sample
///< contains both channels if stereo
) override;
/// return nominal input sample requirement for triggering a processing batch
int getInputSampleReq() const
{
return (int)(nominalSkip + 0.5);
}
/// return nominal output sample amount when running a processing batch
int getOutputBatchSize() const
{
return seekWindowLength - overlapLength;
}
/// return approximate initial input-output latency
int getLatency() const
{
return sampleReq;
}
};
// Implementation-specific class declarations:
#ifdef SOUNDTOUCH_ALLOW_MMX
/// Class that implements MMX optimized routines for 16bit integer samples type.
class TDStretchMMX : public TDStretch
{
protected:
double calcCrossCorr(const short *mixingPos, const short *compare, double &norm) override;
double calcCrossCorrAccumulate(const short *mixingPos, const short *compare, double &norm) override;
virtual void overlapStereo(short *output, const short *input) const override;
virtual void clearCrossCorrState() override;
};
#endif /// SOUNDTOUCH_ALLOW_MMX
#ifdef SOUNDTOUCH_ALLOW_SSE
/// Class that implements SSE optimized routines for floating point samples type.
class TDStretchSSE : public TDStretch
{
protected:
double calcCrossCorr(const float *mixingPos, const float *compare, double &norm) override;
double calcCrossCorrAccumulate(const float *mixingPos, const float *compare, double &norm) override;
};
#endif /// SOUNDTOUCH_ALLOW_SSE
}
#endif /// TDStretch_H

55
lib/soundtouch-master/source/SoundTouch/cpu_detect.h

@ -0,0 +1,55 @@
////////////////////////////////////////////////////////////////////////////////
///
/// A header file for detecting the Intel MMX instructions set extension.
///
/// Please see 'mmx_win.cpp', 'mmx_cpp.cpp' and 'mmx_non_x86.cpp' for the
/// routine implementations for x86 Windows, x86 gnu version and non-x86
/// platforms, respectively.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#ifndef _CPU_DETECT_H_
#define _CPU_DETECT_H_
#include "STTypes.h"
#define SUPPORT_MMX 0x0001
#define SUPPORT_3DNOW 0x0002
#define SUPPORT_ALTIVEC 0x0004
#define SUPPORT_SSE 0x0008
#define SUPPORT_SSE2 0x0010
/// Checks which instruction set extensions are supported by the CPU.
///
/// \return A bitmask of supported extensions, see SUPPORT_... defines.
uint detectCPUextensions(void);
/// Disables given set of instruction extensions. See SUPPORT_... defines.
void disableExtensions(uint wDisableMask);
#endif // _CPU_DETECT_H_

130
lib/soundtouch-master/source/SoundTouch/cpu_detect_x86.cpp

@ -0,0 +1,130 @@
////////////////////////////////////////////////////////////////////////////////
///
/// Generic version of the x86 CPU extension detection routine.
///
/// This file is for GNU & other non-Windows compilers, see 'cpu_detect_x86_win.cpp'
/// for the Microsoft compiler version.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include "cpu_detect.h"
#include "STTypes.h"
#if defined(SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS)
#if defined(__GNUC__) && defined(__i386__)
// gcc
#include "cpuid.h"
#elif defined(_M_IX86)
// windows non-gcc
#include <intrin.h>
#endif
#define bit_MMX (1 << 23)
#define bit_SSE (1 << 25)
#define bit_SSE2 (1 << 26)
#endif
//////////////////////////////////////////////////////////////////////////////
//
// processor instructions extension detection routines
//
//////////////////////////////////////////////////////////////////////////////
// Flag variable indicating whick ISA extensions are disabled (for debugging)
static uint _dwDisabledISA = 0x00; // 0xffffffff; //<- use this to disable all extensions
// Disables given set of instruction extensions. See SUPPORT_... defines.
void disableExtensions(uint dwDisableMask)
{
_dwDisabledISA = dwDisableMask;
}
/// Checks which instruction set extensions are supported by the CPU.
uint detectCPUextensions(void)
{
/// If building for a 64bit system (no Itanium) and the user wants optimizations.
/// Return the OR of SUPPORT_{MMX,SSE,SSE2}. 11001 or 0x19.
/// Keep the _dwDisabledISA test (2 more operations, could be eliminated).
#if ((defined(__GNUC__) && defined(__x86_64__)) \
|| defined(_M_X64)) \
&& defined(SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS)
return 0x19 & ~_dwDisabledISA;
/// If building for a 32bit system and the user wants optimizations.
/// Keep the _dwDisabledISA test (2 more operations, could be eliminated).
#elif ((defined(__GNUC__) && defined(__i386__)) \
|| defined(_M_IX86)) \
&& defined(SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS)
if (_dwDisabledISA == 0xffffffff) return 0;
uint res = 0;
#if defined(__GNUC__)
// GCC version of cpuid. Requires GCC 4.3.0 or later for __cpuid intrinsic support.
uint eax, ebx, ecx, edx; // unsigned int is the standard type. uint is defined by the compiler and not guaranteed to be portable.
// Check if no cpuid support.
if (!__get_cpuid (1, &eax, &ebx, &ecx, &edx)) return 0; // always disable extensions.
if (edx & bit_MMX) res = res | SUPPORT_MMX;
if (edx & bit_SSE) res = res | SUPPORT_SSE;
if (edx & bit_SSE2) res = res | SUPPORT_SSE2;
#else
// Window / VS version of cpuid. Notice that Visual Studio 2005 or later required
// for __cpuid intrinsic support.
int reg[4] = {-1};
// Check if no cpuid support.
__cpuid(reg,0);
if ((unsigned int)reg[0] == 0) return 0; // always disable extensions.
__cpuid(reg,1);
if ((unsigned int)reg[3] & bit_MMX) res = res | SUPPORT_MMX;
if ((unsigned int)reg[3] & bit_SSE) res = res | SUPPORT_SSE;
if ((unsigned int)reg[3] & bit_SSE2) res = res | SUPPORT_SSE2;
#endif
return res & ~_dwDisabledISA;
#else
/// One of these is true:
/// 1) We don't want optimizations.
/// 2) Using an unsupported compiler.
/// 3) Running on a non-x86 platform.
return 0;
#endif
}

392
lib/soundtouch-master/source/SoundTouch/mmx_optimized.cpp

@ -0,0 +1,392 @@
////////////////////////////////////////////////////////////////////////////////
///
/// MMX optimized routines. All MMX optimized functions have been gathered into
/// this single source code file, regardless to their class or original source
/// code file, in order to ease porting the library to other compiler and
/// processor platforms.
///
/// The MMX-optimizations are programmed using MMX compiler intrinsics that
/// are supported both by Microsoft Visual C++ and GCC compilers, so this file
/// should compile with both toolsets.
///
/// NOTICE: If using Visual Studio 6.0, you'll need to install the "Visual C++
/// 6.0 processor pack" update to support compiler intrinsic syntax. The update
/// is available for download at Microsoft Developers Network, see here:
/// http://msdn.microsoft.com/en-us/vstudio/aa718349.aspx
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include "STTypes.h"
#ifdef SOUNDTOUCH_ALLOW_MMX
// MMX routines available only with integer sample type
using namespace soundtouch;
//////////////////////////////////////////////////////////////////////////////
//
// implementation of MMX optimized functions of class 'TDStretchMMX'
//
//////////////////////////////////////////////////////////////////////////////
#include "TDStretch.h"
#include <mmintrin.h>
#include <limits.h>
#include <math.h>
// Calculates cross correlation of two buffers
double TDStretchMMX::calcCrossCorr(const short *pV1, const short *pV2, double &dnorm)
{
const __m64 *pVec1, *pVec2;
__m64 shifter;
__m64 accu, normaccu;
long corr, norm;
int i;
pVec1 = (__m64*)pV1;
pVec2 = (__m64*)pV2;
shifter = _m_from_int(overlapDividerBitsNorm);
normaccu = accu = _mm_setzero_si64();
// Process 4 parallel sets of 2 * stereo samples or 4 * mono samples
// during each round for improved CPU-level parallellization.
for (i = 0; i < channels * overlapLength / 16; i ++)
{
__m64 temp, temp2;
// dictionary of instructions:
// _m_pmaddwd : 4*16bit multiply-add, resulting two 32bits = [a0*b0+a1*b1 ; a2*b2+a3*b3]
// _mm_add_pi32 : 2*32bit add
// _m_psrad : 32bit right-shift
temp = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[0], pVec2[0]), shifter),
_mm_sra_pi32(_mm_madd_pi16(pVec1[1], pVec2[1]), shifter));
temp2 = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[0], pVec1[0]), shifter),
_mm_sra_pi32(_mm_madd_pi16(pVec1[1], pVec1[1]), shifter));
accu = _mm_add_pi32(accu, temp);
normaccu = _mm_add_pi32(normaccu, temp2);
temp = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[2], pVec2[2]), shifter),
_mm_sra_pi32(_mm_madd_pi16(pVec1[3], pVec2[3]), shifter));
temp2 = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[2], pVec1[2]), shifter),
_mm_sra_pi32(_mm_madd_pi16(pVec1[3], pVec1[3]), shifter));
accu = _mm_add_pi32(accu, temp);
normaccu = _mm_add_pi32(normaccu, temp2);
pVec1 += 4;
pVec2 += 4;
}
// copy hi-dword of mm0 to lo-dword of mm1, then sum mmo+mm1
// and finally store the result into the variable "corr"
accu = _mm_add_pi32(accu, _mm_srli_si64(accu, 32));
corr = _m_to_int(accu);
normaccu = _mm_add_pi32(normaccu, _mm_srli_si64(normaccu, 32));
norm = _m_to_int(normaccu);
// Clear MMS state
_m_empty();
if (norm > (long)maxnorm)
{
// modify 'maxnorm' inside critical section to avoid multi-access conflict if in OpenMP mode
maxnorm = norm;
}
// Normalize result by dividing by sqrt(norm) - this step is easiest
// done using floating point operation
dnorm = (double)norm;
return (double)corr / sqrt(dnorm < 1e-9 ? 1.0 : dnorm);
// Note: Warning about the missing EMMS instruction is harmless
// as it'll be called elsewhere.
}
/// Update cross-correlation by accumulating "norm" coefficient by previously calculated value
double TDStretchMMX::calcCrossCorrAccumulate(const short *pV1, const short *pV2, double &dnorm)
{
const __m64 *pVec1, *pVec2;
__m64 shifter;
__m64 accu;
long corr, lnorm;
int i;
// cancel first normalizer tap from previous round
lnorm = 0;
for (i = 1; i <= channels; i ++)
{
lnorm -= (pV1[-i] * pV1[-i]) >> overlapDividerBitsNorm;
}
pVec1 = (__m64*)pV1;
pVec2 = (__m64*)pV2;
shifter = _m_from_int(overlapDividerBitsNorm);
accu = _mm_setzero_si64();
// Process 4 parallel sets of 2 * stereo samples or 4 * mono samples
// during each round for improved CPU-level parallellization.
for (i = 0; i < channels * overlapLength / 16; i ++)
{
__m64 temp;
// dictionary of instructions:
// _m_pmaddwd : 4*16bit multiply-add, resulting two 32bits = [a0*b0+a1*b1 ; a2*b2+a3*b3]
// _mm_add_pi32 : 2*32bit add
// _m_psrad : 32bit right-shift
temp = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[0], pVec2[0]), shifter),
_mm_sra_pi32(_mm_madd_pi16(pVec1[1], pVec2[1]), shifter));
accu = _mm_add_pi32(accu, temp);
temp = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[2], pVec2[2]), shifter),
_mm_sra_pi32(_mm_madd_pi16(pVec1[3], pVec2[3]), shifter));
accu = _mm_add_pi32(accu, temp);
pVec1 += 4;
pVec2 += 4;
}
// copy hi-dword of mm0 to lo-dword of mm1, then sum mmo+mm1
// and finally store the result into the variable "corr"
accu = _mm_add_pi32(accu, _mm_srli_si64(accu, 32));
corr = _m_to_int(accu);
// Clear MMS state
_m_empty();
// update normalizer with last samples of this round
pV1 = (short *)pVec1;
for (int j = 1; j <= channels; j ++)
{
lnorm += (pV1[-j] * pV1[-j]) >> overlapDividerBitsNorm;
}
dnorm += (double)lnorm;
if (lnorm > (long)maxnorm)
{
maxnorm = lnorm;
}
// Normalize result by dividing by sqrt(norm) - this step is easiest
// done using floating point operation
return (double)corr / sqrt((dnorm < 1e-9) ? 1.0 : dnorm);
}
void TDStretchMMX::clearCrossCorrState()
{
// Clear MMS state
_m_empty();
//_asm EMMS;
}
// MMX-optimized version of the function overlapStereo
void TDStretchMMX::overlapStereo(short *output, const short *input) const
{
const __m64 *pVinput, *pVMidBuf;
__m64 *pVdest;
__m64 mix1, mix2, adder, shifter;
int i;
pVinput = (const __m64*)input;
pVMidBuf = (const __m64*)pMidBuffer;
pVdest = (__m64*)output;
// mix1 = mixer values for 1st stereo sample
// mix1 = mixer values for 2nd stereo sample
// adder = adder for updating mixer values after each round
mix1 = _mm_set_pi16(0, overlapLength, 0, overlapLength);
adder = _mm_set_pi16(1, -1, 1, -1);
mix2 = _mm_add_pi16(mix1, adder);
adder = _mm_add_pi16(adder, adder);
// Overlaplength-division by shifter. "+1" is to account for "-1" deduced in
// overlapDividerBits calculation earlier.
shifter = _m_from_int(overlapDividerBitsPure + 1);
for (i = 0; i < overlapLength / 4; i ++)
{
__m64 temp1, temp2;
// load & shuffle data so that input & mixbuffer data samples are paired
temp1 = _mm_unpacklo_pi16(pVMidBuf[0], pVinput[0]); // = i0l m0l i0r m0r
temp2 = _mm_unpackhi_pi16(pVMidBuf[0], pVinput[0]); // = i1l m1l i1r m1r
// temp = (temp .* mix) >> shifter
temp1 = _mm_sra_pi32(_mm_madd_pi16(temp1, mix1), shifter);
temp2 = _mm_sra_pi32(_mm_madd_pi16(temp2, mix2), shifter);
pVdest[0] = _mm_packs_pi32(temp1, temp2); // pack 2*2*32bit => 4*16bit
// update mix += adder
mix1 = _mm_add_pi16(mix1, adder);
mix2 = _mm_add_pi16(mix2, adder);
// --- second round begins here ---
// load & shuffle data so that input & mixbuffer data samples are paired
temp1 = _mm_unpacklo_pi16(pVMidBuf[1], pVinput[1]); // = i2l m2l i2r m2r
temp2 = _mm_unpackhi_pi16(pVMidBuf[1], pVinput[1]); // = i3l m3l i3r m3r
// temp = (temp .* mix) >> shifter
temp1 = _mm_sra_pi32(_mm_madd_pi16(temp1, mix1), shifter);
temp2 = _mm_sra_pi32(_mm_madd_pi16(temp2, mix2), shifter);
pVdest[1] = _mm_packs_pi32(temp1, temp2); // pack 2*2*32bit => 4*16bit
// update mix += adder
mix1 = _mm_add_pi16(mix1, adder);
mix2 = _mm_add_pi16(mix2, adder);
pVinput += 2;
pVMidBuf += 2;
pVdest += 2;
}
_m_empty(); // clear MMS state
}
//////////////////////////////////////////////////////////////////////////////
//
// implementation of MMX optimized functions of class 'FIRFilter'
//
//////////////////////////////////////////////////////////////////////////////
#include "FIRFilter.h"
FIRFilterMMX::FIRFilterMMX() : FIRFilter()
{
filterCoeffsAlign = nullptr;
filterCoeffsUnalign = nullptr;
}
FIRFilterMMX::~FIRFilterMMX()
{
delete[] filterCoeffsUnalign;
}
// (overloaded) Calculates filter coefficients for MMX routine
void FIRFilterMMX::setCoefficients(const short *coeffs, uint newLength, uint uResultDivFactor)
{
uint i;
FIRFilter::setCoefficients(coeffs, newLength, uResultDivFactor);
// Ensure that filter coeffs array is aligned to 16-byte boundary
delete[] filterCoeffsUnalign;
filterCoeffsUnalign = new short[2 * newLength + 8];
filterCoeffsAlign = (short *)SOUNDTOUCH_ALIGN_POINTER_16(filterCoeffsUnalign);
// rearrange the filter coefficients for mmx routines
for (i = 0;i < length; i += 4)
{
filterCoeffsAlign[2 * i + 0] = coeffs[i + 0];
filterCoeffsAlign[2 * i + 1] = coeffs[i + 2];
filterCoeffsAlign[2 * i + 2] = coeffs[i + 0];
filterCoeffsAlign[2 * i + 3] = coeffs[i + 2];
filterCoeffsAlign[2 * i + 4] = coeffs[i + 1];
filterCoeffsAlign[2 * i + 5] = coeffs[i + 3];
filterCoeffsAlign[2 * i + 6] = coeffs[i + 1];
filterCoeffsAlign[2 * i + 7] = coeffs[i + 3];
}
}
// mmx-optimized version of the filter routine for stereo sound
uint FIRFilterMMX::evaluateFilterStereo(short *dest, const short *src, uint numSamples) const
{
// Create stack copies of the needed member variables for asm routines :
uint i, j;
__m64 *pVdest = (__m64*)dest;
if (length < 2) return 0;
for (i = 0; i < (numSamples - length) / 2; i ++)
{
__m64 accu1;
__m64 accu2;
const __m64 *pVsrc = (const __m64*)src;
const __m64 *pVfilter = (const __m64*)filterCoeffsAlign;
accu1 = accu2 = _mm_setzero_si64();
for (j = 0; j < lengthDiv8 * 2; j ++)
{
__m64 temp1, temp2;
temp1 = _mm_unpacklo_pi16(pVsrc[0], pVsrc[1]); // = l2 l0 r2 r0
temp2 = _mm_unpackhi_pi16(pVsrc[0], pVsrc[1]); // = l3 l1 r3 r1
accu1 = _mm_add_pi32(accu1, _mm_madd_pi16(temp1, pVfilter[0])); // += l2*f2+l0*f0 r2*f2+r0*f0
accu1 = _mm_add_pi32(accu1, _mm_madd_pi16(temp2, pVfilter[1])); // += l3*f3+l1*f1 r3*f3+r1*f1
temp1 = _mm_unpacklo_pi16(pVsrc[1], pVsrc[2]); // = l4 l2 r4 r2
accu2 = _mm_add_pi32(accu2, _mm_madd_pi16(temp2, pVfilter[0])); // += l3*f2+l1*f0 r3*f2+r1*f0
accu2 = _mm_add_pi32(accu2, _mm_madd_pi16(temp1, pVfilter[1])); // += l4*f3+l2*f1 r4*f3+r2*f1
// accu1 += l2*f2+l0*f0 r2*f2+r0*f0
// += l3*f3+l1*f1 r3*f3+r1*f1
// accu2 += l3*f2+l1*f0 r3*f2+r1*f0
// l4*f3+l2*f1 r4*f3+r2*f1
pVfilter += 2;
pVsrc += 2;
}
// accu >>= resultDivFactor
accu1 = _mm_srai_pi32(accu1, resultDivFactor);
accu2 = _mm_srai_pi32(accu2, resultDivFactor);
// pack 2*2*32bits => 4*16 bits
pVdest[0] = _mm_packs_pi32(accu1, accu2);
src += 4;
pVdest ++;
}
_m_empty(); // clear emms state
return (numSamples & 0xfffffffe) - length;
}
#else
// workaround to not complain about empty module
bool _dontcomplain_mmx_empty;
#endif // SOUNDTOUCH_ALLOW_MMX

364
lib/soundtouch-master/source/SoundTouch/sse_optimized.cpp

@ -0,0 +1,364 @@
////////////////////////////////////////////////////////////////////////////////
///
/// SSE optimized routines for Pentium-III, Athlon-XP and later CPUs. All SSE
/// optimized functions have been gathered into this single source
/// code file, regardless to their class or original source code file, in order
/// to ease porting the library to other compiler and processor platforms.
///
/// The SSE-optimizations are programmed using SSE compiler intrinsics that
/// are supported both by Microsoft Visual C++ and GCC compilers, so this file
/// should compile with both toolsets.
///
/// NOTICE: If using Visual Studio 6.0, you'll need to install the "Visual C++
/// 6.0 processor pack" update to support SSE instruction set. The update is
/// available for download at Microsoft Developers Network, see here:
/// http://msdn.microsoft.com/en-us/vstudio/aa718349.aspx
///
/// If the above URL is expired or removed, go to "http://msdn.microsoft.com" and
/// perform a search with keywords "processor pack".
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#include "cpu_detect.h"
#include "STTypes.h"
using namespace soundtouch;
#ifdef SOUNDTOUCH_ALLOW_SSE
// SSE routines available only with float sample type
//////////////////////////////////////////////////////////////////////////////
//
// implementation of SSE optimized functions of class 'TDStretchSSE'
//
//////////////////////////////////////////////////////////////////////////////
#include "TDStretch.h"
#include <xmmintrin.h>
#include <math.h>
// Calculates cross correlation of two buffers
double TDStretchSSE::calcCrossCorr(const float *pV1, const float *pV2, double &anorm)
{
int i;
const float *pVec1;
const __m128 *pVec2;
__m128 vSum, vNorm;
// Note. It means a major slow-down if the routine needs to tolerate
// unaligned __m128 memory accesses. It's way faster if we can skip
// unaligned slots and use _mm_load_ps instruction instead of _mm_loadu_ps.
// This can mean up to ~ 10-fold difference (incl. part of which is
// due to skipping every second round for stereo sound though).
//
// Compile-time define SOUNDTOUCH_ALLOW_NONEXACT_SIMD_OPTIMIZATION is provided
// for choosing if this little cheating is allowed.
#ifdef ST_SIMD_AVOID_UNALIGNED
// Little cheating allowed, return valid correlation only for
// aligned locations, meaning every second round for stereo sound.
#define _MM_LOAD _mm_load_ps
if (((ulongptr)pV1) & 15) return -1e50; // skip unaligned locations
#else
// No cheating allowed, use unaligned load & take the resulting
// performance hit.
#define _MM_LOAD _mm_loadu_ps
#endif
// ensure overlapLength is divisible by 8
assert((overlapLength % 8) == 0);
// Calculates the cross-correlation value between 'pV1' and 'pV2' vectors
// Note: pV2 _must_ be aligned to 16-bit boundary, pV1 need not.
pVec1 = (const float*)pV1;
pVec2 = (const __m128*)pV2;
vSum = vNorm = _mm_setzero_ps();
// Unroll the loop by factor of 4 * 4 operations. Use same routine for
// stereo & mono, for mono it just means twice the amount of unrolling.
for (i = 0; i < channels * overlapLength / 16; i ++)
{
__m128 vTemp;
// vSum += pV1[0..3] * pV2[0..3]
vTemp = _MM_LOAD(pVec1);
vSum = _mm_add_ps(vSum, _mm_mul_ps(vTemp ,pVec2[0]));
vNorm = _mm_add_ps(vNorm, _mm_mul_ps(vTemp ,vTemp));
// vSum += pV1[4..7] * pV2[4..7]
vTemp = _MM_LOAD(pVec1 + 4);
vSum = _mm_add_ps(vSum, _mm_mul_ps(vTemp, pVec2[1]));
vNorm = _mm_add_ps(vNorm, _mm_mul_ps(vTemp ,vTemp));
// vSum += pV1[8..11] * pV2[8..11]
vTemp = _MM_LOAD(pVec1 + 8);
vSum = _mm_add_ps(vSum, _mm_mul_ps(vTemp, pVec2[2]));
vNorm = _mm_add_ps(vNorm, _mm_mul_ps(vTemp ,vTemp));
// vSum += pV1[12..15] * pV2[12..15]
vTemp = _MM_LOAD(pVec1 + 12);
vSum = _mm_add_ps(vSum, _mm_mul_ps(vTemp, pVec2[3]));
vNorm = _mm_add_ps(vNorm, _mm_mul_ps(vTemp ,vTemp));
pVec1 += 16;
pVec2 += 4;
}
// return value = vSum[0] + vSum[1] + vSum[2] + vSum[3]
float *pvNorm = (float*)&vNorm;
float norm = (pvNorm[0] + pvNorm[1] + pvNorm[2] + pvNorm[3]);
anorm = norm;
float *pvSum = (float*)&vSum;
return (double)(pvSum[0] + pvSum[1] + pvSum[2] + pvSum[3]) / sqrt(norm < 1e-9 ? 1.0 : norm);
/* This is approximately corresponding routine in C-language yet without normalization:
double corr, norm;
uint i;
// Calculates the cross-correlation value between 'pV1' and 'pV2' vectors
corr = norm = 0.0;
for (i = 0; i < channels * overlapLength / 16; i ++)
{
corr += pV1[0] * pV2[0] +
pV1[1] * pV2[1] +
pV1[2] * pV2[2] +
pV1[3] * pV2[3] +
pV1[4] * pV2[4] +
pV1[5] * pV2[5] +
pV1[6] * pV2[6] +
pV1[7] * pV2[7] +
pV1[8] * pV2[8] +
pV1[9] * pV2[9] +
pV1[10] * pV2[10] +
pV1[11] * pV2[11] +
pV1[12] * pV2[12] +
pV1[13] * pV2[13] +
pV1[14] * pV2[14] +
pV1[15] * pV2[15];
for (j = 0; j < 15; j ++) norm += pV1[j] * pV1[j];
pV1 += 16;
pV2 += 16;
}
return corr / sqrt(norm);
*/
}
double TDStretchSSE::calcCrossCorrAccumulate(const float *pV1, const float *pV2, double &norm)
{
// call usual calcCrossCorr function because SSE does not show big benefit of
// accumulating "norm" value, and also the "norm" rolling algorithm would get
// complicated due to SSE-specific alignment-vs-nonexact correlation rules.
return calcCrossCorr(pV1, pV2, norm);
}
//////////////////////////////////////////////////////////////////////////////
//
// implementation of SSE optimized functions of class 'FIRFilter'
//
//////////////////////////////////////////////////////////////////////////////
#include "FIRFilter.h"
FIRFilterSSE::FIRFilterSSE() : FIRFilter()
{
filterCoeffsAlign = nullptr;
filterCoeffsUnalign = nullptr;
}
FIRFilterSSE::~FIRFilterSSE()
{
delete[] filterCoeffsUnalign;
filterCoeffsAlign = nullptr;
filterCoeffsUnalign = nullptr;
}
// (overloaded) Calculates filter coefficients for SSE routine
void FIRFilterSSE::setCoefficients(const float *coeffs, uint newLength, uint uResultDivFactor)
{
uint i;
float fDivider;
FIRFilter::setCoefficients(coeffs, newLength, uResultDivFactor);
// Scale the filter coefficients so that it won't be necessary to scale the filtering result
// also rearrange coefficients suitably for SSE
// Ensure that filter coeffs array is aligned to 16-byte boundary
delete[] filterCoeffsUnalign;
filterCoeffsUnalign = new float[2 * newLength + 4];
filterCoeffsAlign = (float *)SOUNDTOUCH_ALIGN_POINTER_16(filterCoeffsUnalign);
fDivider = (float)resultDivider;
// rearrange the filter coefficients for mmx routines
for (i = 0; i < newLength; i ++)
{
filterCoeffsAlign[2 * i + 0] =
filterCoeffsAlign[2 * i + 1] = coeffs[i + 0] / fDivider;
}
}
// SSE-optimized version of the filter routine for stereo sound
uint FIRFilterSSE::evaluateFilterStereo(float *dest, const float *source, uint numSamples) const
{
int count = (int)((numSamples - length) & (uint)-2);
int j;
assert(count % 2 == 0);
if (count < 2) return 0;
assert(source != nullptr);
assert(dest != nullptr);
assert((length % 8) == 0);
assert(filterCoeffsAlign != nullptr);
assert(((ulongptr)filterCoeffsAlign) % 16 == 0);
// filter is evaluated for two stereo samples with each iteration, thus use of 'j += 2'
for (j = 0; j < count; j += 2)
{
const float *pSrc;
float *pDest;
const __m128 *pFil;
__m128 sum1, sum2;
uint i;
pSrc = (const float*)source + j * 2; // source audio data
pDest = dest + j * 2; // destination audio data
pFil = (const __m128*)filterCoeffsAlign; // filter coefficients. NOTE: Assumes coefficients
// are aligned to 16-byte boundary
sum1 = sum2 = _mm_setzero_ps();
for (i = 0; i < length / 8; i ++)
{
// Unroll loop for efficiency & calculate filter for 2*2 stereo samples
// at each pass
// sum1 is accu for 2*2 filtered stereo sound data at the primary sound data offset
// sum2 is accu for 2*2 filtered stereo sound data for the next sound sample offset.
sum1 = _mm_add_ps(sum1, _mm_mul_ps(_mm_loadu_ps(pSrc) , pFil[0]));
sum2 = _mm_add_ps(sum2, _mm_mul_ps(_mm_loadu_ps(pSrc + 2), pFil[0]));
sum1 = _mm_add_ps(sum1, _mm_mul_ps(_mm_loadu_ps(pSrc + 4), pFil[1]));
sum2 = _mm_add_ps(sum2, _mm_mul_ps(_mm_loadu_ps(pSrc + 6), pFil[1]));
sum1 = _mm_add_ps(sum1, _mm_mul_ps(_mm_loadu_ps(pSrc + 8) , pFil[2]));
sum2 = _mm_add_ps(sum2, _mm_mul_ps(_mm_loadu_ps(pSrc + 10), pFil[2]));
sum1 = _mm_add_ps(sum1, _mm_mul_ps(_mm_loadu_ps(pSrc + 12), pFil[3]));
sum2 = _mm_add_ps(sum2, _mm_mul_ps(_mm_loadu_ps(pSrc + 14), pFil[3]));
pSrc += 16;
pFil += 4;
}
// Now sum1 and sum2 both have a filtered 2-channel sample each, but we still need
// to sum the two hi- and lo-floats of these registers together.
// post-shuffle & add the filtered values and store to dest.
_mm_storeu_ps(pDest, _mm_add_ps(
_mm_shuffle_ps(sum1, sum2, _MM_SHUFFLE(1,0,3,2)), // s2_1 s2_0 s1_3 s1_2
_mm_shuffle_ps(sum1, sum2, _MM_SHUFFLE(3,2,1,0)) // s2_3 s2_2 s1_1 s1_0
));
}
// Ideas for further improvement:
// 1. If it could be guaranteed that 'source' were always aligned to 16-byte
// boundary, a faster aligned '_mm_load_ps' instruction could be used.
// 2. If it could be guaranteed that 'dest' were always aligned to 16-byte
// boundary, a faster '_mm_store_ps' instruction could be used.
return (uint)count;
/* original routine in C-language. please notice the C-version has differently
organized coefficients though.
double suml1, suml2;
double sumr1, sumr2;
uint i, j;
for (j = 0; j < count; j += 2)
{
const float *ptr;
const float *pFil;
suml1 = sumr1 = 0.0;
suml2 = sumr2 = 0.0;
ptr = src;
pFil = filterCoeffs;
for (i = 0; i < lengthLocal; i ++)
{
// unroll loop for efficiency.
suml1 += ptr[0] * pFil[0] +
ptr[2] * pFil[2] +
ptr[4] * pFil[4] +
ptr[6] * pFil[6];
sumr1 += ptr[1] * pFil[1] +
ptr[3] * pFil[3] +
ptr[5] * pFil[5] +
ptr[7] * pFil[7];
suml2 += ptr[8] * pFil[0] +
ptr[10] * pFil[2] +
ptr[12] * pFil[4] +
ptr[14] * pFil[6];
sumr2 += ptr[9] * pFil[1] +
ptr[11] * pFil[3] +
ptr[13] * pFil[5] +
ptr[15] * pFil[7];
ptr += 16;
pFil += 8;
}
dest[0] = (float)suml1;
dest[1] = (float)sumr1;
dest[2] = (float)suml2;
dest[3] = (float)sumr2;
src += 4;
dest += 4;
}
*/
}
#endif // SOUNDTOUCH_ALLOW_SSE

250
lib/soundtouch-master/source/SoundTouchDLL/SoundTouchDLL.cpp

@ -0,0 +1,250 @@
//////////////////////////////////////////////////////////////////////////////
///
/// SoundTouch DLL wrapper - wraps SoundTouch routines into a Dynamic Load
/// Library interface.
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// SoundTouch WWW: http://www.surina.net/soundtouch
///
////////////////////////////////////////////////////////////////////////////////
//
// License :
//
// SoundTouch audio processing library
// Copyright (c) Olli Parviainen
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
////////////////////////////////////////////////////////////////////////////////
#if defined(_WIN32) || defined(WIN32)
#include <windows.h>
// DLL main in Windows compilation
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
#endif
#include <limits.h>
#include <string.h>
#include "SoundTouchDLL.h"
#include "SoundTouch.h"
#include "BPMDetect.h"
using namespace soundtouch;
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
#error "error - compile the dll version with float samples"
#endif // SOUNDTOUCH_INTEGER_SAMPLES
//////////////
SOUNDTOUCHDLL_API ST_HANDLE __cdecl soundtouch_createInstance()
{
return new SoundTouch();
}
SOUNDTOUCHDLL_API void __cdecl soundtouch_destroyInstance(ST_HANDLE h)
{
delete static_cast<SoundTouch*>(h);
}
/// Get SoundTouch library version string
SOUNDTOUCHDLL_API const char *__cdecl soundtouch_getVersionString()
{
return SoundTouch::getVersionString();
}
/// Get SoundTouch library version Id
SOUNDTOUCHDLL_API uint __cdecl soundtouch_getVersionId()
{
return SoundTouch::getVersionId();
}
/// Sets new rate control value. Normal rate = 1.0, smaller values
/// represent slower rate, larger faster rates.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRate(ST_HANDLE h, float newRate)
{
static_cast<SoundTouch*>(h)->setRate(newRate);
}
/// Sets new tempo control value. Normal tempo = 1.0, smaller values
/// represent slower tempo, larger faster tempo.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setTempo(ST_HANDLE h, float newTempo)
{
static_cast<SoundTouch*>(h)->setTempo(newTempo);
}
/// Sets new rate control value as a difference in percents compared
/// to the original rate (-50 .. +100 %)
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRateChange(ST_HANDLE h, float newRate)
{
static_cast<SoundTouch*>(h)->setRateChange(newRate);
}
/// Sets new tempo control value as a difference in percents compared
/// to the original tempo (-50 .. +100 %)
SOUNDTOUCHDLL_API void __cdecl soundtouch_setTempoChange(ST_HANDLE h, float newTempo)
{
static_cast<SoundTouch*>(h)->setTempoChange(newTempo);
}
/// Sets new pitch control value. Original pitch = 1.0, smaller values
/// represent lower pitches, larger values higher pitch.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitch(ST_HANDLE h, float newPitch)
{
static_cast<SoundTouch*>(h)->setPitch(newPitch);
}
/// Sets pitch change in octaves compared to the original pitch
/// (-1.00 .. +1.00)
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitchOctaves(ST_HANDLE h, float newPitch)
{
static_cast<SoundTouch*>(h)->setPitchOctaves(newPitch);
}
/// Sets pitch change in semi-tones compared to the original pitch
/// (-12 .. +12)
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitchSemiTones(ST_HANDLE h, float newPitch)
{
static_cast<SoundTouch*>(h)->setPitchSemiTones(newPitch);
}
/// Sets the number of channels, 1 = mono, 2 = stereo
SOUNDTOUCHDLL_API void __cdecl soundtouch_setChannels(ST_HANDLE h, uint numChannels)
{
static_cast<SoundTouch*>(h)->setChannels(numChannels);
}
/// Sets sample rate.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setSampleRate(ST_HANDLE h, uint srate)
{
static_cast<SoundTouch*>(h)->setSampleRate(srate);
}
/// Flushes the last samples from the processing pipeline to the output.
/// Clears also the internal processing buffers.
//
/// Note: This function is meant for extracting the last samples of a sound
/// stream. This function may introduce additional blank samples in the end
/// of the sound stream, and thus it's not recommended to call this function
/// in the middle of a sound stream.
SOUNDTOUCHDLL_API void __cdecl soundtouch_flush(ST_HANDLE h)
{
static_cast<SoundTouch*>(h)->flush();
}
/// Adds 'numSamples' pcs of samples from the 'samples' memory position into
/// the input of the object. Notice that sample rate _has_to_ be set before
/// calling this function, otherwise throws a runtime_error exception.
SOUNDTOUCHDLL_API void __cdecl soundtouch_putSamples(ST_HANDLE h,
const SAMPLETYPE *samples, ///< Pointer to sample buffer.
unsigned int numSamples ///< Number of samples in buffer. Notice
///< that in case of stereo-sound a single sample
///< contains data for both channels.
)
{
static_cast<SoundTouch*>(h)->putSamples(samples, numSamples);
}
/// Clears all the samples in the object's output and internal processing
/// buffers.
SOUNDTOUCHDLL_API void __cdecl soundtouch_clear(ST_HANDLE h)
{
return static_cast<SoundTouch*>(h)->clear();
}
/// Changes a setting controlling the processing system behaviour. See the
/// 'SETTING_...' defines for available setting ID's.
///
/// \return 'nonzero' if the setting was successfully changed
SOUNDTOUCHDLL_API int __cdecl soundtouch_setSetting(ST_HANDLE h,
int settingId, ///< Setting ID number. see SETTING_... defines.
int value ///< New setting value.
)
{
return static_cast<SoundTouch*>(h)->setSetting(settingId, value);
}
/// Reads a setting controlling the processing system behaviour. See the
/// 'SETTING_...' defines for available setting ID's.
///
/// \return the setting value.
SOUNDTOUCHDLL_API int __cdecl soundtouch_getSetting(ST_HANDLE h,
int settingId ///< Setting ID number, see SETTING_... defines.
)
{
return static_cast<SoundTouch*>(h)->getSetting(settingId);
}
/// Returns number of samples currently unprocessed.
SOUNDTOUCHDLL_API uint __cdecl soundtouch_numUnprocessedSamples(ST_HANDLE h)
{
return static_cast<SoundTouch*>(h)->numUnprocessedSamples();
}
/// Receive ready samples from the processing pipeline.
///
/// if called with outBuffer=nullptr, just reduces amount of ready samples within the pipeline.
SOUNDTOUCHDLL_API uint __cdecl soundtouch_receiveSamples(ST_HANDLE h,
SAMPLETYPE *outBuffer, ///< Buffer where to copy output samples.
unsigned int maxSamples ///< How many samples to receive at max.
)
{
if (outBuffer)
{
return static_cast<SoundTouch*>(h)->receiveSamples(outBuffer, maxSamples);
}
else
{
return static_cast<SoundTouch*>(h)->receiveSamples(maxSamples);
}
}
/// Returns number of samples currently available.
SOUNDTOUCHDLL_API uint __cdecl soundtouch_numSamples(ST_HANDLE h)
{
return static_cast<SoundTouch*>(h)->numSamples();
}
/// Returns nonzero if there aren't any samples available for outputting.
SOUNDTOUCHDLL_API int __cdecl soundtouch_isEmpty(ST_HANDLE h)
{
return static_cast<SoundTouch*>(h)->isEmpty();
}

100
lib/soundtouch-master/source/SoundTouchDLL/SoundTouchDLL.rc

@ -0,0 +1,100 @@
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "winres.h"
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (United States) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""afxres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Version
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 2,3,2,0
PRODUCTVERSION 2,3,2,0
FILEFLAGSMASK 0x17L
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
#endif
FILEOS 0x4L
FILETYPE 0x2L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "000004b0"
BEGIN
VALUE "Comments", "SoundTouch Library licensed for 3rd party applications subject to LGPL license v2.1. Visit http://www.surina.net/soundtouch for more information about the SoundTouch library."
VALUE "FileDescription", "SoundTouch Dynamic Link Library"
VALUE "FileVersion", "2.3.3.0"
VALUE "InternalName", "SoundTouch"
VALUE "LegalCopyright", "Copyright (C) Olli Parviainen 2024"
VALUE "OriginalFilename", "SoundTouch.dll"
VALUE "ProductName", " SoundTouch Dynamic Link Library"
VALUE "ProductVersion", "2.3.3.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x0, 1200
END
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

15
lib/soundtouch-master/source/SoundTouchDLL/resource.h

@ -0,0 +1,15 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by SoundTouchDLL.rc
//
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 101
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1000
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif

11
lib/u_async.c

@ -518,6 +518,10 @@ static void get_next_timeout(struct UASYNC* ua, struct timeval* tv) {
uint64_t now_ms = timeval_to_ms(&now_tv);
if (entry.expiration <= now_ms) {
struct timeout_node* rn = (struct timeout_node*)entry.data;
DEBUG_DEBUG(DEBUG_CATEGORY_TIMERS, "get_next_timeout: root timer '%s' already expired (exp=%llu now=%llu delta=%lldms size=%zu)",
rn ? rn->name : "?", (unsigned long long)entry.expiration,
(unsigned long long)now_ms, (long long)(now_ms - entry.expiration), ua->timeout_heap->size);
tv->tv_sec = 0;
tv->tv_usec = 0;
return;
@ -1050,6 +1054,13 @@ void uasync_poll(struct UASYNC* ua, int timeout_tb) {
}
}
/* Если ближайший таймер уже истёк (get_next_timeout вернул 0), но heap не пуст —
* опрашиваем немедленно (timeout_ms=0), иначе epoll_wait(-1) заблокирует навсегда,
* а process_timeouts (вызывается только после epoll_wait) никогда не запустится. */
if (timeout_ms == -1 && ua->timeout_heap->size > 0) {
timeout_ms = 0;
}
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "poll(%d sockets, %zu timers, timeout=%dms)",
socket_count, ua->timeout_heap->size, timeout_ms);

6
src/Makefile.am

@ -92,6 +92,9 @@ utun_CORE_SOURCES = \
chat/invite_link.c \
chat/invite_build.c \
chat/chat_headless_control.c \
dm/dm_core.c \
dm/dm_crypto.c \
dm/dm_mailbox.c \
broadcast.c
# libutun: all core sources except main()
@ -183,6 +186,9 @@ libutun_a_SOURCES = \
chat/invite_link.c \
chat/invite_build.c \
chat/chat_headless_control.c \
dm/dm_core.c \
dm/dm_crypto.c \
dm/dm_mailbox.c \
broadcast.c
libutun_a_CFLAGS = $(utun_CFLAGS)

20
src/chat/chat_admin.c

@ -49,7 +49,8 @@ static void admin_key_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
}
struct UTUN_INSTANCE* inst = conn->instance;
if (!inst) { queue_dgram_free(entry); queue_entry_free(entry); return; }
if (!g_cc.initialized || !g_cc.db) { queue_dgram_free(entry); queue_entry_free(entry); return; }
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db) { queue_dgram_free(entry); queue_entry_free(entry); return; }
const uint8_t* d = entry->dgram;
uint64_t from_node;
@ -64,7 +65,7 @@ static void admin_key_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
/* проверить, что ключи соответствуют публичным ключам канала (защита от подлога) */
uint8_t ch_x_pub[32], ch_ed_pub[32];
if (topo_node_sqlite_channel_get(g_cc.db, ch_id, NULL, 0, NULL, ch_x_pub, ch_ed_pub, NULL) != 0) {
if (topo_node_sqlite_channel_get(cc->db, ch_id, NULL, 0, NULL, ch_x_pub, ch_ed_pub, NULL) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: recv — no channel ch=%s from 0x%016llx, drop",
CA_ID, ch_id, (unsigned long long)from_node);
queue_dgram_free(entry); queue_entry_free(entry); return;
@ -80,7 +81,7 @@ static void admin_key_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
queue_dgram_free(entry); queue_entry_free(entry); return;
}
if (topo_node_sqlite_channel_set_privs(g_cc.db, ch_id, x_priv, ed_priv) != 0) {
if (topo_node_sqlite_channel_set_privs(cc->db, ch_id, x_priv, ed_priv) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: recv — set_privs FAILED ch=%s from 0x%016llx",
CA_ID, ch_id, (unsigned long long)from_node);
queue_dgram_free(entry); queue_entry_free(entry); return;
@ -93,7 +94,7 @@ static void admin_key_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
uint8_t cl = (uint8_t)strlen(ch_id);
evt[0] = cl; memcpy(evt + 1, ch_id, cl);
memcpy(evt + 1 + cl, &from_node, 8);
chat_event_post(CHAT_EVT_ADMIN_KEY_RECEIVED, evt, 1 + cl + 8);
chat_event_post(inst, CHAT_EVT_ADMIN_KEY_RECEIVED, evt, 1 + cl + 8);
queue_dgram_free(entry); queue_entry_free(entry);
}
@ -118,8 +119,9 @@ void chat_admin_destroy(struct UTUN_INSTANCE* inst) {
/* ─── передача прав (вызывается из uasync-потока) ─── */
void chat_core_transfer_admin(const char* ch_id, uint64_t target_node_id) {
if (!g_cc.initialized || !g_cc.inst || !g_cc.db) {
void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: transfer — not initialized", CA_ID);
return;
}
@ -130,7 +132,7 @@ void chat_core_transfer_admin(const char* ch_id, uint64_t target_node_id) {
}
uint8_t x_priv[32], ed_priv[32];
if (topo_node_sqlite_channel_get_privs(g_cc.db, ch_id, x_priv, ed_priv) != 0) {
if (topo_node_sqlite_channel_get_privs(cc->db, ch_id, x_priv, ed_priv) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: transfer — no channel privkey ch=%s (not owner?)", CA_ID, ch_id);
return;
}
@ -154,7 +156,7 @@ void chat_core_transfer_admin(const char* ch_id, uint64_t target_node_id) {
memcpy(entry->dgram + 1, payload, sizeof(payload));
entry->len = 1 + (uint16_t)sizeof(payload);
int rc = etcp_route_send(g_cc.inst, group_id, target_node_id, entry, 1,
int rc = etcp_route_send(inst, group_id, target_node_id, entry, 1,
ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT);
if (rc != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: transfer — etcp_route_send FAILED rc=%d ch=%s → 0x%016llx",
@ -169,6 +171,6 @@ void chat_core_transfer_admin(const char* ch_id, uint64_t target_node_id) {
void chat_core_transfer_admin_trampoline(void* arg) {
struct chat_transfer_admin_req* req = (struct chat_transfer_admin_req*)arg;
if (!req) return;
chat_core_transfer_admin(req->ch_id, req->target_node_id);
chat_core_transfer_admin(req->inst, req->ch_id, req->target_node_id);
u_free(req);
}

145
src/chat/chat_channel.c

@ -56,22 +56,25 @@ static int channel_sign_join(const uint8_t* ch_x25519, const uint8_t* ch_ed25519
/* ─── подготовка инфраструктуры канала (db_sync instance) ─── */
void chat_core_ensure_channel_ready(const char* ch_id) {
if (!g_cc.initialized || !ch_id || !ch_id[0]) return;
if (si_find(ch_id)) return;
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return;
if (si_find(inst, ch_id)) return;
char tbl_msg[80]; msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg));
uint64_t gid = strtoull(ch_id, NULL, 10);
struct DB_SYNC_INSTANCE* si = db_sync_instance_add(g_cc.inst, tbl_msg, gid, 1);
struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, tbl_msg, gid, 1);
if (gid != 0 && g_cc.inst->topo_groups && !topo_groups_find(g_cc.inst->topo_groups, gid)) {
struct TOPO_GROUP* g = topo_groups_create_group(g_cc.inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
if (g) member_sync_subscribe_group(g_cc.inst, g);
if (gid != 0 && inst->topo_groups && !topo_groups_find(inst->topo_groups, gid)) {
struct TOPO_GROUP* g = topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
if (g) member_sync_subscribe_group(inst, g);
}
if (si) {
si_register(si, ch_id);
db_sync_set_insert_cb(si, on_msg_inserted, u_strdup(ch_id));
si_register(inst, si, ch_id);
struct msg_insert_arg* ia = u_malloc(sizeof(*ia));
if (ia) { ia->inst = inst; snprintf(ia->ch_id, sizeof(ia->ch_id), "%s", ch_id); }
db_sync_set_insert_cb(si, on_msg_inserted, ia);
db_sync_instance_set_gated(si, 1);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel ready ch=%s tbl=%s group=0x%016llx",
CC_ID, ch_id, tbl_msg, (unsigned long long)gid);
@ -83,8 +86,9 @@ void chat_core_ensure_channel_ready(const char* ch_id) {
/* ─── создание канала ─── */
void chat_core_create_channel(struct chat_channel_create* req) {
if (!g_cc.initialized) {
void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: create_channel NOT INITIALIZED ch=%s",
CC_ID, req ? req->channel_id : "(null)");
return;
@ -94,7 +98,7 @@ void chat_core_create_channel(struct chat_channel_create* req) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: create_channel BEGIN ch=%s name=%s",
CC_ID, req->channel_id, req->name);
int rc = topo_node_sqlite_channel_put(g_cc.db,
int rc = topo_node_sqlite_channel_put(cc->db,
req->channel_id, req->name, req->owner_node_id,
req->x25519_pubkey, req->x25519_privkey,
req->ed25519_pubkey, req->ed25519_privkey,
@ -107,7 +111,7 @@ void chat_core_create_channel(struct chat_channel_create* req) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel_put OK ch=%s name=%s owner=0x%016llx",
CC_ID, req->channel_id, req->name, (unsigned long long)req->owner_node_id);
chat_core_ensure_channel_ready(req->channel_id);
chat_core_ensure_channel_ready(inst, req->channel_id);
uint8_t ch_id_len = (uint8_t)strlen(req->channel_id);
uint8_t data[65];
@ -115,49 +119,53 @@ void chat_core_create_channel(struct chat_channel_create* req) {
memcpy(data + 1, req->channel_id, ch_id_len);
{
uint64_t myid = g_cc.inst->node_id;
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(g_cc.inst);
uint64_t myid = inst->node_id;
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(inst);
uint8_t join_sig[64];
channel_sign_join(req->x25519_pubkey, req->ed25519_pubkey,
myid, g_cc.inst->my_keys.public_key,
join_ts, g_cc.inst->my_ed25519_privkey, join_sig);
myid, inst->my_keys.public_key,
join_ts, inst->my_ed25519_privkey, join_sig);
char juser3[256]; snprintf(juser3, sizeof(juser3), "{\"name\":\"%s\"}",
g_cc.inst->name[0] ? g_cc.inst->name : "");
inst->name[0] ? inst->name : "");
/* корень дерева приглашений: подписан ключом группы (канальный ed25519) */
uint8_t root_sig[64]; memset(root_sig, 0, 64);
member_sync_sign_pubkey(req->ed25519_privkey, g_cc.inst->my_keys.public_key, root_sig);
int mrc = member_sync_put(g_cc.inst, req->channel_id, myid,
g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey,
member_sync_sign_pubkey(req->ed25519_privkey, inst->my_keys.public_key, root_sig);
int mrc = member_sync_put(inst, req->channel_id, myid,
inst->my_keys.public_key, inst->my_ed25519_pubkey,
join_sig, join_ts, NULL, 0, juser3, NULL, NULL, 0, 0, root_sig);
if (mrc < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d",
CC_ID, req->channel_id, mrc);
} else if (mrc & MS_APPLY_CHANGED) {
member_sync_broadcast_one(g_cc.inst, req->channel_id, myid);
member_sync_broadcast_one(inst, req->channel_id, myid);
}
/* адреса + update_ts + подпись + синк — единой функцией */
chat_core_update_my_member();
chat_core_update_my_member(inst);
}
chat_event_post(CHAT_EVT_CHANNEL_UPDATED, data, 1 + ch_id_len);
chat_event_post(inst, CHAT_EVT_CHANNEL_UPDATED, data, 1 + ch_id_len);
}
}
void chat_core_create_channel_trampoline(void* arg) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat_core: TRAMPOLINE invoked arg=%p", arg);
struct chat_channel_create* req = (struct chat_channel_create*)arg;
chat_core_create_channel(req);
if (!req) return;
chat_core_create_channel(req->inst, req);
u_free(req);
}
void chat_core_create_channel_auto_trampoline(void* arg) {
chat_core_create_channel_auto((const char*)arg);
struct chat_create_auto_arg* a = (struct chat_create_auto_arg*)arg;
if (!a) return;
chat_core_create_channel_auto(a->inst, a->name);
u_free(arg);
}
void chat_core_create_channel_auto(const char* name) {
if (!g_cc.initialized || !name) {
void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !name) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: create_channel_auto — not initialized or NULL name", CC_ID);
return;
}
@ -189,7 +197,7 @@ void chat_core_create_channel_auto(const char* name) {
uint8_t* p = msg;
memcpy(p, channelId, chIdLen); p += chIdLen; *p++ = '\0';
memcpy(p, name, nameLen); p += nameLen; *p++ = '\0';
memcpy(p, &g_cc.my_node_id, 8); p += 8;
memcpy(p, &cc->my_node_id, 8); p += 8;
memcpy(p, x25519_pub, 32); p += 32;
memcpy(p, ed25519_pub, 32);
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
@ -201,31 +209,33 @@ void chat_core_create_channel_auto(const char* name) {
struct chat_channel_create* req = u_calloc(1, sizeof(*req));
if (!req) return;
req->inst = inst;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", channelId);
snprintf(req->name, sizeof(req->name), "%s", name);
req->owner_node_id = g_cc.my_node_id;
req->owner_node_id = cc->my_node_id;
memcpy(req->x25519_pubkey, x25519_pub, 32); memcpy(req->x25519_privkey, x25519_priv, 32);
memcpy(req->ed25519_pubkey, ed25519_pub, 32); memcpy(req->ed25519_privkey, ed25519_priv, 32);
memcpy(req->signature, sig, 64);
chat_core_create_channel(req);
chat_core_create_channel(inst, req);
u_free(req);
}
/* ─── управление подключением к каналу (выбор в GUI) ─── */
void chat_core_connect_channel(const char* ch_id) {
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel called ch_id=%s initialized=%d", CC_ID, ch_id ? ch_id : "(null)", g_cc.initialized);
if (!g_cc.initialized || !ch_id || !ch_id[0]) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — skip (not ready)", CC_ID); return; }
if (!g_cc.inst->topo_groups) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — no topo_groups", CC_ID); return; }
void chat_core_connect_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel called ch_id=%s initialized=%d", CC_ID, ch_id ? ch_id : "(null)", cc ? cc->initialized : 0);
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — skip (not ready)", CC_ID); return; }
if (!inst->topo_groups) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — no topo_groups", CC_ID); return; }
uint64_t gid = strtoull(ch_id, NULL, 10);
if (gid == 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — invalid ch_id=%s", CC_ID, ch_id); return; }
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(inst, ch_id);
struct TOPO_GROUP* group = topo_groups_find(g_cc.inst->topo_groups, gid);
if (!group) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — group not found ch=%s gid=%016llx total_groups=%d", CC_ID, ch_id, (unsigned long long)gid, queue_entry_count(g_cc.inst->topo_groups->group_list)); return; }
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
if (!group) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — group not found ch=%s gid=%016llx total_groups=%d", CC_ID, ch_id, (unsigned long long)gid, queue_entry_count(inst->topo_groups->group_list)); return; }
if (group->connect) {
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel — already in progress ch=%s active=%d",
@ -233,7 +243,7 @@ void chat_core_connect_channel(const char* ch_id) {
return;
}
if (!chat_setting_get_int("group_autoconnect", 1)) {
if (!chat_setting_get_int(inst, "group_autoconnect", 1)) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel ch=%s — group_autoconnect disabled, skip auto-connect loop",
CC_ID, ch_id);
return;
@ -244,29 +254,30 @@ void chat_core_connect_channel(const char* ch_id) {
}
void chat_core_connect_channel_trampoline(void* arg) {
char* ch_id = (char*)arg;
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel_trampoline ch_id=%s", CC_ID, ch_id ? ch_id : "(null)");
if (!ch_id) return;
chat_core_connect_channel(ch_id);
u_free(ch_id);
struct chat_create_auto_arg* a = (struct chat_create_auto_arg*)arg;
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel_trampoline ch_id=%s", CC_ID, a ? a->name : "(null)");
if (!a) return;
chat_core_connect_channel(a->inst, a->name);
u_free(a);
}
/* ─── однократное подключение к конкретному узлу канала (GUI → uasync) ─── */
void chat_core_connect_node(const char* ch_id, uint64_t node_id) {
if (!g_cc.initialized || !g_cc.inst || !ch_id || !ch_id[0] || !node_id) {
void chat_core_connect_node(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !inst || !ch_id || !ch_id[0] || !node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_node — skip (not ready or invalid args)", CC_ID);
return;
}
if (node_id == g_cc.inst->node_id) {
if (node_id == inst->node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_node — skip self node=0x%016llx", CC_ID, (unsigned long long)node_id);
return;
}
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(inst, ch_id);
uint64_t gid = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* group = topo_groups_find(g_cc.inst->topo_groups, gid);
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
if (!group) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_node — group not found ch=%s", CC_ID, ch_id);
return;
@ -278,15 +289,14 @@ void chat_core_connect_node(const char* ch_id, uint64_t node_id) {
void chat_core_connect_node_trampoline(void* arg) {
struct chat_connect_node_req* r = (struct chat_connect_node_req*)arg;
if (!r) return;
chat_core_connect_node(r->ch_id, r->node_id);
chat_core_connect_node(r->inst, r->ch_id, r->node_id);
u_free(r);
}
/* ─── применение настройки group_autoconnect ко всем CHAT-группам ─── */
void chat_core_apply_group_autoconnect(void) {
int enabled = chat_setting_get_int("group_autoconnect", 1);
struct UTUN_INSTANCE* inst = g_cc.inst;
void chat_core_apply_group_autoconnect(struct UTUN_INSTANCE* inst) {
int enabled = chat_setting_get_int(inst, "group_autoconnect", 1);
if (!inst || !inst->topo_groups || !inst->topo_groups->group_list) return;
struct ll_entry* e = inst->topo_groups->group_list->head;
@ -306,11 +316,11 @@ void chat_core_apply_group_autoconnect(void) {
/* ─── локальное удаление канала ─── */
static const char* cc_ui_state_get(const char* key) {
static const char* cc_ui_state_get(struct chat_core_ctx* cc, const char* key) {
static char val[512];
val[0] = '\0';
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* v = (const char*)sqlite3_column_text(st, 0);
@ -321,8 +331,9 @@ static const char* cc_ui_state_get(const char* key) {
return val;
}
void chat_core_delete_channel(const char* ch_id) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) {
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel — not initialized", CC_ID);
return;
}
@ -332,35 +343,35 @@ void chat_core_delete_channel(const char* ch_id) {
uint64_t gid = strtoull(ch_id, NULL, 10);
/* 1. отмена медиа-загрузок/релэев/суперузлов канала (держат conn_mgr handle группы) */
media_delivery_cancel_channel(g_cc.inst, gid);
media_delivery_cancel_channel(inst, gid);
/* 2. снять merkle-сессии этого namespace */
member_sync_cancel_channel(g_cc.inst, ch_id);
member_sync_cancel_channel(inst, ch_id);
/* 3. удалить группу (conn_mgr → закрытие соединений, connect-цикл, broadcast, recovery) */
if (g_cc.inst->topo_groups)
topo_groups_remove_group(g_cc.inst->topo_groups, gid);
if (inst->topo_groups)
topo_groups_remove_group(inst->topo_groups, gid);
/* 4. удалить db_sync инстанс (освобождает память, отменяет таймеры) */
{
struct DB_SYNC_INSTANCE* si = si_find(ch_id);
struct DB_SYNC_INSTANCE* si = si_find(inst, ch_id);
if (si) db_sync_instance_remove(si);
}
/* 5. немедленно убрать канал из кеша chat_sync */
chat_sync_on_channel_deleted(g_cc.inst, ch_id);
chat_sync_on_channel_deleted(inst, ch_id);
/* 6. удалить медиафайлы канала на диске + строки media_files */
media_index_delete_channel(g_cc.db, ch_id, cc_ui_state_get("media_base"));
media_index_delete_channel(cc->db, ch_id, cc_ui_state_get(cc, "media_base"));
/* 7. удалить таблицы и метаданные канала */
topo_node_sqlite_channel_delete(g_cc.db, ch_id);
topo_node_sqlite_channel_delete(cc->db, ch_id);
/* 8. уведомить GUI */
{
uint8_t evt[65]; uint8_t cl = (uint8_t)strlen(ch_id);
evt[0] = cl; memcpy(evt + 1, ch_id, cl);
chat_event_post(CHAT_EVT_CHANNEL_DELETED, evt, 1 + cl);
chat_event_post(inst, CHAT_EVT_CHANNEL_DELETED, evt, 1 + cl);
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel DONE ch=%s gid=0x%016llx", CC_ID, ch_id, (unsigned long long)gid);
@ -369,6 +380,6 @@ void chat_core_delete_channel(const char* ch_id) {
void chat_core_delete_channel_trampoline(void* arg) {
struct chat_delete_channel_req* req = (struct chat_delete_channel_req*)arg;
if (!req) return;
chat_core_delete_channel(req->ch_id);
chat_core_delete_channel(req->inst, req->ch_id);
u_free(req);
}

183
src/chat/chat_core.c

@ -1,5 +1,5 @@
/*
* chat_core.c — жизненный цикл чата: init, destroy, глобальное состояние g_cc
* chat_core.c — жизненный цикл чата: init, destroy, per-instance состояние (inst->chat_core)
*
* Под-модули (каждый в своём .c):
* chat_msg.c — отправка сообщений, DB-операции для chat_sync
@ -28,10 +28,6 @@
#include <stdio.h>
#include "../../lib/platform_compat.h"
/* ─── глобальное состояние (extern в chat_core_priv.h) ─── */
struct chat_core_ctx g_cc;
/* ─── хелперы ─── */
void sanitize_ch_id(const char* ch_id, char* out, size_t out_sz) {
@ -56,9 +52,9 @@ void peers_table_name(const char* ch_id, char* buf, size_t sz) {
snprintf(buf, sz, "peers_%s", san);
}
int db_exec(const char* sql) {
static int db_exec(struct chat_core_ctx* cc, const char* sql) {
char* err = NULL;
int rc = sqlite3_exec(g_cc.db, sql, NULL, NULL, &err);
int rc = sqlite3_exec(cc->db, sql, NULL, NULL, &err);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: sql error: %s", CC_ID, err);
sqlite3_free(err);
@ -68,12 +64,12 @@ int db_exec(const char* sql) {
/* ─── db_sync registry ─── */
struct DB_SYNC_INSTANCE* si_find(const char* ch_id) {
if (!g_cc.inst || !g_cc.inst->db_sync || !ch_id || !ch_id[0]) return NULL;
return db_sync_instance_find(g_cc.inst, strtoull(ch_id, NULL, 10));
struct DB_SYNC_INSTANCE* si_find(struct UTUN_INSTANCE* inst, const char* ch_id) {
if (!inst || !inst->db_sync || !ch_id || !ch_id[0]) return NULL;
return db_sync_instance_find(inst, strtoull(ch_id, NULL, 10));
}
void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id) {
void si_register(struct UTUN_INSTANCE* inst, struct DB_SYNC_INSTANCE* si, const char* ch_id) {
uint32_t msg_cnt = db_sync_count(si);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: si_register ch=%s mc=%u", CC_ID, ch_id, msg_cnt);
}
@ -82,40 +78,47 @@ void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id) {
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
if (!inst || !db_path) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid args inst=%p db_path=%p", CC_ID, (void*)inst, (void*)db_path); return -1; }
memset(&g_cc, 0, sizeof(g_cc));
snprintf(g_cc.db_path, sizeof(g_cc.db_path), "%s", db_path);
struct chat_core_ctx* cc = u_calloc(1, sizeof(*cc));
if (!cc) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: OOM for chat_core_ctx", CC_ID); return -1; }
inst->chat_core = cc;
snprintf(cc->db_path, sizeof(cc->db_path), "%s", db_path);
if (inst->topo_sqlite_db) {
g_cc.db = inst->topo_sqlite_db; g_cc.shared_db = 1;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: using shared SQLite db=%p", CC_ID, (void*)g_cc.db);
cc->db = inst->topo_sqlite_db; cc->shared_db = 1;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: using shared SQLite db=%p", CC_ID, (void*)cc->db);
} else {
int rc = sqlite3_open_v2(db_path, &g_cc.db,
int rc = sqlite3_open_v2(db_path, &cc->db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: cannot open DB %s: %s",
CC_ID, db_path, sqlite3_errmsg(g_cc.db));
sqlite3_close(g_cc.db); g_cc.db = NULL; return -1;
CC_ID, db_path, sqlite3_errmsg(cc->db));
sqlite3_close(cc->db); inst->chat_core = NULL; u_free(cc); return -1;
}
sqlite3_exec(g_cc.db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL);
sqlite3_exec(g_cc.db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL);
sqlite3_exec(cc->db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL);
sqlite3_exec(cc->db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL);
}
g_cc.inst = inst;
g_cc.my_node_id = inst->node_id;
cc->inst = inst;
cc->my_node_id = inst->node_id;
/* перенести chat-настройки из конфига (заполнены парсером) в per-instance состояние */
if (inst->config) memcpy(&inst->chat_settings, &inst->config->global.chat_settings, sizeof(inst->chat_settings));
sqlite3_int64 now_sec = (sqlite3_int64)ntp_time_get_seconds(inst);
/* записать себя в nodes + local_identity */
{
const char* my_name = inst->name[0] ? inst->name : "Me";
topo_node_sqlite_node_update_verified(g_cc.db, g_cc.my_node_id,
topo_node_sqlite_node_update_verified(cc->db, cc->my_node_id,
my_name, inst->my_keys.public_key, inst->my_ed25519_pubkey,
(uint64_t)now_sec, (time_t)now_sec);
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(g_cc.db,
sqlite3_prepare_v2(cc->db,
"INSERT OR REPLACE INTO local_identity(id,node_id,name,x25519_pubkey,ed25519_pubkey,created_at,updated_at)"
" VALUES(1,?,?,?,?,?,?)", -1, &st, NULL);
if (st) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)g_cc.my_node_id);
sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id);
sqlite3_bind_text(st, 2, my_name, -1, SQLITE_STATIC);
sqlite3_bind_blob(st, 3, inst->my_keys.public_key, 32, SQLITE_STATIC);
sqlite3_bind_blob(st, 4, inst->my_ed25519_pubkey, 32, SQLITE_STATIC);
@ -125,15 +128,15 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
}
}
g_cc.initialized = 1;
cc->initialized = 1;
/* register adm_tags change callback → CHAT_EVT_MEMBER_UPDATED */
chat_member_init();
chat_member_init(inst);
/* create media_files table */
media_index_init(g_cc.db);
media_index_init(cc->db);
db_exec(
db_exec(cc,
"CREATE TABLE IF NOT EXISTS local_identity ("
" id INTEGER PRIMARY KEY CHECK (id = 1),"
" node_id INTEGER NOT NULL UNIQUE,"
@ -167,14 +170,14 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
else snprintf(mbase, sizeof(mbase), "%s", db_path);
char sql[1024]; snprintf(sql, sizeof(sql),
"INSERT OR REPLACE INTO ui_state(key,value) VALUES('media_base','%s')", mbase);
sqlite3_exec(g_cc.db, sql, NULL, NULL, NULL);
sqlite3_exec(cc->db, sql, NULL, NULL, NULL);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: media_base=%s", CC_ID, mbase);
}
/* seed stub accounts if empty */
{
sqlite3_stmt* cnt_st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT COUNT(*) FROM accounts",
if (sqlite3_prepare_v2(cc->db, "SELECT COUNT(*) FROM accounts",
-1, &cnt_st, NULL) == SQLITE_OK) {
if (sqlite3_step(cnt_st) == SQLITE_ROW && sqlite3_column_int(cnt_st, 0) == 0) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: seeding 5 stub accounts", CC_ID);
@ -185,7 +188,7 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
};
for (int i = 0; i < 5; i++) {
sqlite3_stmt* ins = NULL;
sqlite3_prepare_v2(g_cc.db,
sqlite3_prepare_v2(cc->db,
"INSERT OR IGNORE INTO accounts(node_id,display_name,avatar_color,avatar_letter,created_at)"
" VALUES(?,?,?,?,?)", -1, &ins, NULL);
if (ins) {
@ -205,12 +208,12 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
/* ensure infrastructure for all existing channels (survives restart) */
{
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels ORDER BY created_at ASC",
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels ORDER BY created_at ASC",
-1, &stmt, NULL) == SQLITE_OK) {
int loaded = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* ch = (const char*)sqlite3_column_text(stmt, 0);
if (ch && ch[0]) { chat_core_ensure_channel_ready(ch); loaded++; }
if (ch && ch[0]) { chat_core_ensure_channel_ready(inst, ch); loaded++; }
}
sqlite3_finalize(stmt);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: loaded %d channels from DB", CC_ID, loaded);
@ -218,53 +221,46 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: initialized, db=%s node_id=0x%016llx",
CC_ID, db_path, (unsigned long long)g_cc.my_node_id);
CC_ID, db_path, (unsigned long long)cc->my_node_id);
{ uint8_t nid[8]; memcpy(nid, &g_cc.my_node_id, 8); chat_event_post(CHAT_EVT_MY_NODE_ID, nid, 8); }
{ uint8_t nid[8]; memcpy(nid, &cc->my_node_id, 8); chat_event_post(inst, CHAT_EVT_MY_NODE_ID, nid, 8); }
chat_event_post(CHAT_EVT_DB_READY, NULL, 0);
chat_event_post(inst, CHAT_EVT_DB_READY, NULL, 0);
return 0;
}
void chat_core_destroy(struct UTUN_INSTANCE* inst) {
(void)inst;
if (!g_cc.initialized) return;
g_cc.initialized = 0;
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) return;
cc->initialized = 0;
if (g_cc.db && !g_cc.shared_db) { sqlite3_close(g_cc.db); }
g_cc.db = NULL; g_cc.inst = NULL;
if (cc->db && !cc->shared_db) { sqlite3_close(cc->db); }
inst->chat_core = NULL;
u_free(cc);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CC_ID);
}
sqlite3* chat_core_get_db(void) { return g_cc.db; }
struct UTUN_INSTANCE* chat_core_get_inst(void) { return g_cc.inst; }
int chat_core_is_initialized(void) { return g_cc.initialized; }
sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst) { return CC(inst) ? CC(inst)->db : NULL; }
int chat_core_is_initialized(struct UTUN_INSTANCE* inst) { return CC(inst) ? CC(inst)->initialized : 0; }
/* ─── chat settings (GUI → uasync) ─── */
void chat_core_set_setting(const char* name, const char* value) {
chat_setting_set(name, value);
if (name && strcmp(name, "group_autoconnect") == 0) chat_core_apply_group_autoconnect();
void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value) {
chat_setting_set(inst, name, value);
if (name && strcmp(name, "group_autoconnect") == 0) chat_core_apply_group_autoconnect(inst);
}
struct chat_setting_arg { char data[256]; };
void chat_core_set_setting_trampoline(void* arg) {
struct chat_setting_arg* a = (struct chat_setting_arg*)arg;
const char* name = a->data;
const char* value = name + strlen(name) + 1;
chat_core_set_setting(name, value);
chat_core_set_setting(a->inst, name, value);
u_free(arg);
}
/* ─── headless API ─── */
void chat_core_set_ctx_for_headless(sqlite3* db, struct UTUN_INSTANCE* inst, uint64_t my_node_id) {
if (!g_cc.initialized) g_cc.initialized = 1;
g_cc.db = db; g_cc.inst = inst; g_cc.my_node_id = my_node_id;
g_cc.shared_db = 1;
}
static void json_escape(const char* src, char* dst, size_t dst_sz) {
size_t i = 0; const char* s = src;
while (*s && i < dst_sz - 2) {
@ -274,12 +270,13 @@ static void json_escape(const char* src, char* dst, size_t dst_sz) {
dst[i] = '\0';
}
int chat_core_get_channels_json(char* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !buf || !out_len) return -1;
char* w = buf; char* end = buf + buf_size;
*w++ = '['; const char* sep = "";
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db,
if (sqlite3_prepare_v2(cc->db,
"SELECT c.channel_id, c.name, c.owner_node_id, "
"(SELECT COUNT(*) FROM nodes WHERE node_id=c.owner_node_id AND name IS NOT NULL) AS has_owner_name, "
"(SELECT name FROM nodes WHERE node_id=c.owner_node_id) AS owner_name "
@ -292,16 +289,16 @@ int chat_core_get_channels_json(char* buf, size_t buf_size, size_t* out_len) {
const char* owner_name = (const char*)sqlite3_column_text(st, 4);
if (!ch_id) continue;
uint32_t msg_count = chat_core_count(ch_id);
uint32_t msg_count = chat_core_count(inst, ch_id);
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
int peer_count = 0, online_count = 0;
sqlite3_stmt* ps = NULL;
char psql[200]; snprintf(psql, sizeof(psql), "SELECT node_id FROM \"%s\" WHERE deleted=0", peers_tbl);
if (sqlite3_prepare_v2(g_cc.db, psql, -1, &ps, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, psql, -1, &ps, NULL) == SQLITE_OK) {
while (sqlite3_step(ps) == SQLITE_ROW) {
uint64_t pid = (uint64_t)sqlite3_column_int64(ps, 0);
peer_count++;
if (chat_core_member_online(ch_id, pid)) online_count++;
if (chat_core_member_online(inst, ch_id, pid)) online_count++;
}
sqlite3_finalize(ps);
}
@ -323,9 +320,10 @@ int chat_core_get_channels_json(char* buf, size_t buf_size, size_t* out_len) {
return 0;
}
int chat_core_get_messages_json(const char* ch_id, int count, int offset,
int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !ch_id || !buf || !out_len) return -1;
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !buf || !out_len) return -1;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char* w = buf; char* end = buf + buf_size;
*w++ = '['; const char* sep = "";
@ -335,7 +333,7 @@ int chat_core_get_messages_json(const char* ch_id, int count, int offset,
"FROM \"%s\" m LEFT JOIN nodes n ON m.node_id=n.node_id "
"ORDER BY m.timestamp DESC LIMIT ? OFFSET ?", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
sqlite3_bind_int(st, 1, count > 0 ? count : 100);
sqlite3_bind_int(st, 2, offset);
while (sqlite3_step(st) == SQLITE_ROW) {
@ -367,8 +365,9 @@ int chat_core_get_messages_json(const char* ch_id, int count, int offset,
return 0;
}
int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !ch_id || !buf || !out_len) return -1;
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !buf || !out_len) return -1;
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
char* w = buf; char* end = buf + buf_size;
*w++ = '['; const char* sep = "";
@ -377,11 +376,11 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si
"SELECT p.node_id, n.name, n.x25519_pubkey, n.ed25519_pubkey, p.adm_tags "
"FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { *out_len = 0; return -1; }
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
const char* name = (const char*)sqlite3_column_text(st, 1);
int online = chat_core_member_online(ch_id, nid);
int online = chat_core_member_online(inst, ch_id, nid);
const uint8_t* x25519 = (const uint8_t*)sqlite3_column_blob(st, 2);
const uint8_t* ed25519 = sqlite3_column_blob(st, 3);
int x25519_len = sqlite3_column_bytes(st, 2);
@ -395,8 +394,8 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si
/* check connected */
int connected = 0;
if (g_cc.inst && g_cc.inst->connections) {
struct ll_entry* e = queue_find_data_by_index(g_cc.inst->connections, (const uint8_t*)&nid);
if (inst && inst->connections) {
struct ll_entry* e = queue_find_data_by_index(inst->connections, (const uint8_t*)&nid);
if (e) connected = 1;
}
@ -412,7 +411,7 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si
sqlite3_stmt* as = NULL;
char asql[200]; snprintf(asql, sizeof(asql),
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=? ORDER BY family, protocol");
if (sqlite3_prepare_v2(g_cc.db, asql, -1, &as, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, asql, -1, &as, NULL) == SQLITE_OK) {
sqlite3_bind_int64(as, 1, (sqlite3_int64)nid);
const char* asep2 = "";
while (sqlite3_step(as) == SQLITE_ROW) {
@ -446,11 +445,12 @@ int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, si
return 0;
}
int chat_core_get_node_name(uint64_t node_id, char* out, size_t sz) {
if (!g_cc.initialized || !out) return -1;
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !out) return -1;
out[0] = '\0';
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(cc->db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* n = (const char*)sqlite3_column_text(st, 0);
@ -497,8 +497,8 @@ static int tags_is_append_key(const char* key) {
static void tags_set_effective_val(struct chat_member_tags_key* k, const char* raw) {
uint64_t ts = 0;
const char* eff = tags_append_effective(raw, &ts);
if (ts == 0) ts = (uint64_t)ntp_time_get_seconds(g_cc.inst);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s:%llu", eff, (unsigned long long)ts);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", eff);
(void)ts;
}
static int tags_json_cb(const char* key, const char* json_val, void* arg) {
@ -508,13 +508,13 @@ static int tags_json_cb(const char* key, const char* json_val, void* arg) {
snprintf(k->key, CHAT_MEMBER_TAGS_KEY_SZ, "%s", key);
snprintf(k->val, CHAT_MEMBER_TAGS_VAL_SZ, "%s", json_val);
k->raw = tags_is_append_key(key) ? u_strdup(json_val) : NULL;
if (k->raw) tags_set_effective_val(k, json_val);
t->key_count++;
return 0;
}
struct chat_member_tags* chat_member_tags_load(const char* ch_id, uint64_t node_id) {
if (!g_cc.initialized || !ch_id || !g_cc.db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — not initialized", CC_ID); return NULL; }
struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !cc->db) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — not initialized", CC_ID); return NULL; }
struct chat_member_tags* t = u_calloc(1, sizeof(*t));
if (!t) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_load — OOM", CC_ID); return NULL; }
snprintf(t->ch_id, sizeof(t->ch_id), "%s", ch_id);
@ -523,7 +523,7 @@ struct chat_member_tags* chat_member_tags_load(const char* ch_id, uint64_t node_
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
sqlite3_stmt* st = NULL;
char sql[256]; snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", peers_tbl);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(st, 0);
@ -576,8 +576,9 @@ int chat_member_tags_replace(struct chat_member_tags* t, const char* json) {
return 0;
}
int chat_member_tags_commit(struct chat_member_tags* t) {
if (!t || !g_cc.initialized || !g_cc.inst || !g_cc.db) {
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t) {
struct chat_core_ctx* cc = CC(inst);
if (!t || !cc || !cc->initialized || !cc->inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — invalid state", CC_ID); return -1;
}
char json[4096]; int off = 0, ver = 1;
@ -597,7 +598,7 @@ int chat_member_tags_commit(struct chat_member_tags* t) {
int old_yes = (*old_eff == 'y');
int new_yes = (k->val[0] == 'y'); /* val = "yes" or "yes:<ts>" */
if (new_yes != old_yes) {
uint64_t ts = (uint64_t)ntp_time_get_seconds(g_cc.inst);
uint64_t ts = (uint64_t)ntp_time_get_seconds(cc->inst);
size_t oldl = strlen(k->raw);
char* nraw = u_malloc(oldl + 24);
if (!nraw) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — OOM for raw", CC_ID); return -1; }
@ -616,7 +617,7 @@ int chat_member_tags_commit(struct chat_member_tags* t) {
#undef TAGS_ADVANCE
uint8_t ch_ed_priv[32];
if (topo_node_sqlite_channel_get_priv(g_cc.db, t->ch_id, ch_ed_priv) != 0) {
if (topo_node_sqlite_channel_get_priv(cc->db, t->ch_id, ch_ed_priv) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — no channel privkey for ch=%s (not owner?)", CC_ID, t->ch_id); return -1;
}
uint8_t sign_msg[8192]; size_t mlen = (size_t)off;
@ -633,7 +634,7 @@ int chat_member_tags_commit(struct chat_member_tags* t) {
char sql[384]; snprintf(sql, sizeof(sql),
"SELECT x25519_pubkey, ed25519_pubkey, join_sig, join_ts, update_sig, update_ts, userinfo, storage"
" FROM \"%s\" WHERE node_id=?", peers_tbl);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member query failed", CC_ID); return -1;
}
sqlite3_bind_int64(st, 1, (sqlite3_int64)t->node_id);
@ -654,11 +655,11 @@ int chat_member_tags_commit(struct chat_member_tags* t) {
const char* stg_val = chat_member_tags_get(t, "storage");
if (stg_val && strcmp(stg_val, "yes") == 0) stg = 1;
int rc = member_sync_put(g_cc.inst, t->ch_id, t->node_id, x25, ed, jsig, jts, usig, uts,
int rc = member_sync_put(inst, t->ch_id, t->node_id, x25, ed, jsig, jts, usig, uts,
uinfo ? uinfo : "", json, sig, stg, 0, NULL);
sqlite3_finalize(st);
if (rc < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit — member_sync_put failed rc=%d", CC_ID, rc); return -1; }
if (rc & MS_APPLY_CHANGED) member_sync_broadcast_one(g_cc.inst, t->ch_id, t->node_id);
if (rc & MS_APPLY_CHANGED) member_sync_broadcast_one(inst, t->ch_id, t->node_id);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: tags_commit ch=%s nid=0x%016llx ver=%d keys=%d json=%s",
CC_ID, t->ch_id, (unsigned long long)t->node_id, ver, t->key_count, json);
@ -675,11 +676,11 @@ void chat_member_tags_free(struct chat_member_tags* t) {
void chat_member_tags_commit_trampoline(void* arg) {
struct chat_member_tags_req* req = (struct chat_member_tags_req*)arg;
if (!req || !g_cc.initialized) { u_free(arg); return; }
struct chat_member_tags* t = chat_member_tags_load(req->ch_id, req->node_id);
if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; }
struct chat_member_tags* t = chat_member_tags_load(req->inst, req->ch_id, req->node_id);
if (!t) { u_free(arg); return; }
if (req->json[0]) chat_member_tags_replace(t, req->json);
chat_member_tags_commit(t);
chat_member_tags_commit(req->inst, t);
chat_member_tags_free(t);
u_free(arg);
}

122
src/chat/chat_core.h

@ -2,7 +2,9 @@
* chat_core.h — центральный API чата в потоке uasync
*
* Все DB-операции и сетевые функции выполняются в uasync-потоке.
* GUI вызывает chat_core_submit_message() через gui_bridge_post_uasync().
* Все функции per-instance: первым аргументом struct UTUN_INSTANCE*.
* GUI вызывает через gui_bridge_post_uasync() trampoline-функции,
* чей arg-структура несёт inst.
*/
#ifndef CHAT_CORE_H
@ -22,24 +24,24 @@ void chat_core_destroy(struct UTUN_INSTANCE* inst);
/* Backfill: регистрирует уже скачанные медиафайлы (лежат на диске, но отсутствуют
* в media_files) — вызывается после загрузки каналов/сообщений при старте. */
void chat_media_backfill(void);
void chat_media_backfill(struct UTUN_INSTANCE* inst);
/* Докачка недостающего медиа за последние storage_backfill_days дней. Проходит по
* медиасообщениям каналов и запускает загрузку для тех, чей файл отсутствует на диске. */
void chat_media_autodownload_backfill(void);
void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst);
/* Регистрирует триггер: после первого SYNC_DONE (+ дебаунс 2с) анонсирует локальные
* блоки суперузлам и запускает chat_media_autodownload_backfill. Один раз за процесс. */
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst);
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst);
struct sqlite3* chat_core_get_db(void);
struct UTUN_INSTANCE* chat_core_get_inst(void);
int chat_core_is_initialized(void);
struct sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst);
int chat_core_is_initialized(struct UTUN_INSTANCE* inst);
/* ── Отправка сообщения (GUI → uasync) ── */
struct chat_msg_submit {
struct UTUN_INSTANCE* inst;
char channel_id[64];
char content_type[32];
char media_src[1024];
@ -56,22 +58,23 @@ struct chat_msg_submit {
uint64_t reply_to_node_id; /* node_id автора исходного сообщения */
};
void chat_core_submit_message(struct chat_msg_submit* req);
void chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
/* Отправка медиа-сообщения (вложение): src->dest (copy) или видео (transcode). */
void chat_core_submit_media_message(struct chat_msg_submit* req);
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
/* ── DB-операции (оставлены для интроспекции) ── */
uint32_t chat_core_count(const char* ch_id);
int chat_core_chain_hash_at(const char* ch_id, uint32_t pos, uint8_t* hash_out);
int chat_core_list_channels(uint8_t* buf, size_t buf_size, size_t* out_len);
int chat_core_list_peers(const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len);
int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len);
uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id);
int chat_core_chain_hash_at(struct UTUN_INSTANCE* inst, const char* ch_id, uint32_t pos, uint8_t* hash_out);
int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len);
int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len);
int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len);
/* ── Создание канала (GUI → uasync) ── */
struct chat_channel_create {
struct UTUN_INSTANCE* inst;
char channel_id[64];
char name[128];
uint64_t owner_node_id;
@ -82,50 +85,60 @@ struct chat_channel_create {
uint8_t signature[64];
};
void chat_core_ensure_channel_ready(const char* ch_id);
void chat_core_create_channel(struct chat_channel_create* req);
struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; };
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req);
void chat_core_create_channel_trampoline(void* arg);
void chat_core_create_channel_auto(const char* name);
void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name);
void chat_core_create_channel_auto_trampoline(void* arg);
/* ── Локальное удаление канала (GUI → uasync) ── */
struct chat_delete_channel_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
};
/* Удалить канал с этого устройства: отмена медиа-загрузок, удаление группы
* (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют. */
void chat_core_delete_channel(const char* ch_id);
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_delete_channel_trampoline(void* arg);
/* ── Утилиты ── */
void chat_core_set_my_node_id(uint64_t node_id);
void chat_core_update_my_name(const char* name);
void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id);
void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name);
struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; };
void chat_core_update_my_name_trampoline(void* arg);
void chat_core_sync_my_addresses(void);
void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst);
/* Единое обновление своего мембера: адреса + bump update_ts + подпись (с адресами) + синк */
void chat_core_update_my_member(void);
void chat_core_update_my_member(struct UTUN_INSTANCE* inst);
/* Сохранение key-value в ui_state (вызывается из uasync-потока) */
void chat_core_save_ui_state(const char* key, const char* value);
void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value);
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[256]; };
void chat_core_save_ui_state_trampoline(void* arg);
/* Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) */
void chat_core_set_setting(const char* name, const char* value);
void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value);
struct chat_setting_arg { struct UTUN_INSTANCE* inst; char data[256]; };
void chat_core_set_setting_trampoline(void* arg);
/* Запуск подключения к узлам выбранного канала (GUI → uasync) */
void chat_core_connect_channel(const char* ch_id);
void chat_core_connect_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_connect_channel_trampoline(void* arg);
/* Однократная попытка подключения к конкретному узлу канала (GUI → uasync) */
void chat_core_connect_node(const char* ch_id, uint64_t node_id);
void chat_core_connect_node(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
void chat_core_connect_node_trampoline(void* arg);
struct chat_connect_node_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t node_id;
};
@ -135,58 +148,59 @@ void chat_core_submit_trampoline(void* arg);
/* Трамплин для ручного запуска загрузки медиа (JNI → uasync) */
struct attachment_dl_req {
struct UTUN_INSTANCE* inst;
char channel_id[64];
int64_t msg_id;
};
void chat_core_attachment_download_trampoline(void* arg);
/* Обновление local_attrs для записи (вызывается из uasync-потока) */
int chat_core_update_local_attrs(const char* ch_id, uint64_t ts,
int chat_core_update_local_attrs(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts,
const uint8_t* author_sig,
const char* local_attrs_json);
/* Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs) */
void chat_core_mark_voice_played(const char* channel_id, uint64_t ts,
void chat_core_mark_voice_played(struct UTUN_INSTANCE* inst, const char* channel_id, uint64_t ts,
uint64_t author_node_id);
void chat_core_mark_voice_played_trampoline(void* arg);
struct voice_played_req {
struct UTUN_INSTANCE* inst;
char channel_id[64];
uint64_t ts;
uint64_t author_node_id;
};
/* Сбор статуса (NTP + connections) и отправка в GUI */
/* Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst) */
void chat_core_collect_status_trampoline(void* arg);
/* Сбор списка ETCP-соединений и отправка в GUI (binary packet) */
/* Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst */
void chat_core_collect_conn_list_trampoline(void* arg);
/* Сбор полных метрик одного соединения (arg = u_malloc uint64_t peer_node_id) */
struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; };
/* Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg) */
void chat_core_collect_conn_metrics_trampoline(void* arg);
/* Сбор live-снапшота для панели деталей участника (arg = u_malloc uint64_t node_id) */
/* Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg) */
void chat_core_collect_member_detail_trampoline(void* arg);
/* ── Headless CLI API ── */
/** Вариант для сборки без UTUN_INSTANCE (использует sqlite3* и node_id) */
void chat_core_set_ctx_for_headless(sqlite3* db, struct UTUN_INSTANCE* inst, uint64_t my_node_id);
/** Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] */
int chat_core_get_channels_json(char* buf, size_t buf_size, size_t* out_len);
/* Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] */
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
/** Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data}]
/* Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data}]
* count=0 → все, offset=0 → сначала */
int chat_core_get_messages_json(const char* ch_id, int count, int offset,
int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len);
/** Мемберы канала с полным состоянием в JSON:
/* Мемберы канала с полным состоянием в JSON:
* [{node_id,name,online,connected,x25519,ed25519,addrs:[{ip,port,proto,rtt}]}] */
int chat_core_get_members_json(const char* ch_id, char* buf, size_t buf_size, size_t* out_len);
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len);
/** Имя узла из таблицы nodes */
int chat_core_get_node_name(uint64_t node_id, char* out, size_t sz);
/* Имя узла из таблицы nodes */
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz);
/* ── Редактирование adm_tags (владелец канала) ── */
@ -196,27 +210,28 @@ int chat_core_get_node_name(uint64_t node_id, char* out, size_t sz);
struct chat_member_tags;
/** Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет. */
struct chat_member_tags* chat_member_tags_load(const char* ch_id, uint64_t node_id);
/* Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет. */
struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
/** Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit. */
/* Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit. */
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val);
/** Получить значение ключа. Возвращает NULL если ключ не найден. */
/* Получить значение ключа. Возвращает NULL если ключ не найден. */
const char* chat_member_tags_get(struct chat_member_tags* t, const char* key);
/** Заменить все теги данными из JSON-строки (плоский, строковые значения). */
/* Заменить все теги данными из JSON-строки (плоский, строковые значения). */
int chat_member_tags_replace(struct chat_member_tags* t, const char* json);
/** Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519,
/* Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519,
* записать через member_sync_put() → синхронизируется всем. */
int chat_member_tags_commit(struct chat_member_tags* t);
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t);
/** Освободить без сохранения. */
/* Освободить без сохранения. */
void chat_member_tags_free(struct chat_member_tags* t);
/** Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память). */
/* Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память). */
struct chat_member_tags_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t node_id;
char json[512];
@ -225,11 +240,12 @@ void chat_member_tags_commit_trampoline(void* arg);
/* ── Передача прав админа (владелец → другому узлу) ── */
/** Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router). */
void chat_core_transfer_admin(const char* ch_id, uint64_t target_node_id);
/* Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router). */
void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
void chat_core_transfer_admin_trampoline(void* arg);
struct chat_transfer_admin_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t target_node_id;
};

21
src/chat/chat_core_priv.h

@ -75,6 +75,9 @@ static inline size_t b64_decode(const char* src, size_t src_len, uint8_t* dst, s
/* ── Глобальное состояние (определено в chat_core.c) ── */
struct ms_props_cbk; /* member_sync.c */
struct ms_apply_cbk; /* member_sync.c */
struct chat_core_ctx {
struct UTUN_INSTANCE* inst;
sqlite3* db;
@ -82,28 +85,36 @@ struct chat_core_ctx {
uint8_t shared_db;
uint64_t my_node_id;
uint8_t initialized;
/* per-instance sub-module state (was file-scope statics) */
struct ms_props_cbk* props_cbks; /* was member_sync.c g_props_cbks */
struct ms_apply_cbk* apply_cbks; /* was member_sync.c g_apply_cbks */
uint64_t media_cache_bytes; /* was chat_msg.c g_media_cache_bytes */
void* media_backfill_timer;/* was chat_msg.c g_media_startup_backfill_timer */
int media_backfill_ran; /* was chat_msg.c g_media_startup_backfill_ran */
struct UTUN_INSTANCE* media_backfill_inst; /* was chat_msg.c g_media_startup_backfill_inst */
};
extern struct chat_core_ctx g_cc;
/* Per-instance chat context. inst->chat_core обязателен после chat_core_init. */
#define CC(inst) ((inst)->chat_core)
/* ── Хелперы ── */
void sanitize_ch_id(const char* ch_id, char* out, size_t out_sz);
void msg_table_name(const char* ch_id, char* buf, size_t sz);
void peers_table_name(const char* ch_id, char* buf, size_t sz);
int db_exec(const char* sql);
/* ── db_sync registry (chat_core.c) ── */
struct DB_SYNC_INSTANCE* si_find(const char* ch_id);
void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id);
struct DB_SYNC_INSTANCE* si_find(struct UTUN_INSTANCE* inst, const char* ch_id);
void si_register(struct UTUN_INSTANCE* inst, struct DB_SYNC_INSTANCE* si, const char* ch_id);
/* ── db_sync callback (chat_msg.c) ── */
struct msg_insert_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; };
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg);
/* ── применить настройку group_autoconnect ко всем CHAT-группам (chat_channel.c) ── */
void chat_core_apply_group_autoconnect(void);
void chat_core_apply_group_autoconnect(struct UTUN_INSTANCE* inst);
#endif /* CHAT_CORE_PRIV_H */

15
src/chat/chat_event.c

@ -1,29 +1,30 @@
/*
* chat_event.c — реализация системы нотификаций
*
* Обработчик — per-instance (хранится в UTUN_INSTANCE.chat_event_handler).
* Пока обработчик не зарегистрирован — события логируются.
* При регистрации обработчика — форвардятся ему синхронно.
*/
#include "chat_event.h"
#include "../utun_instance.h"
#include "../lib/debug_config.h"
#ifdef __ANDROID__
#include <android/log.h>
#endif
static chat_event_handler_fn g_handler = NULL;
void chat_event_set_handler(chat_event_handler_fn handler) {
void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler) {
if (!inst) return;
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "utun-evt", "chat_event_set_handler: handler=%p old=%p",
(void*)handler, (void*)g_handler);
(void*)handler, (void*)inst->chat_event_handler);
#endif
g_handler = handler;
inst->chat_event_handler = handler;
}
void chat_event_post(int type, const uint8_t* data, int len) {
if (g_handler) { g_handler(type, data, len); return; }
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
if (inst && inst->chat_event_handler) { inst->chat_event_handler(inst, type, data, len); return; }
static const char* names[] = {
[1]="MSG_RECEIVED", [2]="CONNECT_RESULT", [3]="NEW_PEER",

10
src/chat/chat_event.h

@ -14,6 +14,8 @@
#include <stdint.h>
#include <stddef.h>
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
@ -50,11 +52,13 @@ extern "C" {
#define CHAT_EVT_INVITE_LINK_READY 29 /* [ch_id_len:1][ch_id:var][link_len:2][link:var] — пустой link = ошибка */
#define CHAT_EVT_INVITE_CANDIDATES 30 /* [ch_id_len:1][ch_id:var][auto_node_id:8][count:2][node_id:8]* — достижимые узлы для invite */
#define CHAT_EVT_CHANNEL_DELETED 31 /* [ch_id_len:1][ch_id:var] — канал удалён локально */
#define CHAT_EVT_DM_MSG_RECEIVED 32 /* [conv_id_len:1][conv_id:var][author_node_id:8] */
#define CHAT_EVT_DM_CONV_UPDATED 33 /* [conv_id_len:1][conv_id:var] — беседа создана/изменена */
typedef void (*chat_event_handler_fn)(int type, const uint8_t* data, int len);
typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
void chat_event_set_handler(chat_event_handler_fn handler);
void chat_event_post(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);
#ifdef __cplusplus
}

312
src/chat/chat_headless_control.c

@ -1,40 +1,54 @@
/*
* chat_headless_control.c — TCP control socket for headless chat CLI
*
* JSON line protocol over TCP. Integrated into uasync event loop.
* Default: localhost-only, port from config.
* JSON line protocol:
* Request: {"id":N,"cmd":"<command>",...params}
* Response: {"id":N,"ok":true,"data":{...}} | {"id":N,"ok":false,"error":"..."}
* Event: {"event":"<type>",...}
*
* Commands: ping, status, channels, members, messages, send, invite,
* invite_nodes, connect, create_channel, subscribe, quit
*/
#include "chat_headless_control.h"
#include "invite_link.h"
#include "invite_build.h"
#include "chat_core.h"
#include "chat_sync.h"
#include "chat_core_priv.h"
#include "chat_event.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "chat_sync.h"
#include "chat_join.h"
#include "invite_link.h"
#include "invite_build.h"
#include "../dm/dm_core.h"
#include "../dm/dm_crypto.h"
#include "../transport_layer/secure_channel.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/etcp_connections.h"
#include "../transport_layer/secure_channel.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../../lib/ll_queue.h"
#include "../../lib/u_async.h"
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include "../../lib/ll_queue.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../../lib/socket_compat.h"
#include "../../lib/debug_config.h"
#include "../../lib/platform_compat.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <stdarg.h>
#include <errno.h>
#include <sys/stat.h>
#include "../../lib/platform_compat.h"
#include "../../lib/socket_compat.h"
#include <time.h>
#include <sqlite3.h>
#define HC_ID "headless"
#define MAX_CLIENTS 16
#define RECV_BUF_SIZE 16384
#define SEND_BUF_SIZE 16384
#define SEND_BUF_SIZE 32768
#define MAX_CLIENTS 64
#ifndef DEBUG_CATEGORY_HEADLESS
#define DEBUG_CATEGORY_HEADLESS 27
#define DEBUG_CATEGORY_HEADLESS DEBUG_CATEGORY_GENERAL
#endif
struct headless_client {
@ -47,10 +61,11 @@ struct headless_client {
int send_offset;
int subscribed;
int closing;
struct headless_control* hc;
struct headless_client* next;
};
static struct {
struct headless_control {
struct UASYNC* ua;
struct UTUN_INSTANCE* inst;
struct headless_client* clients;
@ -58,7 +73,11 @@ static struct {
socket_t listen_fd;
void* listen_sock_id;
int running;
} g_hc;
};
static struct headless_control* hc_of(struct UTUN_INSTANCE* inst) {
return inst ? (struct headless_control*)inst->headless : NULL;
}
/* ── Forward declarations ── */
@ -104,6 +123,7 @@ static const char* json_get_cmd_end(const char* cmd_start) {
/* ── Send helpers ── */
static void cli_send(struct headless_client* cli, const char* data, size_t len) {
struct headless_control* hc = cli->hc;
if (!cli || cli->closing || cli->fd == SOCKET_INVALID || !data || len == 0) return;
if (cli->send_len > 0) {
size_t space = sizeof(cli->send_buf) - (size_t)cli->send_len;
@ -115,13 +135,13 @@ static void cli_send(struct headless_client* cli, const char* data, size_t len)
ssize_t r = send(cli->fd, data, (int)len, 0);
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) {
if (len <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data, len); cli->send_len = (int)len; cli->send_offset = 0; }
if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 1);
if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 1);
return;
}
if (r >= 0 && (size_t)r < len) {
size_t remain = len - (size_t)r;
if (remain <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data + r, remain); cli->send_len = (int)remain; cli->send_offset = 0; }
if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 1);
if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 1);
}
}
@ -139,17 +159,19 @@ static void send_response(struct headless_client* cli, int id, const char* ok_da
/* ── Broadcast event ── */
static void hc_broadcast_event(const char* json) {
static void hc_broadcast_event(struct headless_control* hc, const char* json) {
size_t jlen = strlen(json);
char buf[4096]; int blen = snprintf(buf, sizeof(buf), "%s\n", json);
if (blen <= 0) return;
for (struct headless_client* c = g_hc.clients; c; c = c->next)
for (struct headless_client* c = hc->clients; c; c = c->next)
if (c->subscribed) cli_send(c, buf, (size_t)blen);
}
/* ── chat_event callback ── */
static void hc_on_chat_event(int type, const uint8_t* data, int len) {
static void hc_on_chat_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
struct headless_control* hc = hc_of(inst);
if (!hc) return;
if (type == 10) return; /* skip status text */
char json[1024]; json[0] = '\0';
if (type == 1 && data && len >= 10) { /* MSG_RECEIVED */
@ -172,29 +194,38 @@ static void hc_on_chat_event(int type, const uint8_t* data, int len) {
(int)cl, data + 1, (int)nl, data + 2 + cl, (unsigned long long)inviter);
}
}
} else if (type == 32 && data && len >= 10) { /* DM_MSG_RECEIVED */
uint8_t cl = data[0];
uint64_t author; memcpy(&author, data + 1 + cl, 8);
snprintf(json, sizeof(json), "{\"event\":\"dm_msg\",\"conv\":\"%.*s\",\"author_id\":\"0x%016llx\"}", (int)cl, data + 1, (unsigned long long)author);
} else if (type == 33 && data && len >= 2) { /* DM_CONV_UPDATED */
uint8_t cl = data[0];
snprintf(json, sizeof(json), "{\"event\":\"dm_conv\",\"conv\":\"%.*s\"}", (int)cl, data + 1);
}
if (json[0]) hc_broadcast_event(json);
if (json[0]) hc_broadcast_event(hc, json);
}
/* ── Write callback ── */
static void client_write_callback(socket_t fd, void* arg) {
struct headless_client* cli = (struct headless_client*)arg;
struct headless_control* hc = cli ? cli->hc : NULL;
if (!cli || cli->closing || fd != cli->fd) return;
if (cli->send_len <= 0) { if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 0); return; }
if (cli->send_len <= 0) { if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); return; }
ssize_t r = send(cli->fd, cli->send_buf + cli->send_offset, (int)(cli->send_len - cli->send_offset), 0);
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) return;
if (r <= 0) { cli->send_len = 0; cli->send_offset = 0; if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 0); return; }
if (r <= 0) { cli->send_len = 0; cli->send_offset = 0; if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); return; }
cli->send_offset += (int)r;
if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 0); }
if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); }
}
/* ── Close client ── */
static void hc_close_client(struct headless_client* cli) {
struct headless_control* hc = cli ? cli->hc : NULL;
if (!cli || cli->closing) return;
cli->closing = 1;
if (g_hc.ua && cli->socket_id) uasync_remove_socket_t(g_hc.ua, cli->fd);
if (hc && hc->ua && cli->socket_id) uasync_remove_socket_t(hc->ua, cli->fd);
if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; }
}
@ -219,30 +250,29 @@ static void client_read_callback(socket_t fd, void* arg) {
static void hc_handle_ping(struct headless_client* cli, int id, const char* json) {
(void)json;
uint64_t uptime = 0;
(void)uptime;
char data[128]; snprintf(data, sizeof(data), "{\"pong\":true,\"uptime_sec\":%llu}", (unsigned long long)uptime);
char data[128]; snprintf(data, sizeof(data), "{\"pong\":true,\"uptime_sec\":0}");
send_response(cli, id, data, NULL);
}
static void hc_handle_status(struct headless_client* cli, int id, const char* json) {
(void)json;
struct headless_control* hc = cli->hc;
char buf[8192]; int off = 0;
off += snprintf(buf + off, sizeof(buf) - off, "\"");
if (!g_hc.inst) {
if (!hc->inst) {
off += snprintf(buf + off, sizeof(buf) - off, "ERROR: no instance");
} else {
int conn_count = g_hc.inst->connections ? queue_entry_count(g_hc.inst->connections) : 0;
struct NTP_TIME* ntp = &g_hc.inst->ntp;
int conn_count = hc->inst->connections ? queue_entry_count(hc->inst->connections) : 0;
struct NTP_TIME* ntp = &hc->inst->ntp;
off += snprintf(buf + off, sizeof(buf) - off, "NTP: enabled=%s synced=%s offset=%.1fs\\n",
ntp->enabled ? "yes" : "no", ntp->synced ? "yes" : "no", ntp->offset_us / 1000000.0);
off += snprintf(buf + off, sizeof(buf) - off, "Connections: %d active\\n", conn_count);
uint64_t my_id = g_hc.inst->node_id;
struct ll_entry* entry = g_hc.inst->connections ? g_hc.inst->connections->head : NULL;
uint64_t my_id = hc->inst->node_id;
struct ll_entry* entry = hc->inst->connections ? hc->inst->connections->head : NULL;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (ce) {
char name[64]; chat_core_get_node_name(ce->peer_node_id, name, sizeof(name));
char name[64]; chat_core_get_node_name(hc->inst, ce->peer_node_id, name, sizeof(name));
off += snprintf(buf + off, sizeof(buf) - off, " [%04llX]->[%04llX] %s ETCP:%s\\n",
(unsigned long long)(my_id & 0xFFFF), (unsigned long long)(ce->peer_node_id & 0xFFFF),
name[0] ? name : "?", ce->conn && ce->conn->links_up ? "UP" : "DOWN");
@ -258,7 +288,7 @@ static void hc_handle_status(struct headless_client* cli, int id, const char* js
static void hc_handle_channels(struct headless_client* cli, int id, const char* json) {
(void)json;
char buf[16384]; size_t len = 0;
if (chat_core_get_channels_json(buf, sizeof(buf), &len) < 0)
if (chat_core_get_channels_json(cli->hc->inst, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query channels");
else
send_response(cli, id, buf, NULL);
@ -268,7 +298,7 @@ static void hc_handle_members(struct headless_client* cli, int id, const char* j
char ch[64];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
char buf[16384]; size_t len = 0;
if (chat_core_get_members_json(ch, buf, sizeof(buf), &len) < 0)
if (chat_core_get_members_json(cli->hc->inst, ch, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query members");
else
send_response(cli, id, buf, NULL);
@ -280,7 +310,7 @@ static void hc_handle_messages(struct headless_client* cli, int id, const char*
count = json_get_int(json, "count", 20);
offset = json_get_int(json, "offset", 0);
char buf[16384]; size_t len = 0;
if (chat_core_get_messages_json(ch, count, offset, buf, sizeof(buf), &len) < 0)
if (chat_core_get_messages_json(cli->hc->inst, ch, count, offset, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query messages");
else
send_response(cli, id, buf, NULL);
@ -293,11 +323,12 @@ static void hc_handle_send(struct headless_client* cli, int id, const char* json
if (json_get_str(json, "data", data, sizeof(data)) < 0) { send_response(cli, id, NULL, "missing 'data' param"); return; }
struct chat_msg_submit* req = u_calloc(1, sizeof(*req));
if (!req) { send_response(cli, id, NULL, "out of memory"); return; }
req->inst = cli->hc->inst;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch);
snprintf(req->content_type, sizeof(req->content_type), "%s", ct);
req->data = (uint8_t*)u_strdup(data);
req->data_len = (uint32_t)strlen(data);
chat_core_submit_message(req);
chat_core_submit_message(cli->hc->inst, req);
if (req->data) u_free(req->data);
u_free(req);
send_response(cli, id, "{\"sent\":true}", NULL);
@ -317,6 +348,7 @@ static int hc_mkdirs(const char* path) {
}
static void hc_handle_attach(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char ch[64], path[1024], ct[32];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
if (json_get_str(json, "path", path, sizeof(path)) < 0) { send_response(cli, id, NULL, "missing 'path' param"); return; }
@ -327,10 +359,10 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js
struct stat st;
if (stat(path, &st) != 0 || st.st_size <= 0) { send_response(cli, id, NULL, "file not found or empty"); return; }
if (!g_hc.inst || !g_hc.inst->media_async) { send_response(cli, id, NULL, "media engine not initialized"); return; }
if (!hc->inst || !hc->inst->media_async) { send_response(cli, id, NULL, "media engine not initialized"); return; }
/* media_base из ui_state (как в блок-сервере media_delivery) */
sqlite3* db = chat_core_get_db();
sqlite3* db = chat_core_get_db(hc->inst);
char media_base[512] = "/tmp/utun_media";
if (db) {
sqlite3_stmt* ms = NULL;
@ -362,6 +394,7 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js
struct chat_msg_submit* req = u_calloc(1, sizeof(*req));
if (!req) { send_response(cli, id, NULL, "out of memory"); return; }
req->inst = hc->inst;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch);
snprintf(req->content_type, sizeof(req->content_type), "%s", as_video ? "video/mp4" : ct);
snprintf(req->media_src, sizeof(req->media_src), "%s", path);
@ -374,7 +407,7 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: attach ch=%s src=%s dst=%s video=%d",
ch, path, dest, as_video);
chat_core_submit_media_message(req);
chat_core_submit_media_message(hc->inst, req);
u_free(req);
char resp[1400]; snprintf(resp, sizeof(resp), "{\"attached\":true,\"dest\":\"%s\",\"video\":%s}",
@ -383,9 +416,10 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js
}
static void hc_handle_invite(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char ch[64], node_id_str[32];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
if (!g_hc.inst) { send_response(cli, id, NULL, "no instance"); return; }
if (!hc->inst) { send_response(cli, id, NULL, "no instance"); return; }
uint64_t channel_id = strtoull(ch, NULL, 10);
if (channel_id == 0) { send_response(cli, id, NULL, "invalid channel_id"); return; }
@ -396,7 +430,7 @@ static void hc_handle_invite(struct headless_client* cli, int id, const char* js
target_node_id = strtoull(node_id_str, NULL, 16);
char link[1024];
if (chat_invite_build_link(channel_id, target_node_id, NULL, link, sizeof(link)) < 0) {
if (chat_invite_build_link(hc->inst, channel_id, target_node_id, NULL, link, sizeof(link)) < 0) {
send_response(cli, id, NULL, "failed to build invite link (see log)");
return;
}
@ -418,24 +452,25 @@ static void hc_handle_invite(struct headless_client* cli, int id, const char* js
}
static void hc_handle_invite_nodes(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char ch[64];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
uint64_t channel_id = strtoull(ch, NULL, 10);
if (channel_id == 0) { send_response(cli, id, NULL, "invalid channel_id"); return; }
uint64_t ids[INVITE_ADDR_MAX];
int cnt = chat_invite_candidate_nodes(channel_id, ids, INVITE_ADDR_MAX);
int cnt = chat_invite_candidate_nodes(hc->inst, channel_id, ids, INVITE_ADDR_MAX);
if (cnt < 0) { send_response(cli, id, NULL, "no candidates (see log)"); return; }
uint64_t auto_id = 0;
chat_invite_best_node(channel_id, &auto_id);
chat_invite_best_node(hc->inst, channel_id, &auto_id);
char resp[4096]; int off = 0;
off += snprintf(resp + off, sizeof(resp) - off, "{\"ch\":\"%s\",\"auto_node_id\":\"0x%016llx\",\"nodes\":[",
ch, (unsigned long long)auto_id);
for (int i = 0; i < cnt && off < (int)sizeof(resp) - 256; i++) {
char name[64] = "";
chat_core_get_node_name(ids[i], name, sizeof(name));
chat_core_get_node_name(hc->inst, ids[i], name, sizeof(name));
off += snprintf(resp + off, sizeof(resp) - off, "%s{\"node_id\":\"0x%016llx\",\"name\":\"%s\"}",
i ? "," : "", (unsigned long long)ids[i], name);
}
@ -444,11 +479,12 @@ static void hc_handle_invite_nodes(struct headless_client* cli, int id, const ch
}
static void hc_handle_connect(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char link[1024];
if (json_get_str(json, "link", link, sizeof(link)) < 0) { send_response(cli, id, NULL, "missing 'link' param"); return; }
struct InviteData d; char err[256];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { send_response(cli, id, NULL, err); return; }
if (!g_hc.inst) { send_response(cli, id, NULL, "no instance"); return; }
if (!hc->inst) { send_response(cli, id, NULL, "no instance"); return; }
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: connect ch=%llu node=0x%016llx addrs=%d",
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
@ -457,13 +493,12 @@ static void hc_handle_connect(struct headless_client* cli, int id, const char* j
int addrs_len = invite_serialize_addrs(&d, addrs_buf, sizeof(addrs_buf));
if (addrs_len <= 0) { send_response(cli, id, NULL, "serialize addrs failed"); return; }
chat_core_ensure_channel_ready((const char*)(&(char[64]){0}));
{ char chstr[64]; snprintf(chstr, sizeof(chstr), "%llu", (unsigned long long)d.channelId);
chat_core_ensure_channel_ready(chstr); }
chat_core_ensure_channel_ready(hc->inst, chstr); }
chat_core_set_my_node_id(g_hc.inst->node_id);
chat_core_set_my_node_id(hc->inst, hc->inst->node_id);
chat_sync_connect_from_invite(d.channelId, d.nodeId, d.pubkey,
chat_sync_connect_from_invite(hc->inst, d.channelId, d.nodeId, d.pubkey,
addrs_buf, d.addrCount, addrs_len, d.password_len ? d.password : NULL, d.join_key);
char resp[512]; snprintf(resp, sizeof(resp),
@ -475,21 +510,22 @@ static void hc_handle_connect(struct headless_client* cli, int id, const char* j
static void hc_handle_create_channel(struct headless_client* cli, int id, const char* json) {
char name[256];
if (json_get_str(json, "name", name, sizeof(name)) < 0) { send_response(cli, id, NULL, "missing 'name' param"); return; }
chat_core_create_channel_auto(name);
chat_core_create_channel_auto(cli->hc->inst, name);
send_response(cli, id, "{\"created\":true}", NULL);
}
static void hc_handle_invite_to(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char ch[64], node_id_str[32], pubkey_hex[128], addr[128];
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; }
if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; }
if (json_get_str(json, "pubkey", pubkey_hex, sizeof(pubkey_hex)) < 0) { send_response(cli, id, NULL, "missing 'pubkey' param"); return; }
if (json_get_str(json, "addr", addr, sizeof(addr)) < 0) { send_response(cli, id, NULL, "missing 'addr' param"); return; }
int proto = json_get_int(json, "proto", 1);
if (!g_hc.inst) { send_response(cli, id, NULL, "no instance"); return; }
if (!hc->inst) { send_response(cli, id, NULL, "no instance"); return; }
uint64_t target_node_id = strtoull(node_id_str, NULL, 16);
if (target_node_id == 0 || target_node_id == g_hc.inst->node_id) { send_response(cli, id, NULL, "invalid node_id"); return; }
if (target_node_id == 0 || target_node_id == hc->inst->node_id) { send_response(cli, id, NULL, "invalid node_id"); return; }
uint8_t pubkey_bin[SC_PUBKEY_SIZE];
if (sc_hex_to_binary(pubkey_hex, pubkey_bin, SC_PUBKEY_SIZE) != 0) { send_response(cli, id, NULL, "invalid pubkey hex"); return; }
@ -520,7 +556,7 @@ static void hc_handle_invite_to(struct headless_client* cli, int id, const char*
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite_to ch=%s node=0x%016llx addr=%s:%d proto=%d",
ch, (unsigned long long)target_node_id, ip, port, proto);
chat_sync_invite_to_channel_with_addrs(g_hc.inst, ch, target_node_id,
chat_sync_invite_to_channel_with_addrs(hc->inst, ch, target_node_id,
pubkey_bin, addrs_data, 1, addrs_len);
char resp[256]; snprintf(resp, sizeof(resp),
@ -543,14 +579,85 @@ static void hc_handle_quit(struct headless_client* cli, int id, const char* json
hc_close_client(cli);
}
/* ── DM (прямой чат) ── */
static void hc_handle_dm_start(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char node_id_str[64], ch[64];
if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; }
if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) ch[0] = '\0';
uint64_t target = strtoull(node_id_str, NULL, 16);
if (!target || !hc->inst) { send_response(cli, id, NULL, "invalid node_id"); return; }
sqlite3* db = chat_core_get_db(hc->inst);
uint8_t x25519[32], ed25519[32];
int ok = 0;
sqlite3_stmt* st = NULL;
if (db && sqlite3_prepare_v2(db,
"SELECT x25519_pubkey, ed25519_pubkey, COALESCE(name,'') FROM nodes WHERE node_id=?",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)target);
if (sqlite3_step(st) == SQLITE_ROW) {
const uint8_t* x = (const uint8_t*)sqlite3_column_blob(st, 0);
const uint8_t* e = (const uint8_t*)sqlite3_column_blob(st, 1);
if (x && e && sqlite3_column_bytes(st, 0) >= 32 && sqlite3_column_bytes(st, 1) >= 32) {
memcpy(x25519, x, 32); memcpy(ed25519, e, 32);
const char* name = (const char*)sqlite3_column_text(st, 2);
if (dm_start(hc->inst, target, x25519, ed25519, name, ch[0] ? ch : NULL) == 0) ok = 1;
}
}
}
if (st) sqlite3_finalize(st);
if (!ok) { send_response(cli, id, NULL, "failed to start DM (node not found?)"); return; }
char conv[64]; uint64_t conv_num = dm_derive_conv_id(hc->inst->node_id, target);
snprintf(conv, sizeof(conv), "%llu", (unsigned long long)conv_num);
char resp[256]; snprintf(resp, sizeof(resp), "{\"conv_id\":\"%s\",\"started\":true}", conv);
send_response(cli, id, resp, NULL);
}
/* Список DM-бесед. */
static void hc_handle_dm_list(struct headless_client* cli, int id, const char* json) {
(void)json;
char buf[16384]; size_t len = 0;
if (dm_list_conversations_json(cli->hc->inst, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query conversations");
else
send_response(cli, id, buf, NULL);
}
/* Сообщения DM-беседы (расшифрованные). */
static void hc_handle_dm_messages(struct headless_client* cli, int id, const char* json) {
char conv[64]; int count = 20, offset = 0;
if (json_get_str(json, "conv_id", conv, sizeof(conv)) < 0) { send_response(cli, id, NULL, "missing 'conv_id' param"); return; }
count = json_get_int(json, "count", 20);
offset = json_get_int(json, "offset", 0);
char buf[16384]; size_t len = 0;
if (dm_list_messages_json(cli->hc->inst, conv, count, offset, buf, sizeof(buf), &len) < 0)
send_response(cli, id, NULL, "failed to query DM messages");
else
send_response(cli, id, buf, NULL);
}
/* Отправить сообщение в DM-беседу. */
static void hc_handle_dm_send(struct headless_client* cli, int id, const char* json) {
char conv[64], data[4096];
if (json_get_str(json, "conv_id", conv, sizeof(conv)) < 0) { send_response(cli, id, NULL, "missing 'conv_id' param"); return; }
if (json_get_str(json, "data", data, sizeof(data)) < 0) { send_response(cli, id, NULL, "missing 'data' param"); return; }
if (dm_send(cli->hc->inst, conv, "text", (const uint8_t*)data, (uint32_t)strlen(data)) != 0)
send_response(cli, id, NULL, "failed to send DM");
else
send_response(cli, id, "{\"sent\":true}", NULL);
}
/* ── Command dispatch ── */
typedef void (*hc_cmd_fn)(struct headless_client* cli, int id, const char* json);
static const char* cmd_names[] = { "ping", "status", "channels", "members", "messages", "send", "attach", "invite", "invite_nodes", "connect", "create_channel", "invite_to", "subscribe", "quit", NULL };
static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_attach, hc_handle_invite, hc_handle_invite_nodes, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_quit };
static const char* cmd_names[] = { "ping", "status", "channels", "members", "messages", "send", "attach", "invite", "invite_nodes", "connect", "create_channel", "invite_to", "subscribe", "dm_start", "dm_list", "dm_messages", "dm_send", "quit", NULL };
static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_attach, hc_handle_invite, hc_handle_invite_nodes, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_dm_start, hc_handle_dm_list, hc_handle_dm_messages, hc_handle_dm_send, hc_handle_quit };
void hc_handle_command(struct headless_client* cli, const char* json) {
static void hc_handle_command(struct headless_client* cli, const char* json) {
if (!cli || !json) return;
int id = json_get_int(json, "id", 0);
const char* cs = json_get_cmd(json);
@ -568,87 +675,94 @@ void hc_handle_command(struct headless_client* cli, const char* json) {
/* ── Accept callback ── */
static void accept_callback(socket_t fd, void* arg) {
(void)arg;
struct headless_control* hc = (struct headless_control*)arg;
struct sockaddr_storage addr; socklen_t alen = sizeof(addr);
socket_t cfd = accept(fd, (struct sockaddr*)&addr, &alen);
if (cfd == SOCKET_INVALID) { int e = socket_get_error(); if (e != ERR_AGAIN && e != ERR_WOULDBLOCK) DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: accept failed: %s", socket_strerror(e)); return; }
socket_set_nonblocking(cfd);
if (g_hc.client_count >= MAX_CLIENTS) { socket_close_wrapper(cfd); DEBUG_WARN((int)DEBUG_CATEGORY_HEADLESS, "headless: max clients reached"); return; }
if (hc->client_count >= MAX_CLIENTS) { socket_close_wrapper(cfd); DEBUG_WARN((int)DEBUG_CATEGORY_HEADLESS, "headless: max clients reached"); return; }
struct headless_client* cli = u_calloc(1, sizeof(*cli));
if (!cli) { socket_close_wrapper(cfd); DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: failed to allocate client"); return; }
cli->fd = cfd;
cli->hc = hc;
cli->socket_id = uasync_add_socket_t(g_hc.ua, cfd, client_read_callback, client_write_callback, NULL, cli);
cli->socket_id = uasync_add_socket_t(hc->ua, cfd, client_read_callback, client_write_callback, NULL, cli);
if (!cli->socket_id) { u_free(cli); socket_close_wrapper(cfd); DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: failed to register client fd"); return; }
cli->next = g_hc.clients; g_hc.clients = cli; g_hc.client_count++;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: client connected fd=%d total=%d", (int)cfd, g_hc.client_count);
cli->next = hc->clients; hc->clients = cli; hc->client_count++;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: client connected fd=%d total=%d", (int)cfd, hc->client_count);
}
/* ── Periodic cleanup ── */
static void hc_cleanup(void* arg) {
(void)arg;
struct headless_client** prev = &g_hc.clients;
struct headless_control* hc = (struct headless_control*)arg;
struct headless_client** prev = &hc->clients;
while (*prev) {
if ((*prev)->closing) {
struct headless_client* dead = *prev; *prev = dead->next;
if (dead->socket_id && g_hc.ua) uasync_remove_socket_t(g_hc.ua, dead->fd);
if (dead->socket_id && hc->ua) uasync_remove_socket_t(hc->ua, dead->fd);
if (dead->fd != SOCKET_INVALID) socket_close_wrapper(dead->fd);
u_free(dead); g_hc.client_count--;
u_free(dead); hc->client_count--;
} else prev = &(*prev)->next;
}
if (g_hc.ua) uasync_set_timeout(g_hc.ua, 5000, NULL, hc_cleanup, "hc_cleanup");
if (hc->ua) uasync_set_timeout(hc->ua, 5000, hc, hc_cleanup, "hc_cleanup");
}
/* ── Init / Destroy ── */
int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port) {
if (!ua || !bind_ip || port <= 0 || port > 65535) return -1;
if (g_hc.running) return 0;
memset(&g_hc, 0, sizeof(g_hc));
g_hc.ua = ua; g_hc.inst = inst;
if (!ua || !inst || !bind_ip || port <= 0 || port > 65535) return -1;
struct headless_control* hc = hc_of(inst);
if (hc && hc->running) return 0;
if (!hc) {
hc = u_calloc(1, sizeof(*hc));
if (!hc) return -1;
inst->headless = hc;
}
hc->ua = ua; hc->inst = inst;
g_hc.listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (g_hc.listen_fd == SOCKET_INVALID) { DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: socket() failed: %s", socket_strerror(socket_get_error())); return -1; }
hc->listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (hc->listen_fd == SOCKET_INVALID) { DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: socket() failed: %s", socket_strerror(socket_get_error())); return -1; }
socket_set_reuseaddr(g_hc.listen_fd, 1);
socket_set_nonblocking(g_hc.listen_fd);
socket_set_reuseaddr(hc->listen_fd, 1);
socket_set_nonblocking(hc->listen_fd);
struct sockaddr_in sin; memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET; sin.sin_port = htons((uint16_t)port);
if (inet_pton(AF_INET, bind_ip, &sin.sin_addr) != 1) { socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1; }
if (inet_pton(AF_INET, bind_ip, &sin.sin_addr) != 1) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; }
if (bind(g_hc.listen_fd, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
if (bind(hc->listen_fd, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: bind(%s:%d) failed: %s", bind_ip, port, socket_strerror(socket_get_error()));
socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1;
socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1;
}
if (listen(g_hc.listen_fd, 5) < 0) { socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1; }
if (listen(hc->listen_fd, 5) < 0) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; }
g_hc.listen_sock_id = uasync_add_socket_t(ua, g_hc.listen_fd, accept_callback, NULL, NULL, NULL);
if (!g_hc.listen_sock_id) { socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1; }
hc->listen_sock_id = uasync_add_socket_t(ua, hc->listen_fd, accept_callback, NULL, NULL, hc);
if (!hc->listen_sock_id) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; }
chat_event_set_handler(hc_on_chat_event);
uasync_set_timeout(ua, 5000, NULL, hc_cleanup, "hc_cleanup");
chat_event_set_handler(inst, hc_on_chat_event);
uasync_set_timeout(ua, 5000, hc, hc_cleanup, "hc_cleanup");
g_hc.running = 1;
hc->running = 1;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: listening on %s:%d", bind_ip, port);
return 0;
}
void chat_headless_control_destroy(void) {
if (!g_hc.running) return;
g_hc.running = 0;
chat_event_set_handler(NULL);
if (g_hc.listen_sock_id && g_hc.ua) uasync_remove_socket_t(g_hc.ua, g_hc.listen_fd);
if (g_hc.listen_fd != SOCKET_INVALID) { socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; }
struct headless_client* c = g_hc.clients;
void chat_headless_control_destroy(struct UTUN_INSTANCE* inst) {
struct headless_control* hc = hc_of(inst);
if (!hc || !hc->running) return;
hc->running = 0;
chat_event_set_handler(inst, NULL);
if (hc->listen_sock_id && hc->ua) uasync_remove_socket_t(hc->ua, hc->listen_fd);
if (hc->listen_fd != SOCKET_INVALID) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; }
struct headless_client* c = hc->clients;
while (c) { struct headless_client* n = c->next; if (c->fd != SOCKET_INVALID) socket_close_wrapper(c->fd); u_free(c); c = n; }
g_hc.clients = NULL; g_hc.client_count = 0;
hc->clients = NULL; hc->client_count = 0;
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: destroyed");
}

2
src/chat/chat_headless_control.h

@ -20,6 +20,6 @@ struct UTUN_INSTANCE;
int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
const char* bind_ip, int port);
void chat_headless_control_destroy(void);
void chat_headless_control_destroy(struct UTUN_INSTANCE* inst);
#endif /* CHAT_HEADLESS_CONTROL_H */

37
src/chat/chat_join.c

@ -38,13 +38,11 @@ struct join_key_entry {
uint64_t expiry_tb; /* timebase (0.1ms) истечения */
};
static struct join_key_entry* g_keys = NULL;
static uint64_t _now_tb(void) { return get_time_tb(); }
static void _keys_purge_expired(void) {
static void _keys_purge_expired(struct UTUN_INSTANCE* inst) {
uint64_t now = _now_tb();
struct join_key_entry** p = &g_keys;
struct join_key_entry** p = &inst->join_keys;
while (*p) {
if ((*p)->expiry_tb <= now) {
struct join_key_entry* r = *p;
@ -59,30 +57,30 @@ static void _keys_purge_expired(void) {
}
}
static void _keys_put(uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) {
_keys_purge_expired();
static void _keys_put(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key, uint64_t inviter_id) {
_keys_purge_expired(inst);
struct join_key_entry* e = u_malloc(sizeof(*e));
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key alloc failed", CJ_ID); return; }
e->join_key = join_key; e->channel_id = channel_id; e->inviter_id = inviter_id;
e->expiry_tb = _now_tb() + (uint64_t)JOIN_KEY_TTL_SECONDS * 10000ULL;
e->next = g_keys;
g_keys = e;
e->next = inst->join_keys;
inst->join_keys = e;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: join-key stored key=%016llx ch=%llu inviter=%016llx ttl=%ds",
CJ_ID, (unsigned long long)join_key, (unsigned long long)channel_id,
(unsigned long long)inviter_id, JOIN_KEY_TTL_SECONDS);
}
static struct join_key_entry* _keys_find(uint64_t channel_id, uint64_t join_key) {
_keys_purge_expired();
for (struct join_key_entry* e = g_keys; e; e = e->next)
static struct join_key_entry* _keys_find(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
_keys_purge_expired(inst);
for (struct join_key_entry* e = inst->join_keys; e; e = e->next)
if (e->channel_id == channel_id && e->join_key == join_key) return e;
return NULL;
}
static void _keys_free_all(void) {
struct join_key_entry* e = g_keys;
static void _keys_free_all(struct UTUN_INSTANCE* inst) {
struct join_key_entry* e = inst->join_keys;
while (e) { struct join_key_entry* n = e->next; u_free(e); e = n; }
g_keys = NULL;
inst->join_keys = NULL;
}
/* ── Сериализация member-блока ── */
@ -142,7 +140,7 @@ void chat_join_process_request(struct UTUN_INSTANCE* inst, uint64_t group_id,
sqlite3* db = inst->topo_sqlite_db;
/* 0. верификация join-ключа: принимаем только выданные нами (инвайтером) ключи */
struct join_key_entry* ke = _keys_find(group_id, join_key);
struct join_key_entry* ke = _keys_find(inst, group_id, join_key);
if (!ke || ke->inviter_id != inst->node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: process_request invalid join key ch=%s from=%016llx key=%016llx — drop",
CJ_ID, ch_id, (unsigned long long)from_node, (unsigned long long)join_key);
@ -228,7 +226,7 @@ static void join_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
/* payload: [subcmd:1][join_key:8] */
if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
uint64_t join_key; memcpy(&join_key, payload + 1, 8);
_keys_put(group_id, join_key, from_node);
_keys_put(inst, group_id, join_key, from_node);
} else if (subcmd == JOIN_SUBCMD_REQUEST_FWD) {
/* payload: [subcmd:1][join_key:8][member...] */
if (plen < 1 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; }
@ -256,7 +254,7 @@ int chat_join_init(struct UTUN_INSTANCE* inst) {
void chat_join_destroy(struct UTUN_INSTANCE* inst) {
if (!inst) return;
etcp_router_unbind(inst, ETCP_RT_ID_JOIN);
_keys_free_all();
_keys_free_all(inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: destroyed", CJ_ID);
}
@ -267,7 +265,7 @@ void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
if (!inst || channel_id == 0 || join_key == 0) return;
/* инвайтер всегда хранит выданный ключ локально — для верификации REQUEST_FWD */
_keys_put(channel_id, join_key, inst->node_id);
_keys_put(inst, channel_id, join_key, inst->node_id);
if (target_node_id == inst->node_id || target_node_id == 0) {
return;
@ -297,8 +295,7 @@ void chat_join_register_key(struct UTUN_INSTANCE* inst, uint64_t channel_id,
}
uint64_t chat_join_lookup_inviter(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t join_key) {
(void)inst;
struct join_key_entry* e = _keys_find(channel_id, join_key);
struct join_key_entry* e = _keys_find(inst, channel_id, join_key);
return e ? e->inviter_id : 0;
}

77
src/chat/chat_member.c

@ -21,12 +21,13 @@
/* ─── live-онлайн: узел присутствует в BGP/topo_group канала ─── */
int chat_core_member_online(const char* ch_id, uint64_t node_id) {
if (!g_cc.inst || !ch_id || !ch_id[0]) return 0;
if (node_id == g_cc.my_node_id) return 1; /* self всегда онлайн (local_node вне group->nodes) */
if (!g_cc.inst->topo_groups) return 0;
int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->inst || !ch_id || !ch_id[0]) return 0;
if (node_id == cc->my_node_id) return 1; /* self всегда онлайн (local_node вне group->nodes) */
if (!cc->inst->topo_groups) return 0;
uint64_t group_id = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(g_cc.inst->topo_groups, group_id);
struct TOPO_GROUP* g = topo_groups_find(cc->inst->topo_groups, group_id);
if (!g || g->group_type != TOPO_GROUP_TYPE_CHAT) return 0;
return topo_node_find_by_id(g, node_id) != NULL;
}
@ -42,6 +43,10 @@ static uint8_t parse_adm_tags_flags(const char* adm_tags) {
if (json_flat_get(adm_tags, "supernode", buf, sizeof(buf)) == 0
&& strcmp(buf, "yes") == 0) flags |= CHAT_MEMBER_FLAG_SUPERNODE;
/* dm_storage: узел хранит offline-сообщения прямого чата (DM) */
if (json_flat_get(adm_tags, "dm_storage", buf, sizeof(buf)) == 0
&& strcmp(buf, "yes") == 0) flags |= CHAT_MEMBER_FLAG_DM_STORAGE;
/* admin: значение — накопительная строка ed...ed..., последний символ e/d */
if (json_flat_get(adm_tags, "admin", buf, sizeof(buf)) == 0) {
const char* last = buf;
@ -91,16 +96,17 @@ int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display*
/* ─── запрос списка мемберов ─── */
int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count) {
struct chat_core_ctx* cc = CC(inst);
if (!out || !count) return -1;
*out = NULL; *count = 0;
if (!g_cc.initialized || !ch_id || !g_cc.db || !g_cc.inst) {
if (!cc || !cc->initialized || !ch_id || !cc->db || !cc->inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: get_list — not initialized");
return -1;
}
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
uint64_t my_id = g_cc.my_node_id;
uint64_t my_id = cc->my_node_id;
/* 1. запрашиваем всех мемберов */
sqlite3_stmt* st = NULL;
@ -112,9 +118,9 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" ORDER BY p.node_id ASC", peers_tbl);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: get_list — query failed ch=%s err=%s",
ch_id, sqlite3_errmsg(g_cc.db));
ch_id, sqlite3_errmsg(cc->db));
return -1;
}
@ -130,7 +136,7 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
}
struct chat_member_display* m = &members[cnt];
m->node_id = (uint64_t)sqlite3_column_int64(st, 0);
m->online = (uint8_t)chat_core_member_online(ch_id, m->node_id);
m->online = (uint8_t)chat_core_member_online(inst, ch_id, m->node_id);
const char* node_name = (const char*)sqlite3_column_text(st, 1);
const char* local_nick = (const char*)sqlite3_column_text(st, 2);
const char* adm_tags = (const char*)sqlite3_column_text(st, 3);
@ -179,7 +185,7 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
" FROM node_addresses WHERE node_id IN (%s) AND rtt > 0 GROUP BY node_id", id_list);
u_free(id_list);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
int rtt_val = sqlite3_column_int(st, 1);
@ -222,12 +228,13 @@ int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count) {
/* ─── один мембер ─── */
int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* out) {
int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id, uint8_t* out) {
struct chat_core_ctx* cc = CC(inst);
if (!out) return -1;
if (!g_cc.initialized || !ch_id || !g_cc.db || !g_cc.inst) return -1;
if (!cc || !cc->initialized || !ch_id || !cc->db || !cc->inst) return -1;
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
uint64_t my_id = g_cc.my_node_id;
uint64_t my_id = cc->my_node_id;
sqlite3_stmt* st = NULL;
char sql[384];
@ -238,13 +245,13 @@ int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* ou
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" WHERE p.node_id=?", peers_tbl);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); return -1; }
struct chat_member_display m;
m.node_id = node_id;
m.online = (uint8_t)chat_core_member_online(ch_id, node_id);
m.online = (uint8_t)chat_core_member_online(inst, ch_id, node_id);
const char* node_name = (const char*)sqlite3_column_text(st, 0);
const char* local_nick = (const char*)sqlite3_column_text(st, 1);
const char* adm_tags = (const char*)sqlite3_column_text(st, 2);
@ -274,7 +281,7 @@ int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* ou
" WHEN MAX(addr_type=3) THEN 3"
" ELSE 0 END"
" FROM node_addresses WHERE node_id=? AND rtt > 0");
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
int rtt_val = sqlite3_column_int(st, 0);
@ -291,13 +298,14 @@ int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* ou
/* ─── трамплины для gui_bridge ─── */
void chat_core_request_member_list_trampoline(void* arg) {
if (!arg || !g_cc.initialized) { u_free(arg); return; }
char ch_id[64]; strncpy(ch_id, (const char*)arg, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0';
struct chat_member_list_req* req = (struct chat_member_list_req*)arg;
if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; }
char ch_id[64]; strncpy(ch_id, req->ch_id, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0';
u_free(arg);
uint8_t* members = NULL;
int count = 0;
if (chat_core_get_member_list(ch_id, &members, &count) != 0 || !members) return;
if (chat_core_get_member_list(req->inst, ch_id, &members, &count) != 0 || !members) return;
size_t ch_len = strlen(ch_id);
size_t evt_sz = 1 + ch_len + 2 + (size_t)count * CHAT_MEMBER_DISPLAY_SIZE;
@ -309,20 +317,21 @@ void chat_core_request_member_list_trampoline(void* arg) {
memcpy(evt + 1, ch_id, ch_len);
memcpy(evt + 1 + ch_len, &u16cnt, 2);
memcpy(evt + 1 + ch_len + 2, members, (size_t)count * CHAT_MEMBER_DISPLAY_SIZE);
chat_event_post(CHAT_EVT_MEMBER_LIST, evt, (int)evt_sz);
chat_event_post(req->inst, CHAT_EVT_MEMBER_LIST, evt, (int)evt_sz);
u_free(evt);
u_free(members);
}
void chat_core_request_member_rtt_trampoline(void* arg) {
if (!arg || !g_cc.initialized) { u_free(arg); return; }
char ch_id[64]; strncpy(ch_id, (const char*)arg, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0';
struct chat_member_list_req* req = (struct chat_member_list_req*)arg;
if (!req || !CC(req->inst) || !CC(req->inst)->initialized) { u_free(arg); return; }
char ch_id[64]; strncpy(ch_id, req->ch_id, sizeof(ch_id) - 1); ch_id[sizeof(ch_id) - 1] = '\0';
u_free(arg);
uint8_t* members = NULL;
int count = 0;
if (chat_core_get_member_list(ch_id, &members, &count) != 0 || !members) return;
if (chat_core_get_member_list(req->inst, ch_id, &members, &count) != 0 || !members) return;
size_t ch_len = strlen(ch_id);
size_t evt_sz = 1 + ch_len + 1 + CHAT_MEMBER_DISPLAY_SIZE;
@ -339,7 +348,7 @@ void chat_core_request_member_rtt_trampoline(void* arg) {
memcpy(evt + 1, ch_id, ch_len);
evt[1 + ch_len] = 1;
memcpy(evt + 1 + ch_len + 1, members + i * CHAT_MEMBER_DISPLAY_SIZE, CHAT_MEMBER_DISPLAY_SIZE);
chat_event_post(CHAT_EVT_MEMBER_UPDATED, evt, (int)evt_sz);
chat_event_post(req->inst, CHAT_EVT_MEMBER_UPDATED, evt, (int)evt_sz);
}
u_free(evt);
@ -349,14 +358,16 @@ void chat_core_request_member_rtt_trampoline(void* arg) {
/* ─── node_props_changed callback (BGP-события узла → MEMBER_UPDATED в GUI) ─── */
static void on_member_props_changed(uint64_t node_id, const char* adm_tags, const char* channel_id, void* arg) {
(void)adm_tags; (void)arg;
if (!g_cc.initialized || !g_cc.inst || !g_cc.db || !channel_id || !channel_id[0]) return;
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct chat_core_ctx* cc = CC(inst);
(void)adm_tags;
if (!cc || !cc->initialized || !cc->inst || !cc->db || !channel_id || !channel_id[0]) return;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256]; snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
sqlite3_finalize(st);
@ -365,15 +376,15 @@ static void on_member_props_changed(uint64_t node_id, const char* adm_tags, cons
evt[0] = (uint8_t)cl;
memcpy(evt + 1, channel_id, cl);
evt[1 + cl] = 1;
if (chat_core_get_single_member(channel_id, node_id, evt + 1 + cl + 1) == 0)
chat_event_post(CHAT_EVT_MEMBER_UPDATED, evt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE);
if (chat_core_get_single_member(inst, channel_id, node_id, evt + 1 + cl + 1) == 0)
chat_event_post(inst, CHAT_EVT_MEMBER_UPDATED, evt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE);
return;
}
sqlite3_finalize(st);
}
}
void chat_member_init(void) {
member_sync_add_props_cbk(on_member_props_changed, NULL);
void chat_member_init(struct UTUN_INSTANCE* inst) {
member_sync_add_props_cbk(inst, on_member_props_changed, inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: initialized (BGP node events → MEMBER_UPDATED)");
}

14
src/chat/chat_member.h

@ -30,18 +30,19 @@ extern "C" {
#define CHAT_MEMBER_FLAG_ADMIN 0x02
#define CHAT_MEMBER_FLAG_MODER 0x04
#define CHAT_MEMBER_FLAG_DELETED 0x08
#define CHAT_MEMBER_FLAG_DM_STORAGE 0x10
#define CHAT_MEMBER_DISPLAY_SIZE 79
/* Получить всех мемберов канала. out = u_malloc-массив, count = количество. */
int chat_core_get_member_list(const char* ch_id, uint8_t** out, int* count);
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count);
/* Получить одного мембера по node_id. out = буфер размером CHAT_MEMBER_DISPLAY_SIZE. */
int chat_core_get_single_member(const char* ch_id, uint64_t node_id, uint8_t* out);
int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id, uint8_t* out);
/* Live-онлайн: узел присутствует в BGP/topo_group канала (self всегда онлайн).
Не читает nodes.online — статус не хранится в БД. */
int chat_core_member_online(const char* ch_id, uint64_t node_id);
int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
/* Сериализовать из внутренней структуры в wire-формат */
struct chat_member_display {
@ -59,15 +60,16 @@ void chat_core_serialize_member(const struct chat_member_display* src, uint8_t*
int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display* dst);
/* Трамплин для gui_bridge: GUI запрашивает полный список мемберов канала.
* arg = вызов u_strdup(ch_id). Результат: CHAT_EVT_MEMBER_LIST. */
* arg = struct chat_member_list_req*. Результат: CHAT_EVT_MEMBER_LIST. */
struct chat_member_list_req { struct UTUN_INSTANCE* inst; char ch_id[64]; };
void chat_core_request_member_list_trampoline(void* arg);
/* Трамплин для gui_bridge: GUI запрашивает RTT мемберов канала.
* arg = вызов u_strdup(ch_id). Результат: CHAT_EVT_MEMBER_UPDATED с RTT-обновлениями. */
* arg = struct chat_member_list_req*. Результат: CHAT_EVT_MEMBER_UPDATED с RTT-обновлениями. */
void chat_core_request_member_rtt_trampoline(void* arg);
/* Инициализация: регистрирует node_props_changed callback для рассылки MEMBER_UPDATED. */
void chat_member_init(void);
void chat_member_init(struct UTUN_INSTANCE* inst);
#ifdef __cplusplus
}

434
src/chat/chat_msg.c

@ -27,24 +27,22 @@
chat_whisper_trigger_fn g_chat_whisper_trigger = NULL;
/* forward decl */
void chat_core_submit_media_message(struct chat_msg_submit* req);
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
static void wh_transcribe_done_cb(void* arg, const char* ch_id, const char* text, int err,
uint64_t reply_ts, uint64_t reply_node);
static void chat_media_cache_cleanup_orphans(void);
static void chat_media_cache_enforce(void);
static void chat_media_cache_reconcile(void);
static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst);
static void chat_media_cache_enforce(struct UTUN_INSTANCE* inst);
static void chat_media_cache_reconcile(struct UTUN_INSTANCE* inst);
/* ─── счётчик объёма медиакеша (сумма файлов, соответствующих сообщениям) ─── */
static uint64_t g_media_cache_bytes = 0;
static void cache_bytes_add(int64_t sz) {
if (sz > 0) g_media_cache_bytes += (uint64_t)sz;
static void cache_bytes_add(struct chat_core_ctx* cc, int64_t sz) {
if (sz > 0) cc->media_cache_bytes += (uint64_t)sz;
}
static void cache_bytes_sub(int64_t sz) {
static void cache_bytes_sub(struct chat_core_ctx* cc, int64_t sz) {
uint64_t v = sz > 0 ? (uint64_t)sz : 0;
g_media_cache_bytes = g_media_cache_bytes >= v ? g_media_cache_bytes - v : 0;
cc->media_cache_bytes = cc->media_cache_bytes >= v ? cc->media_cache_bytes - v : 0;
}
/* Извлекает имя файла из local_attrs: {"st":"fl","fp":"<filename>"} */
@ -61,11 +59,11 @@ static void extract_attrs_fp(const char* local_attrs, char* out, size_t out_sz)
/* ─── отправка сообщения (GUI → uasync) ─── */
static int msg_get_prev_chain_hash(const char* tbl, uint8_t out[32]) {
static int msg_get_prev_chain_hash(struct chat_core_ctx* cc, const char* tbl, uint8_t out[32]) {
char sql[128]; snprintf(sql, sizeof(sql),
"SELECT chain_hash FROM \"%s\" ORDER BY timestamp DESC LIMIT 1", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) { memset(out,0,32); return -1; }
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) { memset(out,0,32); return -1; }
if (sqlite3_step(st) == SQLITE_ROW) {
const void* blob = sqlite3_column_blob(st, 0); int blen = sqlite3_column_bytes(st, 0);
if (blob && blen == 32) memcpy(out, blob, 32); else memset(out, 0, 32);
@ -73,20 +71,21 @@ static int msg_get_prev_chain_hash(const char* tbl, uint8_t out[32]) {
sqlite3_finalize(st); return 0;
}
void chat_core_submit_message(struct chat_msg_submit* req) {
if (!g_cc.initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: submit before init", CC_ID); return; }
void chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: submit before init", CC_ID); return; }
if (!req) return;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: SUBMIT ch=%s ct=%s len=%u",
CC_ID, req->channel_id, req->content_type, req->data_len);
struct DB_SYNC_INSTANCE* si = si_find(req->channel_id);
struct DB_SYNC_INSTANCE* si = si_find(inst, req->channel_id);
if (!si) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: no db_sync instance for ch=%s", CC_ID, req->channel_id); return; }
char json[4096];
int off = snprintf(json, sizeof(json),
"{\"n\":%llu,\"ch\":\"%s\",\"ct\":\"%s\"",
(unsigned long long)g_cc.my_node_id, req->channel_id, req->content_type);
(unsigned long long)cc->my_node_id, req->channel_id, req->content_type);
if (req->reply_to_ts) {
off += snprintf(json + off, sizeof(json) - off,
",\"rt\":%llu,\"rn\":%llu",
@ -101,7 +100,7 @@ void chat_core_submit_message(struct chat_msg_submit* req) {
memcpy(sig_msg + soff, &ts, 8); soff += 8;
size_t jl = strlen(json); memcpy(sig_msg + soff, json, jl); soff += jl;
uint8_t sig[64];
if (sc_ed25519_sign(g_cc.inst->my_ed25519_privkey, sig_msg, soff, sig) != SC_OK) {
if (sc_ed25519_sign(inst->my_ed25519_privkey, sig_msg, soff, sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: Ed25519 sign failed", CC_ID);
return;
}
@ -115,16 +114,18 @@ void chat_core_submit_message(struct chat_msg_submit* req) {
void chat_core_submit_trampoline(void* arg) {
struct chat_msg_submit* req = (struct chat_msg_submit*)arg;
if (!req) return;
if (req->media_src[0] != '\0')
chat_core_submit_media_message(req);
chat_core_submit_media_message(req->inst, req);
else
chat_core_submit_message(req);
chat_core_submit_message(req->inst, req);
u_free(req);
}
/* ─── Медиа-сообщения: async регистрация в media_files + отправка ─── */
struct media_submit_ctx {
struct UTUN_INSTANCE* inst;
char channel_id[64];
char content_type[32];
char media_dest[1024];
@ -138,6 +139,7 @@ struct media_submit_ctx {
static void on_media_registered(void* arg, int err, const struct media_index_result* result) {
struct media_submit_ctx* mctx = (struct media_submit_ctx*)arg;
struct chat_core_ctx* cc = CC(mctx->inst);
if (err || !result) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media registration failed ch=%s err=%d",
@ -146,7 +148,7 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
return;
}
struct DB_SYNC_INSTANCE* si = si_find(mctx->channel_id);
struct DB_SYNC_INSTANCE* si = si_find(mctx->inst, mctx->channel_id);
if (!si) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: no db_sync instance for ch=%s", CC_ID, mctx->channel_id);
u_free(mctx);
@ -199,7 +201,7 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
if (!json) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: json alloc fail", CC_ID); u_free(full_data); u_free(mctx); return; }
snprintf(json, json_cap,
"{\"n\":%llu,\"ch\":\"%s\",\"ct\":\"%s\",\"d\":\"%.*s\"}",
(unsigned long long)g_cc.my_node_id, mctx->channel_id,
(unsigned long long)cc->my_node_id, mctx->channel_id,
mctx->content_type, (int)full_data_len, full_data);
uint64_t ts = db_sync_next_timestamp(si);
@ -209,7 +211,7 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
memcpy(sig_msg, &ts, 8);
memcpy(sig_msg + 8, json, sig_msg_len - 8);
uint8_t json_sig[64];
if (sc_ed25519_sign(g_cc.inst->my_ed25519_privkey, sig_msg, sig_msg_len, json_sig) != SC_OK) {
if (sc_ed25519_sign(mctx->inst->my_ed25519_privkey, sig_msg, sig_msg_len, json_sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: Ed25519 sign JSON failed", CC_ID);
u_free(sig_msg); u_free(json); u_free(full_data); u_free(mctx); return;
}
@ -223,23 +225,23 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
const char* base = strrchr(mctx->media_dest, '/');
const char* filename = base ? base + 1 : mctx->media_dest;
char attrs[1100]; snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", filename);
chat_core_update_local_attrs(mctx->channel_id, ts, json_sig, attrs);
chat_core_update_local_attrs(mctx->inst, mctx->channel_id, ts, json_sig, attrs);
/* анонсируем блоки суперузлам — иначе первый скачивающий не найдёт держателя */
media_delivery_announce_media(g_cc.inst, strtoull(mctx->channel_id, NULL, 10),
media_delivery_announce_media(mctx->inst, strtoull(mctx->channel_id, NULL, 10),
result->media_id, result->block_ids, result->num_blocks);
/* учесть локально отправленный файл в счётчике и применить лимит медиакеша */
cache_bytes_add(result->file_size);
chat_media_cache_enforce();
cache_bytes_add(cc, result->file_size);
chat_media_cache_enforce(mctx->inst);
/* whisper транскрипция для своих голосовых сообщений */
if (mctx->content_type[0] && strncmp(mctx->content_type, "voice", 5) == 0
&& g_cc.inst && g_cc.inst->media_async && g_chat_whisper_trigger) {
g_chat_whisper_trigger(g_cc.inst->media_async, g_cc.inst->ua,
&& mctx->inst && mctx->inst->media_async && g_chat_whisper_trigger) {
g_chat_whisper_trigger(mctx->inst, mctx->inst->media_async, mctx->inst->ua,
mctx->media_dest, mctx->channel_id,
ts, g_cc.my_node_id,
wh_transcribe_done_cb, NULL);
ts, cc->my_node_id,
wh_transcribe_done_cb, mctx->inst);
}
}
@ -248,6 +250,7 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
static void on_video_prepared(void* arg, int err) {
struct media_submit_ctx* mctx = (struct media_submit_ctx*)arg;
struct chat_core_ctx* cc = CC(mctx->inst);
if (err) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: video prepare failed ch=%s err=%d",
@ -268,28 +271,29 @@ static void on_video_prepared(void* arg, int err) {
char media_base[1024];
{
const char* last_slash = strrchr(g_cc.db_path, '/');
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash)
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else
snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
}
media_index_register_async(
g_cc.inst->media_async, g_cc.inst->ua, g_cc.db,
g_cc.my_node_id, g_cc.inst->my_ed25519_privkey,
mctx->inst->media_async, mctx->inst->ua, cc->db,
cc->my_node_id, mctx->inst->my_ed25519_privkey,
mctx->channel_id,
mctx->media_dest, mctx->media_dest, 0,
media_base,
on_media_registered, mctx);
}
void chat_core_submit_media_message(struct chat_msg_submit* req) {
if (!g_cc.initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media submit before init", CC_ID); return; }
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media submit before init", CC_ID); return; }
if (!req || req->media_src[0] == '\0') return;
/* лимит одного медиафайла применяется и к локальным отправкам */
uint64_t unit = g_cc.inst->config->global.chatserver_storage_unit_size;
uint64_t unit = inst->config->global.chatserver_storage_unit_size;
if (unit > 0) {
int64_t sz = (int64_t)ma_file_size(req->media_src);
if (sz > 0 && (uint64_t)sz > unit) {
@ -303,7 +307,7 @@ void chat_core_submit_media_message(struct chat_msg_submit* req) {
CC_ID, req->channel_id, req->content_type,
req->media_src, req->media_dest, req->media_copy, req->media_video);
if (!g_cc.inst->media_async) {
if (!inst->media_async) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media_async not initialized", CC_ID);
return;
}
@ -311,16 +315,17 @@ void chat_core_submit_media_message(struct chat_msg_submit* req) {
/* build media_base from db_path: dirname(db_path) e.g. /path/to/data */
char media_base[1024];
{
const char* last_slash = strrchr(g_cc.db_path, '/');
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash)
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else
snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
}
/* copy fields for async callback (req is freed by trampoline immediately) */
struct media_submit_ctx* mctx = u_calloc(1, sizeof(*mctx));
if (!mctx) return;
mctx->inst = inst;
snprintf(mctx->channel_id, sizeof(mctx->channel_id), "%s", req->channel_id);
snprintf(mctx->content_type, sizeof(mctx->content_type), "%s", req->content_type);
snprintf(mctx->media_dest, sizeof(mctx->media_dest), "%s", req->media_dest);
@ -334,14 +339,14 @@ void chat_core_submit_media_message(struct chat_msg_submit* req) {
}
if (req->media_video) {
video_transcode_start(g_cc.inst->ua, req->media_src, req->media_dest,
video_transcode_start(inst->ua, req->media_src, req->media_dest,
on_video_prepared, mctx);
return;
}
media_index_register_async(
g_cc.inst->media_async, g_cc.inst->ua, g_cc.db,
g_cc.my_node_id, g_cc.inst->my_ed25519_privkey,
inst->media_async, inst->ua, cc->db,
cc->my_node_id, inst->my_ed25519_privkey,
req->channel_id,
req->media_src, req->media_dest, req->media_copy ? 1 : 0,
media_base,
@ -350,11 +355,12 @@ void chat_core_submit_media_message(struct chat_msg_submit* req) {
/* ─── Обновление local_attrs (для будущей приёмной стороны) ─── */
int chat_core_update_local_attrs(const char* ch_id, uint64_t ts,
int chat_core_update_local_attrs(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts,
const uint8_t* author_sig,
const char* local_attrs_json)
{
if (!g_cc.initialized || !ch_id || !author_sig || !local_attrs_json) return -1;
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !author_sig || !local_attrs_json) return -1;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[256];
@ -362,8 +368,8 @@ int chat_core_update_local_attrs(const char* ch_id, uint64_t ts,
"UPDATE \"%s\" SET local_attrs=? WHERE timestamp=? AND author_signature=?", tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: update local_attrs prep fail: %s", CC_ID, sqlite3_errmsg(g_cc.db));
if (sqlite3_prepare_v2(cc->db, sql, -1, &stmt, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: update local_attrs prep fail: %s", CC_ID, sqlite3_errmsg(cc->db));
return -1;
}
sqlite3_bind_text(stmt, 1, local_attrs_json, -1, SQLITE_STATIC);
@ -381,16 +387,17 @@ int chat_core_update_local_attrs(const char* ch_id, uint64_t ts,
/* ─── Отметка голосового как проигранного ─── */
void chat_core_mark_voice_played(const char* ch_id, uint64_t ts, uint64_t author_node_id) {
if (!g_cc.initialized || !ch_id || ts == 0) return;
void chat_core_mark_voice_played(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts, uint64_t author_node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || ts == 0) return;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[256];
snprintf(sql, sizeof(sql),
"SELECT local_attrs, author_signature FROM \"%s\" WHERE timestamp=? AND node_id=? LIMIT 1", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: mark_voice_played prep fail: %s", CC_ID, sqlite3_errmsg(g_cc.db));
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: mark_voice_played prep fail: %s", CC_ID, sqlite3_errmsg(cc->db));
return;
}
sqlite3_bind_int64(st, 1, (sqlite3_int64)ts);
@ -419,25 +426,27 @@ void chat_core_mark_voice_played(const char* ch_id, uint64_t ts, uint64_t author
}
sqlite3_finalize(st);
chat_core_update_local_attrs(ch_id, ts, author_sig, new_attrs);
chat_core_update_local_attrs(inst, ch_id, ts, author_sig, new_attrs);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: voice marked played ch=%s ts=%llu", CC_ID, ch_id, (unsigned long long)ts);
}
void chat_core_mark_voice_played_trampoline(void* arg) {
struct voice_played_req* req = (struct voice_played_req*)arg;
chat_core_mark_voice_played(req->channel_id, req->ts, req->author_node_id);
if (!req) return;
chat_core_mark_voice_played(req->inst, req->channel_id, req->ts, req->author_node_id);
u_free(req);
}
/* ─── DB-операции для chat_sync ─── */
uint32_t chat_core_count(const char* ch_id) {
if (!g_cc.initialized) return 0;
uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) return 0;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[128]; snprintf(sql, sizeof(sql),
"SELECT COUNT(*) FROM \"%s\"", tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0;
if (sqlite3_prepare_v2(cc->db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0;
uint32_t cnt = 0;
if (sqlite3_step(stmt) == SQLITE_ROW)
cnt = (uint32_t)sqlite3_column_int64(stmt, 0);
@ -445,14 +454,15 @@ uint32_t chat_core_count(const char* ch_id) {
return cnt;
}
int chat_core_chain_hash_at(const char* ch_id, uint32_t pos, uint8_t* hash_out) {
if (!g_cc.initialized || !hash_out) return -1;
int chat_core_chain_hash_at(struct UTUN_INSTANCE* inst, const char* ch_id, uint32_t pos, uint8_t* hash_out) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !hash_out) return -1;
char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl));
char sql[200]; snprintf(sql, sizeof(sql),
"SELECT chain_hash FROM \"%s\" ORDER BY timestamp, node_id ASC LIMIT 1 OFFSET ?",
tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &stmt, NULL) != SQLITE_OK) {
memset(hash_out, 0, 32); return -1;
}
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)pos);
@ -468,10 +478,11 @@ int chat_core_chain_hash_at(const char* ch_id, uint32_t pos, uint8_t* hash_out)
return 0;
}
int chat_core_list_channels(uint8_t* buf, size_t buf_size, size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !buf || !out_len) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db,
if (sqlite3_prepare_v2(cc->db,
"SELECT channel_id FROM channels ORDER BY created_at ASC",
-1, &stmt, NULL) != SQLITE_OK) return -1;
@ -497,14 +508,15 @@ int chat_core_list_channels(uint8_t* buf, size_t buf_size, size_t* out_len) {
return 0;
}
int chat_core_list_peers(const char* ch_id, uint8_t* buf, size_t buf_size,
int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size,
size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !buf || !out_len) return -1;
char tbl[80]; peers_table_name(ch_id, tbl, sizeof(tbl));
char sql[128]; snprintf(sql, sizeof(sql),
"SELECT node_id FROM \"%s\" WHERE deleted=0", tbl);
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &stmt, NULL) != SQLITE_OK) {
uint16_t z = 0; memcpy(buf, &z, 2); *out_len = 2; return -1;
}
uint16_t cnt = 0;
@ -520,12 +532,13 @@ int chat_core_list_peers(const char* ch_id, uint8_t* buf, size_t buf_size,
return 0;
}
int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size,
int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size,
size_t* out_len) {
if (!g_cc.initialized || !buf || !out_len) return -1;
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !buf || !out_len) return -1;
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(g_cc.db,
if (sqlite3_prepare_v2(cc->db,
"SELECT x25519_pubkey, ed25519_pubkey FROM nodes WHERE node_id=?",
-1, &stmt, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id);
@ -544,7 +557,7 @@ int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size,
out += 32;
sqlite3_finalize(stmt);
if (sqlite3_prepare_v2(g_cc.db,
if (sqlite3_prepare_v2(cc->db,
"SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=?",
-1, &stmt, NULL) != SQLITE_OK) {
*out++ = 0; *out_len = (size_t)(out - buf); return 0;
@ -580,6 +593,7 @@ int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size,
/* ─── download completion ─── */
struct md_done_ctx {
struct UTUN_INSTANCE* inst;
char channel_id[64];
uint64_t ts;
int64_t msg_id;
@ -598,46 +612,47 @@ static void md_download_progress_cb(void* arg, int blocks_done, int num_blocks)
memcpy(evt + 1 + cl, &ctx->msg_id, 8);
uint32_t bd = (uint32_t)blocks_done, nb = (uint32_t)num_blocks;
memcpy(evt + 1 + cl + 8, &bd, 4); memcpy(evt + 1 + cl + 12, &nb, 4);
chat_event_post(CHAT_EVT_DOWNLOAD_PROGRESS, evt, 1 + cl + 16);
chat_event_post(ctx->inst, CHAT_EVT_DOWNLOAD_PROGRESS, evt, 1 + cl + 16);
}
static void md_download_done_cb(void* arg, int err) {
struct md_done_ctx* ctx = (struct md_done_ctx*)arg;
struct chat_core_ctx* cc = CC(ctx->inst);
if (!err) {
char attrs[1024];
snprintf(attrs, sizeof(attrs), "{\"st\":\"fl\",\"fp\":\"%s\"}", ctx->dest_relpath);
chat_core_update_local_attrs(ctx->channel_id, ctx->ts, ctx->author_sig, attrs);
chat_core_update_local_attrs(ctx->inst, ctx->channel_id, ctx->ts, ctx->author_sig, attrs);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: media downloaded ch=%s", CC_ID, ctx->channel_id);
/* whisper транскрипция для голосовых сообщений */
if (ctx->content_type[0] && strncmp(ctx->content_type, "voice", 5) == 0
&& g_cc.inst && g_cc.inst->media_async && g_chat_whisper_trigger) {
g_chat_whisper_trigger(g_cc.inst->media_async, g_cc.inst->ua,
&& ctx->inst && ctx->inst->media_async && g_chat_whisper_trigger) {
g_chat_whisper_trigger(ctx->inst, ctx->inst->media_async, ctx->inst->ua,
ctx->dest_abs_path, ctx->channel_id,
ctx->ts, ctx->author_node_id,
wh_transcribe_done_cb, NULL);
wh_transcribe_done_cb, ctx->inst);
}
/* после докачки — учесть файл в счётчике и применить лимит медиакеша */
cache_bytes_add(ma_file_size(ctx->dest_abs_path));
chat_media_cache_enforce();
cache_bytes_add(cc, ma_file_size(ctx->dest_abs_path));
chat_media_cache_enforce(ctx->inst);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: media download failed ch=%s err=%d", CC_ID, ctx->channel_id, err);
/* пометить сообщение ошибкой, чтобы UI показал «повторить» */
char attrs[64];
snprintf(attrs, sizeof(attrs), "{\"st\":\"er\"}");
chat_core_update_local_attrs(ctx->channel_id, ctx->ts, ctx->author_sig, attrs);
chat_core_update_local_attrs(ctx->inst, ctx->channel_id, ctx->ts, ctx->author_sig, attrs);
}
uint8_t evt[80]; uint8_t cl = (uint8_t)strlen(ctx->channel_id);
evt[0] = cl; memcpy(evt + 1, ctx->channel_id, cl);
memcpy(evt + 1 + cl, &ctx->msg_id, 8);
chat_event_post(CHAT_EVT_ATTACHMENT_DOWNLOADED, evt, 1 + cl + 8);
chat_event_post(ctx->inst, CHAT_EVT_ATTACHMENT_DOWNLOADED, evt, 1 + cl + 8);
u_free(ctx);
}
static void wh_transcribe_done_cb(void* arg, const char* ch_id, const char* text, int err,
uint64_t reply_ts, uint64_t reply_node) {
(void)arg;
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
if (err || !text || !text[0]) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: transcription failed ch=%s err=%d", CC_ID, ch_id, err);
return;
@ -645,17 +660,18 @@ static void wh_transcribe_done_cb(void* arg, const char* ch_id, const char* text
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: transcription result ch=%s: [%s]", CC_ID, ch_id, text);
/* mark original voice message as played */
chat_core_mark_voice_played(ch_id, reply_ts, reply_node);
chat_core_mark_voice_played(inst, ch_id, reply_ts, reply_node);
struct chat_msg_submit* req = u_calloc(1, sizeof(*req));
if (!req) return;
req->inst = inst;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch_id);
snprintf(req->content_type, sizeof(req->content_type), "voice_transcription");
req->data = (uint8_t*)text;
req->data_len = (uint32_t)strlen(text);
req->reply_to_ts = reply_ts;
req->reply_to_node_id = reply_node;
chat_core_submit_message(req);
chat_core_submit_message(inst, req);
u_free(req);
}
@ -758,6 +774,7 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
uint64_t author_node_id, int64_t msg_id,
const char* content_type) {
(void)data_len;
struct chat_core_ctx* cc = CC(inst);
struct media_index_result result;
if (chat_msg_parse_media_body(data_str, &result) != 0) return -1;
int nb = result.num_blocks;
@ -769,17 +786,18 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
char dest[1024], media_base[512];
{
const char* last_slash = strrchr(g_cc.db_path, '/');
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) {
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
} else {
snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
}
}
snprintf(dest, sizeof(dest), "%s/media/%s/%s", media_base, ch_id, final_name);
struct md_done_ctx* ctx = u_calloc(1, sizeof(*ctx));
if (!ctx) { media_index_result_free(&result); return -1; }
ctx->inst = inst;
snprintf(ctx->channel_id, sizeof(ctx->channel_id), "%s", ch_id);
ctx->ts = ts; ctx->msg_id = msg_id;
snprintf(ctx->dest_relpath, sizeof(ctx->dest_relpath), "%s", final_name);
@ -803,8 +821,8 @@ static int md_auto_download(struct UTUN_INSTANCE* inst,
uint64_t ts, const uint8_t* author_sig,
uint64_t author, int64_t msg_id,
const char* content_type) {
if (!chat_setting_get_int("storage_autoload", 1)) return 0;
uint64_t max_size = g_cc.inst->config->global.chatserver_storage_unit_size;
if (!chat_setting_get_int(inst, "storage_autoload", 1)) return 0;
uint64_t max_size = inst->config->global.chatserver_storage_unit_size;
const char* p = data_str;
while (*p && *p != '|') p++;
@ -822,13 +840,17 @@ static int md_auto_download(struct UTUN_INSTANCE* inst,
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg) {
(void)si;
const char* ch_id = (const char*)arg;
struct msg_insert_arg* ia = (struct msg_insert_arg*)arg;
struct UTUN_INSTANCE* inst = ia ? ia->inst : NULL;
struct chat_core_ctx* cc = CC(inst);
if (!ia || !cc) return;
const char* ch_id = ia->ch_id;
uint8_t evt[73]; uint8_t cl = (uint8_t)strlen(ch_id); evt[0] = cl; memcpy(evt + 1, ch_id, cl);
memcpy(evt + 1 + cl, &author, 8);
chat_event_post(CHAT_EVT_MSG_RECEIVED, evt, 1 + cl + 8);
chat_event_post(inst, CHAT_EVT_MSG_RECEIVED, evt, 1 + cl + 8);
/* auto-download media if message contains media metadata */
if (data && len > 0 && author != g_cc.my_node_id) {
if (data && len > 0 && author != cc->my_node_id) {
/* data from db_sync may not be null-terminated; copy to local buffer */
char buf[4096]; size_t blen = len < sizeof(buf) - 1 ? len : sizeof(buf) - 1;
memcpy(buf, data, blen); buf[blen] = '\0';
@ -851,12 +873,7 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char
char base_filename[256];
extract_base_filename(body, base_filename, sizeof(base_filename));
const uint8_t* sig_field = (const uint8_t*)strstr(buf, "\"sig\":\"");
uint8_t author_sig[64] = {0};
if (sig_field) {
/* read author_signature from DB — find by ts in on_msg_inserted we don't have sig
so leave as zeros; use record_ts to locate the record later */
}
/* We don't have author_sig in the callback; derive from body content.
Instead, read it from the DB using the just-inserted record */
@ -864,7 +881,7 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char
char sql[256];
snprintf(sql, sizeof(sql), "SELECT author_signature, id FROM \"%s\" WHERE timestamp=? AND node_id=? LIMIT 1", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)record_ts);
sqlite3_bind_int64(st, 2, (sqlite3_int64)author);
int64_t msg_id = 0;
@ -875,13 +892,13 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char
msg_id = sqlite3_column_int64(st, 1);
}
sqlite3_finalize(st);
if (msg_id > 0) md_auto_download(g_cc.inst, body, bi, ch_id, base_filename, record_ts, author_sig, author, msg_id, content_type);
if (msg_id > 0) md_auto_download(inst, body, bi, ch_id, base_filename, record_ts, author_sig, author, msg_id, content_type);
}
}
}
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(g_cc.db, "UPDATE channels SET last_msg_at = ? WHERE channel_id = ?", -1, &st, NULL);
sqlite3_prepare_v2(cc->db, "UPDATE channels SET last_msg_at = ? WHERE channel_id = ?", -1, &st, NULL);
if (st) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)record_ts);
sqlite3_bind_text(st, 2, ch_id, -1, SQLITE_STATIC);
@ -891,13 +908,14 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char
/* ─── manual attachment download (called from JNI bridge via uasync post) ─── */
void chat_core_attachment_download(const char* channel_id, int64_t msg_id) {
if (!channel_id || msg_id <= 0) return;
void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* channel_id, int64_t msg_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !channel_id || msg_id <= 0) return;
char tbl[80]; msg_table_name(channel_id, tbl, sizeof(tbl));
char sql[256];
snprintf(sql, sizeof(sql), "SELECT data, timestamp, author_signature, node_id FROM \"%s\" WHERE id=?", tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "%s: download sql prep failed ch=%s", CC_ID, channel_id);
return;
}
@ -945,12 +963,13 @@ void chat_core_attachment_download(const char* channel_id, int64_t msg_id) {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: manual download ch=%s id=%lld ct=%s file=%s",
CC_ID, channel_id, (long long)msg_id, content_type, base_filename);
md_start_download(g_cc.inst, body, bi, channel_id, base_filename, (uint64_t)db_ts, author_sig, (uint64_t)author_node_id, msg_id, content_type);
md_start_download(inst, body, bi, channel_id, base_filename, (uint64_t)db_ts, author_sig, (uint64_t)author_node_id, msg_id, content_type);
}
static void chat_core_attachment_download_trampoline_impl(void* arg) {
struct attachment_dl_req* req = (struct attachment_dl_req*)arg;
chat_core_attachment_download(req->channel_id, req->msg_id);
if (!req) return;
chat_core_attachment_download(req->inst, req->channel_id, req->msg_id);
u_free(req);
}
@ -958,22 +977,19 @@ void chat_core_attachment_download_trampoline(void* arg) {
chat_core_attachment_download_trampoline_impl(arg);
}
/* ─── Backfill: регистрация уже скачанных медиафайлов в media_files ───
* Файлы, скачанные старым билдом (до md_dl_register_servable), лежат на диске,
* но отсутствуют в media_files. При старте проходим по сообщениям каналов,
* парсим тело "d" (метаданные media_id/block_ids/content_hash уже там) и для
* существующих на диске файлов дорегистрируем блоки — узел снова может их отдавать. */
void chat_media_backfill(void) {
if (!g_cc.initialized || !g_cc.db) return;
/* ─── Backfill: регистрация уже скачанных медиафайлов в media_files ─── */
void chat_media_backfill(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db) return;
char media_base[512];
const char* last_slash = strrchr(g_cc.db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
else snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
int total = 0;
sqlite3_stmt* cs = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(cs, 0);
@ -983,7 +999,7 @@ void chat_media_backfill(void) {
char sql[256];
snprintf(sql, sizeof(sql), "SELECT data FROM \"%s\"", tbl);
sqlite3_stmt* ms = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
if (sqlite3_prepare_v2(cc->db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
while (sqlite3_step(ms) == SQLITE_ROW) {
const char* jdata = (const char*)sqlite3_column_text(ms, 0);
@ -1032,7 +1048,7 @@ void chat_media_backfill(void) {
for (int n = 0; n < result.num_blocks; n++) {
int exists = 0;
sqlite3_stmt* ex = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT 1 FROM media_files WHERE block_id=? LIMIT 1", -1, &ex, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, "SELECT 1 FROM media_files WHERE block_id=? LIMIT 1", -1, &ex, NULL) == SQLITE_OK) {
sqlite3_bind_blob(ex, 1, result.block_ids + n * 16, 16, SQLITE_STATIC);
exists = (sqlite3_step(ex) == SQLITE_ROW);
sqlite3_finalize(ex);
@ -1047,8 +1063,8 @@ void chat_media_backfill(void) {
int64_t chunk_size = (n == result.num_blocks - 1) ? result.file_size - (int64_t)n * result.block_size : result.block_size;
int64_t offset = (int64_t)n * result.block_size;
if (media_index_register_downloaded(g_cc.db, result.media_id, result.block_ids + n * 16,
result.content_hash, ch_id, rel, g_cc.my_node_id,
if (media_index_register_downloaded(cc->db, result.media_id, result.block_ids + n * 16,
result.content_hash, ch_id, rel, cc->my_node_id,
result.file_size, chunk_size, n, offset) == 0) {
registered++;
}
@ -1071,14 +1087,7 @@ void chat_media_backfill(void) {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: media backfill done, %d blocks registered", CC_ID, total);
}
/* ─── Докачка недостающего медиа + вычистка старых файлов (единый проход) ───
* Инвариант кеша: на диске лежат самые свежие storage_total_size байт медиа
* (по timestamp сообщения). Один проход DESC по дате: свежие недостающие —
* докачиваем (в пределах окна storage_backfill_days и бюджета), старые сверх
* бюджета — вычищаем. Фронт докачки идёт с нового конца, фронт вычистки — со
* старого; сходятся на границе бюджета (used упирается в limit). Счётчик
* g_media_cache_bytes корректируется на каждой вычистке; докачки добавляют
* через md_download_done_cb. Вызывается один раз после стартовой синхронизации. */
/* ─── Докачка недостающего медиа + вычистка старых файлов (единый проход) ─── */
struct bf_entry {
uint64_t ts_ms;
@ -1106,9 +1115,9 @@ static int bf_cmp_ts_desc(const void* a, const void* b) {
}
/* Собирает все медиасообщения всех каналов (presence + размеры + контекст докачки). */
static void bf_collect(const char* media_base, struct bf_list* l) {
static void bf_collect(struct chat_core_ctx* cc, const char* media_base, struct bf_list* l) {
sqlite3_stmt* cs = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(cs, 0);
@ -1119,7 +1128,7 @@ static void bf_collect(const char* media_base, struct bf_list* l) {
snprintf(sql, sizeof(sql),
"SELECT id, timestamp, node_id, data, author_signature, local_attrs FROM \"%s\"", tbl);
sqlite3_stmt* ms = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
if (sqlite3_prepare_v2(cc->db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
while (sqlite3_step(ms) == SQLITE_ROW) {
int64_t msg_id = sqlite3_column_int64(ms, 0);
@ -1196,28 +1205,29 @@ static void bf_collect(const char* media_base, struct bf_list* l) {
sqlite3_finalize(cs);
}
void chat_media_autodownload_backfill(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) return;
uint64_t limit = g_cc.inst->config->global.chatserver_storage_total_size;
int autoload = chat_setting_get_int("storage_autoload", 1);
int days = chat_setting_get_int("storage_backfill_days", 7);
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(g_cc.inst) * 1000ULL;
uint64_t limit = inst->config->global.chatserver_storage_total_size;
int autoload = chat_setting_get_int(inst, "storage_autoload", 1);
int days = chat_setting_get_int(inst, "storage_backfill_days", 7);
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(inst) * 1000ULL;
uint64_t cutoff_ms = now_ms - (uint64_t)days * 86400ULL * 1000ULL;
char media_base[512];
const char* last_slash = strrchr(g_cc.db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - g_cc.db_path), g_cc.db_path);
else snprintf(media_base, sizeof(media_base), "%s", g_cc.db_path);
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
struct bf_list list = {0};
bf_collect(media_base, &list);
bf_collect(cc, media_base, &list);
if (list.count > 1) qsort(list.items, (size_t)list.count, sizeof(struct bf_entry), bf_cmp_ts_desc);
/* инициализация счётчика: сумма всех present-файлов */
g_media_cache_bytes = 0;
cc->media_cache_bytes = 0;
for (int i = 0; i < list.count; i++)
if (list.items[i].actual_size > 0) g_media_cache_bytes += (uint64_t)list.items[i].actual_size;
if (list.items[i].actual_size > 0) cc->media_cache_bytes += (uint64_t)list.items[i].actual_size;
uint64_t used = 0;
int present = 0, downloaded = 0, evicted = 0, skipped = 0;
@ -1231,7 +1241,7 @@ void chat_media_autodownload_backfill(void) {
present++;
} else {
if (remove(e->full_path) == 0) {
cache_bytes_sub(e->actual_size);
cache_bytes_sub(cc, e->actual_size);
evicted++; evicted_bytes += (uint64_t)e->actual_size;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache evict: %s ts=%llu size=%lld",
CC_ID, e->full_path, (unsigned long long)e->ts_ms, (long long)e->actual_size);
@ -1240,10 +1250,10 @@ void chat_media_autodownload_backfill(void) {
}
}
} else {
if (!autoload || e->author == g_cc.my_node_id) { skipped++; continue; }
if (!autoload || e->author == cc->my_node_id) { skipped++; continue; }
if (e->ts_ms < cutoff_ms) { skipped++; continue; }
if (limit > 0 && used + (uint64_t)e->claimed_size > limit) { skipped++; continue; }
if (md_auto_download(g_cc.inst, e->body, e->body_len, e->ch_id, e->base_filename,
if (md_auto_download(inst, e->body, e->body_len, e->ch_id, e->base_filename,
e->ts_ms, e->author_sig, e->author, e->msg_id, e->content_type)) {
downloaded++;
used += (uint64_t)e->claimed_size;
@ -1255,18 +1265,15 @@ void chat_media_autodownload_backfill(void) {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache backfill done present=%d downloaded=%d skipped=%d evicted=%d evicted_bytes=%llu bytes=%llu limit=%llu",
CC_ID, present, downloaded, skipped, evicted, (unsigned long long)evicted_bytes,
(unsigned long long)g_media_cache_bytes, (unsigned long long)limit);
(unsigned long long)cc->media_cache_bytes, (unsigned long long)limit);
for (int i = 0; i < list.count; i++) if (list.items[i].body) u_free(list.items[i].body);
u_free(list.items);
chat_media_cache_reconcile();
chat_media_cache_reconcile(inst);
}
/* ─── Кеш медиафайлов: очистка сирот + лимит storage_total_size ───
* Возраст файла = timestamp исходного сообщения (msg_<ch>.timestamp).
* Сироты (файлы без сообщения, включая .chunk_* остатки) удаляются на старте.
* Имя файла на диске берётся из local_attrs "fp" (fallback — по имени из "d"). */
/* ─── Кеш медиафайлов: очистка сирот + лимит storage_total_size ─── */
struct media_msg_file {
char rel_path[1536]; // media/<ch_id>/<final_name>
@ -1276,17 +1283,17 @@ struct media_msg_file {
typedef void (*media_msg_file_cb)(const struct media_msg_file* f, void* arg);
static void chat_media_base(char* out, size_t out_sz) {
const char* ls = strrchr(g_cc.db_path, '/');
if (ls) snprintf(out, out_sz, "%.*s", (int)(ls - g_cc.db_path), g_cc.db_path);
else snprintf(out, out_sz, "%s", g_cc.db_path);
static void chat_media_base(struct chat_core_ctx* cc, char* out, size_t out_sz) {
const char* ls = strrchr(cc->db_path, '/');
if (ls) snprintf(out, out_sz, "%.*s", (int)(ls - cc->db_path), cc->db_path);
else snprintf(out, out_sz, "%s", cc->db_path);
}
/* Итератор по всем медиасообщениям всех каналов (без фильтра по node_id/времени). */
static void chat_media_foreach_media_message(const char* media_base, media_msg_file_cb cb, void* arg) {
if (!g_cc.db || !cb) return;
static void chat_media_foreach_media_message(struct chat_core_ctx* cc, const char* media_base, media_msg_file_cb cb, void* arg) {
if (!cc->db || !cb) return;
sqlite3_stmt* cs = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
if (sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL) != SQLITE_OK) return;
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(cs, 0);
@ -1296,7 +1303,7 @@ static void chat_media_foreach_media_message(const char* media_base, media_msg_f
char sql[256];
snprintf(sql, sizeof(sql), "SELECT timestamp, data, local_attrs FROM \"%s\"", tbl);
sqlite3_stmt* ms = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
if (sqlite3_prepare_v2(cc->db, sql, -1, &ms, NULL) != SQLITE_OK) continue;
while (sqlite3_step(ms) == SQLITE_ROW) {
uint64_t ts = (uint64_t)sqlite3_column_int64(ms, 0);
@ -1406,10 +1413,10 @@ static void cache_file_collect_cb(const struct media_msg_file* f, void* arg) {
}
/* Фактическая сумма размеров файлов, соответствующих сообщениям. */
static uint64_t chat_media_cache_scan_sum(void) {
char media_base[512]; chat_media_base(media_base, sizeof(media_base));
static uint64_t chat_media_cache_scan_sum(struct chat_core_ctx* cc) {
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
struct cache_file_list files = {0};
chat_media_foreach_media_message(media_base, cache_file_collect_cb, &files);
chat_media_foreach_media_message(cc, media_base, cache_file_collect_cb, &files);
uint64_t sum = 0;
for (int i = 0; i < files.count; i++)
if (files.files[i].size > 0) sum += (uint64_t)files.files[i].size;
@ -1418,17 +1425,18 @@ static uint64_t chat_media_cache_scan_sum(void) {
}
/* Сверка счётчика с фактической суммой: при расхождении — WARN + самокоррекция. */
static void chat_media_cache_reconcile(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
uint64_t actual = chat_media_cache_scan_sum();
if (actual != g_media_cache_bytes) {
static void chat_media_cache_reconcile(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) return;
uint64_t actual = chat_media_cache_scan_sum(cc);
if (actual != cc->media_cache_bytes) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: cache counter drift: counter=%llu actual=%llu diff=%lld",
CC_ID, (unsigned long long)g_media_cache_bytes, (unsigned long long)actual,
(long long)((int64_t)actual - (int64_t)g_media_cache_bytes));
g_media_cache_bytes = actual;
CC_ID, (unsigned long long)cc->media_cache_bytes, (unsigned long long)actual,
(long long)((int64_t)actual - (int64_t)cc->media_cache_bytes));
cc->media_cache_bytes = actual;
} else {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "%s: cache reconcile ok bytes=%llu",
CC_ID, (unsigned long long)g_media_cache_bytes);
CC_ID, (unsigned long long)cc->media_cache_bytes);
}
}
@ -1459,14 +1467,15 @@ static void orphan_walk_cb(void* arg, const char* full_path, int64_t size) {
}
}
static void chat_media_cache_cleanup_orphans(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) return;
char media_base[512]; chat_media_base(media_base, sizeof(media_base));
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
char media_dir[600]; snprintf(media_dir, sizeof(media_dir), "%s/media", media_base);
struct cache_rel_list rels = {0};
chat_media_foreach_media_message(media_base, cache_rel_collect_cb, &rels);
chat_media_foreach_media_message(cc, media_base, cache_rel_collect_cb, &rels);
if (rels.count > 1) qsort(rels.items, (size_t)rels.count, sizeof(char*), cache_str_cmp);
struct orphan_walk_ctx ow;
@ -1482,28 +1491,29 @@ static void chat_media_cache_cleanup_orphans(void) {
u_free(rels.items);
}
static void chat_media_cache_enforce(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
uint64_t limit = g_cc.inst->config->global.chatserver_storage_total_size;
static void chat_media_cache_enforce(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) return;
uint64_t limit = inst->config->global.chatserver_storage_total_size;
if (limit == 0 || g_media_cache_bytes <= limit) {
if (limit == 0 || cc->media_cache_bytes <= limit) {
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "%s: cache limit ok bytes=%llu limit=%llu",
CC_ID, (unsigned long long)g_media_cache_bytes, (unsigned long long)limit);
chat_media_cache_reconcile();
CC_ID, (unsigned long long)cc->media_cache_bytes, (unsigned long long)limit);
chat_media_cache_reconcile(inst);
return;
}
char media_base[512]; chat_media_base(media_base, sizeof(media_base));
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
struct cache_file_list files = {0};
chat_media_foreach_media_message(media_base, cache_file_collect_cb, &files);
chat_media_foreach_media_message(cc, media_base, cache_file_collect_cb, &files);
if (files.count > 1) qsort(files.files, (size_t)files.count, sizeof(struct cache_file), cache_file_cmp_ts);
int deleted = 0; uint64_t deleted_bytes = 0;
for (int i = 0; i < files.count && g_media_cache_bytes > limit; i++) {
for (int i = 0; i < files.count && cc->media_cache_bytes > limit; i++) {
if (files.files[i].size <= 0) continue;
if (remove(files.files[i].full_path) == 0) {
cache_bytes_sub(files.files[i].size);
cache_bytes_sub(cc, files.files[i].size);
deleted++; deleted_bytes += (uint64_t)files.files[i].size;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache evict: %s ts=%llu size=%lld",
CC_ID, files.files[i].full_path,
@ -1513,58 +1523,58 @@ static void chat_media_cache_enforce(void) {
}
}
if (g_media_cache_bytes > limit)
if (cc->media_cache_bytes > limit)
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: cache still over limit bytes=%llu limit=%llu",
CC_ID, (unsigned long long)g_media_cache_bytes, (unsigned long long)limit);
CC_ID, (unsigned long long)cc->media_cache_bytes, (unsigned long long)limit);
else
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache limit enforced bytes=%llu limit=%llu deleted=%d bytes=%llu",
CC_ID, (unsigned long long)g_media_cache_bytes, (unsigned long long)limit,
CC_ID, (unsigned long long)cc->media_cache_bytes, (unsigned long long)limit,
deleted, (unsigned long long)deleted_bytes);
u_free(files.files);
chat_media_cache_reconcile();
chat_media_cache_reconcile(inst);
}
/* ─── Триггер: после первого SYNC_DONE (+ дебаунс) — анонс локальных блоков + докачка ─── */
static void* g_media_startup_backfill_timer = NULL;
static int g_media_startup_backfill_ran = 0;
static struct UTUN_INSTANCE* g_media_startup_backfill_inst = NULL;
static void media_startup_backfill_timer_cb(void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
g_media_startup_backfill_timer = NULL;
g_media_startup_backfill_ran = 1;
struct chat_core_ctx* cc = CC(inst);
cc->media_backfill_timer = NULL;
cc->media_backfill_ran = 1;
/* 1) переанонс локальных блоков суперузлам (block_availability),
* 2) чистка сирот, 3) докачка недостающего + вычистка старого (единый проход) */
media_delivery_announce_local_blocks(inst);
chat_media_cache_cleanup_orphans();
chat_media_autodownload_backfill();
chat_media_cache_cleanup_orphans(inst);
chat_media_autodownload_backfill(inst);
}
static void media_startup_backfill_done_cb(struct DB_SYNC_INSTANCE* si, uint64_t peer_node_id, void* arg) {
(void)si; (void)peer_node_id; (void)arg;
if (g_media_startup_backfill_ran || !g_media_startup_backfill_inst) return;
struct UTUN_INSTANCE* inst = g_media_startup_backfill_inst;
if (g_media_startup_backfill_timer) uasync_cancel_timeout(inst->ua, g_media_startup_backfill_timer);
g_media_startup_backfill_timer = uasync_set_timeout(inst->ua, 20000, inst,
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct chat_core_ctx* cc = CC(inst);
(void)si; (void)peer_node_id;
if (!cc || cc->media_backfill_ran || !cc->media_backfill_inst) return;
if (cc->media_backfill_timer) uasync_cancel_timeout(inst->ua, cc->media_backfill_timer);
cc->media_backfill_timer = uasync_set_timeout(inst->ua, 20000, inst,
media_startup_backfill_timer_cb, "chat_media_bl");
}
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst) {
if (!inst) return;
g_media_startup_backfill_inst = inst;
g_media_startup_backfill_ran = 0;
db_sync_add_done_cbk(inst, media_startup_backfill_done_cb, NULL);
struct chat_core_ctx* cc = CC(inst);
if (!inst || !cc) return;
cc->media_backfill_inst = inst;
cc->media_backfill_ran = 0;
db_sync_add_done_cbk(inst, media_startup_backfill_done_cb, inst);
}
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst) {
if (!inst) return;
db_sync_remove_done_cbk(inst, media_startup_backfill_done_cb, NULL);
if (g_media_startup_backfill_timer) {
uasync_cancel_timeout(inst->ua, g_media_startup_backfill_timer);
g_media_startup_backfill_timer = NULL;
struct chat_core_ctx* cc = CC(inst);
if (!inst || !cc) return;
db_sync_remove_done_cbk(inst, media_startup_backfill_done_cb, inst);
if (cc->media_backfill_timer) {
uasync_cancel_timeout(inst->ua, cc->media_backfill_timer);
cc->media_backfill_timer = NULL;
}
g_media_startup_backfill_inst = NULL;
g_media_startup_backfill_ran = 0;
cc->media_backfill_inst = NULL;
cc->media_backfill_ran = 0;
}

71
src/chat/chat_profile.c

@ -23,36 +23,41 @@
#include <openssl/evp.h>
void chat_core_set_my_node_id(uint64_t node_id) {
g_cc.my_node_id = node_id;
void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
if (cc) cc->my_node_id = node_id;
}
void chat_core_update_my_name(const char* name) {
if (!g_cc.initialized || !name) return;
topo_node_sqlite_node_update_verified(g_cc.db, g_cc.my_node_id,
name, g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey,
(uint64_t)ntp_time_get_seconds(g_cc.inst), 0);
snprintf(g_cc.inst->name, sizeof(g_cc.inst->name), "%s", name);
chat_core_update_my_member();
void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !name) return;
topo_node_sqlite_node_update_verified(cc->db, cc->my_node_id,
name, inst->my_keys.public_key, inst->my_ed25519_pubkey,
(uint64_t)ntp_time_get_seconds(inst), 0);
snprintf(inst->name, sizeof(inst->name), "%s", name);
chat_core_update_my_member(inst);
}
void chat_core_update_my_name_trampoline(void* arg) {
chat_core_update_my_name((const char*)arg);
struct update_my_name_arg* a = (struct update_my_name_arg*)arg;
if (!a) return;
chat_core_update_my_name(a->inst, a->name);
u_free(arg);
}
/* ── Обновление своего мембера (userinfo + update_ts + подпись + синк).
Адреса больше не здесь — их синхронизирует BGP/topo_group. ── */
void chat_core_update_my_member(void) {
if (!g_cc.initialized || !g_cc.db || !g_cc.inst) return;
void chat_core_update_my_member(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) return;
uint64_t myid = g_cc.my_node_id;
const char* my_name = g_cc.inst->name[0] ? g_cc.inst->name : "";
uint64_t myid = cc->my_node_id;
const char* my_name = inst->name[0] ? inst->name : "";
char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", my_name);
sqlite3_stmt* cs = NULL;
sqlite3_prepare_v2(g_cc.db, "SELECT channel_id FROM channels", -1, &cs, NULL);
sqlite3_prepare_v2(cc->db, "SELECT channel_id FROM channels", -1, &cs, NULL);
if (!cs) return;
while (sqlite3_step(cs) == SQLITE_ROW) {
const char* ch = (const char*)sqlite3_column_text(cs, 0);
@ -60,26 +65,26 @@ void chat_core_update_my_member(void) {
char tbl[80]; peers_table_name(ch, tbl, sizeof(tbl));
uint8_t join_sig[64]; uint64_t join_ts = 0;
if (topo_node_sqlite_member_get_join(g_cc.db, ch, myid, join_sig, &join_ts) != 0) continue;
if (topo_node_sqlite_member_get_join(cc->db, ch, myid, join_sig, &join_ts) != 0) continue;
uint64_t local_uts = 0;
{ char q[256]; snprintf(q, sizeof(q), "SELECT update_ts FROM \"%s\" WHERE node_id=?", tbl);
sqlite3_stmt* ps = NULL;
if (sqlite3_prepare_v2(g_cc.db, q, -1, &ps, NULL) == SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, q, -1, &ps, NULL) == SQLITE_OK) {
sqlite3_bind_int64(ps, 1, (sqlite3_int64)myid);
if (sqlite3_step(ps) == SQLITE_ROW) local_uts = (uint64_t)sqlite3_column_int64(ps, 0);
sqlite3_finalize(ps);
}
}
uint64_t now = (uint64_t)ntp_time_get_seconds(g_cc.inst);
uint64_t now = (uint64_t)ntp_time_get_seconds(inst);
uint64_t update_ts = (now > local_uts) ? now : (local_uts + 1); /* строго больше локального */
uint8_t umsg[8192];
int ulen = member_sync_build_update_msg(join_sig, update_ts, juser, umsg, (int)sizeof(umsg));
if (ulen < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: build_update_msg failed ch=%s", CC_ID, ch); continue; }
uint8_t update_sig[64]; memset(update_sig, 0, 64);
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, g_cc.inst->my_ed25519_privkey, 32);
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, inst->my_ed25519_privkey, 32);
if (pkey) {
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (mdctx) {
@ -90,11 +95,11 @@ void chat_core_update_my_member(void) {
EVP_PKEY_free(pkey);
}
int r2 = member_sync_put(g_cc.inst, ch, myid, g_cc.inst->my_keys.public_key,
g_cc.inst->my_ed25519_pubkey, join_sig, join_ts,
int r2 = member_sync_put(inst, ch, myid, inst->my_keys.public_key,
inst->my_ed25519_pubkey, join_sig, join_ts,
update_sig, update_ts, juser, NULL, NULL, 0, 0, NULL);
if (r2 < 0) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d", CC_ID, ch, r2);
else if (r2 & MS_APPLY_CHANGED) member_sync_broadcast_one(g_cc.inst, ch, myid);
else if (r2 & MS_APPLY_CHANGED) member_sync_broadcast_one(inst, ch, myid);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: update my member ch=%s ts=%llu", CC_ID, ch,
(unsigned long long)update_ts);
@ -102,19 +107,17 @@ void chat_core_update_my_member(void) {
sqlite3_finalize(cs);
}
void chat_core_sync_my_addresses(void) {
/* Адреса синхронизируются BGP/topo_group через topo_node_update_my_addresses
(реестр + БД node_addresses + классификация node_type/storage + рассылка nodeinfo).
Вызывается после готовности БД, поэтому здесь гарантированно персистит свои адреса. */
if (g_cc.inst)
topo_node_update_my_addresses(g_cc.inst);
chat_core_update_my_member();
void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst) {
if (inst)
topo_node_update_my_addresses(inst);
chat_core_update_my_member(inst);
}
void chat_core_save_ui_state(const char* key, const char* value) {
if (!g_cc.initialized || !key || !value) return;
void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !key || !value) return;
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(g_cc.db,
sqlite3_prepare_v2(cc->db,
"INSERT OR REPLACE INTO ui_state(key, value) VALUES(?, ?)", -1, &st, NULL);
if (st) {
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
@ -123,11 +126,11 @@ void chat_core_save_ui_state(const char* key, const char* value) {
}
}
struct save_ui_state_arg { char data[256]; };
void chat_core_save_ui_state_trampoline(void* arg) {
struct save_ui_state_arg* a = (struct save_ui_state_arg*)arg;
if (!a) return;
const char* key = a->data;
const char* value = key + strlen(key) + 1;
chat_core_save_ui_state(key, value);
chat_core_save_ui_state(a->inst, key, value);
u_free(arg);
}

111
src/chat/chat_setting.c

@ -2,22 +2,19 @@
* chat_setting.c — chat-related settings: validation, storage, on-the-fly apply
*
* Используется и из парсера конфига (headless), и из GUI (через bridge).
* Никаких зависимостей от g_cc или instance — работает автономно.
* Состояние per-instance (struct chat_setting_state).
*/
#include "chat_setting.h"
#include "../utun_instance.h"
#include "../../lib/debug_config.h"
#ifdef DEBUG_CATEGORY_CHAT_SYNC
#define CS_ID DEBUG_CATEGORY_CHAT_SYNC
#else
#define CS_ID DEBUG_CATEGORY_GENERAL
#endif
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define CS_ID DEBUG_CATEGORY_CHAT_SYNC
/* ─── типы настроек ─── */
#define CHAT_SETTING_INT 0
@ -32,12 +29,6 @@ struct chat_setting_def {
int max_val;
};
/* хранилище для строковых значений */
#define CHAT_SETTING_STRING_MAX 16
static int g_chat_string_idx[CHAT_SETTING_STRING_MAX];
static char g_chat_string_val[CHAT_SETTING_STRING_MAX][256];
static int g_chat_string_count = 0;
/* ─── реестр всех известных настроек ─── */
static const struct chat_setting_def g_setting_defs[] = {
@ -59,38 +50,32 @@ static const struct chat_setting_def g_setting_defs[] = {
#define CHAT_SETTING_COUNT (sizeof(g_setting_defs) / sizeof(g_setting_defs[0]))
/* ─── состояние (текущие значения) ─── */
/* ─── state helpers ─── */
static int g_chat_values[CHAT_SETTING_COUNT];
static int g_chat_initialized = 0;
static void chat_setting_reset_defaults(void) {
static void chat_setting_reset_defaults(struct chat_setting_state* s) {
for (size_t i = 0; i < CHAT_SETTING_COUNT; i++)
g_chat_values[i] = g_setting_defs[i].default_val;
g_chat_initialized = 1;
s->values[i] = g_setting_defs[i].default_val;
s->initialized = 1;
}
/* ─── поиск ─── */
static int chat_setting_find(const char* name) {
for (size_t i = 0; i < CHAT_SETTING_COUNT; i++)
if (strcmp(g_setting_defs[i].name, name) == 0) return (int)i;
return -1;
}
/* ─── apply: вызов специфичного для настройки кода ─── */
static void chat_setting_apply(void) {
static void chat_setting_apply(struct chat_setting_state* s) {
(void)s;
/* будет заполняться по мере появления runtime-setter'ов в модулях:
if (idx == ...) some_module_set_xxx(value); */
}
/* ─── public API ─── */
/* ─── state-level API ─── */
int chat_setting_set(const char* name, const char* value) {
if (!name || !value || !*name || !*value) return -2;
int chat_setting_set_state(struct chat_setting_state* s, const char* name, const char* value) {
if (!s || !name || !value || !*name || !*value) return -2;
if (!g_chat_initialized) chat_setting_reset_defaults();
if (!s->initialized) chat_setting_reset_defaults(s);
int idx = chat_setting_find(name);
if (idx < 0) {
@ -102,25 +87,24 @@ int chat_setting_set(const char* name, const char* value) {
int v;
if (def->type == CHAT_SETTING_STRING) {
/* сохраняем строку в отдельном хранилище */
for (int i = 0; i < g_chat_string_count; i++) {
if (g_chat_string_idx[i] == idx) {
if (strcmp(g_chat_string_val[i], value) == 0) return 0;
snprintf(g_chat_string_val[i], sizeof(g_chat_string_val[i]), "%s", value);
for (int i = 0; i < s->string_count; i++) {
if (s->string_idx[i] == idx) {
if (strcmp(s->string_val[i], value) == 0) return 0;
snprintf(s->string_val[i], sizeof(s->string_val[i]), "%s", value);
DEBUG_INFO(CS_ID, "chat_setting: %s = %s", name, value);
chat_setting_apply();
chat_setting_apply(s);
return 0;
}
}
if (g_chat_string_count >= CHAT_SETTING_STRING_MAX) {
if (s->string_count >= CHAT_SETTING_STRING_MAX) {
DEBUG_ERROR(CS_ID, "chat_setting: string storage full for '%s'", name);
return -2;
}
g_chat_string_idx[g_chat_string_count] = idx;
snprintf(g_chat_string_val[g_chat_string_count], sizeof(g_chat_string_val[0]), "%s", value);
g_chat_string_count++;
s->string_idx[s->string_count] = idx;
snprintf(s->string_val[s->string_count], sizeof(s->string_val[0]), "%s", value);
s->string_count++;
DEBUG_INFO(CS_ID, "chat_setting: %s = %s", name, value);
chat_setting_apply();
chat_setting_apply(s);
return 0;
}
@ -138,34 +122,55 @@ int chat_setting_set(const char* name, const char* value) {
v = (int)lv;
}
if (g_chat_values[idx] == v) return 0;
if (s->values[idx] == v) return 0;
g_chat_values[idx] = v;
s->values[idx] = v;
DEBUG_INFO(CS_ID, "chat_setting: %s = %s", name, value);
chat_setting_apply();
chat_setting_apply(s);
return 0;
}
const char* chat_setting_get(const char* name) {
if (!g_chat_initialized) chat_setting_reset_defaults();
const char* chat_setting_get_state(const struct chat_setting_state* s, const char* name) {
if (!s || !s->initialized) return NULL;
int idx = chat_setting_find(name);
if (idx < 0) return NULL;
const struct chat_setting_def* def = &g_setting_defs[idx];
if (def->type == CHAT_SETTING_STRING) {
for (int i = 0; i < g_chat_string_count; i++) {
if (g_chat_string_idx[i] == idx) return g_chat_string_val[i];
for (int i = 0; i < s->string_count; i++) {
if (s->string_idx[i] == idx) return s->string_val[i];
}
return "";
}
static char buf[16];
snprintf(buf, sizeof(buf), "%d", g_chat_values[idx]);
return buf;
snprintf((char*)s->int_buf, sizeof(s->int_buf), "%d", s->values[idx]);
return s->int_buf;
}
int chat_setting_get_int(const char* name, int def) {
if (!g_chat_initialized) chat_setting_reset_defaults();
int chat_setting_get_int_state(const struct chat_setting_state* s, const char* name, int def) {
if (!s || !s->initialized) return def;
int idx = chat_setting_find(name);
return idx >= 0 ? g_chat_values[idx] : def;
return idx >= 0 ? s->values[idx] : def;
}
/* ─── instance-level API ─── */
static struct chat_setting_state* cs_state(struct UTUN_INSTANCE* inst) {
return inst ? &inst->chat_settings : NULL;
}
int chat_setting_set(struct UTUN_INSTANCE* inst, const char* name, const char* value) {
struct chat_setting_state* s = cs_state(inst);
if (!s) return -2;
return chat_setting_set_state(s, name, value);
}
const char* chat_setting_get(struct UTUN_INSTANCE* inst, const char* name) {
struct chat_setting_state* s = cs_state(inst);
return s ? chat_setting_get_state(s, name) : NULL;
}
int chat_setting_get_int(struct UTUN_INSTANCE* inst, const char* name, int def) {
struct chat_setting_state* s = cs_state(inst);
return s ? chat_setting_get_int_state(s, name, def) : def;
}

37
src/chat/chat_setting.h

@ -4,23 +4,48 @@
* Один API для всех потребителей: парсер конфига (headless) и GUI (через bridge).
* chat_setting_set(name, value) — валидация + хранение + apply к модулям.
* Возвращает 0=OK, -1=неизвестная настройка, -2=невалидное значение.
*
* Состояние — per-instance. Конфиг-парсер заполняет struct chat_setting_state
* из [chatserver] ещё до создания instance (state-уровень), затем runtime-модули
* читают через instance-уровень (chat_setting_get_int(inst, ...)).
*/
#ifndef CHAT_SETTING_H
#define CHAT_SETTING_H
#include <stdint.h>
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
/* Установить настройку. Применяет на лету если модуль уже инициализирован. */
int chat_setting_set(const char* name, const char* value);
/* ─── per-instance хранилище ─── */
#define CHAT_SETTING_MAX_SETTINGS 32
#define CHAT_SETTING_STRING_MAX 16
struct chat_setting_state {
int values[CHAT_SETTING_MAX_SETTINGS];
int string_idx[CHAT_SETTING_STRING_MAX];
char string_val[CHAT_SETTING_STRING_MAX][256];
int string_count;
int initialized;
char int_buf[16]; /* буфер для chat_setting_get_state у int-настроек */
};
/* ─── state-уровень (конфиг-парсер, до создания instance) ─── */
int chat_setting_set_state(struct chat_setting_state* s, const char* name, const char* value);
const char* chat_setting_get_state(const struct chat_setting_state* s, const char* name);
int chat_setting_get_int_state(const struct chat_setting_state* s, const char* name, int def);
/* Прочитать сохранённое значение. NULL если такой настройки нет. */
const char* chat_setting_get(const char* name);
/* ─── instance-уровень (runtime) ─── */
/* Прочитать как int. Возвращает def если настройка не найдена. */
int chat_setting_get_int(const char* name, int def);
int chat_setting_set(struct UTUN_INSTANCE* inst, const char* name, const char* value);
const char* chat_setting_get(struct UTUN_INSTANCE* inst, const char* name);
int chat_setting_get_int(struct UTUN_INSTANCE* inst, const char* name, int def);
#ifdef __cplusplus
}

90
src/chat/chat_status.c

@ -24,11 +24,12 @@ static const char* nat_type_str(uint8_t t) {
switch (t) { case 0: return "UNKNOWN"; case 1: return "EIM"; case 2: return "STRICT"; case 3: return "DIRECT"; default: return "?"; }
}
static void get_node_name(uint64_t node_id, char* out, size_t sz) {
static void get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz) {
struct chat_core_ctx* cc = CC(inst);
out[0] = '\0';
if (!g_cc.db || node_id == 0) return;
if (!cc || !cc->db || node_id == 0) return;
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return;
if (sqlite3_prepare_v2(cc->db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* n = (const char*)sqlite3_column_text(st, 0);
@ -37,16 +38,16 @@ static void get_node_name(uint64_t node_id, char* out, size_t sz) {
sqlite3_finalize(st);
}
static void chat_core_collect_status(void) {
static void chat_core_collect_status(struct UTUN_INSTANCE* inst) {
char buf[8192]; int off = 0;
if (!g_cc.initialized || !g_cc.inst) {
if (!CC(inst) || !CC(inst)->initialized || !inst) {
off = snprintf(buf, sizeof(buf), "uTun not initialized\n");
chat_event_post(CHAT_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
return;
}
struct NTP_TIME* ntp = &g_cc.inst->ntp;
struct NTP_TIME* ntp = &inst->ntp;
off += snprintf(buf + off, sizeof(buf) - off, "=== NTP ===\n");
off += snprintf(buf + off, sizeof(buf) - off, "Enabled: %s\n", ntp->enabled ? "yes" : "no");
off += snprintf(buf + off, sizeof(buf) - off, "Synced: %s\n", ntp->synced ? "yes" : "no");
@ -64,12 +65,12 @@ static void chat_core_collect_status(void) {
}
int conn_count = 0;
struct ll_entry* entry = g_cc.inst->connections->head;
struct ll_entry* entry = inst->connections ? inst->connections->head : NULL;
while (entry) { conn_count++; entry = entry->next; }
off += snprintf(buf + off, sizeof(buf) - off, "=== Connections (%d) ===\n", conn_count);
uint64_t my_id = g_cc.inst->node_id;
entry = g_cc.inst->connections->head;
uint64_t my_id = inst->node_id;
entry = inst->connections ? inst->connections->head : NULL;
while (entry) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (!ce || !ce->conn) { entry = entry->next; continue; }
@ -88,7 +89,7 @@ static void chat_core_collect_status(void) {
snprintf(peerhex, sizeof(peerhex), "%04llX", (unsigned long long)(pid & 0xFFFF));
char peername[64];
get_node_name(pid, peername, sizeof(peername));
get_node_name(inst, pid, peername, sizeof(peername));
if (peername[0])
off += snprintf(buf + off, sizeof(buf) - off, "[%s]→[%s] %s ETCP:%s%s(%dL)\n",
@ -117,18 +118,18 @@ static void chat_core_collect_status(void) {
entry = entry->next;
}
chat_event_post(CHAT_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_STATUS_REFRESH, (const uint8_t*)buf, off);
}
/* ── Бинарный список соединений: [count:2][entry:46B]* ── */
/* entry: peer_node_id:8 flags:1 link_count:1 rtt:2 inflight_kb:2 name:32 */
#define CONN_LIST_ENTRY_SIZE 46
static void collect_conn_list(void) {
if (!g_cc.initialized || !g_cc.inst || !g_cc.inst->connections) return;
static void collect_conn_list(struct UTUN_INSTANCE* inst) {
if (!CC(inst) || !CC(inst)->initialized || !inst || !inst->connections) return;
uint16_t count = 0;
struct ll_entry* entry = g_cc.inst->connections->head;
struct ll_entry* entry = inst->connections->head;
while (entry) { count++; entry = entry->next; }
if (count > 250) count = 250;
@ -140,7 +141,7 @@ static void collect_conn_list(void) {
*hdr = count;
uint8_t* p = buf + 2;
entry = g_cc.inst->connections->head;
entry = inst->connections->head;
for (uint16_t i = 0; i < count && entry; i++, entry = entry->next) {
struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data;
if (!ce || !ce->conn) { memset(p, 0, CONN_LIST_ENTRY_SIZE); p += CONN_LIST_ENTRY_SIZE; continue; }
@ -169,41 +170,41 @@ static void collect_conn_list(void) {
memcpy(p, &inflight_kb, 2); p += 2;
char name[32]; memset(name, 0, 32);
get_node_name(ce->peer_node_id, name, 31);
get_node_name(inst, ce->peer_node_id, name, 31);
if (!name[0]) snprintf(name, 32, "%016llX", (unsigned long long)ce->peer_node_id);
memcpy(p, name, 32); p += 32;
}
chat_event_post(CHAT_EVT_CONN_LIST, buf, (int)buf_sz);
chat_event_post(inst, CHAT_EVT_CONN_LIST, buf, (int)buf_sz);
u_free(buf);
}
/* ── Полный текстовый дамп одного соединения ── */
static void collect_conn_metrics(uint64_t peer_node_id) {
static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_id) {
char buf[8192]; int off = 0;
if (!g_cc.initialized || !g_cc.inst || !g_cc.inst->connections) {
if (!CC(inst) || !CC(inst)->initialized || !inst || !inst->connections) {
off = snprintf(buf, sizeof(buf), "uTun not initialized\n");
chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
return;
}
struct ll_entry* e = queue_find_data_by_index(g_cc.inst->connections, (const uint8_t*)&peer_node_id);
struct ll_entry* e = queue_find_data_by_index(inst->connections, (const uint8_t*)&peer_node_id);
if (!e) {
off = snprintf(buf, sizeof(buf), "Connection %016llX not found\n", (unsigned long long)peer_node_id);
chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
return;
}
struct conn_queue_entry* ce = (struct conn_queue_entry*)e->data;
if (!ce || !ce->conn) {
off = snprintf(buf, sizeof(buf), "Connection %016llX has null data\n", (unsigned long long)peer_node_id);
chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
return;
}
struct ETCP_CONN* conn = ce->conn;
uint64_t my_id = g_cc.inst->node_id;
uint64_t my_id = inst->node_id;
char peername[64]; get_node_name(peer_node_id, peername, sizeof(peername));
char peername[64]; get_node_name(inst, peer_node_id, peername, sizeof(peername));
if (!peername[0]) snprintf(peername, sizeof(peername), "%016llX", (unsigned long long)peer_node_id);
int is_server = conn->links ? conn->links->is_server : -1;
@ -284,24 +285,25 @@ static void collect_conn_metrics(uint64_t peer_node_id) {
link = link->next; link_idx++;
}
chat_event_post(CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
chat_event_post(inst, CHAT_EVT_CONN_METRICS, (const uint8_t*)buf, off);
}
void chat_core_collect_status_trampoline(void* arg) {
(void)arg;
chat_core_collect_status();
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
chat_core_collect_status(inst);
}
void chat_core_collect_conn_list_trampoline(void* arg) {
(void)arg;
collect_conn_list();
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
collect_conn_list(inst);
}
void chat_core_collect_conn_metrics_trampoline(void* arg) {
uint64_t peer_id;
memcpy(&peer_id, arg, sizeof(peer_id));
struct chat_node_arg* a = (struct chat_node_arg*)arg;
if (!a) return;
uint64_t peer_id = a->node_id;
collect_conn_metrics(a->inst, peer_id);
u_free(arg);
collect_conn_metrics(peer_id);
}
/* ── Live-снапшот для панели деталей участника ──
@ -334,9 +336,8 @@ static void sock_ifname(const char* name, char* out, size_t out_sz) {
out[len] = '\0';
}
static void collect_member_detail(uint64_t node_id) {
if (!g_cc.initialized || !g_cc.inst) return;
struct UTUN_INSTANCE* inst = g_cc.inst;
static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!CC(inst) || !CC(inst)->initialized || !inst) return;
uint8_t flags = 0;
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
@ -469,7 +470,7 @@ static void collect_member_detail(uint64_t node_id) {
memcpy(p, ifname, 38); p += 38;
}
chat_event_post(CHAT_EVT_MEMBER_DETAIL, buf, (int)(p - buf));
chat_event_post(inst, CHAT_EVT_MEMBER_DETAIL, buf, (int)(p - buf));
u_free(buf);
/* summary text: direct RTT or BGP */
@ -478,7 +479,7 @@ static void collect_member_detail(uint64_t node_id) {
if (rtt > 0) {
char tbuf[32];
int off = snprintf(tbuf, sizeof(tbuf), "direct, rtt:%ums", rtt / 10);
chat_event_post(CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
chat_event_post(inst, CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
}
} else if (conn && !conn->links_up && node_id != inst->node_id) {
struct ll_queue* gl = inst->topo_groups ? inst->topo_groups->group_list : NULL;
@ -495,13 +496,13 @@ static void collect_member_detail(uint64_t node_id) {
int off = snprintf(tbuf, sizeof(tbuf), "rtt:%ums hop:%u via bgp:", cr / 10, hc);
for (uint8_t hi = 0; hi < hc && off < (int)sizeof(tbuf) - 24; hi++) {
char hn[32] = {0};
get_node_name(hops[hi], hn, sizeof(hn));
get_node_name(inst, hops[hi], hn, sizeof(hn));
uint16_t hid = (uint16_t)(hops[hi] & 0xFFFF);
off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off,
" [%04X]%s", hid, hn[0] ? " " : "");
if (hn[0]) off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off, "%s", hn);
}
chat_event_post(CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
chat_event_post(inst, CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
}
break;
}
@ -509,8 +510,9 @@ static void collect_member_detail(uint64_t node_id) {
}
void chat_core_collect_member_detail_trampoline(void* arg) {
uint64_t node_id;
memcpy(&node_id, arg, sizeof(node_id));
struct chat_node_arg* a = (struct chat_node_arg*)arg;
if (!a) return;
uint64_t node_id = a->node_id;
collect_member_detail(a->inst, node_id);
u_free(arg);
collect_member_detail(node_id);
}

385
src/chat/chat_sync.c

@ -35,7 +35,9 @@
#include <android/log.h>
#endif
static struct chat_sync* g_cs = NULL;
static inline struct chat_sync* cs_of(struct UTUN_INSTANCE* inst) {
return inst ? inst->chat_sync : NULL;
}
struct channel_cache {
char channel_id[64];
@ -194,7 +196,8 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
}
return;
}
if (!g_cs || !g_cs->initialized) { u_free(entry->dgram); queue_entry_free(entry); return; }
struct chat_sync* cs = cs_of(conn->instance);
if (!cs || !cs->initialized) { u_free(entry->dgram); queue_entry_free(entry); return; }
uint64_t peer = conn ? conn->peer_node_id : 0;
const uint8_t* d = entry->dgram;
@ -203,7 +206,7 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
uint64_t group_id = be64toh(*(const uint64_t*)(d + 1));
uint8_t type = d[9];
char ch_id[64];
if (cs_group_id_to_ch_id(g_cs->inst, group_id, ch_id, sizeof(ch_id)) != 0) {
if (cs_group_id_to_ch_id(cs->inst, group_id, ch_id, sizeof(ch_id)) != 0) {
/* CHANNEL_INVITE/JOIN_INFO_REQ приходят для ещё не созданного у нас канала:
group_id == channel_id (numeric), поэтому выводим ch_id напрямую. */
if (type == CS_MSG_CHANNEL_INVITE || type == CS_MSG_JOIN_INFO_REQ || type == CS_MSG_JOIN_REQUEST) {
@ -221,19 +224,19 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
CS_ID, cs_msg_name(type), (unsigned long long)peer, ch_id, plen);
switch (type) {
case CS_MSG_JOIN_INFO_REQ: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_REQ", CS_ID); cs_handle_join_info_req(g_cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_INFO_RESP: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_RESP", CS_ID); cs_handle_join_info_resp(g_cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_REQUEST: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_REQUEST", CS_ID); cs_handle_join_request(g_cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_READY: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_READY", CS_ID); cs_handle_join_ready(g_cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_INFO_REQ: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_REQ", CS_ID); cs_handle_join_info_req(cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_INFO_RESP: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_INFO_RESP", CS_ID); cs_handle_join_info_resp(cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_REQUEST: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_REQUEST", CS_ID); cs_handle_join_request(cs, peer, ch_id, pl, plen); break;
case CS_MSG_JOIN_READY: DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: → handle JOIN_READY", CS_ID); cs_handle_join_ready(cs, peer, ch_id, pl, plen); break;
case CS_MSG_ERROR: {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: RECV ERROR from=%016llx ch=%s", CS_ID, (unsigned long long)peer, ch_id);
if (g_cs->info_req_timer) { uasync_cancel_timeout(g_cs->inst->ua, g_cs->info_req_timer); g_cs->info_req_timer = NULL; }
if (cs->info_req_timer) { uasync_cancel_timeout(cs->inst->ua, cs->info_req_timer); cs->info_req_timer = NULL; }
uint8_t err[20]; memcpy(err, &peer, 8); int r = -1; memcpy(err + 8, &r, 4);
memcpy(err + 12, &g_cs->pending_invite_ch_id, 8); chat_event_post(CHAT_EVT_CONNECT_RESULT, err, 20);
g_cs->pending_invite_node_id = 0; g_cs->pending_invite_ch_id = 0;
memcpy(err + 12, &cs->pending_invite_ch_id, 8); chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0;
break;
}
case CS_MSG_CHANNEL_INVITE: cs_handle_channel_invite(g_cs, peer, ch_id, pl, plen); break;
case CS_MSG_CHANNEL_INVITE: cs_handle_channel_invite(cs, peer, ch_id, pl, plen); break;
default: DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: UNKNOWN msg type=%02x from=%016llx", CS_ID, type, (unsigned long long)peer); break;
}
u_free(entry->dgram); queue_entry_free(entry);
@ -251,7 +254,7 @@ static void cs_info_req_timeout_cb(void* arg) {
uint8_t err[20]; memcpy(err, &cs->pending_invite_node_id, 8);
int r = 2; memcpy(err + 8, &r, 4); /* timeout */
memcpy(err + 12, &cs->pending_invite_ch_id, 8);
chat_event_post(CHAT_EVT_CONNECT_RESULT, err, 20);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
}
@ -266,7 +269,7 @@ static void cs_join_timeout_cb(void* arg) {
uint8_t err[20]; memcpy(err, &peer, 8);
int r = 2; memcpy(err + 8, &r, 4); /* timeout */
memcpy(err + 12, &cs->pending_invite_ch_id, 8);
chat_event_post(CHAT_EVT_CONNECT_RESULT, err, 20);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
@ -282,7 +285,7 @@ static void cs_invite_send_timeout_cb(void* arg) {
uint8_t err[20]; memcpy(err, &peer, 8);
int r = 2; memcpy(err + 8, &r, 4); /* timeout */
memcpy(err + 12, &cs->pending_invite_ch_id, 8);
chat_event_post(CHAT_EVT_CONNECT_RESULT, err, 20);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, err, 20);
cs->pending_invite_node_id = 0;
cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
@ -350,7 +353,7 @@ static void cs_post_channel_online(struct chat_sync* cs, const char* ch_id) {
uint8_t evt[66]; evt[0] = (uint8_t)cl;
memcpy(evt + 1, ch_id, cl);
uint16_t oc = (uint16_t)online; memcpy(evt + 1 + cl, &oc, 2);
chat_event_post(CHAT_EVT_CHANNEL_PEERS_ONLINE, evt, 1 + (int)cl + 2);
chat_event_post(cs->inst, CHAT_EVT_CHANNEL_PEERS_ONLINE, evt, 1 + (int)cl + 2);
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: PEERS_ONLINE ch=%.*s total_online=%d", CS_ID, (int)cl, ch_id, online);
}
@ -367,7 +370,7 @@ static void cs_flush_sync(struct chat_sync* cs) {
if (pid == myid || !cs_is_peer_online(cs->inst, pid)) continue;
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: flush_sync start for peer=%016llx ch=%s",
CS_ID, (unsigned long long)pid, ch->channel_id);
member_sync_start(cs->inst, pid, ch->channel_id, _on_member_sync_done, NULL);
member_sync_start(cs->inst, pid, ch->channel_id, _on_member_sync_done, cs);
}
}
}
@ -391,52 +394,53 @@ static void cs_schedule_sync(struct chat_sync* cs) {
/* ── Peer status change (GUI + online-бар + schedule_sync; онлайн в БД не хранится) ── */
static void cs_on_peer_status_changed(uint64_t peer, int online) {
static void cs_on_peer_status_changed(struct chat_sync* cs, uint64_t peer, int online) {
/* 1. GUI + online-бар */
int found_in_channel = 0;
for (int i = 0; i < g_cs->channel_count; i++) {
for (int j = 0; j < g_cs->channels[i].peer_count; j++) {
if (g_cs->channels[i].peer_ids[j] == peer) {
for (int i = 0; i < cs->channel_count; i++) {
for (int j = 0; j < cs->channels[i].peer_count; j++) {
if (cs->channels[i].peer_ids[j] == peer) {
found_in_channel = 1;
const char* ch_id = g_cs->channels[i].channel_id;
const char* ch_id = cs->channels[i].channel_id;
size_t cl = strlen(ch_id);
uint8_t mevt[1 + 64 + 1 + CHAT_MEMBER_DISPLAY_SIZE];
mevt[0] = (uint8_t)cl;
memcpy(mevt + 1, ch_id, cl);
mevt[1 + cl] = 1;
if (chat_core_get_single_member(ch_id, peer, mevt + 1 + cl + 1) == 0)
chat_event_post(CHAT_EVT_MEMBER_UPDATED, mevt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE);
cs_post_channel_online(g_cs, ch_id);
if (chat_core_get_single_member(cs->inst, ch_id, peer, mevt + 1 + cl + 1) == 0)
chat_event_post(cs->inst, CHAT_EVT_MEMBER_UPDATED, mevt, 1 + (int)cl + 1 + CHAT_MEMBER_DISPLAY_SIZE);
cs_post_channel_online(cs, ch_id);
break;
}
}
}
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: peer_status_changed peer=0x%016llx online=%d channels=%d found=%d",
CS_ID, (unsigned long long)peer, online, g_cs->channel_count, found_in_channel);
CS_ID, (unsigned long long)peer, online, cs->channel_count, found_in_channel);
/* 2. Запланировать member_sync_start (throttled) */
cs_schedule_sync(g_cs);
cs_schedule_sync(cs);
}
static void cs_resume_db_sync(const char* ch_id) {
if (!g_cs || !ch_id || !ch_id[0]) return;
struct DB_SYNC_INSTANCE* si = si_find(ch_id);
static void cs_resume_db_sync(struct chat_sync* cs, const char* ch_id) {
if (!cs || !ch_id || !ch_id[0]) return;
struct DB_SYNC_INSTANCE* si = si_find(cs->inst, ch_id);
if (si) db_sync_instance_set_gated(si, 0);
}
static void _on_member_sync_done(uint64_t peer, const char* ns, int result, void* arg) {
(void)arg;
struct chat_sync* cs = (struct chat_sync*)arg;
if (result == MT_OK) {
struct channel_cache* ch = g_cs ? cs_find(g_cs, ns) : NULL;
struct channel_cache* ch = cs ? cs_find(cs, ns) : NULL;
if (ch) ch->synced = CS_SYNC_DONE;
cs_resume_db_sync(ns);
cs_resume_db_sync(cs, ns);
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: member_sync %s ns=%s peer=%016llx",
CS_ID, result == MT_OK ? "OK" : "FAIL", ns, (unsigned long long)peer);
}
struct invite_sync_arg {
struct chat_sync* cs;
uint64_t node_id;
char ch_id[64];
};
@ -446,13 +450,13 @@ static void _on_invite_sync_done(uint64_t peer, const char* ns, int result, void
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_sync %s ns=%s peer=%016llx sa=%p node=0x%016llx ch=%s",
CS_ID, result == MT_OK ? "OK" : "FAIL", ns,
(unsigned long long)peer, (void*)sa, sa ? (unsigned long long)sa->node_id : 0, sa ? sa->ch_id : "(null)");
if (result == MT_OK && sa && g_cs) {
if (result == MT_OK && sa && sa->cs) {
if (sa->node_id == 0 || sa->ch_id[0] == '\0') {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_sync CORRUPTED sa=%p node_id=0x%016llx ch_id='%s' — skip, will free",
CS_ID, (void*)sa, (unsigned long long)sa->node_id, sa->ch_id);
} else {
cs_post_channel_online(g_cs, sa->ch_id);
cs_resume_db_sync(sa->ch_id);
cs_post_channel_online(sa->cs, sa->ch_id);
cs_resume_db_sync(sa->cs, sa->ch_id);
}
}
u_free(sa);
@ -482,62 +486,62 @@ static void cs_start_channel_join(struct chat_sync* cs, uint64_t peer) {
/* ── Connection callbacks ── */
static void cs_on_conn_up(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
(void)arg;
if (!conn || !g_cs) return;
struct chat_sync* cs = (struct chat_sync*)arg;
if (!conn || !cs) return;
uint64_t peer = conn->peer_node_id;
if (peer == 0 || peer == g_cs->inst->node_id) return;
if (peer == 0 || peer == cs->inst->node_id) return;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up peer=%016llx init=%d links=%d", CS_ID, (unsigned long long)peer, conn->initialized, conn->links_up);
if (!conn->initialized) { DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up SKIP — not initialized peer=%016llx", CS_ID, (unsigned long long)peer); return; }
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up peer=%016llx pending_invite=%llu links_up=%d", CS_ID, (unsigned long long)peer, (unsigned long long)g_cs->pending_invite_ch_id, conn->links_up);
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_up peer=%016llx pending_invite=%llu links_up=%d", CS_ID, (unsigned long long)peer, (unsigned long long)cs->pending_invite_ch_id, conn->links_up);
if (g_cs->pending_invite_ch_id != 0) {
if (g_cs->pending_invite_node_id != 0 && g_cs->pending_invite_node_id != peer)
if (cs->pending_invite_ch_id != 0) {
if (cs->pending_invite_node_id != 0 && cs->pending_invite_node_id != peer)
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite node_id MISMATCH: invite=0x%016llx ETCP_peer=0x%016llx — invite is STALE!",
CS_ID, (unsigned long long)g_cs->pending_invite_node_id, (unsigned long long)peer);
g_cs->pending_invite_node_id = peer;
if (g_cs->pending_invite_is_inviter) {
CS_ID, (unsigned long long)cs->pending_invite_node_id, (unsigned long long)peer);
cs->pending_invite_node_id = peer;
if (cs->pending_invite_is_inviter) {
/* we are the inviter — send CHANNEL_INVITE to the joiner */
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu",
(unsigned long long)g_cs->pending_invite_ch_id);
(unsigned long long)cs->pending_invite_ch_id);
char ch_name[128]; uint8_t ch_x2[32], ch_ed2[32], ch_sig[64]; uint64_t ch_owner;
if (topo_node_sqlite_channel_get(g_cs->inst->topo_sqlite_db,
if (topo_node_sqlite_channel_get(cs->inst->topo_sqlite_db,
ch_id, ch_name, (int)sizeof(ch_name), &ch_owner, ch_x2, ch_ed2, ch_sig) == 0) {
cs_send_channel_invite(g_cs, g_cs->inst, ch_id, peer, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
if (g_cs->invite_send_timer) { uasync_cancel_timeout(g_cs->inst->ua, g_cs->invite_send_timer); }
g_cs->invite_send_timer = uasync_set_timeout(g_cs->inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, g_cs, cs_invite_send_timeout_cb, "cs_invite_send");
cs_send_channel_invite(cs, cs->inst, ch_id, peer, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
if (cs->invite_send_timer) { uasync_cancel_timeout(cs->inst->ua, cs->invite_send_timer); }
cs->invite_send_timer = uasync_set_timeout(cs->inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, cs, cs_invite_send_timeout_cb, "cs_invite_send");
}
} else {
cs_start_channel_join(g_cs, peer);
cs_start_channel_join(cs, peer);
}
cs_on_peer_status_changed(peer, 1);
cs_on_peer_status_changed(cs, peer, 1);
return;
}
cs_on_peer_status_changed(peer, 1);
cs_on_peer_status_changed(cs, peer, 1);
}
static void cs_on_conn_down(struct ETCP_CONN* conn, int event, void* arg) { (void)event;
(void)arg;
if (!conn || !g_cs) return;
struct chat_sync* cs = (struct chat_sync*)arg;
if (!conn || !cs) return;
uint64_t peer = conn->peer_node_id;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn_down peer=%016llx", CS_ID, (unsigned long long)peer);
cs_on_peer_status_changed(peer, 0);
cs_on_peer_status_changed(cs, peer, 0);
cs_cancel_proto_timers(g_cs);
if (g_cs->pending_invite_node_id == peer) {
cs_cancel_proto_timers(cs);
if (cs->pending_invite_node_id == peer) {
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: conn down while waiting invite resp peer=%016llx, timers cancelled, state kept for retry on reconnect",
CS_ID, (unsigned long long)peer);
/* invite state survives connection flaps: only timeout or explicit protocol end clears it */
}
for (int i = 0; i < g_cs->channel_count; i++) {
for (int i = 0; i < cs->channel_count; i++) {
int found = 0;
for (int j = 0; j < g_cs->channels[i].peer_count; j++)
if (g_cs->channels[i].peer_ids[j] == peer) { found = 1; break; }
for (int j = 0; j < cs->channels[i].peer_count; j++)
if (cs->channels[i].peer_ids[j] == peer) { found = 1; break; }
if (found)
g_cs->channels[i].synced = CS_SYNC_NONE;
cs->channels[i].synced = CS_SYNC_NONE;
}
}
@ -550,21 +554,11 @@ static void cs_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) {
}
}
#if 0 /* replaced by cs_on_conn_status */
static void cs_on_new_conn(struct ETCP_CONN* conn, void* arg) {
(void)arg;
if (!conn) return;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: new_conn conn=%p", CS_ID, (void*)conn);
etcp_conn_add_cbk(conn, cs_on_conn_up, NULL, ETCP_CBK_EVENT_UP);
etcp_conn_add_cbk(conn, cs_on_conn_down, NULL, ETCP_CBK_EVENT_DOWN);
}
#endif
/* ── Periodic refresh from DB ── */
static void cs_refresh_channels(struct chat_sync* cs) {
uint8_t buf[4096]; size_t buf_len;
if (chat_core_list_channels(buf, sizeof(buf), &buf_len) != 0) return;
if (chat_core_list_channels(cs->inst, buf, sizeof(buf), &buf_len) != 0) return;
if (buf_len < 2) return;
uint16_t cnt; memcpy(&cnt, buf, 2);
@ -584,15 +578,15 @@ static void cs_refresh_channels(struct chat_sync* cs) {
cs->channels[i].channel_id[id_len] = '\0';
p += id_len; rem -= id_len;
cs->channels[i].msg_count = chat_core_count(cs->channels[i].channel_id);
cs->channels[i].msg_count = chat_core_count(cs->inst, cs->channels[i].channel_id);
if (cs->channels[i].msg_count > 0)
chat_core_chain_hash_at(cs->channels[i].channel_id,
chat_core_chain_hash_at(cs->inst, cs->channels[i].channel_id,
cs->channels[i].msg_count - 1, cs->channels[i].last_chain_hash);
else
memset(cs->channels[i].last_chain_hash, 0, 32);
uint8_t peer_buf[2048]; size_t peer_len;
if (chat_core_list_peers(cs->channels[i].channel_id, peer_buf,
if (chat_core_list_peers(cs->inst, cs->channels[i].channel_id, peer_buf,
sizeof(peer_buf), &peer_len) == 0 && peer_len >= 2) {
uint16_t pc; memcpy(&pc, peer_buf, 2);
cs->channels[i].peer_ids = u_calloc(pc, sizeof(uint64_t));
@ -607,18 +601,18 @@ static void cs_refresh_channels(struct chat_sync* cs) {
}
static void refresh_timer_cb(void* arg) {
(void)arg;
if (!g_cs || !g_cs->initialized) return;
struct chat_sync* cs = (struct chat_sync*)arg;
if (!cs || !cs->initialized) return;
#ifdef UTUN_HAVE_STANDBY
if (standby_get_sleep_tb() > 0) {
g_cs->refresh_wait = standby_wait(g_cs, refresh_timer_cb);
cs->refresh_wait = standby_wait(cs, refresh_timer_cb);
return;
}
g_cs->refresh_wait = NULL;
cs->refresh_wait = NULL;
#endif
cs_refresh_channels(g_cs);
g_cs->refresh_timer = uasync_set_timeout(g_cs->inst->ua,
30u * 10000u, g_cs, refresh_timer_cb, "cs_refresh");
cs_refresh_channels(cs);
cs->refresh_timer = uasync_set_timeout(cs->inst->ua,
30u * 10000u, cs, refresh_timer_cb, "cs_refresh");
}
/* ── TTL cleanup ── */
@ -633,10 +627,10 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
if (!cs) return -1;
cs->inst = inst;
cs->initialized = 1;
g_cs = cs;
inst->chat_sync = cs;
etcp_bind(inst, ETCP_RT_ID_CHAT_SYNC, chat_sync_recv_cb);
etcp_add_conn_status_cbk(inst, cs_on_conn_status, NULL);
etcp_add_conn_status_cbk(inst, cs_on_conn_status, cs);
cs_refresh_channels(cs);
@ -646,7 +640,7 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
cs->join_timer = NULL;
member_sync_init(inst);
member_sync_add_apply_cbk(cs_on_member_applied, cs);
member_sync_add_apply_cbk(inst, cs_on_member_applied, cs);
chat_admin_init(inst);
chat_join_init(inst);
@ -655,21 +649,21 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
}
void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
struct chat_sync* cs = g_cs;
struct chat_sync* cs = cs_of(inst);
if (!cs || !inst) return;
DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: destroy", CS_ID);
if (cs->sync_timer) { uasync_cancel_timeout(inst->ua, cs->sync_timer); cs->sync_timer = NULL; }
if (cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, cs->invite_send_timer); cs->invite_send_timer = NULL; }
if (cs->sync_scheduled) { cs->sync_scheduled = 0; cs_flush_sync(cs); }
member_sync_remove_apply_cbk(cs_on_member_applied, cs);
member_sync_remove_apply_cbk(inst, cs_on_member_applied, cs);
cs_pending_free_all(cs);
member_sync_destroy(inst);
chat_admin_destroy(inst);
chat_join_destroy(inst);
cs->initialized = 0; g_cs = NULL;
cs->initialized = 0; inst->chat_sync = NULL;
etcp_unbind(inst, ETCP_RT_ID_CHAT_SYNC);
etcp_remove_conn_status_cbk(inst, cs_on_conn_status, NULL);
etcp_remove_conn_status_cbk(inst, cs_on_conn_status, cs);
if (cs->refresh_timer) { uasync_cancel_timeout(inst->ua, cs->refresh_timer); cs->refresh_timer = NULL; }
#ifdef UTUN_HAVE_STANDBY
if (cs->refresh_wait) { standby_wait_cancel(cs->refresh_wait); cs->refresh_wait = NULL; }
@ -683,14 +677,15 @@ void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
}
void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst) {
if (!g_cs || !g_cs->initialized || !inst->topo_groups) return;
for (int i = 0; i < g_cs->channel_count; i++) {
uint64_t gid = strtoull(g_cs->channels[i].channel_id, NULL, 10);
struct chat_sync* cs = cs_of(inst);
if (!cs || !cs->initialized || !inst->topo_groups) return;
for (int i = 0; i < cs->channel_count; i++) {
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, g_cs->channels[i].channel_id);
CS_ID, cs->channels[i].channel_id);
topo_group_connect_restart(group);
}
}
@ -701,14 +696,14 @@ void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id) {
}
void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id) {
(void)inst;
if (!g_cs || !ch_id || !ch_id[0]) return;
for (int i = 0; i < g_cs->channel_count; i++) {
if (strcmp(g_cs->channels[i].channel_id, ch_id) != 0) continue;
if (g_cs->channels[i].peer_ids) u_free(g_cs->channels[i].peer_ids);
memmove(&g_cs->channels[i], &g_cs->channels[i + 1],
(size_t)(g_cs->channel_count - i - 1) * sizeof(struct channel_cache));
g_cs->channel_count--;
struct chat_sync* cs = cs_of(inst);
if (!cs || !ch_id || !ch_id[0]) return;
for (int i = 0; i < cs->channel_count; i++) {
if (strcmp(cs->channels[i].channel_id, ch_id) != 0) continue;
if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids);
memmove(&cs->channels[i], &cs->channels[i + 1],
(size_t)(cs->channel_count - i - 1) * sizeof(struct channel_cache));
cs->channel_count--;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: channel removed from cache ch=%s", CS_ID, ch_id);
return;
}
@ -721,9 +716,11 @@ struct chat_invite {
uint8_t* addrs_data;
int addrs_data_len;
int addr_count;
uint64_t join_key;
char password[128];
};
struct cm_invite_wrap { struct chat_invite inv; };
struct cm_invite_wrap { struct UTUN_INSTANCE* inst; struct chat_invite inv; };
/* Парсит reality-префикс addrs_data: has(1) + [short_id(8) pubkey(32) ver(3) snlen(1) sn(N)].
Сдвигает *ap за префикс. Возвращает 1, если reality присутствует (и params заполнены). */
@ -762,13 +759,23 @@ static void invite_reality_sock_append(struct TOPO_NODE* ni, uint8_t socket_id,
static void cm_invite_trampoline(void* arg) {
struct cm_invite_wrap* w = (struct cm_invite_wrap*)arg;
struct chat_invite* inv = &w->inv;
struct UTUN_INSTANCE* inst = chat_core_get_inst();
struct UTUN_INSTANCE* inst = w->inst;
struct chat_sync* cs = cs_of(inst);
uint64_t node_id = inv->node_id;
uint64_t channel_id = inv->channel_id;
if (!cs) { u_free(inv->addrs_data); u_free(w); return; }
/* Параметры invite храним в cs->pending_* здесь, в uasync-потоке (не на вызывающем):
* их читает cs_start_channel_join/cs_on_conn_up из uasync-потока. */
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = node_id;
cs->pending_join_key = inv->join_key;
snprintf(cs->pending_password, sizeof(cs->pending_password), "%s", inv->password);
/* build TOPO_NODE from invite data */
struct TOPO_NODE* ni = u_calloc(1, sizeof(*ni));
if (!ni) { u_free(w); return; }
if (!ni) { u_free(inv->addrs_data); u_free(w); return; }
ni->node_id = node_id; memcpy(ni->public_key, inv->pubkey, 32);
const uint8_t* ap = inv->addrs_data;
const uint8_t* ap_end = inv->addrs_data + inv->addrs_data_len;
@ -814,50 +821,52 @@ static void cm_invite_trampoline(void* arg) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "invite: no valid addrs for node=0x%016llx", (unsigned long long)node_id);
while (ni->v4_addrs) { struct TOPO_ADDR4* n = ni->v4_addrs->next; u_free(ni->v4_addrs); ni->v4_addrs = n; }
while (ni->v6_addrs) { struct TOPO_ADDR6* n = ni->v6_addrs->next; u_free(ni->v6_addrs); ni->v6_addrs = n; }
u_free(ni); u_free(w); return;
u_free(ni); u_free(inv->addrs_data); u_free(w); return;
}
/* topo_group_invite_to_channel: прямое ncd-подключение к connection-узлу БЕЗ INVITE_INFO.
Реальный join-протокол (JOIN_INFO_REQ → … → JOIN_READY) запускается по cs_on_conn_up. */
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "channel invite: connecting to 0x%016llx via %d addresses",
(unsigned long long)node_id, inv->addr_count);
int r = topo_group_invite_to_channel(inst, channel_id, ni, node_id);
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "channel invite: FAILED — topo_group_invite_to_channel error=%d", r);
/* если линк к узлу уже есть и он up — не открываем новое подключение,
сразу запускаем join-хендшейк (JOIN_INFO_REQ → … → JOIN_READY) */
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (conn && conn->links_up && conn->initialized) {
cs->pending_invite_node_id = node_id;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
"channel invite: link to 0x%016llx already up — skipping connect, join handshake directly",
(unsigned long long)node_id);
cs_start_channel_join(cs, node_id);
} else {
/* если уже подключены к connection-узлу — cs_on_conn_up отработал до установки
pending_invite_ch_id и повторно не сработает, поэтому запускаем join-хендшейк напрямую */
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (conn && conn->links_up && conn->initialized) {
g_cs->pending_invite_node_id = node_id;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: already connected to 0x%016llx — starting join handshake directly",
CS_ID, (unsigned long long)node_id);
cs_start_channel_join(g_cs, node_id);
}
/* topo_group_invite_to_channel: прямое ncd-подключение к connection-узлу БЕЗ INVITE_INFO.
Реальный join-протокол (JOIN_INFO_REQ → … → JOIN_READY) запускается по cs_on_conn_up. */
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "channel invite: connecting to 0x%016llx via %d addresses",
(unsigned long long)node_id, inv->addr_count);
int r = topo_group_invite_to_channel(inst, channel_id, ni, node_id);
if (r < 0)
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "channel invite: FAILED — topo_group_invite_to_channel error=%d", r);
}
/* free caller-owned ni */
while (ni->v4_addrs) { struct TOPO_ADDR4* n = ni->v4_addrs->next; u_free(ni->v4_addrs); ni->v4_addrs = n; }
while (ni->v6_addrs) { struct TOPO_ADDR6* n = ni->v6_addrs->next; u_free(ni->v6_addrs); ni->v6_addrs = n; }
u_free(ni);
u_free(inv->addrs_data);
u_free(w);
}
void chat_sync_connect_from_invite(uint64_t channel_id, uint64_t node_id,
void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count,
int addrs_data_len,
const char* password, uint64_t join_key) {
if (!g_cs || !g_cs->inst || !g_cs->inst->ua) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !cs->inst || !cs->inst->ua) {
int r = -7;
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_ERROR, "utun-chat", "invite ch=%llu node=%016llx REJECTED g_cs=%p inst=%p ua=%p",
(long long)channel_id, (long long)node_id, (void*)g_cs,
g_cs ? (void*)g_cs->inst : NULL,
(g_cs && g_cs->inst) ? (void*)g_cs->inst->ua : NULL);
__android_log_print(ANDROID_LOG_ERROR, "utun-chat", "invite ch=%llu node=%016llx REJECTED cs=%p inst=%p ua=%p",
(long long)channel_id, (long long)node_id, (void*)cs,
cs ? (void*)cs->inst : NULL,
(cs && cs->inst) ? (void*)cs->inst->ua : NULL);
#endif
uint8_t err[12]; memcpy(err, &node_id, 8); memcpy(err + 8, &r, 4);
chat_event_post(CHAT_EVT_CONNECT_RESULT, err, 12);
chat_event_post(inst, CHAT_EVT_CONNECT_RESULT, err, 12);
return;
}
@ -874,18 +883,9 @@ void chat_sync_connect_from_invite(uint64_t channel_id, uint64_t node_id,
memcpy(inv->addrs_data, addrs_data, addrs_sz);
inv->addrs_data_len = (int)addrs_sz;
inv->addr_count = addr_count;
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = node_id;
g_cs->pending_join_key = join_key;
if (password && password[0]) {
size_t pl = strlen(password);
if (pl >= sizeof(g_cs->pending_password)) pl = sizeof(g_cs->pending_password) - 1;
memcpy(g_cs->pending_password, password, pl);
g_cs->pending_password[pl] = '\0';
} else {
g_cs->pending_password[0] = '\0';
}
inv->join_key = join_key;
if (password && password[0]) snprintf(inv->password, sizeof(inv->password), "%s", password);
else inv->password[0] = '\0';
#ifdef __ANDROID__
__android_log_print(ANDROID_LOG_INFO, "utun-chat", "invite ch=%llu node=%016llx addrs=%d pass=%s",
(long long)channel_id, (long long)node_id, addr_count, password ? "yes" : "no");
@ -895,16 +895,18 @@ void chat_sync_connect_from_invite(uint64_t channel_id, uint64_t node_id,
CS_ID, channel_id, node_id, *(const uint64_t*)pubkey_bin, addr_count, password && password[0] ? "yes" : "no");
#endif
struct cm_invite_wrap { struct chat_invite inv; }* w = u_malloc(sizeof(struct cm_invite_wrap));
struct cm_invite_wrap* w = u_malloc(sizeof(struct cm_invite_wrap));
w->inst = inst;
w->inv = *inv; u_free(inv);
uasync_post(g_cs->inst->ua,
uasync_post(cs->inst->ua,
(void(*)(void*))cm_invite_trampoline, w);
}
/* ─── chat_sync_join_channel: join channel via already-connected peer ─── */
void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id) {
if (!g_cs) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel: g_cs=NULL, chat_sync not initialized", CS_ID); return; }
struct chat_sync* cs = cs_of(inst);
if (!cs) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel: cs=NULL, chat_sync not initialized", CS_ID); return; }
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel: inst=NULL", CS_ID); return; }
if (!ch_id || !ch_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel: ch_id NULL/empty", CS_ID); return; }
uint64_t channel_id = strtoull(ch_id, NULL, 10);
@ -920,10 +922,10 @@ void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint6
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: join_channel ch=%s peer=0x%016llx",
CS_ID, ch_id, (unsigned long long)target_node_id);
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
cs_start_channel_join(g_cs, target_node_id);
cs_on_peer_status_changed(target_node_id, 1);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs_start_channel_join(cs, target_node_id);
cs_on_peer_status_changed(cs, target_node_id, 1);
}
/* ─── Ed25519 sign / verify helpers ─── */
@ -1132,7 +1134,7 @@ static void cs_handle_join_info_resp(struct chat_sync* cs, uint64_t peer,
/* save channel */
topo_node_sqlite_channel_put(cs->inst->topo_sqlite_db, ch_id, name, owner, ch_x25519, NULL, ch_ed, NULL, ch_sig);
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(cs->inst, ch_id);
/* добавить connection-узел в мемберы (дерево приглашений из INFO_RESP) */
sqlite3* vdb = cs->inst->topo_sqlite_db;
@ -1325,18 +1327,18 @@ static void cs_handle_join_ready(struct chat_sync* cs, uint64_t peer,
if (c) topo_group_new_conn(g, c);
}
}
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(cs->inst, ch_id);
uint8_t evt[65]; uint8_t ch_id_len = (uint8_t)strlen(ch_id);
evt[0] = ch_id_len; memcpy(evt + 1, ch_id, ch_id_len);
chat_event_post(CHAT_EVT_CHANNEL_UPDATED, evt, 1 + ch_id_len);
chat_event_post(cs->inst, CHAT_EVT_CHANNEL_UPDATED, evt, 1 + ch_id_len);
cs_post_channel_online(cs, ch_id);
cs_resume_db_sync(ch_id);
cs_resume_db_sync(cs, ch_id);
if (cs->pending_invite_ch_id != 0) {
uint64_t ch_id_num = strtoull(ch_id, NULL, 10);
uint8_t cevt[20]; memcpy(cevt, &peer, 8); int r = 0; memcpy(cevt + 8, &r, 4); memcpy(cevt + 12, &ch_id_num, 8);
chat_event_post(CHAT_EVT_CONNECT_RESULT, cevt, 20);
chat_event_post(cs->inst, CHAT_EVT_CONNECT_RESULT, cevt, 20);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: JOIN_READY invite success ch=%s peer=%016llx", CS_ID, ch_id, (unsigned long long)peer);
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0; cs->pending_join_key = 0;
}
@ -1386,7 +1388,7 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
int policy;
{
struct global_config *gcfg = &cs->inst->config->global;
policy = gcfg->chatserver_enabled ? 0 : chat_setting_get_int("join_policy", 1);
policy = gcfg->chatserver_enabled ? 0 : chat_setting_get_int(cs->inst, "join_policy", 1);
}
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE policy=%d (chatserver=%d) ch=%s from=%016llx",
CS_ID, policy, cs->inst->config->global.chatserver_enabled, ch_id, (unsigned long long)peer);
@ -1405,7 +1407,7 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
ch_id, name, owner, x25519, NULL, ed_pub, NULL, ch_sig);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE channel_put rc=%d ch=%s ch_x25519=%016llx",
CS_ID, cprc, ch_id, *(const uint64_t*)x25519);
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(cs->inst, ch_id);
/* Initiate join: connect back and send CHANNEL_INFO_REQ */
chat_sync_join_channel(cs->inst, ch_id, peer);
return;
@ -1432,7 +1434,7 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer,
memcpy(evt + epos, &peer, 8); epos += 8;
evt[epos++] = (uint8_t)inv_name_len;
if (inv_name_len) { memcpy(evt + epos, inv_name, inv_name_len); epos += inv_name_len; }
chat_event_post(CHAT_EVT_INVITE_RECEIVED, evt, epos);
chat_event_post(cs->inst, CHAT_EVT_INVITE_RECEIVED, evt, epos);
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE ask posted to GUI ch=%s name=%s from=%016llx (%s)",
CS_ID, ch_id, name, (unsigned long long)peer, inv_name);
}
@ -1483,7 +1485,8 @@ static int cs_send_channel_invite(struct chat_sync* cs, struct UTUN_INSTANCE* in
/* ─── chat_sync_invite_to_channel: пригласить узел в наш канал ─── */
void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id) {
if (!g_cs) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: g_cs=NULL, chat_sync not initialized", CS_ID); return; }
struct chat_sync* cs = cs_of(inst);
if (!cs) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: cs=NULL, chat_sync not initialized", CS_ID); return; }
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: inst=NULL", CS_ID); return; }
if (!ch_id || !ch_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: ch_id NULL/empty", CS_ID); return; }
if (target_node_id == 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel: target_node_id=0", CS_ID); return; }
@ -1512,8 +1515,8 @@ void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id,
/* Already have a handle — connection in progress or established */
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel — existing handle for node 0x%016llx, waiting for conn_up",
CS_ID, (unsigned long long)target_node_id);
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
return;
}
}
@ -1526,28 +1529,27 @@ void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id,
DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_to_channel — no topo_groups, cannot connect", CS_ID);
return;
}
/* Mark pending: after conn_up, we send CHANNEL_INVITE.
But we need to distinguish this from the joiner-side invite flow.
Use a separate flag. */
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
g_cs->pending_invite_is_inviter = 1;
/* Mark pending: after conn_up, we send CHANNEL_INVITE. */
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
return;
}
/* Already connected — send invite now, wait for joiner to respond */
cs_send_channel_invite(g_cs, inst, ch_id, target_node_id, ch_name, owner, ch_x25519, ch_ed, ch_sig);
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
g_cs->pending_invite_is_inviter = 1;
if (g_cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, g_cs->invite_send_timer); }
g_cs->invite_send_timer = uasync_set_timeout(inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, g_cs, cs_invite_send_timeout_cb, "cs_invite_send");
cs_send_channel_invite(cs, inst, ch_id, target_node_id, ch_name, owner, ch_x25519, ch_ed, ch_sig);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
if (cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, cs->invite_send_timer); }
cs->invite_send_timer = uasync_set_timeout(inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, cs, cs_invite_send_timeout_cb, "cs_invite_send");
}
/* ─── chat_sync_invite_to_channel_with_addrs: как invite_to_channel, но с адресами ─── */
struct cs_invite_wrap {
struct UTUN_INSTANCE* inst;
uint64_t channel_id;
uint64_t target_node_id;
uint8_t pubkey[32];
@ -1558,8 +1560,9 @@ struct cs_invite_wrap {
static void cs_invite_trampoline(void* arg) {
struct cs_invite_wrap* w = (struct cs_invite_wrap*)arg;
if (!g_cs || !g_cs->inst) { u_free(w); return; }
struct UTUN_INSTANCE* inst = g_cs->inst;
struct chat_sync* cs = cs_of(w->inst);
if (!cs || !cs->inst) { u_free(w); return; }
struct UTUN_INSTANCE* inst = cs->inst;
uint64_t channel_id = w->channel_id;
uint64_t target_node_id = w->target_node_id;
@ -1577,13 +1580,13 @@ static void cs_invite_trampoline(void* arg) {
char ch_name[128]; uint8_t ch_x2[32], ch_ed2[32], ch_sig[64]; uint64_t ch_owner;
if (topo_node_sqlite_channel_get(inst->topo_sqlite_db,
ch_id, ch_name, (int)sizeof(ch_name), &ch_owner, ch_x2, ch_ed2, ch_sig) == 0) {
cs_send_channel_invite(g_cs, inst, ch_id, target_node_id, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
g_cs->pending_invite_is_inviter = 1;
if (g_cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, g_cs->invite_send_timer); }
g_cs->invite_send_timer = uasync_set_timeout(inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, g_cs, cs_invite_send_timeout_cb, "cs_invite_send");
cs_send_channel_invite(cs, inst, ch_id, target_node_id, ch_name, ch_owner, ch_x2, ch_ed2, ch_sig);
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
if (cs->invite_send_timer) { uasync_cancel_timeout(inst->ua, cs->invite_send_timer); }
cs->invite_send_timer = uasync_set_timeout(inst->ua,
CS_INVITE_SEND_TIMEOUT_MS * 10, cs, cs_invite_send_timeout_cb, "cs_invite_send");
}
u_free(w); return;
}
@ -1629,15 +1632,15 @@ static void cs_invite_trampoline(void* arg) {
u_free(ni); u_free(w); return;
}
g_cs->pending_invite_ch_id = channel_id;
g_cs->pending_invite_node_id = target_node_id;
g_cs->pending_invite_is_inviter = 1;
cs->pending_invite_ch_id = channel_id;
cs->pending_invite_node_id = target_node_id;
cs->pending_invite_is_inviter = 1;
int r = topo_group_invite_to_channel(inst, channel_id, ni, target_node_id);
if (r < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs topo_group_invite_to_channel failed rc=%d", CS_ID, r);
g_cs->pending_invite_node_id = 0; g_cs->pending_invite_ch_id = 0;
g_cs->pending_invite_is_inviter = 0;
cs->pending_invite_node_id = 0; cs->pending_invite_ch_id = 0;
cs->pending_invite_is_inviter = 0;
}
while (ni->v4_addrs) { struct TOPO_ADDR4* n = ni->v4_addrs->next; u_free(ni->v4_addrs); ni->v4_addrs = n; }
@ -1651,7 +1654,8 @@ void chat_sync_invite_to_channel_with_addrs(
const uint8_t* pubkey_bin, const uint8_t* addrs_data, int addr_count,
int addrs_data_len)
{
if (!g_cs || !inst || !ch_id || !ch_id[0] || !pubkey_bin || !addrs_data || addr_count <= 0 || addrs_data_len <= 0) {
struct chat_sync* cs = cs_of(inst);
if (!cs || !inst || !ch_id || !ch_id[0] || !pubkey_bin || !addrs_data || addr_count <= 0 || addrs_data_len <= 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: invite_with_addrs invalid args", CS_ID);
return;
}
@ -1664,6 +1668,7 @@ void chat_sync_invite_to_channel_with_addrs(
struct cs_invite_wrap* w = u_calloc(1, sizeof(*w) + (size_t)addrs_data_len);
if (!w) return;
w->inst = inst;
w->channel_id = channel_id;
w->target_node_id = target_node_id;
memcpy(w->pubkey, pubkey_bin, 32);
@ -1679,7 +1684,7 @@ void chat_sync_invite_to_channel_with_addrs(
/* ─── accept/deny incoming invite ─── */
void chat_sync_accept_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_node_id) {
if (!g_cs || !inst) return;
if (!cs_of(inst) || !inst) return;
char ch_id[64]; snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)channel_id);
if (channel_id == 0 || inviter_node_id == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite invalid params ch=%llu node=%016llx",
@ -1688,13 +1693,13 @@ void chat_sync_accept_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, ui
}
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: accept_invite ch=%llu inviter=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(inst, ch_id);
chat_sync_join_channel(inst, ch_id, inviter_node_id);
}
void chat_sync_deny_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t inviter_node_id) {
(void)inst;
if (!g_cs) return;
if (!cs_of(inst)) return;
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "%s: deny_invite ch=%llu inviter=0x%016llx",
CS_ID, (unsigned long long)channel_id, (unsigned long long)inviter_node_id);
/* silently ignore — no response needed */

2
src/chat/chat_sync.h

@ -74,7 +74,7 @@ void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* Подключиться к пиру по данным invite-ссылки (вызывается из uasync-потока).
join_key — 64-битный ключ входа из ссылки (регистрируется инвайтером на connection-узле).
password может быть NULL — тогда отправляется без пароля (совместимость с v1) */
void chat_sync_connect_from_invite(uint64_t channel_id, uint64_t node_id,
void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count,
int addrs_data_len,

28
src/chat/chat_whisper.c

@ -38,6 +38,7 @@ static int g_initialized = 0;
/* очередь транскрипций: whisper не thread-safe, обрабатываем по одной */
struct wh_job {
struct UTUN_INSTANCE* inst;
char audio_path[1024];
char channel_id[64];
uint64_t reply_to_ts;
@ -231,12 +232,14 @@ static float* resample_16k(const float* src, int n_src, int src_rate, int* n_dst
struct wh_work_ctx;
static void chat_whisper_transcribe_async_impl(
struct UTUN_INSTANCE* inst,
struct media_async* ma, struct UASYNC* ua,
const char* audio_path, const char* channel_id,
uint64_t reply_to_ts, uint64_t reply_to_node,
chat_whisper_done_fn done_cb, void* done_arg);
static void chat_whisper_transcribe_async(
struct UTUN_INSTANCE* inst,
struct media_async* ma, struct UASYNC* ua,
const char* audio_path, const char* channel_id,
uint64_t reply_to_ts, uint64_t reply_to_node,
@ -360,7 +363,7 @@ static void wh_done_fn(void* raw, int err) {
if (g_pending_job) {
struct wh_job* nj = g_pending_job;
g_pending_job = NULL;
chat_whisper_transcribe_async_impl(nj->ma, nj->ua, nj->audio_path, nj->channel_id,
chat_whisper_transcribe_async_impl(nj->inst, nj->ma, nj->ua, nj->audio_path, nj->channel_id,
nj->reply_to_ts, nj->reply_to_node,
nj->done_cb, nj->done_arg);
u_free(nj);
@ -369,8 +372,8 @@ static void wh_done_fn(void* raw, int err) {
/* ─── авто-поиск модели ─── */
static int find_model_path(char* out, size_t out_sz) {
const char* setting = chat_setting_get("whisper_model_path");
static int find_model_path(struct UTUN_INSTANCE* inst, char* out, size_t out_sz) {
const char* setting = chat_setting_get(inst, "whisper_model_path");
if (setting && setting[0] != '\0') {
/* если путь относительный, не проверяем доступ — вернём как есть */
if (access(setting, R_OK) == 0) { snprintf(out, out_sz, "%s", setting); return 0; }
@ -397,7 +400,7 @@ static int find_model_path(char* out, size_t out_sz) {
/* ─── публичный API ─── */
int chat_whisper_init(void) {
int chat_whisper_init(struct UTUN_INSTANCE* inst) {
if (g_initialized) return 0;
g_initialized = 1;
@ -405,13 +408,13 @@ int chat_whisper_init(void) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "%s: compiled without whisper support", CW_ID);
return -1;
#else
if (!chat_setting_get_int("whisper_enabled", 0)) {
if (!chat_setting_get_int(inst, "whisper_enabled", 0)) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "%s: whisper disabled in settings", CW_ID);
return -1;
}
char model_path[512];
if (find_model_path(model_path, sizeof(model_path)) != 0) {
if (find_model_path(inst, model_path, sizeof(model_path)) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "%s: model not found (set whisper_model_path)", CW_ID);
return -1;
}
@ -432,9 +435,9 @@ int chat_whisper_init(void) {
#endif /* HAVE_WHISPER */
}
int chat_whisper_available(void) {
int chat_whisper_available(struct UTUN_INSTANCE* inst) {
#ifdef HAVE_WHISPER
return g_whisper_ctx != NULL && chat_setting_get_int("whisper_enabled", 0);
return g_whisper_ctx != NULL && chat_setting_get_int(inst, "whisper_enabled", 0);
#else
return 0;
#endif
@ -455,17 +458,19 @@ void chat_whisper_destroy(void) {
}
void chat_whisper_transcribe_async(
struct UTUN_INSTANCE* inst,
struct media_async* ma, struct UASYNC* ua,
const char* audio_path, const char* channel_id,
uint64_t reply_to_ts, uint64_t reply_to_node,
chat_whisper_done_fn done_cb, void* done_arg)
{
chat_whisper_transcribe_async_impl(ma, ua, audio_path, channel_id,
chat_whisper_transcribe_async_impl(inst, ma, ua, audio_path, channel_id,
reply_to_ts, reply_to_node,
done_cb, done_arg);
}
static void chat_whisper_transcribe_async_impl(
struct UTUN_INSTANCE* inst,
struct media_async* ma, struct UASYNC* ua,
const char* audio_path, const char* channel_id,
uint64_t reply_to_ts, uint64_t reply_to_node,
@ -477,8 +482,8 @@ static void chat_whisper_transcribe_async_impl(
return;
}
if (!chat_whisper_available()) {
int rc = chat_whisper_init();
if (!chat_whisper_available(inst)) {
int rc = chat_whisper_init(inst);
if (rc != 0) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "%s: whisper not available, skip transcription", CW_ID);
done_cb(done_arg, channel_id, NULL, -1, reply_to_ts, reply_to_node);
@ -491,6 +496,7 @@ static void chat_whisper_transcribe_async_impl(
if (g_pending_job) { u_free(g_pending_job); } /* заменяем */
g_pending_job = u_malloc(sizeof(*g_pending_job));
if (!g_pending_job) { done_cb(done_arg, channel_id, NULL, -1, reply_to_ts, reply_to_node); return; }
g_pending_job->inst = inst;
snprintf(g_pending_job->audio_path, sizeof(g_pending_job->audio_path), "%s", audio_path);
snprintf(g_pending_job->channel_id, sizeof(g_pending_job->channel_id), "%s", channel_id);
g_pending_job->reply_to_ts = reply_to_ts;

6
src/chat/chat_whisper.h

@ -24,11 +24,12 @@ extern "C" {
struct media_async;
struct UASYNC;
struct UTUN_INSTANCE;
/* ── жизненный цикл ── */
int chat_whisper_init(void); /* ленивая загрузка модели */
int chat_whisper_available(void); /* модель загружена и готова */
int chat_whisper_init(struct UTUN_INSTANCE* inst); /* ленивая загрузка модели */
int chat_whisper_available(struct UTUN_INSTANCE* inst); /* модель загружена и готова */
void chat_whisper_destroy(void); /* выгрузка модели */
/* ── асинхронная транскрипция ── */
@ -38,6 +39,7 @@ typedef void (*chat_whisper_done_fn)(void* arg, const char* channel_id,
uint64_t reply_ts, uint64_t reply_node);
typedef void (*chat_whisper_trigger_fn)(
struct UTUN_INSTANCE* inst,
struct media_async* ma, struct UASYNC* ua,
const char* audio_path, const char* channel_id,
uint64_t reply_ts, uint64_t reply_node,

136
src/chat/invite_build.c

@ -3,7 +3,7 @@
*
* Общий модуль для всех GUI (desktop chatgui, Android, headless CLI).
* Логика выбора «лучшего узла» перенесена из tools/chatgui-android/jni_bridge.
* Работает в uasync-потоке, использует общий контекст g_cc.
* Работает в uasync-потоке, использует per-instance контекст.
*/
#include "invite_build.h"
#include "invite_link.h"
@ -25,15 +25,16 @@
/* ── живость узла в канале (BGP/прямое/через посредника) ── */
static struct TOPO_GROUP* resolve_chat_group(uint64_t channel_id) {
if (!g_cc.inst || !g_cc.inst->topo_groups) return NULL;
struct TOPO_GROUP* grp = topo_groups_find(g_cc.inst->topo_groups, channel_id);
static struct TOPO_GROUP* resolve_chat_group(struct UTUN_INSTANCE* inst, uint64_t channel_id) {
if (!inst || !inst->topo_groups) return NULL;
struct TOPO_GROUP* grp = topo_groups_find(inst->topo_groups, channel_id);
if (!grp || grp->group_type != TOPO_GROUP_TYPE_CHAT) return NULL;
return grp;
}
static int node_is_alive(struct TOPO_GROUP* grp, uint64_t node_id) {
if (node_id == g_cc.my_node_id) return 1; /* self всегда доступен */
static int node_is_alive(struct UTUN_INSTANCE* inst, struct TOPO_GROUP* grp, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
if (cc && node_id == cc->my_node_id) return 1; /* self всегда доступен */
if (!grp) return 0;
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(grp, node_id);
if (!nq) return 0;
@ -41,13 +42,14 @@ static int node_is_alive(struct TOPO_GROUP* grp, uint64_t node_id) {
}
/* есть ли у узла публичные адреса v4 и v6 (addr_type=DIRECT) */
static int node_has_both_public(uint64_t node_id) {
static int node_has_both_public(struct UTUN_INSTANCE* inst, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
int has_v4 = 0, has_v6 = 0;
sqlite3_stmt* st = NULL;
char sql[256];
snprintf(sql, sizeof(sql),
"SELECT family FROM node_addresses WHERE node_id=? AND addr_type=%d", ADDR_TYPE_DIRECT);
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) return 0;
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return 0;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
while (sqlite3_step(st) == SQLITE_ROW) {
int fam = sqlite3_column_int(st, 0);
@ -60,12 +62,13 @@ static int node_has_both_public(uint64_t node_id) {
/* ── Фаза 1-2: лучший достижимый узел в канале ── */
static uint64_t pick_best_node(uint64_t channel_id, int* out_has_both_public, int* out_priority) {
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_has_both = 0;
struct TOPO_GROUP* grp = resolve_chat_group(channel_id);
struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id);
if (!grp) {
*out_has_both_public = 0;
*out_priority = best_priority;
@ -80,9 +83,9 @@ static uint64_t pick_best_node(uint64_t channel_id, int* out_has_both_public, in
char sql[512];
snprintf(sql, sizeof(sql), "SELECT node_id, node_type FROM \"%s\" ORDER BY node_id", peers_tbl);
sqlite3_stmt* pst = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &pst, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &pst, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: pick_best — query failed ch=%llu err=%s",
(unsigned long long)channel_id, sqlite3_errmsg(g_cc.db));
(unsigned long long)channel_id, sqlite3_errmsg(cc->db));
*out_has_both_public = 0;
*out_priority = best_priority;
return 0;
@ -91,12 +94,12 @@ static uint64_t pick_best_node(uint64_t channel_id, int* out_has_both_public, in
while (sqlite3_step(pst) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(pst, 0);
int ntype = sqlite3_column_int(pst, 1);
if (!node_is_alive(grp, nid)) continue;
if (!node_is_alive(inst, grp, nid)) continue;
int has_both = node_has_both_public(nid);
int has_both = node_has_both_public(inst, nid);
int priority;
if (nid == g_cc.my_node_id) priority = 2; /* self */
if (nid == cc->my_node_id) priority = 2; /* self */
else if (ntype == 4) priority = 3; /* supernode */
else priority = 1; /* regular */
@ -114,10 +117,11 @@ static uint64_t pick_best_node(uint64_t channel_id, int* out_has_both_public, in
}
/* авто-узел: pick_best_node + фолбэк на self */
static uint64_t resolve_auto_node(uint64_t channel_id, int* out_has_both, int* out_priority) {
static uint64_t resolve_auto_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, int* out_has_both, int* out_priority) {
struct chat_core_ctx* cc = CC(inst);
int has_both = 0, prio = 4;
uint64_t best = pick_best_node(channel_id, &has_both, &prio);
if (best == 0) best = g_cc.my_node_id;
uint64_t best = pick_best_node(inst, channel_id, &has_both, &prio);
if (best == 0) best = cc->my_node_id;
if (out_has_both) *out_has_both = has_both;
if (out_priority) *out_priority = prio;
return best;
@ -125,14 +129,15 @@ static uint64_t resolve_auto_node(uint64_t channel_id, int* out_has_both, int* o
/* ── Фаза 3: pubkey узла ── */
static int get_node_pubkey(uint64_t node_id, int is_self, uint8_t pubkey_out[INVITE_PUBKEY_SIZE]) {
static int get_node_pubkey(struct UTUN_INSTANCE* inst, uint64_t node_id, int is_self, uint8_t pubkey_out[INVITE_PUBKEY_SIZE]) {
struct chat_core_ctx* cc = CC(inst);
if (is_self) {
if (!g_cc.inst) return -1;
memcpy(pubkey_out, g_cc.inst->my_keys.public_key, INVITE_PUBKEY_SIZE);
if (!inst) return -1;
memcpy(pubkey_out, inst->my_keys.public_key, INVITE_PUBKEY_SIZE);
return 0;
}
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, "SELECT x25519_pubkey FROM nodes WHERE node_id=?",
if (sqlite3_prepare_v2(cc->db, "SELECT x25519_pubkey FROM nodes WHERE node_id=?",
-1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
int ok = 0;
@ -147,13 +152,13 @@ static int get_node_pubkey(uint64_t node_id, int is_self, uint8_t pubkey_out[INV
/* ── Фаза 4a: свои адреса из сокетов ── */
static int collect_self_addrs(struct InviteAddr* out, int max_cnt) {
static int collect_self_addrs(struct UTUN_INSTANCE* inst, struct InviteAddr* out, int max_cnt) {
int cnt = 0;
if (!g_cc.inst) return 0;
if (!inst) return 0;
struct ETCP_SOCKET* s;
/* UDP */
for (s = g_cc.inst->etcp_sockets; s && cnt < max_cnt; s = s->next) {
for (s = inst->etcp_sockets; s && cnt < max_cnt; s = s->next) {
if (s->is_tcp) continue;
struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr;
if (sa->ss_family == AF_INET) {
@ -169,7 +174,7 @@ static int collect_self_addrs(struct InviteAddr* out, int max_cnt) {
}
}
/* TCP */
for (s = g_cc.inst->etcp_sockets; s && cnt < max_cnt; s = s->next) {
for (s = inst->etcp_sockets; s && cnt < max_cnt; s = s->next) {
if (!s->is_tcp) continue;
struct sockaddr_storage* sa = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr;
uint8_t proto = INVITE_PROTO_TCP | (s->reality_enabled ? INVITE_PROTO_REALITY : 0);
@ -189,13 +194,13 @@ static int collect_self_addrs(struct InviteAddr* out, int max_cnt) {
}
/* reality-параметры self-узла из конфига (node-global, как в topo_node_update_my_addresses). */
static void collect_self_reality(struct InviteReality* out) {
static void collect_self_reality(struct UTUN_INSTANCE* inst, struct InviteReality* out) {
memset(out, 0, sizeof(*out));
if (!g_cc.inst || !g_cc.inst->config) return;
const struct reality_config* rc = &g_cc.inst->config->global.reality;
if (!inst || !inst->config) return;
const struct reality_config* rc = &inst->config->global.reality;
if (!rc->enabled || !rc->has_private_key) return;
int any = 0;
for (struct ETCP_SOCKET* s = g_cc.inst->etcp_sockets; s; s = s->next)
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s; s = s->next)
if (s->is_tcp && s->reality_enabled) { any = 1; break; }
if (!any) return;
uint8_t pub[32];
@ -211,8 +216,9 @@ static void collect_self_reality(struct InviteReality* out) {
/* ── Фаза 4b: адреса узла из БД ── */
static int collect_node_addrs_from_db(uint64_t node_id, struct InviteAddr* out, int max_cnt, int dir_only,
static int collect_node_addrs_from_db(struct UTUN_INSTANCE* inst, uint64_t node_id, struct InviteAddr* out, int max_cnt, int dir_only,
struct InviteReality* reality) {
struct chat_core_ctx* cc = CC(inst);
char sql[256];
if (dir_only)
snprintf(sql, sizeof(sql),
@ -223,7 +229,7 @@ static int collect_node_addrs_from_db(uint64_t node_id, struct InviteAddr* out,
"SELECT family,socket_id,protocol,address,port,options FROM node_addresses WHERE node_id=?");
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) return 0;
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return 0;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
int cnt = 0;
@ -257,13 +263,14 @@ static int collect_node_addrs_from_db(uint64_t node_id, struct InviteAddr* out,
/* ── публичный API ── */
int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
int chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
const char* password, char* out, size_t out_size) {
struct chat_core_ctx* cc = CC(inst);
if (!out || out_size == 0) return -1;
out[0] = '\0';
if (!g_cc.initialized || !g_cc.inst || !g_cc.db) {
if (!cc || !cc->initialized || !inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: not initialized (inst=%p db=%p)",
(void*)g_cc.inst, (void*)g_cc.db);
(void*)inst, (void*)(cc ? cc->db : NULL));
return -1;
}
if (channel_id == 0) {
@ -277,23 +284,23 @@ int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
if (target_nid == 0) {
int prio = 4;
target_nid = resolve_auto_node(channel_id, &has_both_public, &prio);
use_self = (target_nid == g_cc.my_node_id) ? 1 : 0;
target_nid = resolve_auto_node(inst, channel_id, &has_both_public, &prio);
use_self = (target_nid == cc->my_node_id) ? 1 : 0;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: auto ch=%llu best=0x%016llx prio=%d self=%d both=%d",
(unsigned long long)channel_id, (unsigned long long)target_nid,
prio, use_self, has_both_public);
} else {
if (target_nid == g_cc.my_node_id) {
if (target_nid == cc->my_node_id) {
use_self = 1;
} else {
struct TOPO_GROUP* grp = resolve_chat_group(channel_id);
if (!node_is_alive(grp, target_nid)) {
struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id);
if (!node_is_alive(inst, grp, target_nid)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC,
"invite_build: node 0x%016llx not reachable in ch=%llu",
(unsigned long long)target_nid, (unsigned long long)channel_id);
return -1;
}
has_both_public = node_has_both_public(target_nid);
has_both_public = node_has_both_public(inst, target_nid);
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: manual ch=%llu target=0x%016llx self=%d both=%d",
(unsigned long long)channel_id, (unsigned long long)target_nid, use_self, has_both_public);
@ -309,7 +316,7 @@ int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: join_key generation failed");
return -1;
}
if (get_node_pubkey(target_nid, use_self, data.pubkey) != 0) {
if (get_node_pubkey(inst, target_nid, use_self, data.pubkey) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: no pubkey for node=0x%016llx",
(unsigned long long)target_nid);
return -1;
@ -317,11 +324,11 @@ int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
/* ── адреса ── */
if (use_self) {
data.addrCount = (uint8_t)collect_self_addrs(data.addrs, INVITE_ADDR_MAX);
collect_self_reality(&data.reality);
data.addrCount = (uint8_t)collect_self_addrs(inst, data.addrs, INVITE_ADDR_MAX);
collect_self_reality(inst, &data.reality);
} else {
int dir_only = has_both_public ? 1 : 0;
data.addrCount = (uint8_t)collect_node_addrs_from_db(target_nid, data.addrs, INVITE_ADDR_MAX, dir_only, &data.reality);
data.addrCount = (uint8_t)collect_node_addrs_from_db(inst, target_nid, data.addrs, INVITE_ADDR_MAX, dir_only, &data.reality);
}
if (data.addrCount == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: no addrs for node=0x%016llx",
@ -340,28 +347,30 @@ int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
}
/* ── регистрация join-ключа на connection-узле (или локально, если target==self) ── */
chat_join_register_key(g_cc.inst, channel_id, target_nid, data.join_key);
chat_join_register_key(inst, channel_id, target_nid, data.join_key);
return 0;
}
int chat_invite_best_node(uint64_t channel_id, uint64_t* out_node_id) {
int chat_invite_best_node(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_node_id) {
struct chat_core_ctx* cc = CC(inst);
if (!out_node_id) return -1;
if (!g_cc.initialized || !g_cc.inst || !g_cc.db) {
if (!cc || !cc->initialized || !inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: best_node — not initialized");
return -1;
}
*out_node_id = resolve_auto_node(channel_id, NULL, NULL);
*out_node_id = resolve_auto_node(inst, channel_id, NULL, NULL);
return 0;
}
int chat_invite_candidate_nodes(uint64_t channel_id, uint64_t* out_ids, int max_ids) {
int chat_invite_candidate_nodes(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t* out_ids, int max_ids) {
struct chat_core_ctx* cc = CC(inst);
if (!out_ids || max_ids <= 0) return -1;
if (!g_cc.initialized || !g_cc.inst || !g_cc.db) {
if (!cc || !cc->initialized || !inst || !cc->db) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized");
return -1;
}
struct TOPO_GROUP* grp = resolve_chat_group(channel_id);
struct TOPO_GROUP* grp = resolve_chat_group(inst, channel_id);
if (!grp) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — group not found ch=%llu",
(unsigned long long)channel_id);
@ -376,16 +385,16 @@ int chat_invite_candidate_nodes(uint64_t channel_id, uint64_t* out_ids, int max_
char sql[512];
snprintf(sql, sizeof(sql), "SELECT node_id FROM \"%s\" ORDER BY node_id", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(g_cc.db, sql, -1, &st, NULL) != SQLITE_OK) {
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — query failed ch=%llu err=%s",
(unsigned long long)channel_id, sqlite3_errmsg(g_cc.db));
(unsigned long long)channel_id, sqlite3_errmsg(cc->db));
return -1;
}
int cnt = 0;
while (sqlite3_step(st) == SQLITE_ROW && cnt < max_ids) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
if (node_is_alive(grp, nid)) out_ids[cnt++] = nid;
if (node_is_alive(inst, grp, nid)) out_ids[cnt++] = nid;
}
sqlite3_finalize(st);
return cnt;
@ -394,18 +403,19 @@ int chat_invite_candidate_nodes(uint64_t channel_id, uint64_t* out_ids, int max_
/* ── трамплины для gui_bridge ── */
void chat_invite_candidates_trampoline(void* arg) {
if (!arg) return;
struct chat_invite_cand_req* req = (struct chat_invite_cand_req*)arg;
if (!req) return;
char ch_id[64];
strncpy(ch_id, (const char*)arg, sizeof(ch_id) - 1);
strncpy(ch_id, req->ch_id, sizeof(ch_id) - 1);
ch_id[sizeof(ch_id) - 1] = '\0';
u_free(arg);
if (!g_cc.initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized"); return; }
if (!CC(req->inst) || !CC(req->inst)->initialized) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "invite_build: candidates — not initialized"); return; }
uint64_t channel_id = strtoull(ch_id, NULL, 10);
uint64_t auto_id = 0;
chat_invite_best_node(channel_id, &auto_id);
chat_invite_best_node(req->inst, channel_id, &auto_id);
uint64_t ids[INVITE_ADDR_MAX];
int cnt = chat_invite_candidate_nodes(channel_id, ids, INVITE_ADDR_MAX);
int cnt = chat_invite_candidate_nodes(req->inst, channel_id, ids, INVITE_ADDR_MAX);
if (cnt < 0) cnt = 0;
size_t ch_len = strlen(ch_id);
@ -418,7 +428,7 @@ void chat_invite_candidates_trampoline(void* arg) {
uint16_t u16 = (uint16_t)cnt;
memcpy(evt + 1 + ch_len + 8, &u16, 2);
for (int i = 0; i < cnt; i++) memcpy(evt + 1 + ch_len + 8 + 2 + i * 8, &ids[i], 8);
chat_event_post(CHAT_EVT_INVITE_CANDIDATES, evt, (int)evt_sz);
chat_event_post(req->inst, CHAT_EVT_INVITE_CANDIDATES, evt, (int)evt_sz);
u_free(evt);
}
@ -438,7 +448,7 @@ void chat_invite_build_link_trampoline(void* arg) {
}
char link[1024];
int ok = chat_invite_build_link(channel_id, target, pass, link, sizeof(link));
int ok = chat_invite_build_link(req->inst, channel_id, target, pass, link, sizeof(link));
size_t ch_len = strlen(req->ch_id);
size_t link_len = (ok == 0) ? strlen(link) : 0;
@ -450,7 +460,7 @@ void chat_invite_build_link_trampoline(void* arg) {
uint16_t u16 = (uint16_t)link_len;
memcpy(evt + 1 + ch_len, &u16, 2);
if (link_len) memcpy(evt + 1 + ch_len + 2, link, link_len);
chat_event_post(CHAT_EVT_INVITE_LINK_READY, evt, (int)evt_sz);
chat_event_post(req->inst, CHAT_EVT_INVITE_LINK_READY, evt, (int)evt_sz);
u_free(evt);
u_free(req);
}

12
src/chat/invite_build.h

@ -15,32 +15,36 @@
extern "C" {
#endif
struct UTUN_INSTANCE;
/* Собрать invite-ссылку для канала.
* target_node_id = 0 → auto (лучший достижимый мембер, иначе self).
* target_node_id != 0 → конкретный узел (должен быть достижимым мембером канала).
* password = NULL → без пароля.
* Возвращает 0 и заполняет out, либо -1 (детали в логе). */
int chat_invite_build_link(uint64_t channel_id, uint64_t target_node_id,
int chat_invite_build_link(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t target_node_id,
const char* password, char* out, size_t out_size);
/* Список достижимых узлов-кандидатов канала (включая self) — тот же фильтр, что в auto.
* Возвращает количество (0..max_ids), либо -1. */
int chat_invite_candidate_nodes(uint64_t channel_id, uint64_t* out_ids, int max_ids);
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. */
int chat_invite_best_node(uint64_t channel_id, uint64_t* out_node_id);
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)).
* Результат: CHAT_EVT_INVITE_LINK_READY. */
struct chat_invite_build_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t target_node_id; /* 0 = auto */
char password[128];
};
void chat_invite_build_link_trampoline(void* arg);
/* Трамплин (arg = u_strdup(ch_id)). Результат: CHAT_EVT_INVITE_CANDIDATES
/* Трамплин (arg = u_malloc(struct chat_invite_cand_req)). Результат: CHAT_EVT_INVITE_CANDIDATES
* (формат: [ch_id_len:1][ch_id][auto_node_id:8][count:2][node_id:8]*). */
struct chat_invite_cand_req { struct UTUN_INSTANCE* inst; char ch_id[64]; };
void chat_invite_candidates_trampoline(void* arg);
#ifdef __cplusplus

50
src/chat/member_sync.c

@ -240,25 +240,25 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level,
/* ── node_props_changed (adm_tags) callbacks ── */
struct ms_props_cbk { node_props_changed_fn fn; void* arg; struct ms_props_cbk* next; };
static struct ms_props_cbk* g_props_cbks = NULL;
static void _fire_props_changed(uint64_t node_id, const char* adm_tags, const char* channel_id) {
struct ms_props_cbk* pc = g_props_cbks;
static void _fire_props_changed(struct UTUN_INSTANCE* inst, uint64_t node_id, const char* adm_tags, const char* channel_id) {
if (!inst || !CC(inst)) return;
struct ms_props_cbk* pc = CC(inst)->props_cbks;
while (pc) { pc->fn(node_id, adm_tags, channel_id, pc->arg); pc = pc->next; }
}
void member_sync_add_props_cbk(node_props_changed_fn fn, void* arg) {
if (!fn) return;
void member_sync_add_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg) {
if (!inst || !fn || !CC(inst)) return;
struct ms_props_cbk* e = u_malloc(sizeof(*e));
if (!e) return;
e->fn = fn; e->arg = arg;
e->next = g_props_cbks;
g_props_cbks = e;
e->next = CC(inst)->props_cbks;
CC(inst)->props_cbks = e;
}
void member_sync_remove_props_cbk(node_props_changed_fn fn, void* arg) {
if (!fn) return;
struct ms_props_cbk** p = &g_props_cbks;
void member_sync_remove_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg) {
if (!inst || !fn || !CC(inst)) return;
struct ms_props_cbk** p = &CC(inst)->props_cbks;
while (*p) {
if ((*p)->fn == fn && (*p)->arg == arg) {
struct ms_props_cbk* r = *p;
@ -272,27 +272,27 @@ void member_sync_remove_props_cbk(node_props_changed_fn fn, void* arg) {
/* ── Коллбэк применения рекорда мембера (ready-хендлинг join на connection-узле) ── */
struct ms_apply_cbk { member_apply_cbk_fn fn; void* arg; struct ms_apply_cbk* next; };
static struct ms_apply_cbk* g_apply_cbks = NULL;
static void _fire_apply_cbk(const char* ch_id, uint64_t node_id,
static void _fire_apply_cbk(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id,
const uint8_t* x25519, uint64_t signed_by,
const uint8_t* signature) {
struct ms_apply_cbk* ac = g_apply_cbks;
if (!inst || !CC(inst)) return;
struct ms_apply_cbk* ac = CC(inst)->apply_cbks;
while (ac) { ac->fn(ch_id, node_id, x25519, signed_by, signature, ac->arg); ac = ac->next; }
}
void member_sync_add_apply_cbk(member_apply_cbk_fn fn, void* arg) {
if (!fn) return;
void member_sync_add_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_fn fn, void* arg) {
if (!inst || !fn || !CC(inst)) return;
struct ms_apply_cbk* e = u_malloc(sizeof(*e));
if (!e) return;
e->fn = fn; e->arg = arg;
e->next = g_apply_cbks;
g_apply_cbks = e;
e->next = CC(inst)->apply_cbks;
CC(inst)->apply_cbks = e;
}
void member_sync_remove_apply_cbk(member_apply_cbk_fn fn, void* arg) {
if (!fn) return;
struct ms_apply_cbk** p = &g_apply_cbks;
void member_sync_remove_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_fn fn, void* arg) {
if (!inst || !fn || !CC(inst)) return;
struct ms_apply_cbk** p = &CC(inst)->apply_cbks;
while (*p) {
if ((*p)->fn == fn && (*p)->arg == arg) {
struct ms_apply_cbk* r = *p;
@ -329,7 +329,7 @@ static void _ms_on_bgp_node(struct TOPO_GROUP* group, uint64_t node_id, int even
DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: bgp_node ev=%d nid=0x%016llx tags=%s",
MS_ID, event, (unsigned long long)node_id, tags);
_fire_props_changed(node_id, tags[0] ? tags : NULL, group->channel_id);
_fire_props_changed(inst, node_id, tags[0] ? tags : NULL, group->channel_id);
}
static int _sig_is_zero64(const uint8_t* sig) {
@ -840,13 +840,9 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer,
if (r & MS_APPLY_CHANGED) {
changed_count++;
/* fire node_props_changed callbacks */
struct ms_props_cbk* pc = g_props_cbks;
if (pc) {
const char* tags_final = atags[0] ? atags : NULL;
while (pc) { pc->fn(nid, tags_final, ns, pc->arg); pc = pc->next; }
}
_fire_props_changed(inst, nid, atags[0] ? atags : NULL, ns);
/* fire apply callbacks (ready-хендлинг join на connection-узле) */
_fire_apply_cbk(ns, nid, x25, sb, sgn);
_fire_apply_cbk(inst, ns, nid, x25, sb, sgn);
}
if (r & MS_APPLY_STALE) {
/* у нас версия новее — отправить наш полный рекорд автору */

8
src/chat/member_sync.h

@ -100,8 +100,8 @@ int member_sync_validate_pubkey(struct UTUN_INSTANCE* inst, const char* ch_id,
typedef void (*member_apply_cbk_fn)(const char* ch_id, uint64_t node_id,
const uint8_t* x25519, uint64_t signed_by,
const uint8_t* signature, void* arg);
void member_sync_add_apply_cbk(member_apply_cbk_fn fn, void* arg);
void member_sync_remove_apply_cbk(member_apply_cbk_fn fn, void* arg);
void member_sync_add_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_fn fn, void* arg);
void member_sync_remove_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_fn fn, void* arg);
/*
* Инициализировать модуль: создаёт merkle_sync с коллбэками для мемберов
@ -237,8 +237,8 @@ const uint8_t* member_sync_get_hash(struct UTUN_INSTANCE* inst,
* Многоподписочный — можно добавить несколько подписчиков.
*/
typedef void (*node_props_changed_fn)(uint64_t node_id, const char* adm_tags, const char* channel_id, void* arg);
void member_sync_add_props_cbk(node_props_changed_fn fn, void* arg);
void member_sync_remove_props_cbk(node_props_changed_fn fn, void* arg);
void member_sync_add_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg);
void member_sync_remove_props_cbk(struct UTUN_INSTANCE* inst, node_props_changed_fn fn, void* arg);
#ifdef __cplusplus
}

4
src/config_parser.c

@ -1231,7 +1231,7 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename)
if (strcmp(key, "db_path") == 0) {
assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value);
} else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 &&
chat_setting_set(key, value) < 0) {
chat_setting_set_state(&cfg->global.chat_settings, key, value) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, media_download_max_peers", filename, line_num, key);
}
break;
@ -1391,7 +1391,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
break;
case SECTION_NETWORK: if (cur_network) parse_network(key, value, cur_network, filename, line_num); break;
case SECTION_NTP: if (strcmp(key, "enabled") == 0) cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "server") == 0) { struct CFG_NTP_SERVER *ns = u_calloc(1, sizeof(struct CFG_NTP_SERVER)); if (ns) { strncpy(ns->name, value, sizeof(ns->name) - 1); ns->name[sizeof(ns->name) - 1] = '\0'; ns->next = cfg->global.ntp_servers; cfg->global.ntp_servers = ns; cfg->global.ntp_server_count++; } } else if (strcmp(key, "interval") == 0) { cfg->global.ntp_resync_interval = atoi(value); if (cfg->global.ntp_resync_interval < 60) cfg->global.ntp_resync_interval = 60; } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown ntp option '%s'", filename, line_num, key); break;
case SECTION_CHATSERVER: cfg->global.chatserver_enabled = 1; cfg->global.db_sync_enabled = 1; if (strcmp(key, "db_path") == 0) assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value); else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 && chat_setting_set(key, value) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, media_download_max_peers", filename, line_num, key); } break;
case SECTION_CHATSERVER: cfg->global.chatserver_enabled = 1; cfg->global.db_sync_enabled = 1; if (strcmp(key, "db_path") == 0) assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value); else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 && chat_setting_set_state(&cfg->global.chat_settings, key, value) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, media_download_max_peers", filename, line_num, key); } break;
case SECTION_GUI: break;
case SECTION_REALITY: parse_reality(key, value, &cfg->global.reality, filename, line_num); break;
case SECTION_SOCKS: parse_socks(key, value, &cfg->global, filename, line_num); break;

2
src/config_parser.h

@ -7,6 +7,7 @@
#include "../lib/platform_compat.h"
#include "secure_channel.h"
#include "transport_layer/reality.h"
#include "chat/chat_setting.h"
#ifdef __cplusplus
extern "C" {
@ -221,6 +222,7 @@ struct global_config {
uint64_t chatserver_storage_total_size; // bytes, общий лимит хранилища (0 = безлимит)
uint64_t chatserver_storage_unit_size; // bytes, лимит одного файла (0 = отключено)
char headless_control_bind[64]; // ip:port для headless chat CLI (пусто = отключено)
struct chat_setting_state chat_settings; // chat-настройки из [chatserver] (per-instance)
// Reality-камуфляж ([reality] section)
struct reality_config reality;

785
src/dm/dm_core.c

@ -0,0 +1,785 @@
/*
* dm_core.c — прямой p2p чат между двумя пользователями (DM)
*
* Отдельная подсистема (не канал): беседы хранятся в dm_conversations,
* сообщения в dm_messages, синхронизация идёт напрямую через etcp_router
* (сервис 0x34), при недоступности пира — через dm_mailbox (0x35).
*
* Два независимых направленных потока (я→пир, пир→я) с монотонным seq.
* Каноническое тело сообщения — см. dm_core.h (общее с dm_mailbox).
*/
#include "dm_core.h"
#include "dm_crypto.h"
#include "dm_mailbox.h"
#include "../chat/chat_core.h"
#include "../chat/chat_event.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
#include "../routing_layer/etcp_router.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/topo_group.h"
#include "../transport_layer/etcp_api.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/secure_channel.h"
#include "../../lib/mem.h"
#include "../../lib/ll_queue.h"
#include "../../lib/debug_config.h"
#include "../../lib/platform_compat.h"
#include <string.h>
#include <stdlib.h>
#include <sqlite3.h>
#define DM_ID "dm_core"
#define DM_DATA_MAX 1024 /* макс размер открытого текста сообщения (v1 — текст) */
/* Подкоманды протокола DM (первый байт payload после ROUTER_SVC-заголовка). */
#define DM_SUBCMD_MSG 0x01 /* [msg] — сообщение */
#define DM_SUBCMD_ACK 0x02 /* [conv_id:8][seq:8] — подтверждение приёма */
#define DM_SUBCMD_HELLO 0x03 /* [conv_id:8][last_out_seq:8][last_in_seq:8] — catch-up при connect */
/* Per-instance состояние модуля. */
struct dm_state {
struct UTUN_INSTANCE* inst;
sqlite3* db;
int initialized;
};
/* ── вспомогательные ── */
static struct dm_state* dm_of(struct UTUN_INSTANCE* inst) {
return inst ? inst->dm : NULL;
}
/* Выполнить SQL без результата. Возвращает 0/ошибку. */
static int dm_exec(struct dm_state* dm, const char* sql) {
char* err = NULL;
if (sqlite3_exec(dm->db, sql, NULL, NULL, &err) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: sql failed: %s (%s)", DM_ID, sql, err ? err : "?");
if (err) sqlite3_free(err);
return -1;
}
return 0;
}
/* Создать таблицы бесед и сообщений (общие для всех узлов). */
static void dm_create_tables(struct dm_state* dm) {
dm_exec(dm, "CREATE TABLE IF NOT EXISTS dm_conversations ("
" conv_id TEXT PRIMARY KEY,"
" peer_node_id INTEGER NOT NULL,"
" peer_x25519 BLOB NOT NULL,"
" peer_ed25519 BLOB NOT NULL,"
" peer_name TEXT DEFAULT '',"
" group_id INTEGER DEFAULT 0,"
" last_out_seq INTEGER DEFAULT 0,"
" last_in_seq INTEGER DEFAULT 0,"
" created_at INTEGER DEFAULT 0,"
" last_ts INTEGER DEFAULT 0)");
dm_exec(dm, "CREATE TABLE IF NOT EXISTS dm_messages ("
" conv_id TEXT NOT NULL,"
" dir INTEGER NOT NULL," /* 0=входящее, 1=исходящее */
" seq INTEGER NOT NULL,"
" ts INTEGER NOT NULL,"
" author INTEGER NOT NULL,"
" ct TEXT DEFAULT '',"
" data BLOB," /* зашифрованное тело (ciphertext+tag) */
" sig BLOB," /* Ed25519 автора */
" PRIMARY KEY (conv_id, dir, seq))");
}
/* Прочитать оба pubkey узла (x25519 + ed25519) из таблицы nodes. 0=ок, <0=нет. */
static int dm_node_pubkeys(struct dm_state* dm, uint64_t node_id, uint8_t x25519[32], uint8_t ed25519[32]) {
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT x25519_pubkey, ed25519_pubkey FROM nodes WHERE node_id=?",
-1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
int rc = -1;
if (sqlite3_step(st) == SQLITE_ROW) {
const uint8_t* x = (const uint8_t*)sqlite3_column_blob(st, 0);
const uint8_t* e = (const uint8_t*)sqlite3_column_blob(st, 1);
if (x && e && sqlite3_column_bytes(st, 0) >= 32 && sqlite3_column_bytes(st, 1) >= 32) {
memcpy(x25519, x, 32);
memcpy(ed25519, e, 32);
rc = 0;
}
}
sqlite3_finalize(st);
return rc;
}
/* Есть ли общая группа с пиром (anti-spam: принимаем DM только от знакомых из общих каналов). */
static int dm_share_group(struct dm_state* dm, uint64_t peer_id) {
uint64_t* ch_ids = NULL;
int ch_count = 0;
if (topo_node_sqlite_get_member_channels(dm->db, dm->inst->node_id, &ch_ids, &ch_count) != 0)
return 0;
int shared = 0;
for (int i = 0; i < ch_count; i++) {
char ch[64]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)ch_ids[i]);
if (topo_node_sqlite_member_in_channel(dm->db, ch, peer_id)) { shared = 1; break; }
}
if (ch_ids) u_free(ch_ids);
return shared;
}
/* Доступен ли пир для прямой доставки: живой ETCP-линк или присутствие в BGP-группе. */
static int dm_peer_reachable(struct dm_state* dm, uint64_t peer_id) {
struct ETCP_CONN* conn = instance_find_conn(dm->inst, peer_id);
if (conn && conn->links_up && conn->initialized) return 1;
if (dm->inst->topo_groups && dm->inst->topo_groups->group_list) {
struct ll_entry* e = dm->inst->topo_groups->group_list->head;
while (e) {
struct TOPO_GROUP* g = (struct TOPO_GROUP*)e;
if (g->group_type == TOPO_GROUP_TYPE_CHAT && topo_node_find_by_id(g, peer_id)) return 1;
e = e->next;
}
}
return 0;
}
/* Отправить payload подкомандой subcmd в dst через etcp_router (mode=0, своё E2E). */
static int dm_route_send(struct dm_state* dm, 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;
entry->dgram[1] = subcmd;
if (body_len) memcpy(entry->dgram + 2, body, body_len);
entry->len = (uint16_t)(2 + body_len);
int rc = etcp_route_send(dm->inst, group_id, dst, entry, 1, 0);
if (rc != 0) { queue_dgram_free(entry); queue_entry_free(entry); return -1; }
return 0;
}
/* ── работа с беседой ── */
/* Загрузить беседу из БД в структуру. Возвращает 0/ошибку. */
struct dm_conv {
char conv_id[64];
uint64_t peer_node_id;
uint8_t peer_x25519[32];
uint8_t peer_ed25519[32];
char peer_name[64];
uint64_t group_id;
uint8_t content_key[DM_CONTENT_KEY_SIZE];
uint64_t last_out_seq;
uint64_t last_in_seq;
};
static int dm_conv_load(struct dm_state* dm, const char* conv_id, struct dm_conv* c) {
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT peer_node_id, peer_x25519, peer_ed25519, COALESCE(peer_name,''),"
" COALESCE(group_id,0), last_out_seq, last_in_seq FROM dm_conversations WHERE conv_id=?",
-1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC);
int rc = -1;
if (sqlite3_step(st) == SQLITE_ROW) {
memset(c, 0, sizeof(*c));
snprintf(c->conv_id, sizeof(c->conv_id), "%s", conv_id);
c->peer_node_id = (uint64_t)sqlite3_column_int64(st, 0);
const uint8_t* x = (const uint8_t*)sqlite3_column_blob(st, 1);
const uint8_t* e = (const uint8_t*)sqlite3_column_blob(st, 2);
if (x && sqlite3_column_bytes(st, 1) >= 32) memcpy(c->peer_x25519, x, 32);
if (e && sqlite3_column_bytes(st, 2) >= 32) memcpy(c->peer_ed25519, e, 32);
const char* name = (const char*)sqlite3_column_text(st, 3);
if (name) snprintf(c->peer_name, sizeof(c->peer_name), "%s", name);
c->group_id = (uint64_t)sqlite3_column_int64(st, 4);
c->last_out_seq = (uint64_t)sqlite3_column_int64(st, 5);
c->last_in_seq = (uint64_t)sqlite3_column_int64(st, 6);
dm_derive_content_key(dm->inst->my_keys.private_key, c->peer_x25519, c->content_key);
rc = 0;
}
sqlite3_finalize(st);
return rc;
}
/* Вставить/обновить беседу в БД. Возвращает 0/ошибку. */
static int dm_conv_save(struct dm_state* dm, const struct dm_conv* c) {
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"INSERT INTO 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)"
" VALUES(?,?,?,?,?,?,?,?,?,0)"
" ON CONFLICT(conv_id) DO UPDATE SET"
" peer_x25519=excluded.peer_x25519, peer_ed25519=excluded.peer_ed25519,"
" peer_name=excluded.peer_name, group_id=excluded.group_id,"
" last_out_seq=excluded.last_out_seq, last_in_seq=excluded.last_in_seq",
-1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_text(st, 1, c->conv_id, -1, SQLITE_STATIC);
sqlite3_bind_int64(st, 2, (sqlite3_int64)c->peer_node_id);
sqlite3_bind_blob(st, 3, c->peer_x25519, 32, SQLITE_STATIC);
sqlite3_bind_blob(st, 4, c->peer_ed25519, 32, SQLITE_STATIC);
sqlite3_bind_text(st, 5, c->peer_name, -1, SQLITE_STATIC);
sqlite3_bind_int64(st, 6, (sqlite3_int64)c->group_id);
sqlite3_bind_int64(st, 7, (sqlite3_int64)c->last_out_seq);
sqlite3_bind_int64(st, 8, (sqlite3_int64)c->last_in_seq);
sqlite3_bind_int64(st, 9, (sqlite3_int64)ntp_time_get_seconds(dm->inst));
int rc = sqlite3_step(st) == SQLITE_DONE ? 0 : -1;
sqlite3_finalize(st);
return rc;
}
/* ── сборка/разбор канонического тела ── */
/* Собрать каноническое тело сообщения (зашифровать + подписать).
* out — буфер (u_malloc), *out_len — длина. dir=1 для исходящего. */
static int dm_build_body(struct dm_state* dm, const struct dm_conv* c, uint64_t seq, uint64_t ts,
const char* ct, const uint8_t* data, uint32_t data_len,
uint8_t** out, size_t* out_len) {
if (!c || !ct || (!data && data_len) || data_len > DM_DATA_MAX) return -1;
uint8_t ct_len = (uint8_t)strnlen(ct, 255);
uint64_t conv_num = (uint64_t)strtoull(c->conv_id, NULL, 10);
uint8_t nonce[DM_NONCE_SIZE];
dm_build_nonce(conv_num, dm->inst->node_id, seq, nonce);
/* шифротекст = data_len + tag(16) */
uint8_t* enc = u_malloc(data_len + DM_TAG_SIZE);
if (!enc) return -1;
size_t enc_len = 0;
if (dm_encrypt(c->content_key, nonce, data, data_len, enc, &enc_len) != 0) {
u_free(enc);
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: encrypt failed conv=%s", DM_ID, c->conv_id);
return -1;
}
uint64_t author = dm->inst->node_id;
size_t body_len = DM_MSG_FIXED_HDR + ct_len + 2 + enc_len + DM_MSG_SIG_SIZE;
uint8_t* body = u_malloc(body_len);
if (!body) { u_free(enc); return -1; }
size_t off = 0;
memcpy(body + off, &conv_num, 8); off += 8;
memcpy(body + off, &seq, 8); off += 8;
memcpy(body + off, &ts, 8); off += 8;
memcpy(body + off, &author, 8); off += 8;
body[off++] = ct_len;
memcpy(body + off, ct, ct_len); off += ct_len;
uint16_t el = (uint16_t)enc_len;
memcpy(body + off, &el, 2); off += 2;
memcpy(body + off, enc, enc_len); off += enc_len;
/* подпись по всем полям до sig */
sc_ed25519_sign(dm->inst->my_ed25519_privkey, body, off, body + off);
u_free(enc);
*out = body;
*out_len = body_len;
return 0;
}
/* ── обработка входящего сообщения ── */
/* Обработать каноническое тело входящего сообщения (из live-доставки или mailbox):
* проверить подпись, расшифровать, dedup по seq, вставить, отослать ACK, событие GUI. */
static void dm_on_msg(struct dm_state* dm, const uint8_t* msg, size_t mlen) {
if (!msg || mlen < DM_MSG_FIXED_HDR + DM_MSG_SIG_SIZE) return;
uint64_t conv_num; memcpy(&conv_num, msg, 8);
uint64_t seq; memcpy(&seq, msg + 8, 8);
uint64_t ts; memcpy(&ts, msg + 16, 8);
uint64_t author; memcpy(&author, msg + 24, 8);
uint8_t ct_len = msg[32];
const uint8_t* ct = msg + 33;
uint16_t enc_len; memcpy(&enc_len, msg + 33 + ct_len, 2);
const uint8_t* enc = msg + 35 + ct_len;
const uint8_t* sig = msg + mlen - DM_MSG_SIG_SIZE;
if (author == dm->inst->node_id) return; /* своё (эхо) — игнор */
char conv_id[64];
snprintf(conv_id, sizeof(conv_id), "%llu", (unsigned long long)conv_num);
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) != 0) {
/* первое сообщение: автоприём при общей группе + совпадение conv_id */
if (dm_derive_conv_id(dm->inst->node_id, author) != conv_num) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: conv_id mismatch from 0x%016llx", DM_ID, (unsigned long long)author);
return;
}
if (!dm_share_group(dm, author)) {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: no shared group with 0x%016llx — reject DM", DM_ID, (unsigned long long)author);
return;
}
memset(&c, 0, sizeof(c));
snprintf(c.conv_id, sizeof(c.conv_id), "%s", conv_id);
c.peer_node_id = author;
if (dm_node_pubkeys(dm, author, c.peer_x25519, c.peer_ed25519) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no pubkeys for 0x%016llx", DM_ID, (unsigned long long)author);
return;
}
dm_derive_content_key(dm->inst->my_keys.private_key, c.peer_x25519, c.content_key);
c.group_id = 0;
dm_conv_save(dm, &c);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: auto-created conversation conv=%s peer=0x%016llx",
DM_ID, conv_id, (unsigned long long)author);
}
/* проверка подписи автора по всем полям до sig */
if (sc_ed25519_verify(c.peer_ed25519, msg, mlen - DM_MSG_SIG_SIZE, sig) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: invalid signature conv=%s seq=%llu", DM_ID, conv_id, (unsigned long long)seq);
return;
}
/* dedup: вставляем только новое */
if (seq <= c.last_in_seq) {
/* уже есть — только подтвердить */
uint8_t ack[16]; memcpy(ack, &conv_num, 8); memcpy(ack + 8, &seq, 8);
dm_route_send(dm, c.group_id, author, DM_SUBCMD_ACK, ack, sizeof(ack));
return;
}
/* расшифровать (nonce детерминирован по conv_id+author+seq) */
uint8_t nonce[DM_NONCE_SIZE];
dm_build_nonce(conv_num, author, seq, nonce);
uint8_t* plain = u_malloc(enc_len > DM_TAG_SIZE ? enc_len - DM_TAG_SIZE : 0);
if (!plain) return;
size_t plain_len = 0;
if (dm_decrypt(c.content_key, nonce, enc, enc_len, plain, &plain_len) != 0) {
u_free(plain);
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: decrypt failed conv=%s seq=%llu", DM_ID, conv_id, (unsigned long long)seq);
return;
}
/* вставить в dm_messages (dir=0, входящее) */
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"INSERT OR IGNORE INTO dm_messages(conv_id,dir,seq,ts,author,ct,data,sig)"
" VALUES(?,0,?,?,?,?,?,?)",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC);
sqlite3_bind_int64(st, 2, (sqlite3_int64)seq);
sqlite3_bind_int64(st, 3, (sqlite3_int64)ts);
sqlite3_bind_int64(st, 4, (sqlite3_int64)author);
sqlite3_bind_text(st, 5, (const char*)ct, ct_len, SQLITE_STATIC);
sqlite3_bind_blob(st, 6, enc, enc_len, SQLITE_STATIC);
sqlite3_bind_blob(st, 7, sig, DM_MSG_SIG_SIZE, SQLITE_STATIC);
sqlite3_step(st);
}
if (st) sqlite3_finalize(st);
c.last_in_seq = seq;
dm_conv_save(dm, &c);
/* подтвердить приём (нужно и для mailbox-доставки → storage удалит по ACK) */
uint8_t ack[16]; memcpy(ack, &conv_num, 8); memcpy(ack + 8, &seq, 8);
dm_route_send(dm, c.group_id, author, DM_SUBCMD_ACK, ack, sizeof(ack));
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: recv conv=%s seq=%llu author=0x%016llx ct=%.*s len=%zu",
DM_ID, conv_id, (unsigned long long)seq, (unsigned long long)author, ct_len, ct, plain_len);
/* событие GUI: [conv_id_len:1][conv_id][author_node_id:8] */
{
size_t cl = strlen(conv_id);
uint8_t evt[1 + 64 + 8];
evt[0] = (uint8_t)cl;
memcpy(evt + 1, conv_id, cl);
memcpy(evt + 1 + cl, &author, 8);
chat_event_post(dm->inst, CHAT_EVT_DM_MSG_RECEIVED, evt, 1 + (int)cl + 8);
}
u_free(plain);
}
/* ── обработка HELLO (catch-up) ── */
/* Пересобрать каноническое тело из сохранённых полей (для catch-up resend). */
static int dm_rebuild_body(uint64_t conv_num, uint64_t seq, uint64_t ts, uint64_t author,
const char* ct, const uint8_t* enc, int enc_len, const uint8_t sig[64],
uint8_t** out, size_t* out_len) {
uint8_t ctl = (uint8_t)strnlen(ct, 255);
size_t body_len = DM_MSG_FIXED_HDR + ctl + 2 + (size_t)enc_len + DM_MSG_SIG_SIZE;
uint8_t* body = u_malloc(body_len);
if (!body) return -1;
size_t off = 0;
memcpy(body + off, &conv_num, 8); off += 8;
memcpy(body + off, &seq, 8); off += 8;
memcpy(body + off, &ts, 8); off += 8;
memcpy(body + off, &author, 8); off += 8;
body[off++] = ctl;
memcpy(body + off, ct, ctl); off += ctl;
uint16_t el = (uint16_t)enc_len;
memcpy(body + off, &el, 2); off += 2;
memcpy(body + off, enc, enc_len); off += enc_len;
memcpy(body + off, sig, DM_MSG_SIG_SIZE);
*out = body;
*out_len = body_len;
return 0;
}
/* На connect обе стороны шлют HELLO со своими счётчиками; получив HELLO, досылаем
* пиру недостающие ему наши сообщения (его last_in_seq+1 .. наш last_out_seq). */
static void dm_on_hello(struct dm_state* dm, uint64_t conv_num, uint64_t from_peer, uint64_t peer_out_seq, uint64_t peer_in_seq) {
char conv_id[64];
snprintf(conv_id, sizeof(conv_id), "%llu", (unsigned long long)conv_num);
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) != 0) return;
if (peer_in_seq < c.last_out_seq) {
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT seq, ts, author, ct, data, sig FROM dm_messages"
" WHERE conv_id=? AND dir=1 AND seq>? ORDER BY seq",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC);
sqlite3_bind_int64(st, 2, (sqlite3_int64)peer_in_seq);
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t seq = (uint64_t)sqlite3_column_int64(st, 0);
uint64_t ts = (uint64_t)sqlite3_column_int64(st, 1);
uint64_t author = (uint64_t)sqlite3_column_int64(st, 2);
const char* ct = (const char*)sqlite3_column_text(st, 3);
const uint8_t* enc = (const uint8_t*)sqlite3_column_blob(st, 4);
int enc_len = sqlite3_column_bytes(st, 4);
const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(st, 5);
uint8_t* body = NULL; size_t body_len = 0;
if (dm_rebuild_body(conv_num, seq, ts, author, ct, enc, enc_len, sig, &body, &body_len) == 0) {
dm_route_send(dm, c.group_id, from_peer, DM_SUBCMD_MSG, body, body_len);
u_free(body);
}
}
}
if (st) sqlite3_finalize(st);
}
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: hello from 0x%016llx conv=%s out=%llu in=%llu",
DM_ID, (unsigned long long)from_peer, conv_id,
(unsigned long long)peer_out_seq, (unsigned long long)peer_in_seq);
}
/* ── обработка ACK ── */
/* Получено подтверждение доставки от пира — логируем (надёжность гарантирует etcp_router). */
static void dm_on_ack(uint64_t conv_num, uint64_t seq) {
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: ack conv=%llu seq=%llu", DM_ID,
(unsigned long long)conv_num, (unsigned long long)seq);
}
/* ── etcp_router recv (0x34) ── */
static void dm_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) {
if (!entry || !conn || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 1) {
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); }
return;
}
struct dm_state* dm = dm_of(conn->instance);
if (!dm || !dm->initialized) { queue_dgram_free(entry); queue_entry_free(entry); return; }
const uint8_t* d = entry->dgram;
uint64_t from_node; memcpy(&from_node, d + ROUTER_SVC_SRC_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 == DM_SUBCMD_MSG) {
size_t mlen = 0;
if (dm_msg_len(p + 1, plen - 1, &mlen))
dm_on_msg(dm, p + 1, mlen);
} else if (subcmd == DM_SUBCMD_ACK) {
if (plen >= 1 + 16) {
uint64_t conv_num; memcpy(&conv_num, p + 1, 8);
uint64_t seq; memcpy(&seq, p + 9, 8);
dm_on_ack(conv_num, seq);
}
} else if (subcmd == DM_SUBCMD_HELLO) {
if (plen >= 1 + 24) {
uint64_t conv_num; memcpy(&conv_num, p + 1, 8);
uint64_t out_seq; memcpy(&out_seq, p + 9, 8);
uint64_t in_seq; memcpy(&in_seq, p + 17, 8);
dm_on_hello(dm, conv_num, from_node, out_seq, in_seq);
}
} else {
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: unknown subcmd=%02x", DM_ID, subcmd);
}
queue_dgram_free(entry); queue_entry_free(entry);
}
/* ── connection status ── */
/* При поднятии соединения с пиром: HELLO (catch-up) + вытянуть mailbox. */
static void dm_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct dm_state* dm = dm_of(inst);
if (!conn || status != ETCP_CONN_STATUS_UP || !dm) return;
uint64_t peer = conn->peer_node_id;
if (!peer) return;
/* На подъём ЛЮБОГО соединения: HELLO для беседы с подключившимся пиром +
* PULL mailbox для бесед, чей пир не подключён напрямую (offline-доставка). */
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT conv_id, peer_node_id, last_out_seq, last_in_seq FROM dm_conversations",
-1, &st, NULL) != SQLITE_OK) return;
while (sqlite3_step(st) == SQLITE_ROW) {
const char* conv_id = (const char*)sqlite3_column_text(st, 0);
uint64_t conv_peer = (uint64_t)sqlite3_column_int64(st, 1);
uint64_t out_seq = (uint64_t)sqlite3_column_int64(st, 2);
uint64_t in_seq = (uint64_t)sqlite3_column_int64(st, 3);
uint64_t conv_num = strtoull(conv_id, NULL, 10);
if (conv_peer == peer) {
uint8_t hello[24];
memcpy(hello, &conv_num, 8);
memcpy(hello + 8, &out_seq, 8);
memcpy(hello + 16, &in_seq, 8);
/* group_id берём из строки беседы — для простоты HELLO по 0, маршрутизация глобальная */
dm_route_send(dm, 0, peer, DM_SUBCMD_HELLO, hello, sizeof(hello));
}
/* пир не подключён напрямую — тянем mailbox со storage-узлов */
struct ETCP_CONN* pc = instance_find_conn(inst, conv_peer);
if (!pc || !pc->links_up || !pc->initialized) {
dm_mailbox_pull(inst, conv_peer, in_seq);
}
}
sqlite3_finalize(st);
}
/* ── публичный API ── */
/* Начать беседу с пользователем. Создаёт запись если её нет, выводит ключи,
* обеспечивает прямое соединение (если есть общая группа). */
int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
const uint8_t peer_x25519[32], const uint8_t peer_ed25519[32],
const char* peer_name, const char* source_ch_id) {
if (!inst || !peer_node_id || !peer_x25519 || !peer_ed25519) return -1;
if (peer_node_id == inst->node_id) return -1;
struct dm_state* dm = dm_of(inst);
if (!dm || !dm->initialized) return -1;
uint64_t conv_num = dm_derive_conv_id(inst->node_id, peer_node_id);
char conv_id[64];
snprintf(conv_id, sizeof(conv_id), "%llu", (unsigned long long)conv_num);
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) == 0) {
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: conversation already exists conv=%s", DM_ID, conv_id);
return 0;
}
memset(&c, 0, sizeof(c));
snprintf(c.conv_id, sizeof(c.conv_id), "%s", conv_id);
c.peer_node_id = peer_node_id;
memcpy(c.peer_x25519, peer_x25519, 32);
memcpy(c.peer_ed25519, peer_ed25519, 32);
if (peer_name) snprintf(c.peer_name, sizeof(c.peer_name), "%s", peer_name);
c.group_id = source_ch_id ? strtoull(source_ch_id, NULL, 10) : 0;
dm_derive_content_key(inst->my_keys.private_key, c.peer_x25519, c.content_key);
if (dm_conv_save(dm, &c) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: conv_save failed conv=%s", DM_ID, conv_id);
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: started conv=%s peer=0x%016llx name=%s group=%llu",
DM_ID, conv_id, (unsigned long long)peer_node_id, c.peer_name,
(unsigned long long)c.group_id);
/* событие GUI */
{
size_t cl = strlen(conv_id);
uint8_t evt[1 + 64];
evt[0] = (uint8_t)cl;
memcpy(evt + 1, conv_id, cl);
chat_event_post(inst, CHAT_EVT_DM_CONV_UPDATED, evt, 1 + (int)cl);
}
return 0;
}
/* Отправить сообщение в беседу: зашифровать, подписать, сохранить, доставить. */
int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type,
const uint8_t* data, uint32_t data_len) {
if (!inst || !conv_id || !content_type || (!data && data_len)) return -1;
struct dm_state* dm = dm_of(inst);
if (!dm || !dm->initialized) return -1;
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no conversation %s", DM_ID, conv_id);
return -1;
}
uint64_t seq = c.last_out_seq + 1;
uint64_t ts = (uint64_t)(ntp_time_get_us(inst) / 1000);
uint8_t* body = NULL;
size_t body_len = 0;
if (dm_build_body(dm, &c, seq, ts, content_type, data, data_len, &body, &body_len) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: build body failed", DM_ID);
return -1;
}
/* сохранить локально (dir=1, исходящее) */
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"INSERT INTO dm_messages(conv_id,dir,seq,ts,author,ct,data,sig)"
" VALUES(?,1,?,?,?,?,?,?)",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC);
sqlite3_bind_int64(st, 2, (sqlite3_int64)seq);
sqlite3_bind_int64(st, 3, (sqlite3_int64)ts);
sqlite3_bind_int64(st, 4, (sqlite3_int64)inst->node_id);
sqlite3_bind_text(st, 5, content_type, -1, SQLITE_STATIC);
/* data/sig уже внутри body: data_enc по смещению 35+ct_len, sig в конце */
uint8_t ctl = (uint8_t)strnlen(content_type, 255);
const uint8_t* enc = body + 35 + ctl;
uint16_t enc_len; memcpy(&enc_len, body + 33 + ctl, 2);
sqlite3_bind_blob(st, 6, enc, enc_len, SQLITE_STATIC);
sqlite3_bind_blob(st, 7, body + body_len - DM_MSG_SIG_SIZE, DM_MSG_SIG_SIZE, SQLITE_STATIC);
sqlite3_step(st);
}
if (st) sqlite3_finalize(st);
c.last_out_seq = seq;
dm_conv_save(dm, &c);
/* доставка: доступен → прямой send, иначе → mailbox */
if (dm_peer_reachable(dm, c.peer_node_id)) {
dm_route_send(dm, c.group_id, c.peer_node_id, DM_SUBCMD_MSG, body, body_len);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: sent direct conv=%s seq=%llu → 0x%016llx",
DM_ID, conv_id, (unsigned long long)seq, (unsigned long long)c.peer_node_id);
} else {
dm_mailbox_put(inst, c.peer_node_id, inst->node_id, body, body_len);
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: peer offline conv=%s seq=%llu → mailbox",
DM_ID, conv_id, (unsigned long long)seq);
}
u_free(body);
return 0;
}
/* Вставка сообщения, полученного через mailbox (вызывается dm_mailbox по PULL_RESP).
* Обрабатывается тем же путём, что и live-сообщение (verify/decrypt/dedup/insert). */
void dm_core_inject_message(const uint8_t* msg, size_t len, void* arg) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg;
struct dm_state* dm = dm_of(inst);
if (!dm) return;
dm_on_msg(dm, msg, len);
}
/* ── жизненный цикл ── */
int dm_core_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (inst->dm && inst->dm->initialized) return 0;
struct dm_state* dm = u_calloc(1, sizeof(*dm));
if (!dm) return -1;
inst->dm = dm;
dm->inst = inst;
dm->db = chat_core_get_db(inst);
if (!dm->db) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no chat db", DM_ID); u_free(dm); inst->dm = NULL; return -1; }
dm_create_tables(dm);
if (etcp_router_bind(inst, ETCP_RT_ID_DM, dm_recv_cb) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: etcp_router_bind failed", DM_ID);
u_free(dm); inst->dm = NULL; return -1;
}
etcp_add_conn_status_cbk(inst, dm_on_conn_status, inst);
dm_mailbox_set_deliver_cb(inst, dm_core_inject_message, inst);
dm->initialized = 1;
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: initialized", DM_ID);
return 0;
}
void dm_core_destroy(struct UTUN_INSTANCE* inst) {
if (!inst || !inst->dm || !inst->dm->initialized) return;
etcp_remove_conn_status_cbk(inst, dm_on_conn_status, inst);
etcp_router_unbind(inst, ETCP_RT_ID_DM);
u_free(inst->dm);
inst->dm = NULL;
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: destroyed", DM_ID);
}
/* ── JSON для GUI/headless ── */
/* Список бесед в JSON. */
int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len) {
struct dm_state* dm = dm_of(inst);
if (!buf || !out_len || !dm || !dm->db) return -1;
int off = 0;
off += snprintf(buf + off, buf_size - (size_t)off, "[");
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db,
"SELECT conv_id, peer_node_id, COALESCE(peer_name,''), last_ts, last_out_seq, last_in_seq"
" FROM dm_conversations ORDER BY last_ts DESC",
-1, &st, NULL) == SQLITE_OK) {
int first = 1;
while (sqlite3_step(st) == SQLITE_ROW) {
const char* conv_id = (const char*)sqlite3_column_text(st, 0);
uint64_t peer = (uint64_t)sqlite3_column_int64(st, 1);
const char* name = (const char*)sqlite3_column_text(st, 2);
uint64_t last_ts = (uint64_t)sqlite3_column_int64(st, 3);
uint64_t out_seq = (uint64_t)sqlite3_column_int64(st, 4);
uint64_t in_seq = (uint64_t)sqlite3_column_int64(st, 5);
off += snprintf(buf + off, buf_size - (size_t)off,
"%s{\"conv_id\":\"%s\",\"peer_node_id\":%llu,\"peer_name\":\"%s\","
"\"last_ts\":%llu,\"out_seq\":%llu,\"in_seq\":%llu}",
first ? "" : ",", conv_id, (unsigned long long)peer,
name ? name : "", (unsigned long long)last_ts,
(unsigned long long)out_seq, (unsigned long long)in_seq);
first = 0;
}
}
if (st) sqlite3_finalize(st);
off += snprintf(buf + off, buf_size - (size_t)off, "]");
*out_len = (size_t)off;
return 0;
}
/* Сообщения беседы (расшифрованные) в JSON. */
int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len) {
struct dm_state* dm = dm_of(inst);
if (!conv_id || !buf || !out_len || !dm || !dm->db) return -1;
struct dm_conv c;
if (dm_conv_load(dm, conv_id, &c) != 0) return -1;
char sql[256];
if (count > 0)
snprintf(sql, sizeof(sql),
"SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY seq LIMIT %d OFFSET %d",
count, offset);
else
snprintf(sql, sizeof(sql),
"SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY seq");
int off = 0;
off += snprintf(buf + off, buf_size - (size_t)off, "[");
uint64_t conv_num = (uint64_t)strtoull(conv_id, NULL, 10);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(dm->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC);
int first = 1;
while (sqlite3_step(st) == SQLITE_ROW) {
int dir = sqlite3_column_int(st, 0);
uint64_t seq = (uint64_t)sqlite3_column_int64(st, 1);
uint64_t ts = (uint64_t)sqlite3_column_int64(st, 2);
uint64_t author = (uint64_t)sqlite3_column_int64(st, 3);
const char* ct = (const char*)sqlite3_column_text(st, 4);
const uint8_t* enc = (const uint8_t*)sqlite3_column_blob(st, 5);
int enc_len = sqlite3_column_bytes(st, 5);
/* расшифровать (nonce детерминирован по conv_id+author+seq) */
char text[DM_DATA_MAX + 1];
text[0] = '\0';
if (enc && enc_len >= (int)DM_TAG_SIZE) {
uint8_t nonce[DM_NONCE_SIZE];
dm_build_nonce(conv_num, author, seq, nonce);
uint8_t* plain = u_malloc((size_t)enc_len);
size_t plen = 0;
if (dm_decrypt(c.content_key, nonce, enc, (size_t)enc_len, plain, &plen) == 0) {
if (plen >= sizeof(text)) plen = sizeof(text) - 1;
memcpy(text, plain, plen);
text[plen] = '\0';
}
u_free(plain);
}
off += snprintf(buf + off, buf_size - (size_t)off,
"%s{\"dir\":%d,\"seq\":%llu,\"ts\":%llu,\"author\":%llu,\"ct\":\"%s\",\"data\":\"%s\"}",
first ? "" : ",", dir, (unsigned long long)seq, (unsigned long long)ts,
(unsigned long long)author, ct ? ct : "", text);
first = 0;
}
}
if (st) sqlite3_finalize(st);
off += snprintf(buf + off, buf_size - (size_t)off, "]");
*out_len = (size_t)off;
return 0;
}

76
src/dm/dm_core.h

@ -0,0 +1,76 @@
/*
* dm_core.h — прямой p2p чат между двумя пользователями (DM)
*
* Отдельная подсистема (НЕ канал): без TOPO_GROUP/member_sync/merkle.
* - conv_id и content_key детерминированно выводятся обеими сторонами
* из своих ключей и pubkey пира (см. dm_crypto.h) — без переговоров.
* - сообщения идут через etcp_router (прямое соединение или релей),
* при недоступности пира — через dm_mailbox (storage-узлы).
* - содержимое шифруется E2E (AES-256-CCM), запись подписывается Ed25519.
*
* Два независимых направленных потока (я→пир и пир→я) с монотонным seq.
*/
#ifndef DM_CORE_H
#define DM_CORE_H
#include <stdint.h>
#include <stddef.h>
#include <string.h>
struct UTUN_INSTANCE;
#ifdef __cplusplus
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]
* 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
/* Вычислить полную длину канонического тела из его заголовка.
* Возвращает 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;
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 (avail < total) return 0;
*out_len = total;
return 1;
}
/* Жизненный цикл: инициализация после 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/ключи, пытается установить прямое соединение.
* Возвращает 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],
const char* peer_name, const char* source_ch_id);
/* Отправить сообщение в беседу. Шифрует, подписывает, сохраняет локально и
* доставляет (прямое соединение при доступности пира, иначе mailbox).
* conv_id — строковое представление (как в dm_conversations.conv_id). */
int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type,
const uint8_t* data, uint32_t data_len);
/* Список бесед в JSON: [{conv_id,peer_node_id,peer_name,last_ts,out_seq,in_seq}] */
int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
/* Сообщения беседы в JSON (расшифрованные): [{dir,seq,ts,author,ct,data}]
* count=0 → все, offset=0 → с начала. */
int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len);
#ifdef __cplusplus
}
#endif
#endif /* DM_CORE_H */

128
src/dm/dm_crypto.c

@ -0,0 +1,128 @@
/*
* dm_crypto.c — криптография прямого чата (DM)
*/
#include "dm_crypto.h"
#include "../../lib/debug_config.h"
#define OPENSSL_API_COMPAT 0x10100000L
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <string.h>
#include <stdio.h>
#define DM_ID "dm_crypto"
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;
SHA256_CTX ctx;
uint8_t hash[32];
SHA256_Init(&ctx);
SHA256_Update(&ctx, "utun_dm_v1", 9);
SHA256_Update(&ctx, &lo, 8);
SHA256_Update(&ctx, &hi, 8);
SHA256_Final(hash, &ctx);
uint64_t id;
memcpy(&id, hash, 8);
id &= 0x7FFFFFFFFFFFFFFFULL;
if (id == 0) id = 1;
return id;
}
int dm_derive_content_key(const uint8_t my_priv[32], const uint8_t peer_pub[32],
uint8_t out[DM_CONTENT_KEY_SIZE]) {
if (!my_priv || !peer_pub || !out) return -1;
EVP_PKEY* my_pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, my_priv, 32);
if (!my_pkey) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: EVP_PKEY_new_raw_private_key failed", DM_ID); return -1; }
EVP_PKEY* peer_pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL, peer_pub, 32);
if (!peer_pkey) { EVP_PKEY_free(my_pkey); DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: EVP_PKEY_new_raw_public_key failed", DM_ID); return -1; }
EVP_PKEY_CTX* dctx = EVP_PKEY_CTX_new(my_pkey, NULL);
uint8_t shared[32];
size_t shared_len = sizeof(shared);
int rc = -1;
if (!dctx) goto out;
if (EVP_PKEY_derive_init(dctx) <= 0) goto out;
if (EVP_PKEY_derive_set_peer(dctx, peer_pkey) <= 0) goto out;
if (EVP_PKEY_derive(dctx, shared, &shared_len) <= 0 || shared_len != sizeof(shared)) goto out;
SHA256_CTX sctx;
SHA256_Init(&sctx);
SHA256_Update(&sctx, shared, sizeof(shared));
SHA256_Update(&sctx, "utun_dm_content", 15);
SHA256_Final(out, &sctx);
rc = 0;
out:
if (dctx) EVP_PKEY_CTX_free(dctx);
EVP_PKEY_free(my_pkey);
EVP_PKEY_free(peer_pkey);
if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: X25519 derive failed", DM_ID);
return rc;
}
void dm_build_nonce(uint64_t conv_id, uint64_t author, uint64_t seq, uint8_t nonce[DM_NONCE_SIZE]) {
SHA256_CTX ctx;
uint8_t hash[32];
SHA256_Init(&ctx);
SHA256_Update(&ctx, &conv_id, 8);
SHA256_Update(&ctx, &author, 8);
SHA256_Update(&ctx, &seq, 8);
SHA256_Final(hash, &ctx);
memcpy(nonce, hash, DM_NONCE_SIZE);
}
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;
EVP_CIPHER_CTX* ectx = EVP_CIPHER_CTX_new();
if (!ectx) return -1;
const EVP_CIPHER* cipher = EVP_aes_256_ccm();
int rc = -1;
if (encrypt) {
if (EVP_EncryptInit_ex(ectx, cipher, NULL, NULL, NULL) != 1
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_IVLEN, DM_NONCE_SIZE, NULL) != 1
|| 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, 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;
if (EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_GET_TAG, DM_TAG_SIZE, out + in_len) != 1) goto done;
*out_len = in_len + DM_TAG_SIZE;
} else {
if (in_len < DM_TAG_SIZE) goto done;
size_t body = in_len - DM_TAG_SIZE;
if (EVP_DecryptInit_ex(ectx, cipher, NULL, NULL, NULL) != 1
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_IVLEN, DM_NONCE_SIZE, NULL) != 1
|| 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, 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);
return rc;
}
int dm_encrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE],
const uint8_t* plain, size_t plain_len, uint8_t* out, size_t* out_len) {
return dm_ccm_crypt(key, nonce, plain, plain_len, out, out_len, 1, NULL);
}
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) {
return dm_ccm_crypt(key, nonce, ct, ct_len, out, out_len, 0, NULL);
}

50
src/dm/dm_crypto.h

@ -0,0 +1,50 @@
/*
* dm_crypto.h — криптография прямого чата (DM)
*
* Детерминированная идентичность и E2E-шифрование:
* - conv_id / content_key выводятся обеими сторонами из своих ключей и
* pubkey пира (известен из общей группы) — без переговоров.
* - шифрование AES-256-CCM с детерминированным nonce из (dir, seq).
*/
#ifndef DM_CRYPTO_H
#define DM_CRYPTO_H
#include <stdint.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define DM_CONTENT_KEY_SIZE 32
#define DM_NONCE_SIZE 13 /* AES-CCM требует ровно 13 байт */
#define DM_TAG_SIZE 16
/* conv_id = SHA256("utun_dm_v1" || min(a,b) || max(a,b))[..8] & 0x7FFF...,
* 63-битное число, одинаковое на обеих сторонах (одна ветка на пару). */
uint64_t dm_derive_conv_id(uint64_t a, uint64_t b);
/* content_key = SHA256(X25519(my_priv, peer_pub) || "utun_dm_content").
* Возвращает 0 при успехе, <0 при ошибке. */
int dm_derive_content_key(const uint8_t my_priv[32], const uint8_t peer_pub[32],
uint8_t out[DM_CONTENT_KEY_SIZE]);
/* Детерминированный nonce, уникальный на каждое сообщение:
* SHA256(conv_id || author || seq)[..13]. Одинаков на обеих сторонах,
* не пересекается между направлениями (author различается). */
void dm_build_nonce(uint64_t conv_id, uint64_t author, uint64_t seq, 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],
const uint8_t* plain, size_t plain_len, uint8_t* out, size_t* out_len);
/* Обратная операция. ct_len = plain_len + DM_TAG_SIZE. 0=ок, <0=ошибка. */
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);
#ifdef __cplusplus
}
#endif
#endif /* DM_CRYPTO_H */

353
src/dm/dm_mailbox.c

@ -0,0 +1,353 @@
/*
* dm_mailbox.c — offline-хранение DM-сообщений на storage-узлах
*
* Storage-узел хранит зашифрованные блобы в таблице dm_mail и отдаёт их
* получателю по PULL, удаляя после ACK. Отправитель кладёт блобы по PUT.
* Содержимое для storage-узла непрозрачно (E2E-шифрование + подпись автора).
*/
#include "dm_mailbox.h"
#include "dm_core.h"
#include "../chat/chat_core.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>
#define MB_ID "dm_mailbox"
/* Подкоманды протокола 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;
};
static struct dm_mb_state* mb_of(struct UTUN_INSTANCE* inst) {
return inst ? inst->dm_mailbox : 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;
}
/* Выполнить 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);
return -1;
}
return 0;
}
/* Прочитать seq из канонического тела сообщения (смещение 8). */
static uint64_t mb_msg_seq(const uint8_t* msg) {
uint64_t seq; memcpy(&seq, msg + 8, 8); return seq;
}
/* Создать таблицу 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))");
}
/* Отправить 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;
}
/* ── обработка входящих 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) {
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);
}
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;
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);
}
} 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);
}
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);
}
queue_dgram_free(entry); queue_entry_free(entry);
}
/* ── Жизненный цикл ── */
int dm_mailbox_init(struct UTUN_INSTANCE* inst) {
if (!inst) return -1;
if (inst->dm_mailbox && inst->dm_mailbox->initialized) return 0;
struct dm_mb_state* mb = u_calloc(1, sizeof(*mb));
if (!mb) return -1;
inst->dm_mailbox = mb;
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->initialized = 1;
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: initialized", MB_ID);
return 0;
}
void dm_mailbox_destroy(struct UTUN_INSTANCE* inst) {
struct dm_mb_state* mb = mb_of(inst);
if (!inst || !mb || !mb->initialized) return;
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);
}
/* ── Публичные операции ── */
/* Собрать storage-узлы (dm_storage=yes) из общих групп без дублей. */
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);
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;
const char* tags = (const char*)sqlite3_column_text(st, 1);
if (tags && json_flat_get(tags, "dm_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;
}
}
sqlite3_finalize(st);
}
if (ch_ids) u_free(ch_ids);
return found;
}
/* Отправитель: разослать зашифрованное сообщение на все 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);
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;
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;
}
/* Получатель: запросить почту от отправителя со всех 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);
}

49
src/dm/dm_mailbox.h

@ -0,0 +1,49 @@
/*
* dm_mailbox.h — offline-хранение DM-сообщений на storage-узлах
*
* Когда пир недоступен (нет прямой связи), отправитель кладёт зашифрованное
* сообщение на storage-узлы (мемберы общих групп с флагом dm_storage=yes).
* Получатель при выходе в online вытягивает накопившуюся почту, проверяет
* подпись, расшифровывает и вставляет в dm_messages (dedup по seq).
*
* Сервис ETCP_RT_ID_DM_MAILBOX (0x35), подкоманды PUT/PULL/PULL_RESP/ACK.
*/
#ifndef DM_MAILBOX_H
#define DM_MAILBOX_H
#include <stdint.h>
#include <stddef.h>
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
/* Жизненный цикл: init после chat_core_init, destroy перед chat_core_destroy. */
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=ошибка. */
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-узлов (dm_storage=yes) из общих групп. До max штук.
* Возвращает количество найденных. out — буфер на max*8 байт. */
int dm_mailbox_get_storage_nodes(struct UTUN_INSTANCE* inst, uint64_t* out, int max);
#ifdef __cplusplus
}
#endif
#endif /* DM_MAILBOX_H */

4
src/media_delivery/media_delivery.c

@ -1378,7 +1378,7 @@ int media_delivery_init(struct UTUN_INSTANCE* inst) {
}
etcp_add_conn_status_cbk(inst, md_on_conn_status, md);
member_sync_add_props_cbk(md_on_props_changed, md);
member_sync_add_props_cbk(inst, md_on_props_changed, md);
/* determine initial supernode state and subscribe to BGP callbacks */
struct ll_entry* gle = inst->topo_groups ? inst->topo_groups->group_list->head : NULL;
@ -1416,7 +1416,7 @@ void media_delivery_destroy(struct UTUN_INSTANCE* inst) {
etcp_router_unbind(inst, ETCP_RT_ID_MEDIA_DELIVERY);
etcp_remove_conn_status_cbk(inst, md_on_conn_status, md);
member_sync_remove_props_cbk(md_on_props_changed, md);
member_sync_remove_props_cbk(inst, md_on_props_changed, md);
/* unsubscribe from BGP callbacks */
struct ll_entry* gle = inst->topo_groups ? inst->topo_groups->group_list->head : NULL;

4
src/routing_layer/etcp_router.c

@ -184,8 +184,10 @@ static struct ETCP_CONN* router_send_conn(struct ETCP_ROUTER_CONN* rconn) {
if (c) return c;
}
}
return topo_group_find_conn_for_node(group, rconn->remote_node_id);
}
return topo_group_find_conn_for_node(group, rconn->remote_node_id);
/* group_id=0 (или группа не найдена) — глобальная маршрутизация: прямое соединение по node_id */
return instance_find_conn(inst, rconn->remote_node_id);
}
// Собрать и закодировать (подпись/шифрование) пакет в начале отправки.

2
src/routing_layer/topo_group.c

@ -555,7 +555,7 @@ struct TOPO_GROUP* topo_groups_create_group(struct TOPO_GROUPS* g, uint64_t grou
}
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Group created: group_id=%016llx type=%u ch_id=%s", (unsigned long long)group_id, group_type, group->channel_id);
if (group_type == TOPO_GROUP_TYPE_CHAT && chat_setting_get_int("group_autoconnect", 1)) topo_group_connect_init(group);
if (group_type == TOPO_GROUP_TYPE_CHAT && chat_setting_get_int(group->instance, "group_autoconnect", 1)) topo_group_connect_init(group);
return group;
}

2
src/routing_layer/topo_group_connect.c

@ -91,7 +91,7 @@ static void tgc_notify_connecting(struct TOPO_GROUP_CONNECT* gc, const uint64_t*
*p++ = (uint8_t)cl; memcpy(p, gc->group->channel_id, cl); p += cl;
uint16_t c = (uint16_t)count; memcpy(p, &c, 2); p += 2;
for (int i = 0; i < count; i++) { memcpy(p, &ids[i], 8); p += 8; }
chat_event_post(CHAT_EVT_CONNECTING_NODES, buf, (int)sz);
chat_event_post(gc->group->instance, CHAT_EVT_CONNECTING_NODES, buf, (int)sz);
u_free(buf);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: notify_connecting ch=%s count=%d",
TGC_ID, gc->group->channel_id, count);

6
src/routing_layer/topo_group_invite.c

@ -180,7 +180,7 @@ static void tgi_handle_info_req(struct ETCP_CONN* conn, const uint8_t* data, siz
}
/* ── создаём инфраструктуру группы СРАЗУ ── */
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(inst, ch_id);
/* запуск BGP: узел соединён с группой, членство подтверждено (inviter, INFO_REQ) */
{
@ -310,11 +310,11 @@ static void tgi_handle_info_resp(struct ETCP_CONN* conn, const uint8_t* data, si
if (ldb) topo_node_sqlite_channel_put(ldb, ch_id, ch_name, ch_owner,
ch_x25519, NULL, ch_ed_pub, NULL, ch_sig);
}
chat_core_ensure_channel_ready(ch_id);
chat_core_ensure_channel_ready(inv->inst, ch_id);
{ uint8_t evt[65]; uint8_t ch_id_len = (uint8_t)strlen(ch_id);
evt[0] = ch_id_len; memcpy(evt + 1, ch_id, ch_id_len);
chat_event_post(CHAT_EVT_CHANNEL_UPDATED, evt, 1 + ch_id_len); }
chat_event_post(inv->inst, CHAT_EVT_CHANNEL_UPDATED, evt, 1 + ch_id_len); }
/* ── верифицируем update_sig приглашающего ── */
{

2
src/transport_layer/auto_socket.c

@ -251,7 +251,7 @@ static void auto_socket_post_sockets_changed(struct AUTO_SOCKET* as) {
need_reconnect = 1;
as->v4_addr_changed = 0; as->v6_addr_changed = 0;
chat_event_post(CHAT_EVT_LOCAL_SOCKETS, NULL, 0);
chat_event_post(as->instance, CHAT_EVT_LOCAL_SOCKETS, NULL, 0);
if (need_reconnect) {
DEBUG_INFO(DEBUG_CATEGORY_AS, "[as] connectivity change: v4 %d→%d v6 %d→%d — triggering reconnect",

2
src/transport_layer/etcp_api.h

@ -47,6 +47,8 @@ extern "C" {
#define ETCP_RT_ID_CONN_MGR 0x11 // Connection Manager — management connections
#define ETCP_RT_ID_NCD_CONTROL 0x12 // node_conn_direct — CLOSE / KEEP_ALIVE control
#define ETCP_RT_ID_ADMIN_KEY 0x32 // chat_admin — передача канального приватного ключа (encrypted+signed)
#define ETCP_RT_ID_DM 0x34 // dm_core — прямой p2p чат (msg/ack/hello)
#define ETCP_RT_ID_DM_MAILBOX 0x35 // dm_mailbox — offline-хранение на storage-узлах (put/pull/ack)
// Connection status events (instance-level callback)
#define ETCP_CONN_STATUS_NEW 0 // соединение создано

11
src/utun_instance.c

@ -18,6 +18,8 @@
#include "chat/chat_core.h"
#include "chat/chat_sync.h"
#include "chat/chat_headless_control.h"
#include "dm/dm_core.h"
#include "dm/dm_mailbox.h"
#include "broadcast.h"
#include "stcp_server.h"
#include "control_server.h"
@ -478,8 +480,10 @@ void utun_instance_destroy(struct UTUN_INSTANCE *instance) {
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] G done — ETCP sockets");
/* Phase H: chat */
chat_headless_control_destroy();
chat_headless_control_destroy(instance);
chat_media_startup_backfill_destroy(instance);
dm_mailbox_destroy(instance);
dm_core_destroy(instance);
chat_sync_destroy(instance);
chat_core_destroy(instance);
DEBUG_INFO(DEBUG_CATEGORY_SYS, "[DESTROY] H done — chat");
@ -672,8 +676,11 @@ int utun_instance_init(struct UTUN_INSTANCE *instance) {
snprintf(db_file, sizeof(db_file), "%s/chats.db", instance->config->global.db_path);
if (chat_core_init(instance, db_file) == 0) {
chat_sync_init(instance);
chat_media_backfill();
chat_media_backfill(instance);
chat_media_startup_backfill_init(instance);
/* dm_mailbox до dm_core: dm_core_init ставит deliver-cb в mailbox */
dm_mailbox_init(instance);
dm_core_init(instance);
}
}

17
src/utun_instance.h

@ -29,9 +29,16 @@ extern "C" {
#include "etcp_api.h"
#include "config_parser.h"
#include "topo_group.h"
#include "chat/chat_setting.h"
#include "chat/chat_event.h"
// Forward declarations
struct utun_config;
struct chat_core_ctx;
struct chat_sync;
struct join_key_entry; /* chat_join.c */
struct dm_state;
struct dm_mb_state;
struct uasync_s;
struct ROUTE_TABLE;
struct ETCP_CONN;
@ -186,6 +193,16 @@ struct UTUN_INSTANCE {
struct CONN_MGR* conn_mgr; // Connection Manager (может быть NULL)
struct DB_SYNC* db_sync; // Distributed DB sync (может быть NULL)
// Chat/DM subsystem (per-instance contexts; were global singletons)
struct chat_core_ctx* chat_core; // chat_core.c (was g_cc)
struct chat_sync* chat_sync; // chat_sync.c (was g_cs)
struct join_key_entry* join_keys; // chat_join.c (was g_keys; собственный init/destroy)
struct dm_state* dm; // dm/dm_core.c (was g_dm)
struct dm_mb_state* dm_mailbox; // dm/dm_mailbox.c (was g_mb)
struct chat_setting_state chat_settings; // chat_setting.c (was globals)
chat_event_handler_fn chat_event_handler; // chat_event.c (was g_handler)
void* headless; // chat_headless_control.c (was g_hc)
// E2E encryption cache — per-peer sc_context_t with derived session key
#define E2E_CTX_CACHE_SIZE 8
struct { uint64_t peer_node_id; struct secure_channel ctx; uint8_t valid; } e2e_ctx_cache[E2E_CTX_CACHE_SIZE];

79
task.txt

@ -1,79 +0,0 @@
## Текущая задача: отладка merkle sync в chatgui
### Контекст
Две инстанции chatgui обмениваются мемберами (участниками) каналов через merkle sync.
Одна на Linux (node `41eab6811e3ff67b`), вторая на Windows (node `32a3a1ee5625ceb9`).
### Что уже исправлено (vetka topo_upd)
1. **`conn_mgr.c:276`** — убрано требование `links_up` при поиске существующего коннекта.
Раньше второй `conn_mgr_connect_node` (для второй группы/канала) не находил первый коннект
потому что тот был в handshake (links_up=0) и создавал дублирующий линк → два init-таймера
→ два потока INIT-пакетов → бесконечный цикл реинициализации ETCP.
2. **`etcp_connections.c:insert_link_queue`** — детект коллизии адреса: если адрес уже занят
ДРУГИМ коннектом → жирная ошибка + return -1 (вместо молчаливой замены).
3. **`etcp.c:etcp_conn_set_peer_node_id`** — детект коллизии peer_id: если key уже занят
другим коннектом → жирная ошибка + return (не переиндексировать).
4. **`merkle_sync.c`** — исправлен формат сообщений:
- Отправитель клал type ПЕРЕД ch_len+ns: `[svc][TYPE][ch_len][ns]...`
- Получатель (`_recv_cb`) ожидал `[svc][ch_len][ns][TYPE]...` (как у chat_sync)
- Переставлены байты в `_send_hashes`, `_send_batch`, MSG_REQUEST builder
- Результат: handle_hashes/handle_request/handle_batch начали реально получать сообщения
5. **`merkle_sync.c`** — `_handle_batch` теперь вызывает `_session_done(s, MT_OK)` после
обработки всех терминальных бакетов. До этого сессия никогда не завершалась,
таймер истекал → retry → к тому моменту соединение уже отваливалось.
### Текущее состояние
Merkle sync обменивается сообщениями и завершает сессии (`session SYNCED` в логах).
Но данные мемберов не совпадают: Linux имеет 3 мембера в канале, Windows — 2.
### Найденная проблема (исследуется)
В логах `handle_batch` показывает `len=7` для обоих каналов с обеих сторон.
Это означает что в BATCH-сообщении данные мемберов = всего 2 байта (count=0).
При этом `merkle_tree_hash` показывает НЕнулевой bitmap (00008000) → `update_bucket_hash`
находит мемберов в бакете. Но `get_items` для того же бакета возвращает 0 записей.
**Гипотеза:** `update_bucket_hash` и `get_items` используют разную логику фильтрации
для одного и того же level/prefix → SQL запрос в `get_items` не находит мемберов.
Файлы: `tools/chatgui/transport/member_sync.c` функции `_member_update_bucket_hash` и `_member_get_items`.
### Данные из БД (Linux)
Канал 17611191138590969914: 3 мембера
- 4749809422690154107 (0x41eab6811e3ff67b — Linux self)
- 3648938168119840441 (0x32a3a1ee5625ceb9 — Windows peer)
- 5749221071098079620 (0x4fc9561a7dfde984 — третий узел)
Канал 10175004475814804132: 2 мембера
- 3648938168119840441 (0x32a3a1ee5625ceb9)
- 4749809422690154107 (0x41eab6811e3ff67b)
Merkle tree для канала 176111...:
```
level=1 prefix=0x4000000000000000 member_count=1 hash=F612B771...
level=2 prefix=0x41E0000000000000 member_count=1 hash=F612B771...
level=3 prefix=0x41EA000000000000 member_count=1 hash=F612B771...
level=4 prefix=0x41EAB00000000000 member_count=1 hash=F612B771...
level=5 prefix=0x41EAB68000000000 member_count=1 hash=F612B771...
```
### Логи
Linux: `/home/vnc1/proj/utun3/tools/chatgui/build/chatgui.log`
Windows: `/home/vnc1/proj/utun3/tools/chatgui/build/chatgui2.log`
Конфиг: `db_sync=debug` включён.
### БД
Linux: `/home/vnc1/proj/utun3/tools/chatgui/build/chat_data/chats.db`
Windows: `C:/ARM/_uTun/utun2/tools/chatgui/build/chat_data/chats.db` (доступа нет)

10
tests/Makefile.am

@ -62,6 +62,7 @@ check_PROGRAMS = \
test_chat_sync_stress \
test_chat_join \
test_chat_join_e2e \
test_dm_e2e \
test_stcp_traffic \
test_bbr_integration \
test_audio_compressor \
@ -83,6 +84,7 @@ check_PROGRAMS = \
test_reality_hello \
test_reality_bgp \
test_reality_config \
test_dm \
bench_timeout_heap \
bench_uasync_timeouts
@ -142,6 +144,10 @@ test_reality_config_SOURCES = test_reality_config.c
test_reality_config_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib
test_reality_config_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_dm_SOURCES = test_dm.c
test_dm_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib
test_dm_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_transport_SOURCES = test_stcp_link.c
test_transport_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_transport_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
@ -377,6 +383,10 @@ test_chat_join_e2e_SOURCES = test_chat_join_e2e.c
test_chat_join_e2e_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib
test_chat_join_e2e_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_dm_e2e_SOURCES = test_dm_e2e.c
test_dm_e2e_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/src/chat -I$(top_srcdir)/src/dm -I$(top_srcdir)/src/routing_layer -I$(top_srcdir)/src/transport_layer -I$(top_srcdir)/lib
test_dm_e2e_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)
test_stcp_traffic_SOURCES = test_stcp_traffic.c
test_stcp_traffic_CFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/lib
test_stcp_traffic_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS)

6
tests/test_chat_join_e2e.c

@ -258,7 +258,7 @@ static void channel_put_shared(struct UTUN_INSTANCE* inst, int has_priv) {
topo_node_sqlite_channel_put(inst->topo_sqlite_db, g_sh.ch_id, CH_NAME, g_sh.owner,
g_sh.ch_x_pub, has_priv ? g_sh.ch_x_priv : NULL,
g_sh.ch_ed_pub, has_priv ? g_sh.ch_ed_priv : NULL, g_sh.ch_sig);
chat_core_ensure_channel_ready(g_sh.ch_id);
chat_core_ensure_channel_ready(inst, g_sh.ch_id);
}
static void member_put_self(struct UTUN_INSTANCE* inst, const char* name) {
@ -317,7 +317,7 @@ static int child_main(const char* role, const char* dir, int invalid_key) {
/* invite_build требует conn_presence узла — может отставать от paths; ретраим */
{ int built = 0;
for (int a = 0; a < 1000; a++) {
if (chat_invite_build_link(ch_num, target, NULL, link, sizeof(link)) == 0) { built = 1; break; }
if (chat_invite_build_link(inst, ch_num, target, NULL, link, sizeof(link)) == 0) { built = 1; break; }
uasync_poll(inst->ua, POLL_MS);
}
if (!built) { fprintf(stderr, "A: invite_build_link failed (target=0x%016llx)\n", (unsigned long long)target); goto out; }
@ -390,7 +390,7 @@ static int child_main(const char* role, const char* dir, int invalid_key) {
if (addrs_len <= 0) { fprintf(stderr, "J: serialize addrs failed\n"); goto out; }
uint64_t key = invalid_key ? (d.join_key ^ 0xDEADBEEFCAFEBABEULL) : d.join_key;
chat_sync_connect_from_invite(d.channelId, d.nodeId, d.pubkey,
chat_sync_connect_from_invite(inst, d.channelId, d.nodeId, d.pubkey,
addrs_buf, d.addrCount, addrs_len, d.password_len ? d.password : NULL, key);
if (invalid_key) {

109
tests/test_dm.c

@ -0,0 +1,109 @@
/*
* test_dm.c — тест криптографии прямого чата (dm_crypto)
*
* Проверяет:
* - детерминированность и симметричность conv_id (одинаков на обеих сторонах);
* - симметричность content_key (ECDH: обе стороны выводят одинаковый ключ);
* - round-trip шифрования AES-256-CCM и детекцию неверного ключа.
*/
#include "../src/dm/dm_crypto.h"
#include "../src/transport_layer/secure_channel.h"
#include "../lib/debug_config.h"
#include <string.h>
#include <stdio.h>
int main(void) {
debug_config_init();
debug_set_level(DEBUG_LEVEL_ERROR);
int failures = 0;
/* ── 1. conv_id: симметричность и уникальность ── */
uint64_t a = 0x1122334455667788ULL;
uint64_t b = 0x8877665544332211ULL;
uint64_t id_ab = dm_derive_conv_id(a, b);
uint64_t id_ba = dm_derive_conv_id(b, a);
if (id_ab == 0 || id_ab != id_ba) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "conv_id not symmetric: ab=%llu ba=%llu",
(unsigned long long)id_ab, (unsigned long long)id_ba);
failures++;
} else {
DEBUG_INFO(DEBUG_CATEGORY_DM, "conv_id symmetric: %llu == %llu",
(unsigned long long)id_ab, (unsigned long long)id_ba);
}
uint64_t c = 0xDEADBEEF00000001ULL;
if (dm_derive_conv_id(a, c) == id_ab) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "conv_id collision for different pairs");
failures++;
}
/* ── 2. content_key: симметричность ECDH ── */
struct SC_MYKEYS k1, k2;
if (sc_generate_keypair(&k1) != SC_OK || sc_generate_keypair(&k2) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "keypair generation failed");
return 1;
}
uint8_t key1[DM_CONTENT_KEY_SIZE], key2[DM_CONTENT_KEY_SIZE];
if (dm_derive_content_key(k1.private_key, k2.public_key, key1) != 0
|| dm_derive_content_key(k2.private_key, k1.public_key, key2) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "content_key derive failed");
failures++;
} else if (memcmp(key1, key2, DM_CONTENT_KEY_SIZE) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "content_key not symmetric (ECDH mismatch)");
failures++;
} else {
DEBUG_INFO(DEBUG_CATEGORY_DM, "content_key symmetric (ECDH ok)");
}
/* ── 3. AES-256-CCM round-trip ── */
const char* text = "hello, direct chat!";
size_t text_len = strlen(text);
uint8_t nonce[DM_NONCE_SIZE];
dm_build_nonce(id_ab, a, 42, nonce);
uint8_t enc[256];
size_t enc_len = 0;
if (dm_encrypt(key1, nonce, (const uint8_t*)text, text_len, enc, &enc_len) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "encrypt failed");
failures++;
} else if (enc_len != text_len + DM_TAG_SIZE) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "encrypt length mismatch: %zu", enc_len);
failures++;
} else {
uint8_t dec[256];
size_t dec_len = 0;
if (dm_decrypt(key2, nonce, enc, enc_len, dec, &dec_len) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "decrypt failed");
failures++;
} else if (dec_len != text_len || memcmp(dec, text, text_len) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "decrypt content mismatch");
failures++;
} else {
DEBUG_INFO(DEBUG_CATEGORY_DM, "AES-256-CCM round-trip ok");
}
}
/* ── 4. детекция неверного ключа ── */
{
uint8_t wrong[DM_CONTENT_KEY_SIZE];
memset(wrong, 0x5A, sizeof(wrong));
uint8_t dec[256];
size_t dec_len = 0;
if (enc_len > 0 && dm_decrypt(wrong, nonce, enc, enc_len, dec, &dec_len) == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_DM, "decrypt with wrong key should fail");
failures++;
} else {
DEBUG_INFO(DEBUG_CATEGORY_DM, "wrong-key tamper detection ok");
}
}
if (failures == 0) {
printf("TEST PASSED\n");
return 0;
}
printf("TEST FAILED (%d failures)\n", failures);
return 1;
}

452
tests/test_dm_e2e.c

@ -0,0 +1,452 @@
// test_dm_e2e.c — интеграционный тест прямого чата (DM): прямая отправка + offline-хранение.
//
// Однопоточный: 3 узла (A, B, C) в одном процессе, один UASYNC, без fork.
// Топология — цепочка B→C→A (A владелец канала, C storage, B target).
// Сценарий управляется одной master state-machine (таймер dm_tick).
//
// Сценарий:
// фаза 1 (прямая): A,B,C online. A dm_start(B) → dm_send("hello1") → B получает напрямую.
// фаза 2 (offline): B1 завершается (A теряет B). A dm_send("hello2") → недоступен →
// кладёт в mailbox на C (dm_storage=yes). C видит блоб в dm_mail.
// фаза 3 (reconnect): B2 переподключается к C → dm_on_conn_status → PULL mailbox → получает "hello2".
#include "chat_core.h"
#include "chat_sync.h"
#include "member_sync.h"
#include "../dm/dm_core.h"
#include "../dm/dm_crypto.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/topo_group.h"
#include "../routing_layer/topo_node.h"
#include "../utun_instance.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/secure_channel.h"
#include "../ntp_time.h"
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/platform_compat.h"
#include <sqlite3.h>
#define OPENSSL_API_COMPAT 0x10100000L
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <time.h>
#include <unistd.h>
#define CH_ID "4242424242424242"
#define IDX_A 0
#define IDX_B 1
#define IDX_C 2
#define TICK_TB 100 /* 10 ms на такт state machine */
#define MAX_TICKS 3000 /* глобальный таймаут ≈ 30 c */
/* фазы master state-machine */
enum dm_phase {
P_WAIT_CONN, /* ждём подъёма всех соединений */
P_SETUP, /* однократная настройка канала/мемберов на A,B,C */
P_WAIT_BGP, /* ждём сходимости BGP CHAT-группы */
P_SEND1, /* A: dm_start + hello1 */
P_WAIT_MSG1, /* ждём hello1 у B */
P_B_OFFLINE, /* destroy B1 */
P_WAIT_B_OFFLINE, /* ждём ухода B из BGP у A */
P_SEND2, /* A: hello2 → mailbox */
P_WAIT_MAIL, /* ждём блоб в dm_mail у C */
P_RECONNECT, /* create B2 */
P_WAIT_B2, /* ждём hello2 у B2 и опустошение dm_mail у C */
P_DONE,
};
struct dm_ctx {
struct UASYNC* ua;
struct UTUN_INSTANCE* inst[3];
enum dm_phase phase;
int result; /* 0=running, 1=ok, 2=fail */
int ticks;
void* timer;
};
/* общие данные (ключи узлов и канала) */
struct dm_shared {
uint8_t ch_x_pub[32], ch_ed_pub[32], ch_ed_priv[32], ch_sig[64];
uint64_t nid[3];
uint8_t x_pub[3][32], x_priv[3][32], ed_pub[3][32];
int port[3];
char conv_id[64];
};
static struct dm_shared g_sh;
static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0;
#define TEST(n) do { G_TOTAL++; printf(" %-58s", n); fflush(stdout); } while(0)
#define OK() do { G_PASSED++; printf("OK\n"); } while(0)
#define FAIL(f,...) do { G_FAILED++; printf("FAIL: " f "\n", ##__VA_ARGS__); } while(0)
/* ── крипто-хелперы ── */
static void gen_x25519(uint8_t pub[32], uint8_t priv[32]) {
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL);
EVP_PKEY* pkey = NULL;
EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx);
size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l);
l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l);
EVP_PKEY_free(pkey);
}
static void gen_ed25519(uint8_t pub[32], uint8_t priv[32]) {
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL);
EVP_PKEY* pkey = NULL;
EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx);
size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l);
l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l);
EVP_PKEY_free(pkey);
}
static void to_hex(const uint8_t* bin, size_t n, char* out) {
for (size_t i = 0; i < n; i++) sprintf(out + i * 2, "%02x", bin[i]);
out[n * 2] = '\0';
}
static int wf(const char* p, const char* f, ...) {
va_list ap; FILE* fp = fopen(p, "w"); if (!fp) return -1;
va_start(ap, f); vfprintf(fp, f, ap); va_end(ap); fclose(fp); return 0;
}
/* ── генерация данных сценария + конфигов ── */
static void fill_shared(int base_port) {
memset(&g_sh, 0, sizeof(g_sh));
for (int i = 0; i < 3; i++) {
gen_x25519(g_sh.x_pub[i], g_sh.x_priv[i]);
sc_derive_ed25519_pubkey(g_sh.x_priv[i], g_sh.ed_pub[i]);
g_sh.nid[i] = sc_derive_node_id_from_pubkey(g_sh.x_pub[i]);
g_sh.port[i] = base_port + i * 1000;
}
{ uint8_t ch_x_priv[32]; gen_x25519(g_sh.ch_x_pub, ch_x_priv); }
gen_ed25519(g_sh.ch_ed_pub, g_sh.ch_ed_priv);
/* подпись канала: Ed25519(ch_ed_priv, ch_id||'\0'||name||'\0'||owner(8)||ch_x_pub(32)||ch_ed_pub(32)) */
{
uint8_t msg[256]; size_t off = 0;
off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", CH_ID) + 1;
off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", "dm-test") + 1;
memcpy(msg + off, &g_sh.nid[IDX_A], 8); off += 8;
memcpy(msg + off, g_sh.ch_x_pub, 32); off += 32;
memcpy(msg + off, g_sh.ch_ed_pub, 32); off += 32;
sc_ed25519_sign(g_sh.ch_ed_priv, msg, off, g_sh.ch_sig);
}
uint64_t conv = dm_derive_conv_id(g_sh.nid[IDX_A], g_sh.nid[IDX_B]);
snprintf(g_sh.conv_id, sizeof(g_sh.conv_id), "%llu", (unsigned long long)conv);
}
static void write_configs(const char* dir) {
for (int i = 0; i < 3; i++) {
char priv[65], pub[65], path[512], dbd[512];
to_hex(g_sh.x_priv[i], 32, priv);
to_hex(g_sh.x_pub[i], 32, pub);
snprintf(path, sizeof(path), "%s/%s.conf", dir, i == IDX_A ? "a" : i == IDX_B ? "b" : "c");
snprintf(dbd, sizeof(dbd), "%s/db%s", dir, i == IDX_A ? "a" : i == IDX_B ? "b" : "c");
utun_mkdir(dbd, 0755);
char client[512] = "";
if (i == IDX_C) {
/* C → A (клиент к A) */
char apub[65]; to_hex(g_sh.x_pub[IDX_A], 32, apub);
snprintf(client, sizeof(client),
"[client: to_a]\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n",
apub, g_sh.port[IDX_A]);
} else if (i == IDX_B) {
/* B → C (клиент к C) */
char cpub[65]; to_hex(g_sh.x_pub[IDX_C], 32, cpub);
snprintf(client, sizeof(client),
"[client: to_c]\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n",
cpub, g_sh.port[IDX_C]);
}
wf(path,
"[global]\ntun_ip=10.97.%d.1/24\ntun_ifname=tun%d0\ndb_path=%s/db%s\n"
"my_private_key=%s\nmy_public_key=%s\n"
"[server: s1]\naddr=127.0.0.1:%d\ntype=public\n"
"%s"
"[chatserver]\nstorage_autoload=0\n[allowed_keys]\nallow_all=1\n",
i, i, dir, i == IDX_A ? "a" : i == IDX_B ? "b" : "c",
priv, pub, g_sh.port[i], client);
}
}
/* ── чистые проверки (без опроса) ── */
static int conn_up(struct UTUN_INSTANCE* inst, uint64_t nid) {
struct ETCP_CONN* c = instance_find_conn(inst, nid);
return c && c->initialized && c->links_up;
}
static int bgp_has(struct UTUN_INSTANCE* inst, uint64_t nid) {
uint64_t gid = strtoull(CH_ID, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid);
if (!g) return 0;
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, nid);
return nq && nq->paths && nq->paths->head;
}
/* есть ли расшифрованный текст needle в беседе (одна проверка) */
static int dm_msg_has(struct UTUN_INSTANCE* inst, const char* needle) {
char buf[8192]; size_t len = 0;
if (dm_list_messages_json(inst, g_sh.conv_id, 100, 0, buf, sizeof(buf), &len) != 0) return 0;
return strstr(buf, needle) != NULL;
}
/* количество блобов в dm_mail для recipient=B от sender=A (одна проверка) */
static int dm_mail_count(struct UTUN_INSTANCE* inst) {
sqlite3_stmt* s = NULL;
int n = 0;
if (sqlite3_prepare_v2(inst->topo_sqlite_db,
"SELECT COUNT(*) FROM dm_mail WHERE recipient=? AND sender=?",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_int64(s, 1, (sqlite3_int64)g_sh.nid[IDX_B]);
sqlite3_bind_int64(s, 2, (sqlite3_int64)g_sh.nid[IDX_A]);
if (sqlite3_step(s) == SQLITE_ROW) n = sqlite3_column_int(s, 0);
}
if (s) sqlite3_finalize(s);
return n;
}
/* ── настройка канала и мемберов (однократно) ── */
static void channel_put_shared(struct UTUN_INSTANCE* inst, int has_priv) {
topo_node_sqlite_channel_put(inst->topo_sqlite_db, CH_ID, "dm-test", g_sh.nid[IDX_A],
g_sh.ch_x_pub, NULL, g_sh.ch_ed_pub, has_priv ? g_sh.ch_ed_priv : NULL, g_sh.ch_sig);
chat_core_ensure_channel_ready(inst, CH_ID);
}
static void member_put_self(struct UTUN_INSTANCE* inst, const char* name) {
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(inst);
uint8_t jm[128];
int jl = member_sync_build_join_msg(g_sh.ch_x_pub, g_sh.ch_ed_pub, inst->node_id,
inst->my_keys.public_key, join_ts, jm, (int)sizeof(jm));
uint8_t join_sig[64]; sc_ed25519_sign(inst->my_ed25519_privkey, jm, (size_t)jl, join_sig);
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name);
member_sync_put(inst, CH_ID, inst->node_id, inst->my_keys.public_key, inst->my_ed25519_pubkey,
join_sig, join_ts, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL);
}
static void member_put_placeholder(struct UTUN_INSTANCE* inst, int idx, const char* name) {
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name);
member_sync_put(inst, CH_ID, g_sh.nid[idx], g_sh.x_pub[idx], g_sh.ed_pub[idx],
NULL, 0, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL);
}
/* мембер C со флагом dm_storage=yes (adm_tags подписан канальным ключом) */
static void member_put_storage(struct UTUN_INSTANCE* inst, int idx, const char* name) {
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name);
char adm_tags[64]; snprintf(adm_tags, sizeof(adm_tags), "{\"dm_storage\":\"yes\",\"ver\":\"1\"}");
uint8_t ats[64];
uint8_t amsg[256]; size_t off = strlen(adm_tags);
memcpy(amsg, adm_tags, off);
memcpy(amsg + off, &g_sh.nid[idx], 8); off += 8;
sc_ed25519_sign(g_sh.ch_ed_priv, amsg, off, ats);
member_sync_put(inst, CH_ID, g_sh.nid[idx], g_sh.x_pub[idx], g_sh.ed_pub[idx],
NULL, 0, NULL, 0, userinfo, adm_tags, ats, 0, 0, NULL);
}
/* однократная настройка роли */
static void do_setup(struct UTUN_INSTANCE* inst, int role) {
uint64_t nc = g_sh.nid[IDX_C];
switch (role) {
case IDX_A:
channel_put_shared(inst, 1);
member_put_self(inst, "A");
member_put_placeholder(inst, IDX_B, "B");
member_put_storage(inst, IDX_C, "C");
member_sync_start(inst, nc, CH_ID, NULL, NULL);
break;
case IDX_B:
channel_put_shared(inst, 0);
member_put_self(inst, "B");
member_put_placeholder(inst, IDX_A, "A");
member_put_storage(inst, IDX_C, "C");
member_sync_start(inst, nc, CH_ID, NULL, NULL);
break;
case IDX_C:
channel_put_shared(inst, 0);
member_put_self(inst, "C");
member_put_placeholder(inst, IDX_A, "A");
member_put_placeholder(inst, IDX_B, "B");
member_sync_start(inst, g_sh.nid[IDX_A], CH_ID, NULL, NULL);
member_sync_start(inst, g_sh.nid[IDX_B], CH_ID, NULL, NULL);
break;
}
}
/* ── master state-machine (движется таймером) ── */
static void dm_tick(void* arg) {
struct dm_ctx* t = arg;
t->timer = NULL;
if (t->result) return;
struct UTUN_INSTANCE* A = t->inst[IDX_A];
struct UTUN_INSTANCE* B = t->inst[IDX_B];
struct UTUN_INSTANCE* C = t->inst[IDX_C];
uint64_t na = g_sh.nid[IDX_A], nb = g_sh.nid[IDX_B], nc = g_sh.nid[IDX_C];
switch (t->phase) {
case P_WAIT_CONN:
/* C→A и B→C должны быть up */
if (conn_up(A, nc) && conn_up(C, na) && conn_up(B, nc) && conn_up(C, nb))
t->phase = P_SETUP;
break;
case P_SETUP:
do_setup(A, IDX_A);
do_setup(B, IDX_B);
do_setup(C, IDX_C);
t->phase = P_WAIT_BGP;
break;
case P_WAIT_BGP:
/* A видит C и B; C видит A и B; B видит A (и C) */
if (bgp_has(A, nc) && bgp_has(A, nb) && bgp_has(C, na) && bgp_has(C, nb) && bgp_has(B, na))
t->phase = P_SEND1;
break;
case P_SEND1:
if (dm_start(A, nb, g_sh.x_pub[IDX_B], g_sh.ed_pub[IDX_B], "B", CH_ID) != 0
|| dm_send(A, g_sh.conv_id, "text", (const uint8_t*)"hello1", 6) != 0) {
fprintf(stderr, "A: dm_start/send hello1 failed\n");
t->result = 2;
} else {
t->phase = P_WAIT_MSG1;
}
break;
case P_WAIT_MSG1:
if (dm_msg_has(B, "hello1")) t->phase = P_B_OFFLINE;
break;
case P_B_OFFLINE:
utun_instance_destroy(B);
t->inst[IDX_B] = NULL;
t->phase = P_WAIT_B_OFFLINE;
break;
case P_WAIT_B_OFFLINE:
if (!bgp_has(A, nb)) t->phase = P_SEND2;
break;
case P_SEND2:
if (dm_send(A, g_sh.conv_id, "text", (const uint8_t*)"hello2", 6) != 0) {
fprintf(stderr, "A: dm_send hello2 failed\n");
t->result = 2;
} else {
t->phase = P_WAIT_MAIL;
}
break;
case P_WAIT_MAIL:
if (dm_mail_count(C) > 0) t->phase = P_RECONNECT;
break;
case P_RECONNECT: {
char cfg[512];
snprintf(cfg, sizeof(cfg), "%s/b.conf", getenv("UTUN_TEST_DIR") ? getenv("UTUN_TEST_DIR") : ".");
struct UTUN_INSTANCE* b2 = utun_instance_create(t->ua, cfg);
if (!b2) { fprintf(stderr, "B2 create failed\n"); t->result = 2; break; }
utun_instance_init(b2);
t->inst[IDX_B] = b2;
t->phase = P_WAIT_B2;
break;
}
case P_WAIT_B2:
if (dm_msg_has(B, "hello2") && dm_mail_count(C) == 0) t->phase = P_DONE;
break;
case P_DONE:
t->result = 1;
uasync_stop(t->ua);
return;
}
if (t->phase == P_DONE) { t->result = 1; uasync_stop(t->ua); return; }
if (++t->ticks > MAX_TICKS) {
fprintf(stderr, "test: timeout in phase %d (A_conn=%d C_conn=%d B_conn=%d)\n",
(int)t->phase,
conn_up(A, nc), conn_up(C, na), B ? conn_up(B, nc) : -1);
t->result = 2;
uasync_stop(t->ua);
return;
}
t->timer = uasync_set_timeout(t->ua, TICK_TB, t, dm_tick, "dm_tick");
}
/* ── main ── */
int main(int argc, char** argv) {
(void)argc; (void)argv;
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
if (getenv("UTUN_TEST_DEBUG")) {
debug_set_category_level_by_name("dm", "trace");
debug_set_category_level_by_name("chat_sync", "info");
debug_set_category_level_by_name("member_sync", "info");
debug_set_category_level_by_name("bgp", "info");
debug_set_category_level_by_name("etcp_route", "info");
debug_set_category_level_by_name("crypto", "trace");
}
utun_instance_set_tun_init_enabled(0);
srand((unsigned)time(NULL));
char dir[512]; snprintf(dir, sizeof(dir), "/tmp/utun_dm_e2e_XXXXXX");
if (!mkdtemp(dir)) { fprintf(stderr, "mkdtemp failed\n"); return 1; }
setenv("UTUN_TEST_DIR", dir, 1);
fill_shared(58000 + (getpid() % 2000));
write_configs(dir);
printf("=== test_dm_e2e ===\n"); fflush(stdout);
TEST("single-thread: direct send + offline mailbox + reconnect pull"); {
struct UASYNC* ua = uasync_create();
if (!ua) { FAIL("uasync_create failed"); return 1; }
struct dm_ctx t;
memset(&t, 0, sizeof(t));
t.ua = ua;
t.phase = P_WAIT_CONN;
char cfgA[512], cfgB[512], cfgC[512];
snprintf(cfgA, sizeof(cfgA), "%s/a.conf", dir);
snprintf(cfgB, sizeof(cfgB), "%s/b.conf", dir);
snprintf(cfgC, sizeof(cfgC), "%s/c.conf", dir);
t.inst[IDX_A] = utun_instance_create(ua, cfgA);
t.inst[IDX_B] = utun_instance_create(ua, cfgB);
t.inst[IDX_C] = utun_instance_create(ua, cfgC);
if (!t.inst[IDX_A] || !t.inst[IDX_B] || !t.inst[IDX_C]) {
FAIL("instance create failed A=%p B=%p C=%p",
(void*)t.inst[IDX_A], (void*)t.inst[IDX_B], (void*)t.inst[IDX_C]);
uasync_destroy(ua, 0);
return 1;
}
utun_instance_init(t.inst[IDX_A]);
utun_instance_init(t.inst[IDX_C]);
/* B стартует после C, чтобы B→C пришлось на уже поднятый C (как в сценарии) */
utun_instance_init(t.inst[IDX_B]);
t.timer = uasync_set_timeout(ua, TICK_TB, &t, dm_tick, "dm_tick");
uasync_mainloop(ua);
int ok = (t.result == 1);
if (t.inst[IDX_A]) utun_instance_destroy(t.inst[IDX_A]);
if (t.inst[IDX_B]) utun_instance_destroy(t.inst[IDX_B]);
if (t.inst[IDX_C]) utun_instance_destroy(t.inst[IDX_C]);
uasync_destroy(ua, 0);
if (ok) OK(); else FAIL("result=%d phase=%d", t.result, (int)t.phase);
}
char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", dir); (void)!system(cmd);
printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED);
return G_FAILED > 0 ? 1 : 0;
}

30
tools/chatcli

@ -21,6 +21,10 @@
# create <name> create new channel
# invite_to <ch_id> <node_id> <pubkey_hex> <ip:port> [proto]
# invite a node to add our channel
# dm_start <node_id> [ch_id] start direct chat (DM) with a group member
# dm_list list DM conversations
# dm_messages <conv_id> [count] read DM messages (decrypted)
# dm_send <conv_id> <text> send DM text message
# listen interactive event listener
import sys, os, json, socket, struct
@ -126,6 +130,24 @@ def cmd_invite_to(ch_id, node_id, pubkey, addr, proto=None):
params["proto"] = int(proto)
_req("invite_to", **params)
def cmd_dm_start(node_id, ch=None):
params = {"node_id": node_id}
if ch:
params["ch"] = ch
_req("dm_start", **params)
def cmd_dm_list():
_req("dm_list")
def cmd_dm_messages(conv_id, count=None):
params = {"conv_id": conv_id}
if count:
params["count"] = int(count)
_req("dm_messages", **params)
def cmd_dm_send(conv_id, text):
_req("dm_send", conv_id=conv_id, data=text)
def cmd_listen():
print(f"Listening on {DEFAULT_HOST}:{DEFAULT_PORT} (Ctrl+C to quit)")
s = _connect()
@ -149,6 +171,10 @@ def cmd_listen():
print(f"[{obj.get('ch','?')}] channel updated")
elif evt == "invite_received":
print(f"INVITE: ch={obj.get('ch','?')} name={obj.get('ch_name','?')} from={obj.get('from_id','?')}")
elif evt == "dm_msg":
print(f"DM[{obj.get('conv','?')}] from={obj.get('author_id','?')}: new message")
elif evt == "dm_conv":
print(f"DM[{obj.get('conv','?')}] conversation updated")
else:
print(json.dumps(obj, indent=2, ensure_ascii=False))
except KeyboardInterrupt:
@ -191,6 +217,10 @@ def main():
elif cmd in ("connect","join"): cmd_connect(*args[1:2] if len(args) > 1 else (_die("usage: connect <utun://...>"),))
elif cmd == "create": cmd_create(*args[1:2] if len(args) > 1 else (_die("usage: create <name>"),))
elif cmd == "invite_to": cmd_invite_to(*args[1:6] if len(args) > 4 else (_die("usage: invite_to <ch_id> <node_id> <pubkey_hex> <ip:port> [proto]"),))
elif cmd == "dm_start": cmd_dm_start(*args[1:3] if len(args) > 1 else (_die("usage: dm_start <node_id> [ch_id]"),))
elif cmd == "dm_list": cmd_dm_list()
elif cmd in ("dm_messages","dm_msgs"): cmd_dm_messages(*args[1:3] if len(args) > 1 else (_die("usage: dm_messages <conv_id> [count]"),))
elif cmd == "dm_send": cmd_dm_send(*args[1:3] if len(args) > 2 else (_die("usage: dm_send <conv_id> <text>"),))
elif cmd == "listen": cmd_listen()
else: print(f"Unknown command: {cmd}\nUse --help for usage", file=sys.stderr); sys.exit(1)
except socket.timeout:

13
tools/chatgui-android/headless/headless_control.c

@ -4,6 +4,7 @@
#include "headless_control.h"
#include "../libutun_lite/utun_config_api.h"
#include "../libutun_lite/invite_link_c.h"
#include "../libutun_lite/instance_lite.h"
#include "../../../src/chat/chat_setting.h"
#include "../../../src/chat/chat_sync.h"
#include "../../../lib/debug_config.h"
@ -315,7 +316,13 @@ static int handle_join(int fd, int id, const char* json) {
}
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "join: calling chat_sync_connect_from_invite...");
chat_sync_connect_from_invite(d.channelId, d.nodeId, d.pubkey, addrs_buf, d.addrCount, addrs_len, d.password, d.join_key);
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "join: no instance running — drop");
send_response(fd, id, NULL, "instance not running");
return 0;
}
chat_sync_connect_from_invite(inst, d.channelId, d.nodeId, d.pubkey, addrs_buf, d.addrCount, addrs_len, d.password, d.join_key);
char resp[512];
snprintf(resp, sizeof(resp),
@ -331,7 +338,9 @@ static int handle_chat_setting(int fd, int id, const char* json) {
send_response(fd, id, NULL, "missing name or value");
return 0;
}
int rc = chat_setting_set(name, value);
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) { send_response(fd, id, NULL, "instance not running"); return 0; }
int rc = chat_setting_set(inst, name, value);
if (rc == -1) { send_response(fd, id, NULL, "unknown setting"); return 0; }
if (rc == -2) { send_response(fd, id, NULL, "invalid value"); return 0; }
printf("control: chat_setting %s=%s\n", name, value);

114
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -190,6 +190,7 @@ void utun_bridge_send_message(const char* channel_id, const char* text) {
struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + datalen);
if (!req) return;
req->inst = instance_lite_get_instance();
strncpy(req->channel_id, channel_id, sizeof(req->channel_id) - 1);
strncpy(req->content_type, "text/plain", sizeof(req->content_type) - 1);
req->data = (uint8_t*)(req + 1);
@ -206,9 +207,11 @@ void utun_bridge_create_channel(const char* name, const char* channel_id) {
if (!name || !name[0]) { bridge_log(BLEV_ERROR, "create_channel: empty name"); return; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "create_channel: uasync not running"); return; }
char* n = u_strdup(name);
if (!n) { bridge_log(BLEV_ERROR, "create_channel: OOM"); return; }
uasync_post(ua, chat_core_create_channel_auto_trampoline, n);
struct chat_create_auto_arg* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "create_channel: OOM"); return; }
req->inst = instance_lite_get_instance();
snprintf(req->name, sizeof(req->name), "%s", name);
uasync_post(ua, chat_core_create_channel_auto_trampoline, req);
}
void utun_bridge_connect_node(const char* address, int port, const char* pubkey_hex) {
@ -235,7 +238,13 @@ void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id,
utun_bridge_event(6, "{\"node_id\":0,\"result\":-7}"); /* CHAT_EVT_CONNECT_RESULT */
return;
}
chat_sync_connect_from_invite(channel_id, real_node_id, pubkey_bin, addrs_data, addr_count, addrs_data_len, NULL, join_key);
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) {
bridge_log(BLEV_ERROR, "join: instance not running");
utun_bridge_event(6, "{\"node_id\":0,\"result\":-7}"); /* CHAT_EVT_CONNECT_RESULT */
return;
}
chat_sync_connect_from_invite(inst, channel_id, real_node_id, pubkey_bin, addrs_data, addr_count, addrs_data_len, NULL, join_key);
}
void utun_bridge_request_invite_candidates(const char* channel_id) {
@ -245,10 +254,12 @@ void utun_bridge_request_invite_candidates(const char* channel_id) {
}
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "request_invite_candidates: uasync not running"); return; }
char* cid = u_strdup(channel_id);
if (!cid) { bridge_log(BLEV_ERROR, "request_invite_candidates: OOM"); return; }
struct chat_invite_cand_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "request_invite_candidates: OOM"); return; }
req->inst = instance_lite_get_instance();
snprintf(req->ch_id, sizeof(req->ch_id), "%s", channel_id);
bridge_log(BLEV_INFO, "request_invite_candidates ch=%s", channel_id);
uasync_post(ua, chat_invite_candidates_trampoline, cid);
uasync_post(ua, chat_invite_candidates_trampoline, req);
}
void utun_bridge_request_invite_link(const char* channel_id, uint64_t target_node_id) {
@ -260,6 +271,7 @@ void utun_bridge_request_invite_link(const char* channel_id, uint64_t target_nod
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "request_invite_link: uasync not running"); return; }
struct chat_invite_build_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "request_invite_link: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
req->target_node_id = target_node_id;
bridge_log(BLEV_INFO, "request_invite_link ch=%s target=0x%016llx",
@ -274,10 +286,12 @@ void utun_bridge_connect_channel(const char* channel_id) {
}
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) return;
char* cid = u_strdup(channel_id);
if (!cid) return;
struct chat_create_auto_arg* req = u_calloc(1, sizeof(*req));
if (!req) return;
req->inst = instance_lite_get_instance();
snprintf(req->name, sizeof(req->name), "%s", channel_id);
bridge_log(BLEV_INFO, "connect_channel ch=%s", channel_id);
uasync_post(ua, chat_core_connect_channel_trampoline, cid);
uasync_post(ua, chat_core_connect_channel_trampoline, req);
}
void utun_bridge_connect_member_node(const char* channel_id, uint64_t node_id) {
@ -290,6 +304,7 @@ void utun_bridge_connect_member_node(const char* channel_id, uint64_t node_id) {
if (!uasync_is_running(ua)) return;
struct chat_connect_node_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "connect_member_node: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
req->node_id = node_id;
bridge_log(BLEV_INFO, "connect_member_node ch=%s node=0x%016llx", channel_id, (unsigned long long)node_id);
@ -391,10 +406,11 @@ static void bridge_format_preview_body(const char* ct, const char* d, char* out,
}
char* utun_bridge_get_channels_json(void) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !db) {
bridge_log(BLEV_WARN, "get_channels: not initialized db=%p initialized=%d",
(void*)db, chat_core_is_initialized());
(void*)db, chat_core_is_initialized(inst));
return u_strdup("[]");
}
size_t cap = 4096, pos = 1;
@ -445,8 +461,7 @@ char* utun_bridge_get_channels_json(void) {
}
int online = 0;
uint64_t my_id = g_cc.my_node_id;
struct UTUN_INSTANCE* inst = chat_core_get_inst();
uint64_t my_id = inst->node_id;
sqlite3_stmt* pc = NULL;
snprintf(sql, sizeof(sql), "SELECT DISTINCT node_id FROM \"%s\"", tbl_peers);
if (sqlite3_prepare_v2(db, sql, -1, &pc, NULL) == SQLITE_OK) {
@ -480,9 +495,10 @@ char* utun_bridge_get_channels_json(void) {
}
char* utun_bridge_get_messages_json(const char* channel_id, int limit) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db || !channel_id || !channel_id[0]) return u_strdup("[]");
uint64_t my_id = g_cc.my_node_id;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !db || !channel_id || !channel_id[0]) return u_strdup("[]");
uint64_t my_id = inst->node_id;
char tbl_msg[80];
msg_table_name(channel_id, tbl_msg, sizeof(tbl_msg));
@ -580,7 +596,8 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit) {
}
uint64_t utun_bridge_get_my_node_id(void) {
return chat_core_is_initialized() ? g_cc.my_node_id : 0;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
return (inst && chat_core_is_initialized(inst)) ? inst->node_id : 0;
}
void utun_bridge_set_debug_level(const char* category, const char* level) {
@ -594,11 +611,14 @@ void utun_bridge_set_debug_level(const char* category, const char* level) {
}
void utun_bridge_chat_setting_set(const char* name, const char* value) {
chat_core_set_setting(name, value);
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) { bridge_log(BLEV_ERROR, "chat_setting_set: instance not running"); return; }
chat_core_set_setting(inst, name, value);
}
int utun_bridge_chat_setting_get_int(const char* name, int def) {
return chat_setting_get_int(name, def);
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
return chat_setting_get_int(inst, name, def);
}
/* ──────────────────────────────────────────────────────────────────
@ -610,8 +630,9 @@ int utun_bridge_chat_setting_get_int(const char* name, int def) {
#include <openssl/evp.h>
char* utun_bridge_get_channel_members_json(const char* channel_id) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db || !channel_id || !channel_id[0]) return u_strdup("[]");
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !db || !channel_id || !channel_id[0]) return u_strdup("[]");
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
@ -644,7 +665,7 @@ char* utun_bridge_get_channel_members_json(const char* channel_id) {
if (!json) { sqlite3_finalize(st); return u_strdup("[]"); }
json[0] = '[';
uint64_t my_id = g_cc.my_node_id;
uint64_t my_id = inst->node_id;
int first = 1;
while (sqlite3_step(st) == SQLITE_ROW) {
@ -702,8 +723,9 @@ char* utun_bridge_get_channel_members_json(const char* channel_id) {
}
char* utun_bridge_get_member_detail_json(const char* channel_id, uint64_t node_id) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db || !channel_id || node_id == 0) return u_strdup("{}");
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !db || !channel_id || node_id == 0) return u_strdup("{}");
char buf[8192];
size_t pos = 0;
@ -774,8 +796,9 @@ char* utun_bridge_get_member_detail_json(const char* channel_id, uint64_t node_i
}
void utun_bridge_set_member_nick(const char* channel_id, uint64_t node_id, const char* nick) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db || !channel_id || node_id == 0) return;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !db || !channel_id || node_id == 0) return;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256];
@ -796,6 +819,7 @@ void utun_bridge_set_member_nick(const char* channel_id, uint64_t node_id, const
}
struct member_tags_edit_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t node_id;
int admin, supernode, moder, storage, verified, deleted;
@ -803,8 +827,8 @@ struct member_tags_edit_req {
static void member_tags_edit_trampoline(void* arg) {
struct member_tags_edit_req* req = (struct member_tags_edit_req*)arg;
if (!req) return;
struct chat_member_tags* t = chat_member_tags_load(req->ch_id, req->node_id);
if (!req || !req->inst) { u_free(arg); return; }
struct chat_member_tags* t = chat_member_tags_load(req->inst, req->ch_id, req->node_id);
if (!t) {
bridge_log(BLEV_ERROR, "set_member_flags: tags_load failed ch=%s nid=0x%016llx",
req->ch_id, (unsigned long long)req->node_id);
@ -817,7 +841,7 @@ static void member_tags_edit_trampoline(void* arg) {
chat_member_tags_set(t, "storage", req->storage ? "yes" : "no");
chat_member_tags_set(t, "verified", req->verified ? "yes" : "no");
chat_member_tags_set(t, "deleted", req->deleted ? "yes" : "no");
int rc = chat_member_tags_commit(t);
int rc = chat_member_tags_commit(req->inst, t);
bridge_log(rc == 0 ? BLEV_INFO : BLEV_ERROR,
"set_member_flags ch=%s nid=0x%016llx admin=%d super=%d moder=%d storage=%d verified=%d deleted=%d rc=%d",
req->ch_id, (unsigned long long)req->node_id, req->admin, req->supernode,
@ -827,8 +851,9 @@ static void member_tags_edit_trampoline(void* arg) {
}
int utun_bridge_is_channel_owner(const char* channel_id) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db || !channel_id || !channel_id[0]) return 0;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !db || !channel_id || !channel_id[0]) return 0;
uint8_t priv[32];
return topo_node_sqlite_channel_get_priv(db, channel_id, priv) == 0 ? 1 : 0;
}
@ -841,6 +866,7 @@ void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id,
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "set_member_flags: uasync not running"); return; }
struct member_tags_edit_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "set_member_flags: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
req->node_id = node_id;
req->admin = admin; req->supernode = supernode; req->moder = moder;
@ -851,7 +877,7 @@ void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id,
}
char* utun_bridge_get_member_links_json(uint64_t node_id) {
struct UTUN_INSTANCE* inst = chat_core_get_inst();
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst || node_id == 0) return u_strdup("{}");
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
@ -916,6 +942,7 @@ void utun_bridge_transfer_admin(const char* channel_id, uint64_t node_id) {
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "transfer_admin: uasync not running"); return; }
struct chat_transfer_admin_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "transfer_admin: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
req->target_node_id = node_id;
uasync_post(ua, chat_core_transfer_admin_trampoline, req);
@ -932,6 +959,7 @@ void utun_bridge_delete_channel(const char* channel_id) {
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "delete_channel: uasync not running"); return; }
struct chat_delete_channel_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "delete_channel: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
uasync_post(ua, chat_core_delete_channel_trampoline, req);
bridge_log(BLEV_INFO, "delete_channel queued ch=%s", channel_id);
@ -944,8 +972,8 @@ void utun_bridge_delete_channel(const char* channel_id) {
#include "../../../lib/socket_compat.h"
char* utun_bridge_get_local_sockets_json(void) {
if (!chat_core_is_initialized() || !g_cc.inst) return u_strdup("[]");
struct UTUN_INSTANCE* inst = g_cc.inst;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst || !chat_core_is_initialized(inst)) return u_strdup("[]");
size_t cap = 4096, pos = 1;
char* json = u_malloc(cap);
@ -1049,14 +1077,15 @@ char* utun_bridge_get_local_sockets_json(void) {
}
void utun_bridge_on_network_change(void) {
if (!g_cc.inst) return;
auto_socket_on_network_change(g_cc.inst);
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) return;
auto_socket_on_network_change(inst);
bridge_log(BLEV_DEBUG, "network change notified to auto_socket");
}
static void bridge_set_active_trampoline(void* arg) {
int active = (int)(intptr_t)arg;
struct UTUN_INSTANCE* inst = chat_core_get_inst();
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) return;
utun_set_client_activity(inst, active);
standby_set_enabled(inst, !active);
@ -1181,6 +1210,7 @@ int utun_bridge_attachment_download(const char* channel_id, int64_t msg_id) {
if (!uasync_is_running(ua)) return -1;
struct attachment_dl_req* req = u_calloc(1, sizeof(*req));
if (!req) return -1;
req->inst = instance_lite_get_instance();
snprintf(req->channel_id, sizeof(req->channel_id), "%s", channel_id);
req->msg_id = msg_id;
bridge_log(BLEV_INFO, "attachment_download: ch=%s msg_id=%lld", channel_id, (long long)msg_id);
@ -1678,7 +1708,8 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetChatSetting(
(void)thiz;
const char* n = (*env)->GetStringUTFChars(env, name, NULL);
const char* v = (*env)->GetStringUTFChars(env, value, NULL);
chat_core_set_setting(n, v);
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (inst) chat_core_set_setting(inst, n, v);
if (v) (*env)->ReleaseStringUTFChars(env, value, v);
if (n) (*env)->ReleaseStringUTFChars(env, name, n);
}
@ -1687,7 +1718,8 @@ JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeGetChatSettingInt
JNIEnv* env, jobject thiz, jstring name, jint def) {
(void)thiz;
const char* n = (*env)->GetStringUTFChars(env, name, NULL);
int val = chat_setting_get_int(n, (int)def);
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
int val = chat_setting_get_int(inst, n, (int)def);
if (n) (*env)->ReleaseStringUTFChars(env, name, n);
return (jint)val;
}

1
tools/chatgui-android/libutun_lite/attachment_sender.c

@ -66,6 +66,7 @@ int attachment_send(const char* channel_id, const char* src_file_path, const cha
struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + datalen + 1);
if (!req) return -1;
req->inst = instance_lite_get_instance();
snprintf(req->channel_id, sizeof(req->channel_id), "%s", channel_id);
snprintf(req->content_type, sizeof(req->content_type), "application/octet-stream");
snprintf(req->media_src, sizeof(req->media_src), "%s", src_file_path);

54
tools/chatgui-android/libutun_lite/instance_lite.c

@ -158,7 +158,8 @@ static void cfg_get_val(const char* text, const char* key, char* out, size_t out
/* ── Chat event forward → Kotlin callback ── */
static void chat_event_forward(int type, const uint8_t* data, int len) {
static void chat_event_forward(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
(void)inst;
if (g_event_handler) { g_event_handler(type, data, len); return; }
IL_LOGE("[EVT_DIAG] chat_event_forward: g_event_handler is NULL! type=%d len=%d", type, len);
}
@ -271,7 +272,6 @@ static void* instance_thread(void* arg) {
}
IL_LOGI("[EVT_DIAG] thread: about to set chat_event handler, g_event_handler=%p", (void*)g_event_handler);
chat_event_set_handler(chat_event_forward);
ensure_keys(config);
g_inst = utun_instance_create_from_config(g_ua, config);
@ -285,6 +285,7 @@ static void* instance_thread(void* arg) {
pthread_detach(pthread_self());
return NULL;
}
chat_event_set_handler(g_inst, chat_event_forward);
IL_LOGI("instance created, node_id=0x%016llx", (unsigned long long)g_inst->node_id);
@ -322,7 +323,7 @@ static void* instance_thread(void* arg) {
}
/* sockets are now created — sync my addresses to DB */
chat_core_sync_my_addresses();
chat_core_sync_my_addresses(g_inst);
fire_local_sockets_event();
/* Bind chat sync via etcp_router */
@ -333,7 +334,7 @@ static void* instance_thread(void* arg) {
/* Set my_name from config */
if (g_inst->config->global.name[0]) {
chat_core_update_my_name(g_inst->config->global.name);
chat_core_update_my_name(g_inst, g_inst->config->global.name);
IL_LOGI("my_name set to '%s'", g_inst->config->global.name);
}
@ -341,13 +342,13 @@ static void* instance_thread(void* arg) {
/* Notify GUI of current public key (initial generation or existing) */
if (strlen(config->global.my_public_key_hex) == 64) {
chat_event_post(CHAT_EVT_KEYS_GENERATED,
chat_event_post(g_inst, CHAT_EVT_KEYS_GENERATED,
(const uint8_t*)config->global.my_public_key_hex, 64);
}
__atomic_store_n(&g_running, 1, __ATOMIC_RELEASE);
uasync_mark_running(g_ua);
chat_event_post(CHAT_EVT_SERVICE_STARTED, NULL, 0);
chat_event_post(g_inst, CHAT_EVT_SERVICE_STARTED, NULL, 0);
while (!__atomic_load_n(&g_stop, __ATOMIC_ACQUIRE)) {
uasync_poll(g_ua, -1);
@ -373,8 +374,6 @@ static void* instance_thread(void* arg) {
if (!g_ua) { IL_LOGE("poll exit: uasync_create failed"); break; }
standby_init(g_ua);
chat_event_set_handler(chat_event_forward);
char* cfg = (char*)__atomic_exchange_n(&g_restart_config, NULL, __ATOMIC_ACQUIRE);
if (!cfg) { IL_LOGE("poll exit: restart_config is NULL"); break; }
__atomic_store_n(&g_stop, 0, __ATOMIC_RELEASE);
@ -386,6 +385,7 @@ static void* instance_thread(void* arg) {
g_inst = utun_instance_create_from_config(g_ua, config);
if (!g_inst) { IL_LOGE("poll exit: create_from_config failed"); break; }
chat_event_set_handler(g_inst, chat_event_forward);
char db_file[512];
snprintf(db_file, sizeof(db_file), "%s/chats.db", g_db_path);
@ -399,17 +399,17 @@ static void* instance_thread(void* arg) {
} else { IL_LOGE("poll exit: SQLite open failed rc=%d", rc); break; }
if (utun_instance_init(g_inst) != 0) { IL_LOGE("poll exit: utun_instance_init failed"); break; }
chat_core_sync_my_addresses();
chat_core_sync_my_addresses(g_inst);
fire_local_sockets_event();
if (g_inst->config->global.name[0]) chat_core_update_my_name(g_inst->config->global.name);
if (g_inst->config->global.name[0]) chat_core_update_my_name(g_inst, g_inst->config->global.name);
etcp_router_bind(g_inst, ETCP_RT_ID_CHAT_SYNC, NULL);
utun_add_nodeinfo_cbk(g_inst, nodeinfo_event_cb, NULL);
etcp_add_link_status_cbk(g_inst, on_link_status_changed, NULL);
chat_event_post(CHAT_EVT_KEYS_GENERATED, (const uint8_t*)config->global.my_public_key_hex, 64);
utun_add_nodeinfo_cbk(g_inst, nodeinfo_event_cb, NULL);
etcp_add_link_status_cbk(g_inst, on_link_status_changed, NULL);
chat_event_post(g_inst, CHAT_EVT_KEYS_GENERATED, (const uint8_t*)config->global.my_public_key_hex, 64);
__atomic_store_n(&g_running, 1, __ATOMIC_RELEASE);
uasync_mark_running(g_ua);
chat_event_post(CHAT_EVT_SERVICE_STARTED, NULL, 0);
chat_event_post(g_inst, CHAT_EVT_SERVICE_STARTED, NULL, 0);
IL_LOGI("poll exit: restart done, entering new poll loop");
while (!__atomic_load_n(&g_stop, __ATOMIC_ACQUIRE)) uasync_poll(g_ua, -1);
@ -420,6 +420,7 @@ static void* instance_thread(void* arg) {
if (my_gen == g_generation) {
__atomic_store_n(&g_running, 0, __ATOMIC_RELEASE);
if (g_inst) {
chat_event_post(g_inst, CHAT_EVT_SERVICE_STOPPED, NULL, 0);
utun_instance_destroy(g_inst);
g_inst = NULL;
}
@ -431,7 +432,6 @@ static void* instance_thread(void* arg) {
char* stale_cfg = (char*)__atomic_exchange_n(&g_restart_config, NULL, __ATOMIC_ACQUIRE);
u_free(stale_cfg);
u_report_unfreed_blocks();
chat_event_post(CHAT_EVT_SERVICE_STOPPED, NULL, 0);
IL_LOGI("cleanup complete (gen=%d)", my_gen);
} else {
IL_LOGI("cleanup skipped — thread gen=%d but current gen=%d (stale thread)", my_gen, g_generation);
@ -662,16 +662,19 @@ int instance_lite_is_responsive(void) {
void instance_lite_collect_conn_list(void) {
struct UASYNC* ua = (struct UASYNC*)__atomic_load_n(&g_ua, __ATOMIC_ACQUIRE);
if (!ua) return;
uasync_post(ua, chat_core_collect_conn_list_trampoline, NULL);
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)__atomic_load_n(&g_inst, __ATOMIC_ACQUIRE);
if (!ua || !inst) return;
uasync_post(ua, chat_core_collect_conn_list_trampoline, inst);
}
void instance_lite_collect_conn_metrics(uint64_t peer_node_id) {
struct UASYNC* ua = (struct UASYNC*)__atomic_load_n(&g_ua, __ATOMIC_ACQUIRE);
if (!ua) return;
uint64_t* arg = (uint64_t*)u_malloc(sizeof(uint64_t));
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)__atomic_load_n(&g_inst, __ATOMIC_ACQUIRE);
if (!ua || !inst) return;
struct chat_node_arg* arg = (struct chat_node_arg*)u_malloc(sizeof(struct chat_node_arg));
if (!arg) return;
*arg = peer_node_id;
arg->inst = inst;
arg->node_id = peer_node_id;
uasync_post(ua, chat_core_collect_conn_metrics_trampoline, arg);
}
@ -684,6 +687,10 @@ struct UASYNC* instance_lite_get_uasync(void) {
return (struct UASYNC*)__atomic_load_n(&g_ua, __ATOMIC_ACQUIRE);
}
struct UTUN_INSTANCE* instance_lite_get_instance(void) {
return (struct UTUN_INSTANCE*)__atomic_load_n(&g_inst, __ATOMIC_ACQUIRE);
}
static void regenerate_trampoline(void* arg) {
(void)arg;
if (!g_inst || !g_inst->config) { IL_LOGE("regenerate_keys: instance not running"); return; }
@ -698,7 +705,7 @@ static void regenerate_trampoline(void* arg) {
g_inst->node_id = sc_derive_node_id_from_pubkey(pk_bin);
sc_init_local_keys(&g_inst->my_keys, g_generated_pub, g_generated_priv);
IL_LOGI("keys regenerated, node_id=0x%016llx", (unsigned long long)g_inst->node_id);
chat_event_post(CHAT_EVT_KEYS_GENERATED, (const uint8_t*)g_generated_pub, 64);
chat_event_post(g_inst, CHAT_EVT_KEYS_GENERATED, (const uint8_t*)g_generated_pub, 64);
}
}
@ -712,5 +719,8 @@ void instance_lite_regenerate_keys(void) {
void instance_lite_set_event_handler(instance_lite_event_fn handler) {
IL_LOGI("[EVT_DIAG] instance_lite_set_event_handler handler=%p g_running=%d", (void*)handler, g_running);
g_event_handler = handler;
if (__atomic_load_n(&g_running, __ATOMIC_ACQUIRE)) chat_event_set_handler(chat_event_forward);
if (__atomic_load_n(&g_running, __ATOMIC_ACQUIRE)) {
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)__atomic_load_n(&g_inst, __ATOMIC_ACQUIRE);
if (inst) chat_event_set_handler(inst, chat_event_forward);
}
}

4
tools/chatgui-android/libutun_lite/instance_lite.h

@ -15,6 +15,7 @@ extern "C" {
#endif
struct UASYNC;
struct UTUN_INSTANCE;
/* Start full uTun stack from INI config text.
* Creates a pthread for uasync event loop.
@ -32,6 +33,9 @@ int instance_lite_is_running(void);
/* Get current uasync handle (NULL if not running). */
struct UASYNC* instance_lite_get_uasync(void);
/* Get current UTUN_INSTANCE handle (NULL if not running). */
struct UTUN_INSTANCE* instance_lite_get_instance(void);
/* Set Kotlin callback for chat events (CONNECT_RESULT, MSG_RECEIVED, ...) */
typedef void (*instance_lite_event_fn)(int type, const uint8_t* data, int len);
void instance_lite_set_event_handler(instance_lite_event_fn handler);

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

Loading…
Cancel
Save