diff --git a/doc/chat_call_arch.md b/doc/chat_call_arch.md
new file mode 100644
index 00000000..7b37692d
--- /dev/null
+++ b/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.
diff --git a/doc/dm_arch.md b/doc/dm_arch.md
new file mode 100644
index 00000000..1cfc3996
--- /dev/null
+++ b/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_) и кэширует 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
diff --git a/doc/tasks.md b/doc/tasks.md
index 9b2c856a..79d34471 100644
--- a/doc/tasks.md
+++ b/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) его не трогает.
diff --git a/lib/debug_config.c b/lib/debug_config.c
index a7196562..ba23fb73 100644
--- a/lib/debug_config.c
+++ b/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}
};
diff --git a/lib/debug_config.h b/lib/debug_config.h
index ce990eac..52d478fc 100644
--- a/lib/debug_config.h
+++ b/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 */
diff --git a/lib/soundtouch-master/.gitignore b/lib/soundtouch-master/.gitignore
new file mode 100644
index 00000000..94e524d8
--- /dev/null
+++ b/lib/soundtouch-master/.gitignore
@@ -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
diff --git a/lib/soundtouch-master/CMakeLists.txt b/lib/soundtouch-master/CMakeLists.txt
new file mode 100644
index 00000000..d9bc15d5
--- /dev/null
+++ b/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
+ $
+ $
+)
+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 $)
+ 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}"
+)
diff --git a/lib/soundtouch-master/COPYING.TXT b/lib/soundtouch-master/COPYING.TXT
new file mode 100644
index 00000000..c04d4d80
--- /dev/null
+++ b/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
\ No newline at end of file
diff --git a/lib/soundtouch-master/README.html b/lib/soundtouch-master/README.html
new file mode 100644
index 00000000..826e72d2
--- /dev/null
+++ b/lib/soundtouch-master/README.html
@@ -0,0 +1,1020 @@
+
+
+
+
+ SoundTouch library README
+
+
+
+
+
+
+
+
+ SoundTouch audio processing library v2.3.3
+ SoundTouch library Copyright © Olli Parviainen 2001-2024
+
+ 1. Introduction
+ SoundTouch is an open-source audio processing library that allows
+ changing the sound tempo, pitch and playback rate parameters
+ independently from each other, i.e.:
+
+ - Sound tempo can be increased or decreased while maintaining the
+ original pitch
+ - Sound pitch can be increased or decreased while maintaining the
+ original tempo
+ - Change playback rate that affects both tempo and pitch at the
+ same time
+ - Choose any combination of tempo/pitch/rate
+
+ 1.1 Contact information
+ Author email: oparviai 'at' iki.fi
+ SoundTouch WWW page: http://soundtouch.surina.net
+ SoundTouch git repository: https://codeberg.org/soundtouch/soundtouch.git
+
+ 2. Compiling SoundTouch
+ Before compiling, notice that you can choose the sample data format if it's
+ desirable to use 16bit integer sample data instead of floating point samples. See
+ section "sample data format" for more information.
+ Also notice that SoundTouch can use OpenMP instructions for parallel
+ computation to accelerate the runtime processing speed in multi-core systems,
+ however, these improvements need to be separately enabled before compiling. See
+ OpenMP notes in Chapter 3 below.
+ 2.1. Building in Microsoft Windows
+ Project files for Microsoft Visual C++ are supplied with the source
+ code package. Go to Microsoft WWW page to download
+
+ Microsoft Visual Studio Express version for free.
+
+ To build the binaries with Visual C++ compiler, either run
+ "make-win.bat" script, or open the appropriate project files in source
+ code directories with Visual Studio. The final executable will appear
+ under the "SoundTouch\bin" directory. If using the Visual Studio IDE
+ instead of the make-win.bat script, directories bin and lib may need to
+ be created manually to the SoundTouch package root for the final
+ executables. The make-win.bat script creates these directories
+ automatically.
+ C# example: The source code package includes also a C# example
+ application for Windows that shows how to invoke SoundTouch.dll
+ dynamic-load library for processing mp3 audio.
+
OpenMP NOTE: If activating the OpenMP parallel computing in
+ the compilation, the target program will require additional vcomp dll library to
+ properly run. In Visual C++ 9.0 these libraries can be found in the following
+ folders.
+
+ - x86 32bit: C:\Program Files (x86)\Microsoft Visual Studio
+ 9.0\VC\redist\x86\Microsoft.VC90.OPENMP\vcomp90.dll
+ - x64 64bit: C:\Program Files (x86)\Microsoft Visual Studio
+ 9.0\VC\redist\amd64\Microsoft.VC90.OPENMP\vcomp90.dll
+
+ In other VC++ versions the required library will be expectedly found in similar
+ "redist" location.
+ Notice that as minor demonstration of a "dll hell" phenomenon both the 32-bit
+ and 64-bit version of vcomp90.dll have the same filename but different contents,
+ thus choose the proper version to allow the program to start.
+ 2.2. Building in Gnu platforms
+ The SoundTouch library compiles in practically any platform
+ supporting GNU compiler (GCC) tools.
+
2.2.1 Compiling with autotools
+ To install build prerequisites for 'autotools' tool chain:
+ sudo apt-get install automake autoconf libtool build-essential
+ To build and install the binaries, run the following commands in
+ /soundtouch directory:
+
+
+
+
+ ./bootstrap -
+ |
+ Creates "configure" file with
+ local autoconf/automake toolset.
+ |
+
+
+
+ ./configure -
+ |
+
+ Configures the SoundTouch package for the local environment.
+ Notice that "configure" file is not available before running the
+ "./bootstrap" command as above.
+
+ |
+
+
+
+ make -
+ |
+
+ Builds the SoundTouch library & SoundStretch utility. You can
+ optionally add "-j" switch after "make" to speed up the compilation in
+ multi-core systems.
+ |
+
+
+
+ make install -
+ |
+
+ Installs the SoundTouch & BPM libraries to /usr/local/lib
+ and SoundStretch utility to /usr/local/bin. Please notice that
+ 'root' privileges may be required to install the binaries to the
+ destination locations.
+ |
+
+
+
+
+ Compiling portable Shared Library / DLL version
+ The GNU autotools compilation automatically builds an additional dynamic-link version
+ of SoundTouch library that features position-independent code and "C"-style API that is
+ more suitable for calling the SoundTouch routines from other programming languages.
+ This dynamic-link library is built under source/SoundTouchDLL directory, whose
+ subdirectories also comtain simple example apps that use the dynamic-link library.
+
+
+ 2.2.2 Compiling with cmake
+ 'cmake' build scripts are provided as an alternative to the autotools toolchain.
+ To install cmake build prerequisites:
+ sudo apt-get install libtool build-essential cmake
+ To build:
+
+ cmake .
+ make -j
+ make install
+ To list available build options:
+
+ cmake -LH
+ To compile the additional portable Shared Library / DLL version with the native C-language API:
+
+ cmake . -DSOUNDTOUCH_DLL=ON
+ make -j
+ make install
+
+ 2.3. Building in Android
+ Android compilation instructions are within the
+ source code package, see file "source/Android-lib/README-SoundTouch-Android.html"
+ in the source code package.
+ The Android compilation automatically builds separate .so library binaries
+ for ARM, X86 and MIPS processor architectures. For optimal device support,
+ include all these .so library binaries into the Android .apk application
+ package, so the target Android device can automatically choose the proper
+ library binary version to use.
+ The source/Android-lib folder includes also an Android
+ example application that processes WAV audio files using SoundTouch library in
+ Android devices.
+
+ 2.4. Building in Mac
+ Install autoconf tool as instructed in http://macappstore.org/autoconf/, or alternatively the 'cmake' toolchain.
+ Then, build as described above in section "Building in Gnu platforms".
+
+
+ 3. About implementation & Usage tips 3.1. Supported sample data formats
+
The sample data format can be chosen between 16bit signed integer
+ and 32bit floating point values.
+
The default sample type is 32bit floating point format,
+ which also provides better sound quality than integer format because
+ integer algorithms need to scale already intermediate calculation results to
+ avoid integer overflows. These early integer scalings can slightly degrade
+ output quality.
+ In Windows environment, the sample data format is chosen in file
+ "STTypes.h" by choosing one of the following defines:
+
+ - #define
+ SOUNDTOUCH_INTEGER_SAMPLES for 16bit signed integer
+ - #define SOUNDTOUCH_FLOAT_SAMPLES for 32bit floating
+ point
+
+ In GNU environment, the floating sample format is used by default,
+ but integer sample format can be chosen by giving the following switch
+ to the configure script:
+
+ ./configure --enable-integer-samples
+
+ The sample data can have either single (mono) or double (stereo)
+ audio channel. Stereo data is interleaved so that every other data
+ value is for left channel and every second for right channel. Notice
+ that while it'd be possible in theory to process stereo sound as two
+ separate mono channels, this isn't recommended because processing the
+ channels separately would result in losing the phase coherency between
+ the channels, which consequently would ruin the stereo effect.
+ Sample rates between 8000-48000Hz are supported.
+ 3.2. Processing latency
+ The processing and latency constraints of the SoundTouch library are:
+
+ - Input/output processing latency for the SoundTouch processor is
+ around 100 ms. This is when time-stretching is used. If the rate
+ transposing effect alone is used, the latency requirement is much
+ shorter, see section 'About algorithms'.
+ - Processing CD-quality sound (16bit stereo sound with 44100H
+ sample rate) in real-time or faster is possible starting from
+ processors equivalent to Intel Pentium 133Mh or better, if using the
+ "quick" processing algorithm. If not using the "quick" mode or if
+ floating point sample data are being used, several times more CPU power
+ is typically required.
+
+ 3.3. About algorithms
+ SoundTouch provides three seemingly independent effects: tempo,
+ pitch and playback rate control. These three controls are implemented
+ as combination of two primary effects, sample rate transposing
+ and time-stretching.
+ Sample rate transposing affects both the audio stream
+ duration and pitch. It's implemented simply by converting the original
+ audio sample stream to the desired duration by interpolating from
+ the original audio samples. In SoundTouch, linear interpolation with
+ anti-alias filtering is used. Theoretically a higher-order
+ interpolation provide better result than 1st order linear
+ interpolation, but in audio application linear interpolation together
+ with anti-alias filtering performs subjectively about as well as
+ higher-order filtering would.
+ Time-stretching means changing the audio stream duration
+ without affecting it's pitch. SoundTouch uses WSOLA-like
+ time-stretching routines that operate in the time domain. Compared to
+ sample rate transposing, time-stretching is a much heavier operation
+ and also requires a longer processing "window" of sound samples used by
+ the processing algorithm, thus increasing the algorithm input/output
+ latency. Typical i/o latency for the SoundTouch time-stretch algorithm
+ is around 100 ms.
+ Sample rate transposing and time-stretching are then used together
+ to produce the tempo, pitch and rate controls:
+
+ - 'Tempo' control is implemented purely by
+ time-stretching.
+ - 'Rate' control is implemented purely by sample
+ rate transposing.
+ - 'Pitch' control is implemented as a
+ combination of time-stretching and sample rate transposing. For
+ example, to increase pitch the audio stream is first time-stretched to
+ longer duration (without affecting pitch) and then transposed back to
+ original duration by sample rate transposing, which simultaneously
+ reduces duration and increases pitch. The result is original duration
+ but increased pitch.
+
+ 3.4 Tuning the algorithm parameters
+ The time-stretch algorithm has few parameters that can be tuned to
+ optimize sound quality for certain application. The current default
+ parameters have been chosen by iterative if-then analysis (read: "trial
+ and error") to obtain best subjective sound quality in pop/rock music
+ processing, but in applications processing different kind of sound the
+ default parameter set may result into a sub-optimal result.
+ The time-stretch algorithm default parameter values are set by the
+ following #defines in file "TDStretch.h":
+
+ #define DEFAULT_SEQUENCE_MS AUTOMATIC
#define DEFAULT_SEEKWINDOW_MS AUTOMATIC
#define DEFAULT_OVERLAP_MS 8
+
+ These parameters affect to the time-stretch algorithm as follows:
+
+ - DEFAULT_SEQUENCE_MS: This is the default
+ length of a single processing sequence in milliseconds which determines
+ the how the original sound is chopped in the time-stretch algorithm.
+ Larger values mean fewer sequences are used in processing. In principle
+ a larger value sounds better when slowing down the tempo, but worse
+ when increasing the tempo and vice versa.
+
+ By default, this setting value is calculated automatically according to
+ tempo value.
+
+ - DEFAULT_SEEKWINDOW_MS: The seeking window
+ default length in milliseconds is for the algorithm that seeks the best
+ possible overlapping location. This determines from how wide a sample
+ "window" the algorithm can use to find an optimal mixing location when
+ the sound sequences are to be linked back together.
+
+ The bigger this window setting is, the higher the possibility to find a
+ better mixing position becomes, but at the same time large values may
+ cause a "drifting" sound artifact because neighboring sequences can be
+ chosen at more uneven intervals. If there's a disturbing artifact that
+ sounds as if a constant frequency was drifting around, try reducing
+ this setting.
+
+ By default, this setting value is calculated automatically according to
+ tempo value.
+
+ - DEFAULT_OVERLAP_MS: Overlap length in
+ milliseconds. When the sound sequences are mixed back together to form
+ again a continuous sound stream, this parameter defines how much the
+ ends of the consecutive sequences will overlap with 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.
+
+
+ Notice that these parameters can also be set during execution time
+ with functions "TDStretch::setParameters()" and "SoundTouch::setSetting()".
+ The table below summaries how the parameters can be adjusted for
+ different applications:
+
+
+
+ | Parameter name |
+ Default value magnitude |
+ Larger value affects... |
+ Smaller value affects... |
+ Effect to CPU burden |
+
+
+
+ SEQUENCE_MS
+ |
+ Default value is relatively large, chosen for
+ slowing down music tempo |
+ Larger value is usually better for slowing down
+ tempo. Growing the value decelerates the "echoing" artifact when
+ slowing down the tempo. |
+ Smaller value might be better for speeding up
+ tempo. Reducing the value accelerates the "echoing" artifact when
+ slowing down the tempo |
+ Increasing the parameter value reduces
+ computation burden |
+
+
+
+ SEEKWINDOW_MS
+ |
+ Default value is relatively large, chosen for
+ slowing down music tempo |
+ Larger value eases finding a good mixing
+ position, but may cause a "drifting" artifact |
+ Smaller reduce possibility to find a good mixing
+ position, but reduce the "drifting" artifact. |
+ Increasing the parameter value increases
+ computation burden |
+
+
+
+ OVERLAP_MS
+ |
+ Default value is relatively large, chosen to
+ suit with above parameters. |
+ |
+ If you reduce the "sequence ms" setting, you
+ might wish to try a smaller value. |
+ Increasing the parameter value increases
+ computation burden |
+
+
+
+ 3.5 Performance Optimizations
+ Integer vs floating point:
+ Floating point sample type is generally recommended because it provides
+ better sound quality.
+
+ However, execution speed difference between integer and floating point processing
+ depends on the CPU architecture. As rule of thumb,
+
+ - in 32-bit x86 floating point and integer are roughly equally fast
+ - in 64-bit x86/x64 floating point can be significantly faster than integer
+ version, because MMX integer optimizations are not available in the x64 architecture.
+ That depends on the compiler however, so that gcc can autovectorize integer routines
+ to work equally fast as floating point, where as Visual C++ (2017) does not
+ perform equally well and produces integer code that runs some 3x slower than
+ SSE-optimized floating poing code.
+
+ - in ARMv7 integer routines are twice as fast as floating point. Their
+ relative difference is roughly the same both with and without NEON; NEON
+ vfpu can however bring 2.4x speed improvement.
+
+ - in other platforms: try out if the execution time performance makes a
+ big difference
+
+
+ General optimizations:
+ The time-stretch routine has a 'quick' mode that substantially
+ speeds up the algorithm but may slightly compromise the sound quality.
+ This mode is activated by calling SoundTouch::setSetting()
+ function with parameter id of SETTING_USE_QUICKSEEK and value
+ "1", i.e.
+
+ setSetting(SETTING_USE_QUICKSEEK, 1);
+
+ CPU-specific optimizations:
+ Intel x86 specific SIMD optimizations are implemented using compiler
+ intrinsics, providing about a 3x processing speedup for x86 compatible
+ processors vs. non-SIMD implementation:
+
+ - MMX optimized routines are used in 32-bit x86 build when 16bit integer
+ sample type is used
+ - SSE optimized routines are used in 32- and 64-bit x86 CPUs when 32bit
+ floating point sample type is used
+
+ The algorithms are tuned to utilize autovectorization efficiently
+ also in other CPU architectures, for example ARM cpus see approx 2.4x processing
+ speedup when NEON SIMD support is present.
+
+ 3.5 OpenMP parallel computation
+ SoundTouch 1.9 onwards support running the algorithms parallel in several CPU
+ cores. Based on benchmark the experienced multi-core processing speed-up gain
+ ranges between +30% (on a high-spec dual-core x86 Windows PC) to 215% (on a moderately low-spec
+ quad-core ARM of Raspberry Pi2).
+ See an external blog article with more detailed discussion about the
+
+ SoundTouch OpenMP optimization.
+
+ The parallel computing support is implemented using OpenMP spec 3.0
+ instructions. These instructions are supported by Visual C++ 2008 and later, and
+ GCC v4.2 and later. Compilers that do not supporting OpenMP will ignore these
+ optimizations and routines will still work properly. Possible warnings about
+ unknown #pragmas are related to OpenMP support and can be safely ignored.
+ The OpenMP improvements are disabled by default, and need to be enabled by
+ developer during compile-time. Reason for this is that parallel processing adds
+ moderate runtime overhead in managing the multi-threading, so it may not be
+ necessary nor desirable in all applications. For example real-time processing
+ that is not constrained by CPU power will not benefit of speed-up provided by
+ the parallel processing, in the contrary it may increase power consumption due
+ to the increased overhead.
+ However, applications that run on low-spec multi-core CPUs and may otherwise
+ have possibly constrained performance will benefit of the OpenMP improvements.
+ This include for example multi-core embedded devices.
+ OpenMP parallel computation can be enabled before compiling SoundTouch
+ library as follows:
+
+ - Visual Studio: Open properties for the SoundTouch
+ sub-project, browse to C/C++ and Language
+ settings. Set
+ there "OpenMP support" to "Yes". Alternatively add
+ /openmp switch to command-line
+ parameters
+
+ - GNU: Run the configure script with "./configure
+ --enable-openmp" switch, then run make as usually
+ - Android: Add "-fopenmp" switches to compiler & linker
+ options, see README-SoundTouch-Android.html in the source code package for
+ more detailed instructions.
+
+
+ 4. SoundStretch audio processing utility
+
+ SoundStretch audio processing utility
+ Copyright (c) Olli Parviainen 2002-2024
+ SoundStretch is a simple command-line application that can change
+ tempo, pitch and playback rates of WAV sound files. This program is
+ intended primarily to demonstrate how the "SoundTouch" library can be
+ used to process sound in your own program, but it can as well be used
+ for processing sound files.
+ 4.1. SoundStretch Usage Instructions
+ SoundStretch Usage syntax:
+
+ soundstretch infilename outfilename [switches]
+
+ Where:
+
+
+
+
+ "infilename"
+ |
+ Name of the input sound data file (in .WAV audio
+ file format). Give "stdin" as filename to use standard input pipe. |
+
+
+
+ "outfilename"
+ |
+ Name of the output sound file where the
+ resulting sound is saved (in .WAV audio file format). This parameter
+ may be omitted if you don't want to save the output (e.g. when
+ only calculating BPM rate with '-bpm' switch). Give "stdout" as
+ filename to use standard output pipe. |
+
+
+
+ [switches]
+ |
+ Are one or more control switches. |
+
+
+
+ Available control switches are:
+
+
+
+
+ -tempo=n
+ |
+ Change the sound tempo by n percents (n = -95.0
+ .. +5000.0 %) |
+
+
+
+ -pitch=n
+ |
+ Change the sound pitch by n semitones (n = -60.0
+ .. + 60.0 semitones) |
+
+
+
+ -rate=n
+ |
+ Change the sound playback rate by n percents (n
+ = -95.0 .. +5000.0 %) |
+
+
+
+ -bpm=n
+ |
+ Detect the Beats-Per-Minute (BPM) rate of the
+ sound and adjust the tempo to meet 'n' BPMs. When this switch is
+ applied, the "-tempo" switch is ignored. If "=n" is omitted, i.e.
+ switch "-bpm" is used alone, then the BPM rate is estimated and
+ displayed, but tempo not adjusted according to the BPM value. |
+
+
+
+ -quick
+ |
+ Use quicker tempo change algorithm. Gains speed
+ but loses sound quality. |
+
+
+
+ -naa
+ |
+ Don't use anti-alias filtering in sample rate
+ transposing. Gains speed but loses sound quality. |
+
+
+
+ -license
+ |
+ Displays the program license text (LGPL) |
+
+
+
+ Notes:
+
+ - To use standard input/output pipes for processing, give "stdin"
+ and "stdout" as input/output filenames correspondingly. The standard
+ input/output pipes will still carry the audio data in .wav audio file
+ format.
+ - The numerical switches allow both integer (e.g. "-tempo=123")
+ and decimal (e.g. "-tempo=123.45") numbers.
+ - The "-naa" and/or "-quick" switches can be used to reduce CPU
+ usage while compromising some sound quality
+ - The BPM detection algorithm works by detecting repeating bass or
+ drum patterns at low frequencies of <250Hz. A lower-than-expected
+ BPM figure may be reported for music with uneven or complex bass
+ patterns.
+
+ 4.2. SoundStretch usage examples
+ Example 1
+ The following command increases tempo of the sound file
+ "originalfile.wav" by 12.5% and stores result to file
+ "destinationfile.wav":
+
+ soundstretch originalfile.wav destinationfile.wav -tempo=12.5
+
+ Example 2
+ The following command decreases the sound pitch (key) of the sound
+ file "orig.wav" by two semitones and stores the result to file
+ "dest.wav":
+
+ soundstretch orig.wav dest.wav -pitch=-2
+
+ Example 3
+ The following command processes the file "orig.wav" by decreasing
+ the sound tempo by 25.3% and increasing the sound pitch (key) by 1.5
+ semitones. Resulting .wav audio data is directed to standard output
+ pipe:
+
+ soundstretch orig.wav stdout -tempo=-25.3 -pitch=1.5
+
+ Example 4
+ The following command detects the BPM rate of the file "orig.wav"
+ and adjusts the tempo to match 100 beats per minute. Result is stored
+ to file "dest.wav":
+
+ soundstretch orig.wav dest.wav -bpm=100
+
+ Example 5
+ The following command reads .wav sound data from standard input pipe
+ and estimates the BPM rate:
+
+ soundstretch stdin -bpm
+
+ Example 6
+ The following command tunes song from original 440Hz tuning to 432Hz tuning:
+ this corresponds to lowering the pitch by -0.318 semitones:
+
+ soundstretch original.wav output.wav -pitch=-0.318
+
+
+ 5. Change History
+ 5.1. SoundTouch library Change History
+ 2.3.3:
+
+ - Fixing compiler warnings, maintenance fixes to make/build files for various systems
+
+
+ 2.3.2:
+
+ - Improve autotools makefiles to build the `SoundTouchDLL` dynamic-link link library with
+ C-style API. This library variation is easier to import and use from other programming
+ languages than the default C++ library.
+
+
+ 2.3.1:
+
+ - Adjusted cmake build settings and header files that cmake installs
+
+ 2.3.0:
+
+ - Disable setting "SOUNDTOUCH_ALLOW_NONEXACT_SIMD_OPTIMIZATION" by default. The original
+ purpose of this setting was to avoid performance penalty due to unaligned SIMD memory
+ accesses in old CPUs, but that is not any more issue in concurrent CPU SIMD implementations
+ and having this setting enabled can cause slight compromise in result quality.
+
+ - Bugfix: soundtouch.clear() to really clear whole processing pipeline state. Earlier
+ individual variables were left uncleared, which caused slightly different result if
+ the same audio stream were processed again after calling clear().
+
+ - Bugfix: TDstretch to align initial offset position to be in middle of correlation search
+ window. This ensures that with zero tempo change the output will be same as input.
+
+ - Bugfix: Fix a bug in TDstrectch with too small initial skipFract value that occurred
+ with certain processing parameter settings: Replace assert with assignment that
+ corrects the situation.
+
+ - Remove OpenMP "_init_threading" workaround from Android build as it's not needed with concurrent
+ Android SDKs any more.
+
+ 2.2:
+
+ - Improved source codes so that compiler can autovectorize them more effectively.
+ This brings remarkable improvement e.g. ARM cpus equipped with NEON vfpu: Bencmarked
+ 2.4x improvement in execution speed in ARMv7l vs the previous SoundTouch version
+ for both integer and floating point sample types.
+
+ - Bugfix: Resolved bad sound quality when using integer sample types in non-x86 CPU
+ - Bugfix: Fixed possible reading past end of array in BPM peak detection algorithm
+
+ 2.1.2:
+
+ - Bump version to 2.1.2 also in configure.ac. The earlier release had old version info for GNU autotools.
+
+ 2.1.1:
+
+ - Bugfixes: Fixed potential buffer overwrite bugs in WavFile routines. Replaced asserts with runtime exceptions.
+
+ - Android: Migrated the SoundTouch Android example to new Android Studio
+ - Automake: unset ACLOCAL in bootstrap script in case earlier build script has set it
+
+
+ 2.1:
+
+ - Refactored C# interface example
+ - Disable anti-alias filter when switch
+ SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER defined because anti-alias
+ filter cause slight click if the rate change crosses zero during
+ processing
+ - Added script for building SoundTouchDll dynamic-link-library for GNU platforms
+ - Rewrote Beats-per-Minute analysis algorithm for more reliable BPM detection
+ - Added BPM functions to SoundTouchDll API
+ - Migrated Visual Studio project files to MSVC 201x format
+ - Replaced function parameter value asserts with runtime exceptions
+ - Code maintenance & style cleanup
+
+ 2.0:
+
+ - Added functions to get initial processing latency, duration ratio between the original input and processed
+ output tracks, and clarified reporting of input/output batch sizes
+ - Fixed issue that added brief sequence of silence to beginning of output audio
+ - Adjusted algorithm parameters to reduce reverberating effect at tempo slowdown
+ - Bugfix: Fixed a glitch that could cause negative array indexing in quick seek algorithm
+ - Bugfix: flush() didn't properly flush final samples from the pipeline on 2nd time in case that soundtouch
+ object instance was recycled and used for processing a second audio stream.
+ - Bugfix: Pi value had incorrect 9th/10th decimals
+ - Added C# example application that uses SoundTouch dll library for processing MP3 files
+
+ 1.9.2:
+
+ - Fix in GNU package configuration
+
+ 1.9.1:
+
+ - Improved SoundTouch::flush() function so that it returns precisely the desired amount of samples for exact
+ output duration control
+ - Redesigned quickseek algorithm for improved sound quality when using the quickseek mode. The new quickseek
+ algorithm can find 99% as good results as the
+ default full-scan mode, while the quickseek algorithm is remarkable less
+ CPU intensive.
+ - Added adaptive integer divider scaling for improved sound quality when using integer processing algorithm
+
+
+ 1.9:
+
+ - Added support for parallel computation support via OpenMP primitives for better performance in multicore
+ systems.
+ Benchmarks show that achieved parallel processing speedup improvement
+ typically range from +30% (x86 dual-core) to +180% (ARM quad-core). The
+ OpenMP optimizations are disabled by default, see OpenMP notes above in this
+ readme file how to enabled these optimizations.
+ - Android: Added support for Android devices featuring X86 and MIPS CPUs,
+ in addition to ARM CPUs.
+ - Android: More versatile Android example application that processes WAV
+ audio files with SoundTouch library
+ - Replaced Windows-like 'BOOL' types with native 'bool'
+ - Changed documentation token to "dist_doc_DATA" in Makefile.am file
+ - Miscellaneous small fixes and improvements
+
+ 1.8.0:
+
+ - Added support for multi-channel audio processing
+ - Added support for cubic and shannon interpolation for rate and pitch shift effects besides
+ the original linear interpolation, to reduce aliasing at high frequencies due to interpolation.
+ Cubic interpolation is used as default for floating point processing, and linear interpolation for integer
+ processing.
+ - Fixed bug in anti-alias filtering that limited stop-band attenuation to -10 dB instead of <-50dB, and
+ increased filter length from 32 to 64 taps to further reduce aliasing due to frequency folding.
+ - Performance improvements in cross-correlation algorithm
+ - Other bug and compatibility fixes
+
+ 1.7.1:
+
+ - Added files for Android compilation
+
+ 1.7.0:
+
+ - Sound quality improvements/li>
+
- Improved flush() to adjust output sound stream duration to match better with
+ ideal duration
+ - Rewrote x86 cpu feature check to resolve compatibility problems
+ - Configure script automatically checks if CPU supports mmx & sse compatibility for GNU platform, and
+ the script support now "--enable-x86-optimizations" switch to allow disabling x86-specific optimizations.
+ - Revised #define conditions for 32bit/64bit compatibility
+ - gnu autoconf/automake script compatibility fixes
+ - Tuned beat-per-minute detection algorithm
+
+ 1.6.0:
+
+ - Added automatic cutoff threshold adaptation to beat detection
+ routine to better adapt BPM calculation to different types of music
+ - Retired 3DNow! optimization support as 3DNow! is nowadays
+ obsoleted and assembler code is nuisance to maintain
+ - Retired "configure" file from source code package due to
+ autoconf/automake versio conflicts, so that it is from now on to be
+ generated by invoking "boostrap" script that uses locally available
+ toolchain version for generating the "configure" file
+ - Resolved namespace/label naming conflicts with other libraries by
+ replacing global labels such as INTEGER_SAMPLES with more specific
+ SOUNDTOUCH_INTEGER_SAMPLES etc.
+
+ - Updated windows build scripts & project files for Visual
+ Studio 2008 support
+ - Updated SoundTouch.dll API for .NET compatibility
+ - Added API for querying nominal processing input & output
+ sample batch sizes
+
+ 1.5.0:
+
+ - Added normalization to correlation calculation and improvement
+ automatic seek/sequence parameter calculation to improve sound quality
+ - Bugfixes:
+
+ - Fixed negative array indexing in quick seek algorithm
+ - FIR autoalias filter running too far in processing buffer
+ - Check against zero sample count in rate transposing
+ - Fix for x86-64 support: Removed pop/push instructions from
+ the cpu detection algorithm.
+ - Check against empty buffers in FIFOSampleBuffer
+ - Other minor fixes & code cleanup
+
+
+ - Fixes in compilation scripts for non-Intel platforms
+ - Added Dynamic-Link-Library (DLL) version of SoundTouch library
+ build, provided with Delphi/Pascal wrapper for calling the dll routines
+
+ - Added #define PREVENT_CLICK_AT_RATE_CROSSOVER that prevents a
+ click artifact when crossing the nominal pitch from either positive to
+ negative side or vice versa
+
+ 1.4.1:
+
+ - Fixed a buffer overflow bug in BPM detect algorithm routines if
+ processing more than 2048 samples at one call
+
+ 1.4.0:
+
+ - Improved sound quality by automatic calculation of time stretch
+ algorithm processing parameters according to tempo setting
+ - Moved BPM detection routines from SoundStretch application into
+ SoundTouch library
+ - Bugfixes: Usage of uninitialied variables, GNU build scripts,
+ compiler errors due to 'const' keyword mismatch.
+ - Source code cleanup
+
+ 1.3.1:
+
+ - Changed static class declaration to GCC 4.x compiler compatible
+ syntax.
+ - Enabled MMX/SSE-optimized routines also for GCC compilers.
+ Earlier the MMX/SSE-optimized routines were written in
+ compiler-specific inline assembler, now these routines are migrated to
+ use compiler intrinsic syntax which allows compiling the same
+ MMX/SSE-optimized source code with both Visual C++ and GCC compilers.
+ - Set floating point as the default sample format and added switch
+ to the GNU configure script for selecting the other sample format.
+
+ 1.3.0:
+
+ - Fixed tempo routine output duration inaccuracy due to rounding
+ error
+ - Implemented separate processing routines for integer and
+ floating arithmetic to allow improvements to floating point routines
+ (earlier used algorithms mostly optimized for integer arithmetic also
+ for floating point samples)
+ - Fixed a bug that distorts sound if sample rate changes during
+ the sound stream
+ - Fixed a memory leak that appeared in MMX/SSE/3DNow! optimized
+ routines
+ - Reduced redundant code pieces in MMX/SSE/3DNow! optimized
+ routines vs. the standard C routines.
+ - MMX routine incompatibility with new gcc compiler versions
+ - Other miscellaneous bug fixes
+
+ 1.2.1:
+
+ - Added automake/autoconf scripts for GNU platforms (in courtesy
+ of David Durham)
+ - Fixed SCALE overflow bug in rate transposer routine.
+ - Fixed 64bit address space bugs.
+ - Created a 'soundtouch' namespace for SAMPLETYPE definitions.
+
+ 1.2.0:
+
+ - Added support for 32bit floating point sample data type with
+ SSE/3DNow! optimizations for Win32 platform (SSE/3DNow! optimizations
+ currently not supported in GCC environment)
+ - Replaced 'make-gcc' script for GNU environment by master
+ Makefile
+ - Added time-stretch routine configurability to SoundTouch main
+ class
+ - Bugfixes
+
+ 1.1.1:
+
+ - Moved SoundTouch under lesser GPL license (LGPL). This allows
+ using SoundTouch library in programs that aren't released under GPL
+ license.
+ - Changed MMX routine organiation so that MMX optimized routines
+ are now implemented in classes that are derived from the basic classes
+ having the standard non-mmx routines.
+ - MMX routines to support gcc version 3.
+ - Replaced windows makefiles by script using the .dsw files
+
+ 1.0.1:
+
+ - "mmx_gcc.cpp": Added "using namespace std" and removed "return
+ 0" from a function with void return value to fix compiler errors when
+ compiling the library in Solaris environment.
+ - Moved file "FIFOSampleBuffer.h" to "include" directory to allow
+ accessing the FIFOSampleBuffer class from external files.
+
+ 1.0:
+
+ 5.2. SoundStretch application Change History
+ 2.3.3:
+
+ - Added support for Asian / non-latin filenames in Windows. Gnu platform has supported them already earlier.
+
+ 1.9:
+
+ - Added support for WAV file 'fact' information chunk.
+
+ 1.7.0:
+
+ - Bugfixes in Wavfile: exception string formatting, avoid getLengthMs() integer
+ precision overflow, support WAV files using 24/32bit sample format.
+
+ 1.5.0:
+
+ - Added "-speech" switch to activate algorithm parameters more
+ suitable for speech processing than the default parameters tuned for
+ music processing.
+
+ 1.4.0:
+
+ - Moved BPM detection routines from SoundStretch application into
+ SoundTouch library
+ - Allow using standard input/output pipes as audio processing
+ input/output streams
+
+ 1.3.0:
+
+ - Simplified accessing WAV files with floating point sample
+ format.
+
+ 1.2.1:
+
+ - Fixed 64bit address space bugs.
+
+ 1.2.0:
+
+ - Added support for 32bit floating point sample data type
+ - Restructured the BPM routines into separate library
+ - Fixed big-endian conversion bugs in WAV file routines (hopefully
+ :)
+
+ 1.1.1:
+
+ - Fixed bugs in WAV file reading & added byte-order conversion
+ for big-endian processors.
+ - Moved SoundStretch source code under 'example' directory to
+ highlight difference from SoundTouch stuff.
+ - Replaced windows makefiles by script using the .dsw files
+ - Output file name isn't required if output isn't desired (e.g. if
+ using the switch '-bpm' in plain format only)
+
+ 1.1:
+
+ - Fixed "Release" settings in Microsoft Visual C++ project file
+ (.dsp)
+ - Added beats-per-minute (BPM) detection routine and command-line
+ switch "-bpm"
+
+ 1.01:
+
+
+ 6. Acknowledgements
+ Kudos for these people who have contributed to development or
+ submitted bugfixes:
+
+ - Arthur A
+ - Paul Adenot
+ - Richard Ash
+ - Stanislav Brabec
+ - Christian Budde
+ - Jamie Bullock
+ - Chris Bryan
+ - Jacek Caban
+ - Marketa Calabkova
+ - Brian Cameron
+ - Jason Champion
+ - Giuseppe Cigala
+ - David Clark
+ - Patrick Colis
+ - Miquel Colon
+ - Jim Credland
+ - Sandro Cumerlato
+ - Gerry Fan
+ - Justin Frankel
+ - Masa H.
+ - Jason Garland
+ - Takashi Iwai
+ - Thomas Klausner
+ - Lu Zhihe
+ - Luzpaz
+ - Tony Mechelynck
+ - Mathias Möhl
+ - Yuval Naveh
+ - Mats Palmgren
+ - Chandni Patel
+ - Paulo Pizarro
+ - Andrey Ponomarenko
+ - Blaise Potard
+ - Michael Pruett
+ - Rajeev Puran
+ - RJ Ryan
+ - Serge Sans Paille
+ - John Sheehy
+ - Tim Shuttleworth
+ - Albert Sirvent
+ - Tyson Smith
+ - John Stumpo
+ - Mario di Vece
+ - Rémi Verschelde
+ - Katja Vetter
+ - Wu Q.
+
+ Moral greetings to all other contributors and users also!
+
+ 7. 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 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
+ ---
+ commercial license alternative also available, contact author for details.
+
+
+
+
\ No newline at end of file
diff --git a/lib/soundtouch-master/SoundTouchConfig.cmake.in b/lib/soundtouch-master/SoundTouchConfig.cmake.in
new file mode 100644
index 00000000..3e6ef89c
--- /dev/null
+++ b/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()
diff --git a/lib/soundtouch-master/include/soundtouch/BPMDetect.h b/lib/soundtouch-master/include/soundtouch/BPMDetect.h
new file mode 100644
index 00000000..bf596a1b
--- /dev/null
+++ b/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
+#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 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_
diff --git a/lib/soundtouch-master/include/soundtouch/FIFOSampleBuffer.h b/lib/soundtouch-master/include/soundtouch/FIFOSampleBuffer.h
new file mode 100644
index 00000000..e7cdbed9
--- /dev/null
+++ b/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
diff --git a/lib/soundtouch-master/include/soundtouch/FIFOSamplePipe.h b/lib/soundtouch-master/include/soundtouch/FIFOSamplePipe.h
new file mode 100644
index 00000000..93b6bf47
--- /dev/null
+++ b/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
+#include
+#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
diff --git a/lib/soundtouch-master/include/soundtouch/STTypes.h b/lib/soundtouch-master/include/soundtouch/STTypes.h
new file mode 100644
index 00000000..9ed15291
--- /dev/null
+++ b/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
+ #define ST_THROW_RT_ERROR(x) {assert((const char *)x);}
+#else
+ // use c++ standard exceptions
+ #include
+ #include
+ #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
diff --git a/lib/soundtouch-master/include/soundtouch/SoundTouch.h b/lib/soundtouch-master/include/soundtouch/SoundTouch.h
new file mode 100644
index 00000000..079d4094
--- /dev/null
+++ b/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
diff --git a/lib/soundtouch-master/include/soundtouch/SoundTouchDLL.h b/lib/soundtouch-master/include/soundtouch/SoundTouchDLL.h
new file mode 100644
index 00000000..a83da6eb
--- /dev/null
+++ b/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_
+
diff --git a/lib/soundtouch-master/readme.md b/lib/soundtouch-master/readme.md
new file mode 100644
index 00000000..cda19195
--- /dev/null
+++ b/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
diff --git a/lib/soundtouch-master/source/SoundTouch/AAFilter.cpp b/lib/soundtouch-master/source/SoundTouch/AAFilter.cpp
new file mode 100644
index 00000000..f0972003
--- /dev/null
+++ b/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
+#include
+#include
+#include
+#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
+
+ 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();
+}
diff --git a/lib/soundtouch-master/source/SoundTouch/AAFilter.h b/lib/soundtouch-master/source/SoundTouch/AAFilter.h
new file mode 100644
index 00000000..5b63ed6e
--- /dev/null
+++ b/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
diff --git a/lib/soundtouch-master/source/SoundTouch/BPMDetect.cpp b/lib/soundtouch-master/source/SoundTouch/BPMDetect.cpp
new file mode 100644
index 00000000..a6d370a1
--- /dev/null
+++ b/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
+#include
+#include
+#include
+#include
+#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 &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;
+}
diff --git a/lib/soundtouch-master/source/SoundTouch/FIFOSampleBuffer.cpp b/lib/soundtouch-master/source/SoundTouch/FIFOSampleBuffer.cpp
new file mode 100644
index 00000000..9e0d5b24
--- /dev/null
+++ b/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
+#include
+#include
+#include
+
+#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;
+}
diff --git a/lib/soundtouch-master/source/SoundTouch/FIRFilter.cpp b/lib/soundtouch-master/source/SoundTouch/FIRFilter.cpp
new file mode 100644
index 00000000..94a7f814
--- /dev/null
+++ b/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
+#include
+#include
+#include
+#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;
+ }
+}
diff --git a/lib/soundtouch-master/source/SoundTouch/FIRFilter.h b/lib/soundtouch-master/source/SoundTouch/FIRFilter.h
new file mode 100644
index 00000000..0acb199b
--- /dev/null
+++ b/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
+#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
diff --git a/lib/soundtouch-master/source/SoundTouch/InterpolateCubic.cpp b/lib/soundtouch-master/source/SoundTouch/InterpolateCubic.cpp
new file mode 100644
index 00000000..fe4c98a8
--- /dev/null
+++ b/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
+#include
+#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;
+}
diff --git a/lib/soundtouch-master/source/SoundTouch/InterpolateCubic.h b/lib/soundtouch-master/source/SoundTouch/InterpolateCubic.h
new file mode 100644
index 00000000..4eae60a6
--- /dev/null
+++ b/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
diff --git a/lib/soundtouch-master/source/SoundTouch/InterpolateLinear.cpp b/lib/soundtouch-master/source/SoundTouch/InterpolateLinear.cpp
new file mode 100644
index 00000000..b4571d68
--- /dev/null
+++ b/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
+#include
+#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;
+}
diff --git a/lib/soundtouch-master/source/SoundTouch/InterpolateLinear.h b/lib/soundtouch-master/source/SoundTouch/InterpolateLinear.h
new file mode 100644
index 00000000..22eda3ce
--- /dev/null
+++ b/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
diff --git a/lib/soundtouch-master/source/SoundTouch/InterpolateShannon.cpp b/lib/soundtouch-master/source/SoundTouch/InterpolateShannon.cpp
new file mode 100644
index 00000000..ac0b81ab
--- /dev/null
+++ b/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
+#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;
+}
diff --git a/lib/soundtouch-master/source/SoundTouch/InterpolateShannon.h b/lib/soundtouch-master/source/SoundTouch/InterpolateShannon.h
new file mode 100644
index 00000000..fec56a77
--- /dev/null
+++ b/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
diff --git a/lib/soundtouch-master/source/SoundTouch/PeakFinder.cpp b/lib/soundtouch-master/source/SoundTouch/PeakFinder.cpp
new file mode 100644
index 00000000..ebb79ee7
--- /dev/null
+++ b/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
+#include
+
+#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;
+}
diff --git a/lib/soundtouch-master/source/SoundTouch/PeakFinder.h b/lib/soundtouch-master/source/SoundTouch/PeakFinder.h
new file mode 100644
index 00000000..106eac80
--- /dev/null
+++ b/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_
diff --git a/lib/soundtouch-master/source/SoundTouch/RateTransposer.cpp b/lib/soundtouch-master/source/SoundTouch/RateTransposer.cpp
new file mode 100644
index 00000000..2b921186
--- /dev/null
+++ b/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
+#include
+#include
+#include
+#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
+}
diff --git a/lib/soundtouch-master/source/SoundTouch/RateTransposer.h b/lib/soundtouch-master/source/SoundTouch/RateTransposer.h
new file mode 100644
index 00000000..addf780a
--- /dev/null
+++ b/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
+#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
diff --git a/lib/soundtouch-master/source/SoundTouch/SoundTouch.cpp b/lib/soundtouch-master/source/SoundTouch/SoundTouch.cpp
new file mode 100644
index 00000000..0eda80ae
--- /dev/null
+++ b/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
+#include
+#include
+#include
+#include
+
+#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);
+}
diff --git a/lib/soundtouch-master/source/SoundTouch/SoundTouch.sln b/lib/soundtouch-master/source/SoundTouch/SoundTouch.sln
new file mode 100644
index 00000000..0e08834e
--- /dev/null
+++ b/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
diff --git a/lib/soundtouch-master/source/SoundTouch/SoundTouch.vcxproj b/lib/soundtouch-master/source/SoundTouch/SoundTouch.vcxproj
new file mode 100644
index 00000000..588ab6f0
--- /dev/null
+++ b/lib/soundtouch-master/source/SoundTouch/SoundTouch.vcxproj
@@ -0,0 +1,342 @@
+
+
+
+
+ Debug
+ Win32
+
+
+ Debug
+ x64
+
+
+ Release
+ Win32
+
+
+ Release
+ x64
+
+
+
+ {68A5DD20-7057-448B-8FE0-B6AC8D205509}
+ 10.0
+
+
+
+ StaticLibrary
+ v142
+ false
+ Unicode
+
+
+ StaticLibrary
+ v142
+ false
+ Unicode
+
+
+ StaticLibrary
+ v142
+ false
+ Unicode
+
+
+ StaticLibrary
+ v142
+ false
+ Unicode
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_ProjectFileVersion>14.0.23107.0
+
+
+ $(Platform)\$(Configuration)\
+ $(Platform)\$(Configuration)\
+
+
+ $(Platform)\$(Configuration)\
+ $(Platform)\$(Configuration)\
+ $(ProjectName)_x64
+
+
+ $(Platform)\$(Configuration)\
+ $(Platform)\$(Configuration)\
+ $(ProjectName)D
+
+
+ $(Platform)\$(Configuration)\
+ $(Platform)\$(Configuration)\
+ $(ProjectName)D_x64
+
+
+
+ Full
+ AnySuitable
+ true
+ ..\..\include;%(AdditionalIncludeDirectories)
+ WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)
+ true
+ MultiThreaded
+ true
+ Fast
+
+ $(OutDir)$(TargetName).pch
+ $(OutDir)
+ $(OutDir)
+ $(OutDir)
+ Level3
+ true
+
+ Default
+ StreamingSIMDExtensions2
+ $(IntDir)
+ $(IntDir)
+ true
+
+
+ NDEBUG;%(PreprocessorDefinitions)
+ 0x040b
+
+
+ $(OutDir)$(TargetName)$(TargetExt)
+ true
+
+
+ if not exist ..\..\lib mkdir ..\..\lib
+copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib
+
+
+
+
+ X64
+
+
+ Full
+ AnySuitable
+ true
+ ..\..\include;%(AdditionalIncludeDirectories)
+ WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)
+ true
+ MultiThreaded
+ true
+ Fast
+
+ $(OutDir)$(TargetName).pch
+ $(OutDir)
+ $(OutDir)
+ $(OutDir)
+ Level3
+ true
+
+ Default
+
+
+ $(IntDir)
+ $(IntDir)
+ true
+
+
+ NDEBUG;%(PreprocessorDefinitions)
+ 0x040b
+
+
+ $(OutDir)$(TargetName)$(TargetExt)
+ true
+
+
+ if not exist ..\..\lib mkdir ..\..\lib
+copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib
+
+
+
+
+ Disabled
+ ..\..\include;%(AdditionalIncludeDirectories)
+ WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)
+ EnableFastChecks
+ MultiThreadedDebug
+ Fast
+
+ $(OutDir)$(TargetName).pch
+ $(OutDir)
+ $(OutDir)
+ $(OutDir)
+ true
+ Level3
+ true
+ ProgramDatabase
+ Default
+ StreamingSIMDExtensions2
+ $(IntDir)
+ $(IntDir)
+ true
+
+
+ _DEBUG;%(PreprocessorDefinitions)
+ 0x040b
+
+
+ $(OutDir)$(TargetName)$(TargetExt)
+ true
+
+
+ if not exist ..\..\lib mkdir ..\..\lib
+copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib
+
+
+
+
+ X64
+
+
+ Disabled
+ ..\..\include;%(AdditionalIncludeDirectories)
+ WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)
+ EnableFastChecks
+ MultiThreadedDebug
+ Fast
+
+ $(OutDir)$(TargetName).pch
+ $(OutDir)
+ $(OutDir)
+ $(OutDir)
+ true
+ Level3
+ true
+ ProgramDatabase
+ Default
+
+
+ $(IntDir)
+ $(IntDir)
+ true
+
+
+ _DEBUG;%(PreprocessorDefinitions)
+ 0x040b
+
+
+ $(OutDir)$(TargetName)$(TargetExt)
+ true
+
+
+ if not exist ..\..\lib mkdir ..\..\lib
+copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib
+
+
+
+
+ Disabled
+ EnableFastChecks
+ true
+ Disabled
+ EnableFastChecks
+ true
+ MaxSpeed
+ MaxSpeed
+
+
+ 4996
+ 4996
+ 4996
+ 4996
+
+
+
+ Disabled
+ EnableFastChecks
+ true
+ Disabled
+ EnableFastChecks
+ true
+ MaxSpeed
+ MaxSpeed
+
+
+ Disabled
+ EnableFastChecks
+ true
+ Disabled
+ EnableFastChecks
+ true
+ MaxSpeed
+ MaxSpeed
+
+
+
+
+
+
+
+ Disabled
+ EnableFastChecks
+ true
+ Disabled
+ EnableFastChecks
+ true
+ MaxSpeed
+ MaxSpeed
+
+
+ Disabled
+ EnableFastChecks
+ true
+ Disabled
+ EnableFastChecks
+ true
+ MaxSpeed
+ MaxSpeed
+
+
+
+ Disabled
+ EnableFastChecks
+ true
+ Disabled
+ EnableFastChecks
+ true
+ MaxSpeed
+ MaxSpeed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/soundtouch-master/source/SoundTouch/TDStretch.cpp b/lib/soundtouch-master/source/SoundTouch/TDStretch.cpp
new file mode 100644
index 00000000..80c421ca
--- /dev/null
+++ b/lib/soundtouch-master/source/SoundTouch/TDStretch.cpp
@@ -0,0 +1,1085 @@
+///////////////////////////////////////////////////////////////////////////////
+///
+/// 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.
+///
+/// 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
+#include
+#include
+#include
+#include
+
+#include "STTypes.h"
+#include "cpu_detect.h"
+#include "TDStretch.h"
+
+using namespace soundtouch;
+
+#define max(x, y) (((x) > (y)) ? (x) : (y))
+
+/*****************************************************************************
+ *
+ * Implementation of the class 'TDStretch'
+ *
+ *****************************************************************************/
+
+
+TDStretch::TDStretch() : FIFOProcessor(&outputBuffer)
+{
+ bQuickSeek = false;
+ channels = 2;
+
+ pMidBuffer = nullptr;
+ pMidBufferUnaligned = nullptr;
+ overlapLength = 0;
+
+ bAutoSeqSetting = true;
+ bAutoSeekSetting = true;
+
+ tempo = 1.0f;
+ setParameters(44100, DEFAULT_SEQUENCE_MS, DEFAULT_SEEKWINDOW_MS, DEFAULT_OVERLAP_MS);
+ setTempo(1.0f);
+
+ clear();
+}
+
+
+
+TDStretch::~TDStretch()
+{
+ delete[] pMidBufferUnaligned;
+}
+
+
+
+// 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 (default = 82 ms)
+// 'seekwindowMS' = seeking window length for scanning the best overlapping
+// position (default = 28 ms)
+// 'overlapMS' = overlapping length (default = 12 ms)
+
+void TDStretch::setParameters(int aSampleRate, int aSequenceMS,
+ int aSeekWindowMS, int aOverlapMS)
+{
+ // accept only positive parameter values - if zero or negative, use old values instead
+ if (aSampleRate > 0)
+ {
+ if (aSampleRate > 192000) ST_THROW_RT_ERROR("Error: Excessive samplerate");
+ this->sampleRate = aSampleRate;
+ }
+
+ if (aOverlapMS > 0) this->overlapMs = aOverlapMS;
+
+ if (aSequenceMS > 0)
+ {
+ this->sequenceMs = aSequenceMS;
+ bAutoSeqSetting = false;
+ }
+ else if (aSequenceMS == 0)
+ {
+ // if zero, use automatic setting
+ bAutoSeqSetting = true;
+ }
+
+ if (aSeekWindowMS > 0)
+ {
+ this->seekWindowMs = aSeekWindowMS;
+ bAutoSeekSetting = false;
+ }
+ else if (aSeekWindowMS == 0)
+ {
+ // if zero, use automatic setting
+ bAutoSeekSetting = true;
+ }
+
+ calcSeqParameters();
+
+ calculateOverlapLength(overlapMs);
+
+ // set tempo to recalculate 'sampleReq'
+ setTempo(tempo);
+}
+
+
+
+/// 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 TDStretch::getParameters(int *pSampleRate, int *pSequenceMs, int *pSeekWindowMs, int *pOverlapMs) const
+{
+ if (pSampleRate)
+ {
+ *pSampleRate = sampleRate;
+ }
+
+ if (pSequenceMs)
+ {
+ *pSequenceMs = (bAutoSeqSetting) ? (USE_AUTO_SEQUENCE_LEN) : sequenceMs;
+ }
+
+ if (pSeekWindowMs)
+ {
+ *pSeekWindowMs = (bAutoSeekSetting) ? (USE_AUTO_SEEKWINDOW_LEN) : seekWindowMs;
+ }
+
+ if (pOverlapMs)
+ {
+ *pOverlapMs = overlapMs;
+ }
+}
+
+
+// Overlaps samples in 'midBuffer' with the samples in 'pInput'
+void TDStretch::overlapMono(SAMPLETYPE *pOutput, const SAMPLETYPE *pInput) const
+{
+ int i;
+ SAMPLETYPE m1, m2;
+
+ m1 = (SAMPLETYPE)0;
+ m2 = (SAMPLETYPE)overlapLength;
+
+ for (i = 0; i < overlapLength ; i ++)
+ {
+ pOutput[i] = (pInput[i] * m1 + pMidBuffer[i] * m2 ) / overlapLength;
+ m1 += 1;
+ m2 -= 1;
+ }
+}
+
+
+
+void TDStretch::clearMidBuffer()
+{
+ memset(pMidBuffer, 0, channels * sizeof(SAMPLETYPE) * overlapLength);
+}
+
+
+void TDStretch::clearInput()
+{
+ inputBuffer.clear();
+ clearMidBuffer();
+ isBeginning = true;
+ maxnorm = 0;
+ maxnormf = 1e8;
+ skipFract = 0;
+}
+
+
+// Clears the sample buffers
+void TDStretch::clear()
+{
+ outputBuffer.clear();
+ clearInput();
+}
+
+
+
+// Enables/disables the quick position seeking algorithm. Zero to disable, nonzero
+// to enable
+void TDStretch::enableQuickSeek(bool enable)
+{
+ bQuickSeek = enable;
+}
+
+
+// Returns nonzero if the quick seeking algorithm is enabled.
+bool TDStretch::isQuickSeekEnabled() const
+{
+ return bQuickSeek;
+}
+
+
+// Seeks for the optimal overlap-mixing position.
+int TDStretch::seekBestOverlapPosition(const SAMPLETYPE *refPos)
+{
+ if (bQuickSeek)
+ {
+ return seekBestOverlapPositionQuick(refPos);
+ }
+ else
+ {
+ return seekBestOverlapPositionFull(refPos);
+ }
+}
+
+
+// Overlaps samples in 'midBuffer' with the samples in 'pInputBuffer' at position
+// of 'ovlPos'.
+inline void TDStretch::overlap(SAMPLETYPE *pOutput, const SAMPLETYPE *pInput, uint ovlPos) const
+{
+#ifndef USE_MULTICH_ALWAYS
+ if (channels == 1)
+ {
+ // mono sound.
+ overlapMono(pOutput, pInput + ovlPos);
+ }
+ else if (channels == 2)
+ {
+ // stereo sound
+ overlapStereo(pOutput, pInput + 2 * ovlPos);
+ }
+ else
+#endif // USE_MULTICH_ALWAYS
+ {
+ assert(channels > 0);
+ overlapMulti(pOutput, pInput + channels * ovlPos);
+ }
+}
+
+
+// Seeks for the optimal overlap-mixing position. The 'stereo' version of the
+// routine
+//
+// The best position is determined as the position where the two overlapped
+// sample sequences are 'most alike', in terms of the highest cross-correlation
+// value over the overlapping period
+int TDStretch::seekBestOverlapPositionFull(const SAMPLETYPE *refPos)
+{
+ int bestOffs;
+ double bestCorr;
+ int i;
+ double norm;
+
+ bestCorr = -FLT_MAX;
+ bestOffs = 0;
+
+ // Scans for the best correlation value by testing each possible position
+ // over the permitted range.
+ bestCorr = calcCrossCorr(refPos, pMidBuffer, norm);
+ bestCorr = (bestCorr + 0.1) * 0.75;
+
+ for (i = 1; i < seekLength; i ++)
+ {
+ double corr;
+ // Calculates correlation value for the mixing position corresponding to 'i'
+#if defined(_OPENMP) || defined(ST_SIMD_AVOID_UNALIGNED)
+ // in parallel OpenMP mode, can't use norm accumulator version as parallel executor won't
+ // iterate the loop in sequential order
+ // in SIMD mode, avoid accumulator version to allow avoiding unaligned positions
+ corr = calcCrossCorr(refPos + channels * i, pMidBuffer, norm);
+#else
+ // In non-parallel version call "calcCrossCorrAccumulate" that is otherwise same
+ // as "calcCrossCorr", but saves time by reusing & updating previously stored
+ // "norm" value
+ corr = calcCrossCorrAccumulate(refPos + channels * i, pMidBuffer, norm);
+#endif
+ // heuristic rule to slightly favour values close to mid of the range
+ double tmp = (double)(2 * i - seekLength) / (double)seekLength;
+ corr = ((corr + 0.1) * (1.0 - 0.25 * tmp * tmp));
+
+ // Checks for the highest correlation value
+ if (corr > bestCorr)
+ {
+ // For optimal performance, enter critical section only in case that best value found.
+ // in such case repeat 'if' condition as it's possible that parallel execution may have
+ // updated the bestCorr value in the mean time
+ if (corr > bestCorr)
+ {
+ bestCorr = corr;
+ bestOffs = i;
+ }
+ }
+ }
+
+#ifdef SOUNDTOUCH_INTEGER_SAMPLES
+ adaptNormalizer();
+#endif
+
+ // clear cross correlation routine state if necessary (is so e.g. in MMX routines).
+ clearCrossCorrState();
+
+ return bestOffs;
+}
+
+
+// Quick seek algorithm for improved runtime-performance: First roughly scans through the
+// correlation area, and then scan surroundings of two best preliminary correlation candidates
+// with improved precision
+//
+// Based on testing:
+// - This algorithm gives on average 99% as good match as the full algorithm
+// - this quick seek algorithm finds the best match on ~90% of cases
+// - on those 10% of cases when this algorithm doesn't find best match,
+// it still finds on average ~90% match vs. the best possible match
+int TDStretch::seekBestOverlapPositionQuick(const SAMPLETYPE *refPos)
+{
+#define _MIN(a, b) (((a) < (b)) ? (a) : (b))
+#define SCANSTEP 16
+#define SCANWIND 8
+
+ int bestOffs;
+ int i;
+ int bestOffs2;
+ float bestCorr, corr;
+ float bestCorr2;
+ double norm;
+
+ // note: 'float' types used in this function in case that the platform would need to use software-fp
+
+ bestCorr =
+ bestCorr2 = -FLT_MAX;
+ bestOffs =
+ bestOffs2 = SCANWIND;
+
+ // Scans for the best correlation value by testing each possible position
+ // over the permitted range. Look for two best matches on the first pass to
+ // increase possibility of ideal match.
+ //
+ // Begin from "SCANSTEP" instead of SCANWIND to make the calculation
+ // catch the 'middlepoint' of seekLength vector as that's the a-priori
+ // expected best match position
+ //
+ // Roughly:
+ // - 15% of cases find best result directly on the first round,
+ // - 75% cases find better match on 2nd round around the best match from 1st round
+ // - 10% cases find better match on 2nd round around the 2nd-best-match from 1st round
+ for (i = SCANSTEP; i < seekLength - SCANWIND - 1; i += SCANSTEP)
+ {
+ // Calculates correlation value for the mixing position corresponding
+ // to 'i'
+ corr = (float)calcCrossCorr(refPos + channels*i, pMidBuffer, norm);
+ // heuristic rule to slightly favour values close to mid of the seek range
+ float tmp = (float)(2 * i - seekLength - 1) / (float)seekLength;
+ corr = ((corr + 0.1f) * (1.0f - 0.25f * tmp * tmp));
+
+ // Checks for the highest correlation value
+ if (corr > bestCorr)
+ {
+ // found new best match. keep the previous best as 2nd best match
+ bestCorr2 = bestCorr;
+ bestOffs2 = bestOffs;
+ bestCorr = corr;
+ bestOffs = i;
+ }
+ else if (corr > bestCorr2)
+ {
+ // not new best, but still new 2nd best match
+ bestCorr2 = corr;
+ bestOffs2 = i;
+ }
+ }
+
+ // Scans surroundings of the found best match with small stepping
+ int end = _MIN(bestOffs + SCANWIND + 1, seekLength);
+ for (i = bestOffs - SCANWIND; i < end; i++)
+ {
+ if (i == bestOffs) continue; // this offset already calculated, thus skip
+
+ // Calculates correlation value for the mixing position corresponding
+ // to 'i'
+ corr = (float)calcCrossCorr(refPos + channels*i, pMidBuffer, norm);
+ // heuristic rule to slightly favour values close to mid of the range
+ float tmp = (float)(2 * i - seekLength - 1) / (float)seekLength;
+ corr = ((corr + 0.1f) * (1.0f - 0.25f * tmp * tmp));
+
+ // Checks for the highest correlation value
+ if (corr > bestCorr)
+ {
+ bestCorr = corr;
+ bestOffs = i;
+ }
+ }
+
+ // Scans surroundings of the 2nd best match with small stepping
+ end = _MIN(bestOffs2 + SCANWIND + 1, seekLength);
+ for (i = bestOffs2 - SCANWIND; i < end; i++)
+ {
+ if (i == bestOffs2) continue; // this offset already calculated, thus skip
+
+ // Calculates correlation value for the mixing position corresponding
+ // to 'i'
+ corr = (float)calcCrossCorr(refPos + channels*i, pMidBuffer, norm);
+ // heuristic rule to slightly favour values close to mid of the range
+ float tmp = (float)(2 * i - seekLength - 1) / (float)seekLength;
+ corr = ((corr + 0.1f) * (1.0f - 0.25f * tmp * tmp));
+
+ // Checks for the highest correlation value
+ if (corr > bestCorr)
+ {
+ bestCorr = corr;
+ bestOffs = i;
+ }
+ }
+
+ // clear cross correlation routine state if necessary (is so e.g. in MMX routines).
+ clearCrossCorrState();
+
+#ifdef SOUNDTOUCH_INTEGER_SAMPLES
+ adaptNormalizer();
+#endif
+
+ return bestOffs;
+}
+
+
+
+
+/// For integer algorithm: adapt normalization factor divider with music so that
+/// it'll not be pessimistically restrictive that can degrade quality on quieter sections
+/// yet won't cause integer overflows either
+void TDStretch::adaptNormalizer()
+{
+ // Do not adapt normalizer over too silent sequences to avoid averaging filter depleting to
+ // too low values during pauses in music
+ if ((maxnorm > 1000) || (maxnormf > 40000000))
+ {
+ //norm averaging filter
+ maxnormf = 0.9f * maxnormf + 0.1f * (float)maxnorm;
+
+ if ((maxnorm > 800000000) && (overlapDividerBitsNorm < 16))
+ {
+ // large values, so increase divider
+ overlapDividerBitsNorm++;
+ if (maxnorm > 1600000000) overlapDividerBitsNorm++; // extra large value => extra increase
+ }
+ else if ((maxnormf < 1000000) && (overlapDividerBitsNorm > 0))
+ {
+ // extra small values, decrease divider
+ overlapDividerBitsNorm--;
+ }
+ }
+
+ maxnorm = 0;
+}
+
+
+/// clear cross correlation routine state if necessary
+void TDStretch::clearCrossCorrState()
+{
+ // default implementation is empty.
+}
+
+
+/// Calculates processing sequence length according to tempo setting
+void TDStretch::calcSeqParameters()
+{
+ // Adjust tempo param according to tempo, so that variating processing sequence length is used
+ // at various tempo settings, between the given low...top limits
+ #define AUTOSEQ_TEMPO_LOW 0.5 // auto setting low tempo range (-50%)
+ #define AUTOSEQ_TEMPO_TOP 2.0 // auto setting top tempo range (+100%)
+
+ // sequence-ms setting values at above low & top tempo
+ #define AUTOSEQ_AT_MIN 90.0
+ #define AUTOSEQ_AT_MAX 40.0
+ #define AUTOSEQ_K ((AUTOSEQ_AT_MAX - AUTOSEQ_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW))
+ #define AUTOSEQ_C (AUTOSEQ_AT_MIN - (AUTOSEQ_K) * (AUTOSEQ_TEMPO_LOW))
+
+ // seek-window-ms setting values at above low & top tempoq
+ #define AUTOSEEK_AT_MIN 20.0
+ #define AUTOSEEK_AT_MAX 15.0
+ #define AUTOSEEK_K ((AUTOSEEK_AT_MAX - AUTOSEEK_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW))
+ #define AUTOSEEK_C (AUTOSEEK_AT_MIN - (AUTOSEEK_K) * (AUTOSEQ_TEMPO_LOW))
+
+ #define CHECK_LIMITS(x, mi, ma) (((x) < (mi)) ? (mi) : (((x) > (ma)) ? (ma) : (x)))
+
+ double seq, seek;
+
+ if (bAutoSeqSetting)
+ {
+ seq = AUTOSEQ_C + AUTOSEQ_K * tempo;
+ seq = CHECK_LIMITS(seq, AUTOSEQ_AT_MAX, AUTOSEQ_AT_MIN);
+ sequenceMs = (int)(seq + 0.5);
+ }
+
+ if (bAutoSeekSetting)
+ {
+ seek = AUTOSEEK_C + AUTOSEEK_K * tempo;
+ seek = CHECK_LIMITS(seek, AUTOSEEK_AT_MAX, AUTOSEEK_AT_MIN);
+ seekWindowMs = (int)(seek + 0.5);
+ }
+
+ // Update seek window lengths
+ seekWindowLength = (sampleRate * sequenceMs) / 1000;
+ if (seekWindowLength < 2 * overlapLength)
+ {
+ seekWindowLength = 2 * overlapLength;
+ }
+ seekLength = (sampleRate * seekWindowMs) / 1000;
+}
+
+
+
+// Sets new target tempo. Normal tempo = 'SCALE', smaller values represent slower
+// tempo, larger faster tempo.
+void TDStretch::setTempo(double newTempo)
+{
+ int intskip;
+
+ tempo = newTempo;
+
+ // Calculate new sequence duration
+ calcSeqParameters();
+
+ // Calculate ideal skip length (according to tempo value)
+ nominalSkip = tempo * (seekWindowLength - overlapLength);
+ intskip = (int)(nominalSkip + 0.5);
+
+ // Calculate how many samples are needed in the 'inputBuffer' to
+ // process another batch of samples
+ //sampleReq = max(intskip + overlapLength, seekWindowLength) + seekLength / 2;
+ sampleReq = max(intskip + overlapLength, seekWindowLength) + seekLength;
+}
+
+
+
+// Sets the number of channels, 1 = mono, 2 = stereo
+void TDStretch::setChannels(int numChannels)
+{
+ if (!verifyNumberOfChannels(numChannels) ||
+ (channels == numChannels)) return;
+
+ channels = numChannels;
+ inputBuffer.setChannels(channels);
+ outputBuffer.setChannels(channels);
+
+ // re-init overlap/buffer
+ overlapLength=0;
+ setParameters(sampleRate);
+}
+
+
+// nominal tempo, no need for processing, just pass the samples through
+// to outputBuffer
+/*
+void TDStretch::processNominalTempo()
+{
+ assert(tempo == 1.0f);
+
+ if (bMidBufferDirty)
+ {
+ // If there are samples in pMidBuffer waiting for overlapping,
+ // do a single sliding overlapping with them in order to prevent a
+ // clicking distortion in the output sound
+ if (inputBuffer.numSamples() < overlapLength)
+ {
+ // wait until we've got overlapLength input samples
+ return;
+ }
+ // Mix the samples in the beginning of 'inputBuffer' with the
+ // samples in 'midBuffer' using sliding overlapping
+ overlap(outputBuffer.ptrEnd(overlapLength), inputBuffer.ptrBegin(), 0);
+ outputBuffer.putSamples(overlapLength);
+ inputBuffer.receiveSamples(overlapLength);
+ clearMidBuffer();
+ // now we've caught the nominal sample flow and may switch to
+ // bypass mode
+ }
+
+ // Simply bypass samples from input to output
+ outputBuffer.moveSamples(inputBuffer);
+}
+*/
+
+
+// Processes as many processing frames of the samples 'inputBuffer', store
+// the result into 'outputBuffer'
+void TDStretch::processSamples()
+{
+ int ovlSkip;
+ int offset = 0;
+ int temp;
+
+ /* Removed this small optimization - can introduce a click to sound when tempo setting
+ crosses the nominal value
+ if (tempo == 1.0f)
+ {
+ // tempo not changed from the original, so bypass the processing
+ processNominalTempo();
+ return;
+ }
+ */
+
+ // Process samples as long as there are enough samples in 'inputBuffer'
+ // to form a processing frame.
+ while ((int)inputBuffer.numSamples() >= sampleReq)
+ {
+ if (isBeginning == false)
+ {
+ // apart from the very beginning of the track,
+ // scan for the best overlapping position & do overlap-add
+ offset = seekBestOverlapPosition(inputBuffer.ptrBegin());
+
+ // Mix the samples in the 'inputBuffer' at position of 'offset' with the
+ // samples in 'midBuffer' using sliding overlapping
+ // ... first partially overlap with the end of the previous sequence
+ // (that's in 'midBuffer')
+ overlap(outputBuffer.ptrEnd((uint)overlapLength), inputBuffer.ptrBegin(), (uint)offset);
+ outputBuffer.putSamples((uint)overlapLength);
+ offset += overlapLength;
+ }
+ else
+ {
+ // Adjust processing offset at beginning of track by not perform initial overlapping
+ // and compensating that in the 'input buffer skip' calculation
+ isBeginning = false;
+ int skip = (int)(tempo * overlapLength + 0.5 * seekLength + 0.5);
+
+ #ifdef ST_SIMD_AVOID_UNALIGNED
+ // in SIMD mode, round the skip amount to value corresponding to aligned memory address
+ if (channels == 1)
+ {
+ skip &= -4;
+ }
+ else if (channels == 2)
+ {
+ skip &= -2;
+ }
+ #endif
+ skipFract -= skip;
+ if (skipFract <= -nominalSkip)
+ {
+ skipFract = -nominalSkip;
+ }
+ }
+
+ // ... then copy sequence samples from 'inputBuffer' to output:
+
+ // crosscheck that we don't have buffer overflow...
+ if ((int)inputBuffer.numSamples() < (offset + seekWindowLength - overlapLength))
+ {
+ continue; // just in case, shouldn't really happen
+ }
+
+ // length of sequence
+ temp = (seekWindowLength - 2 * overlapLength);
+ outputBuffer.putSamples(inputBuffer.ptrBegin() + channels * offset, (uint)temp);
+
+ // Copies the end of the current sequence from 'inputBuffer' to
+ // 'midBuffer' for being mixed with the beginning of the next
+ // processing sequence and so on
+ assert((offset + temp + overlapLength) <= (int)inputBuffer.numSamples());
+ memcpy(pMidBuffer, inputBuffer.ptrBegin() + channels * (offset + temp),
+ channels * sizeof(SAMPLETYPE) * overlapLength);
+
+ // Remove the processed samples from the input buffer. Update
+ // the difference between integer & nominal skip step to 'skipFract'
+ // in order to prevent the error from accumulating over time.
+ skipFract += nominalSkip; // real skip size
+ ovlSkip = (int)skipFract; // rounded to integer skip
+ skipFract -= ovlSkip; // maintain the fraction part, i.e. real vs. integer skip
+ inputBuffer.receiveSamples((uint)ovlSkip);
+ }
+}
+
+
+// Adds 'numsamples' pcs of samples from the 'samples' memory position into
+// the input of the object.
+void TDStretch::putSamples(const SAMPLETYPE *samples, uint nSamples)
+{
+ // Add the samples into the input buffer
+ inputBuffer.putSamples(samples, nSamples);
+ // Process the samples in input buffer
+ processSamples();
+}
+
+
+
+/// Set new overlap length parameter & reallocate RefMidBuffer if necessary.
+void TDStretch::acceptNewOverlapLength(int newOverlapLength)
+{
+ int prevOvl;
+
+ assert(newOverlapLength >= 0);
+ prevOvl = overlapLength;
+ overlapLength = newOverlapLength;
+
+ if (overlapLength > prevOvl)
+ {
+ delete[] pMidBufferUnaligned;
+
+ pMidBufferUnaligned = new SAMPLETYPE[overlapLength * channels + 16 / sizeof(SAMPLETYPE)];
+ // ensure that 'pMidBuffer' is aligned to 16 byte boundary for efficiency
+ pMidBuffer = (SAMPLETYPE *)SOUNDTOUCH_ALIGN_POINTER_16(pMidBufferUnaligned);
+
+ clearMidBuffer();
+ }
+}
+
+
+// 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.
+void * TDStretch::operator new(size_t)
+{
+ // Notice! don't use "new TDStretch" directly, use "newInstance" to create a new instance instead!
+ ST_THROW_RT_ERROR("Error in TDStretch::new: Don't use 'new TDStretch' directly, use 'newInstance' member instead!");
+ return newInstance();
+}
+
+
+TDStretch * TDStretch::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 TDStretchMMX;
+ }
+ else
+#endif // SOUNDTOUCH_ALLOW_MMX
+
+
+#ifdef SOUNDTOUCH_ALLOW_SSE
+ if (uExtensions & SUPPORT_SSE)
+ {
+ // SSE support
+ return ::new TDStretchSSE;
+ }
+ else
+#endif // SOUNDTOUCH_ALLOW_SSE
+
+ {
+ // ISA optimizations not supported, use plain C version
+ return ::new TDStretch;
+ }
+}
+
+
+//////////////////////////////////////////////////////////////////////////////
+//
+// Integer arithmetic specific algorithm implementations.
+//
+//////////////////////////////////////////////////////////////////////////////
+
+#ifdef SOUNDTOUCH_INTEGER_SAMPLES
+
+// Overlaps samples in 'midBuffer' with the samples in 'input'. The 'Stereo'
+// version of the routine.
+void TDStretch::overlapStereo(short *poutput, const short *input) const
+{
+ int i;
+ short temp;
+ int cnt2;
+
+ for (i = 0; i < overlapLength ; i ++)
+ {
+ temp = (short)(overlapLength - i);
+ cnt2 = 2 * i;
+ poutput[cnt2] = (input[cnt2] * i + pMidBuffer[cnt2] * temp ) / overlapLength;
+ poutput[cnt2 + 1] = (input[cnt2 + 1] * i + pMidBuffer[cnt2 + 1] * temp ) / overlapLength;
+ }
+}
+
+
+// Overlaps samples in 'midBuffer' with the samples in 'input'. The 'Multi'
+// version of the routine.
+void TDStretch::overlapMulti(short *poutput, const short *input) const
+{
+ short m1;
+ int i = 0;
+
+ for (m1 = 0; m1 < overlapLength; m1 ++)
+ {
+ short m2 = (short)(overlapLength - m1);
+ for (int c = 0; c < channels; c ++)
+ {
+ poutput[i] = (input[i] * m1 + pMidBuffer[i] * m2) / overlapLength;
+ i++;
+ }
+ }
+}
+
+// Calculates the x having the closest 2^x value for the given value
+static int _getClosest2Power(double value)
+{
+ return (int)(log(value) / log(2.0) + 0.5);
+}
+
+
+/// Calculates overlap period length in samples.
+/// Integer version rounds overlap length to closest power of 2
+/// for a divide scaling operation.
+void TDStretch::calculateOverlapLength(int aoverlapMs)
+{
+ int newOvl;
+
+ assert(aoverlapMs >= 0);
+
+ // calculate overlap length so that it's power of 2 - thus it's easy to do
+ // integer division by right-shifting. Term "-1" at end is to account for
+ // the extra most significatnt bit left unused in result by signed multiplication
+ overlapDividerBitsPure = _getClosest2Power((sampleRate * aoverlapMs) / 1000.0) - 1;
+ if (overlapDividerBitsPure > 9) overlapDividerBitsPure = 9;
+ if (overlapDividerBitsPure < 3) overlapDividerBitsPure = 3;
+ newOvl = (int)pow(2.0, (int)overlapDividerBitsPure + 1); // +1 => account for -1 above
+
+ acceptNewOverlapLength(newOvl);
+
+ overlapDividerBitsNorm = overlapDividerBitsPure;
+
+ // calculate sloping divider so that crosscorrelation operation won't
+ // overflow 32-bit register. Max. sum of the crosscorrelation sum without
+ // divider would be 2^30*(N^3-N)/3, where N = overlap length
+ slopingDivider = (newOvl * newOvl - 1) / 3;
+}
+
+
+double TDStretch::calcCrossCorr(const short *mixingPos, const short *compare, double &norm)
+{
+ long corr;
+ unsigned long lnorm;
+ int i;
+
+ #ifdef ST_SIMD_AVOID_UNALIGNED
+ // in SIMD mode skip 'mixingPos' positions that aren't aligned to 16-byte boundary
+ if (((ulongptr)mixingPos) & 15) return -1e50;
+ #endif
+
+ // hint compiler autovectorization that loop length is divisible by 8
+ int ilength = (channels * overlapLength) & -8;
+
+ corr = lnorm = 0;
+ // Same routine for stereo and mono
+ for (i = 0; i < ilength; i += 2)
+ {
+ corr += (mixingPos[i] * compare[i] +
+ mixingPos[i + 1] * compare[i + 1]) >> overlapDividerBitsNorm;
+ lnorm += (mixingPos[i] * mixingPos[i] +
+ mixingPos[i + 1] * mixingPos[i + 1]) >> overlapDividerBitsNorm;
+ // do intermediate scalings to avoid integer overflow
+ }
+
+ if (lnorm > maxnorm)
+ {
+ // modify 'maxnorm' inside critical section to avoid multi-access conflict if in OpenMP mode
+ if (lnorm > maxnorm)
+ {
+ maxnorm = lnorm;
+ }
+ }
+ // Normalize result by dividing by sqrt(norm) - this step is easiest
+ // done using floating point operation
+ norm = (double)lnorm;
+ return (double)corr / sqrt((norm < 1e-9) ? 1.0 : norm);
+}
+
+
+/// Update cross-correlation by accumulating "norm" coefficient by previously calculated value
+double TDStretch::calcCrossCorrAccumulate(const short *mixingPos, const short *compare, double &norm)
+{
+ long corr;
+ long lnorm;
+ int i;
+
+ // hint compiler autovectorization that loop length is divisible by 8
+ int ilength = (channels * overlapLength) & -8;
+
+ // cancel first normalizer tap from previous round
+ lnorm = 0;
+ for (i = 1; i <= channels; i ++)
+ {
+ lnorm -= (mixingPos[-i] * mixingPos[-i]) >> overlapDividerBitsNorm;
+ }
+
+ corr = 0;
+ // Same routine for stereo and mono.
+ for (i = 0; i < ilength; i += 2)
+ {
+ corr += (mixingPos[i] * compare[i] +
+ mixingPos[i + 1] * compare[i + 1]) >> overlapDividerBitsNorm;
+ }
+
+ // update normalizer with last samples of this round
+ for (int j = 0; j < channels; j ++)
+ {
+ i --;
+ lnorm += (mixingPos[i] * mixingPos[i]) >> overlapDividerBitsNorm;
+ }
+
+ norm += (double)lnorm;
+ if (norm > maxnorm)
+ {
+ maxnorm = (unsigned long)norm;
+ }
+
+ // Normalize result by dividing by sqrt(norm) - this step is easiest
+ // done using floating point operation
+ return (double)corr / sqrt((norm < 1e-9) ? 1.0 : norm);
+}
+
+#endif // SOUNDTOUCH_INTEGER_SAMPLES
+
+//////////////////////////////////////////////////////////////////////////////
+//
+// Floating point arithmetic specific algorithm implementations.
+//
+
+#ifdef SOUNDTOUCH_FLOAT_SAMPLES
+
+// Overlaps samples in 'midBuffer' with the samples in 'pInput'
+void TDStretch::overlapStereo(float *pOutput, const float *pInput) const
+{
+ int i;
+ float fScale;
+ float f1;
+ float f2;
+
+ fScale = 1.0f / (float)overlapLength;
+
+ f1 = 0;
+ f2 = 1.0f;
+
+ for (i = 0; i < 2 * (int)overlapLength ; i += 2)
+ {
+ pOutput[i + 0] = pInput[i + 0] * f1 + pMidBuffer[i + 0] * f2;
+ pOutput[i + 1] = pInput[i + 1] * f1 + pMidBuffer[i + 1] * f2;
+
+ f1 += fScale;
+ f2 -= fScale;
+ }
+}
+
+
+// Overlaps samples in 'midBuffer' with the samples in 'input'.
+void TDStretch::overlapMulti(float *pOutput, const float *pInput) const
+{
+ int i;
+ float fScale;
+ float f1;
+ float f2;
+
+ fScale = 1.0f / (float)overlapLength;
+
+ f1 = 0;
+ f2 = 1.0f;
+
+ i=0;
+ for (int i2 = 0; i2 < overlapLength; i2 ++)
+ {
+ // note: Could optimize this slightly by taking into account that always channels > 2
+ for (int c = 0; c < channels; c ++)
+ {
+ pOutput[i] = pInput[i] * f1 + pMidBuffer[i] * f2;
+ i++;
+ }
+ f1 += fScale;
+ f2 -= fScale;
+ }
+}
+
+
+/// Calculates overlapInMsec period length in samples.
+void TDStretch::calculateOverlapLength(int overlapInMsec)
+{
+ int newOvl;
+
+ assert(overlapInMsec >= 0);
+ newOvl = (sampleRate * overlapInMsec) / 1000;
+ if (newOvl < 16) newOvl = 16;
+
+ // must be divisible by 8
+ newOvl -= newOvl % 8;
+
+ acceptNewOverlapLength(newOvl);
+}
+
+
+/// Calculate cross-correlation
+double TDStretch::calcCrossCorr(const float *mixingPos, const float *compare, double &anorm)
+{
+ float corr;
+ float norm;
+ int i;
+
+ #ifdef ST_SIMD_AVOID_UNALIGNED
+ // in SIMD mode skip 'mixingPos' positions that aren't aligned to 16-byte boundary
+ if (((ulongptr)mixingPos) & 15) return -1e50;
+ #endif
+
+ // hint compiler autovectorization that loop length is divisible by 8
+ int ilength = (channels * overlapLength) & -8;
+
+ corr = norm = 0;
+ // Same routine for stereo and mono
+ for (i = 0; i < ilength; i ++)
+ {
+ corr += mixingPos[i] * compare[i];
+ norm += mixingPos[i] * mixingPos[i];
+ }
+
+ anorm = norm;
+ return corr / sqrt((norm < 1e-9 ? 1.0 : norm));
+}
+
+
+/// Update cross-correlation by accumulating "norm" coefficient by previously calculated value
+double TDStretch::calcCrossCorrAccumulate(const float *mixingPos, const float *compare, double &norm)
+{
+ float corr;
+ int i;
+
+ corr = 0;
+
+ // cancel first normalizer tap from previous round
+ for (i = 1; i <= channels; i ++)
+ {
+ norm -= mixingPos[-i] * mixingPos[-i];
+ }
+
+ // hint compiler autovectorization that loop length is divisible by 8
+ int ilength = (channels * overlapLength) & -8;
+
+ // Same routine for stereo and mono
+ for (i = 0; i < ilength; i ++)
+ {
+ corr += mixingPos[i] * compare[i];
+ }
+
+ // update normalizer with last samples of this round
+ for (int j = 0; j < channels; j ++)
+ {
+ i --;
+ norm += mixingPos[i] * mixingPos[i];
+ }
+
+ return corr / sqrt((norm < 1e-9 ? 1.0 : norm));
+}
+
+
+#endif // SOUNDTOUCH_FLOAT_SAMPLES
diff --git a/lib/soundtouch-master/source/SoundTouch/TDStretch.h b/lib/soundtouch-master/source/SoundTouch/TDStretch.h
new file mode 100644
index 00000000..483dd536
--- /dev/null
+++ b/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
+#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
diff --git a/lib/soundtouch-master/source/SoundTouch/cpu_detect.h b/lib/soundtouch-master/source/SoundTouch/cpu_detect.h
new file mode 100644
index 00000000..0794c448
--- /dev/null
+++ b/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_
diff --git a/lib/soundtouch-master/source/SoundTouch/cpu_detect_x86.cpp b/lib/soundtouch-master/source/SoundTouch/cpu_detect_x86.cpp
new file mode 100644
index 00000000..18d88e9e
--- /dev/null
+++ b/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
+ #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
+}
diff --git a/lib/soundtouch-master/source/SoundTouch/mmx_optimized.cpp b/lib/soundtouch-master/source/SoundTouch/mmx_optimized.cpp
new file mode 100644
index 00000000..e26c0c72
--- /dev/null
+++ b/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
+#include
+#include
+
+
+// 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
diff --git a/lib/soundtouch-master/source/SoundTouch/sse_optimized.cpp b/lib/soundtouch-master/source/SoundTouch/sse_optimized.cpp
new file mode 100644
index 00000000..31cd90df
--- /dev/null
+++ b/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
+#include
+
+// 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
diff --git a/lib/soundtouch-master/source/SoundTouchDLL/SoundTouchDLL.cpp b/lib/soundtouch-master/source/SoundTouchDLL/SoundTouchDLL.cpp
new file mode 100644
index 00000000..22b6e0ad
--- /dev/null
+++ b/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
+
+ // 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
+#include
+#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(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(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(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(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(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(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(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(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(h)->setChannels(numChannels);
+}
+
+/// Sets sample rate.
+SOUNDTOUCHDLL_API void __cdecl soundtouch_setSampleRate(ST_HANDLE h, uint srate)
+{
+ static_cast(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(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(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(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(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(h)->getSetting(settingId);
+}
+
+
+/// Returns number of samples currently unprocessed.
+SOUNDTOUCHDLL_API uint __cdecl soundtouch_numUnprocessedSamples(ST_HANDLE h)
+{
+ return static_cast(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(h)->receiveSamples(outBuffer, maxSamples);
+ }
+ else
+ {
+ return static_cast(h)->receiveSamples(maxSamples);
+ }
+}
+
+
+/// Returns number of samples currently available.
+SOUNDTOUCHDLL_API uint __cdecl soundtouch_numSamples(ST_HANDLE h)
+{
+ return static_cast(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(h)->isEmpty();
+}
diff --git a/lib/soundtouch-master/source/SoundTouchDLL/SoundTouchDLL.rc b/lib/soundtouch-master/source/SoundTouchDLL/SoundTouchDLL.rc
new file mode 100644
index 00000000..c9f7db5f
--- /dev/null
+++ b/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
+
diff --git a/lib/soundtouch-master/source/SoundTouchDLL/resource.h b/lib/soundtouch-master/source/SoundTouchDLL/resource.h
new file mode 100644
index 00000000..0ef1170a
--- /dev/null
+++ b/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
diff --git a/lib/u_async.c b/lib/u_async.c
index e4ff1b0e..b86623e8 100644
--- a/lib/u_async.c
+++ b/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);
diff --git a/src/Makefile.am b/src/Makefile.am
index f1418856..30070706 100644
--- a/src/Makefile.am
+++ b/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)
diff --git a/src/chat/chat_admin.c b/src/chat/chat_admin.c
index f4a67118..5b862910 100644
--- a/src/chat/chat_admin.c
+++ b/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);
}
diff --git a/src/chat/chat_channel.c b/src/chat/chat_channel.c
index c87a1036..ed39cfec 100644
--- a/src/chat/chat_channel.c
+++ b/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);
}
diff --git a/src/chat/chat_core.c b/src/chat/chat_core.c
index 9809b190..aea6c124 100644
--- a/src/chat/chat_core.c
+++ b/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
#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:" */
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);
}
diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h
index d6066caa..50938cbe 100644
--- a/src/chat/chat_core.h
+++ b/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;
};
diff --git a/src/chat/chat_core_priv.h b/src/chat/chat_core_priv.h
index 96a7c1f7..6e7a6327 100644
--- a/src/chat/chat_core_priv.h
+++ b/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 */
diff --git a/src/chat/chat_event.c b/src/chat/chat_event.c
index 1dbe7825..28d5fe3f 100644
--- a/src/chat/chat_event.c
+++ b/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
#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",
diff --git a/src/chat/chat_event.h b/src/chat/chat_event.h
index 70c94c7f..dcbeda57 100644
--- a/src/chat/chat_event.h
+++ b/src/chat/chat_event.h
@@ -14,6 +14,8 @@
#include
#include
+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
}
diff --git a/src/chat/chat_headless_control.c b/src/chat/chat_headless_control.c
index 6a58a17d..5f5cb876 100644
--- a/src/chat/chat_headless_control.c
+++ b/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":"",...params}
+ * Response: {"id":N,"ok":true,"data":{...}} | {"id":N,"ok":false,"error":"..."}
+ * Event: {"event":"",...}
+ *
+ * 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
#include
#include
-#include
+#include
+#include
+#include
#include
-#include "../../lib/platform_compat.h"
-#include "../../lib/socket_compat.h"
+#include
+
+#include
+
+#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");
}
diff --git a/src/chat/chat_headless_control.h b/src/chat/chat_headless_control.h
index 254ede94..58338c5b 100644
--- a/src/chat/chat_headless_control.h
+++ b/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 */
diff --git a/src/chat/chat_join.c b/src/chat/chat_join.c
index ac5623b6..f2c3b2e5 100644
--- a/src/chat/chat_join.c
+++ b/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;
}
diff --git a/src/chat/chat_member.c b/src/chat/chat_member.c
index 0b9f2b4e..805f62de 100644
--- a/src/chat/chat_member.c
+++ b/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)");
}
diff --git a/src/chat/chat_member.h b/src/chat/chat_member.h
index 60d8c59d..5af94a9b 100644
--- a/src/chat/chat_member.h
+++ b/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
}
diff --git a/src/chat/chat_msg.c b/src/chat/chat_msg.c
index 93de0f72..8483f6bf 100644
--- a/src/chat/chat_msg.c
+++ b/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":""} */
@@ -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_.timestamp).
- * Сироты (файлы без сообщения, включая .chunk_* остатки) удаляются на старте.
- * Имя файла на диске берётся из local_attrs "fp" (fallback — по имени из "d"). */
+/* ─── Кеш медиафайлов: очистка сирот + лимит storage_total_size ─── */
struct media_msg_file {
char rel_path[1536]; // media//
@@ -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;
}
diff --git a/src/chat/chat_profile.c b/src/chat/chat_profile.c
index 87add3ab..53f3947a 100644
--- a/src/chat/chat_profile.c
+++ b/src/chat/chat_profile.c
@@ -23,36 +23,41 @@
#include
-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);
}
diff --git a/src/chat/chat_setting.c b/src/chat/chat_setting.c
index be9c1ed9..89b1cc42 100644
--- a/src/chat/chat_setting.c
+++ b/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
#include
#include
+#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;
}
diff --git a/src/chat/chat_setting.h b/src/chat/chat_setting.h
index fd8df873..2a29f0bd 100644
--- a/src/chat/chat_setting.h
+++ b/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
+
+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
}
diff --git a/src/chat/chat_status.c b/src/chat/chat_status.c
index d693da01..d8335bff 100644
--- a/src/chat/chat_status.c
+++ b/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);
}
diff --git a/src/chat/chat_sync.c b/src/chat/chat_sync.c
index 66059ab4..3ac8b785 100644
--- a/src/chat/chat_sync.c
+++ b/src/chat/chat_sync.c
@@ -35,7 +35,9 @@
#include
#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 */
diff --git a/src/chat/chat_sync.h b/src/chat/chat_sync.h
index 3354132c..b8f31f46 100644
--- a/src/chat/chat_sync.h
+++ b/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,
diff --git a/src/chat/chat_whisper.c b/src/chat/chat_whisper.c
index d5ca26db..3a8268a4 100644
--- a/src/chat/chat_whisper.c
+++ b/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;
diff --git a/src/chat/chat_whisper.h b/src/chat/chat_whisper.h
index b7025315..ee107760 100644
--- a/src/chat/chat_whisper.h
+++ b/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,
diff --git a/src/chat/invite_build.c b/src/chat/invite_build.c
index ed7f67e0..6b58b1b0 100644
--- a/src/chat/invite_build.c
+++ b/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);
}
diff --git a/src/chat/invite_build.h b/src/chat/invite_build.h
index b69ba350..dff1aa5c 100644
--- a/src/chat/invite_build.h
+++ b/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
diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c
index 163f95de..c5c83caf 100644
--- a/src/chat/member_sync.c
+++ b/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) {
/* у нас версия новее — отправить наш полный рекорд автору */
diff --git a/src/chat/member_sync.h b/src/chat/member_sync.h
index f11d73b4..185ee408 100644
--- a/src/chat/member_sync.h
+++ b/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
}
diff --git a/src/config_parser.c b/src/config_parser.c
index ec4bbf78..4a04d57c 100644
--- a/src/config_parser.c
+++ b/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;
diff --git a/src/config_parser.h b/src/config_parser.h
index 22b11765..df592169 100644
--- a/src/config_parser.h
+++ b/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;
diff --git a/src/dm/dm_core.c b/src/dm/dm_core.c
new file mode 100644
index 00000000..aa1e2c99
--- /dev/null
+++ b/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
+#include
+#include
+
+#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;
+}
diff --git a/src/dm/dm_core.h b/src/dm/dm_core.h
new file mode 100644
index 00000000..fbb58fe7
--- /dev/null
+++ b/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
+#include
+#include
+
+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 */
diff --git a/src/dm/dm_crypto.c b/src/dm/dm_crypto.c
new file mode 100644
index 00000000..12deafe6
--- /dev/null
+++ b/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
+#include
+
+#include
+#include
+
+#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);
+}
diff --git a/src/dm/dm_crypto.h b/src/dm/dm_crypto.h
new file mode 100644
index 00000000..6ca1ae3c
--- /dev/null
+++ b/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
+#include
+
+#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 */
diff --git a/src/dm/dm_mailbox.c b/src/dm/dm_mailbox.c
new file mode 100644
index 00000000..5bc55c71
--- /dev/null
+++ b/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
+#include
+#include
+
+#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);
+}
diff --git a/src/dm/dm_mailbox.h b/src/dm/dm_mailbox.h
new file mode 100644
index 00000000..7fb26112
--- /dev/null
+++ b/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
+#include
+
+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 */
diff --git a/src/media_delivery/media_delivery.c b/src/media_delivery/media_delivery.c
index 1e4e8317..c3de69ea 100644
--- a/src/media_delivery/media_delivery.c
+++ b/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;
diff --git a/src/routing_layer/etcp_router.c b/src/routing_layer/etcp_router.c
index c49f9cd4..a5e2e238 100644
--- a/src/routing_layer/etcp_router.c
+++ b/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);
}
// Собрать и закодировать (подпись/шифрование) пакет в начале отправки.
diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c
index 816ab89e..03b59348 100644
--- a/src/routing_layer/topo_group.c
+++ b/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;
}
diff --git a/src/routing_layer/topo_group_connect.c b/src/routing_layer/topo_group_connect.c
index aee6ec84..8e93382a 100644
--- a/src/routing_layer/topo_group_connect.c
+++ b/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);
diff --git a/src/routing_layer/topo_group_invite.c b/src/routing_layer/topo_group_invite.c
index 3ba368cc..51420cb9 100644
--- a/src/routing_layer/topo_group_invite.c
+++ b/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 приглашающего ── */
{
diff --git a/src/transport_layer/auto_socket.c b/src/transport_layer/auto_socket.c
index 9c244a5c..ff5cc5ad 100644
--- a/src/transport_layer/auto_socket.c
+++ b/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",
diff --git a/src/transport_layer/etcp_api.h b/src/transport_layer/etcp_api.h
index a1cd7cb4..75462ccd 100644
--- a/src/transport_layer/etcp_api.h
+++ b/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 // соединение создано
diff --git a/src/utun_instance.c b/src/utun_instance.c
index eceb7ff8..fc05a67c 100644
--- a/src/utun_instance.c
+++ b/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);
}
}
diff --git a/src/utun_instance.h b/src/utun_instance.h
index 911e6324..3d4a130d 100644
--- a/src/utun_instance.h
+++ b/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];
diff --git a/task.txt b/task.txt
deleted file mode 100644
index aabca001..00000000
--- a/task.txt
+++ /dev/null
@@ -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` (доступа нет)
diff --git a/tests/Makefile.am b/tests/Makefile.am
index ecfbce11..1b652f7b 100644
--- a/tests/Makefile.am
+++ b/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)
diff --git a/tests/test_chat_join_e2e.c b/tests/test_chat_join_e2e.c
index 6dcdbb7b..c526a445 100644
--- a/tests/test_chat_join_e2e.c
+++ b/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) {
diff --git a/tests/test_dm.c b/tests/test_dm.c
new file mode 100644
index 00000000..05c5ba59
--- /dev/null
+++ b/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
+#include
+
+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;
+}
diff --git a/tests/test_dm_e2e.c b/tests/test_dm_e2e.c
new file mode 100644
index 00000000..e4b47d9f
--- /dev/null
+++ b/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
+#define OPENSSL_API_COMPAT 0x10100000L
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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;
+}
diff --git a/tools/chatcli b/tools/chatcli
index 783f8917..510d7814 100755
--- a/tools/chatcli
+++ b/tools/chatcli
@@ -21,6 +21,10 @@
# create create new channel
# invite_to [proto]
# invite a node to add our channel
+# dm_start [ch_id] start direct chat (DM) with a group member
+# dm_list list DM conversations
+# dm_messages [count] read DM messages (decrypted)
+# dm_send 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 "),))
elif cmd == "create": cmd_create(*args[1:2] if len(args) > 1 else (_die("usage: create "),))
elif cmd == "invite_to": cmd_invite_to(*args[1:6] if len(args) > 4 else (_die("usage: invite_to [proto]"),))
+ elif cmd == "dm_start": cmd_dm_start(*args[1:3] if len(args) > 1 else (_die("usage: dm_start [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 [count]"),))
+ elif cmd == "dm_send": cmd_dm_send(*args[1:3] if len(args) > 2 else (_die("usage: dm_send "),))
elif cmd == "listen": cmd_listen()
else: print(f"Unknown command: {cmd}\nUse --help for usage", file=sys.stderr); sys.exit(1)
except socket.timeout:
diff --git a/tools/chatgui-android/headless/headless_control.c b/tools/chatgui-android/headless/headless_control.c
index e0e4df73..49b16c46 100644
--- a/tools/chatgui-android/headless/headless_control.c
+++ b/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);
diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c
index 8a04499d..44080a0b 100644
--- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c
+++ b/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
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;
}
diff --git a/tools/chatgui-android/libutun_lite/attachment_sender.c b/tools/chatgui-android/libutun_lite/attachment_sender.c
index 80ff0c8b..bc1f5e81 100644
--- a/tools/chatgui-android/libutun_lite/attachment_sender.c
+++ b/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);
diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c
index 1eb9397c..8e565e02 100644
--- a/tools/chatgui-android/libutun_lite/instance_lite.c
+++ b/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);
+ }
}
diff --git a/tools/chatgui-android/libutun_lite/instance_lite.h b/tools/chatgui-android/libutun_lite/instance_lite.h
index c3c16b1a..826e8812 100644
--- a/tools/chatgui-android/libutun_lite/instance_lite.h
+++ b/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);
diff --git a/tools/chatgui-android/libutun_lite/photo_sender.c b/tools/chatgui-android/libutun_lite/photo_sender.c
index fd9497ce..68ee35f2 100644
--- a/tools/chatgui-android/libutun_lite/photo_sender.c
+++ b/tools/chatgui-android/libutun_lite/photo_sender.c
@@ -74,6 +74,7 @@ int photo_send(const char* channel_id, const char* src_file_path, const char* db
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), "%s", content_type);
snprintf(req->media_src, sizeof(req->media_src), "%s", src_file_path);
diff --git a/tools/chatgui-android/libutun_lite/standby.c b/tools/chatgui-android/libutun_lite/standby.c
index 6d26882b..b8fc7539 100644
--- a/tools/chatgui-android/libutun_lite/standby.c
+++ b/tools/chatgui-android/libutun_lite/standby.c
@@ -38,6 +38,7 @@ struct standby_wait_entry {
};
static struct UASYNC* g_ua = NULL;
+static struct UTUN_INSTANCE* g_inst = NULL; /* для чтения chat_setting (per-instance) */
static int g_enabled = 0;
static int g_phase = PHASE_ACTIVE;
static uint64_t g_phase_start_tb = 0; /* начало текущей фазы */
@@ -59,15 +60,15 @@ static uint32_t g_cnt_net_events = 0;
static uint32_t g_cnt_net_ignored = 0;
static int standby_active_sec(void) {
- return chat_setting_get_int("standby_active_sec", 2);
+ return chat_setting_get_int(g_inst, "standby_active_sec", 2);
}
static int standby_sleep_sec(void) {
- return chat_setting_get_int("standby_sleep_sec", 60);
+ return chat_setting_get_int(g_inst, "standby_sleep_sec", 60);
}
static int standby_min_sleep_sec(void) {
- int m = chat_setting_get_int("standby_min_sleep_sec", 15);
+ int m = chat_setting_get_int(g_inst, "standby_min_sleep_sec", 15);
if (m < 0) m = 0;
int s = standby_sleep_sec();
if (m > s) m = s;
@@ -195,6 +196,7 @@ static void standby_wait_timer_cb(void* arg) {
void standby_init(struct UASYNC* ua) {
g_ua = ua;
+ g_inst = NULL;
g_enabled = 0;
g_phase = PHASE_ACTIVE;
g_phase_timer = NULL;
@@ -225,6 +227,7 @@ void standby_deinit(void) {
}
g_enabled = 0;
g_ua = NULL;
+ g_inst = NULL;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "standby: deinit (wakes_timer=%u wakes_early=%u sleeps=%u net=%u ignored=%u)",
g_cnt_wakes_timer, g_cnt_wakes_early, g_cnt_sleeps, g_cnt_net_events, g_cnt_net_ignored);
}
@@ -234,6 +237,7 @@ void standby_set_enabled(struct UTUN_INSTANCE* inst, int enabled) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "standby: set_enabled(%d) but not initialized", enabled);
return;
}
+ if (inst) g_inst = inst;
if (g_enabled == enabled) return;
if (enabled) {
diff --git a/tools/chatgui-android/libutun_lite/video_sender.c b/tools/chatgui-android/libutun_lite/video_sender.c
index 9204e3e2..7bc24e90 100644
--- a/tools/chatgui-android/libutun_lite/video_sender.c
+++ b/tools/chatgui-android/libutun_lite/video_sender.c
@@ -63,6 +63,7 @@ int video_send(const char* channel_id, const char* src_file_path, const char* db
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), "video/mp4");
snprintf(req->media_src, sizeof(req->media_src), "%s", src_file_path);
diff --git a/tools/chatgui-android/libutun_lite/voice_recorder.c b/tools/chatgui-android/libutun_lite/voice_recorder.c
index 0e58aab5..8cf1efdf 100644
--- a/tools/chatgui-android/libutun_lite/voice_recorder.c
+++ b/tools/chatgui-android/libutun_lite/voice_recorder.c
@@ -383,6 +383,7 @@ int voice_recorder_stop(int* out_duration_ms) {
struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + total_data_len + 1);
if (!req) { voice_cleanup_locked(g_rec); pthread_mutex_unlock(&g_rec->mtx); pthread_mutex_unlock(&g_init_mtx); return -1; }
+ req->inst = instance_lite_get_instance();
snprintf(req->channel_id, sizeof(req->channel_id), "%s", g_rec->channel_id);
snprintf(req->content_type, sizeof(req->content_type), "audio/opus");
snprintf(req->media_src, sizeof(req->media_src), "%s", temp_path);
diff --git a/tools/chatgui/src/accountlist.cpp b/tools/chatgui/src/accountlist.cpp
index e976466d..012ead4d 100644
--- a/tools/chatgui/src/accountlist.cpp
+++ b/tools/chatgui/src/accountlist.cpp
@@ -273,10 +273,10 @@ void AccountList::showMemberDetail(quint64 nodeId) {
void AccountList::requestMemberDetail() {
if (m_detailNodeId == 0) return;
- void* arg = u_malloc(sizeof(uint64_t));
+ struct chat_node_arg* arg = (struct chat_node_arg*)u_malloc(sizeof(struct chat_node_arg));
if (!arg) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "AccountList: req alloc failed"); return; }
- uint64_t nid = m_detailNodeId;
- memcpy(arg, &nid, sizeof(nid));
+ arg->inst = gui_bridge_get_inst();
+ arg->node_id = m_detailNodeId;
gui_bridge_post_uasync_fn(chat_core_collect_member_detail_trampoline, arg);
}
@@ -482,6 +482,7 @@ void AccountList::onMemberContextMenu(const QPoint& pos) {
struct chat_connect_node_req* r = (struct chat_connect_node_req*)u_malloc(sizeof(*r));
if (!r) return;
memset(r, 0, sizeof(*r));
+ r->inst = gui_bridge_get_inst();
strncpy(r->ch_id, m_channelId.toUtf8().constData(), sizeof(r->ch_id) - 1);
r->node_id = nodeId;
gui_bridge_post_uasync_fn(chat_core_connect_node_trampoline, r);
diff --git a/tools/chatgui/src/audiodevicesettingspage.cpp b/tools/chatgui/src/audiodevicesettingspage.cpp
index edfa53d2..5f890f08 100644
--- a/tools/chatgui/src/audiodevicesettingspage.cpp
+++ b/tools/chatgui/src/audiodevicesettingspage.cpp
@@ -193,9 +193,11 @@ void AudioDeviceSettingsPage::applyAndSave() {
size_t klen = strlen(key), vlen = (size_t)v.size();
size_t total = klen + 1 + vlen + 1;
if (total > 256) return;
- void* arg = u_malloc(total);
- memcpy(arg, key, klen + 1);
- memcpy((char*)arg + klen + 1, v.constData(), vlen + 1);
+ struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
+ if (!arg) return;
+ arg->inst = gui_bridge_get_inst();
+ memcpy(arg->data, key, klen + 1);
+ memcpy(arg->data + klen + 1, v.constData(), vlen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
};
saveKey("audio_output_device", outIdx);
@@ -219,9 +221,11 @@ void AudioDeviceSettingsPage::applyAndSave() {
QByteArray v = QByteArray::number(val);
size_t nlen = strlen(name), vlen = (size_t)v.size(), total = nlen + 1 + vlen + 1;
if (total > 256) return;
- void* arg = u_malloc(total);
- memcpy(arg, name, nlen + 1);
- memcpy((char*)arg + nlen + 1, v.constData(), vlen + 1);
+ struct chat_setting_arg* arg = (struct chat_setting_arg*)u_malloc(sizeof(struct chat_setting_arg));
+ if (!arg) return;
+ arg->inst = gui_bridge_get_inst();
+ memcpy(arg->data, name, nlen + 1);
+ memcpy(arg->data + nlen + 1, v.constData(), vlen + 1);
gui_bridge_post_uasync_fn(chat_core_set_setting_trampoline, arg);
};
postChat(m_configPath, "opus_codec_preset", codecPreset);
diff --git a/tools/chatgui/src/channelsettingsdialog.cpp b/tools/chatgui/src/channelsettingsdialog.cpp
index 09adad79..51e5e0b5 100644
--- a/tools/chatgui/src/channelsettingsdialog.cpp
+++ b/tools/chatgui/src/channelsettingsdialog.cpp
@@ -93,9 +93,11 @@ void ChannelSettingsDialog::onSave() {
size_t klen = (size_t)keyBA.size(), vlen = (size_t)valBA.size();
size_t total = klen + 1 + vlen + 1;
if (total <= 256) {
- void* arg = u_malloc(total);
- memcpy(arg, keyBA.constData(), klen + 1);
- memcpy((char*)arg + klen + 1, valBA.constData(), vlen + 1);
+ struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
+ if (!arg) return;
+ arg->inst = gui_bridge_get_inst();
+ memcpy(arg->data, keyBA.constData(), klen + 1);
+ memcpy(arg->data + klen + 1, valBA.constData(), vlen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
}
};
diff --git a/tools/chatgui/src/connmonitorwindow.cpp b/tools/chatgui/src/connmonitorwindow.cpp
index 801a6595..9bcbfd68 100644
--- a/tools/chatgui/src/connmonitorwindow.cpp
+++ b/tools/chatgui/src/connmonitorwindow.cpp
@@ -103,7 +103,7 @@ void ConnMonitorWindow::hideEvent(QHideEvent* event) {
void ConnMonitorWindow::onTimerTick() {
if (m_details->textCursor().hasSelection()) return;
if (QApplication::mouseButtons()) return;
- gui_bridge_post_uasync_fn(chat_core_collect_conn_list_trampoline, nullptr);
+ gui_bridge_post_uasync_fn(chat_core_collect_conn_list_trampoline, gui_bridge_get_inst());
if (m_selectedNodeId) requestMetricsForSelected();
}
@@ -115,9 +115,10 @@ void ConnMonitorWindow::onConnSelected(QListWidgetItem* current, QListWidgetItem
}
void ConnMonitorWindow::requestMetricsForSelected() {
- void* arg = u_malloc(sizeof(uint64_t));
+ struct chat_node_arg* arg = (struct chat_node_arg*)u_malloc(sizeof(struct chat_node_arg));
if (!arg) return;
- memcpy(arg, &m_selectedNodeId, sizeof(uint64_t));
+ arg->inst = gui_bridge_get_inst();
+ arg->node_id = m_selectedNodeId;
gui_bridge_post_uasync_fn(chat_core_collect_conn_metrics_trampoline, arg);
}
diff --git a/tools/chatgui/src/inviteby.cpp b/tools/chatgui/src/inviteby.cpp
index a8d24b56..56241941 100644
--- a/tools/chatgui/src/inviteby.cpp
+++ b/tools/chatgui/src/inviteby.cpp
@@ -182,7 +182,7 @@ void InviteByLinkDialog::onSendInvite() {
QByteArray addrsBuf = serializeInviteAddrs(d);
chat_sync_invite_to_channel_with_addrs(
- chat_core_get_inst(), m_channelId.toUtf8().constData(), targetNodeId,
+ gui_bridge_get_inst(), m_channelId.toUtf8().constData(), targetNodeId,
(const uint8_t*)d.pubkey.constData(),
(const uint8_t*)addrsBuf.constData(), d.addrs.size(), addrsBuf.size());
diff --git a/tools/chatgui/src/invitedialog.cpp b/tools/chatgui/src/invitedialog.cpp
index 34ac6af7..96be4d19 100644
--- a/tools/chatgui/src/invitedialog.cpp
+++ b/tools/chatgui/src/invitedialog.cpp
@@ -89,8 +89,11 @@ ShareInviteDialog::ShareInviteDialog(const QString& channelId, DbManager* db, QW
});
/* запросить кандидатов; линк строится после их прихода */
- QByteArray chBytes = m_channelId.toUtf8();
- gui_bridge_post_uasync_fn(chat_invite_candidates_trampoline, u_strdup(chBytes.constData()));
+ struct chat_invite_cand_req* creq = (struct chat_invite_cand_req*)u_calloc(1, sizeof(*creq));
+ if (!creq) return;
+ creq->inst = gui_bridge_get_inst();
+ strncpy(creq->ch_id, m_channelId.toUtf8().constData(), sizeof(creq->ch_id) - 1);
+ gui_bridge_post_uasync_fn(chat_invite_candidates_trampoline, creq);
}
ShareInviteDialog::~ShareInviteDialog() {
@@ -111,6 +114,7 @@ void ShareInviteDialog::requestLink(uint64_t targetNodeId) {
return;
}
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
QByteArray chBytes = m_channelId.toUtf8();
strncpy(req->ch_id, chBytes.constData(), sizeof(req->ch_id) - 1);
req->target_node_id = targetNodeId;
diff --git a/tools/chatgui/src/joindialog.cpp b/tools/chatgui/src/joindialog.cpp
index 2817e2a7..724fcc49 100644
--- a/tools/chatgui/src/joindialog.cpp
+++ b/tools/chatgui/src/joindialog.cpp
@@ -193,7 +193,7 @@ void JoinDialog::onConnectClicked() {
int addrCount = d.addrs.size();
const char* pass = d.password.isEmpty() ? NULL : d.password.toUtf8().constData();
- chat_sync_connect_from_invite(d.channelId, nodeId,
+ chat_sync_connect_from_invite(gui_bridge_get_inst(), d.channelId, nodeId,
(const uint8_t*)d.pubkey.constData(),
(const uint8_t*)addrsBuf.constData(), addrCount, addrsBuf.size(), pass, d.joinKey);
}
diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp
index 7092aff2..9d106873 100644
--- a/tools/chatgui/src/mainwindow.cpp
+++ b/tools/chatgui/src/mainwindow.cpp
@@ -62,9 +62,11 @@ static void postUiStateKv(const char* key, const QString& value) {
QByteArray val = value.toUtf8();
size_t klen = strlen(key), vlen = (size_t)val.size(), total = klen + 1 + vlen + 1;
if (total > 256) return;
- void* arg = u_malloc(total);
- memcpy(arg, key, klen + 1);
- memcpy((char*)arg + klen + 1, val.constData(), vlen + 1);
+ struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
+ if (!arg) return;
+ arg->inst = gui_bridge_get_inst();
+ memcpy(arg->data, key, klen + 1);
+ memcpy(arg->data + klen + 1, val.constData(), vlen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
}
@@ -369,7 +371,7 @@ void MainWindow::setupBridgeCallbacks() {
gui_bridge_set_channel_peers_online_cb(onChannelPeersOnlineCallback);
gui_bridge_set_db_ready_cb([]() {
if (!s_mainWindow || !s_mainWindow->m_db) return;
- s_mainWindow->m_db->setDb(chat_core_get_db());
+ s_mainWindow->m_db->setDb(chat_core_get_db(gui_bridge_get_inst()));
s_mainWindow->m_channelList->loadChannels();
s_mainWindow->restoreWindowGeometry();
QString lastCid = s_mainWindow->m_db->getUiState("last_channel_id");
@@ -424,9 +426,12 @@ void MainWindow::setupConnects() {
m_currentChannelId = channelId;
m_messageList->loadChannel(channelId);
m_accountList->setChannel(channelId);
- QByteArray cidBA = channelId.toUtf8();
- char* cid = u_strdup(cidBA.constData());
- if (cid) gui_bridge_post_uasync_fn(chat_core_connect_channel_trampoline, cid);
+ struct chat_create_auto_arg* arg = (struct chat_create_auto_arg*)u_calloc(1, sizeof(*arg));
+ if (arg) {
+ arg->inst = gui_bridge_get_inst();
+ snprintf(arg->name, sizeof(arg->name), "%s", channelId.toUtf8().constData());
+ gui_bridge_post_uasync_fn(chat_core_connect_channel_trampoline, arg);
+ }
});
connect(m_channelList, &ChannelList::inviteRequested, this, &MainWindow::showShareInviteDialog);
connect(m_channelList, &ChannelList::inviteByLinkRequested, this, &MainWindow::showInviteByLinkDialog);
@@ -671,7 +676,15 @@ void MainWindow::onCreateGroupRequested() {
if (name.isEmpty()) return;
char* name_copy = u_strdup(name.toUtf8().constData());
- if (name_copy) gui_bridge_post_uasync_fn(chat_core_create_channel_auto_trampoline, name_copy);
+ if (name_copy) {
+ struct chat_create_auto_arg* arg = (struct chat_create_auto_arg*)u_malloc(sizeof(*arg));
+ if (arg) {
+ arg->inst = gui_bridge_get_inst();
+ snprintf(arg->name, sizeof(arg->name), "%s", name_copy);
+ gui_bridge_post_uasync_fn(chat_core_create_channel_auto_trampoline, arg);
+ }
+ u_free(name_copy);
+ }
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MainWindow: create group request posted name=%s", qPrintable(name));
}
@@ -734,6 +747,7 @@ void MainWindow::autoPlayCheckNewMessage(const QString& channelId) {
u_malloc(sizeof(struct attachment_dl_req));
if (!req) return;
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
strncpy(req->channel_id, channelId.toUtf8().constData(), sizeof(req->channel_id) - 1);
req->msg_id = msg.id;
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req);
@@ -787,6 +801,7 @@ void MainWindow::autoPlayTimerTick() {
u_malloc(sizeof(struct attachment_dl_req));
if (!req) return;
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
strncpy(req->channel_id, item.channelId.toUtf8().constData(), sizeof(req->channel_id) - 1);
req->msg_id = item.msgId;
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req);
diff --git a/tools/chatgui/src/memberlistmodel.cpp b/tools/chatgui/src/memberlistmodel.cpp
index baf79a3e..bbbd3275 100644
--- a/tools/chatgui/src/memberlistmodel.cpp
+++ b/tools/chatgui/src/memberlistmodel.cpp
@@ -66,8 +66,11 @@ void MemberListModel::setChannel(const QString& channelId, bool isOwner) {
endResetModel();
if (!channelId.isEmpty()) {
- char* ch = u_strdup(channelId.toUtf8().constData());
- gui_bridge_post_uasync_fn(chat_core_request_member_list_trampoline, ch);
+ struct chat_member_list_req* req = (struct chat_member_list_req*)u_calloc(1, sizeof(*req));
+ if (!req) return;
+ req->inst = gui_bridge_get_inst();
+ strncpy(req->ch_id, channelId.toUtf8().constData(), sizeof(req->ch_id) - 1);
+ gui_bridge_post_uasync_fn(chat_core_request_member_list_trampoline, req);
}
}
@@ -77,8 +80,11 @@ void MemberListModel::refresh() {
void MemberListModel::refreshRtt() {
if (m_channelId.isEmpty()) return;
- char* ch = u_strdup(m_channelId.toUtf8().constData());
- gui_bridge_post_uasync_fn(chat_core_request_member_rtt_trampoline, ch);
+ struct chat_member_list_req* req = (struct chat_member_list_req*)u_calloc(1, sizeof(*req));
+ if (!req) return;
+ req->inst = gui_bridge_get_inst();
+ strncpy(req->ch_id, m_channelId.toUtf8().constData(), sizeof(req->ch_id) - 1);
+ gui_bridge_post_uasync_fn(chat_core_request_member_rtt_trampoline, req);
}
quint64 MemberListModel::nodeIdAt(int row) const {
diff --git a/tools/chatgui/src/memberpropsdialog.cpp b/tools/chatgui/src/memberpropsdialog.cpp
index 79665c65..c5a43a52 100644
--- a/tools/chatgui/src/memberpropsdialog.cpp
+++ b/tools/chatgui/src/memberpropsdialog.cpp
@@ -198,6 +198,7 @@ void MemberPropsDialog::onSave() {
u_calloc(1, sizeof(struct chat_member_tags_req));
if (!req) { accept(); return; }
+ req->inst = gui_bridge_get_inst();
QByteArray ch_id_utf8 = m_channelId.toUtf8();
strncpy(req->ch_id, ch_id_utf8.constData(), sizeof(req->ch_id) - 1);
req->node_id = m_nodeId;
@@ -226,6 +227,7 @@ void MemberPropsDialog::onTransferAdmin() {
u_calloc(1, sizeof(struct chat_transfer_admin_req));
if (!req) return;
+ req->inst = gui_bridge_get_inst();
QByteArray ch_id_utf8 = m_channelId.toUtf8();
strncpy(req->ch_id, ch_id_utf8.constData(), sizeof(req->ch_id) - 1);
req->target_node_id = m_nodeId;
diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp
index cd89bba2..d0157491 100644
--- a/tools/chatgui/src/messagedelegate.cpp
+++ b/tools/chatgui/src/messagedelegate.cpp
@@ -101,6 +101,7 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
u_malloc(sizeof(struct attachment_dl_req));
if (!req) return false;
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
strncpy(req->channel_id, chId.toUtf8().constData(), sizeof(req->channel_id) - 1);
req->msg_id = msgId;
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req);
@@ -127,6 +128,7 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
u_malloc(sizeof(struct attachment_dl_req));
if (!req) return false;
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
strncpy(req->channel_id, chId.toUtf8().constData(), sizeof(req->channel_id) - 1);
req->msg_id = msgId;
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req);
@@ -151,6 +153,7 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
u_malloc(sizeof(struct attachment_dl_req));
if (!req) return false;
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
strncpy(req->channel_id, chId.toUtf8().constData(), sizeof(req->channel_id) - 1);
req->msg_id = msgId;
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req);
@@ -178,6 +181,7 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
u_malloc(sizeof(struct attachment_dl_req));
if (!req) return false;
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
strncpy(req->channel_id, chId.toUtf8().constData(), sizeof(req->channel_id) - 1);
req->msg_id = msgId;
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req);
diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp
index 5d9ca4e9..f96c1536 100644
--- a/tools/chatgui/src/messagelist.cpp
+++ b/tools/chatgui/src/messagelist.cpp
@@ -39,7 +39,6 @@ extern "C" {
#include "../../lib/u_async.h"
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
-struct save_ui_state_arg { char data[256]; };
void chat_core_save_ui_state_trampoline(void* arg);
}
@@ -364,6 +363,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
struct chat_msg_submit* req = (struct chat_msg_submit*)
u_malloc(sizeof(struct chat_msg_submit) + textData.size());
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
m_currentChannelId.toUtf8().constData());
snprintf(req->content_type, sizeof(req->content_type), "text/plain");
@@ -384,6 +384,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
struct chat_msg_submit* req = (struct chat_msg_submit*)
u_malloc(sizeof(struct chat_msg_submit) + textData.size());
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
m_currentChannelId.toUtf8().constData());
snprintf(req->content_type, sizeof(req->content_type), "audio/opus");
@@ -407,6 +408,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
struct chat_msg_submit* req = (struct chat_msg_submit*)
u_malloc(sizeof(struct chat_msg_submit) + textData.size());
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
m_currentChannelId.toUtf8().constData());
snprintf(req->content_type, sizeof(req->content_type), "application/octet-stream");
@@ -430,6 +432,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
struct chat_msg_submit* req = (struct chat_msg_submit*)
u_malloc(sizeof(struct chat_msg_submit) + textData.size());
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
m_currentChannelId.toUtf8().constData());
snprintf(req->content_type, sizeof(req->content_type), "video/mp4");
@@ -470,6 +473,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
struct chat_msg_submit* req = (struct chat_msg_submit*)
u_malloc(sizeof(struct chat_msg_submit) + textData.size());
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
m_currentChannelId.toUtf8().constData());
snprintf(req->content_type, sizeof(req->content_type), "%s", ct.toUtf8().constData());
@@ -669,6 +673,7 @@ void MessageList::saveChannelState() {
if (total > 256) return;
struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
if (!arg) return;
+ arg->inst = gui_bridge_get_inst();
memcpy(arg->data, key.constData(), keyLen + 1);
memcpy(arg->data + keyLen + 1, valStr.constData(), valLen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
@@ -762,6 +767,7 @@ void MessageList::updateReadPosition() {
if (total > 256) return;
struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
if (!arg) return;
+ arg->inst = gui_bridge_get_inst();
memcpy(arg->data, key.constData(), keyLen + 1);
memcpy(arg->data + keyLen + 1, valStr.constData(), valLen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
@@ -897,6 +903,7 @@ void MessageList::onMessageContextMenu(const QPoint& pos) {
u_malloc(sizeof(struct attachment_dl_req));
if (!req) return;
memset(req, 0, sizeof(*req));
+ req->inst = gui_bridge_get_inst();
strncpy(req->channel_id, chId.toUtf8().constData(), sizeof(req->channel_id) - 1);
req->msg_id = msgId;
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req);
diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp
index 49dac9d4..95d577f1 100644
--- a/tools/chatgui/src/settingsdialog.cpp
+++ b/tools/chatgui/src/settingsdialog.cpp
@@ -223,8 +223,12 @@ void SettingsDialog::onSave() {
/* update name in uTun if changed */
if (nameChanged) {
- char* name_copy = u_strdup(newName.toUtf8().constData());
- if (name_copy) gui_bridge_post_uasync_fn(chat_core_update_my_name_trampoline, name_copy);
+ struct update_my_name_arg* arg = (struct update_my_name_arg*)u_calloc(1, sizeof(*arg));
+ if (arg) {
+ arg->inst = gui_bridge_get_inst();
+ snprintf(arg->name, sizeof(arg->name), "%s", newName.toUtf8().constData());
+ gui_bridge_post_uasync_fn(chat_core_update_my_name_trampoline, arg);
+ }
m_oldNickname = newName;
}
@@ -273,9 +277,11 @@ void SettingsDialog::onSave() {
size_t klen = strlen(key), vlen = (size_t)val.size();
size_t total = klen + 1 + vlen + 1;
if (total <= 256) {
- void* arg = u_malloc(total);
- memcpy(arg, key, klen + 1);
- memcpy((char*)arg + klen + 1, val.constData(), vlen + 1);
+ struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
+ if (!arg) return;
+ arg->inst = gui_bridge_get_inst();
+ memcpy(arg->data, key, klen + 1);
+ memcpy(arg->data + klen + 1, val.constData(), vlen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
}
}
@@ -291,9 +297,11 @@ void SettingsDialog::onSave() {
QByteArray val = QByteArray::number(v);
size_t klen = strlen(key), vl = (size_t)val.size(), total = klen + 1 + vl + 1;
if (total <= 256) {
- void* arg = u_malloc(total);
- memcpy(arg, key, klen + 1);
- memcpy((char*)arg + klen + 1, val.constData(), vl + 1);
+ struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
+ if (!arg) return;
+ arg->inst = gui_bridge_get_inst();
+ memcpy(arg->data, key, klen + 1);
+ memcpy(arg->data + klen + 1, val.constData(), vl + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
}
if (!m_configPath.isEmpty()) {
@@ -308,9 +316,11 @@ void SettingsDialog::onSave() {
QByteArray val = QByteArray::number(v);
size_t klen = strlen(key), vl = (size_t)val.size(), total = klen + 1 + vl + 1;
if (total <= 256) {
- void* arg = u_malloc(total);
- memcpy(arg, key, klen + 1);
- memcpy((char*)arg + klen + 1, val.constData(), vl + 1);
+ struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
+ if (!arg) return;
+ arg->inst = gui_bridge_get_inst();
+ memcpy(arg->data, key, klen + 1);
+ memcpy(arg->data + klen + 1, val.constData(), vl + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
}
if (!m_configPath.isEmpty()) {
diff --git a/tools/chatgui/src/soundsettingspage.cpp b/tools/chatgui/src/soundsettingspage.cpp
index c6b04c71..d2d93c1c 100644
--- a/tools/chatgui/src/soundsettingspage.cpp
+++ b/tools/chatgui/src/soundsettingspage.cpp
@@ -156,9 +156,11 @@ void SoundSettingsPage::applyAndSave() {
size_t klen = strlen(k), vlen = strlen(v);
size_t total = klen + 1 + vlen + 1;
if (total > 256) continue;
- void* arg = u_malloc(total);
- memcpy(arg, k, klen + 1);
- memcpy((char*)arg + klen + 1, v, vlen + 1);
+ struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
+ if (!arg) continue;
+ arg->inst = gui_bridge_get_inst();
+ memcpy(arg->data, k, klen + 1);
+ memcpy(arg->data + klen + 1, v, vlen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
}
}
@@ -172,9 +174,11 @@ void SoundSettingsPage::applyAndSave() {
size_t klen = strlen(keys[j]), vlen = vals[j].size();
size_t total = klen + 1 + vlen + 1;
if (total > 256) continue;
- void* arg = u_malloc(total);
- memcpy(arg, keys[j], klen + 1);
- memcpy((char*)arg + klen + 1, vals[j].constData(), vlen + 1);
+ struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
+ if (!arg) continue;
+ arg->inst = gui_bridge_get_inst();
+ memcpy(arg->data, keys[j], klen + 1);
+ memcpy(arg->data + klen + 1, vals[j].constData(), vlen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
}
}
diff --git a/tools/chatgui/src/statuspage.cpp b/tools/chatgui/src/statuspage.cpp
index b1b28f09..ba89b77e 100644
--- a/tools/chatgui/src/statuspage.cpp
+++ b/tools/chatgui/src/statuspage.cpp
@@ -113,7 +113,7 @@ void StatusPage::updateClock() {
void StatusPage::refreshStatus() {
updateClock();
m_textEdit->setPlainText("Loading...");
- gui_bridge_post_uasync_fn(chat_core_collect_status_trampoline, NULL);
+ gui_bridge_post_uasync_fn(chat_core_collect_status_trampoline, gui_bridge_get_inst());
}
void StatusPage::onRefreshClicked() {
diff --git a/tools/chatgui/src/storagesettingspage.cpp b/tools/chatgui/src/storagesettingspage.cpp
index 72dc68d8..071011fe 100644
--- a/tools/chatgui/src/storagesettingspage.cpp
+++ b/tools/chatgui/src/storagesettingspage.cpp
@@ -17,9 +17,11 @@ static void saveUiStateInt(const char* key, int val) {
QByteArray v = QByteArray::number(val);
size_t klen = strlen(key), vlen = (size_t)v.size(), total = klen + 1 + vlen + 1;
if (total > 256) return;
- void* arg = u_malloc(total);
- memcpy(arg, key, klen + 1);
- memcpy((char*)arg + klen + 1, v.constData(), vlen + 1);
+ struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
+ if (!arg) return;
+ arg->inst = gui_bridge_get_inst();
+ memcpy(arg->data, key, klen + 1);
+ memcpy(arg->data + klen + 1, v.constData(), vlen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
}
@@ -28,9 +30,11 @@ void postChatSetting(const QString& configPath, const char* name, int val) {
QByteArray v = QByteArray::number(val);
size_t nlen = strlen(name), vlen = (size_t)v.size(), total = nlen + 1 + vlen + 1;
if (total > 256) return;
- void* arg = u_malloc(total);
- memcpy(arg, name, nlen + 1);
- memcpy((char*)arg + nlen + 1, v.constData(), vlen + 1);
+ struct chat_setting_arg* arg = (struct chat_setting_arg*)u_malloc(sizeof(struct chat_setting_arg));
+ if (!arg) return;
+ arg->inst = gui_bridge_get_inst();
+ memcpy(arg->data, name, nlen + 1);
+ memcpy(arg->data + nlen + 1, v.constData(), vlen + 1);
gui_bridge_post_uasync_fn(chat_core_set_setting_trampoline, arg);
}
diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h
index 97370e7e..a6d3b150 100644
--- a/tools/chatgui/transport/gui_bridge.h
+++ b/tools/chatgui/transport/gui_bridge.h
@@ -11,6 +11,7 @@ extern "C" {
struct UASYNC;
struct TOPO_GROUP;
struct TOPO_GROUP_NODE;
+struct UTUN_INSTANCE;
/* ── Типы уведомлений uasync→GUI (fire-and-forget) ── */
@@ -53,6 +54,13 @@ void gui_bridge_init(void);
/* Установить uasync (вызывается из UtunNode после создания ua) */
void gui_bridge_set_uasync(struct UASYNC* ua);
+/* Установить UTUN_INSTANCE (вызывается из UtunNode после создания instance).
+ Читается GUI-потоком через gui_bridge_get_inst() для сборки trampoline-аргументов. */
+void gui_bridge_set_inst(struct UTUN_INSTANCE* inst);
+
+/* Текущий UTUN_INSTANCE (NULL пока не создан). Безопасно из любого потока. */
+struct UTUN_INSTANCE* gui_bridge_get_inst(void);
+
/* uasync → GUI: уведомление (fire-and-forget, через Qt::QueuedConnection) */
void gui_bridge_post(int event_type, const uint8_t* data, int data_len);
diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp
index f88654c0..5ecf4fb2 100644
--- a/tools/chatgui/transport/gui_bridge_impl.cpp
+++ b/tools/chatgui/transport/gui_bridge_impl.cpp
@@ -49,6 +49,7 @@ static gui_admin_key_received_fn g_admin_key_received_cb = nullptr;
static gui_invite_link_ready_fn g_invite_link_ready_cb = nullptr;
static gui_invite_candidates_fn g_invite_candidates_cb = nullptr;
static struct UASYNC* g_ua = nullptr;
+static struct UTUN_INSTANCE* g_inst = nullptr;
/* ── GuiBridgeReceiver implementation ── */
@@ -255,6 +256,14 @@ void gui_bridge_set_uasync(struct UASYNC* ua) {
if (g_receiver) g_receiver->ua = ua;
}
+void gui_bridge_set_inst(struct UTUN_INSTANCE* inst) {
+ g_inst = inst;
+}
+
+struct UTUN_INSTANCE* gui_bridge_get_inst(void) {
+ return g_inst;
+}
+
void gui_bridge_post(int event_type, const uint8_t* data, int data_len) {
if (!g_receiver) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "gui_bridge_post: g_receiver=NULL, dropping event %d", event_type); return; }
diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp
index d4977d3e..a9af22e6 100644
--- a/tools/chatgui/transport/utun_node.cpp
+++ b/tools/chatgui/transport/utun_node.cpp
@@ -90,6 +90,7 @@ void UtunNode::stop() {
}
void UtunNode::finalize() {
+ gui_bridge_set_inst(nullptr);
if (m_ua) { uasync_destroy(m_ua, 0); m_ua = nullptr; }
m_instance = nullptr;
m_running = false;
@@ -234,6 +235,7 @@ void UtunNode::runLoop() {
/* set ua for gui_bridge before init, so GUI can post */
gui_bridge_set_uasync(ua);
+ gui_bridge_set_inst(m_instance);
if (utun_instance_init(m_instance) != 0) {
QMetaObject::invokeMethod(this, [this] { emit error("utun_instance_init failed"); });
@@ -254,14 +256,15 @@ void UtunNode::runLoop() {
utun_add_nodeinfo_cbk(m_instance, gui_nodeinfo_cb_impl, nullptr);
/* Bridge chat events to GUI via gui_bridge */
- chat_event_set_handler([](int type, const uint8_t* data, int len) {
+ chat_event_set_handler(m_instance, [](struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) {
+ (void)inst;
gui_bridge_post(type, data, len);
});
/* Initialize chat_core (DB) and chat_sync (channel/message P2P sync) */
chat_core_init(m_instance, QString(m_dbPath + "/chats.db").toUtf8().constData());
chat_sync_init(m_instance);
- chat_core_sync_my_addresses();
+ chat_core_sync_my_addresses(m_instance);
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "chat_core + chat_sync initialized");
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "utun_node: entering poll loop");