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 nameDefault value magnitudeLarger value affects...Smaller value affects...Effect to CPU burden
+
SEQUENCE_MS
+
Default value is relatively large, chosen for + slowing down music tempoLarger 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 tempoLarger value eases finding a good mixing + position, but may cause a "drifting" artifactSmaller 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:

+
    +
  • Initial release
  • +
+

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:

+
    +
  • Initial release
  • +
+
+

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