Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
320bc2a7e4 |
No files matched your search
@@ -2,9 +2,7 @@
|
||||
/build/
|
||||
build-win/
|
||||
tools/chatgui/build/
|
||||
tools/chatgui/rlottie_build/
|
||||
tools/chatgui/ffmpeg/ffmpeg_build/
|
||||
tools/lightsout/build/
|
||||
|
||||
# Build outputs
|
||||
*.o
|
||||
@@ -13,8 +11,6 @@ tools/lightsout/build/
|
||||
*.a
|
||||
*.so
|
||||
*.so.*
|
||||
# Prebuilt native runtime для Android VAD (onnxruntime) — коммитим несмотря на *.so
|
||||
!tools/chatgui-android/app/src/main/jniLibs/*/libonnxruntime.so
|
||||
/utun
|
||||
/src/utun
|
||||
test_serialize
|
||||
@@ -32,7 +28,6 @@ index.html
|
||||
*.exe
|
||||
|
||||
# Autotools
|
||||
/.utun-build-state*
|
||||
Makefile
|
||||
!tools/etcpmon/Makefile
|
||||
Makefile.in
|
||||
@@ -85,7 +80,6 @@ logs/
|
||||
tests/test_*
|
||||
!tests/test_*.c
|
||||
!tests/test_*.h
|
||||
!tests/test_*.py
|
||||
!tests/test_*.conf
|
||||
!tests/test_*.json
|
||||
tests/bench_*
|
||||
@@ -114,7 +108,6 @@ tools/proxy/udp_proxy
|
||||
|
||||
# Reference projects (not part of utun)
|
||||
tools/tdesktop-dev/
|
||||
/hardware/
|
||||
|
||||
# Debug/temp artifacts
|
||||
*.bak
|
||||
|
||||
@@ -25,24 +25,12 @@ This file contains essential information for AI coding agents working in the uTu
|
||||
При поиске багов всегда проверяй достаточно ли отладочной информации. Если не достаточно - фокусируйся на том чтобы добавить нужную информацию в вывод.
|
||||
Помни что отладка управляется фильтрами по категормяи, которые нужно проверить и при необходимости правильно настроить (в конфиге или в коде).
|
||||
|
||||
Для поиска выстараивай информативные логи. По которым максимально чётко понятно что происходит. Если какие-то места которые могут иметь отношение к ошибке в логах пропущены - это повод доработать логи и прогнать еще раз.
|
||||
Всегда отладку выстраивай через доработку логов, пока точно и однозначно не будет видно в каком точно месте проблема.
|
||||
|
||||
По хорошей диагностике сразу должно быть понятно что происходит, в том числе точную причину проблемы. Если приходится догадываться или плохо понятно - это значит что надо улучшать диагностику.
|
||||
|
||||
Доствп на узел colo2: ssh -p44322 logs@colo2
|
||||
|
||||
Структура .h файлов:
|
||||
- в начале краткое и понятное описание: что делает модуль, как пользоваться.
|
||||
- поля структур также кратко описываем - что, для его, если есть нюансы/значения
|
||||
- для функций: описываем назначение, как пользоваться, описание аргументов, возврата. кратко но понятно.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
**Repository:** uTun — общее сетевое ядро ETCP, независимо запускаемые UTUN и P2P-чат
|
||||
**Repository:** uTun - Secure VPN tunnel with ETCP protocol
|
||||
**Language:** C (C99)
|
||||
**Build System:** GNU Autotools (autoconf/automake)
|
||||
**Cryptography:** OpenSSL (AES-CCM, X25519, Ed25519, SHA256)
|
||||
**Cryptography:** OpenSSL (AES-CCM, X25519, SHA256)
|
||||
|
||||
## Build Commands
|
||||
|
||||
@@ -56,19 +44,9 @@ This file contains essential information for AI coding agents working in the uTu
|
||||
powershell build_full.bat # запускает bash build.sh --full через MSYS2 UCRT64
|
||||
```
|
||||
|
||||
### Build (FreeBSD)
|
||||
```sh
|
||||
# Первичная установка инструментов от root:
|
||||
pkg install -y bash gmake autoconf automake libtool pkgconf openssl python3
|
||||
./build.sh -j4 # общий скрипт; автоматически выбирает gmake и Clang
|
||||
./build_bsd_full.sh -j4 # полная сборка через общий скрипт
|
||||
```
|
||||
Python 3 нужен для проверок скрипта сборки. SHA-256 берётся из базовой системы FreeBSD.
|
||||
|
||||
### Incremental Build
|
||||
```bash
|
||||
./build.sh -j4 # инкрементальная сборка, конфигурация и очистка при необходимости
|
||||
./build.sh --no-gui -j4 # только ядро и тесты, без отдельной сборки GUI
|
||||
./build.sh -j4 # make с авто-конфигурацией если надо
|
||||
```
|
||||
```powershell
|
||||
powershell -Command ".\build.bat" 2>&1 # Windows, логи: build_win.log
|
||||
@@ -83,10 +61,9 @@ make distclean # Clean everything including configure files
|
||||
|
||||
### Partial Builds
|
||||
```bash
|
||||
make -C lib # базовые библиотеки
|
||||
make -C lib/libopus # встроенный Opus
|
||||
make -C src # основной бинарник src/utun (библиотеки уже должны быть собраны)
|
||||
make -C tests # тесты (используют объекты src/)
|
||||
cd lib && make # Build only the library (libuasync.a)
|
||||
cd src && make # Build only the main program
|
||||
cd tests && make # Build only tests
|
||||
```
|
||||
|
||||
### Direct Build (Windows, без autotools)
|
||||
@@ -102,7 +79,7 @@ python3 tools/chatcli members <ch_id> # мемберы с полн
|
||||
python3 tools/chatcli messages <ch_id> [count] # последние сообщения
|
||||
python3 tools/chatcli send <ch_id> "text" # отправить сообщение
|
||||
python3 tools/chatcli invite <ch_id> # создать invite-ссылку
|
||||
python3 tools/chatcli connect 'utun://...' # join по ссылке
|
||||
python3 tools/chatcli connect <utun://...> # join по ссылке
|
||||
python3 tools/chatcli create "Name" # создать канал
|
||||
python3 tools/chatcli listen # слушать события (Ctrl+C выход)
|
||||
```
|
||||
@@ -110,14 +87,13 @@ python3 tools/chatcli listen # слушать событи
|
||||
|
||||
### ASAN Build (AddressSanitizer, Linux)
|
||||
```bash
|
||||
./build.sh --asan -j4 # собирает lib/ + src/; тесты этот режим скрипта пропускает
|
||||
./build.sh --asan -j4 # lib/ + src/ only (tests not ASAN-compatible)
|
||||
```
|
||||
Запуск с ASAN (detect_leaks отключены чтобы не спамить при shutdown):
|
||||
```bash
|
||||
ASAN_OPTIONS=detect_leaks=0:halt_on_error=0:log_path=/tmp/utun_asan ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log
|
||||
ASAN_OPTIONS=detect_leaks=0:halt_on_error=0 ./src/utun -f -p /tmp/utun.pid -l /tmp/utun.log
|
||||
```
|
||||
В этом примере отчёты ASAN пишутся в `/tmp/utun_asan.<pid>` благодаря `log_path`.
|
||||
Для отдельного теста нужно собирать его зависимости с теми же sanitizer-флагами; обычные и ASAN-объекты не смешивать.
|
||||
Краш-логи ASAN пишутся в `/tmp/utun_asan.<pid>`.
|
||||
|
||||
## Test Commands
|
||||
|
||||
@@ -135,14 +111,13 @@ cd tests/
|
||||
./test_etcp_two_instances
|
||||
```
|
||||
|
||||
### Build and Run a Single Test
|
||||
### Run Single Test with Debug Info
|
||||
```bash
|
||||
make -C tests test_services
|
||||
cd tests && ./test_services
|
||||
cd tests/
|
||||
gcc -I../src -I../lib -I../tinycrypt/lib/include \
|
||||
-o my_test test_file.c ../src/*.c ../lib/*.c ../tinycrypt/lib/source/*.c
|
||||
./my_test
|
||||
```
|
||||
Использовать цель из `tests/Makefile.am`: ручная компиляция `src/*.c` пропускает подкаталоги и добавляет лишний `main`.
|
||||
Уровни и категории диагностики настраиваются в самом тесте через `debug_set_level` / `debug_set_category_level`.
|
||||
Проверки самого скрипта сборки запускаются отдельно: `make -C tests check-build-script` (нужен Python 3); в `./check.sh` они не входят.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
@@ -155,7 +130,7 @@ cd tests && ./test_services
|
||||
|
||||
### Formatting
|
||||
- **Indentation:** 4 spaces, no tabs
|
||||
- **Braces:** Открывающая скобка на строке функции или условия:
|
||||
- **Braces:** Same line for functions, new line for control structures:
|
||||
```c
|
||||
int function(void) {
|
||||
if (condition) {
|
||||
@@ -163,7 +138,7 @@ cd tests && ./test_services
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Comments:** Краткие описания на русском; сохранять принятые имена API и протокольных полей.
|
||||
- **Comments:** Primary language is Russian for business logic, English for API docs
|
||||
- **Line Length:** Aim for 80-100 characters, but can go up to 150 if logically coherent
|
||||
|
||||
### Include Order
|
||||
@@ -219,7 +194,7 @@ sc_context_t ctx;
|
||||
sc_init_ctx(&ctx, &my_keys);
|
||||
|
||||
// 2. Set peer public key (for key exchange)
|
||||
sc_set_peer_public_key(&ctx, peer_public_key_bin, SC_PEER_PUBKEY_BIN); // 32 бинарных байта; HEX — для hex-строки
|
||||
sc_set_peer_public_key(&ctx, peer_public_key, SC_PEER_PUBKEY_HEX); // 0=bin, 1=hex
|
||||
|
||||
// 3. Ready for encrypt/decrypt
|
||||
sc_encrypt(&ctx, plaintext, plaintext_len, ciphertext, &ciphertext_len);
|
||||
@@ -241,97 +216,62 @@ sc_obfuscate_pubkey(salt, peer_pubkey_bin, my_pubkey_bin, obfuscated_output);
|
||||
### Debug Levels
|
||||
`none < error < warn < info < debug < trace`
|
||||
trace - логируем заходы в функции и важные ветвления внутри функций
|
||||
debug - отладка: всё что нужно для понимание сути происходящего (состояний, важных переменных) алгоритма. тоесть по уровню debug мы должны видеть все важные внутреннии нюансы работы функции включая состояния и переменные.
|
||||
debug - всё что нужно для понимание сути происходящего (состояний, важных переменных) алгоритма. тоесть по уровню debug мы должны видеть все важные внутреннии нюансы работы функции включая состояния и переменные.
|
||||
info - сообщения для пользователя о работе. мы должны видеть в читаемом виде понятные и осмысленные сообщения о каких-либо значимых действиях с точки зрения логики работы модуля или приложения
|
||||
warn / error - ошибки и предупреждения (аномалии). должны быть во всех ошибочных ветках. Молча вываливаться с ошибкой нельзя, все ошибки и предупреждения должны логироваться.
|
||||
|
||||
### Debug Categories
|
||||
Актуальные имена и константы — в `lib/debug_config.h`, текстовые имена — в `lib/debug_config.c`.
|
||||
Основные: `sys`, `connection`, `etcp`, `crypto`, `config`, `tun`, `routing`, `timers`, `bgp`,
|
||||
`socket`, `control`, `dump`, `traffic`, `debug`, `general`, `nat`, `keepalive`, `etcp_route`,
|
||||
`media`, `etcp_dump`, `chat`, `chat_sync`, `member_sync`, `proxy`, `video`, `reality`, `dm`, `call`, `radio`, `vad`, `aec`.
|
||||
UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в ETCP; старые числовые списки не использовать.
|
||||
### Debug Categories (28 категорий)
|
||||
```
|
||||
NONE=0, UASYNC=1, LL_QUEUE=2, CONNECTION=3, ETCP=4, CRYPTO=5, MEMORY=6,
|
||||
TIMING=7, CONFIG=8, TUN=9, ROUTING=10, TIMERS=11, NORMALIZER=12, BGP=13,
|
||||
SOCKET=14, CONTROL=15, DUMP=16, TRAFFIC=17, DEBUG=18, GENERAL=19, NAT=20,
|
||||
KEEPALIVE=21, ETCPROUTE=22, BBR=23, ETCP_DUMP=24, CONNECTIVITY=25, DB_SYNC=26,
|
||||
MEMBER_SYNC=27
|
||||
```
|
||||
|
||||
### Настройка отладки
|
||||
- В INI-конфиге: секция `[debug]`, каждая категория отдельной строкой (`etcp=trace`, `config=info`).
|
||||
- Строку `etcp=trace,config=info` принимает `debug_parse_config()`; GUI хранит её в `[gui] debug_categories`.
|
||||
- В конфиге: `debug = etcp=trace,config=info` (формат: `категория=уровень,...`)
|
||||
- В коде: глобальный уровень и per-category уровни из `debug_config_t g_debug_config`
|
||||
- Макросы: `DEBUG_ERROR(cat,fmt,...)` `DEBUG_WARN` `DEBUG_INFO` `DEBUG_DEBUG` `DEBUG_TRACE`
|
||||
- `log_dump(level, category, prefix, data, len)` — hex dump в лог
|
||||
- `log_dump(prefix, data, len)` — hex dump в лог
|
||||
|
||||
### Dual Output
|
||||
- Фильтр уровней общий для выходов; `debug_enable_console()` включает/выключает консоль.
|
||||
- Консоль и файл настраиваются раздельно (`debug_set_console_level`, `debug_set_file_level`)
|
||||
- `debug_enable_file_output(path, truncate)` / `debug_disable_file_output()`
|
||||
|
||||
## Architecture
|
||||
|
||||
### Ядро, сервисы и владение ресурсами
|
||||
|
||||
- `utun_instance_create*()` создаёт ядро; `utun_core_start()` запускает общие сетевые службы.
|
||||
- `utun_service_start/stop()` управляет UTUN-группой, TUN/NAT и запросами к пирам из конфига.
|
||||
- `chat_service_start/stop()` управляет чатом и CHAT-группами. Чат не требует UTUN; desktop и Android запускают ядро и чат явно.
|
||||
- Ядро владеет общей SQLite (`db_path/chats.db`, без пути — `:memory:`), идентичностью, транспортом и маршрутизатором.
|
||||
Остановка сервиса сохраняет БД и ресурсы другого сервиса. `utun_instance_destroy()` освобождает ядро, но не переданный `UASYNC`.
|
||||
- Группа владеет NCD handle с начала CONNECTING. Инициатор хранит отменяемый `TOPO_PEER_REQUEST`;
|
||||
закрытие своего запроса не разрывает транспорт других владельцев. Готовая сессия сохраняется у группы.
|
||||
- UP означает работоспособный транспорт. READY означает завершённое согласование и обмен таблицей конкретной группы.
|
||||
Recovery ждёт нужных маршрутов через READY-пиров; наличие `ETCP_CONN*` само по себе не гарантирует UP.
|
||||
- JOIN добавляет подписанного мембера, Merkle распространяет запись. Доступ к CHAT-группе каждый узел даёт по своей таблице участников;
|
||||
invite-ключ не даёт временного членства. Протокол описан в `src/chat/chat_join.h`, групповое согласование — в `src/routing_layer/topo_group.h`.
|
||||
- `TOPO_NODE` — общая подписанная запись узла. Более свежий timestamp заменяет запись целиком, с полями и подписью;
|
||||
адреса не версионируются отдельно. BGP обновляет запись и передаёт изменения NCD (см. `doc/node_snapshot.md`).
|
||||
- BGP хранит один путь к назначению от каждого соседа. WITHDRAW удаляет только путь отправителя;
|
||||
NODEINFO с прежним timestamp также обновляет путь. Выбор: живой путь с минимумом хопов, затем меньший ID соседа.
|
||||
REINIT сохраняет пути до проверки нового снимка; TABLE_COMPLETE удаляет неподтверждённые, зависший обмен перезапускается.
|
||||
- ETCP-router дописывает ID при транзите и отбрасывает повторное посещение для DATA/ACK/control.
|
||||
Список переходов находится вне подписанного тела; формат описан в `src/routing_layer/etcp_router.h`.
|
||||
|
||||
Все операции с ядром выполняются в его uasync-потоке; stop/destroy — вне callbacks останавливаемых сервисов.
|
||||
Порядок освобождения ресурсов и нюансы media workers: `doc/service_lifecycle.md`.
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
├── lib/ # uasync, очереди, SQLite, Opus, SpeexDSP, Silero VAD, SoundTouch, LMDB
|
||||
├── lib/ # Core libraries (18 .c + 20 .h + libopus/liblmdb)
|
||||
├── src/ # Main source code
|
||||
│ ├── transport_layer/ # ETCP, сессии, crypto, STCP/REALITY, SOCKS-клиент, NCD, BBR
|
||||
│ ├── routing_layer/ # Routing, группы/BGP, recovery, conn_mgr, etcp_router
|
||||
│ ├── chat/ # P2P чат, join, db_sync, merkle_sync, member_sync
|
||||
│ ├── dm/ # Личные сообщения, E2E-вложения и offline-mailbox
|
||||
│ ├── call/ # Звонки и общий голосовой стек
|
||||
│ ├── radio/ # Групповая PTT-рация
|
||||
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси
|
||||
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек
|
||||
│ ├── media_delivery/ # Блоки медиа каналов и передача неизменяемых файлов
|
||||
│ ├── media_async/ # Фоновые задачи, метаданные вложений, подготовка голоса/видео
|
||||
│ ├── video/ # Пробинг и транскодирование видео через FFmpeg
|
||||
│ ├── dnsmasq/ # Архив исходников для встроенной сборки dnsmasq
|
||||
│ └── *.c/h # utun, instance, config, TUN, firewall, NAT, NTP, control, broadcast
|
||||
├── tests/ # Состав тестов и цели запуска: tests/Makefile.am
|
||||
│ ├── transport_layer/ # ETCP stack, crypto, STCP, normalizer, BBR (~30 файлов)
|
||||
│ ├── routing_layer/ # Routing, BGP, conn_mgr, etcp_router, NAT detection (~20 файлов)
|
||||
│ ├── chat/ # P2P чат, db_sync, merkle_sync, member_sync (17 файлов)
|
||||
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси (10 файлов)
|
||||
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек (7 файлов)
|
||||
│ ├── media_delivery/ # Доставка медиафайлов (6 файлов)
|
||||
│ ├── media_async/ # Асинхронный движок медиа (2 файла)
|
||||
│ └── *.c/h # Корневые модули: utun, config, TUN, firewall, NAT, NTP, control (~14 файлов)
|
||||
├── tests/ # ~70 тестов, 64 в check_PROGRAMS
|
||||
├── doc/ # Technical Specifications
|
||||
├── tools/ # Auxiliary tools
|
||||
│ ├── etcpmon/ # GUI монитор ETCP
|
||||
│ ├── chatgui/ # GUI чат (Qt 6 с Qt 5 fallback, сборка CMake)
|
||||
│ ├── chatgui-android/ # Android P2P чат (Jetpack Compose + libutun_lite)
|
||||
│ ├── logreceiver/ # Приём UDP-логов Android
|
||||
│ ├── chat_tcp_test/ # Сценарий проверки чата через TCP
|
||||
│ ├── lightsout/ # Отдельная игра Lights Out на Qt
|
||||
│ ├── lightsout-android/ # Android-версия Lights Out
|
||||
│ ├── tdesktop-dev/ # Референс: Telegram Desktop (только для изучения)
|
||||
│ ├── proxy/ # UDP прокси для тестов
|
||||
│ ├── bping/ # BPing (bandwidth ping)
|
||||
│ └── chatcli # Headless CLI; описание команд — chatcli_commands.txt
|
||||
│ └── bping/ # BPing (bandwidth ping)
|
||||
├── tinycrypt/ # TinyCrypt crypto library (external)
|
||||
└── net_emulator/ # Network emulator (delays, loss, reordering)
|
||||
├── net_emulator/ # Network emulator (delays, loss, reordering)
|
||||
└── c2/ # Test instance 2 (конфиг и бинарник для тестов)
|
||||
```
|
||||
|
||||
### File Overview
|
||||
|
||||
Пути ниже относительны к каталогу секции; `name.c/h` означает пару `.c` и `.h`.
|
||||
Перечислены модули проекта; внутренние файлы сторонних библиотек сгруппированы по каталогам.
|
||||
|
||||
**Core (src/)**
|
||||
- `utun.c` - Main program entry point, CLI parsing, daemon mode
|
||||
- `utun_instance.c/h` - Создание ядра и независимый жизненный цикл UTUN/чата, владение общими ресурсами
|
||||
- `utun_instance.c/h` - Root instance lifecycle, config loading, all submodule init
|
||||
- `tun_if.c/h` - TUN interface API (init/write/close, cross-platform)
|
||||
- `tun_linux.c` `tun_freebsd.c` `tun_windows.c` - Platform-specific TUN implementations
|
||||
- `tun_route.c/h` - TUN routing table sync
|
||||
@@ -343,22 +283,16 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `nat_transport.c/h` - NAT transport layer (packet relay)
|
||||
- `ntp_time.c/h` - NTP time synchronization
|
||||
- `ntp_node_time.c/h` - Inter-node time synchronization
|
||||
- `broadcast.c/h` - Широковещательная рассылка данных по topo-группе (UUID-дедупликация, TTL)
|
||||
- `video/video.c/h` - Пробинг и транскодирование видео (FFmpeg, опционально; H.264/MP4 ≤720p)
|
||||
- `dnsmasq/` - Архив dnsmasq-2.93.tar.xz для встроенной сборки
|
||||
|
||||
**Transport Layer (src/transport_layer/)**
|
||||
- `etcp.c/h` - ETCP protocol implementation (inflight queues, retrans, ACK, RTT/jitter)
|
||||
- `etcp_api.c/h` - ETCP public API (send/recv/bind callbacks)
|
||||
- `etcp_connections.c/h` - Socket and link management, INIT handshake, keepalive, PING/PONG
|
||||
- `etcp_session.c/h` - Согласование эпох ETCP-сессии через HELLO/CHALLENGE/CONFIRM и проверка принадлежности DATA сессии
|
||||
- `etcp_loadbalancer.c/h` - Multi-link load balancing with traffic shaper
|
||||
- `etcp_debug.c/h` - ETCP packet dump/formatting
|
||||
- `etcp_dump.c/h` - ETCP дамп/декодирование пакетов
|
||||
- `etcp_bbr.c/h` - BBR congestion control for ETCP
|
||||
- `etcp_connect.c/h` - ETCP outbound connections
|
||||
- `etcp_keepalive.c/h` - Keepalive-пакеты, адаптивный период, смена режима standby/normal (мобильные узлы)
|
||||
- `auto_socket.c/h` - Автосоздание UDP+TCP сокетов на всех интерфейсах и линков к ETCP-соединениям (`auto_sockets=yes`)
|
||||
- `pkt_normalizer.c/h` - Packet fragmentation/reassembly (packer/unpacker)
|
||||
- `packet_dump.c/h` - Packet hex dump utility
|
||||
- `dummynet.c/h` - Network emulator integrated into utun (for testing)
|
||||
@@ -368,13 +302,8 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `stcp_server.c/h` - STCP сервер
|
||||
- `stcp_client.c/h` - STCP клиент
|
||||
- `stcp_link.c/h` - STCP линк-уровень
|
||||
- `socks_client.c/h` - Исходящий SOCKS5-транспорт: TCP CONNECT и UDP ASSOCIATE с переподключением control-сокета
|
||||
- `node_conn_direct.c/h` - Общий прямой транспорт к пиру с отдельным handle каждого владельца; обновление линков по адресам узла
|
||||
- `node_conn_direct.c/h` - Direct config-based connections
|
||||
- `socket_monitor.c/h` - Socket state monitoring
|
||||
- `reality.c/h` - REALITY-style TLS ClientHello/ServerHello камуфляж для STCP
|
||||
- `reality_fingerprint.c/h` - Статические отпечатки (cipher suites, groups, ALPN) для REALITY-камуфляжа
|
||||
- `reality_relay.c/h` - Релей неавторизованных клиентов на реальный HTTPS-сайт
|
||||
- `call_ring.c/h` - Кольцевой трейс контрольных точек освобождения ETCP/STCP-ресурсов для диагностики teardown/UAF
|
||||
|
||||
**BBR (src/transport_layer/BBR/)**
|
||||
- `bbr_v3.c` - BBR congestion control algorithm v3
|
||||
@@ -383,78 +312,38 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `routing.c/h` - Routing table (local)
|
||||
- `route_lib.c/h` - Routing library
|
||||
- `route6_lib.c/h` - IPv6 routing
|
||||
- `route_ping.c/h` - Пинг цели через явно выбранного BGP-посредника для NAT-проверки
|
||||
- `route_ping.c/h` - Route ping probing (NAT check, liveness)
|
||||
- `route_connectivity.c/h` - Route connectivity checks
|
||||
- `topo_group.c/h` - Сессии UTUN/CHAT (JOIN/ACCEPT, эпохи, READY), групповые запросы, NODEINFO/WITHDRAW и владение NCD
|
||||
- `topo_group_connect.c/h` - Подбор пиров CHAT-группы по роли: суперузлы → desktop → mobile; история/RTT внутри класса, backoff и standby burst
|
||||
- `topo_group_invite.c/h` - Получение описания канала через INVITE_INFO_REQ/RESP; добавление мембера реализовано в chat_join/chat_sync
|
||||
- `topo_group.c/h` - BGP-like route exchange between nodes (NODEINFO/WITHDRAW, изолированные группы UTUN/CHAT)
|
||||
- `topo_group_connect.c/h` - Авто-подключение к узлам CHAT-группы (3 фазы, бесконечный цикл)
|
||||
- `topo_group_invite.c/h` - Invite/join канала через прямое ncd-подключение (INVITE_INFO_REQ/RESP)
|
||||
- `topo_node.c/h` - Node management (модель узла: TOPO_NODE/TOPO_GROUP_NODE, сериализация, node_registry)
|
||||
- `topo_node_sqlite.c/h` - SQLite persistence for nodes
|
||||
- `topo_recovery.c/h` - Восстановление потерянных маршрутов группы через READY-сессии, отмена попыток при stop
|
||||
- `topo_recovery.c/h` - Восстановление каскадно отвалившихся узлов после разрыва соединения
|
||||
- `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND)
|
||||
- `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding)
|
||||
- `nat_detection.c/h` - NAT type detection
|
||||
- `sock_match.c/h` - Классификация локальных сокетов и адресов пира, отбор совместимых пар для прямого подключения
|
||||
- `route_crypto.c/h` - Пред/пост-обработка SVC_ROUTE пакетов (encrypt-then-sign)
|
||||
|
||||
**Chat (src/chat/)** — децентрализованный P2P чат: сообщения и участники автоматически синхронизируются между всеми узлами канала без центрального сервера.
|
||||
- `chat_core.c/h` - API сообщений, каналов, профиля и настроек; chat_service_start/stop управляют чат-сервисом поверх общей БД ядра
|
||||
- `chat_core.c/h` - Главный модуль чата. Единственная точка входа из GUI: принять сообщение, создать канал, изменить настройку. Управляет БД и жизненным циклом всех chat-модулей
|
||||
- `chat_core_priv.h` - Внутренний API для частей chat_core: общий контекст и утилиты БД. Снаружи не используется
|
||||
- `chat_sync.c/h` - Связывает чат с ETCP-сетью: отслеживает появление/разрыв соединений, обновляет онлайн-статус узлов, запускает синхронизацию участников каналов, обрабатывает приглашения
|
||||
- `chat_event.c/h` - Доставка событий из ядра чата в GUI. Единый механизм: любой модуль отправляет событие (новое сообщение, смена участников, статус), GUI получает через один обработчик
|
||||
- `chat_setting.c/h` - Проверка и хранение настроек чата per-instance; общая логика для конфигурации и GUI
|
||||
- `chat_join.c/h` - Регистрация invite-ключа с ACK, пересылка запроса инвайтеру и добавление подписанного мембера; контракт join в заголовке
|
||||
- `db_sync.c/h` - Репликация подписанных записей SQLite через прямые ETCP-соединения; расхождения по цепочке хешей, новые записи через PUSH
|
||||
- `chat_setting.c/h` - Настройки чата: проверяет корректность значений, хранит текущие. Используется и headless-парсером конфига, и GUI. Не зависит от других модулей
|
||||
- `db_sync.c/h` - Автоматическая репликация данных между подключёнными узлами. Запись на одном узле — появляется у всех остальных. Криптоподписи и цепочка хешей защищают от подделок
|
||||
- `member_sync.c/h` - Синхронизация участников каналов поверх merkle_sync. Два подписанных блока (мембер/владелец), по-блочное сравнение версий, верификация подписей. Удаление — только битая запись
|
||||
- `merkle_sync.c/h` - Универсальный движок синхронизации на Merkle-деревьях (5 уровней × 32 бакета, SHA256). Пиры обмениваются хешами и передают только различия; per-session out-очередь с backpressure. Не привязан к конкретному типу данных (коллбэки `data_ops`)
|
||||
- `merkle_tree.c/h` - Производный индекс Merkle-хешей в SQLite; восстанавливается из данных модели, обновляется с ними в одной транзакции
|
||||
- `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core)
|
||||
- `chat_channel.c` - Создание и настройка каналов: таблицы, криптоключи, CHAT-группа и планировщик подключения (часть chat_core)
|
||||
- `chat_channel.c` - Создание и настройка каналов: готовит таблицы в БД, генерирует криптоключи, запускает подключение ко всем участникам (часть chat_core)
|
||||
- `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core)
|
||||
- `chat_status.c` - Сбор диагностики: текущее время, активные соединения, типы NAT. Отправляется в GUI (часть chat_core)
|
||||
- `chat_member.c/h` - Единая структура мембера для отображения в GUI (десктоп/headless/Android), фиксированная сериализация на проводе
|
||||
- `chat_admin.c/h` - Передача канального приватного ключа (прав админа) другому узлу
|
||||
- `chat_whisper.c/h` - Whisper speech-to-text транскрипция голосовых сообщений (опционально, асинхронно через media_async)
|
||||
- `chat_headless_control.c/h` - TCP control-socket для headless chat CLI (JSON line protocol)
|
||||
- `invite_build.c/h` - Сборка invite-ссылок utun:// (выбор лучшего узла + адреса)
|
||||
- `invite_link.c/h` - Кодирование utun://: версия, пароль, channel_id, join_key, Reality-параметры, ключ и адреса узла
|
||||
|
||||
**Личные сообщения (src/dm/)**
|
||||
|
||||
- `dm_core.c/h` - Беседы двух узлов, подписанные E2E-сообщения, SQLite/outbox, доставка через router/mailbox и квитанции
|
||||
- `dm_crypto.c/h` - Вывод ID беседы и ключа X25519, AES-256-CCM сообщений и потоковое шифрование/проверка медиа
|
||||
- `dm_mailbox.c/h` - Offline-очередь суперузла: хранение подписанных сообщений и повтор доставки до квитанции получателя
|
||||
- `dm_media.c/h` - Подготовка, доставка и публикация E2E-вложений; ciphertext-хранилище и отдельные квитанции файлов
|
||||
- `dm_media_priv.h` - Внутренние SQL/control-операции для dm_media и dm_mailbox_media
|
||||
- `dm_mailbox_media.c/h` - Сохраняемые задания суперузла для размещения медиа, доставки метаданных и удаления копий до DELETE_ACK
|
||||
|
||||
**Звонки (src/call/)**
|
||||
|
||||
- `call.c/h` - Сигналинг и медиа P2P-звонка через ETCP-router, состояния звонка и оптимизация пути через conn_mgr
|
||||
- `call_proto.h` - Wire-формат сигналинга и медиа, треки/кодеки, причины завершения
|
||||
- `call_audio.c/h` - Общий аудио-движок: кодирование/декодирование Opus, jitter-буфер и тоны, API feed/pull PCM
|
||||
- `call_jitter.cpp/h` - FIFO кодированных Opus-кадров, адаптивный запас и SoundTouch time-stretch при воспроизведении
|
||||
- `call_jitter_window.h` - Окно из десяти секундных интервалов для оценки глубины очереди и изменения задержки доставки
|
||||
- `call_tones.c/h` - Генерация PCM-тонов паузы и завершения звонка
|
||||
- `call_headless.c/h` - Отдельный TCP-аудиосокет headless-клиента: обмен PCM; сигналинг остаётся на control-сокете
|
||||
|
||||
**Рация (src/radio/)**
|
||||
|
||||
- `radio.c/h` - Передача Opus по CHAT-группе, подписки, дедупликация и пересылка по ветвям с подписчиками
|
||||
- `radio_proto.h` - Wire-формат PTT: источник, поток, seq, FIN и hop-list против циклов
|
||||
- `radio_audio.c/h` - Общий аудио-движок: независимые RX/TX, Opus, микширование источников, ручной PTT и VAD-автомат
|
||||
- `radio_jitter.cpp/h` - Буфер кодированных кадров каждого источника с pre-roll и SoundTouch time-stretch на pull
|
||||
- `radio_vad.h` - Ресемплинг 48→16 кГц и автомат авто-PTT с подтверждением речи, задержкой завершения и учётом занятого канала
|
||||
- `radio_headless.c/h` - TCP-аудиосокет headless-рации: PCM и команды начала/конца PTT
|
||||
|
||||
**Прокси (src/proxy/)**
|
||||
|
||||
- `proxy_protocol.h` - Общий TCP-протокол и управление потоком: CONNECTED, DATA, кредит WINDOW, упорядоченный FIN
|
||||
- `socks_proxy.c/h` - Клиентские SOCKS5 CONNECT и HTTP proxy/CONNECT, DNS и передача потока выбранному exit-узлу
|
||||
- `socks_proxy.c/h` - SOCKS5 прокси
|
||||
- `udp_proxy.c/h` - UDP прокси
|
||||
- `icmp_proxy.c/h` - ICMP прокси
|
||||
- `tcp_proxy_server.c/h` - Exit-сторона TCP-прокси: реальные исходящие сокеты, потоки по (peer, stream_id), окна и half-close
|
||||
- `tcp_proxy_client.c/h` - Клиентская сторона TCP-прокси: адаптер TUN/lwIP и обмен с настроенным exit через ETCP-router
|
||||
- `tcp_proxy_server.c/h` - TCP прокси (серверная сторона)
|
||||
- `tcp_proxy_client.c/h` - TCP прокси (клиентская сторона)
|
||||
|
||||
**lwIP TCP (src/lwip_tcp/)**
|
||||
- `lwip_tcp.c/h` - lwIP TCP стек
|
||||
@@ -463,50 +352,32 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- `lwip_tcp_priv.h`, `lwip_tcp_opts.h` - Внутренние настройки lwIP
|
||||
|
||||
**Media Delivery (src/media_delivery/)**
|
||||
- `media_delivery.c/h` - Доставка блоков медиа каналов, учёт держателей, репликация доступности между суперузлами и relay
|
||||
- `media_delivery_proto.h` - Кодограммы канального медиа: QUERY, BLOCK_REQ/CHUNK/DONE, HAVE_BLOCK и SUPER_REPL
|
||||
- `media_download.c/h` - Загрузка блоков с выбором держателей, failover, контролем простоя, проверкой подписей и сборкой файла
|
||||
- `media_index.c/h` - Индекс media_files: UUID, хеши и подписи блоков, асинхронная регистрация и удаление медиа канала
|
||||
- `file_transfer.c/h` - Передача неизменяемого файла между заданными узлами; авторизация lookup и владение CM handle до завершения
|
||||
- `media_delivery.c/h` + `media_delivery_proto.h` - Доставка медиафайлов, протокол
|
||||
- `media_download.c/h` - Скачивание медиа
|
||||
- `media_index.c/h` - Индексация медиафайлов
|
||||
|
||||
**Media Async (src/media_async/)**
|
||||
- `media_async.c/h` - Фоновые crypto/file-задачи: work в pthread, done в uasync; destroy ждёт workers и завершает ожидающие callbacks с CANCELLED
|
||||
- `attachment.c/h` - Общие метаданные file/voice/video, проверка и компактная wire-сериализация
|
||||
- `voice_file.c/h` - Worker-кодирование PCM в контейнер Opus с длительностью и waveform для desktop/Android
|
||||
- `attachment_send.c/h` - Асинхронная подготовка голоса/видео и отправка в зафиксированный канал или DM
|
||||
- `media_async.c/h` - Асинхронный движок (thread-per-task для crypto/file операций)
|
||||
|
||||
**Libraries (lib/)**
|
||||
- `u_async.c/h` - Async event loop (epoll/poll/select, timers via timeout_heap)
|
||||
- `ll_queue.c/h` - Двусвязная очередь одного uasync-потока с callbacks, хеш-индексом и ожиданием свободного места
|
||||
- `memory_pool.c/h` - Пул объектов: ленивое выделение и повторное использование, до 64 свободных объектов в кеше
|
||||
- `ll_queue.c/h` - Lock-free linked list queue with callbacks, hash index, threshold waiter
|
||||
- `memory_pool.c/h` - Fast object pool allocator (pre-allocated blocks)
|
||||
- `debug_config.c/h` - Debug logging system (levels, categories, dual output, runtime config)
|
||||
- `timeout_heap.c/h` - Min-heap for timer management (used by u_async)
|
||||
- `mem.c/h` - Memory wrappers with leak tracking (`u_malloc`/`u_free`/`u_calloc`/`u_strdup`)
|
||||
- `serialize.c/h` - Binary serialization utilities
|
||||
- `reed_solomon.c/h` - Кодирование и восстановление пачки данных Рида—Соломона с настраиваемым числом субблоков и избыточностью
|
||||
- `swm_min.c/h` - Sliding window minimum (for RTT min tracking)
|
||||
- `getmyip.c/h` - Локальный IP, который ОС выберет для UDP к заданному адресу (bind/connect/getsockname)
|
||||
- `getmyip.c/h` - Get local IP / default route detection
|
||||
- `myip.c` - My IP helper (no header)
|
||||
- `socket_compat.c/h` - Cross-platform socket compatibility (Linux/FreeBSD/Windows)
|
||||
- `platform_compat.c/h` - Cross-platform compatibility layer (byte order, time, random)
|
||||
- `radix.c/h` - Radix tree for IP routing lookups
|
||||
- `tcp_io.c/h` - Неблокирующий TCP поверх uasync/ll_queue: backpressure, FIN/close и callbacks
|
||||
- `tcp_io.c/h` - TCP I/O abstraction (Windows IOCP, Linux epoll)
|
||||
- `sqlite3.c/h` - SQLite3 amalgamation (embedded database)
|
||||
- `opus_codec.c/h` - Opus audio codec wrapper
|
||||
- `audio_compressor.c/h` - Audio dynamic range compressor
|
||||
- `speex_aec.c/h` - Эхоподавление SpeexDSP: согласование playback/capture PCM через линию задержки и диагностика её заполнения
|
||||
- `silero_vad.c/h` - Стриминговый VAD через ONNX Runtime: окна 512 отсчётов, 16 кГц моно, вероятность речи и сброс состояния
|
||||
- `silero_vad_model.inc` - Встроенная ONNX-модель Silero VAD для silero_vad_create_default
|
||||
- `json_flat.c/h` - Flat JSON parser
|
||||
- `strbuf.c/h` - Безопасный растущий printf-буфер (замена snprintf-цепочек)
|
||||
- `async_dns.c/h` - Асинхронный (неблокирующий) DNS-резолвер A-записей поверх uasync, адаптер над libdns
|
||||
- `dns.c/h` - Рекурсивный reentrant DNS-резолвер (libdns, MIT)
|
||||
- `miniaudio.h` - Воспроизведение/захват аудио (single-header)
|
||||
- `dr_mp3.h` - MP3-декодер (single-header, на базе minimp3)
|
||||
- `liblmdb/` - Встраиваемый key-value store LMDB
|
||||
- `libopus/` - Встраиваемый кодек Opus
|
||||
- `speexdsp/` - Встроенные исходники эхоканселлера SpeexDSP и KISS FFT
|
||||
- `soundtouch-master/` - SoundTouch: изменение темпа аудио в jitter-буферах звонка и рации
|
||||
- `wintun.h` - Wintun API header for Windows TUN driver
|
||||
|
||||
**Memory Pools in UTUN_INSTANCE:**
|
||||
@@ -516,7 +387,7 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
|
||||
### Key Components
|
||||
- **UASYNC:** One per thread. Async event loop (epoll/poll), timers via timeout_heap
|
||||
- **LL_QUEUE:** Очередь с auto-callback, хеш-индексом и threshold waiter; не средство обмена между потоками
|
||||
- **LL_QUEUE:** Lock-free queue with auto-callback, hash index lookup, threshold waiter
|
||||
- **Memory Pool:** Fast allocation for hot-path objects (packets, inflight entries, fragments)
|
||||
- **ETCP:** TCP-like reliable protocol with encryption, multi-link, load balancing
|
||||
- **Secure Channel:** AES-CCM + X25519 key exchange, nonce-based encryption, pubkey obfuscation
|
||||
@@ -532,8 +403,6 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
- Очереди забивать нельзя. Добавляй следующий элемент только когда очередь стала пустой.
|
||||
- Порог задаётся через `queue_set_threshold(q, max_packets, max_bytes)`.
|
||||
- Используй `queue_waiter_wait` для ожидания освобождения очереди до заданного порога.
|
||||
- Handle ожидания обнуляется целиком, живёт до callback/отмены и отменяется до освобождения владельца.
|
||||
Callback может быть синхронным. `queue_entry_free` не освобождает dgram — его освобождают отдельно.
|
||||
|
||||
### Чтение из очереди
|
||||
- Используй `queue_set_callback`: при вызове callback обработай один или несколько элементов, потом вызови `queue_resume_callback`.
|
||||
@@ -553,13 +422,12 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
## Config Rules
|
||||
- В серверном конфиге только собственные ключи и нет секций `[client]`
|
||||
- В клиентском конфиге есть собственные ключи и pubkey каждого сервера в секции `[client]`
|
||||
- Chat-настройки хранятся в `[chatserver]` (также `[chat]`). Парсер обрабатывает общие поля секции,
|
||||
затем вызывает `chat_setting_set_state(&cfg->global.chat_settings, key, value)`; неизвестные ключи логируются как ошибка. Пример:
|
||||
- Chat-настройки хранятся в секции `[chat]`. Парсер вызывает `chat_setting_set(key, value)` для каждого ключа — неизвестные ключи вызывают ошибку. Пример:
|
||||
```ini
|
||||
[chatserver]
|
||||
[chat]
|
||||
storage_autoload=1
|
||||
storage_unit_size=10M
|
||||
storage_total_size=1G
|
||||
storage_autoload_maxsize_mb=10
|
||||
storage_maxsize_gb=1
|
||||
opus_codec_preset=1
|
||||
compressor_enabled=0
|
||||
compressor_max_gain_db=25
|
||||
@@ -577,8 +445,7 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
|
||||
Если видишь спам-логи - подумай как их выборочно отключить чтобы не мешали.
|
||||
|
||||
Для отладки добавляй в DEBUG_CATEGORY_DEBUG диагностические сообщения где надо. И включи эту категорию в настройках. Убирай только после проверки (путём запуска) когда все ошибки устранены.
|
||||
Явный уровень категории заменяет глобальный. DEBUG_LEVEL_NONE наследует глобальный уровень;
|
||||
DEBUG_LEVEL_DISABLED полностью отключает категорию, в том числе при глобальном TRACE (см. debug_should_output).
|
||||
сообщение выводится по или - либо debug_set_level(DEBUG_LEVEL_TRACE) - выводится ВСЁ независимо от настроек по категориям. Тоесть берется max(global level, category lavel)
|
||||
|
||||
Твой бич - ты постоянно гадаешь и анализируешь код. Что приводит к снежному кобу ошибок и неверных гипотез. 10 раз повторяю - только логи логи логи и никакого гадания. Лоооги!!! правильные логи покажут всё с предельной точностью. Вся суть отладки - информативные логи
|
||||
И смотри логи. не задавливай их grep-ом. лучше больше. единственное с чем борись - это бесполезные спам логи.
|
||||
@@ -588,8 +455,7 @@ DEBUG_LEVEL_DISABLED полностью отключает категорию,
|
||||
|
||||
Но полезные смотри всегда, и всегда оставляй логи которые выводятся нечасто.
|
||||
Частые логи - это трафик которые >100 раз повторяются. Но которые мало раз обязательно оставлять и выводить.
|
||||
В тесте можно временно поднять нужные категории до TRACE, сохранив прежние уровни и восстановив их после участка.
|
||||
Одного debug_set_level(DEBUG_LEVEL_NONE) недостаточно для отключения категорий с явно заданным уровнем.
|
||||
можешь в тесте включить логи в нужный интервал чтобы не спамить. debug_set_level(DEBUG_LEVEL_TRACE) и выключить debug_set_level(DEBUG_LEVEL_NONE)
|
||||
Если видишь проблему и ее решение неочевидно то выстраивай диагностику вокруг неё пока не будет очевидно где и что происходит не так. Это базовое и обязательное требование к отладке.
|
||||
Подробные логи ты должен выводить не обрезая всегда и анализировать. Если лог большой - выведи в файл и анализируй файл.
|
||||
Рассуждение должно быть примерно таким:
|
||||
@@ -631,7 +497,7 @@ DEBUG_LEVEL_DISABLED полностью отключает категорию,
|
||||
- Нельзя ничего восстанавливать из репозитория не спрашивая
|
||||
- Для отладки не printf а DEBUG_*
|
||||
- Запрещено прямое включение arpa/inet.h, sys/time.h, unistd.h — использовать ../lib/platform_compat.h + socket_compat.h
|
||||
- Меняй режим и флаги сборки через `./build.sh`: он отслеживает конфигурацию и очищает объекты только при необходимости.
|
||||
- Перед сборкой всегда make clean
|
||||
- Все лишнее что менял при отладке - строго вернуть назад в состояние до вмешательства
|
||||
|
||||
## Написание кода
|
||||
@@ -640,17 +506,12 @@ DEBUG_LEVEL_DISABLED полностью отключает категорию,
|
||||
- Обязательно наличие подробной диагностики в функциях кода которые не сильно спамят (не часто вызываются).
|
||||
- Для каждого отладочного вывода подумай какая из доступной информация будет полезна чтобы можно было наиболее завершенно оценить состояние алгоритма и состояний влияющих на алгоритм.
|
||||
|
||||
## Оформление кода
|
||||
- в .c: перед каждой функцией - краткое но понятное описание что делает функция (кроме совсем простых)
|
||||
- в .h: в начале - описание модуля - для чего предназначен, как пользоваться, нюансы. Далее структуры с комментариями, далее публичные функции с описаниями (что делает, как пользоваться, какие аргументы, возврат, нюансы работы)
|
||||
|
||||
## chatgui (GUI Chat Client)
|
||||
|
||||
Chatgui (бинарник `vibechat`) — десктопный чат на Qt 6 (Qt 5 fallback), отдельная CMake-сборка общего ядра.
|
||||
Корневой `build.sh` дополнительно вызывает её, если уже существует `tools/chatgui/build/CMakeCache.txt`.
|
||||
Chatgui — десктопный GUI-чат на Qt 6 (Qt 5 fallback), отдельный проект внутри репозитория. Не связан с autotools-сборкой utun.
|
||||
|
||||
GUI работает в Qt-потоке, ядро и чат — в uasync-потоке. Команды передаются через
|
||||
`gui_bridge_post_uasync()` / `uasync_post()`, события возвращаются через Qt-сигналы.
|
||||
В chat gui интегрированы библиотеки utun. Чат и библиотеки работают в разных потоках. Поэтому нужно использовать семафоры, сокеты или другие механизмы синхронизации (uasync_post, uasync_memsync, uasync_get_wakeup_fd)
|
||||
|
||||
База данных sqlite в chatgui: можно писать (update) из потока utun / instance. можно только читать из gui потока.
|
||||
|
||||
@@ -658,37 +519,37 @@ GUI работает в Qt-потоке, ядро и чат — в uasync-пот
|
||||
- **Язык:** C++20
|
||||
- **Фреймворк:** Qt 6 (предпочитаемый) или Qt 5.15+ (Widgets + Network)
|
||||
- **Сборка:** CMake 3.16+
|
||||
- **БД:** SQLite3 (WAL mode, компилируется общий `lib/sqlite3.c`)
|
||||
- **БД:** SQLite3 (WAL mode, встроенный `db/sqlite3.c` amalgamation)
|
||||
- **Анимации:** rlottie (C API) + zlib (gzip-декомпрессия TGS)
|
||||
|
||||
### Сборка
|
||||
```bash
|
||||
# Установка зависимостей
|
||||
sudo apt install librlottie-dev zlib1g-dev qt6-base-dev libssl-dev libx11-dev libxfixes-dev libpipewire-0.3-dev
|
||||
sudo apt install librlottie-dev zlib1g-dev qt6-base-dev
|
||||
# или qtbase5-dev для Qt 5 fallback
|
||||
|
||||
# Сборка
|
||||
cd tools/chatgui && mkdir -p build && cd build
|
||||
cmake .. && make -j4
|
||||
./vibechat
|
||||
./chatgui
|
||||
```
|
||||
|
||||
### Конфиг и логи чатгуи
|
||||
- **Конфиг:** `vibechat.cfg` рядом с бинарником (обычно `tools/chatgui/build/vibechat.cfg`, содержит приватные ключи)
|
||||
- **Конфиг:** `tools/chatgui/build/chatgui.cfg` (в `.gitignore`, содержит приватные ключи)
|
||||
- Секция `[gui]`: `debug_file`, `debug_level`, `debug_categories=cat=level,...`
|
||||
- Секция `[control]`: `ip`, `port` для подключения etcpmon
|
||||
- Секция `[ntp]`: синхронизация времени
|
||||
- **Лог:** `tools/chatgui/build/chatgui.log` (путь задаётся в конфиге `debug_file`)
|
||||
- **БД чата:** `chats.db` в каталоге `[gui] db_path` (по умолчанию `chat_data/` рядом с бинарником)
|
||||
- **БД чата:** `tools/chatgui/build/chat_data/` (SQLite, путь из `db_path` в конфиге)
|
||||
|
||||
### Структура файлов (src/)
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| `main.cpp` | Точка входа, инициализация QApplication |
|
||||
| `mainwindow.h/cpp` | Главное окно, списки каналов/DM/участников, сообщения, трей, события ядра и окна звонков |
|
||||
| `mainwindow.h/cpp` | Главное окно, QSplitter (ChannelList \| MessageList \| AccountList), трей |
|
||||
| `chatview.h/cpp` | QListView с фоновым изображением, hover-сигнал, drag-to-select |
|
||||
| `messagelist.h/cpp` | Модель сообщений из БД, обновления доставки/медиа, анимации, состояние просмотра канала |
|
||||
| `messagelist.h/cpp` | QStandardItemModel, stub-данные, инициализация анимаций, hover→activate |
|
||||
| `messagedelegate.h/cpp` | QStyledItemDelegate: бабблы, текст, цитаты, реакции, оверлей анимаций |
|
||||
| `emoji.h/cpp` | EmojiData/EmojiCategory/EmojiType, 4 категории, `g_animatedEmojiMap` |
|
||||
| `emojipanel.h/cpp` | Эмодзи-пикер: QTabWidget + QGridLayout, EmojiButton с hover-анимацией |
|
||||
@@ -697,70 +558,20 @@ cmake .. && make -j4
|
||||
| `lottieicon.h/cpp` | Загрузка TGS (gzip→zlib), рендер кадров через `lottie_animation_render` |
|
||||
| `animtimer.h/cpp` | Синглтон-таймер 30fps, refcount activate/deactivate, сигнал ticked |
|
||||
| `channellist.h/cpp` `channeldelegate.h/cpp` | Список каналов с аватарами и превью |
|
||||
| `accountlist.h/cpp` `accountdelegate.h/cpp` | Список аккаунтов/пользователей и его делегат отрисовки |
|
||||
| `memberlistmodel.h/cpp` | Модель списка участников канала |
|
||||
| `memberpropsdialog.h/cpp` | Диалог свойств участника |
|
||||
| `flagpainter.h/cpp` | Отрисовка иконок мембера (admin/mod/supernode/storage) |
|
||||
| `accountlist.h/cpp` | Список аккаунтов/пользователей |
|
||||
| `joindialog.h/cpp` | Диалог присоединения к группе/каналу |
|
||||
| `invitedialog.h/cpp` | Диалог приглашения пользователя |
|
||||
| `invite_link.h/cpp` | Логика invite-ссылок |
|
||||
| `inviteby.h/cpp` | Диалог приглашения по invite-ссылке (отправка через узел) |
|
||||
| `settingsdialog.h/cpp` | Окно настроек |
|
||||
| `creategroupdialog.h/cpp` | Диалог создания группы |
|
||||
| `channelsettingsdialog.h/cpp` | Диалог настроек канала |
|
||||
| `renamedialog.h/cpp` | Диалог переименования |
|
||||
| `networksettingspage.h/cpp` | Страница сетевых настроек |
|
||||
| `nodespage.h/cpp` | Таблица узлов + дамп в настройках |
|
||||
| `statuspage.h/cpp` | Страница статуса (NTP + соединения + живые часы) |
|
||||
| `databasesettingspage.h/cpp` | Страница обслуживания БД в настройках |
|
||||
| `storagesettingspage.h/cpp` | Страница настроек хранилища |
|
||||
| `soundsettingspage.h/cpp` | Страница настроек звука |
|
||||
| `audiodevicesettingspage.h/cpp` | Страница настроек аудиоустройств |
|
||||
| `sound_manager.h/cpp` | Общий miniaudio-контекст, уведомления/PCM, микширование голоса, маршруты устройств и восстановление |
|
||||
| `audio_device.h/cpp` | Жизненный цикл miniaudio-устройства, прогресс callbacks, пауза и повтор запуска после сбоя |
|
||||
| `audio_event_queue.h` | Ограниченная очередь PCM/команд с несколькими producers и резервом для управляющих событий |
|
||||
| `audio_frame_ring.h` | Предвыделенное SPSC-кольцо кадров переменной длины под mutex; при переполнении удаляет старейший |
|
||||
| `voice_audio_io.h/cpp` | Workers захвата/AEC и воспроизведения; упорядочивание PCM/PTT/mute и итоговый render-reference |
|
||||
| `audiorecorder.h/cpp` | Захват голосовых сообщений, worker обработки PCM, компрессор, индикатор уровня и передача записи задаче |
|
||||
| `voicemessageencoder.h/cpp` | Выбор Opus preset и создание медиа-каталога; кодирование выполняет общий voice_file |
|
||||
| `voiceplayback.h/cpp` | Воспроизведение голосовых сообщений |
|
||||
| `callwindow.h/cpp` | Окно звонка: состояния, управление, путь соединения и статистика аудио |
|
||||
| `call_audio_engine.h/cpp` | Desktop-контур звонка: захват, VoiceAudioIo, общий выход SoundManager и завершение аудиосессии |
|
||||
| `radio_audio_engine.h/cpp` | Desktop-контур рации: независимые захват/прослушивание, PTT/VAD и подключение к общему выходу |
|
||||
| `radiopanel.h/cpp` | Панель активной рации: выбор аудиоустройств, VAD, PTT и состояние передачи из ядра |
|
||||
| `radiosettingspage.h/cpp` | Настройки рации и запись глобальных сочетаний PTT |
|
||||
| `ptt_hotkey_manager.h/cpp` | Глобальный PTT: физические клавиши Windows/X11, системный портал Wayland |
|
||||
| `ptt_key_chord.h` | Хранение физических сочетаний с отдельными левыми/правыми модификаторами и запись одновременного нажатия |
|
||||
| `ptt_portal.h/cpp` | D-Bus-сессия GlobalShortcuts для Wayland, настройка сочетаний и обработка нажатий/отпусканий |
|
||||
| `media_blocks.h/cpp` | Чтение/сборка фрагментированных медиафайлов |
|
||||
| `imageviewer_window.h/cpp` | Окно просмотра изображений |
|
||||
| `videoplayer_engine.h/cpp` | Движок декодирования видео |
|
||||
| `videoplayer_window.h/cpp` | Окно просмотра видео |
|
||||
| `connmonitorwindow.h/cpp` | Окно мониторинга ETCP-соединений (standalone) |
|
||||
| `qrcode_utils.h/cpp` | Утилиты для QR-кодов |
|
||||
| `debug_ui.h` | Отладочный UI |
|
||||
|
||||
### Другие поддиректории
|
||||
|
||||
- `db/` — `db_manager.h/cpp`: доступ GUI к данным nodes, channels, msg_<channel>, peers_<channel> и состоянию интерфейса;
|
||||
копия `db/sqlite3.c/h` не используется текущей CMake-целью, SQLite берётся из `lib/`
|
||||
- `transport/` — интеграция GUI с ядром; файлы перечислены ниже
|
||||
- `libutun/CMakeLists.txt` — состав и зависимости CMake-сборки общего ядра из `src/` и `lib/`
|
||||
- `CMakeLists.txt` — приложение vibechat, Qt-зависимости, ресурсы и цели GUI-тестов
|
||||
- `tests/` — проверки аудиоустройств и восстановления, голосового тракта, jitter-буферов, PTT, кодирования и GUI-компонентов
|
||||
- `db/` — SQLite3 amalgamation (`sqlite3.c/h`), `db_manager.h/cpp` (схема: nodes, node_addresses, channels, msg_<channel>, peers_<channel>, local_identity, accounts, ui_state)
|
||||
- `transport/` — интеграция с uTun: `gui_bridge.h/gui_bridge_impl.cpp` (Qt signal-based обмен), `chat_core.h/c` (управление), `chat_sync.h/c` (синхронизация), `db_sync_stub.c` (заглушка БД), `node_config.h/cpp`, `config_updater.h/cpp`, `utun_node.h/cpp`, `topo_node_sqlite.h/c`
|
||||
- `resources/` — `bg.jpg`, `chatgui.qrc` (встраивает 5 TGS в бинарник), `animations/*.tgs`
|
||||
- `zxing-cpp/` — встроенная библиотека QR-кодов; `third_party/qhotkey/` — исходники QHotkey
|
||||
|
||||
### Интеграция с ядром (transport/)
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| `gui_bridge.h` / `gui_bridge_impl.cpp` | Команды GUI → uasync и события ядра → Qt-сигналы |
|
||||
| `utun_node.h/cpp` | Запуск и остановка ядра/чата в выделенном uasync-потоке |
|
||||
| `node_config.h/cpp` | Загрузка и сохранение конфигурации узла |
|
||||
| `config_updater.h/cpp` | Изменение настроек в конфиге |
|
||||
| `miniaudio_impl.c` | Реализация single-header miniaudio в отдельной единице трансляции |
|
||||
| `audio_diagnostics.h/cpp` | Диагностика этапов аудио и событий backend; записывает метаданные без PCM |
|
||||
|
||||
### Система анимированных эмодзи
|
||||
|
||||
@@ -854,70 +665,29 @@ void lottie_animation_destroy(Lottie_Animation *anim);
|
||||
---
|
||||
|
||||
## Key Documentation Files
|
||||
|
||||
- `doc/service_lifecycle.md` — ядро, независимые сервисы, владение ресурсами и остановка
|
||||
- `doc/node_snapshot.md` — подписанная запись узла, timestamp, SQLite и обновление NCD
|
||||
- `doc/etcp_protocol.txt` — ETCP: кодограммы, ACK, handshake, keepalive
|
||||
- `doc/etcp_arch.md` — архитектура ETCP
|
||||
- `doc/etcp_router_arch.md` — архитектура маршрутизатора
|
||||
- `src/chat/chat_join.h` — нормативный протокол добавления участника
|
||||
- `src/routing_layer/topo_group.h` — протокол групповой сессии и критерий READY
|
||||
- `tests/test_chat_join_e2e.c`, `tests/test_topo_recovery.c`, `tests/test_services.c` — проверки join, recovery и жизненного цикла
|
||||
- `doc/dm_arch.md`, `doc/chat_call_arch.md` — личные сообщения и звонки
|
||||
- `doc/proxy_protocol.md` — TCP/UDP/ICMP-прокси, управление окнами, half-close и проверки
|
||||
- `doc/lwip_nuances.md` — особенности адаптированного lwIP TCP-стека
|
||||
- `doc/linux_audio_recovery.md` — диагностика аудиотракта Linux, контракты владения и восстановление устройств
|
||||
- `src/media_delivery/file_transfer.h`, `src/dm/dm_media.h` — контракты передачи файлов и E2E-вложений DM
|
||||
- `src/call/call_audio.h`, `src/radio/radio_audio.h` — API общего голосового стека и правила потоков
|
||||
- `tools/chatcli_commands.txt` — команды headless CLI, в том числе DM, звонки и рация
|
||||
|
||||
## Список задач по проекту
|
||||
|
||||
Список задач — `doc/tasks.md` относительно корня репозитория (текущая работа, берём по одной задаче сверху).
|
||||
При нахождении попутных багов/fail/флаков тестов добавляем задачу (либо фиксим сразу если баг простой).
|
||||
Как сделали - помечаем [+] выполнено.
|
||||
- `/doc/etcp_protocol.txt` - ETCP протокол (формат кодограмм, ACK, handshake, keepalive)
|
||||
- `/doc/etcp_arch.md` - ETCP архитектура
|
||||
- `/doc/etcp_config.txt` - Конфигурация ETCP
|
||||
- `/doc/etcp_router_arch.md` - ETCP роутер архитектура
|
||||
- `/doc/route_p2pconn.txt` - Route peer-to-peer соединения
|
||||
- `/src/route_bgp.txt` - BGP обмен маршрутами (дизайн-документ)
|
||||
|
||||
## chatgui-android (Android P2P Chat)
|
||||
|
||||
**Расположение:** `tools/chatgui-android/`
|
||||
|
||||
Android-версия чатгуи — P2P чат на общем ETCP/STCP-ядре, UI на Jetpack Compose (Kotlin).
|
||||
C-библиотека `utun_lite` собирает `lib/` и `src/` по `libutun_lite/utun_sources.cmake`;
|
||||
`instance_lite` запускает ядро и чат в отдельном потоке, без UTUN-сервиса.
|
||||
Android-версия чатгуи — P2P чат на STCP (TCP), UI на Jetpack Compose (Kotlin).
|
||||
C-ядро: `libutun_lite` (выборочная компиляция нужных .c из `lib/` и `src/`).
|
||||
Сборка: CMake (headless, Linux) + Gradle/NDK (Android APK).
|
||||
- 'fw' - собрать и обновить chatgui-android на всех подключенных телефонах (clean + сборка + install)
|
||||
- 'fw' - собрать и обновить chatgui-android на телефоне (clean + сборка + install)
|
||||
|
||||
**Подробная инструкция:** `tools/chatgui-android/AGENTS.md`
|
||||
**Chat-модули:** все файлы из `src/chat/` (описаны выше в секции «Chat») компилируются в `libutun_lite`.
|
||||
|
||||
### Основные файлы native-слоя
|
||||
|
||||
Пути относительны к `tools/chatgui-android/`; UI и Android-сервисы описаны в локальном `AGENTS.md`.
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| `libutun_lite/utun_sources.cmake` | Общий список исходников ядра для Android и headless |
|
||||
| `libutun_lite/CMakeLists.txt` | Сборка native-библиотеки и зависимостей |
|
||||
| `libutun_lite/instance_lite.c/h` | Запуск ядра/чата по INI-тексту и управление отдельным uasync-потоком |
|
||||
| `libutun_lite/utun_config_api.c/h` | Провайдер конфигурации: callbacks Kotlin и значения для native-слоя |
|
||||
| `libutun_lite/standby.c/h` | Фоновый duty-cycle, таймеры активных интервалов и ожидание пробуждения |
|
||||
| `libutun_lite/invite_link_c.c/h` | Старая копия C-кодека; .c исключён из UTUN_SOURCES, рабочий код — src/chat/invite_link.c/h |
|
||||
| `libutun_lite/attachment_sender.c/h` | Отправка файлов в канал через chat_msg_submit и канальный медиа-индекс |
|
||||
| `libutun_lite/photo_sender.c/h` | Отправка фотографий в канал с MIME-типом и размерами изображения |
|
||||
| `libutun_lite/video_sender.c/h` | Отправка видео через общий механизм подготовки вложений |
|
||||
| `libutun_lite/voice_recorder.c/h` | Накопление PCM записи, компрессор и отправка голоса через общий attachment_send |
|
||||
| `jni_bridge/android_jni_bridge.c/h` | JNI-команды и callbacks между Kotlin и C-ядром, события и аудио API |
|
||||
| `jni_bridge/android_udp_log.c/h` | Потокобезопасный буфер UDP-логов с flush-таймером в потоке ядра |
|
||||
| `headless/headless_main.c` | Отдельный каркас control-loop; сам не запускает ядро/чат |
|
||||
| `headless/headless_control.c/h` | Каркас control-сокета; часть команд — заглушки, рабочий API — src/chat/chat_headless_control.c/h |
|
||||
| `app/src/main/cpp/CMakeLists.txt` | NDK-сборка библиотеки приложения |
|
||||
| `libutun_lite/tests/test_standby.c` | Проверка duty-cycle и жизненного цикла standby |
|
||||
|
||||
## Runtime
|
||||
- После autotools-сборки бинарник — `src/utun`. Запуск из корня с TUN: `sudo ./src/utun -f -c utun.cfg`.
|
||||
- Старый `utun_start.sh` ожидает `./utun` в корне. `utun_stop.sh` делает `killall -9 utun`, а не штатную остановку одного экземпляра.
|
||||
- `-f` оставляет процесс на переднем плане; остановка — SIGINT/SIGTERM. Пути конфигурации, PID и лога задаются через `-c`, `-p`, `-l`.
|
||||
- `utun_start.sh` перенаправляет stdout в `utun.log`, stderr в `utun_err.log`.
|
||||
- Запуск utun от root (для tun): `/home/vnc1/proj/utun3/utun_start.sh`
|
||||
- Стоп utun: `sudo /home/vnc1/proj/utun3/utun_stop1.sh`
|
||||
- Логи: `utun.log` (stdout), `utun_err.log` (stderr)
|
||||
- Тестовые логи: `tests/logs/`
|
||||
|
||||
## Git Conventions
|
||||
@@ -928,19 +698,19 @@ C-библиотека `utun_lite` собирает `lib/` и `src/` по `libut
|
||||
|
||||
## Quick Start for New Features
|
||||
1. Add new source file to `src/Makefile.am` under `utun_SOURCES`
|
||||
2. Add test target and its sources/dependencies to `tests/Makefile.am` under `check_PROGRAMS`
|
||||
2. Add test file to `tests/Makefile.am` under `check_PROGRAMS`
|
||||
3. Use existing patterns from similar modules
|
||||
4. Run `./check.sh` after changes
|
||||
5. Run `./check.sh` after changes
|
||||
5. Commit with descriptive message in appropriate language
|
||||
|
||||
## chatgui: отправка сообщений через xdotool
|
||||
|
||||
1. **Запуск:** из каталога сборки `setsid env QT_ACCESSIBILITY=1 ./vibechat & disown` с DISPLAY текущей X11-сессии.
|
||||
2. **Окно:** найти актуальный ID через `xdotool search --onlyvisible --class vibechat`; ID и размеры окна меняются между запусками.
|
||||
3. **Фокус:** активировать найденное окно и выбрать InputBar; координаты определять по текущему расположению интерфейса.
|
||||
4. **Отправка:** `xdotool type --window WID "text"` + `xdotool key --window WID Return`, где WID заменён найденным ID.
|
||||
5. **Проверка:** через `chats.db` из каталога `db_path`; таблицы сообщений называются `msg_<числовой channel_id>`.
|
||||
6. Доступность UI-автоматизации зависит от текущей X11/Wayland-сессии и настроек Qt accessibility; старые ID окон и DISPLAY не переносить.
|
||||
1. **Запуск:** `setsid env DISPLAY=:1.0 QT_ACCESSIBILITY=1 ./chatgui & disown` (иначе SIGTERM убьёт процесс при таймауте bash)
|
||||
2. **Окно:** `0x4600006 "Chat"` (WM_CLASS "chatgui"), 900×600. Не путать с `0x4800001` (10×10, временное)
|
||||
3. **Фокус:** клик в область InputBar (x=400, y=570 относительно окна) перед вводом
|
||||
4. **Отправка:** `xdotool type --window WID "text"` + `xdotool key --window WID Return`
|
||||
5. **Проверка:** `sqlite3 chats.db "SELECT ... FROM msg_ch_GENERAL"` — сообщения в per-channel таблицах
|
||||
6. **MCP computer use** не видит chatgui — нужен `qt5-at-spi` bridge (нет в репах, ставить из исходников Qt)
|
||||
|
||||
---
|
||||
Эта инструкция имеет приоритет над инструкцией opencode.
|
||||
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, 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
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If 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 convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU 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
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
@@ -1,39 +0,0 @@
|
||||
uTun licensing
|
||||
==============
|
||||
|
||||
The original uTun source code, including its command-line, desktop and Android
|
||||
applications, is licensed under the GNU General Public License, version 3 only
|
||||
(GPL-3.0-only), with the additional permission for Wintun below.
|
||||
|
||||
uTun is free software: you can redistribute it and/or modify it under the terms
|
||||
of the GNU General Public License as published by the Free Software Foundation,
|
||||
version 3 of the License.
|
||||
|
||||
uTun 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 General Public License for more details.
|
||||
|
||||
The complete GNU GPL version 3 is provided in LICENSE. Copyright remains with
|
||||
the respective authors. Third-party code and binaries retain their own licenses
|
||||
and copyright notices; see THIRD_PARTY_NOTICES and the notices in their files.
|
||||
|
||||
Additional permission under GNU GPL version 3 section 7
|
||||
------------------------------------------------------
|
||||
|
||||
If you modify this Program, or any covered work, by linking or combining it with
|
||||
Wintun (or a modified version of that library), containing parts covered by the
|
||||
terms of the Wintun Prebuilt Binaries License, the licensors of this Program
|
||||
grant you additional permission to convey the resulting work.
|
||||
|
||||
This permission applies to the original uTun code. It does not change the
|
||||
license of Wintun or grant rights to modify its prebuilt binaries. Wintun must
|
||||
be used and distributed in accordance with its own license, through its
|
||||
Permitted API. No exception is granted for any other library.
|
||||
|
||||
Distribution
|
||||
------------
|
||||
|
||||
Include LICENSE, LICENSING and THIRD_PARTY_NOTICES with source and binary
|
||||
distributions. For GPL-covered binaries, also provide Corresponding Source
|
||||
as required by GNU GPL version 3; including these documents does not replace
|
||||
that requirement. Preserve upstream notices when updating bundled libraries.
|
||||
-16
@@ -3,22 +3,6 @@
|
||||
# Order matters: lib -> src -> tests (tests depend on src objects)
|
||||
SUBDIRS = lib lib/libopus src tests
|
||||
|
||||
# Полный комплект лицензий устанавливается вместе с бинарником и входит в make dist.
|
||||
licensedir = $(datadir)/licenses/utun
|
||||
dist_license_DATA = LICENSE LICENSING THIRD_PARTY_NOTICES
|
||||
|
||||
EXTRA_DIST = readme.md \
|
||||
doc/nat_detection.txt \
|
||||
doc/stun_nat.md \
|
||||
doc/node_snapshot.md \
|
||||
tinycrypt/LICENSE \
|
||||
tools/chatgui/zxing-cpp/LICENSE \
|
||||
tools/chatgui/zxing-cpp/core/src/libzueci/LICENSE \
|
||||
tools/chatgui/third_party/qhotkey/LICENSE \
|
||||
tools/chatgui-android/app/src/main/cpp/onnxruntime/LICENSE \
|
||||
tools/chatgui-android/app/src/main/cpp/onnxruntime/ThirdPartyNotices.txt \
|
||||
tools/chatgui-android/app/src/main/cpp/openssl/LICENSE.txt
|
||||
|
||||
# Clean build directory
|
||||
clean-local:
|
||||
-rm -rf build/
|
||||
@@ -1,149 +0,0 @@
|
||||
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](src/routing_layer/etcp_router_doc.md).
|
||||
|
||||
# План: эпоха (reset_id) для ETCP Router
|
||||
|
||||
Аналог фикса `reset_id` из ETCP (см. `61c572c`), применённый к сервисному слою
|
||||
маршрутизации `etcp_router` (src/routing_layer/etcp_router.c).
|
||||
|
||||
## 1. Контекст
|
||||
|
||||
`etcp_router` — «упрощённый TCP поверх ETCP»: своя seq-нумерация (`tx_seq`/`rx_seq`),
|
||||
reorder (`recv_q`), inflight-контроль (`inflight_q`), ретрансмиты, START/RST handshake.
|
||||
|
||||
Текущий механизм детекции рестарта пира:
|
||||
|
||||
- `SVC_ROUTE_HDR.flags & ROUTER_FLAG_START` — флаг на первом data-пакете сеанса
|
||||
(`seq==0 && !start_sent && pl_len>0`, `etcp_router.c:207`).
|
||||
- `router_check_peer_restart` (`etcp_router.c:1055`) — если пришёл START на уже
|
||||
синхронизированное соединение (`peer_sync_done==1`), то это рестарт пира;
|
||||
**дедуп ретрансмитов** — побайтовое сравнение с `last_start_dgram`
|
||||
(`etcp_router.c:1063`). Совпало → это ретрансмит (не рестарт). Не совпало →
|
||||
`etcp_router_conn_restart` (полный сброс + `sess_id++`).
|
||||
- `ROUTER_FLAG_RST` + `router_restart_send` (`etcp_router.c:285`) — сброс только
|
||||
TX-стороны; повторные RST гасятся через `send_restart_pending`.
|
||||
- `sess_id` (`etcp_router.h:121`) — **локальный счётчик переустановок, на wire
|
||||
не пишется**. Используется только в `DEBUG_INFO`.
|
||||
|
||||
## 2. Проблема (аналог ETCP seq-коллизии)
|
||||
|
||||
Дедуп рестарта строится на **побайтовой идентичности** `last_start_dgram`. Это тот
|
||||
же класс хрупкости, что убрали в ETCP:
|
||||
|
||||
1. **Байт-идентичный START** может быть и ретрансмитом, и (теоретически) новым
|
||||
сеансом — если у нового START совпали `seq==0`, `svc_id`, `timestamp` (16-битные
|
||||
часы 0.1ms заворачиваются каждые ~6.5 сек) и payload. Тогда рестарт **не
|
||||
детектится**.
|
||||
2. Если рестарт не распознан: у пира `tx_seq` ушёл в 0, а у нас `rx_seq` остался
|
||||
«высоким». В `router_handle_data_packet` (`etcp_router.c:1104`) ветка
|
||||
`(rx_seq - seq) > 0` / `queue_find_data_by_index` считает пакеты новой сессии
|
||||
**дубликатами** → молчаливый `c_dup_dropped` + пере-ACK (как `RX dup` в ETCP) →
|
||||
потеря сервисных пакетов.
|
||||
3. `sess_id` не на wire → пир вообще не может отличить «мы перезапустились» от
|
||||
«продолжаем сеанс» иначе как по байтам START.
|
||||
|
||||
То есть нужен надёжный маркер **эпохи сеанса** на wire — аналог `reset_id`.
|
||||
|
||||
## 3. Решение (аналог reset_id)
|
||||
|
||||
Семантика та же, что в ETCP:
|
||||
|
||||
- эпоха генерится **один раз** при создании `ETCP_ROUTER_CONN`;
|
||||
- меняется **только** при (фатальном) рестарте соединения;
|
||||
- при приёме START/RST с **другой** эпохой → рестарт (adopt);
|
||||
- **первая** увиденная эпоха принимается без рестарта (свежие при создании);
|
||||
- дедуп ретрансмитов START — по эпохе, а не по байтам.
|
||||
|
||||
Два варианта источника эпохи:
|
||||
|
||||
- **(A) Перенести `sess_id` на wire** — он уже есть как `uint32` счётчик
|
||||
(0 при создании, `++` при рестарте). Минимально инвазивно.
|
||||
- **(B) Новый `reset_id` (uint64, случайный)** — симметрично ETCP; случайный
|
||||
надёжнее счётчика (нет wrap-around и предсказуемости), но чуть больше поле.
|
||||
|
||||
Рекомендация — **(A)** как минимальный шаг (поле уже есть), при желании апгрейд
|
||||
до (B).
|
||||
|
||||
### Разрыв цикла (master/slave)
|
||||
|
||||
Как и в ETCP, симметричное «увидел другую эпоху → рестарт» зацикливается при
|
||||
одновременном рестарте обеих сторон (A++ → B++ → A видит B → A++ → …).
|
||||
|
||||
Ти-брейкер по `node_id` (как в ETCP):
|
||||
|
||||
- на приёме START/RST с чужой эпохой:
|
||||
- **master** (меньший `node_id`) — держит свою эпоху, шлёт RST пиру;
|
||||
- **slave** (больший `node_id`) — принимает эпоху пира и рестартится.
|
||||
|
||||
Существующие предохранители (`send_restart_pending`, `router_check_peer_restart`
|
||||
на стороне master) сохраняются.
|
||||
|
||||
## 4. Wire-формат
|
||||
|
||||
`SVC_ROUTE_HDR` (`etcp_router.h:26`) сейчас 33 байта:
|
||||
|
||||
```
|
||||
cmd(1) group_id(8) dst(8) src(8) seq(4) svc_id(1) flags(1) timestamp(2)
|
||||
```
|
||||
|
||||
Добавить поле эпохи (только START/RST, в обычных data/ACK — 0):
|
||||
|
||||
- вариант (A): `uint32_t sess_id` → заголовок 33→37 байт (упакован);
|
||||
- вариант (B): `uint8_t reset_id[8]` → 33→41 байт.
|
||||
|
||||
Заполняется в `router_build_packet` (START) и `router_send_rst`; читается в
|
||||
`router_check_peer_restart` / обработчике RST.
|
||||
|
||||
## 5. Шаги реализации
|
||||
|
||||
Файлы: `src/routing_layer/etcp_router.h`, `src/routing_layer/etcp_router.c`,
|
||||
(опц.) `tests/`.
|
||||
|
||||
1. **Поле в заголовке**: добавить `sess_id`/`reset_id` в `struct SVC_ROUTE_HDR`;
|
||||
обновить `SVC_ROUTE_HDR_SIZE`.
|
||||
2. **Локальное состояние** (`struct ETCP_ROUTER_CONN`):
|
||||
- `uint32_t sess_id` (уже есть) — оставить как «моя эпоха»;
|
||||
- добавить `uint32_t peer_sess_id` — последняя увиденная эпоха пира.
|
||||
- `router_conn_reset` (`etcp_router.c:380`) НЕ сбрасывает `sess_id`
|
||||
(комментарий на :379 уже это гарантирует).
|
||||
3. **Запись эпохи**:
|
||||
- `router_build_packet` (`:193`) — при `ROUTER_FLAG_START` писать `sess_id`;
|
||||
- `router_send_rst` (`:251`) — писать `sess_id`.
|
||||
4. **Детект рестарта по эпохе** — переписать `router_check_peer_restart` (`:1055`):
|
||||
- если `peer_sess_id==0` → первая эпоха, принять без рестарта (сохранить);
|
||||
- если `hdr->sess_id == peer_sess_id` → ретрансмит (не рестарт);
|
||||
- если отличается → master/slave: slave → `etcp_router_conn_restart` + adopt;
|
||||
master → держать свою, при необходимости RST.
|
||||
- `last_start_dgram`-байт-дедуп заменить на это сравнение (поле можно удалить).
|
||||
5. **RST-обработчик** (`:1151`) — при RST также сверять `sess_id` (дедуп RST по
|
||||
эпохе вместо/в дополнение к `send_restart_pending`).
|
||||
6. **Инициализация** — `peer_sess_id = 0` в `router_conn_reset`/`etcp_router_conn_get`.
|
||||
|
||||
## 6. Тест
|
||||
|
||||
`tests/test_etcp_router_*` (существующие `test_etcp_router`, `test_etcp_router_reconnect`,
|
||||
`test_etcp_router_unit`) — прогнать после правки заголовка (37/41 байт).
|
||||
|
||||
Добавить (по возможности) регрессию, аналогичную `test_etcp_seq_collision`:
|
||||
|
||||
- два инстанса, роутер-коннект, отправка N пакетов;
|
||||
- принудительный рестарт одной стороны (смена эпохи);
|
||||
- assert: пакеты новой эпохи доставлены (не `c_dup_dropped`), обе стороны сходятся
|
||||
к одной эпохе.
|
||||
|
||||
## 7. Решения (зафиксировано)
|
||||
|
||||
1. **Вариант (B)** — `uint64_t reset_id`, случайный (`random_bytes`). Причина: вариант (A)
|
||||
`sess_id` стартует с 0 и конфликтует с сентинелом «эпоха не задана». На wire — native
|
||||
order (как остальные uint64-поля заголовка, SVC_ROUTE не использует htobe*).
|
||||
2. **master/slave по node_id** — нужен: `etcp_router_conn_restart` делает полный
|
||||
двунаправленный сброс, без ти-брейкера симметричный рестарт зациклится. master
|
||||
(меньший node_id) держит эпоху и шлёт RST; slave принимает и ресетится.
|
||||
3. **route_crypto** — совместим: заголовок не шифруется (только payload), подписывается
|
||||
только в SIGN-режиме; `SVC_ROUTE_HDR_SIZE = sizeof(...)` авто-адаптируется. START
|
||||
идёт через `route_crypto_encode`, RST/ACK/CLOSE — plain header-only.
|
||||
|
||||
## 8. Статус
|
||||
|
||||
Реализовано и проверено: `etcp_router.h/.c` + `tests/test_etcp_router_unit.c`.
|
||||
Сборка `make clean && make` без ошибок; `./check.sh` → 74 passed / 0 failed / 1 skipped;
|
||||
`test_etcp_router_unit` 34/34, `test_etcp_router` и `test_etcp_router_reconnect` — PASS.
|
||||
-11786
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,4 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions
|
||||
echo uTun Build Script for MSYS2 UCRT64
|
||||
echo.
|
||||
|
||||
@@ -11,11 +10,11 @@ rem export https_proxy="http://192.168.29.1:9999"
|
||||
|
||||
|
||||
REM Default MSYS2 path
|
||||
set "MSYS2_PATH=C:\msys64"
|
||||
set MSYS2_PATH=C:\msys64
|
||||
|
||||
REM Use environment variable if set
|
||||
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
|
||||
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
|
||||
if not "%MSYS2_ROOT%"=="" set MSYS2_PATH=%MSYS2_ROOT%
|
||||
if not "%MSYS2%"=="" set MSYS2_PATH=%MSYS2%
|
||||
|
||||
REM Check if msys2_shell.cmd exists
|
||||
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
|
||||
@@ -23,15 +22,10 @@ if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
|
||||
echo Please install MSYS2 from https://www.msys2.org/
|
||||
echo or set MSYS2_ROOT environment variable.
|
||||
pause
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Launch MSYS2 UCRT64 shell and run build script
|
||||
echo Starting MSYS2 UCRT64 environment...
|
||||
rem call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -c "bash build.sh > build_win.log 2>&1"
|
||||
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --installer 2>&1 | tee build_win.log"
|
||||
set "BUILD_RESULT=%ERRORLEVEL%"
|
||||
if not "%BUILD_RESULT%"=="0" echo [ERROR] Build or installer failed. See build_win.log.
|
||||
popd
|
||||
endlocal & exit /b %BUILD_RESULT%
|
||||
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh 2>&1 | tee build_win.log"
|
||||
@@ -1,221 +1,197 @@
|
||||
#!/usr/bin/env bash
|
||||
# Инкрементальная сборка Linux, FreeBSD и Windows/MSYS2; флаги задаются через configure.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
#!/bin/bash
|
||||
# Build script for uTun (supports both Linux and Windows/MSYS2)
|
||||
# Usage: ./build.sh [options]
|
||||
# Options:
|
||||
# -h, --help Show this help
|
||||
# --clean Clean before build
|
||||
# --full Full rebuild (autoreconf + configure + make)
|
||||
# --asan Build with AddressSanitizer (debug)
|
||||
# --hardened Build with compiler security hardening flags
|
||||
# -j[N] Build with N parallel jobs (default: 4)
|
||||
|
||||
MODE=normal
|
||||
set -e # Exit on error
|
||||
|
||||
# Detect platform
|
||||
if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ -n "$MSYSTEM" ]]; then
|
||||
IS_WINDOWS=1
|
||||
echo "Detected Windows/MSYS2 environment"
|
||||
else
|
||||
IS_WINDOWS=0
|
||||
echo "Detected Linux environment"
|
||||
fi
|
||||
|
||||
# Parse arguments
|
||||
CLEAN=0
|
||||
FULL=0
|
||||
BUILD_GUI=1
|
||||
BUILD_INSTALLER=0
|
||||
JOBS=4
|
||||
MAKE_ARGS=()
|
||||
MAKE_SETTINGS=()
|
||||
STATE=.utun-build-state
|
||||
FULL_REBUILD=0
|
||||
USE_ASAN=0
|
||||
USE_HARDENED=0
|
||||
JOBS="-j4"
|
||||
MAKE_ARGS=""
|
||||
HARDEN_FLAGS=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
case $1 in
|
||||
-h|--help)
|
||||
echo "Usage: ./build.sh [--clean] [--full] [--asan|--hardened] [--no-gui] [--installer] [-jN] [make arguments]"
|
||||
echo " --clean Rebuild objects without forcing configure"
|
||||
echo " --full Regenerate, configure and rebuild everything"
|
||||
echo " --asan AddressSanitizer; skip tests"
|
||||
echo " --hardened Enable compiler security hardening"
|
||||
echo " --no-gui Skip the separately configured ChatGUI"
|
||||
echo " --installer Build Windows ChatGUI in Release and create its Inno Setup installer"
|
||||
echo " -jN, -j N Parallel jobs (default: 4)"
|
||||
echo " CC/CFLAGS/CPPFLAGS/LDFLAGS/LIBS assignments are passed to configure"
|
||||
exit 0 ;;
|
||||
--clean) CLEAN=1 ;;
|
||||
--full) FULL=1 ;;
|
||||
--no-gui) BUILD_GUI=0 ;;
|
||||
--installer) BUILD_INSTALLER=1 ;;
|
||||
--asan|--hardened)
|
||||
selected_mode=${1#--}
|
||||
if [[ "$MODE" != normal && "$MODE" != "$selected_mode" ]]; then
|
||||
echo "ERROR: --asan and --hardened cannot be combined" >&2; exit 1
|
||||
fi
|
||||
MODE=$selected_mode ;;
|
||||
-j)
|
||||
[[ $# -gt 1 ]] || { echo "ERROR: -j requires a positive job count" >&2; exit 1; }
|
||||
shift; JOBS=$1 ;;
|
||||
-j*) JOBS=${1#-j} ;;
|
||||
CC=*|CFLAGS=*|CPPFLAGS=*|LDFLAGS=*|LIBS=*) export "$1" ;;
|
||||
CC+=*|CFLAGS+=*|CPPFLAGS+=*|LDFLAGS+=*|LIBS+=*)
|
||||
echo "ERROR: use NAME=value for configure flags; append assignments are not supported" >&2; exit 1 ;;
|
||||
echo "Usage: ./build.sh [options]"
|
||||
echo "Options:"
|
||||
echo " -h, --help Show this help"
|
||||
echo " --clean Clean before build"
|
||||
echo " --full Full rebuild (autoreconf + configure + make)"
|
||||
echo " --asan Build with AddressSanitizer"
|
||||
echo " --hardened Build with compiler security hardening flags"
|
||||
echo " -j[N] Build with N parallel jobs (default: 4)"
|
||||
exit 0
|
||||
;;
|
||||
--clean)
|
||||
CLEAN=1
|
||||
shift
|
||||
;;
|
||||
--full)
|
||||
FULL_REBUILD=1
|
||||
shift
|
||||
;;
|
||||
--asan)
|
||||
USE_ASAN=1
|
||||
shift
|
||||
;;
|
||||
--hardened)
|
||||
USE_HARDENED=1
|
||||
shift
|
||||
;;
|
||||
-j*)
|
||||
JOBS="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
MAKE_ARGS+=("$1")
|
||||
[[ "$1" != *=* ]] || MAKE_SETTINGS+=("$1") ;;
|
||||
MAKE_ARGS="$MAKE_ARGS $1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
[[ "$JOBS" =~ ^[1-9][0-9]*$ ]] || { echo "ERROR: invalid job count: $JOBS" >&2; exit 1; }
|
||||
if [[ "$BUILD_INSTALLER" = 1 ]]; then
|
||||
[[ "$BUILD_GUI" = 1 && "$MODE" = normal ]] || { echo "ERROR: --installer requires GUI and normal build mode" >&2; exit 1; }
|
||||
bash tools/chatgui/package_win.sh --check
|
||||
fi
|
||||
|
||||
PLATFORM=$(uname -s)
|
||||
MAKE_TOOL=${MAKE:-make}
|
||||
if [[ "$PLATFORM" = FreeBSD ]]; then
|
||||
MAKE_TOOL=${MAKE:-gmake}
|
||||
export CC="${CC:-clang}"
|
||||
fi
|
||||
MAKE_PATH=$(command -v "$MAKE_TOOL") || { echo "ERROR: GNU make not found: $MAKE_TOOL (FreeBSD: pkg install gmake)" >&2; exit 1; }
|
||||
MAKE_ID=$("$MAKE_TOOL" --version) || { echo "ERROR: GNU make is required; use MAKE=gmake on BSD" >&2; exit 1; }
|
||||
[[ "$MAKE_ID" = *'GNU Make'* ]] || { echo "ERROR: GNU make is required; use MAKE=gmake on BSD" >&2; exit 1; }
|
||||
export MAKE="$MAKE_TOOL"
|
||||
if command -v sha256sum >/dev/null; then
|
||||
HASH_COMMAND=(sha256sum)
|
||||
elif command -v sha256 >/dev/null; then
|
||||
HASH_COMMAND=(sha256 -r)
|
||||
elif command -v shasum >/dev/null; then
|
||||
HASH_COMMAND=(shasum -a 256)
|
||||
else
|
||||
echo "ERROR: SHA-256 utility not found: sha256sum, sha256 or shasum is required" >&2; exit 1
|
||||
fi
|
||||
read -r -a COMPILER <<< "${CC:-gcc}"
|
||||
COMPILER_PATH=$(command -v "${COMPILER[0]}") || { echo "ERROR: compiler not found: ${CC:-gcc}" >&2; exit 1; }
|
||||
COMPILER_ID=$("${COMPILER[@]}" --version) || { echo "ERROR: cannot identify compiler: ${CC:-gcc}" >&2; exit 1; }
|
||||
|
||||
# Хеши учитывают содержимое и имена файлов, включая добавление/удаление макросов.
|
||||
hash_files() {
|
||||
local result
|
||||
result=$({ printf '%s\0' "$@"; "${HASH_COMMAND[@]}" "$@"; } | "${HASH_COMMAND[@]}")
|
||||
printf '%s\n' "${result%% *}"
|
||||
}
|
||||
|
||||
BUILD_DIRS=(lib lib/libopus src tests)
|
||||
AUTOTOOLS_INPUTS=(configure.ac Makefile.am lib/Makefile.am lib/libopus/Makefile.am src/Makefile.am tests/Makefile.am)
|
||||
AUTOTOOLS_OUTPUTS=(configure aclocal.m4 config.h.in Makefile.in lib/Makefile.in lib/libopus/Makefile.in src/Makefile.in tests/Makefile.in)
|
||||
CONFIG_OUTPUTS=(config.status config.h Makefile lib/Makefile lib/libopus/Makefile src/Makefile tests/Makefile)
|
||||
shopt -s globstar nullglob
|
||||
for macro in m4/**/*.m4; do AUTOTOOLS_INPUTS+=("$macro"); done
|
||||
GENERATION_INPUT=$(hash_files "${AUTOTOOLS_INPUTS[@]}")
|
||||
GENERATION_OUTPUT=missing
|
||||
CONFIG_OUTPUT=missing
|
||||
missing_generation=0
|
||||
missing_configuration=0
|
||||
for file in "${AUTOTOOLS_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_generation=1; done
|
||||
for file in "${CONFIG_OUTPUTS[@]}"; do [[ -f "$file" ]] || missing_configuration=1; done
|
||||
if [[ "$missing_generation" = 0 ]]; then GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}"); fi
|
||||
if [[ "$missing_configuration" = 0 ]]; then CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}"); fi
|
||||
|
||||
FFMPEG_PACKAGES=(libavformat libavcodec libavutil libswscale libswresample)
|
||||
FFMPEG_OPTION=--without-ffmpeg
|
||||
FFMPEG_ID=unavailable
|
||||
FFMPEG_CFLAGS=""
|
||||
FFMPEG_LDFLAGS=""
|
||||
if command -v pkg-config >/dev/null && pkg-config --exists "${FFMPEG_PACKAGES[@]}"; then
|
||||
FFMPEG_OPTION=--with-ffmpeg
|
||||
FFMPEG_ID=$(pkg-config --modversion "${FFMPEG_PACKAGES[@]}")
|
||||
FFMPEG_CFLAGS=$(pkg-config --cflags "${FFMPEG_PACKAGES[@]}")
|
||||
FFMPEG_LDFLAGS=$(pkg-config --libs-only-L "${FFMPEG_PACKAGES[@]}")
|
||||
fi
|
||||
request_sum=$({
|
||||
printf '%s\0' "$MODE" "$PLATFORM" "${MSYSTEM:-}" "${MSYSTEM_PREFIX:-}" "${CC:-gcc}" "$COMPILER_PATH" "$COMPILER_ID"
|
||||
printf '%s\0' "$MAKE_PATH" "$MAKE_ID" "${HASH_COMMAND[@]}"
|
||||
printf '%s\0' "${CFLAGS+x}" "${CFLAGS-}" "${CPPFLAGS+x}" "${CPPFLAGS-}" "${LDFLAGS+x}" "${LDFLAGS-}"
|
||||
printf '%s\0' "${LIBS+x}" "${LIBS-}" "${MAKEFLAGS-}" "${DEBUG_FLAGS+x}" "${DEBUG_FLAGS-}"
|
||||
printf '%s\0' "$FFMPEG_OPTION" "$FFMPEG_ID" "$FFMPEG_CFLAGS" "$FFMPEG_LDFLAGS"
|
||||
printf '%s\0' "${MAKE_SETTINGS[@]}"
|
||||
hash_files build.sh
|
||||
} | "${HASH_COMMAND[@]}")
|
||||
CONFIG_REQUEST=${request_sum%% *}
|
||||
|
||||
OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT=""
|
||||
if [[ -f "$STATE" ]]; then
|
||||
if ! {
|
||||
read -r OLD_GENERATION_INPUT; read -r OLD_GENERATION_OUTPUT
|
||||
read -r OLD_CONFIG_REQUEST; read -r OLD_CONFIG_OUTPUT
|
||||
} < "$STATE"; then
|
||||
echo "WARN: incomplete build state; adopting the build with a clean rebuild"
|
||||
OLD_GENERATION_INPUT=""; OLD_GENERATION_OUTPUT=""; OLD_CONFIG_REQUEST=""; OLD_CONFIG_OUTPUT=""
|
||||
fi
|
||||
fi
|
||||
|
||||
REGENERATE=0
|
||||
RECONFIGURE=0
|
||||
if [[ "$FULL" = 1 || "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" || "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then
|
||||
echo "INFO: regenerating Autotools: full=$FULL"
|
||||
if [[ "$GENERATION_INPUT" != "$OLD_GENERATION_INPUT" ]]; then echo "INFO: Autotools descriptions changed or unrecorded"; fi
|
||||
if [[ "$GENERATION_OUTPUT" != "$OLD_GENERATION_OUTPUT" ]]; then echo "INFO: generated Autotools files changed or missing"; fi
|
||||
command -v autoreconf >/dev/null || { echo "ERROR: autoreconf is required to regenerate the build" >&2; exit 1; }
|
||||
mkdir -p m4
|
||||
autoreconf -fi 2>&1 | tee autoreconf.log || { echo "ERROR: autoreconf failed; see autoreconf.log" >&2; exit 1; }
|
||||
REGENERATE=1
|
||||
fi
|
||||
if [[ "$REGENERATE" = 1 || "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" || "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then
|
||||
echo "INFO: configuring: mode=$MODE regenerated=$REGENERATE"
|
||||
if [[ "$CONFIG_REQUEST" != "$OLD_CONFIG_REQUEST" ]]; then echo "INFO: compiler, flags or optional dependencies changed or unrecorded"; fi
|
||||
if [[ "$CONFIG_OUTPUT" != "$OLD_CONFIG_OUTPUT" ]]; then echo "INFO: configuration files changed or missing"; fi
|
||||
CONFIG_ARGS=("$FFMPEG_OPTION" --disable-hardening)
|
||||
if [[ "$MODE" = asan ]]; then
|
||||
export CFLAGS="${CFLAGS-} -g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie"
|
||||
export LDFLAGS="${LDFLAGS-} -fsanitize=address -no-pie"
|
||||
if [[ "$PLATFORM" = FreeBSD ]]; then export ASAN_OPTIONS="${ASAN_OPTIONS:-detect_leaks=0}"; fi
|
||||
elif [[ "$MODE" = hardened ]]; then
|
||||
CONFIG_ARGS+=(--enable-hardening)
|
||||
fi
|
||||
if [[ "$FFMPEG_OPTION" = --with-ffmpeg ]]; then
|
||||
export CPPFLAGS="${CPPFLAGS-} $FFMPEG_CFLAGS"
|
||||
export LDFLAGS="${LDFLAGS-} $FFMPEG_LDFLAGS"
|
||||
fi
|
||||
./configure "${CONFIG_ARGS[@]}" 2>&1 | tee configure.log || { echo "ERROR: configure failed; see configure.log" >&2; exit 1; }
|
||||
RECONFIGURE=1
|
||||
fi
|
||||
|
||||
# Проверяем только зависимости объектов, перечисленных Automake; посторонние сборки не затрагиваем.
|
||||
MISSING_DEPS=0
|
||||
for directory in "${BUILD_DIRS[@]}"; do
|
||||
while read -r keyword depfile marker; do
|
||||
[[ "$keyword" = include && "$marker" = '# am--include-marker' && "$depfile" = *.Po ]] || continue
|
||||
depfile=${depfile//\$\(DEPDIR\)/.deps}
|
||||
object=${depfile/.deps\//}
|
||||
object=${object%.Po}
|
||||
[[ -f "$directory/$object.o" || -f "$directory/$object.obj" ]] || continue
|
||||
dependency_line=""
|
||||
if [[ -s "$directory/$depfile" ]]; then IFS= read -r dependency_line < "$directory/$depfile" || true; fi
|
||||
if [[ "$dependency_line" != *:* ]]; then
|
||||
echo "WARN: missing or empty dependencies for $directory/$object: $directory/$depfile"
|
||||
MISSING_DEPS=1
|
||||
break
|
||||
fi
|
||||
done < "$directory/Makefile"
|
||||
done
|
||||
if [[ "$RECONFIGURE" = 1 || "$CLEAN" = 1 || "$MISSING_DEPS" = 1 ]]; then
|
||||
echo "INFO: cleaning objects: reconfigured=$RECONFIGURE requested=$CLEAN missing_dependencies=$MISSING_DEPS"
|
||||
"$MAKE_TOOL" clean 2>&1 | tee build.log || { echo "ERROR: clean failed; see build.log" >&2; exit 1; }
|
||||
else
|
||||
echo "INFO: configuration unchanged; incremental build (mode=$MODE)"
|
||||
: > build.log
|
||||
fi
|
||||
|
||||
echo "INFO: compiling: mode=$MODE jobs=$JOBS"
|
||||
if [[ "$MODE" = asan ]]; then
|
||||
echo "INFO: AddressSanitizer: building lib, Opus and src; skipping tests"
|
||||
MAKE_ARGS+=("SUBDIRS=lib lib/libopus src")
|
||||
fi
|
||||
"$MAKE_TOOL" "-j$JOBS" "${MAKE_ARGS[@]}" 2>&1 | tee -a build.log || { echo "ERROR: build failed; see build.log" >&2; exit 1; }
|
||||
|
||||
# Состояние обновляется после успешной сборки ядра; ошибка GUI не инвалидирует готовые объекты.
|
||||
GENERATION_OUTPUT=$(hash_files "${AUTOTOOLS_OUTPUTS[@]}")
|
||||
CONFIG_OUTPUT=$(hash_files "${CONFIG_OUTPUTS[@]}")
|
||||
state_tmp="$STATE.tmp.$$"
|
||||
trap 'rm -f "$state_tmp"' EXIT
|
||||
printf '%s\n' "$GENERATION_INPUT" "$GENERATION_OUTPUT" "$CONFIG_REQUEST" "$CONFIG_OUTPUT" > "$state_tmp"
|
||||
mv -f "$state_tmp" "$STATE"
|
||||
|
||||
if [[ "$BUILD_GUI" = 1 && ( "$BUILD_INSTALLER" = 1 || -f tools/chatgui/build/CMakeCache.txt ) ]]; then
|
||||
command -v cmake >/dev/null || { echo "ERROR: ChatGUI exists but cmake is not available" >&2; exit 1; }
|
||||
echo "INFO: building ChatGUI with its CMake configuration: jobs=$JOBS"
|
||||
if [[ "${MSYSTEM:-}" = UCRT64 ]]; then
|
||||
GUI_ARGS=("-j$JOBS")
|
||||
[[ "$BUILD_INSTALLER" != 1 ]] || GUI_ARGS+=(--installer)
|
||||
(cd tools/chatgui && bash build.sh "${GUI_ARGS[@]}") || { echo "ERROR: ChatGUI build failed" >&2; exit 1; }
|
||||
# Check required tools
|
||||
if ! command -v gcc &> /dev/null; then
|
||||
echo "Error: gcc not found."
|
||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||
echo "Please install: pacman -S mingw-w64-ucrt-x86_64-toolchain"
|
||||
else
|
||||
cmake --build tools/chatgui/build --parallel "$JOBS" || { echo "ERROR: ChatGUI build failed" >&2; exit 1; }
|
||||
echo "Please install build-essential or gcc"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean if requested
|
||||
if [ "$CLEAN" -eq 1 ]; then
|
||||
echo "Cleaning build..."
|
||||
make clean 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Full rebuild: regenerate build system
|
||||
if [ "$FULL_REBUILD" -eq 1 ]; then
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "FULL REBUILD: autoreconf"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Check for autotools
|
||||
if ! command -v autoreconf &> /dev/null; then
|
||||
echo "Error: autoreconf not found."
|
||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools"
|
||||
else
|
||||
echo "Install autotools: apt-get install autoconf automake libtool"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean generated files
|
||||
echo "Cleaning generated files..."
|
||||
make distclean 2>/dev/null || true
|
||||
rm -rf build/
|
||||
|
||||
# Regenerate build system (root Makefile is hand-written, not generated)
|
||||
echo ""
|
||||
echo "Running autoreconf -fi..."
|
||||
autoreconf -fi 2>&1 | tee autoreconf.log
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Build system regenerated successfully"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
else
|
||||
# Quick/incremental build: generate configure if needed
|
||||
if [ ! -f ./configure ]; then
|
||||
echo "Configure script not found. Generating..."
|
||||
if ! command -v autoreconf &> /dev/null; then
|
||||
echo "Error: autoreconf not found."
|
||||
if [ "$IS_WINDOWS" -eq 1 ]; then
|
||||
echo "Install autotools: pacman -S mingw-w64-ucrt-x86_64-autotools"
|
||||
else
|
||||
echo "Install autotools: apt-get install autoconf automake libtool"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
autoreconf -fiv
|
||||
fi
|
||||
fi
|
||||
echo "Build completed successfully!"
|
||||
echo "Binary: src/utun"
|
||||
|
||||
# Generate Makefiles if needed
|
||||
if [ ! -f Makefile ]; then
|
||||
echo ""
|
||||
echo "Running ./configure..."
|
||||
if [ "$USE_ASAN" -eq 1 ]; then
|
||||
./configure CFLAGS="-g -Og -fsanitize=address -fno-omit-frame-pointer -fno-pie" LDFLAGS="-no-pie"
|
||||
elif [ "$USE_HARDENED" -eq 1 ]; then
|
||||
./configure --enable-hardening
|
||||
else
|
||||
./configure
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build (all .o files go to build/ directory, binary copied to project root)
|
||||
echo ""
|
||||
echo "Compiling with $JOBS..."
|
||||
BUILD_SUCCESS=0
|
||||
if [ "$USE_ASAN" -eq 1 ]; then
|
||||
echo "(AddressSanitizer enabled — lib/ and src/ only, skipping tests)"
|
||||
AF="-fsanitize=address -fno-omit-frame-pointer -g -Og"
|
||||
AL="-fsanitize=address"
|
||||
(cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; }
|
||||
(cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$AF" "LDFLAGS+=$AL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; }
|
||||
BUILD_SUCCESS=1
|
||||
elif [ "$USE_HARDENED" -eq 1 ]; then
|
||||
echo "(Hardening flags enabled)"
|
||||
HF="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE"
|
||||
HL="-Wl,-z,relro -Wl,-z,now -pie"
|
||||
(cd lib && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "lib build failed"; exit 1; }
|
||||
(cd src && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "src build failed"; exit 1; }
|
||||
(cd tests && make clean $JOBS $MAKE_ARGS 2>&1 && make "CFLAGS+=$HF" "LDFLAGS+=$HL" $JOBS $MAKE_ARGS 2>&1) || { echo "tests build failed"; exit 1; }
|
||||
BUILD_SUCCESS=1
|
||||
else
|
||||
make $JOBS $MAKE_ARGS 2>&1 | tee build.log
|
||||
if [ ${PIPESTATUS[0]} -eq 0 ]; then
|
||||
BUILD_SUCCESS=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ $BUILD_SUCCESS -eq 1 ]; then
|
||||
echo ""
|
||||
echo "Build completed successfully!"
|
||||
echo "Binary: utun"
|
||||
|
||||
# Rebuild chatgui (CMake project) if transport sources changed
|
||||
if [ -f tools/chatgui/build/CMakeCache.txt ] && command -v cmake &> /dev/null; then
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Rebuilding chatgui (CMake)"
|
||||
echo "======================================"
|
||||
(cd tools/chatgui/build && cmake --build . -j4 2>&1) || echo "WARNING: chatgui build failed"
|
||||
fi
|
||||
else
|
||||
echo ""
|
||||
echo "ERROR: Build failed!"
|
||||
echo "Check build.log for details"
|
||||
exit 1
|
||||
fi
|
||||
+53
-25
@@ -1,32 +1,60 @@
|
||||
#!/bin/sh
|
||||
# Полная сборка FreeBSD: зависимости и общий инкрементальный скрипт.
|
||||
|
||||
set -e
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
case " ${*} " in
|
||||
*" --help "*|*" -h "*)
|
||||
echo "Usage: ./build_bsd_full.sh [--asan|--hardened] [-jN] [build.sh arguments]"
|
||||
echo "Installs missing build tools and runs build.sh --full"
|
||||
exit 0 ;;
|
||||
esac
|
||||
|
||||
need_install=0
|
||||
for tool in bash gmake autoreconf aclocal automake libtoolize pkg-config; do
|
||||
command -v "$tool" >/dev/null 2>&1 || need_install=1
|
||||
# Проверка и установка зависимостей
|
||||
need_install=""
|
||||
|
||||
for cmd in autoreconf aclocal automake libtoolize; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
need_install=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$need_install" -eq 1 ]; then
|
||||
echo "INFO: installing missing FreeBSD build dependencies (requires root)"
|
||||
pkg install -y bash gmake autoconf automake libtool pkgconf openssl || {
|
||||
echo "ERROR: cannot install FreeBSD build dependencies" >&2; exit 1;
|
||||
}
|
||||
|
||||
if [ -n "$need_install" ]; then
|
||||
echo "Устанавливаются необходимые пакеты..."
|
||||
pkg update
|
||||
pkg install -y autoconf automake libtool pkgconf openssl
|
||||
fi
|
||||
|
||||
jobs_found=0
|
||||
for argument do
|
||||
case "$argument" in -j*) jobs_found=1 ;; esac
|
||||
done
|
||||
if [ "$jobs_found" -eq 0 ]; then
|
||||
jobs=$(sysctl -n hw.ncpu) || { echo "ERROR: cannot determine CPU count" >&2; exit 1; }
|
||||
set -- "-j$jobs" "$@"
|
||||
mkdir -p m4
|
||||
|
||||
echo "=== Полная пересборка uTun ==="
|
||||
|
||||
# 1. Очистка
|
||||
echo "[1/4] Очистка..."
|
||||
make distclean 2>/dev/null || true
|
||||
rm -f config.h config.status config.log
|
||||
rm -f configure.log autoreconf.log build.log
|
||||
|
||||
# 2. Генерация configure
|
||||
echo "[2/4] Автогенерация..."
|
||||
autoreconf -fi 2>&1 | tee autoreconf.log
|
||||
|
||||
# Проверяем, появился ли configure
|
||||
if [ ! -f configure ]; then
|
||||
echo "Ошибка: autoreconf не создал configure."
|
||||
exit 1
|
||||
fi
|
||||
echo "INFO: running shared build script for FreeBSD"
|
||||
exec bash "$SCRIPT_DIR/build.sh" --full "$@"
|
||||
|
||||
chmod +x configure
|
||||
|
||||
# 3. Конфигурация
|
||||
echo "[3/4] Конфигурация..."
|
||||
./configure --prefix=/usr/local 2>&1 | tee configure.log
|
||||
|
||||
# Определяем количество ядер
|
||||
if command -v nproc >/dev/null 2>&1; then
|
||||
JOBS=$(nproc)
|
||||
else
|
||||
JOBS=$(sysctl -n hw.ncpu)
|
||||
fi
|
||||
|
||||
# 4. Сборка
|
||||
echo "[4/4] Сборка..."
|
||||
make -j"$JOBS" 2>&1 | tee build.log
|
||||
|
||||
echo
|
||||
echo "=== Готово ==="
|
||||
echo "Бинарный файл: src/utun"
|
||||
+1
-1
@@ -70,7 +70,7 @@ echo "Building utun.exe..."
|
||||
cd src
|
||||
EXE_NEEDS_REBUILD=0
|
||||
|
||||
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_padding.c transport_layer/reality_record.c transport_layer/reality_io.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
|
||||
CORE_SOURCES="utun.c utun_instance.c config_parser.c config_updater.c routing_layer/route_lib.c topo_bgp.c routing_layer/routing.c tun_if.c tun_route.c transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_loadbalancer.c transport_layer/secure_channel.c transport_layer/crc32.c transport_layer/pkt_normalizer.c transport_layer/etcp_api.c tun_windows.c"
|
||||
|
||||
for src in $CORE_SOURCES; do
|
||||
if [ -f "$src" ]; then
|
||||
|
||||
+5
-11
@@ -1,5 +1,4 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions
|
||||
echo uTun Full Build Script for MSYS2 UCRT64
|
||||
echo =========================================
|
||||
echo This will regenerate build system with autoreconf
|
||||
@@ -12,11 +11,11 @@ rem export https_proxy="http://192.168.29.1:9999"
|
||||
|
||||
|
||||
REM Default MSYS2 path
|
||||
set "MSYS2_PATH=C:\msys64"
|
||||
set MSYS2_PATH=C:\msys64
|
||||
|
||||
REM Use environment variable if set
|
||||
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
|
||||
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
|
||||
if not "%MSYS2_ROOT%"=="" set MSYS2_PATH=%MSYS2_ROOT%
|
||||
if not "%MSYS2%"=="" set MSYS2_PATH=%MSYS2%
|
||||
|
||||
REM Check if msys2_shell.cmd exists
|
||||
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
|
||||
@@ -24,16 +23,11 @@ if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
|
||||
echo Please install MSYS2 from https://www.msys2.org/
|
||||
echo or set MSYS2_ROOT environment variable.
|
||||
pause
|
||||
popd
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Launch MSYS2 UCRT64 shell and run build script with --full flag
|
||||
echo Starting MSYS2 UCRT64 environment for FULL REBUILD...
|
||||
echo Running: bash build.sh --full --installer
|
||||
echo Running: bash build.sh --full
|
||||
echo.
|
||||
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --full --installer 2>&1 | tee build_full.log"
|
||||
set "BUILD_RESULT=%ERRORLEVEL%"
|
||||
if not "%BUILD_RESULT%"=="0" echo [ERROR] Build or installer failed. See build_full.log.
|
||||
popd
|
||||
endlocal & exit /b %BUILD_RESULT%
|
||||
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "set -o pipefail; bash build.sh --full 2>&1 | tee build_full.log"
|
||||
+2
-91
@@ -1,78 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# --- определение пакетного менеджера ---
|
||||
detect_pm() {
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
PM=apt; PM_UPDATE="apt-get update"; PM_INSTALL="apt-get install -y"
|
||||
P_AUTOCONF=autoconf; P_AUTOMAKE=automake; P_BUILD=build-essential; P_SSL=libssl-dev
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
PM=dnf; PM_UPDATE="true"; PM_INSTALL="dnf install -y"
|
||||
P_AUTOCONF=autoconf; P_AUTOMAKE=automake; P_BUILD="gcc make"; P_SSL=openssl-devel
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
PM=yum; PM_UPDATE="true"; PM_INSTALL="yum install -y"
|
||||
P_AUTOCONF=autoconf; P_AUTOMAKE=automake; P_BUILD="gcc make"; P_SSL=openssl-devel
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
PM=pacman; PM_UPDATE="pacman -Sy --noconfirm"; PM_INSTALL="pacman -S --noconfirm"
|
||||
P_AUTOCONF=autoconf; P_AUTOMAKE=automake; P_BUILD=base-devel; P_SSL=openssl
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
PM=apk; PM_UPDATE="apk update"; PM_INSTALL="apk add"
|
||||
P_AUTOCONF=autoconf; P_AUTOMAKE=automake; P_BUILD=build-base; P_SSL=openssl-dev
|
||||
else
|
||||
PM=none
|
||||
fi
|
||||
}
|
||||
|
||||
# --- чего не хватает (логические маркеры) ---
|
||||
detect_missing() {
|
||||
missing=""
|
||||
command -v autoreconf >/dev/null 2>&1 || missing="$missing autoconf"
|
||||
command -v automake >/dev/null 2>&1 || missing="$missing automake"
|
||||
if ! command -v gcc >/dev/null 2>&1 || ! command -v make >/dev/null 2>&1; then
|
||||
missing="$missing buildtools"
|
||||
fi
|
||||
[ -f /usr/include/openssl/sha.h ] || missing="$missing openssl"
|
||||
}
|
||||
|
||||
# --- перевести маркеры в пакеты выбранного PM ---
|
||||
pkg_names() {
|
||||
pkgs=""
|
||||
case "$missing" in *autoconf*) pkgs="$pkgs $P_AUTOCONF";; esac
|
||||
case "$missing" in *automake*) pkgs="$pkgs $P_AUTOMAKE";; esac
|
||||
case "$missing" in *buildtools*) pkgs="$pkgs $P_BUILD";; esac
|
||||
case "$missing" in *openssl*) pkgs="$pkgs $P_SSL";; esac
|
||||
}
|
||||
|
||||
# --- установка, если что-то отсутствует ---
|
||||
detect_pm
|
||||
detect_missing
|
||||
if [ -n "$missing" ]; then
|
||||
if [ "$PM" = "none" ]; then
|
||||
echo "Ошибка: не найден пакетный менеджер. Установите вручную:$missing"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
SUDO=""
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
SUDO="sudo"
|
||||
else
|
||||
echo "Ошибка: нужны права root (sudo не найден). Установите вручную:$missing"
|
||||
exit 1
|
||||
fi
|
||||
pkg_names
|
||||
echo "Установка недостающих пакетов ($PM):$pkgs"
|
||||
$SUDO $PM_UPDATE && $SUDO $PM_INSTALL $pkgs || {
|
||||
echo "Ошибка: не удалось установить пакеты:$pkgs"
|
||||
exit 1
|
||||
}
|
||||
detect_missing
|
||||
if [ -n "$missing" ]; then
|
||||
echo "Ошибка: после установки всё ещё не хватает:$missing"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p m4
|
||||
set -e
|
||||
|
||||
echo "=== Полная пересборка uTun ==="
|
||||
|
||||
@@ -81,36 +9,19 @@ echo "[1/4] Очистка..."
|
||||
make distclean 2>/dev/null || true
|
||||
rm -f config.h config.status config.log
|
||||
rm -f configure.log autoreconf.log build.log
|
||||
rm -f src/utun
|
||||
|
||||
# 2. Автогенерация (autoreconf)
|
||||
echo "[2/4] Автогенерация (autoreconf -fi)..."
|
||||
autoreconf -fi 2>&1 | tee autoreconf.log
|
||||
|
||||
if [ ! -f configure ]; then
|
||||
echo "Ошибка: autoreconf не создал configure. См. autoreconf.log"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Конфигурация
|
||||
echo "[3/4] Конфигурация..."
|
||||
./configure --prefix=/usr/local 2>&1 | tee configure.log
|
||||
|
||||
if [ ! -f Makefile ]; then
|
||||
echo "Ошибка: configure не создал Makefile. См. configure.log"
|
||||
echo "Возможно не установлен libssl-dev (OpenSSL)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. Сборка
|
||||
echo "[4/4] Сборка..."
|
||||
make -j$(nproc) 2>&1 | tee build.log
|
||||
|
||||
if [ ! -f src/utun ]; then
|
||||
echo "Ошибка: сборка не создала src/utun. См. build.log"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Готово ==="
|
||||
echo "Бинарный файл: src/utun (скопирован в ./utun)"
|
||||
echo "Бинарный файл: src/utun"
|
||||
@@ -16,7 +16,7 @@ set "TOTAL=0"
|
||||
if not exist "%LOGS_DIR%" mkdir "%LOGS_DIR%"
|
||||
|
||||
:: List of all test executables
|
||||
set "TEST_LIST=bench_timeout_heap bench_uasync_timeouts test_etcp_minimal test_u_async_performance test_packet_dump test_config_debug test_debug_categories test_memory_pool_and_config test_etcp_crypto test_route_lib test_crypto test_ecc_encrypt test_ll_queue test_intensive_memory_pool test_pkt_normalizer_standalone test_etcp_two_instances test_u_async_comprehensive test_etcp_simple_traffic test_pkt_normalizer_etcp test_etcp_100_packets test_etcp_api test_bgp_route_exchange"
|
||||
set "TEST_LIST=test_etcp_crypto test_crypto test_etcp_two_instances test_etcp_simple_traffic test_etcp_minimal test_etcp_100_packets test_pkt_normalizer_etcp test_pkt_normalizer_standalone test_etcp_api test_ll_queue test_ecc_encrypt test_intensive_memory_pool test_memory_pool_and_config test_packet_dump test_u_async_comprehensive test_u_async_performance test_debug_categories test_config_debug test_route_lib test_bgp_route_exchange bench_timeout_heap bench_uasync_timeouts"
|
||||
|
||||
echo Running tests...
|
||||
echo.
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
MAKE_TOOL=${MAKE:-$(command -v gmake || command -v make)}
|
||||
TIMEOUT_TOOL=$(command -v timeout || command -v gtimeout) || {
|
||||
echo "ERROR: timeout or gtimeout is required for the test runner" >&2; exit 1;
|
||||
}
|
||||
|
||||
LAST=0; [ -f .check_time ] && read -r LAST < .check_time
|
||||
TMO=$(( LAST + 30 ))
|
||||
@@ -12,33 +8,14 @@ TMO=$(( LAST + 30 ))
|
||||
|
||||
START=$(date +%s)
|
||||
TMP=$(mktemp)
|
||||
"$TIMEOUT_TOOL" "$TMO" "$MAKE_TOOL" check -j4 > "$TMP" 2>&1 || {
|
||||
timeout $TMO make check -j4 > "$TMP" 2>&1 || {
|
||||
tail -40 "$TMP"; rm -f "$TMP"
|
||||
[ $(date +%s) -ge $((START + TMO)) ] && echo "TIMEOUT (${TMO}s)" >&2
|
||||
exit 1
|
||||
}
|
||||
grep -E '\[INFO|\[PASS|\[FAIL|\[SKIP|Test Results|Failed tests|Log directory|^====' "$TMP"
|
||||
grep -E '\[PASS|\[FAIL|\[SKIP|Test Results|Failed tests|Log directory|^====' "$TMP"
|
||||
rm -f "$TMP"
|
||||
|
||||
# Интеграционный тест tcp_proxy_full (требует sudo; без passwordless sudo — пропускаем)
|
||||
echo ""
|
||||
if [ "$(uname -s)" != Linux ]; then
|
||||
echo "[SKIP] tcp_proxy_full integration tests require Linux networking tools"
|
||||
elif [ "$(id -u)" -eq 0 ]; then
|
||||
"$MAKE_TOOL" -C tests check-proxy
|
||||
"$MAKE_TOOL" -C tests check-burst
|
||||
"$MAKE_TOOL" -C tests check-load
|
||||
elif command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
|
||||
echo "=== tcp_proxy_full integration test (sudo) ==="
|
||||
sudo -n "$MAKE_TOOL" -C tests check-proxy
|
||||
echo "=== tcp_proxy_full burst test (sudo) ==="
|
||||
sudo -n "$MAKE_TOOL" -C tests check-burst
|
||||
echo "=== tcp_proxy_full load test (sudo) ==="
|
||||
sudo -n "$MAKE_TOOL" -C tests check-load
|
||||
else
|
||||
echo "[SKIP] tcp_proxy_full integration test (requires passwordless sudo)"
|
||||
fi
|
||||
|
||||
ELAPSED=$(( $(date +%s) - START ))
|
||||
echo "Time: ${ELAPSED}s"
|
||||
echo "$ELAPSED" > .check_time
|
||||
@@ -1,34 +0,0 @@
|
||||
@echo off
|
||||
setlocal
|
||||
pushd "%~dp0"
|
||||
if errorlevel 1 (
|
||||
echo [ERROR] Cannot open the script directory.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "MSYS2_PATH=C:\msys64"
|
||||
if not "%MSYS2_ROOT%"=="" set "MSYS2_PATH=%MSYS2_ROOT%"
|
||||
if not "%MSYS2%"=="" set "MSYS2_PATH=%MSYS2%"
|
||||
set "PROBE_LOG=%CD%\cc-probe.log"
|
||||
set "PROBE_RESULT=1"
|
||||
|
||||
> "%PROBE_LOG%" echo [INFO] MSYS2: %MSYS2_PATH%
|
||||
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
|
||||
>> "%PROBE_LOG%" echo [ERROR] msys2_shell.cmd not found. Set MSYS2_ROOT to the MSYS2 directory.
|
||||
goto finish
|
||||
)
|
||||
|
||||
echo [INFO] Checking the UCRT64 C compiler and Opus build...
|
||||
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "bash ./check_cc.sh" >> "%PROBE_LOG%" 2>&1
|
||||
set "PROBE_RESULT=%ERRORLEVEL%"
|
||||
|
||||
:finish
|
||||
>> "%PROBE_LOG%" echo [INFO] MSYS2 shell exit code: %PROBE_RESULT%
|
||||
type "%PROBE_LOG%"
|
||||
echo.
|
||||
echo [INFO] Log: "%PROBE_LOG%"
|
||||
if not "%PROBE_RESULT%"=="0" echo [ERROR] Compiler or Opus check failed. See the log above.
|
||||
if /i not "%~1"=="--no-pause" pause
|
||||
popd
|
||||
exit /b %PROBE_RESULT%
|
||||
-150
@@ -1,150 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -uo pipefail
|
||||
|
||||
# Проверяем базовую компиляцию, затем Opus отдельно и через существующую CMake-сборку.
|
||||
probe_cc="${UTUN_PROBE_CC:-/ucrt64/bin/cc.exe}"
|
||||
printf '[INFO] MSYSTEM=%s\n[INFO] PATH=%s\n[INFO] Compiler=%s\n' "${MSYSTEM:-unset}" "$PATH" "$probe_cc"
|
||||
if [ ! -x "$probe_cc" ]; then
|
||||
printf '[ERROR] Compiler is missing or not executable: %s\n' "$probe_cc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
probe_dir=$(mktemp -d "${TMPDIR:-/tmp}/utun-cc-probe.XXXXXX") || {
|
||||
printf '[ERROR] Cannot create the temporary probe directory.\n'
|
||||
exit 1
|
||||
}
|
||||
trap 'rm -rf -- "$probe_dir" || printf "[WARN] Cannot remove probe directory: %s\n" "$probe_dir"' EXIT
|
||||
printf '[INFO] Temporary directory: %s\n' "$probe_dir"
|
||||
|
||||
# Каждый этап сохраняет собственный код завершения, включая молчаливые сбои.
|
||||
probe_step() {
|
||||
local step="$1" result
|
||||
shift
|
||||
printf '\n[INFO] %s\n[INFO] Directory: %s\n[INFO] Command:' "$step" "$PWD"
|
||||
printf ' %q' "$@"
|
||||
printf '\n'
|
||||
"$@"
|
||||
result=$?
|
||||
printf '[INFO] %s exit code: %d\n' "$step" "$result"
|
||||
if [ "$result" -ne 0 ]; then
|
||||
printf '[ERROR] Failed stage: %s\n' "$step"
|
||||
fi
|
||||
return "$result"
|
||||
}
|
||||
|
||||
printf '#include <stdio.h>\nint main(void) { puts("utun compiler probe OK"); return 0; }\n' > "$probe_dir/probe.c" || {
|
||||
printf '[ERROR] Cannot write the probe source: %s/probe.c\n' "$probe_dir"
|
||||
exit 1
|
||||
}
|
||||
probe_step 'Compiler version' "$probe_cc" --version || exit $?
|
||||
probe_step 'Compile' "$probe_cc" -v -std=c99 -c "$probe_dir/probe.c" -o "$probe_dir/probe.o" || exit $?
|
||||
probe_step 'Link' "$probe_cc" -v "$probe_dir/probe.o" -o "$probe_dir/probe.exe" || exit $?
|
||||
probe_step 'Run' "$probe_dir/probe.exe" || exit $?
|
||||
printf '\n[SUCCESS] Compile, link and run completed.\n'
|
||||
|
||||
probe_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) || {
|
||||
printf '[ERROR] Cannot locate the repository directory.\n'
|
||||
exit 1
|
||||
}
|
||||
probe_opus="$probe_root/lib/libopus"
|
||||
probe_includes=()
|
||||
for probe_include in '' include celt silk silk/float silk/fixed src; do
|
||||
probe_includes+=("-I$probe_opus/$probe_include")
|
||||
done
|
||||
for probe_source in laplace bands; do
|
||||
probe_step "Compile Opus $probe_source.c" "$probe_cc" -v -DOPUS_BUILD -DUSE_ALLOCA -DVAR_ARRAYS "${probe_includes[@]}" \
|
||||
-O3 -DNDEBUG -std=gnu99 -MD -MF "$probe_dir/$probe_source.d" \
|
||||
-c "$probe_opus/celt/$probe_source.c" -o "$probe_dir/$probe_source.o" || exit $?
|
||||
done
|
||||
|
||||
probe_build="$probe_root/tools/chatgui/build"
|
||||
if [ ! -f "$probe_build/CMakeCache.txt" ]; then
|
||||
printf '[ERROR] Existing ChatGUI build cache not found: %s/CMakeCache.txt\n' "$probe_build"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v cmake >/dev/null 2>&1; then
|
||||
printf '[ERROR] cmake is not available in PATH.\n'
|
||||
exit 1
|
||||
fi
|
||||
probe_step 'CMake cache settings' sed -n -E \
|
||||
'/^CMAKE_(C_COMPILER|C_COMPILER_LAUNCHER|C_FLAGS(_(DEBUG|RELEASE|MINSIZEREL|RELWITHDEBINFO))?|MAKE_PROGRAM|GENERATOR|BUILD_TYPE):/p' \
|
||||
"$probe_build/CMakeCache.txt" || exit $?
|
||||
|
||||
# Повторяем Windows-команду без преобразования аргументов MSYS2; добавляем только GCC -v.
|
||||
if [ "${MSYSTEM:-}" = UCRT64 ]; then
|
||||
probe_ninja=$(sed -n 's/^CMAKE_MAKE_PROGRAM:[^=]*=//p' "$probe_build/CMakeCache.txt")
|
||||
probe_ninja=${probe_ninja%$'\r'}
|
||||
probe_ninja=$(cygpath -u "$probe_ninja") || exit $?
|
||||
"$probe_ninja" -C "$probe_build" -t commands opus_internal > "$probe_dir/ninja-commands.txt"
|
||||
probe_result=$?
|
||||
if [ "$probe_result" -ne 0 ]; then
|
||||
cat "$probe_dir/ninja-commands.txt"
|
||||
printf '[ERROR] Cannot read Ninja commands, exit code: %d\n' "$probe_result"
|
||||
exit "$probe_result"
|
||||
fi
|
||||
probe_command=''
|
||||
while IFS= read -r probe_line; do
|
||||
probe_line=${probe_line%$'\r'}
|
||||
case "$probe_line" in
|
||||
*' -DOPUS_BUILD '*' -c '*'/celt/bands.c'|*' -DOPUS_BUILD '*' -c '*'/celt/bands.c"')
|
||||
probe_command="$probe_line"; break ;;
|
||||
esac
|
||||
done < "$probe_dir/ninja-commands.txt"
|
||||
if [ -z "$probe_command" ]; then
|
||||
printf '[ERROR] Cannot find the Opus bands.c compile command in Ninja.\n'
|
||||
exit 1
|
||||
fi
|
||||
probe_command=${probe_command/ -DOPUS_BUILD/ -v -DOPUS_BUILD}
|
||||
printf '[INFO] Native command: %s\n' "$probe_command"
|
||||
printf '@echo off\r\n%s\r\nexit /b %%errorlevel%%\r\n' "$probe_command" > "$probe_dir/replay.cmd" || {
|
||||
printf '[ERROR] Cannot write the native compiler replay script.\n'
|
||||
exit 1
|
||||
}
|
||||
probe_cmd=$(cygpath -u "${COMSPEC:-C:/Windows/System32/cmd.exe}") || exit $?
|
||||
probe_replay=$(cygpath -w "$probe_dir/replay.cmd") || exit $?
|
||||
|
||||
# CMD сохраняет полный Windows-код завершения cc1; Bash оставляет только младший байт.
|
||||
probe_cc1=$("$probe_cc" -print-prog-name=cc1) || { printf '[ERROR] Cannot locate cc1.\n'; exit 1; }
|
||||
probe_cc1=${probe_cc1%$'\r'}
|
||||
probe_cc1=$(cygpath -u "$probe_cc1") || { printf '[ERROR] Cannot resolve the cc1 path.\n'; exit 1; }
|
||||
probe_cc1_native=$(cygpath -w "$probe_cc1") || { printf '[ERROR] Cannot resolve the native cc1 path.\n'; exit 1; }
|
||||
printf '@echo off\r\n"%s" --version\r\nset "UTUN_CC1_RESULT=%%errorlevel%%"\r\n' "$probe_cc1_native" \
|
||||
> "$probe_dir/cc1.cmd" || { printf '[ERROR] Cannot write the cc1 probe script.\n'; exit 1; }
|
||||
printf 'echo [INFO] cc1 Windows exit code: %%UTUN_CC1_RESULT%%\r\nexit /b %%UTUN_CC1_RESULT%%\r\n' \
|
||||
>> "$probe_dir/cc1.cmd" || { printf '[ERROR] Cannot finish the cc1 probe script.\n'; exit 1; }
|
||||
probe_cc1_script=$(cygpath -w "$probe_dir/cc1.cmd") || { printf '[ERROR] Cannot resolve the cc1 script path.\n'; exit 1; }
|
||||
for probe_cwd in "$probe_root" "$probe_build"; do
|
||||
cd -- "$probe_cwd" || { printf '[ERROR] Cannot open probe directory: %s\n' "$probe_cwd"; exit 1; }
|
||||
MSYS2_ARG_CONV_EXCL='*' probe_step 'Launch cc1 before runtime refresh' "$probe_cmd" /d /c call "$probe_cc1_script"
|
||||
# При ошибке продолжаем сравнение каталогов и DLL, затем повторяем исходный сбой.
|
||||
done
|
||||
cd -- "$probe_root" || { printf '[ERROR] Cannot open the repository directory.\n'; exit 1; }
|
||||
probe_objdump="${probe_cc%/*}/objdump.exe"
|
||||
"$probe_objdump" -p "$probe_cc1" > "$probe_dir/cc1-imports.txt" || {
|
||||
printf '[ERROR] Cannot inspect cc1 imports.\n'; exit 1;
|
||||
}
|
||||
printf '\n[INFO] cc1 DLL imports:\n'
|
||||
sed -n '/DLL Name:/p' "$probe_dir/cc1-imports.txt" || { printf '[ERROR] Cannot read cc1 imports.\n'; exit 1; }
|
||||
source "$probe_root/tools/chatgui/compiler_runtime.sh"
|
||||
for probe_dll in "$probe_build/"*.dll; do
|
||||
[ -f "$probe_dll" ] || continue
|
||||
chatgui_is_compiler_runtime_dll "${probe_dll##*/}" || continue
|
||||
probe_system_dll="${probe_cc%/*}/${probe_dll##*/}"
|
||||
[ -f "$probe_system_dll" ] || continue
|
||||
cmp -s "$probe_dll" "$probe_system_dll"
|
||||
probe_result=$?
|
||||
case "$probe_result" in
|
||||
0) printf '[INFO] DLL matches UCRT64: %s\n' "${probe_dll##*/}" ;;
|
||||
1) printf '[WARN] DLL differs from UCRT64: %s\n' "${probe_dll##*/}"
|
||||
probe_step 'DLL hashes' sha256sum "$probe_dll" "$probe_system_dll" || exit $? ;;
|
||||
*) printf '[ERROR] Cannot compare DLL: %s, exit code: %d\n' "$probe_dll" "$probe_result"; exit "$probe_result" ;;
|
||||
esac
|
||||
done
|
||||
probe_step 'Refresh compiler runtime DLLs' chatgui_refresh_compiler_runtime "$probe_build" "${probe_cc%/*}" || exit $?
|
||||
cd -- "$probe_build" || { printf '[ERROR] Cannot open the ChatGUI build directory.\n'; exit 1; }
|
||||
MSYS2_ARG_CONV_EXCL='*' probe_step 'Launch cc1 after runtime refresh' "$probe_cmd" /d /c call "$probe_cc1_script" || exit $?
|
||||
MSYS2_ARG_CONV_EXCL='*' probe_step 'Replay Ninja bands.c command with GCC verbose output' \
|
||||
"$probe_cmd" /d /c call "$probe_replay" || exit $?
|
||||
fi
|
||||
probe_step 'Build Opus with CMake' cmake --build "$probe_build" --target opus_internal --parallel 1 --verbose || exit $?
|
||||
printf '\n[SUCCESS] Standalone Opus compilation and CMake Opus build completed.\n'
|
||||
+5
-101
@@ -62,24 +62,11 @@ AC_ARG_ENABLE([hardening],
|
||||
[enable_hardening=no])
|
||||
|
||||
if test "x$enable_hardening" = "xyes"; then
|
||||
HARDEN_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -Wformat=2"
|
||||
compiler_version=$($CC --version 2>&1) || AC_MSG_ERROR([Cannot identify compiler for hardening])
|
||||
case "$compiler_version" in
|
||||
*clang*) ;;
|
||||
*) HARDEN_CFLAGS="$HARDEN_CFLAGS -fstack-clash-protection -Wformat-overflow=2" ;;
|
||||
esac
|
||||
case "$host" in
|
||||
*mingw* | *msys* | *cygwin*)
|
||||
HARDEN_LDFLAGS="-Wl,--dynamicbase,--nxcompat,--high-entropy-va"
|
||||
;;
|
||||
*)
|
||||
HARDEN_CFLAGS="$HARDEN_CFLAGS -fPIE"
|
||||
HARDEN_LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie"
|
||||
;;
|
||||
esac
|
||||
HARDEN_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fstack-clash-protection -Wformat=2 -Wformat-overflow=2 -fPIE"
|
||||
HARDEN_LDFLAGS="-Wl,-z,relro -Wl,-z,now -pie"
|
||||
CFLAGS="$CFLAGS $HARDEN_CFLAGS"
|
||||
LDFLAGS="$LDFLAGS $HARDEN_LDFLAGS"
|
||||
AC_MSG_NOTICE([Hardening flags enabled: CFLAGS=$HARDEN_CFLAGS LDFLAGS=$HARDEN_LDFLAGS])
|
||||
AC_MSG_NOTICE([Hardening flags enabled: $HARDEN_CFLAGS])
|
||||
fi
|
||||
|
||||
# ==================== НАДЁЖНЫЙ ДЕТЕКТ WINDOWS ====================
|
||||
@@ -108,19 +95,6 @@ AC_MSG_RESULT([$os_freebsd])
|
||||
|
||||
AM_CONDITIONAL([OS_FREEBSD], [test "x$os_freebsd" = "xyes"])
|
||||
|
||||
# ==================== DETECT LINUX ====================
|
||||
AC_MSG_CHECKING([for Linux])
|
||||
AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
|
||||
#ifndef __linux__
|
||||
#error "Not Linux"
|
||||
#endif
|
||||
]], [[ return 0; ]])],
|
||||
[os_linux=yes],
|
||||
[os_linux=no])
|
||||
AC_MSG_RESULT([$os_linux])
|
||||
|
||||
AM_CONDITIONAL([OS_LINUX], [test "x$os_linux" = "xyes"])
|
||||
|
||||
# Set TUN libs based on platform
|
||||
if test "x$os_windows" = "xyes"; then
|
||||
TUN_LIBS="-liphlpapi -lws2_32 -ladvapi32 -lbcrypt -ldbghelp"
|
||||
@@ -153,75 +127,6 @@ fi
|
||||
AM_CONDITIONAL([HAVE_WHISPER], [test "x$with_whisper" = "xyes"])
|
||||
AC_SUBST([WHISPER_LIBS])
|
||||
|
||||
# ==================== FFMPEG (OPTIONAL) ====================
|
||||
AC_ARG_WITH([ffmpeg],
|
||||
AS_HELP_STRING([--with-ffmpeg], [Enable video transcoding (requires FFmpeg libav libraries)]),
|
||||
[with_ffmpeg=$withval],
|
||||
[with_ffmpeg=no])
|
||||
|
||||
if test "x$with_ffmpeg" = "xyes"; then
|
||||
save_LIBS="$LIBS"
|
||||
FFMPEG_LIBS="-lavformat -lavcodec -lavutil -lswscale -lswresample"
|
||||
LIBS="$FFMPEG_LIBS $LIBS"
|
||||
AC_CHECK_HEADER([libavformat/avformat.h], [
|
||||
AC_CHECK_HEADER([libavcodec/avcodec.h], [
|
||||
AC_CHECK_HEADER([libavutil/imgutils.h], [
|
||||
AC_CHECK_HEADER([libswscale/swscale.h], [
|
||||
AC_CHECK_HEADER([libswresample/swresample.h], [
|
||||
AC_CHECK_LIB([avformat], [avformat_open_input], [
|
||||
AC_DEFINE([HAVE_FFMPEG], [1], [Define to enable FFmpeg video transcoding])
|
||||
AC_MSG_NOTICE([FFmpeg enabled])
|
||||
], [
|
||||
AC_MSG_WARN([FFmpeg libs not found, disabling ffmpeg])
|
||||
with_ffmpeg=no
|
||||
])
|
||||
], [AC_MSG_WARN([swresample.h not found, disabling ffmpeg]); with_ffmpeg=no])
|
||||
], [AC_MSG_WARN([swscale.h not found, disabling ffmpeg]); with_ffmpeg=no])
|
||||
], [AC_MSG_WARN([imgutils.h not found, disabling ffmpeg]); with_ffmpeg=no])
|
||||
], [AC_MSG_WARN([avcodec.h not found, disabling ffmpeg]); with_ffmpeg=no])
|
||||
], [AC_MSG_WARN([avformat.h not found, disabling ffmpeg]); with_ffmpeg=no])
|
||||
LIBS="$save_LIBS"
|
||||
fi
|
||||
|
||||
if test "x$with_ffmpeg" != "xyes"; then
|
||||
FFMPEG_LIBS=""
|
||||
FFMPEG_CFLAGS=""
|
||||
fi
|
||||
AM_CONDITIONAL([HAVE_FFMPEG], [test "x$with_ffmpeg" = "xyes"])
|
||||
AC_SUBST([FFMPEG_LIBS])
|
||||
AC_SUBST([FFMPEG_CFLAGS])
|
||||
|
||||
# ==================== SILERO VAD (OPTIONAL, requires ONNX Runtime) ====================
|
||||
AC_ARG_WITH([silero-vad],
|
||||
AS_HELP_STRING([--with-silero-vad], [Enable Silero voice activity detector (requires ONNX Runtime)]),
|
||||
[with_silero_vad=$withval],
|
||||
[with_silero_vad=no])
|
||||
|
||||
if test "x$with_silero_vad" = "xyes"; then
|
||||
AC_CHECK_HEADER([onnxruntime/onnxruntime_c_api.h], [
|
||||
save_LIBS="$LIBS"
|
||||
LIBS="-lonnxruntime $LIBS"
|
||||
AC_CHECK_LIB([onnxruntime], [OrtGetApiBase], [
|
||||
SILERO_VAD_LIBS="-lonnxruntime"
|
||||
AC_DEFINE([HAVE_SILERO_VAD], [1], [Define to enable Silero VAD via ONNX Runtime])
|
||||
AC_MSG_NOTICE([Silero VAD enabled (ONNX Runtime)])
|
||||
], [
|
||||
AC_MSG_WARN([libonnxruntime not found, disabling silero-vad])
|
||||
with_silero_vad=no
|
||||
])
|
||||
LIBS="$save_LIBS"
|
||||
], [
|
||||
AC_MSG_WARN([onnxruntime/onnxruntime_c_api.h not found, disabling silero-vad])
|
||||
with_silero_vad=no
|
||||
])
|
||||
fi
|
||||
|
||||
if test "x$with_silero_vad" != "xyes"; then
|
||||
SILERO_VAD_LIBS=""
|
||||
fi
|
||||
AM_CONDITIONAL([HAVE_SILERO_VAD], [test "x$with_silero_vad" = "xyes"])
|
||||
AC_SUBST([SILERO_VAD_LIBS])
|
||||
|
||||
# Checks for header files, typedefs, functions...
|
||||
AC_CHECK_HEADERS([arpa/inet.h fcntl.h limits.h netinet/in.h stdint.h stdlib.h string.h sys/ioctl.h sys/socket.h unistd.h])
|
||||
AC_TYPE_SIZE_T
|
||||
@@ -248,11 +153,10 @@ AC_OUTPUT
|
||||
# Summary
|
||||
echo "
|
||||
Configuration summary:
|
||||
---------------------
|
||||
----------------------
|
||||
Host: ${host}
|
||||
Windows: ${os_windows}
|
||||
Prefix: ${prefix}
|
||||
Compiler: ${CC}
|
||||
CFLAGS: ${CFLAGS}
|
||||
Silero VAD: ${with_silero_vad}
|
||||
"
|
||||
"
|
||||
+4
-4
@@ -197,14 +197,14 @@ SRC_SOURCES=(
|
||||
routing_layer/route_lib.c routing_layer/route6_lib.c topo_bgp.c routing_layer/route_ping.c
|
||||
routing_layer/topo_node.c topo_node_lmdb.c routing_layer/route_connectivity.c routing_layer/conn_mgr.c
|
||||
routing_layer/routing.c tun_if.c tun_route.c tun_linux.c tun_freebsd.c tun_windows.c
|
||||
transport_layer/etcp.c transport_layer/etcp_session.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c
|
||||
transport_layer/etcp.c transport_layer/etcp_connections.c transport_layer/etcp_bbr.c transport_layer/etcp_loadbalancer.c
|
||||
transport_layer/etcp_debug.c transport_layer/etcp_dump.c transport_layer/secure_channel.c transport_layer/crc32.c
|
||||
transport_layer/stcp_link.c transport_layer/stcp.c transport_layer/stcp_server.c transport_layer/stcp_client.c
|
||||
transport_layer/pkt_normalizer.c transport_layer/packet_dump.c transport_layer/etcp_api.c
|
||||
control_server.c
|
||||
firewall.c eim_nat.c nat_transport.c transport_layer/dummynet.c
|
||||
etcp_router.c route_crypto.c
|
||||
proxy/udp_proxy.c proxy/socks_proxy.c proxy/http_proxy.c
|
||||
etcp_router.c
|
||||
proxy/udp_proxy.c proxy/socks_proxy.c
|
||||
lwip_tcp/lwip_pbuf.c
|
||||
)
|
||||
|
||||
@@ -272,7 +272,7 @@ echo ""
|
||||
if [ -f "$SCRIPT_DIR/lib/wintun.dll" ]; then
|
||||
echo "Run on Windows:"
|
||||
echo " 1. Install Wintun driver (https://www.wintun.net)"
|
||||
echo " 2. Copy utun.exe + wintun.dll + LICENSE + LICENSING + THIRD_PARTY_NOTICES to the same directory"
|
||||
echo " 2. Copy utun.exe + wintun.dll to the same directory"
|
||||
echo " 3. Run: utun.exe -f -p utun.pid -l utun.log"
|
||||
else
|
||||
echo "Run on Windows:"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
План доработок
|
||||
[ ] продумать архитектуру репликации таблицы узлов группы
|
||||
[ ] добавить узлы хелперы (для проксирования трафика)
|
||||
|
||||
|
||||
Категории узлов:
|
||||
- узлы без нат []
|
||||
- узлы с nat [restricted]
|
||||
|
||||
Узлы за nat сами подключаются к прямым
|
||||
|
||||
Узлы за eim nat сами подключаются и сообщают обновление адреса.
|
||||
|
||||
Прямые узлы должны сами должны объединиться в сеть следующим образом:
|
||||
|
||||
Инит: база содержит RTT по узлам.
|
||||
выбираем n=16 узлов с минимальным rtt. запускаем подключение к ним.
|
||||
синхронизируем роутинг таблицу с ними (обмен маршрутами должен инициироваться сразу после подключения).
|
||||
при потере связи узлы через nat должны найти альтернативы.
|
||||
|
||||
в таблице обновляется
|
||||
|
||||
как обмен завершен
|
||||
|
||||
Нужно собирать и обновлять информацию о том
|
||||
- к каким публичным нодам подключены nat узлы.
|
||||
|
||||
- метрика доступности узла - как давно не менялся ip:port - собираем с клиентов. вместе с рейтигном (каждый клиент по узлу фолрмирует свой рейтинг кандидата в суперузлы и отправляет подписанный рейтинг узлу)
|
||||
рейтинг включает:
|
||||
- число разных часов фигурирует в замерах
|
||||
- число разных дат фигурирует в замерах
|
||||
- гистограмма rtt: 0-2ms, 2-4ms, 4-6ms, 6-10ms, 10-13ms, 14-18ms, 18-25ms, 25-35ms, 35-50ms, 50-75ms, 75-100ms, 100-130ms, 130-160ms, 160+ms
|
||||
- гистограмма потерь: 0-1% 1-2% 2-4% 4-8% >8% (обновляется каждый час)
|
||||
|
||||
Сделать подключение к группе
|
||||
-1524
File diff suppressed because it is too large.
Load diff
@@ -1,413 +0,0 @@
|
||||
# P2P аудио-звонок пользователю — архитектура
|
||||
|
||||
Статус: реализовано. Звонок использует роутер, CM подготавливает оптимальный путь.
|
||||
|
||||
## 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 →
|
||||
аудиопоток через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
|
||||
|
||||
### Входящие вызовы и телефонная занятость
|
||||
|
||||
Android хранит пять независимых настроек: показывать статус звонка, предупреждать вызывающего,
|
||||
отклонять при телефонной занятости, отклонять все обычные звонки, отклонять все приглашения в рацию.
|
||||
Они передаются маской `CALL_POLICY_*` через `[chat] incoming_call_policy` при старте,
|
||||
а при изменениях — вместе с живым состоянием телефона через JNI в uasync.
|
||||
Первые три настройки используют `READ_PHONE_STATE`; запреты всех входящих разрешения не требуют.
|
||||
Наблюдатель учитывает все активные SIM, набор номера, удержание и входящий телефонный вызов.
|
||||
Без разрешения или без телефонной службы состояние неизвестно, а не свободно.
|
||||
|
||||
Приоритет: запрет типа вызова → автоматический отказ при телефонной занятости → предупреждение с выбором.
|
||||
Отказ при занятости возвращает `CALL_REASON_PHONE_BUSY`/`RADIO_CALL_PHONE_BUSY`.
|
||||
Предупреждение переводит попытку в `WAIT_DECISION`: у вызываемого нет события INCOMING, рингтона и регистрации в Telecom.
|
||||
Вызывающий видит «Абонент разговаривает по телефону», «Всё равно позвонить» и «Завершить».
|
||||
Продолжение подтверждается повторяемой командой внутри той же попытки; освобождение телефона само не запускает звонок.
|
||||
Повторные пакеты не создают повторных входящих событий. При изменении политики перед продолжением запрет проверяется снова.
|
||||
Предел всей попытки — 60с от начала, включая выбор; продолжение deadline не меняет.
|
||||
У рации первый ответ и подтверждение продолжения ограничены 2с; сообщение о занятости является ответом, но не RINGING.
|
||||
|
||||
Рация, уже включённая у адресата в указанном канале, возвращает `RADIO_CALL_ALREADY_ACTIVE`:
|
||||
вызывающий получает уведомление, входящий вызов не показывается, его собственная рация не включается.
|
||||
Этот ответ описывает существующую подписку и не создаёт нового входящего вызова даже при включённом запрете приглашений.
|
||||
Принятие обычного приглашения в рацию по-прежнему сначала включает местную подписку, затем отправляет ACCEPT.
|
||||
Android обычного звонка показывает своё уведомление после `onShowIncomingCallUi` и обрабатывает отказ Telecom.
|
||||
|
||||
## 2. Разбиение файлов (отдельный каталог `src/call/`)
|
||||
|
||||
| Файл | Язык | Сборки | Назначение |
|
||||
|---|---|---|---|
|
||||
| `src/call/call_proto.h` | C | везде | wire-формат, subcmd, svc_id `ETCP_RT_ID_CALL 0x36` |
|
||||
| `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/call_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
|
||||
|
||||
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
|
||||
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
|
||||
|
||||
Диагностика: категория `call` (`DEBUG_CATEGORY_CALL`); транспортная доставка — `etcp_route`.
|
||||
|
||||
## 3. Протокол (сигналинг svc_id 0x36, первый байт payload = subcmd)
|
||||
|
||||
Формат приёма: `entry->dgram[0]=svc_id`, служебный заголовок роутера до
|
||||
`ROUTER_SVC_PAYLOAD_OFF=26`, затем subcmd и тело команды.
|
||||
|
||||
```
|
||||
CALL_INVITE 0x01 {call_id:8, group_id:8, num_tracks:1, force:1, expires_us:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
|
||||
CALL_RINGING 0x02 {call_id:8}
|
||||
CALL_ACCEPT 0x03 {call_id:8}
|
||||
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_END_ACK 0x08 {call_id:8}
|
||||
CALL_PHONE_BUSY 0x09 {call_id:8}
|
||||
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capture_epoch:8, codec_delay_samples: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 кадров/с.
|
||||
- Заголовок медиа — 36 байт с subcmd. `ts_ms` — время отправки для оценки сетевого джиттера;
|
||||
`capture_pts_us` — время первого входного сэмпла в монотонной шкале отправителя, `INT64_MIN` при отсутствии метки.
|
||||
`capture_epoch` различает разрывы захвата; `codec_delay_samples` передаёт Opus lookahead.
|
||||
Старый формат не поддерживается; медиа принимается только от пира текущего звонка.
|
||||
|
||||
## 4. Машина состояний
|
||||
|
||||
```
|
||||
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → ACTIVE(ACCEPT) → ENDED
|
||||
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACTIVE(accept) → ENDED
|
||||
```
|
||||
|
||||
`ENDED` reasons: `LOCAL_HANGUP / REMOTE_HANGUP / DECLINE / BUSY / CANCEL /
|
||||
NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`.
|
||||
|
||||
Таймауты:
|
||||
- **кодограмма INVITE 300мс** — вызывающий шлёт её весь период вызова
|
||||
(`OUTGOING` + `RINGING`) как «сердцебиение»: покрывает потерю INVITE/RINGING и
|
||||
держит маршрут живым;
|
||||
- **watchdog 2с у вызываемого** — нет кодограммы 2с → вызов считается сорванным
|
||||
(отмена потерялась), сессия закрывается локально (`CALL_REASON_CANCEL`);
|
||||
- ring-timeout 60с от начала, включая ожидание решения вызывающего (нет ответа → CANCEL);
|
||||
- `CONN_EVENT_DOWN/TIMEOUT` CM не завершают звонок: роутер использует доступные пути;
|
||||
- **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP.
|
||||
|
||||
Завершающие кодограммы HANGUP/CANCEL/DECLINE повторяются каждые 300мс до END_ACK,
|
||||
не дольше 20с. Это необходимо при смене эпохи роутера: старые очереди удаляются,
|
||||
даже если отправитель ранее получил успешный результат постановки команды в очередь.
|
||||
Медиа и их локальные очереди освобождаются сразу, ENDED публикуется один раз.
|
||||
В реестре до ACK/таймаута остаётся только завершённая сессия с CM handle и таймером.
|
||||
Получатель подтверждает также повторные команды после удаления сессии, без повторного ENDED.
|
||||
Команда и ACK проверяются по подписи, шифрованию, размеру, call_id, пиру и группе существующей сессии.
|
||||
ACK не подтверждается. Новый звонок имеет другой call_id и не затрагивается поздними командами старого.
|
||||
При одновременном завершении стороны независимо подтверждают команды друг друга.
|
||||
После ACK сессия удаляется на ближайшем tick завершения; при удалённом завершении CM удерживается
|
||||
ещё 500мс для отправки ACK. Остановка ядра отменяет таймер без повторных отправок.
|
||||
Запоздалые кодограммы вызова при идущем разговоре (`ACTIVE`) игнорируются.
|
||||
|
||||
Онлайн-проверка: `topo_node_find_by_id(group, peer) == NULL` → «не в сети» (event).
|
||||
|
||||
CM handle инициирует оптимальное подключение и удерживается до завершения звонка.
|
||||
Медиа всегда идёт через `etcp_route_send`: роутер сам выбирает лучший путь и fallback.
|
||||
Звонок не хранит ETCP_CONN и не завершается при потере отдельного транспорта.
|
||||
Очистка записи CM отсоединяет handles и отменяет отложенные UP; handle закрывает владелец.
|
||||
Закрытие CM handle не посылает DISCONNECT удалённым владельцам. Механизм NCD не меняется.
|
||||
|
||||
## 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), ёмкостью
|
||||
15с (750 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`.
|
||||
- Алгоритм (`call_tx_flush`): пока `tx_q` не пуст:
|
||||
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=1)` (после проверки места);
|
||||
- иначе — если waiter не зарегистрирован → `etcp_router_on_send_ready(... drain_cb)` и выход.
|
||||
- `drain_cb(q, arg)` → снова `call_tx_flush(session)` (re-arm при необходимости).
|
||||
- Весь код — в uasync-потоке (single-threaded, реентерабельность как в
|
||||
`media_delivery.c:stream_send_chunk_cb`).
|
||||
|
||||
## 6. Потоки и жизненный цикл
|
||||
|
||||
`call_session`, CM handle, очередь TX и таймеры принадлежат uasync-потоку.
|
||||
Аудиопоток кодирует PCM и постит `call_send_media`; приём в uasync кладёт Opus в `call_jitter`.
|
||||
Декодер, SoundTouch и генератор тонов принадлежат аудиопотоку.
|
||||
Кольцо кодированных кадров и снимок статистики защищены мьютексом; статистика не читает SoundTouch из другого потока.
|
||||
`CallTimeline` хранит фактическое соответствие входных участков SoundTouch выходному PCM.
|
||||
Платформа публикует позицию воспроизведения с локальной монотонной меткой; `call_audio_get_clock`
|
||||
возвращает исходный PTS слышимого участка. Локальная тишина и тоны удерживают PTS.
|
||||
Невалидная/устаревшая отметка устройства не разрешает продвижение видео. Контракт и ограничения — `call_video.txt`.
|
||||
|
||||
GUI запускает аудиоустройство по CALL_ACCEPTED. При CALL_ENDED начинается финальный тон;
|
||||
устройство останавливается перед `call_audio_release`. Явное завершение звонка прекращает
|
||||
воспроизведение накопленной речи и включает тон завершения — на обеих платформах.
|
||||
Android останавливает гудки до настройки звонкового аудиотракта. После выбора маршрута
|
||||
capture/play потоки независимо создают и запускают AudioRecord/AudioTrack; каждый сразу обслуживает своё направление.
|
||||
`start()` подтверждает запуск подготовки, ошибки устройств передаются владельцу на main через `onFailure`.
|
||||
Ошибка любого направления завершает звонок. Общий `stop()` отменяет подготовку, разблокирует I/O и ждёт оба потока
|
||||
до освобождения Android-устройств и native-аудиостека. Отдельные locks защищают подготовку/остановку каждого устройства.
|
||||
Замер AEC планируется через 300мс после готовности обоих направлений и не задерживает воспроизведение.
|
||||
При наличии Telecom Connection режимом и выходом звонка управляет только Telecom.
|
||||
ACTIVE запрашивается до создания аудиоустройств; AudioManager/прямой preferred device не дублируют выбор.
|
||||
На API 34+ маршрут запрашивается через доступный CallEndpoint, на старых — через Connection.setAudioRoute.
|
||||
Подтверждённый маршрут приходит из callback системы, отдельно от желания пользователя; ошибка запроса логируется.
|
||||
Если Connection ещё нет, используется AudioManager. Поздняя регистрация передаёт управление Telecom,
|
||||
снимая заявку приложения на режим и прямые устройства. Рация сохраняет отдельный AudioManager-путь.
|
||||
Реальное время запуска/переключения зависит от Android и требует проверки на устройстве.
|
||||
Маршрутизатор использует один снимок входных/выходных устройств; обновляет его при старте,
|
||||
событиях устройств/BT-профиля и явной переоценке. По завершении снимок освобождается.
|
||||
INFO-логи Android показывают длительности настройки режима, регистрации callback, BT-профиля, выбора маршрута,
|
||||
создания/старта устройств, первого чтения микрофона и первой записи в AudioTrack, а также готовность другого направления.
|
||||
Общий `call_audio` логирует время ожидания первого PCM; запись в AudioTrack может содержать начальную тишину.
|
||||
Эффективный рабочий буфер AudioTrack на всех поддерживаемых API при старте запрашивается размером
|
||||
два блока PCM (40мс); на API 31+ стартовый порог следует фактически принятому размеру.
|
||||
Ёмкость выделенной памяти может быть больше. Ограничение повторяется по callback фактической
|
||||
смены выхода на main, под trackLock; callback старого AudioTrack игнорируется и снимается при stop.
|
||||
Подачу PCM задаёт свободное место в AudioTrack, без дополнительного таймера 20мс.
|
||||
Частичная запись удерживает остаток одного блока до полной передачи; следующий pull ждёт.
|
||||
Сетевой запас остаётся в адаптивном JB, его рост и SoundTouch-догон не изменены.
|
||||
Лог `playback buffering` показывает причину настройки, запрошенный/принятый размер, порог и ёмкость;
|
||||
ежесекундный `audio summary` — queueUs, текущий порог и underruns; новый episode underrun имеет уровень WARN.
|
||||
Это очередь AudioTrack, а не полная сквозная задержка и не сетевой jitter reserve.
|
||||
Ранее на Samsung startFrames=1920 был принят, но без ограничения рабочего буфера
|
||||
written-played оставался 120–160мс; одного стартового порога недостаточно.
|
||||
Замер Samsung подтвердил постоянную очередь 40мс. При прежнем параллельном управлении маршрутом
|
||||
AudioTrack пересоздавался, очередь временно достигала 160мс, а ожидание свободного места — 328–336мс.
|
||||
При старте AudioTrack.play занимал 1080мс. После перехода к единственному владельцу Telecom
|
||||
в проверенном звонке старт занял 62мс, очередь оставалась не более 40мс и при смене выхода.
|
||||
Переключение всё ещё останавливало потребление на 293–328мс; этот всплеск принимает сетевой JB.
|
||||
|
||||
На desktop `AudioDiag: capture clock` при call=debug выводит секундную сводку исходных backend-блоков:
|
||||
число блоков/сэмплов, накопленную последовательность, диапазон ошибки относительно конца предыдущего блока,
|
||||
latency PulseAudio, число скачков >2мс, hole-сэмплы и блоки без достоверного измерения.
|
||||
Первый скачок за секунду содержит обе latency/поправки конвертера, expected/actual PTS и монотонное время.
|
||||
Backend публикует только числовой снимок в ограниченное кольцо; форматирование выполняет reporter.
|
||||
Исходный backend-снимок остаётся наблюдением. В capture worker позиция PCM задаёт непрерывные PTS,
|
||||
а backend медленно корректирует их привязку: постоянная времени 2с, предел 0.5% (100мкс на 20мс PCM).
|
||||
Скорректированное время используется и для Opus, и для выбора AEC-референса. Сам PCM не изменяется.
|
||||
Разрыв sequence, смена route и скачок исходных меток >20мс по-прежнему сбрасывают capture/AEC и эпоху;
|
||||
порог 2мс в call_audio не увеличивается. Секундный `voice-io: capture clock` показывает phase/error/adjustment.
|
||||
Тесты проверяют десять минут дрейфа +/-300ppm, скачки оценки до 14мс, разбиение чанков 25мс на Opus 20мс,
|
||||
сохранность PCM, явную потерю, смену устройства и большой разрыв исходных часов.
|
||||
Счётчик sequence описывает полученные backend-сэмплы; отсутствие потерь очереди само по себе
|
||||
не доказывает непрерывность физического захвата. Проверять вместе с hole/overflow и route-событиями.
|
||||
|
||||
Для захвата звонка desktop явно запрашивает период 20мс вместо дефолта PulseAudio 25мс;
|
||||
фактический период сообщает `audio: call-capture initialized`. Сборщик по-прежнему принимает произвольные чанки.
|
||||
Секундные замеры задержки при `call=debug`:
|
||||
- `voice-io: capture latency`: возраст первого сэмпла перед native feed (включает сам блок 20мс),
|
||||
подготовка/AEC в worker и вызов native feed; диапазоны min/avg/max в микросекундах.
|
||||
- `voice-io: playback latency`: приблизительный остаток PCM после mix, включая частичный блок,
|
||||
длительность pull и недостающие сэмплы. Это очередь приложения, отдельно от backend.
|
||||
- `AudioDiag: playback clock`: latency backend, задержка конвертера/cache и максимальный размер
|
||||
backend-порции в микросекундах. При measured=0 нулевые границы не означают нулевую задержку.
|
||||
- Windows `AudioDiag: WASAPI queue`: текущий padding shared-mode, отдельный stream latency,
|
||||
converter/cache, размер callback и незавершённого mapped buffer. Диапазоны — min/avg/max в мкс;
|
||||
`padding_samples=0` означает отсутствие пригодного замера, а не пустую очередь.
|
||||
Снимок снимается перед заполнением client PCM; запись и форматирование выполняет reporter.
|
||||
`AudioDiag: audio timing` для стадий `callback`/`write` при `call=debug` показывает интервалы
|
||||
callback и время блокирующей передачи в WASAPI. HRESULT ошибок и потери диагностической очереди логируются.
|
||||
Снимки только наблюдают: Windows-аудиочасы остаются `callback-estimate` (`measured=0`).
|
||||
- `video playout`: показанные кадры, `speed_ppm`, диапазон A/V-фазы, интервалы показа,
|
||||
опоздание worker и ожидания аудиочасов. Опоздание кадра не вызывает пропуск; догон до 1.6x.
|
||||
- Desktop `video RX worker`: число декодированных кадров, максимальное время декодирования,
|
||||
возраст кадра после декодирования и опоздание пробуждения worker после sleep. Снимок аудио
|
||||
содержит `audio_measured`, PTS и позиции played/written. Все эти строки относятся к `call=debug`;
|
||||
`video=trace` для этой диагностики не требуется.
|
||||
- `video GUI delivery`: доставленные изображения, ожидание уведомления, промежуток доставки,
|
||||
`paint_wait_max_us` до подтверждения рисования. В GUI один ожидающий кадр, замены нет.
|
||||
- `video port TX paused/resumed`: сохранённый хвост пачки, первый ID, длительность ожидания
|
||||
и число повторов. Busy сохраняет encoded-кадры и не запрашивает KEY.
|
||||
- `video port RX full/drained`: заполнение и полная разгрузка encoded FIFO перед возобновлением
|
||||
приёма. Hysteresis переносит ограничение до новых входов sender encoder, освобождая запас для догона.
|
||||
- `call audio TX timing`: ожидание g_mtx, обработка/кодирование под ним, ожидание posted task;
|
||||
post включает ожидание lock при выполнении задачи, его разрешение 100мкс. На Android этот же
|
||||
замер обработки включает программный AEC, на desktop AEC уже выполнен в capture worker.
|
||||
- `call audio TX queue`: число ожидающих кадров, возраст старейшего и avg/max ожидания кадров,
|
||||
переданных роутеру за интервал. Отправка роутеру не является подтверждением доставки пиру.
|
||||
- Android `audio capture latency`: возраст первого сэмпла перед native feed и число измеренных
|
||||
привязок; дополнительные поля старта устройств разделяют min-buffer query, constructor, routing и buffer setup.
|
||||
Возраст PCM содержит часть измеренной обработки: эти числа нельзя механически складывать.
|
||||
PTS/возраст используют локальный MONOTONIC, интервалы внутри native — локальный get_time_us/get_time_tb;
|
||||
эти временные базы и часы разных устройств напрямую не вычитаются. Все сводки формируются вне device callback.
|
||||
Период захвата 20мс и новые измерения требуют контрольного звонка; дополнительная desktop PCM-очередь,
|
||||
жизненный цикл подготовки Android и таймер показа видео этим изменением не менялись.
|
||||
|
||||
На Android `CallPlaybackBuffer` подбирает рабочую очередь отдельно от JB/SoundTouch:
|
||||
после 30с наблюдаемого продвижения playback head без underrun пробует уменьшить её на один
|
||||
блок PCM (960 сэмплов, 20мс), до одного блока или фактического минимума устройства.
|
||||
При росте AudioTrack.underrunCount добавляет один блок в пределах выделенной ёмкости.
|
||||
Интервал следующей пробы удваивается (60/120/240/300с), чтобы не повторять частые неудачные уменьшения.
|
||||
После старта, запроса/подтверждения маршрута и увеличения даются 2с на стабилизацию;
|
||||
после уменьшения первый underrun учитывается немедленно. Отсутствие продвижения head или
|
||||
пропуск наблюдений >1с обнуляет стабильный участок. Сброс системного счётчика учитывается отдельно.
|
||||
Фактическая смена выхода начинает подбор с 40мс заново. Непринятая меньшая заявка фиксирует
|
||||
нижнюю границу до следующего выхода; ошибки API выключают подбор с WARN/ERROR, сохраняя воспроизведение.
|
||||
Reporter управляет размером под trackLock независимо от фильтра debug; playback не берёт этот lock.
|
||||
Нет flush/recreate, частичная запись PCM сохраняется. После смены размера/выхода через секунду
|
||||
повторно измеряется задержка для AEC. Ожидание полного буфера и недостача сети не увеличивают его размер.
|
||||
Лог настройки содержит reason, previous/requested/actualFrames и прирост underruns;
|
||||
достижение ёмкости предупреждает не чаще раза в 10с. Принятие и стабильность 20мс ещё требуют
|
||||
проверки на Samsung; буферы HAL и Bluetooth могут добавлять собственную задержку.
|
||||
На desktop адаптация устройства ещё не реализована: выход принадлежит SoundManager,
|
||||
изменение backend-буфера должно выполняться его владельцем, а не отдельным звонком.
|
||||
|
||||
Диагностика Android при `call=debug`: playback сохраняет один числовой снимок на блок (20мс)
|
||||
в кольце 512 элементов, без форматирования/вывода. Отдельный reporter раз в 200мс забирает записи;
|
||||
раз в секунду выводит `audio summary` с min/avg/max очередей, задержек pull/write и опоздания потока,
|
||||
целевым запасом/разбросом доставки JB, темпом, недостающим PCM и отдельными счётчиками JB/AudioTrack.
|
||||
Снимок JB не сбрасывает события для CALL_STATS. encodedMs — очередь Opus после pull,
|
||||
processingMs — оставшийся PCM SoundTouch в длительности исходной речи; reserveMs зависит только от доставки.
|
||||
Reporter опрашивает AudioTimestamp, routedDevice, underrunCount и размеры буфера не чаще раза в 200мс.
|
||||
Playback читает только опубликованный неизменяемый снимок. Его begin/end и возраст deviceAgeUs
|
||||
отличают задержку опроса от задержки аудио; stampQueryUs/routeQueryUs/propertiesQueryUs показывают стоимость запросов.
|
||||
Android API может удерживать общие внутренние locks даже при вызове из reporter: перенос убирает
|
||||
прямое ожидание диагностического запроса в playback, но сам по себе не доказывает отсутствие влияния на write/head.
|
||||
frame/time аппаратного timestamp и время начала опроса печатаются отдельно;
|
||||
при недоступности стоит stampOk=0. Времена trace — MONOTONIC; очередь устройства — written-played.
|
||||
Эти показатели нельзя складывать с возрастом AudioTimestamp как независимые задержки.
|
||||
В playback отдельно измеряются writeCallUs (только вызовы write), headCallUs (чтение позиции),
|
||||
progressCallUs (публикация позиции ядру), jbSnapshotUs (нативный снимок), writeSpanUs (весь цикл записи)
|
||||
и blockUs (pull до формирования строки). При частичных записях длительности вызовов суммируются за блок.
|
||||
pullGapExcessUs — превышение интервала между началами соседних pull над 20мс;
|
||||
включает ожидание устройства, а не только задержку планировщика (прежнее поле wakeLateUs удалено).
|
||||
Старое поле writeUs включало чтение позиции и прогресс: его нельзя трактовать как длительность одного AudioTrack.write.
|
||||
Stop дожидается reporter, включая непрерываемый Android-запрос, до освобождения AudioTrack.
|
||||
|
||||
`audio detail` сохраняет 2с до и 3с после старта, фактической смены routedDevice или новой аномалии:
|
||||
очередь устройства >100мс в течение секунды, отсутствие продвижения head 100мс при наличии данных,
|
||||
либо новый episode underrun (затухает через 3с без повторений). Порог очереди — диагностический,
|
||||
не настройка JB и не изменение воспроизведения. События внутри окна объединяются, между выгрузками
|
||||
не менее 10с; продолжающаяся аномалия не создаёт повторных дампов. Восстановление логируется INFO.
|
||||
Подробные строки содержат по четыре снимка и заголовок с порядком колонок; при stop окно может быть неполным.
|
||||
Переполнение кольца при задержке reporter имеет отдельный WARN lost=N. Аудиоданные не сохраняются.
|
||||
Выключение call=debug прекращает сбор/выгрузку; буфер и reporter освобождаются с playback worker.
|
||||
Сравнивать сводки следует по call ID и monoUs; рост сетевого запаса сам по себе не является ошибкой.
|
||||
|
||||
## 7. Буфер звонка и восстановление
|
||||
|
||||
`call_jitter` независим от `radio_jitter`: рация сохраняет свои правила задержки и сброса.
|
||||
Буфер звонка хранит до 750 кодированных кадров (15с): 10с перебоя и 5с запаса.
|
||||
Целевой запас — `clamp(max-min, 60, max_reserve_ms)` мс по изменению задержки доставки:
|
||||
на входящем кадре накапливается разность интервалов приёма и отправки, десять секундных интервалов хранят её экстремумы.
|
||||
Отправитель уже передаёт timestamp в CALL_MEDIA; смещение часов пиров исключается, переполнение uint32 учитывается.
|
||||
Подготовка местного аудиоустройства и ускоренное воспроизведение очереди не изменяют оценку доставки.
|
||||
Минимум/максимум глубины очереди для GUI собираются отдельно в аудиопотоке за те же десять секунд.
|
||||
Верхний предел задаётся `call_jitter_max_reserve_ms`: 500–2000 мс, по умолчанию 1000 мс.
|
||||
Настройка есть в аудиопараметрах desktop и Android и применяется к следующему звонку.
|
||||
Начальное и повторное накопление ждёт этот запас; увеличение запаса не прерывает текущую речь.
|
||||
Ускорение зависит от превышения запаса. При отсутствии входящих кадров обычное воспроизведение
|
||||
может исчерпать запас: это не жёсткий запрет воспроизведения ниже цели.
|
||||
Статистика раз в секунду отображается в desktop-окне и Android-экране звонка (под кнопками): глубина, запас, минимум/максимум,
|
||||
темп, RTT, удаления при переполнении и недостачи PCM. Диагностика буфера — категория `call=debug`.
|
||||
После восстановления кадры последовательно декодируются; темп плавно растёт до 1.6x
|
||||
без изменения высоты голоса: линейный предел роста 1.0x→1.6x за 500мс воспроизведения.
|
||||
При уменьшении накопления целевой темп снижается к 1.0x; скорость снижения ограничена тем же пределом.
|
||||
Пауза аудиоустройства не сбрасывает буфер. Переполнение удаляет старейший кадр с WARN и счётчиком.
|
||||
|
||||
Роутер сохраняет последовательность и неподтверждённые данные при смене пути.
|
||||
Для сервиса звонка срок без прогресса ACK увеличен примерно до 20с; прочие сервисы сохраняют прежний срок.
|
||||
Перезапуск удалённого процесса/роутерной сессии не является кратковременным перебоем пути и может сбросить данные.
|
||||
SoundTouch: `USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4`.
|
||||
|
||||
## 8. Тоны и диагностика
|
||||
|
||||
- **Исходящий звонок — две фазы с разными гудками** (у вызывающего):
|
||||
- фаза 1 «пытаемся связаться» (`OUTGOING`: INVITE отправлен/ретранслируется, маршрут
|
||||
устанавливается) — гудок «соединение» (короткие бипы);
|
||||
- фаза 2 «звоним» (`RINGING`: получен RINGING, у пира идёт вызов) — длинные гудки (ringback).
|
||||
Гудки генерируются платформенно-нативно: desktop — цикличные MP3 через `SoundManager`
|
||||
(`call_connecting`/`call_ringing`), Android — цикличные MP3 через `CallRingback`/`MediaPlayer`.
|
||||
Переключение — по `CHAT_EVT_CALL_RINGING`,
|
||||
остановка — по `CALL_ACCEPTED`/`CALL_ENDED`/`CALL_ERROR`/hangup.
|
||||
- **Начальное ожидание первого PCM** → тишина без сигнала потери звука. Подготовка удалённого микрофона
|
||||
и начальное накопление jitter не считаются перебоем. Существующий таймаут 20с без медиа действует с ACTIVE.
|
||||
- **Недобор PCM ≥500мс после начала воспроизведения** → в `call_audio_pull_pcm` воспроизводится
|
||||
`CALL_TONE_GLITCH`: `пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс)
|
||||
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл. До порога — тишина при нехватке PCM.
|
||||
При возобновлении PCM сигнал останавливается; одного входящего пакета без достаточного запаса недостаточно.
|
||||
- **Завершение звонка → всегда** `CALL_TONE_ENDED`: 3 нисходящих 700/500/400 Гц по 150 мс.
|
||||
- Счётчики: tx/rx кадры, `c_tx_dropped`/`c_rx_dropped`, дропы буфера, сталлы,
|
||||
tempo-события, причины завершения. Логи `DEBUG_CATEGORY_CALL` (+стата раз в 1с).
|
||||
|
||||
## 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 39 [call_id:8][rtt:2][buffer:2][tempo:2][dropped:2][underruns:2][min:2][max:2][reserve:2]
|
||||
CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
|
||||
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,292 @@
|
||||
# DB Sync Protocol v2 — Техническое задание
|
||||
|
||||
## 1. Проблема
|
||||
|
||||
chain_hash = SHA256(prev_chain_hash || id || ts || datahash) — хеш-цепочка, где хеш каждой записи зависит от предыдущей.
|
||||
|
||||
`db_record_insert` (db_sync.c:341-463) при вставке записи в середину сортированного порядка делает каскадный пересчёт chain_hash всех последующих записей — O(N). При двустороннем обмене PUSH'ами получается O(N²).
|
||||
|
||||
## 2. Решение
|
||||
|
||||
**Разделить два концерна:**
|
||||
- **Сравнение при синхронизации** — перейти на `datahash` (первые 8 байт SHA256(data), позиционно-независимый). Не требует каскада.
|
||||
- **Целостность цепочки** — `chain_hash` остаётся в схеме БД, но используется только для однократной стартап-проверки и диагностики.
|
||||
|
||||
**Синхронизация:** per-peer позиционная (`synced_pos`). При PUSH каскад не делается. При SEND_DATA батч-вставка + один каскад после батча.
|
||||
|
||||
## 3. Модель данных
|
||||
|
||||
### 3.1. SI_PEER
|
||||
|
||||
```c
|
||||
struct SI_PEER {
|
||||
uint64_t node_id;
|
||||
uint32_t synced_pos; // последняя подтверждённо общая позиция (0-based index)
|
||||
uint8_t sync_state; // 0=not_synced, 1=syncing, 2=synced
|
||||
};
|
||||
```
|
||||
|
||||
- `synced_pos` = индекс последней записи в глобальном порядке `ORDER BY timestamp, datahash`, для которой chain_hash (а после v2 — datahash) подтверждённо совпадает у обоих пиров.
|
||||
- При PUSH-вставке записи на позицию P, для всех peer'ов у которых `synced_pos >= P`: `synced_pos = P - 1`.
|
||||
- При SEND_DATA: `synced_pos = from + received_count - 1`.
|
||||
|
||||
### 3.2. Схема БД (без изменений)
|
||||
|
||||
```sql
|
||||
CREATE TABLE "db_sync_<name>_<id>" (
|
||||
timestamp INTEGER NOT NULL,
|
||||
datahash INTEGER NOT NULL,
|
||||
id INTEGER NOT NULL,
|
||||
chain_hash BLOB NOT NULL,
|
||||
author INTEGER NOT NULL,
|
||||
flags INTEGER NOT NULL DEFAULT 0,
|
||||
data BLOB,
|
||||
author_signature BLOB,
|
||||
delivered_peers INTEGER NOT NULL DEFAULT 0,
|
||||
delivery_chain TEXT NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (timestamp, datahash)
|
||||
);
|
||||
```
|
||||
|
||||
## 4. Wire-формат
|
||||
|
||||
### 4.1. Сообщения, которые меняются
|
||||
|
||||
#### INIT_SYNC
|
||||
|
||||
```
|
||||
Было: [type:1][count:4][last_chain_hash:32] = 37 байт
|
||||
Стало: [type:1][count:4] = 5 байт
|
||||
```
|
||||
|
||||
#### INIT_RESP
|
||||
|
||||
```
|
||||
Было: [type:1][tp:4][chain_at_tp:32][sc:1][(pos:4,chain_hash:32)*sc]
|
||||
Стало: [type:1][tp:4][dh_at_tp:8] [sc:1][(pos:4,dh:8)*sc]
|
||||
|
||||
dh_at_tp — datahash на позиции tp (8 байт вместо 32)
|
||||
sparse — datahash вместо chain_hash (8 байт на элемент вместо 32)
|
||||
|
||||
При sc=0 (exact match): длина 1+4+8+1 = 14 байт (было 37)
|
||||
При sc=16 sparse: длина 14 + 16*12 = 206 байт (было 37 + 16*36 = 613)
|
||||
```
|
||||
|
||||
#### SYNC_DONE
|
||||
|
||||
```
|
||||
Было: [type:1][count:4][last_chain_hash:32] = 37 байт
|
||||
Стало: [type:1][count:4][last_dh:8] = 13 байт
|
||||
```
|
||||
|
||||
### 4.2. Сообщения, которые НЕ меняются
|
||||
|
||||
- **PUSH**: `[type:1][id:8][ts:8][dh:8][dlen:4][data][sig_len:1][sig]` — без изменений. `prev_ch` НЕ добавляется.
|
||||
- **ACK_PUSH**: `[type:1][dh:8][ts:8]` — без изменений.
|
||||
- **REFINE**: `[type:1][from:4][to:4][hc:1][(pos:4,dh:8)*hc]` — уже использует datahash, без изменений.
|
||||
- **SEND_DATA**: `[type:1][from:4][count:2][(id:8,ts:8,dh:8,dlen:4,data,sig_len:1,sig)*count]` — без изменений.
|
||||
- **ERROR**: `[type:1][code:1]` — без изменений.
|
||||
|
||||
## 5. Новые/изменённые функции
|
||||
|
||||
### 5.1. `db_cascade_from`
|
||||
|
||||
```c
|
||||
static void db_cascade_from(struct DB_SYNC_INSTANCE* si, uint32_t from_pos);
|
||||
```
|
||||
|
||||
Выполняет каскадный пересчёт `chain_hash` для записей с позиции `from_pos` до конца таблицы.
|
||||
|
||||
Алгоритм:
|
||||
1. BEGIN IMMEDIATE
|
||||
2. SELECT chain_hash записи на позиции `from_pos - 1` (или zero если from_pos = 0) как prev_ch
|
||||
3. SELECT id, timestamp, datahash от позиции from_pos до конца
|
||||
4. Для каждой: chain_hash = SHA256(prev_ch || id || ts || dh), UPDATE в БД, prev_ch = новый chain_hash
|
||||
5. COMMIT
|
||||
|
||||
Выделяется из текущего кода `db_record_insert` (строки 412-451) в отдельную функцию.
|
||||
|
||||
### 5.2. `db_record_insert` — новый параметр
|
||||
|
||||
```c
|
||||
static int db_record_insert(struct DB_SYNC_INSTANCE* si,
|
||||
uint64_t id, uint64_t ts, uint64_t dh,
|
||||
const char* json, size_t jlen,
|
||||
const uint8_t* sig, size_t sig_len,
|
||||
int do_cascade);
|
||||
```
|
||||
|
||||
- `do_cascade=1` — после INSERT выполняется cascade (строки 412-451). Используется для локальных вставок.
|
||||
- `do_cascade=0` — без cascade. Используется для PUSH и SEND_DATA (каскад делается отдельно).
|
||||
|
||||
### 5.3. `db_verify_chain` (стартап-проверка)
|
||||
|
||||
```c
|
||||
static void db_verify_chain(struct DB_SYNC_INSTANCE* si);
|
||||
```
|
||||
|
||||
Вызывается один раз после `db_sync_instance_add` (перед инициацией sync с пирами).
|
||||
|
||||
Алгоритм:
|
||||
1. Проход записей 0..N-1 в порядке `ORDER BY timestamp, datahash`
|
||||
2. Для каждой: chain_hash = SHA256(prev_ch || id || ts || dh)
|
||||
3. Сравнить с хранимым в БД
|
||||
4. При первом расхождении на позиции P: пересчитать цепочку от P до конца через `db_cascade_from(si, P)`. Завершить.
|
||||
|
||||
Сложность: O(N) SHA256, однократно при старте.
|
||||
|
||||
### 5.4. `si_find_pos` (новая)
|
||||
|
||||
```c
|
||||
static uint32_t si_find_pos(struct DB_SYNC_INSTANCE* si,
|
||||
uint64_t ts, uint64_t dh);
|
||||
```
|
||||
|
||||
Возвращает позицию (0-based index) записи с ключом `(ts, dh)` в глобальном порядке. Используется после вставки PUSH для корректировки `synced_pos`.
|
||||
|
||||
### 5.5. `db_datahash_at` — уже существует (стр. 295)
|
||||
|
||||
Возвращает `datahash` на заданной позиции. Используется для сравнения в INIT_SYNC/INIT_RESP/SYNC_DONE.
|
||||
|
||||
## 6. Обработчики сообщений
|
||||
|
||||
### 6.1. PUSH (`db_handle_push`)
|
||||
|
||||
```
|
||||
Было: db_record_insert(si, ..., /* cascade встроен */)
|
||||
Стало: db_record_insert(si, ..., /* do_cascade= */ 0)
|
||||
uint32_t pos = si_find_pos(si, ts, dh);
|
||||
for each peer: if peer->synced_pos >= pos: peer->synced_pos = pos - 1;
|
||||
```
|
||||
|
||||
### 6.2. INIT_SYNC (`db_handle_init_sync`)
|
||||
|
||||
```
|
||||
Изменения:
|
||||
- Принимает [count:4] вместо [count:4][chain_hash:32] (проверка len >= 5 вместо 36)
|
||||
- Сравнение: datahash вместо chain_hash (8 байт вместо 32)
|
||||
- sparse: datahash вместо chain_hash (8 байт на элемент вместо 32)
|
||||
- Размер sparse-элемента: 4(pos) + 8(dh) = 12 байт (было 4+32=36)
|
||||
- resp буфер: 4096 → достаточно (14 + 16*12 = 206 байт при max sparse)
|
||||
```
|
||||
|
||||
### 6.3. INIT_RESP (`db_handle_init_resp`)
|
||||
|
||||
```
|
||||
Изменения:
|
||||
- Принимает [tp:4][dh:8][sc:1][...] вместо [tp:4][ch:32][sc:1][...]
|
||||
- Проверка len >= 13 вместо 37
|
||||
- Сравнение: datahash вместо chain_hash
|
||||
- Пустая БД пира: zero-хеш 8 байт вместо 32
|
||||
- Sparse элементы: 12 байт вместо 36
|
||||
- При совпадении: synced_pos = tp (в дополнение к sync_state = 2)
|
||||
```
|
||||
|
||||
### 6.4. SEND_DATA (`db_handle_send_data`)
|
||||
|
||||
```
|
||||
Стало:
|
||||
struct SI_PEER* sp = si_peer_find(si, src);
|
||||
uint32_t fix_from = sp ? sp->synced_pos : 0;
|
||||
|
||||
for each record:
|
||||
db_record_insert(si, ..., /* do_cascade= */ 0);
|
||||
|
||||
db_cascade_from(si, fix_from);
|
||||
|
||||
sp->synced_pos = from + received_count - 1;
|
||||
sp->sync_state = 2;
|
||||
```
|
||||
|
||||
Каскад от `fix_from` (позиция, которая была synced ДО этого батча) гарантирует, что все chain_hash после этой точки пересчитаны — независимо от того, какие PUSH'и испортили их между синхронизациями.
|
||||
|
||||
### 6.5. SYNC_DONE (`db_handle_sync_done`)
|
||||
|
||||
```
|
||||
Изменения:
|
||||
- Принимает [count:4][last_dh:8] вместо [count:4][last_chain_hash:32] (len >= 13 вместо 36)
|
||||
- Сравнение последнего datahash вместо chain_hash
|
||||
```
|
||||
|
||||
## 7. Механика многопировой синхронизации
|
||||
|
||||
При наличии нескольких пиров с одинаковым `sync_state` (например, syncing=1) выбирается пир с минимальным `synced_pos` — у него самый старый общий префикс. После завершения его синхронизации выбор повторяется.
|
||||
|
||||
Это гарантирует: двигаемся от самого старого несинхронизированного участка, не прыгая. Новые записи (в хвосте) синхронизируются последними.
|
||||
|
||||
## 8. Полный сценарий: три участника A, B, C
|
||||
|
||||
```
|
||||
Начальное состояние: все пусты.
|
||||
|
||||
=== Шаг 1. A создаёт R1 (ts=1000) ===
|
||||
A: PUSH R1 → B, C. Все вставляют в конец, cascade 0.
|
||||
synced_pos = 0 у всех.
|
||||
|
||||
=== Шаг 2. B создаёт R2 (ts=2000), C создаёт R3 (ts=1500) ===
|
||||
B: [R1(1000), R2(2000)], PUSH R2 → A, C.
|
||||
A: R2 в конец. C: R2 в конец.
|
||||
C: [R1(1000), R2(2000)]? Нет — C создал R3(1500).
|
||||
C: [R1(1000), R3(1500)] локально, потом PUSH R2 → R2.ts=2000 → в конец.
|
||||
C: [R1(1000), R3(1500), R2(2000)]
|
||||
C: PUSH R3 → A, B.
|
||||
A: [R1(1000), R2(2000)] + R3(1500) → между R1 и R2:
|
||||
A: [R1, R3, R2] ← R3 в середину, cascade ПРОПУЩЕН
|
||||
B: [R1, R3, R2] ← аналогично
|
||||
|
||||
synced_pos после PUSH R3 на A относительно B:
|
||||
A вставил R3 на поз.1 → synced_pos_B: было 1 → стало 0
|
||||
(A считает, что совпадает с B только на позиции 0)
|
||||
|
||||
=== Шаг 3. A инициирует sync с B (чемпион: min synced_pos) ===
|
||||
A→B: INIT_SYNC [count=3]
|
||||
B→A: INIT_RESP [tp=min(3,3)-1=2, dh_at_tp=H2, sc=2, sparse:{(1,H3),(0,H1)}]
|
||||
A проверяет: A@2=R2→dh=H2✓, A@1=R3→dh=H3✓, A@0=R1→dh=H1✓
|
||||
→ sync complete, synced_pos_B=2, sync_state=2
|
||||
|
||||
=== Шаг 4. Рестарт A ===
|
||||
db_verify_chain:
|
||||
pos 0: R1.ch = SHA256(0||R1) ✓
|
||||
pos 1: R3.ch = SHA256(R1.ch||R3) ✓ (посчитан при PUSH без cascade — корректен)
|
||||
pos 2: R2.ch = SHA256(R3.ch||R2) ✗ (STALE: был посчитан как SHA256(R1.ch||R2) до PUSH R3)
|
||||
→ расхождение на pos=2 → db_cascade_from(2): пересчёт R2.ch = SHA256(R3.ch||R2) ✓
|
||||
|
||||
=== Шаг 5. A, B, C активно обмениваются ===
|
||||
Каждый PUSH: вставка без cascade.
|
||||
Периодические sync (по min synced_pos): SEND_DATA + один cascade от synced_pos.
|
||||
synced_pos растёт.
|
||||
```
|
||||
|
||||
## 9. Что НЕ трогать
|
||||
|
||||
- `db_sync_insert_signed` — локальная вставка. ts всегда монотонный, запись в конец, `do_cascade=1`, каскад всегда 0 строк.
|
||||
- TTL cleanup (`db_sync_instance_ttl_cb`) — удаление старых записей. Нужен cascade после удаления (уже существующая логика, не меняется).
|
||||
- ACK_PUSH, ERROR, REFINE — без изменений.
|
||||
- Схема БД — без изменений.
|
||||
- `etcp_bind`/эпилог — без изменений.
|
||||
|
||||
## 10. Сложность операций (итого)
|
||||
|
||||
| Операция | Каскад | Сложность |
|
||||
|----------|--------|-----------|
|
||||
| Локальная вставка | 0 строк (конец) | O(1) |
|
||||
| PUSH (приём) | нет | O(1) |
|
||||
| SEND_DATA (батч N записей) | 1 каскад после батча | O(N + tail) |
|
||||
| Стартап-проверка | 1 раз | O(total) |
|
||||
| Sync-протокол (INIT_SYNC/INIT_RESP/REFINE) | нет cascade | O(log N) сообщений |
|
||||
|
||||
## 11. Порядок реализации
|
||||
|
||||
1. **Выделить `db_cascade_from(si, from_pos)`** из тела `db_record_insert`.
|
||||
2. **Добавить параметр `do_cascade`** в `db_record_insert`. При `0` — только INSERT, cascade вызывается отдельно.
|
||||
3. **Добавить `synced_pos`** в `struct SI_PEER`. Инициализировать в 0.
|
||||
4. **Добавить `si_find_pos(si, ts, dh)`** — поиск позиции записи в глобальном порядке.
|
||||
5. **Изменить wire-формат** INIT_SYNC, INIT_RESP, SYNC_DONE (chain_hash → datahash, 32→8 байт).
|
||||
6. **Изменить обработчики**:
|
||||
- `db_handle_init_sync`: новые размеры, datahash вместо chain_hash
|
||||
- `db_handle_init_resp`: новые размеры, datahash вместо chain_hash, обновлять synced_pos
|
||||
- `db_handle_send_data`: `do_cascade=0`, `db_cascade_from(fix_from)`, обновить synced_pos
|
||||
- `db_handle_push`: `do_cascade=0`, скорректировать все synced_pos
|
||||
- `db_handle_sync_done`: обновить synced_pos
|
||||
7. **Добавить `db_verify_chain(si)`** — вызвать при старте после `db_sync_instance_add`.
|
||||
8. **Многопировая синхронизация**: в `db_sync_peer_check_cb` выбирать пира с min synced_pos вместо первого попавшегося.
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
# DM: доставка сообщений и файлов
|
||||
|
||||
Личные беседы принадлежат `src/dm/`. Общие CHAT-группы используются для знакомства,
|
||||
проверки ролей и маршрутизации; группа не становится идентичностью беседы.
|
||||
|
||||
## Реализовано
|
||||
|
||||
- История и постоянная исходящая очередь сообщений в общей SQLite ядра.
|
||||
- Доставка через живой маршрут router, включая BGP-транзит.
|
||||
- При отсутствии маршрута — подписанный PUT доступному суперузлу общей группы сторон.
|
||||
- Приём после проверки подписи и расшифровки, подтверждение только после commit.
|
||||
- Доставка с суперузла после возвращения получателя, независимо от отправителя.
|
||||
- Точное удаление промежуточного сообщения и сохранение квитанции для поздних повторов.
|
||||
- Подготовка, шифрование, проверка и durable публикация медиа в рабочих потоках.
|
||||
- Прямая передача файлов либо custody на отдельном M, доставка после restart и подтверждённое удаление.
|
||||
- TTL и квота M; вложения и статусы доставки в Qt и Android.
|
||||
- Голосовые Opus и видео MP4 с зашифрованными метаданными; общий ввод и отображение с каналами.
|
||||
|
||||
## Идентичность и шифрование
|
||||
|
||||
```text
|
||||
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
|
||||
content_key = SHA256(X25519(my_private, target_public) || "utun_dm_content")
|
||||
msg_nonce = SHA256(conv_id || author || seq)[..13]
|
||||
media_nonce = SHA256("utun_dm_media" || author || media_id || part)[..13]
|
||||
```
|
||||
|
||||
Сообщения и медиа используют один content_key target, без ключей отдельных файлов.
|
||||
AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная порция plaintext — 65535 байт;
|
||||
порция медиа — 32768 байт. UUID медиа новый для каждой подготовки. Повтор доставки
|
||||
использует неизменяемый ciphertext. Повторная подготовка после ошибки требует нового UUID.
|
||||
|
||||
Каноническое тело сообщения:
|
||||
|
||||
```text
|
||||
[conv:8][seq:8][ts:8][author:8][ct_len:1][ct][enc_len:2][ciphertext+tag][author_sig:64]
|
||||
```
|
||||
|
||||
Автор подписывает все поля до sig. conv связывает автора с получателем.
|
||||
Для `ct=file` перед подписью добавляется descriptor `[UUID:16][plaintext_size:8][SHA256(ciphertext):32]`.
|
||||
В зашифрованном теле находится `attachment_info`; пути хранения строятся только из UUID.
|
||||
Размер plaintext сообщения ограничен 1024 байтами. seq — положительное int64.
|
||||
|
||||
```text
|
||||
[kind:1][name_len:2][duration_ms:4][width:2][height:2][UTF8_name:name_len][waveform:100, только voice]
|
||||
```
|
||||
|
||||
Поля метаданных многобайтной длины имеют big-endian формат. kind: 1=file,
|
||||
2=voice, 3=video. Имя — basename до 255 байт, без разделителей, управляющих символов
|
||||
и некорректного UTF-8. FILE имеет нулевые duration/dimensions; VOICE — положительную
|
||||
длительность и 100 символов уровней waveform; VIDEO — длительность и размеры.
|
||||
Длительность ограничена 24 часами, размеры — 16384. Метаданные проверяются до
|
||||
локального commit и до подтверждения получателем; S/M видят только ciphertext.
|
||||
|
||||
Медиа ciphertext — последовательность порций с отдельным tag каждой порции.
|
||||
Последняя порция короче; пустой файл содержит один tag. Размер plaintext и SHA256
|
||||
ciphertext должны входить в подписанный manifest. Потоковая расшифровка проверяет все
|
||||
tags, точный размер и хеш; владелец публикует временный файл только после успеха
|
||||
и собственного durable сохранения.
|
||||
|
||||
## Владение и данные
|
||||
|
||||
- `dm_core`: беседы, история, outbox и подтверждение получателя.
|
||||
- `dm_mailbox`: промежуточные сообщения суперузла и квитанции.
|
||||
- `dm_crypto`: вывод ключей, nonce, CCM и файловые примитивы без БД/сети.
|
||||
- `dm_media`: файлы endpoint, загрузки и отдельная custody M.
|
||||
- `dm_mailbox_media`: durable задания S, выбор M и повтор удаления до DELETE_ACK.
|
||||
- `file_transfer`: подписанная передача байтов, backpressure и отдельный CM handle каждой операции.
|
||||
- `attachment`: формат, валидация и тип метаданных, без сети и БД.
|
||||
- `voice_file`: общий Opus-кодек отправки; `attachment_send` готовит файл в worker
|
||||
и передаёт результат отдельно в `chat_core` либо `dm_media`.
|
||||
|
||||
```text
|
||||
dm_conversations(conv_id, peer_node_id, peer_x25519, peer_ed25519, peer_name,
|
||||
group_id, last_out_seq, last_in_seq, created_at, last_ts)
|
||||
dm_messages(conv_id, dir, seq, ts, author, ct, data, sig)
|
||||
dm_outbox(conv_id, seq, body, retry_at)
|
||||
dm_pending_messages(recipient, sender, seq, msg, group_id, retry_at)
|
||||
dm_mail_receipts(recipient, sender, seq, receipt, group_id)
|
||||
```
|
||||
|
||||
Запись исходящего сообщения, увеличение seq и запись неизменяемого body в outbox —
|
||||
одна транзакция. При ошибке откатывается всё, сообщение не выходит в сеть.
|
||||
Приём, создание/обновление беседы и история — также одна транзакция.
|
||||
|
||||
Dedup — точный ключ (conv,dir,seq) с проверкой сохранённой подписи. last_in_seq —
|
||||
статистика максимального номера, не курсор подтверждения: seq=99 после seq=100
|
||||
сохраняется. Тело с другой подписью для уже сохранённого seq отклоняется.
|
||||
|
||||
У каждой локальной очереди таймер 1с; порция обработки до 32 задач, повтор задачи
|
||||
через 5с. retry_at предотвращает голодание хвоста очереди. Перед отправкой проверяется
|
||||
backpressure router. Потеря пути сохраняет durable задачу; остановка отменяет таймеры,
|
||||
но сохраняет БД. При старте очередь продолжает работу.
|
||||
|
||||
## Подтверждения и протокол
|
||||
|
||||
Сервис `ETCP_RT_ID_DM=0x34`: MSG=1, RECEIPT=2.
|
||||
|
||||
```text
|
||||
["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(message):32][recipient_sig:64]
|
||||
```
|
||||
|
||||
Квитанцию создаёт только получатель после commit. Подпись охватывает первые 72 байта.
|
||||
Отправитель удаляет outbox только после проверки подписи и точного хеша тела.
|
||||
История беседы сохраняется. Подтверждение транспорта router не заменяет квитанцию.
|
||||
|
||||
Сервис `ETCP_RT_ID_DM_MAILBOX=0x35`:
|
||||
|
||||
```text
|
||||
PUT=1 [recipient:8][message]
|
||||
DELIVER=2 [message]
|
||||
RECEIPT=3 [receipt:136]
|
||||
STORED=4 [conv:8][seq:8]
|
||||
```
|
||||
|
||||
Все пакеты идут с подписью router; unsigned control отклоняется.
|
||||
PUT принимается только от автора, при роли supernode=yes у локального узла
|
||||
в общей группе автора и получателя. Роль берётся из подписанного adm_tags;
|
||||
node_type=4 — производная сетевой классификации, для авторизации не используется.
|
||||
storage=yes само по себе не даёт права хранить сообщения.
|
||||
|
||||
STORED подтверждает durable запись суперузлом, outbox до квитанции получателя остаётся
|
||||
у автора. Суперузел повторяет DELIVER при появлении живого маршрута. RECEIPT проверяется
|
||||
по точному сохранённому телу; запись квитанции и удаление копии — одна транзакция.
|
||||
Квитанция пересылается автору. Поздний PUT возвращает её и не возрождает задачу.
|
||||
|
||||
dm_mail_receipts содержит только служебные квитанции; доставленное содержимое
|
||||
с промежуточного узла удаляется. Ранее использовавшийся dm_mail/PULL/HELLO и
|
||||
диапазонные ACK не поддерживаются: это devel, совместимость не сохраняется.
|
||||
TTL текстовых сообщений не введён; недельный TTL из pm.txt относится к медиа.
|
||||
|
||||
## Доставка медиа по pm.txt
|
||||
|
||||
1. Общая передача байтов: явные peer/group/file/size, backpressure и CM handle
|
||||
на всю активную операцию. Владение handle сохраняется после DOWN/TIMEOUT.
|
||||
Используется `ETCP_RT_ID_FILE_TRANSFER=0x37`. Один callback читает/отправляет
|
||||
не более 8 КиБ, продолжение через `call_soon`; inflight ограничен 16 пакетами.
|
||||
2. Отдельный owner dm_media: подготовка ciphertext, manifest, отправка/скачивание,
|
||||
временные файлы, TTL и лимит хранения. Каталоги dm_media/ и dm_pending/ вне
|
||||
группового обхода очистки orphan-файлов; групповые объявления HAVE_BLOCK не используются.
|
||||
3. Суперузел сначала durable регистрирует сообщение и media job, затем выбирает M.
|
||||
M скачивает ciphertext у A и сообщает S подтверждённое хранение, место и expiry.
|
||||
4. Текстовая квитанция и квитанция медиа независимы. Получатель подтверждает медиа
|
||||
после проверки, публикации файла и commit. S хранит задачу удаления у M до DELETE_ACK;
|
||||
поздний STORED не возрождает завершённую доставку.
|
||||
5. TTL медиа `dm_media_ttl_sec` по умолчанию 604800 с; квота `dm_media_storage_mb`
|
||||
по умолчанию 1024 МиБ. Настройки на M. Новая custody отклоняется при
|
||||
недостатке квоты; уже подтверждённые копии не вытесняются молча раньше срока.
|
||||
6. Stop снимает новые задачи и закрывает transfer/CM handles, затем ждёт
|
||||
media_async с живыми owner-контекстами. Группы уничтожаются после owners.
|
||||
|
||||
Control `ETCP_RT_ID_DM_MEDIA=0x38`: OFFER, STORE, STORED, RECEIPT, DELETE,
|
||||
DELETE_ACK, EXPIRED, REJECT; точные форматы заданы в `dm_media.h` и реализациях owners.
|
||||
Подпись router обязательна. Media receipt содержит `DMFILE01`, conv, seq, author,
|
||||
recipient, UUID и SHA256 полного подписанного сообщения; B подписывает первые 88 байт.
|
||||
Отдельные таблицы `dm_files`, `dm_custody`, `dm_custody_done`, `dm_media_jobs`,
|
||||
`dm_media_holders` сохраняют состояния и tombstones. Все выданные grants M
|
||||
учитываются при удалении; повторный STORE не продлевает TTL.
|
||||
|
||||
Qt и Android получают историю через события ядра: 41 — conv + JSON сообщений,
|
||||
45 — JSON бесед. Снимки ограничены 200 записями. Отправка вложения вызывает
|
||||
`dm_send_file` в uasync; длительная обработка выполняется в `media_async`.
|
||||
Android копирует выбранный URI и сохраняет полученный файл через `Dispatchers.IO`;
|
||||
Qt сохраняет файл из контекстного меню в отдельном потоке. Выбор «как файл» сохраняет
|
||||
оригинал; «как видео» в Qt использует общий FFmpeg worker и метаданные конечного MP4.
|
||||
Android записывает MP4 через CameraX; импорт MP4 проходит через платформенный
|
||||
MediaMetadataRetriever в IO. Другие контейнеры на Android отправляются как файлы.
|
||||
|
||||
Голосовая запись отдаёт worker собственный PCM либо компрессор без копирования
|
||||
в GUI/uasync; worker выполняет flush, Opus-кодирование и вычисление waveform.
|
||||
Кодирование Qt и Android использует один `voice_file`. Адресат фиксируется до
|
||||
записи, выбора URI и подготовки; завершение не читает текущую беседу GUI.
|
||||
Подготовленный PM-файл и Android cache удаляются после копирования, ошибки или отмены;
|
||||
пользовательский исходник Qt остаётся во владении пользователя.
|
||||
|
||||
Событие 46 `[target_len:1][target][job_UUID:16][state:1]` сообщает начало (0),
|
||||
готовность (1) и ошибку (2). Target канала — числовой ID, PM — `dm:<conv_id>`.
|
||||
Подготовка, доставка и воспроизведение имеют независимые состояния. UI отображает
|
||||
только задания текущей беседы. Состояния файла и delivery receipt приходят в JSON
|
||||
41; `content_type`, duration, dimensions и waveform преобразуются в общие роли
|
||||
сообщения. Канальные скачивания вызываются контроллером только для каналов;
|
||||
PM-файлы доставляются автоматически через `dm_media`.
|
||||
|
||||
Android использует общие `ConversationComposer`/`ConversationMessages` для обеих
|
||||
бесед; Qt — `InputBar` и `MessageDelegate`. Превью Qt создаются в общем пуле
|
||||
с двумя workers; декодирование длинного Opus-файла выполняется вне GUI. Оба плеера
|
||||
проверяют заголовок, пакеты и end marker; Qt ограничивает PCM буфер 256 МиБ.
|
||||
|
||||
## Проверки
|
||||
|
||||
`test_dm`: симметрия ключа target, CCM round-trip, пустое тело, пределы длины,
|
||||
неверный ключ/tag, разделение nonce; потоковые файлы на границах 32 КиБ,
|
||||
неверные author/UUID/hash, обрезание и лишние байты.
|
||||
|
||||
`test_dm_e2e`: реальные A–C–B соединения и BGP; нет физического A–B линка.
|
||||
Проверяет out-of-order, dedup/conflict, подписи, SQL rollback при приёме и enqueue,
|
||||
ACK чужого тела, отказ mailbox без роли, точное удаление и поздний PUT.
|
||||
Первый сценарий передаёт 40 offline-сообщений (больше порции pump) и перезапускает источник с pending outbox; второй перезапускает
|
||||
суперузел и доставляет первое сообщение при выключенном источнике.
|
||||
|
||||
`test_dm_media`: реальные A/B/S/M с раздельными ролями supernode и storage;
|
||||
прямая передача, custody, restart S/M при выключенном A, точный файл на B,
|
||||
удаление копии и тела S, поздние STORE/STORED, TTL, отказ по квоте, неверная квитанция,
|
||||
SQL rollback регистрации, состояния JSON и отмена операции с CM handle.
|
||||
Голосовое передаётся напрямую; метаданные видео сохраняются после offline custody
|
||||
и restart S/M при выключенном источнике.
|
||||
|
||||
`test_dm_worker`: повторное шифрование 8 МиБ не менее 0,5 с и проверка расшифровки;
|
||||
work выполняется вне uasync, done в его потоке, heartbeat продолжает работать.
|
||||
Destroy отменяет уже posted completion, callback вызывается ровно один раз.
|
||||
|
||||
`test_attachment`: строгий формат, повреждения и граничные длины, реальное
|
||||
кодирование/декодирование 50 Opus-кадров, длительность и waveform.
|
||||
`test_attachment_send`: выбранная беседа после смены текущего target, освобождение
|
||||
PCM в worker, остановка до completion без регистрации и удаление отклонённого cache.
|
||||
Qt `test_voice_file`: реальные PCM/Opus, повреждённые файлы и отмена async decoder.
|
||||
|
||||
Проверка 01.10.2026: чистые сборки Qt и Android APK; полный `check.sh` —
|
||||
117 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root), proxy/burst/load PASS.
|
||||
Qt CTest: 7 PASS, 0 FAIL; Android APK собран после `clean`.
|
||||
В тесте worker максимальный интервал heartbeat составил 10,3 мс.
|
||||
Интерактивная проверка GUI на телефоне и десктопе не выполнялась.
|
||||
@@ -1,118 +0,0 @@
|
||||
# Зашифрованные вложения группового чата
|
||||
|
||||
В desktop chatgui после выбора файла или перетаскивания в чат рядом с его именем
|
||||
появляется флажок «Зашифровать вложение». Он применяется к выбранному файлу,
|
||||
изображению или видео. Удаление, замена файла и переход в другую беседу сбрасывают
|
||||
флажок. Выбор фиксируется вместе с адресатом при отправке, до подготовки.
|
||||
В DM флажок скрыт: используется существующее автоматическое E2E-шифрование лички.
|
||||
|
||||
Android показывает зашифрованное вложение отдельной карточкой со статусом запроса
|
||||
ключа. После одобрения карточка обновляется на исходный файл, изображение, видео
|
||||
или голосовое. В интерфейс передаётся только расшифрованный путь; контейнер `.uenc`
|
||||
нельзя открыть или сохранить как готовое вложение. Цитаты и список чатов показывают
|
||||
название «Зашифрованное вложение» вместо manifest.
|
||||
|
||||
Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую
|
||||
подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер
|
||||
передаётся через обычные media_index/media_delivery. UUID транспортного медиа
|
||||
и crypto_UUID имеют разные назначения: первый идентифицирует сетевые блоки,
|
||||
второй — контейнер и ключ. Повтор доставки передаёт прежний контейнер.
|
||||
|
||||
## Подписанное сообщение и контейнер
|
||||
|
||||
В подписанном сообщении `ct=application/x-utun-encrypted`, базовое имя тела —
|
||||
`<crypto_UUID:32hex>.uenc`. Остальные поля manifest описывают шифротекст:
|
||||
размер, SHA256 всего контейнера, UUID и подписи блоков. Ключ, исходное имя,
|
||||
MIME, waveform и остальные медиаметаданные в историю канала не записываются.
|
||||
|
||||
Контейнер:
|
||||
|
||||
```text
|
||||
magic "UTENC001":8
|
||||
crypto_UUID:16
|
||||
plaintext_size:8BE
|
||||
encrypted_metadata_size:2BE
|
||||
SHA256(encrypted_file_body):32
|
||||
encrypted_metadata:variable
|
||||
encrypted_file_body:variable
|
||||
```
|
||||
|
||||
Метаданные содержат MIME в 32 байтах и сериализованный `attachment_info`.
|
||||
Каждая часть шифруется AES-256-CCM с тегом 16 байт и nonce 13 байт.
|
||||
Nonce метаданных использует `part=UINT64_MAX`; файл — порции 32768 байт
|
||||
с номерами от нуля. Общие примитивы находятся в `media_async/media_crypto`;
|
||||
формат существующего DM при их выделении не менялся.
|
||||
|
||||
Получатель проверяет подписанный SHA256 полного контейнера, UUID, tag метаданных,
|
||||
все tags файла, точный размер и hash тела. Результат пишется во временный файл
|
||||
и публикуется после успешной проверки и durable rename. Все длительные файловые
|
||||
операции и криптография выполняются в media_async.
|
||||
|
||||
## Хранение и разрешения
|
||||
|
||||
Локальные таблицы не регистрируются в db_sync или Merkle:
|
||||
|
||||
- `attachment_keys`: ключ автора/получателя, связь с общим message_id,
|
||||
crypto_UUID, hash и пути двух копий, расшифрованные метаданные и состояние.
|
||||
- `attachment_key_requests`: один запрос на `(channel,message_id,peer)`,
|
||||
направление, решение и дата создания.
|
||||
- `attachment_key_outbox`: команда, адресат и время следующей попытки.
|
||||
|
||||
Шифротекст хранится в `media/<channel>/<crypto_UUID>.uenc`, локальная открытая
|
||||
копия — в `decrypted/<channel>/<crypto_UUID>.file`. В media_files и объявлениях
|
||||
держателей остаётся только шифротекст, включая backfill после restart.
|
||||
Квота медиакеша учитывает обе копии; очистка кеша не удаляет ключ. При повторном
|
||||
открытии удалённая открытая копия восстанавливается с сохранённым ключом.
|
||||
|
||||
После полной проверенной загрузки обычный клиент автоматически запрашивает
|
||||
ключ автора. Фоновая копия узла с подписанной ролью `storage=yes` запрос не
|
||||
создаёт; ручная пользовательская загрузка на таком узле запрашивает доступ.
|
||||
Сетевая классификация `peers.storage` для этого не используется.
|
||||
|
||||
В шапке канала кнопка «Запросы расшифровки» показывает число запросов без решения.
|
||||
Список содержит участника, вложение, дату и отдельные кнопки выдачи/отказа.
|
||||
Двойной клик переводит к исходному сообщению. После выдачи видны ожидание доставки
|
||||
и подтверждённая доставка ключа; отказ также сохраняется.
|
||||
|
||||
## Адресный протокол
|
||||
|
||||
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x40`. Каждый пакет обязан иметь
|
||||
`ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED`; промежуточные узлы ключа не видят.
|
||||
Протокол не создаёт личную беседу и не публикует служебные сообщения в канал.
|
||||
|
||||
```text
|
||||
magic "AK01":4 | command:1 | message_id:32 | crypto_UUID:16 | cipher_hash:32
|
||||
GRANT дополнительно содержит key:32
|
||||
|
||||
REQUEST=1, REQUEST_ACK=2, GRANT=3, DENY=4, RESULT_ACK=5
|
||||
```
|
||||
|
||||
Автор принимает запрос только на собственное опубликованное сообщение с
|
||||
сохранённым ключом. Участие запрашивающего проверяется по полному подписанному
|
||||
мемберу, join и дереву приглашений; удалённые мемберы отклоняются. Проверка
|
||||
повторяется при решении и перед каждой отправкой GRANT. Получатель принимает
|
||||
ответ только от автора, по своему существующему запросу и точной идентичности
|
||||
контейнера. Конфликтующие ключи и результаты отклоняются.
|
||||
|
||||
REQUEST_ACK отправляется после сохранения запроса. GRANT/DENY появляются
|
||||
в outbox только после сохранения решения. Полученный ключ сохраняется до
|
||||
RESULT_ACK и до запуска расшифровки. Повторы возвращают прежнее решение и не
|
||||
создают новых строк. Таймер обрабатывает ограниченную порцию очереди с
|
||||
backpressure и повтором через 5 секунд; restart продолжает очереди.
|
||||
|
||||
Для доставки нужен живой маршрут обеих сторон: офлайн-mailbox этого сервиса
|
||||
не реализован. Запрос ждёт появления автора; решение ждёт появления получателя.
|
||||
Уже выданный ключ невозможно отозвать у получателя. Защита содержимого не
|
||||
скрывает автора, канал, время и размер шифротекста. Автоматическая публикация
|
||||
транскрипции зашифрованного голосового в канал не запускается.
|
||||
|
||||
## Проверки
|
||||
|
||||
`test_encrypted_file`: границы порций, пустой файл, metadata roundtrip,
|
||||
неверные ключ/автор/UUID/hash, подмена метаданных с пересчитанным внешним hash,
|
||||
лишние байты и отсутствие частичного plaintext после отказа.
|
||||
|
||||
`test_encrypted_attachment`: реальные A–S–B/C, скачивание с S при выключенном A,
|
||||
перезапуски автора с запросами и получателя с ключом, выдача B и отказ C,
|
||||
отсутствие ключа на S/C, отрицательные control-пакеты, повтор GRANT,
|
||||
восстановление удалённого plaintext без нового согласия и удаление канала.
|
||||
@@ -0,0 +1,10 @@
|
||||
В конфиге явно прописываются сокеты с ip и портом.
|
||||
Через эти сокеты идёт весь трафик, случайные порты клиент не использует - только эти сокеты.
|
||||
Соответственно, возможен только один линк между двумя пирами по одному каналу.
|
||||
|
||||
виртуальные сокеты - плохо т.к. используются ip/port для поиск подключения
|
||||
|
||||
|
||||
|
||||
./configure --with-openssl - openssl
|
||||
./configure --without-openssl - tinycrypt
|
||||
@@ -1,178 +0,0 @@
|
||||
# Разбор полного прогона 2026-09-27
|
||||
|
||||
Исходный результат `./check.sh` вне песочницы: 92 passed, 5 failed, 1 skipped (98 тестов).
|
||||
Песочница запрещает сетевые сокеты; её результаты не использовались для оценки протокола.
|
||||
|
||||
Промежуточный прогон: 96 passed, 1 failed, 1 skipped; оставался `test_chat_join_e2e`.
|
||||
После KEY_REGISTER_ACK и исправления невыровненных чтений повторный полный
|
||||
`make check -j4`: **97 passed, 0 failed, 1 skipped (98)**.
|
||||
Лог: `/tmp/utun-ack-final-check.log`. Исходное падение отдельного root-теста и
|
||||
последующая доработка описаны ниже. После исправления autosockets общий набор дал
|
||||
97 PASS / 1 SKIP, а `check-proxy`, `check-burst`, `check-load` прошли:
|
||||
`/tmp/root-fix-full-check.log`.
|
||||
|
||||
`test_etcp_lifecycle`, `test_etcp_link_stress`, `test_conn_mgr_phases` дополнительно
|
||||
прошли ASan/UBSan/LeakSanitizer. Инструментированы тесты и изменённые транспортные/
|
||||
маршрутные модули, остальные объекты библиотек взяты из обычной сборки.
|
||||
Логи: `/tmp/ncd-late-asan.log`, `/tmp/link-role-asan.log`, `/tmp/cm-phases-asan.log`.
|
||||
|
||||
## Исправленные причины
|
||||
|
||||
- `test_conn_mgr_phases`: после TIMEOUT фазы DIRECT NCD навсегда подавлял UP,
|
||||
хотя conn_mgr сохранял handle для REVERSE. В логе физический ETCP уже UP,
|
||||
но conn_mgr продолжал DIRECT_REQ до своего таймаута. TIMEOUT теперь обозначает
|
||||
истечение срока первой попытки, CLOSED — окончательное завершение. Удерживаемый
|
||||
handle получает поздний UP. Отдельный lifecycle-регрессионный тест воспроизводит
|
||||
TIMEOUT → INIT/UP (одно уведомление) → DOWN → UP; до исправления он падал.
|
||||
- `test_etcp_link_stress`: ожидал reset всей сессии при коллизии дополнительного
|
||||
линка. Проверка заменена на сохранение эпох, отсутствие reset и дублей линков,
|
||||
доставку и ACK сообщений через новый линк. Прежний тест забирал сырые фрагменты
|
||||
вместо normalizer и мог считать служебные пакеты полезным трафиком. Теперь он
|
||||
использует отдельный ETCP service и проверяет каждый байт и порядок сообщений.
|
||||
Эта проверка выявила ошибку реализации: при принятии встречного INIT сохранённый
|
||||
исходящий линк не менял `is_server`. Роль обновляется при отправке INIT_RESPONSE.
|
||||
- `test_bgp_route_exchange`: после разрыва B–C recovery успешно строил C–A,
|
||||
нарушая предположение теста о фиксированной цепочке. Теперь A и C принимают
|
||||
только ключ B: все прежние проверки withdraw/restore сохранены, обход запрещён
|
||||
условиями тестовой сети. Автоматический обход отдельно проверяет `test_topo_recovery`.
|
||||
- `test_tcp_io`: адрес 192.0.2.1:81 в окружении устанавливал TCP-соединение,
|
||||
поэтому connect timeout закономерно не срабатывал. Теперь используется локальный
|
||||
listener с заполненной accept-очередью, проверяется ETIMEDOUT. Перед уничтожением
|
||||
uasync тест завершает отложенное освобождение TCP и проверяет баланс таймеров.
|
||||
|
||||
Диагностика тестов включается `UTUN_TEST_DEBUG=1`; обычный запуск не включает DEBUG.
|
||||
Остаточные DATA после удаления соединения могут быть undecryptable и сами по себе
|
||||
не означают ошибку транспорта.
|
||||
|
||||
## Исправлено: гонка регистрации приглашения
|
||||
|
||||
`test_chat_join_e2e`, happy path A != C, падал из-за отсутствия подтверждения KEY_REGISTER.
|
||||
Раньше `chat_invite_build_link()` вызывал `chat_join_register_key()` и сразу возвращал ссылку.
|
||||
Успех `etcp_route_send()` означает постановку в очередь. Он не подтверждает обработку
|
||||
ключа посредником C. Параллельное подключение J–C может завершиться раньше доставки A–C.
|
||||
|
||||
Доказательство: `/tmp/join-e2e-debug.log`, один ключ `90c4b081833450a0`:
|
||||
|
||||
- 01:39:02.437851 — A пишет `join-key registered`, фактически ключ только поставлен в отправку.
|
||||
- 01:39:02.446527 — C отвергает JOIN_INFO_REQ: ключ ещё неизвестен.
|
||||
- 01:39:02.449724 — C сохраняет этот ключ, но J уже получил терминальный ERROR.
|
||||
|
||||
Теперь C отвечает KEY_REGISTER_ACK только после сохранения ключа. A сопоставляет
|
||||
channel/target/key и выдаёт ссылку после успешного ACK и локального сохранения ключа.
|
||||
Отрицательный ACK завершает запрос ошибкой. Чужие, неподписанные, незашифрованные,
|
||||
повторные и запоздалые подтверждения не могут завершить другой запрос.
|
||||
|
||||
Контракт асинхронных API `chat_join_register_key` / `chat_invite_build_link`:
|
||||
NULL означает ошибку запуска без callback; принятый запрос даёт ровно один callback.
|
||||
Обычное завершение происходит после возврата API, включая self. Cancel/destroy
|
||||
завершают запрос синхронно с CANCELLED. Handle недействителен после callback.
|
||||
Для удалённого узла дедлайн 5 секунд; доставку и повтор потерянных пакетов обеспечивает
|
||||
надёжный router. Повторная регистрация того же ключа тем же инвайтером обновляет TTL;
|
||||
замена инвайтера для существующего ключа запрещена. Отмена может оставить ключ на C
|
||||
до истечения TTL, но A не авторизует по нему вход, поскольку локально такой ключ
|
||||
сохраняется только при успешном завершении.
|
||||
|
||||
GUI получает request_id в событии и игнорирует устаревшие результаты. Новый запрос
|
||||
отменяет предыдущее ожидание GUI. Headless связывает ответ с JSON id и отменяет все
|
||||
ожидания отключившегося клиента. Ни один из путей не запускает вложенный event loop.
|
||||
|
||||
Дополнительные исправления:
|
||||
|
||||
- Устранён double-free при ошибке `etcp_route_send`: пакет уже освобождён роутером.
|
||||
- Headless прекращает разбор пакета после `quit`; последующая команда не создаёт
|
||||
запрос на уже закрытом клиенте. При destroy отменяются cleanup_timer и запросы,
|
||||
удаляются клиентские сокеты и освобождается контекст.
|
||||
- UBSan обнаружил невыровненное чтение group_id из `d + 1` в chat_sync. Оно и три
|
||||
аналогичных диагностических чтения заменены на memcpy.
|
||||
- Тест chat_join теперь останавливает вручную запущенный member_sync;
|
||||
проверяется баланс выделения/освобождения таймеров.
|
||||
|
||||
Проверки:
|
||||
|
||||
- `test_chat_join`: 31/31, включая задержанный/отсутствующий ACK, таймаут,
|
||||
неверные channel/peer/key/flags/размер/status, дубликат, отказ хранения из-за
|
||||
конфликта инвайтера, отмену, shutdown, параллельные ключи и ошибку отправки.
|
||||
- `test_chat_join_e2e`: 3/3 (A≠C, A=C, неверный ключ). C принудительно откладывает
|
||||
регистрацию на 200 мс; проверяется отсутствие ссылки до обработки регистрации.
|
||||
Дополнительно проверяются подавление старого GUI-запроса и реальный headless
|
||||
control socket: параллельные ответы, валидность ссылок, quit и команда после него.
|
||||
- ASan/UBSan: оба теста проходят. LeakSanitizer и баланс таймеров проверены в
|
||||
test_chat_join; E2E использует fork/_exit, поэтому сам по себе не проверяет утечки
|
||||
при завершении дочерних процессов. Инструментированы изменённые chat-модули и
|
||||
transport/routing, остальные объекты взяты из обычной библиотеки.
|
||||
Логи: `/tmp/utun-ack-join-asan.log`, `/tmp/utun-ack-e2e-asan.log`.
|
||||
|
||||
## Отдельный root-тест auto_socket_dynamic
|
||||
|
||||
Запуск в `sudo unshare --net` с поднятым loopback закончился TIMEOUT через 30 секунд.
|
||||
Лог: `/tmp/utun-ack-root-checks.log`. Это не PASS основного набора: в нём тест пропущен.
|
||||
|
||||
В конфиге теста включён auto_sockets, но не отключён стандартный фильтр
|
||||
`auto_socket_skip_no_default_route=1`. Dummy-интерфейсы теста не имеют default route,
|
||||
а `auto_socket.c:reconcile_iface()` исключает такие интерфейсы. Лог показывает
|
||||
`no links created`, затем TIMEOUT. Исправленный тест создаёт собственный network
|
||||
namespace, поднимает loopback и добавляет default route только клиентским dummy.
|
||||
Фильтр остаётся включённым; отдельно проверяется исключение серверного dummy,
|
||||
иначе его сокет скрывал бы полный обрыв клиентской сети.
|
||||
|
||||
Кроме того, тест сохраняет handle общего дедлайна, traffic_send и traffic_monitor
|
||||
в одну переменную timeout_handle; фазовые таймеры вообще не сохраняются. При выходе
|
||||
не отменялись все таймеры; зафиксирован остаток 9 таймеров (665/656). В обработчиках
|
||||
трафика освобождался entry без queue_dgram_free. Теперь у каждого таймера свой slot,
|
||||
waiters отменяются до teardown, payload освобождается, deferred cleanup завершается
|
||||
до проверки баланса таймеров, сокетов и `u_get_allocated_count()`.
|
||||
|
||||
Новый генератор шлёт 1024-байтовые пакеты независимо в обе стороны, по одному через
|
||||
threshold waiter, с бюджетом 64 пакета на tick. Проверяются содержимое, строгий seq,
|
||||
ограниченность TX/RX очередей, реальное срабатывание backpressure, остановка
|
||||
генераторов при полном обрыве и полное опустошение после восстановления. NCD handle
|
||||
удерживает логическую сессию: смена интерфейса не является разрешением терять её данные.
|
||||
Остатки после действительно удалённого соединения — отдельный допустимый случай.
|
||||
|
||||
Прогон нагрузки обнаружил ошибки, скрытые прежними проверками:
|
||||
|
||||
- `tcp_socket_add` не сохранял `netif_index`: монитор принимал interface-bound сокет
|
||||
за сокет default route и менял его адрес при событиях другого интерфейса.
|
||||
- Удаление TCP модели искало её по имени уже удалённого интерфейса и порту из БД.
|
||||
Теперь запись интерфейса хранит имя и прямую ссылку на модель; один путь закрывает
|
||||
listener, линки и модель. Портовые записи удаляются с сохранённым именем.
|
||||
- `tcp_socket_remove` не закрывал линки, созданные вне NCD. Теперь закрывает все
|
||||
связанные линки до освобождения модели; root-тест создаёт такой линк отдельно.
|
||||
- Поиск UDP модели не исключал TCP. Добавлена проверка типа сокета.
|
||||
- При selective ACK sender мог выдавать новые seq за границей `MAX_INFLIGHT_SIZE`
|
||||
относительно cumulative ACK. Теперь генерация seq останавливается на границе,
|
||||
cumulative ACK возобновляет очередь даже при отсутствии уже selectively-acked записей.
|
||||
Тесты проверяют блокировку, возобновление и uint32 wrap. ACK не может подтвердить
|
||||
ещё не назначенный seq; декодирование старшего байта seq/ACK не делает signed shift.
|
||||
- Timestamp Q16 умножался как signed int и переполнялся на циклических/отрицательных
|
||||
смещениях. Перевод выполняется в uint32. Jitter раньше всегда получал нулевую разницу:
|
||||
предыдущий RTT читался после перезаписи; теперь берётся до неё, EWMA использует int64.
|
||||
- UBSan выявил невыровненные canary/указатели в `mem.c` и `memory_pool.c`. Метаданные
|
||||
читаются/пишутся через memcpy; покрыты размеры блоков 1..17 и повторное использование пула.
|
||||
- Повторный общий прогон поймал `test_etcp_connect`: PID-порты пересекались с занятым
|
||||
UDP-портом (`EADDRINUSE`). Теперь сервер bind-ится на порт 0, клиент получает фактический
|
||||
порт через getsockname; собственного алгоритма поиска свободного порта нет.
|
||||
|
||||
Root ASan/UBSan/LSan: PASS, 153335 / 175912 пакетов, reinit=0, все очереди пусты,
|
||||
баланс памяти/таймеров/сокетов совпал (`/tmp/root-fix-asan.log`). Инструментированы
|
||||
тест, ETCP/session/router/NCD/autosocket/STCP/normalizer, очереди, event loop и аллокаторы;
|
||||
прочие объекты архивов обычные. Unit-логи: `/tmp/root-session-asan.log`,
|
||||
`/tmp/root-memory-asan.log`. LSan запускается вне sandbox: под ptrace он не работает.
|
||||
|
||||
Финальная чистая обычная сборка: `./check.sh` — **97 PASS / 0 FAIL / 1 SKIP**;
|
||||
proxy, burst и load — PASS (`/tmp/root-fix-final-check.log`). Отдельный root-тест —
|
||||
PASS, 188069 / 191552 пакета, все очереди пусты, reinit=0, баланс ресурсов совпал
|
||||
(`/tmp/root-test-final.log`). После добавления drain отложенной очистки
|
||||
`test_etcp_connect` повторно прошёл без остаточных таймеров (`/tmp/root-connect-final.log`).
|
||||
|
||||
Подробные socket/connection INFO включаются только через `UTUN_TEST_DEBUG=1`.
|
||||
`ENETUNREACH` непосредственно при принудительном удалении IP/интерфейса ожидаем до
|
||||
обработки netlink; тест требует последующей доставки всех данных. Ошибки выхода
|
||||
за receive window недопустимы и после исправления sender gate не наблюдаются.
|
||||
|
||||
## Попутная проблема диагностики
|
||||
|
||||
`lib/debug_config.c:debug_parse_config()` требует `:`, хотя документация описывает `=`.
|
||||
Кроме того, функция всё ещё вычисляет индекс через битовую маску, хотя категории
|
||||
теперь являются индексами. Исправление в этот набор не включено; тесты используют
|
||||
`debug_set_category_level()` напрямую.
|
||||
+4
-68
@@ -41,12 +41,6 @@ inflight - две очереди (ll_queue):
|
||||
- send_hist[8] - история через какие линки отправлялся (индекс линка)
|
||||
- inflight_bytes/inflight_packets per-link отслеживается в ETCP_LINK
|
||||
Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight.
|
||||
Отдельно ограничиваем размах sequence относительно кумулятивного ACK: новый seq выдаётся
|
||||
только если (uint32_t)(next_tx_id - rx_ack_till) <= MAX_INFLIGHT_SIZE (16384).
|
||||
Selective ACK освобождает отдельную inflight-запись, но не продвигает эту границу:
|
||||
приёмник пока не может освободить пакеты за дыркой. Кумулятивный ACK возобновляет
|
||||
input_queue даже если подтверждённые записи уже удалены по selective ACK.
|
||||
ACK ограничивается последним назначенным seq (next_tx_id - 1); сравнения учитывают uint32 wrap.
|
||||
optimal_inflight расчитывается из RTT и bandwidth линка.
|
||||
|
||||
bandwidth по каждому линку адаптивно подстраиваем:
|
||||
@@ -159,7 +153,7 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
|
||||
- session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует)
|
||||
- mtu_local: uint16_t локальный MTU сервера
|
||||
- local_link_id: uint8_t идентификатор канала сервера
|
||||
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
|
||||
- remote_socket_id: uint8_t (копируется из запроса)
|
||||
- only_local: uint8_t флаг сервера
|
||||
- type: uint8_t тип сокета сервера
|
||||
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal)
|
||||
@@ -167,21 +161,15 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
|
||||
- random_padding: случайные байты
|
||||
|
||||
3) PING (0x06):
|
||||
[0x06] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...] [padding...]
|
||||
[0x06] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...]
|
||||
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
|
||||
Отправляется с отдельным crypto-контекстом (не связан с ETCP-сессией).
|
||||
Используется для проверки доступности узла и NAT-тестирования.
|
||||
|
||||
4) PONG (0x07):
|
||||
[0x07] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] [padding...]
|
||||
[0x07] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...]
|
||||
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
|
||||
Ответ на PING. nonce копируется из запроса для сопоставления.
|
||||
Nonce генерируется CSPRNG. Проверяются также ключ отправителя, source IP:port,
|
||||
ID и поколение принимающего сокета. Флаг OBSERVE (0x04) запрашивает ответ
|
||||
address(19) || tag(25): family(4/6), IP(16), port(2, BE), NAT2UDP!(8),
|
||||
socket_id(1), generation(8, BE), round(8, BE).
|
||||
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
|
||||
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
|
||||
|
||||
При получении init получатель пакета должен:
|
||||
- reset ETCP_LINK с этим ip_port если он есть
|
||||
@@ -189,65 +177,13 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
|
||||
приём кодограмм из UDP выглядит так:
|
||||
uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue
|
||||
|
||||
Keepalive: [0x08] [flags(1)] [period_ms(2 LE)] [sleep_fields?:4] [padding...].
|
||||
При KA_FLAG_SLEEP добавляются active_remaining(2 LE) и sleep_duration(2 LE), оба в единицах 0.1 с.
|
||||
Полезное тело занимает 4 байта без SLEEP или 8 байт с SLEEP; остальные байты — padding.
|
||||
Keepalive: пакет без секций (только timestamp+flag_up в шифрованном заголовке, data_len=0).
|
||||
keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика.
|
||||
Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента).
|
||||
flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты).
|
||||
|
||||
Padding keepalive, PING/PONG и STCP-handshake:
|
||||
- ETCP_CONN хранит собственный диапазон min/max и независимый таймер для всех своих линков.
|
||||
- UTUN_INSTANCE хранит отдельный диапазон и таймер для PING/PONG без существующего ETCP_CONN.
|
||||
- При создании владельца и каждом обновлении: min=rand(0,30), max=min+rand(32,128), границы включительные.
|
||||
- Следующее обновление назначается через rand(50,300) секунд. При закрытии владельца таймер отменяется.
|
||||
- Каждый пакет независимо выбирает длину rand(min,max); padding заполняется нулями ДО шифрования.
|
||||
- UDP PING/PONG: padding шифруется перед CRC/tag; открытый salt+pubkey остаётся в конце датаграммы.
|
||||
user_data_len/resp_data_len обозначают только полезные данные. PONG не копирует padding запроса.
|
||||
- STCP: salt(8)+pubkey(32) открыты, затем 51 байт зашифрованного handshake и padding_size байт
|
||||
нулевого padding, зашифрованного продолжением того же CTR. Приём продвигает CTR через padding;
|
||||
padding_size=0 сразу завершает handshake. Параметры берутся у ETCP_CONN, иначе у instance.
|
||||
- Каждый STCP-поток использует соль своего отправителя: клиентскую для client→server, серверную для server→client.
|
||||
CTR IV = первые 12 байт SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream") + четыре нулевых байта.
|
||||
Отправка инициализируется после генерации своей соли, приём — после чтения соли пира, до расшифровки тела.
|
||||
После handshake CTR непрерывно продолжается через padding и DATA, без повторной инициализации на кадр.
|
||||
- Padding не должен превышать 158 байт. Для UDP длина ограничивается оставшимся MTU;
|
||||
если не помещается даже min, отправка завершается ошибкой без обрезания пользовательских данных.
|
||||
- Диагностика диапазонов/обновлений/PING/PONG: etcp=debug; keepalive: keepalive=debug.
|
||||
|
||||
Клиент: если все линки =down то начинается процедура восстановления связи:
|
||||
- клиент посылает init без сброса (0x04), link_state=2
|
||||
- сервер как получает init проверяет session_id:
|
||||
- если session_id совпадает — шлёт подтверждение без сброса (0x05)
|
||||
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03)
|
||||
|
||||
**** Connection session confirmation (2026-09) ****
|
||||
Physical UDP INIT / STCP handshake establishes an authenticated link. A stream reset
|
||||
uses the same control protocol on every initialized UDP or TCP link, bypassing the
|
||||
normalizer and reliable DATA/ACK queues. A physical reset does not generate DOWN/UP.
|
||||
|
||||
Encrypted DATA/ACK plaintext after timestamp(2) + flag_up(1):
|
||||
09 | sender_epoch:u64be | receiver_epoch:u64be | existing ACK/DATA sections
|
||||
The extra 17 bytes participate in the MTU budget. Unwrapped DATA/ACK and frames with
|
||||
an epoch mismatch are discarded before stream state or link liveness is updated.
|
||||
|
||||
Control frames (all 25 bytes after timestamp/flag):
|
||||
0a HELLO | sender_epoch | receiver_epoch | 0
|
||||
0b CHALLENGE | sender_epoch | receiver_epoch | random_cookie:u64be
|
||||
0c CONFIRM | sender_epoch | receiver_epoch | echoed_cookie:u64be
|
||||
|
||||
Each connection owns one pending peer epoch, fresh challenge, and retry timer.
|
||||
HELLO and a changed epoch advertised by INIT only request a challenge. Only a
|
||||
CONFIRM for the current local epoch and pending random challenge commits a peer
|
||||
epoch. Changing the peer epoch resets the stream but preserves our own epoch.
|
||||
A local fatal reset generates a new local epoch and blocks DATA until confirmation.
|
||||
Both sides may reset concurrently. Loss of any control frame is recovered by a
|
||||
500 ms retry on all initialized links. After 10 s an unconfirmed local reset closes
|
||||
the connection; an unconfirmed candidate alone is discarded without closing a
|
||||
working session. Close cancels the timer before destroying links.
|
||||
|
||||
TCP may initially receive peer epoch zero because the server creates ETCP_CONN
|
||||
only after STCP handshake. It completes the same session confirmation before INIT/UP.
|
||||
REINIT notifies stream consumers without tearing down physical ownership. The
|
||||
router retains end-to-end inflight; BGP repeats JOIN_GROUP and TABLE_REQUEST because
|
||||
its previous control messages may have been discarded by the stream reset.
|
||||
+42
-47
@@ -1,5 +1,3 @@
|
||||
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md).
|
||||
|
||||
# ETCP Router Architecture
|
||||
|
||||
## Структуры данных
|
||||
@@ -22,8 +20,7 @@ ETCP_ROUTER_CONN (один на пару remote_node_id + svc_id)
|
||||
│ │
|
||||
│ ack_timer: void* периодический 100ms │
|
||||
│ idle_ack_timer: void* idle таймаут 500ms │
|
||||
│ send_waiter: queue_waiter_handle waiter на send_input_q │
|
||||
│ watchdog_timer: void* проверка инварианта 500ms │
|
||||
│ send_resume_timer: void* retry send_q 50ms │
|
||||
│ send_blocked: u8 inflight полон │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
@@ -53,41 +50,40 @@ SVC_ROUTE_HDR (22 байта)
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ router_enqueue_send(rconn) │ всегда через send_q (FIFO)
|
||||
│ if send_q >= 64 && !force: │ → вернуть -1 (backpressure)
|
||||
│ return -1 │
|
||||
└──────┬───────────────────────┘
|
||||
│ копия в send_q + router_send_kick()
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ router_send_kick(rconn) │
|
||||
│ closed/no_route → return │
|
||||
│ send_q пуст → return │
|
||||
│ inflight >= limit → send_blocked=1, ret │
|
||||
│ waiter уже ждёт → return │
|
||||
│ нет conn → set_no_route │
|
||||
│ queue_waiter_wait(send_input_q, │
|
||||
│ &send_waiter, drain_cb) │
|
||||
└──────┬───────────────────────────────────┘
|
||||
│ send_input_q опустел (round-robin: FIFO waiter-ов)
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ router_send_drain_cb(rconn) │ шлём ОДИН пакет
|
||||
│ e = send_q.pop() │
|
||||
│ router_send_one_flags(e) │
|
||||
│ → tx_seq++ │
|
||||
│ → etcp_send(conn, entry) │
|
||||
│ если send_q ещё непуст: │
|
||||
│ inflight полон → blocked=1│
|
||||
│ иначе → queue_waiter_wait │ (в хвост → round-robin)
|
||||
│ если пуст → send_blocked=0 │
|
||||
└──────────────────────────────┘
|
||||
|
||||
Повторный запуск drain:
|
||||
• приход ACK → router_handle_ack → kick
|
||||
• маршрут появился → router_no_route_retry_cb → kick
|
||||
• рост inflight-лимита → router_update_inflight_limit → kick
|
||||
• страховка → router_send_watchdog_cb (500ms) → DEBUG_ERROR + kick
|
||||
│ inflight < ROUTER_MAX(256)? │
|
||||
│ tx_seq - tx_acked < 256 ? │
|
||||
└──────┬───────────┬───────────┘
|
||||
│ YES │ NO (inflight полон)
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌─────────────────────┐
|
||||
│router_send_one│ │ router_enqueue_send │ → send_q (FIFO)
|
||||
│ (прямая) │ │ (в очередь) │ send_blocked = 1
|
||||
└──────┬────────┘ └─────────┬───────────┘ send_resume_timer (50ms)
|
||||
│ │
|
||||
▼ │
|
||||
┌────────────────────┐ │
|
||||
│ tx_seq++ │ │
|
||||
│ hdr.dst = dst │ │
|
||||
│ hdr.src = node_id │ │
|
||||
│ hdr.seq = tx_seq │ │
|
||||
│ hdr.svc_id = svc │ │
|
||||
│ │ │
|
||||
│ etcp_send(conn, e) │ │
|
||||
└────────────────────┘ │
|
||||
│
|
||||
┌────────────────────────────┘
|
||||
│ (при получении ACK от remote)
|
||||
▼
|
||||
┌────────────────────┐
|
||||
│ router_drain_send_q│ вызывается из ACK handler
|
||||
│ │ или по send_resume_timer
|
||||
│ while inflight < 256│
|
||||
│ e = send_q.pop() │
|
||||
│ router_send_one(e)│
|
||||
│ │
|
||||
│ если send_q пуст: │
|
||||
│ send_blocked = 0 │
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
## Receive Path (приём данных)
|
||||
@@ -123,7 +119,7 @@ SVC_ROUTE_HDR (22 байта)
|
||||
│ │ = hdr.seq││ |d| < ROUTER_MAX_INFLIGHT? │
|
||||
│ │ (только ││ │
|
||||
│ │ вперёд) ││ Дубликат? │
|
||||
│ │ kick ││ rx_seq - seq > 0 ИЛИ │
|
||||
│ │ drain_q ││ rx_seq - seq > 0 ИЛИ │
|
||||
│ └──────────┘│ seq уже в recv_q? │
|
||||
│ └──────┬─────────────┬──────────────────┘
|
||||
│ │ OK │ out-of-bounds / dup
|
||||
@@ -230,8 +226,7 @@ SVC_ROUTE_HDR (22 байта)
|
||||
│ DEBUG_WARN "stale ACK" │
|
||||
│ │
|
||||
│ if send_blocked: │
|
||||
│ send_blocked = 0 │
|
||||
│ router_send_kick(rconn) │
|
||||
│ router_drain_send_q(rconn) │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -246,12 +241,12 @@ SVC_ROUTE_HDR (22 байта)
|
||||
При блокировке:
|
||||
данные → send_q (FIFO, без дропов)
|
||||
send_blocked = 1
|
||||
send_resume_timer = 50ms retry
|
||||
|
||||
Разблокировка:
|
||||
приход ACK от remote → tx_acked обновляется
|
||||
→ router_send_kick() → waiter → drain_cb разгребает send_q
|
||||
или маршрут появился / inflight-лимит вырос → kick
|
||||
страховка: router_send_watchdog_cb (500ms) → force kick
|
||||
→ router_drain_send_q() разгребает send_q
|
||||
или send_resume_timer (50ms) → router_drain_send_q()
|
||||
|
||||
send_q полностью разобран → send_blocked = 0
|
||||
```
|
||||
@@ -309,8 +304,8 @@ SVC_ROUTE_HDR (22 байта)
|
||||
│ idle_ack_timer ─── 500ms ─────▶ router_idle_ack_cb │
|
||||
│ (досылка последнего ACK) │
|
||||
│ │
|
||||
│ watchdog_timer ── 500ms ─────▶ router_send_watchdog│
|
||||
│ (страховка инварианта drain, force kick) │
|
||||
│ send_resume_timer ─ 50ms ─────▶ router_send_resume │
|
||||
│ (retry когда send_q не пуст) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
Аватарка CHAT-группы
|
||||
===================
|
||||
|
||||
Модуль src/chat/group_avatar.c. Картинка — бинарный BLOB до 16384 байт в общей
|
||||
SQLite каждого участника: channels.avatar_data/version/hash/signature.
|
||||
Storage-сервер, каталоги файлов и пользовательские квоты не используются.
|
||||
При старте добавляются отсутствующие колонки одной транзакцией, записи/ключи
|
||||
каналов сохраняются. channel_put использует UPSERT и сохраняет аватарку,
|
||||
UI-состояние и локальные приватные ключи; смена публичных ключей существующего
|
||||
channel_id запрещена. Удаление группы удаляет и её аватарку.
|
||||
|
||||
Право публикации — приватный Ed25519 ключ группы плюс собственное действующее
|
||||
членство. creator UID/admin-флаг не заменяют ключ. group_avatar_set подписывает:
|
||||
ASCII "group-avatar-v1" без NUL, group:u64 LE, version:u64 LE, size:u16 LE, bytes.
|
||||
Версия — max(NTP microseconds/1000, previous+1), ограничена INT64_MAX.
|
||||
SHA256 считается по картинке; при одинаковой версии побеждает больший SHA256.
|
||||
Обе конкурирующие записи должны иметь правильную подпись группы; конфликт — WARN.
|
||||
Публикация одинаковой картинки не создаёт новую версию. Удаление — подписанная
|
||||
запись с size=0 и новой версией; подпись/версия сохраняются, старый пир не может
|
||||
восстановить удалённую картинку. Чтение проверяет сохранённый хеш и подпись.
|
||||
|
||||
Транспорт
|
||||
---------
|
||||
|
||||
ETCP_RT_ID_GROUP_AVATAR=0x3A, router wire version=1. Отправитель/группа берутся
|
||||
из проверенного router-заголовка. Все пакеты подписаны ключом узла и зашифрованы
|
||||
end-to-end; в DATA дополнительно передаётся подпись группы над всей картинкой.
|
||||
Обмен только между READY-соседями CHAT-группы с действующим подписанным членством.
|
||||
Любой участник пересылает сохранённую запись, владелец ключа может быть offline.
|
||||
|
||||
Все числа протокола LE. Общая часть, 44 байта:
|
||||
version:u8=1, command:u8, avatar_version:u64, total_size:u16, SHA256:32.
|
||||
STATE=1 объявляет локальную версию/хеш; начальное отсутствие — version=0, hash=0.
|
||||
GET=2 запрашивает актуальную запись. Объявление не меняет БД без полной DATA.
|
||||
DATA=3 добавляет group_signature:64, offset:u16, block_size:u16, bytes.
|
||||
DATA header=112 байт. Картинка разбивается на блоки по 4096 байт; последний
|
||||
короче. Полная картинка 16 КиБ — ровно четыре DATA. Удаление — один DATA без bytes.
|
||||
|
||||
Порядок блоков гарантирует router. Сборка проверяет offset, постоянные version,
|
||||
total_size/hash/signature и допустимые размеры. До полной сборки прежняя картинка
|
||||
остаётся в БД. После SHA256/Ed25519 вся запись заменяется одним UPDATE, затем
|
||||
CHANNEL_UPDATED доставляет изменение GUI и STATE распространяется соседям.
|
||||
Промежуточные блоки не публикуются, ACK/retransmit каждого блока — только router.
|
||||
|
||||
Отправка заполняет router до нижнего порога send_q count<=0 и ждёт send_ready;
|
||||
планировщик отправляет максимум четыре пакета за callback. Snapshot отправителя
|
||||
не меняется при новом локальном обновлении. Максимум 16 tx и отдельно 16 rx
|
||||
сборок на экземпляр, каждая до 16 КиБ. receive_ready при заполнении rx сохраняет
|
||||
пакет в router recv_q без продвижения ACK; освобождение слота возобновляет приём.
|
||||
Несовпадение эпох/закрытие router отменяет незавершённую сборку и начинает сверку.
|
||||
Эпоха нового контекста берётся из текущей READY-сессии, не сбрасывает первый приём.
|
||||
Обнаружение соседей/таймауты — таймер 1 с; STATE также раз в 30 с. Нет прогресса
|
||||
15 с — отмена незавершённого snapshot, повтор запроса/сверки. Остановка сервиса
|
||||
отменяет таймеры/waiters/call_soon до удаления группы/ядра.
|
||||
|
||||
ChatGUI
|
||||
-------
|
||||
|
||||
Настройки группы: Upload group avatar/Remove group avatar, предпросмотр и статус.
|
||||
Кнопки доступны только при локальном приватном ключе. Применение сразу, отдельно
|
||||
от Save локальных настроек. Worker обрезает центральный квадрат, создаёт JPEG
|
||||
128×128, снижает качество/размер до лимита 16 КиБ. Вход ограничен 16 мегапикселями.
|
||||
GUI отправляет байты в uasync; ядро повторно проверяет размер/членство/ключ.
|
||||
|
||||
SQLite — постоянный кеш. GroupAvatarManager хранит QImage до 32 МиБ, ключ кеша:
|
||||
group/version/hash. Декодирование в отдельном worker, проверка SHA256, JPEG и
|
||||
квадратных размеров до 128×128. Поздний результат старой версии не заменяет
|
||||
новую. Готовая картинка обновляет только строку группы; без аватарки — инициалы.
|
||||
Приватные ключи/байты картинки не выводятся в диагностику.
|
||||
|
||||
Проверки
|
||||
--------
|
||||
|
||||
test_group_avatar: настоящие два ядра/UDP/router, 16 КиБ четырьмя блоками,
|
||||
атомарность сборки, hash/signature/cross-group отказ, приостановка receive,
|
||||
перезапуск канала, stale/conflict, редактор с ключом вне creator UID, подписанное
|
||||
удаление, повторное описание группы и restart БД. Schema failure откатывает
|
||||
частичные ALTER, сохраняет прежние данные/ключи, повторное добавление идемпотентно.
|
||||
test_group_avatars (Qt): нормализация, лимит/отображение, кеш, права кнопок и отказ
|
||||
ядра без ключа, удаление. chat=debug показывает состояния и каждый 4КиБ блок;
|
||||
chat=info — публикации и жизненный цикл. Ошибки содержат group/peer/version/offset.
|
||||
@@ -1,528 +0,0 @@
|
||||
# Linux: диагностика и восстановление аудио
|
||||
|
||||
Проверка 2026-10-02: desktop chatgui, miniaudio/PulseAudio, общий голосовой стек,
|
||||
звонки, рация и запись сообщений. Исправления обработки PCM, владения устройствами
|
||||
и автоматическое восстановление реализованы. Ниже сохранены исходные результаты
|
||||
аудита; раздел «Реализация» описывает действующие контракты и проверки.
|
||||
|
||||
## Вывод
|
||||
|
||||
До исправлений тракт был некорректен при сбоях. Самое существенное: воспроизведение
|
||||
звонка зависит от наличия микрофонных кадров в duplex-ring. Его опустошение
|
||||
останавливает вызов пользовательского callback, оставляя хвост выходного буфера
|
||||
незаполненным. Восстановление jitter-буфера не исправляет остановившееся устройство.
|
||||
Отдельного управляющего механизма восстановления устройств не было.
|
||||
|
||||
В реализации восстановлены три гарантии: каждый запрос output полностью заполнен; обработка
|
||||
одного callback всегда ограничена; остановка/пересоздание устройства выполняются
|
||||
вне аудиопотока и не уничтожают сетевую сессию звонка.
|
||||
|
||||
## Что установлено по работающим клиентам
|
||||
|
||||
В локальном старом логе звонка `132e4a9698f47bdf` с Samsung SM_A525F наблюдались
|
||||
паузы callback примерно 0,5–2,6 секунды при зарегистрированной обработке 0–2 мс.
|
||||
Входящий буфер desktop достигал 15 секунд, росли потери, включалось ускорение 160%.
|
||||
На Samsung захват продолжал давать около 50 кадров/с, playback регистрировал
|
||||
underrun и тоны разрыва. RTT порядка 1–4 мс не объясняет эти паузы сам по себе.
|
||||
|
||||
Это признаки проблемы обслуживания локального аудио, но ещё не доказательство
|
||||
конкретной причины зависания PulseAudio. Старое измерение исключало время
|
||||
синхронных DEBUG-вызовов между концом обработки и обновлением timestamp.
|
||||
Новая диагностика охватывает весь callback, включая эти логи.
|
||||
Работающий GUI не перезапускался, поэтому новый инструмент ещё не записал этот
|
||||
сбой на реальном звонке. Лог Samsung сохранён в `/tmp/sm-audio-current.log`.
|
||||
|
||||
## Найденные дефекты и ограничения до исправлений
|
||||
|
||||
| Место | Поведение и последствия | Необходимая доработка |
|
||||
| --- | --- | --- |
|
||||
| `lib/miniaudio.h`, `ma_device__handle_duplex_callback_playback()` | При пустом capture-ring `acquire_read` возвращает success с нулём кадров. Выполняется `break`, output остаётся частично незаполненным, call callback не вызывается. PulseAudio затем отправляет весь mapped buffer. | Обеспечить полный output и продолжение playback при отсутствии capture. Нельзя делать commit_read для синтетических кадров. |
|
||||
| `ma_device_on_write__pulse()` | Успешная запись с нулём обработанных кадров не уменьшает остаток цикла. Возможно бесконечное вращение с блокированием того же PA mainloop, включая capture. | Немедленно выходить при отсутствии продвижения, возвращать управление mainloop. Логировать состояние и запрос, не пытаться записывать до успеха в этом callback. |
|
||||
| `ma_device_on_read__pulse()` | PA hole отбрасывается без подачи соответствующего количества нулевых кадров. Из capture timeline исчезает интервал, duplex-ring может опустеть. | Сохранять длительность hole тишиной либо явно обозначать discontinuity независимому capture-тракту. |
|
||||
| `lib/speex_aec.c`, `speex_aec_feed_playback()` | Обрабатывает только остаток одного 960-sample кадра, отбрасывая хвост входного блока. Контракт разрешает произвольный count. | Циклом потреблять весь count, складывать все полные кадры, сохранять остаток. |
|
||||
| `CallAudioEngine::setPlaybackDeviceIndex()` | После неудачного пересоздания `m_active` остаётся true при отсутствии устройства; `start` того же звонка возвращает успех. | Разделить желание продолжать звонок и работоспособность устройства. Ошибка запуска переводит устройство в recovering. |
|
||||
| `RadioAudioEngine::teardownDevice()` | Если playback init успешен, а start нет, uninit пропускается: он зависит от флага started. Объект удаляется вместе с незавершёнными ресурсами. | Разделить initialized и started; каждый успешный init обязательно завершать uninit. |
|
||||
| `MainWindow::~MainWindow()` / `SoundManager::shutdown()` | Общий context освобождается вместе с engine, но звонок и рация здесь явно не останавливаются. Их singleton-объекты могут оставлять живые устройства. `m_context` также не обнуляется. | Сначала остановить все зависимые устройства и дождаться callbacks, затем освобождать общий context. |
|
||||
| `AudioRecorder` | Callback выбирает глобальный `g_recorder`, игнорируя `pUserData`. Второй recorder, в том числе тест микрофона, заменяет получателя. Флаги/уровень/вектор waveform используются из GUI и аудиопотока без согласованной синхронизации. | Использовать `pUserData`; передавать уровни атомарно, waveform — безопасным snapshot; буфер записи должен иметь однозначного владельца. |
|
||||
|
||||
До исправлений первые два дефекта воспроизводились стендом без аппаратуры:
|
||||
duplex оставлял sentinel в output при starvation, а PA-write без продвижения
|
||||
требовал внешней остановки. AEC из 1200 + 720 samples сохранял один кадр вместо
|
||||
двух. Теперь эти проверки требуют полного output, самостоятельного возврата
|
||||
из callback и двух сохранённых AEC-кадров; результаты приведены ниже.
|
||||
|
||||
Capture-AEC также обрабатывает не более одного кадра за вызов. В звонке его
|
||||
вызывают ровно с 960 samples, поэтому текущий call feed этот случай не нарушает.
|
||||
При расширении API нельзя объявлять произвольный count без согласованного размера
|
||||
выходного буфера. `speex_aec_reset()` уже существует: новый API для сброса самого
|
||||
AEC не нужен, но нужен согласованный сброс всего локального voice-тракта.
|
||||
|
||||
## Чистота реализации
|
||||
|
||||
Этот раздел фиксирует исходный аудит; изменения владения и ограничение работы
|
||||
callback описаны в разделе «Реализация».
|
||||
|
||||
Нормальная структура уже есть: GUI владеет устройствами, сервисный слой кодирует
|
||||
Opus и обрабатывает jitter, сетевой поток доставляет пакеты. Нет необходимости
|
||||
переписывать весь стек или заводить новый backend.
|
||||
|
||||
Однако границы владения не завершены: context фактически принадлежит engine
|
||||
звуковых уведомлений, а используется независимыми устройствами звонка/рации.
|
||||
Флаги active/started/initialized местами смешивают три разных состояния.
|
||||
Выбор input применяется к recorder, звонок использует default input, рация —
|
||||
default устройства. Output звонка и SoundManager тоже выбираются отдельно.
|
||||
Индекс перечисления не является устойчивой идентичностью устройства после hotplug.
|
||||
В radio capture fallback `useDefault` фактически повторяет ту же попытку:
|
||||
`pDeviceID` и так NULL в обеих ветках. Callbacks рации запускаются раньше
|
||||
подготовки capture-буфера и установки VAD; порядок запуска нужно привести к
|
||||
«сначала состояние и буферы, затем устройства», чтобы исключить гонки при старте.
|
||||
|
||||
Callback не является строго realtime-safe. Capture удерживает `g_mtx` во время
|
||||
AEC/AGC/Opus; отправка выделяет память и вызывает `uasync_post`. Playback вызывает
|
||||
SoundTouch и код с возможным расширением контейнеров, mutex и синхронными логами.
|
||||
Приём пакетов тоже может держать mutex при работе с jitter и логировании.
|
||||
Плохие Opus-кадры могут последовательно выбираться из большой очереди в одном
|
||||
pull: нужен предел работы на callback. Новая диагностика покажет, где это
|
||||
действительно нарушает deadline. Перенос всего DSP в ещё один worker заранее
|
||||
увеличит число очередей и точек синхронизации без установленной необходимости.
|
||||
|
||||
AEC получает reference только звонка. Звуки отдельного SoundManager в него не
|
||||
попадают; delay по умолчанию фиксирован, не вычисляется по фактической задержке
|
||||
устройств. Это ограничение качества эхоподавления, а не установленная причина
|
||||
текущего зависания.
|
||||
|
||||
Jitter умеет повторно накопить резерв после сетевого underrun, но диапазон tempo
|
||||
ограничен 1,0–1,6. Ускорение убирает избыток, а замедления для компенсации более
|
||||
медленных часов отправителя нет. При устойчивом отрицательном дрейфе резерв будет
|
||||
снова опустошаться. Это отдельная задача длительных звонков: после восстановления
|
||||
устройств проверить наклон глубины буфера и при необходимости добавить небольшую
|
||||
компенсацию дрейфа в обе стороны, не смешивая её с быстрым catch-up после паузы.
|
||||
|
||||
Принудительные 480 frames задают период 10 мс при 48 кГц. Собственный PA default
|
||||
miniaudio — 25 мс с комментарием о проблемах PipeWire при периодах меньше ~20 мс.
|
||||
Период backend и Opus-кадр 20 мс должны оставаться независимыми. Увеличивать
|
||||
период до исправления AEC нельзя: 1200-sample блоки уже воспроизводят потерю хвоста.
|
||||
После исправления разумно сначала проверить default backend и измерить задержку.
|
||||
|
||||
## Предлагаемое восстановление без большого усложнения
|
||||
|
||||
### 1. Сначала восстановить инварианты обработки и владения
|
||||
|
||||
Исправить полный output, zero-progress, AEC accumulation и uninit при неудачном
|
||||
start. Перед освобождением context явно остановить recorder, звонок, рацию и
|
||||
прочие его устройства. До остановки отменять recovery timer; после join можно
|
||||
сбрасывать voice-ресурсы. Проверять возвраты feed/start/init и логировать причину.
|
||||
|
||||
Самый небольшой вариант — оставить один duplex-device: при нехватке capture
|
||||
подать в call callback нулевой input, полностью сформировать playback и не
|
||||
продвигать реальный capture-ring за синтетические samples. Это чинит непосредственный
|
||||
дефект, но сохраняет общий жизненный цикл input/output и синтетический TX по
|
||||
playback-clock.
|
||||
|
||||
Для устойчивости к исчезновению микрофона предпочтительнее два устройства
|
||||
capture/playback внутри существующего `CallAudioEngine`, как уже сделано в рации.
|
||||
Capture только накапливает 960 samples и вызывает feed; playback независимо
|
||||
вызывает pull и всегда заполняет output. Это два handle с одним владельцем,
|
||||
без нового DSP-worker или дополнительной PCM-очереди. Потребуется уточнить
|
||||
потоковый контракт voice-слоя: один capture-writer и один playback-writer,
|
||||
согласованный доступ к AEC, остановка обоих до release. Текущий `g_mtx` уже
|
||||
защищает AEC, но остальные общие поля нужно проверить перед разделением.
|
||||
|
||||
### 2. Разделить краткий underrun и неисправность устройства
|
||||
|
||||
| Событие | Действие |
|
||||
| --- | --- |
|
||||
| Нет сетевого PCM, callback продолжает приходить | Тишина/существующий gap tone; jitter автоматически накапливает данные. Устройство не пересоздавать. |
|
||||
| Один backend underrun, новые кадры принимаются | Подать следующий полностью заполненный буфер; собрать счётчик. Не сбрасывать AEC и не запускать restart на каждый xrun. |
|
||||
| Hole/переполнение capture | Учесть потерянную длительность; краткую потерю пережить, длительную отметить discontinuity для AEC. |
|
||||
| Поток failed/stopped или нет продвижения кадров длительное время | Перейти в recovering и пересоздать затронутое устройство вне callback. |
|
||||
| Suspend/resume ОС | Учитывать suspend как отдельную причину, восстановление проверять после resume; не запускать частый restart-loop во время сна. |
|
||||
| Потерян общий backend/context | Однократно остановить всех его пользователей, восстановить владельца, затем требуемые устройства. |
|
||||
|
||||
Один owner timer и состояния `Stopped / Running / Recovering` достаточны.
|
||||
`wanted` хранит желание пользователя продолжать; generation меняется при stop,
|
||||
новом звонке и смене устройства. Каждая отложенная попытка проверяет generation
|
||||
и wanted — законченный звонок никогда не возрождается.
|
||||
|
||||
Callbacks только обновляют атомарные счётчики успешно обработанных кадров и
|
||||
признак backend failure. Watchdog проверяет фактический прогресс отдельно по
|
||||
направлениям. Тишина в PCM не означает остановку устройства. Reporter диагностической
|
||||
очереди не должен становиться watchdog: его события могут теряться.
|
||||
|
||||
Начальные значения для проверки: timer 200 мс, подозрение при отсутствии прогресса
|
||||
500 мс после startup grace; повторные попытки через 250/500/1000 мс с потолком
|
||||
2 секунды. Это предлагаемые настройки, не доказанные универсальные пределы.
|
||||
Suspend и фактический согласованный backend period должны учитываться отдельно.
|
||||
Нельзя обещать bounded recovery, пока init/stop/uninit сами могут зависнуть:
|
||||
сначала устранить spin-loop и проверить время backend-операций. Если они реально
|
||||
блокируют GUI, вынести только управление устройствами в один owner-thread,
|
||||
сохранив тот же автомат состояний.
|
||||
|
||||
На пересоздании сбросить capture-accumulator, AEC delay/filter и локальный PCM/
|
||||
stretch, сохранив call ID и сетевую сессию. После длительной аппаратной паузы
|
||||
устаревшую речь нужно явно отбросить до актуального резерва jitter, иначе
|
||||
15 секунд накопленного звука при скорости 160% разгружаются примерно 25 секунд.
|
||||
Этот сброс применять только при аппаратной discontinuity; обычный сетевой jitter
|
||||
не должен автоматически приводить к отбрасыванию очереди.
|
||||
|
||||
Устройство хранить по backend ID либо как default, не по индексу. При его
|
||||
исчезновении попробовать default и явно сообщить о смене; выбранное устройство
|
||||
можно вернуть при появлении. Нужны ошибки с ID/именем/направлением и результатом
|
||||
попытки. Для PA-ошибок следующий уровень диагностики — stream/context state и
|
||||
`pa_context_errno`, потому что один `-1` от API не объясняет причину.
|
||||
|
||||
### 3. Проверить сценарии, а не только успешный старт
|
||||
|
||||
После реализации добавить проверки: capture starvation 20/500 мс и возврат;
|
||||
hole; AEC блоки, пересекающие границу кадра; zero writable; ошибка записи;
|
||||
исчезновение выбранного устройства и появление default; suspend/resume;
|
||||
перезапуск звукового сервера; stop во время backoff; закрытие GUI при активном
|
||||
звонке/рации. Проверять полный output, bounded очереди и работу callback,
|
||||
возобновление прогресса, отсутствие callbacks после release и отсутствие
|
||||
повторного старта после пользовательского stop.
|
||||
|
||||
## Как читать добавленную диагностику
|
||||
|
||||
В GUI-категориях включить `call=debug` либо задать отдельную строку в INI:
|
||||
|
||||
```ini
|
||||
[debug]
|
||||
call=debug
|
||||
```
|
||||
|
||||
Обычные сводки идут на DEBUG, аномалии — WARN/ERROR; miniaudio info перенесён
|
||||
на DEBUG, подробный backend log — TRACE. Все устройства используют категорию
|
||||
call, включая recorder, radio и sound-engine; role и ID различают их в строках.
|
||||
Не включать глобальный trace для всего проекта при измерении аудио.
|
||||
|
||||
Reporter раз в секунду печатает stage, tid, callbacks/s, requested frames/s,
|
||||
максимальный интервал между входами, полное wall-time обработки и thread CPU-time,
|
||||
незавершённый вход и время с последнего входа. `work_max` и `cpu_max` — отдельные
|
||||
максимумы за окно, они не обязательно относятся к одному вызову.
|
||||
|
||||
| Stage | Что измеряется |
|
||||
| --- | --- |
|
||||
| loop | Один `pa_mainloop_iterate`, включая ожидание и вложенные callbacks. Долгое время само по себе не доказывает тяжёлый DSP. |
|
||||
| read / write | Полный backend read/write callback. Frames/s здесь — запрошенные, не гарантированно обработанные кадры. |
|
||||
| map | `writable_size` и `begin_write`; пустой/ошибочный выход тоже закрывает измерение. |
|
||||
| submit | Вызов `pa_stream_write`. |
|
||||
| duplex | Перенос capture-ring в duplex-output; detail содержит глубину capture-ring. |
|
||||
| callback | Полный miniaudio data callback, включая код приложения и его синхронные логи. |
|
||||
| feed / pull | Capture DSP/encode/send и playback jitter/stretch/tone в звонке. |
|
||||
|
||||
`unfilled_frames` — незаполненный output, `hole_frames` — потеря capture timeline,
|
||||
`capture_drop_frames` — переполнение capture-ring, `no_progress` — PA-write без
|
||||
продвижения. `backend_underruns` — отдельный callback самого PA-stream; это не
|
||||
то же самое, что пустой сетевой jitter. `backend playback started/resumed`
|
||||
показывает фактическое начало/возобновление playback. Notification содержит
|
||||
числовые miniaudio type/state.
|
||||
|
||||
Первое событие каждого типа за окно имеет tid и CLOCK_MONOTONIC timestamp;
|
||||
повторения суммируются. Для задержавшихся потоков reporter читает `/proc` wchan
|
||||
и накопленные schedstat CPU/ready-wait/slices. Сравнивать соседние snapshots:
|
||||
сами totals не являются длительностью текущей паузы. CPU-time намного меньше
|
||||
wall-time означает ожидание/вытеснение; wchan помогает отличить poll от mutex/
|
||||
I/O wait. Это снимок в момент отчёта, не исторический стек зависшего вызова.
|
||||
|
||||
Producer не выделяет память, не ждёт mutex reporter и не пишет диагностические
|
||||
логи: ограниченная MPSC-очередь 8192 события, не более четырёх CAS-попыток.
|
||||
Reporter не разыменовывает ma_device/pa_stream. Переполнение логируется как
|
||||
`diagnostic events lost`; такое окно нельзя считать полным. Инструментация
|
||||
добавляет получение часов/TID и атомарные операции, поэтому overhead ненулевой.
|
||||
Частые события call jitter/AEC/TX теперь публикуются счётчиками и выводятся из
|
||||
uasync вне аудиомьютекса. Часть существующих логов рации остаётся синхронной;
|
||||
тракт не объявляется строго realtime-safe.
|
||||
После close устройство удаляется из reporter: для устройств короче секунды
|
||||
доступны lifecycle и первые аномалии, но не полная секундная сводка.
|
||||
|
||||
Новый binary собран в `tools/chatgui/build/vibechat`; для нового измерения нужен
|
||||
его обычный перезапуск. Изменение файла binary не обновляет уже запущенный процесс.
|
||||
|
||||
## Реализация (2026-10-02)
|
||||
|
||||
`SoundManager` владеет общим контекстом и конечными выходами. На каждый фактический
|
||||
выход приходится один микшер: PCM сообщений/видео, заранее декодированные сигналы,
|
||||
готовый голос звонка и рации. После суммирования float PCM ограничивается, и один
|
||||
и тот же stereo буфер отправляется устройству и AEC соответствующего микрофона.
|
||||
Звук другого выхода в референс не входит. Default/явный backend ID объединяются;
|
||||
PulseAudio также публикует идентификатор фактического маршрута. После server move
|
||||
совпавшие выходы объединяются в GUI только после подтверждения фактического
|
||||
маршрута; предположение о default не используется вместо PA snapshot. При временной коллизии дополнительный выход
|
||||
отдаёт тишину до объединения. Изменение списка источников выполняется после stop
|
||||
callback, с сохранением backend stream и выбранного сервером маршрута.
|
||||
|
||||
Устройства сохраняются как backend ID вместо индекса. При init/start failure
|
||||
явного выбора пробуется default, причина логируется. Выбранный ID сохраняется;
|
||||
вернуться к нему можно повторным выбором или при следующем recovery. Автоматический
|
||||
опрос hotplug для возврата на первоначальный выбор не добавлен.
|
||||
|
||||
`AudioDevice` — владелец одного miniaudio device в GUI. Успешный init всегда имеет
|
||||
парный uninit, включая ошибку start. Callback и его состояние готовятся до start.
|
||||
Close останавливает/join-ит device worker перед освобождением callback. Отдельный
|
||||
атомарный счётчик кадров контролирует прогресс. Таймер 200 мс проверяет stopped
|
||||
и отсутствие продвижения 1 с; suspend продлевает grace, отдельный xrun не вызывает
|
||||
restart. Backoff: 250/500/1000/2000 мс, сброс после продвижения.
|
||||
|
||||
Miniaudio не добавляет накопитель фиксированного периода: `noFixedSizedCallback`
|
||||
включён, hardware chunks могут иметь любую длину. В PulseAudio физическое начало
|
||||
блока определяется по timing snapshot с AUTO_TIMING_UPDATE/INTERPOLATE_TIMING.
|
||||
Capture: monotonic now минус latency read index, без повторного вычитания длины
|
||||
блока. Playback: now плюс latency write index. Учитываются задержка converter и
|
||||
остающийся input cache. Один backend dispatch имеет один snapshot, позиции
|
||||
последующих client callbacks продвигаются по числу кадров. В отсутствие snapshot
|
||||
используется оценка по callback; источник времени и physical route логируются.
|
||||
PA hole отдельно помечен как отсутствующий capture, а не как реальная тишина.
|
||||
|
||||
`VoiceAudioIo` создаётся только для звонка и рации. Capture и RX обрабатывают
|
||||
независимые потоки. Hardware capture только копирует PCM в фиксированную MPSC-очередь;
|
||||
RX worker выполняет decode/SoundTouch/тоны и поддерживает до 40 мс готового голоса.
|
||||
В сервисном слое рации вычисление Silero держит только TX mutex: общий mutex
|
||||
отпущен, поэтому VAD не блокирует RX; lifetime и порядок PCM/PTT сохранены.
|
||||
Hardware output смешивает готовый PCM. Opus, AEC, VAD, AGC и растущие буферы записи
|
||||
не исполняются в аппаратных callbacks. Обработка самого backend, PCM/resampler
|
||||
узлов miniaudio и атомарные уведомления остаются в callback; строгая hard realtime
|
||||
гарантия не заявляется.
|
||||
|
||||
Capture worker — единственный владелец AEC и порядка PCM/PTT/mute. Кадр AEC —
|
||||
960 отсчётов на канал; 1/2 канала микрофона и 2 независимых канала референса.
|
||||
Противофазное стерео не усредняется. История рендера содержит до 1,92 с PCM и
|
||||
физические timestamps. Reference выбирается по времени микрофона; небольшое
|
||||
различие часов компенсируется интерполяцией только референса. Реальная тишина
|
||||
сохраняется как валидный PCM; при отсутствующем reference AEC пропускает микрофон
|
||||
и не адаптируется. Потеря reference, смена маршрута/поколения и скачок часов
|
||||
сбрасывают фильтр. Возраст capture-очереди ограничен 100 мс от enqueue, отдельно
|
||||
от собственной задержки backend. Потери и очередь видны в сводках категории `aec`.
|
||||
|
||||
Mute применяется после AEC/AGC: фильтр продолжает получать настоящий микрофон,
|
||||
а Opus получает нули только в отмеченной части кадра. Desktop запускает core
|
||||
через `call_audio_start_prepared`: повторного core AEC нет. Raw API с core AEC
|
||||
сохранён для Android/headless; смешивать политики в активном звонке нельзя.
|
||||
|
||||
У рации один автомат передачи: manual PTT или VAD. VAD читает непрерывный очищенный
|
||||
сигнал до AGC, включая время ручного PTT. Передача использует отдельный накопитель
|
||||
Opus-кадра; границы PTT внутри AEC-кадра сохраняются. При закрытии серии хвост
|
||||
дополняется нулями только перед Opus и помещается в очередь перед FIN. Короткий
|
||||
capture tail при stop проходит без AEC, а не дополняет непрерывный фильтр. Разрыв
|
||||
захвата завершает старый сетевой хвост и сбрасывает VAD, исключая склейку через
|
||||
потерянные отсчёты. Capture restart завершает принятую серию; новое прослушивание
|
||||
того же канала отменяет старые BEGIN/media. FIN проверяет токен своего capture/burst
|
||||
и поколение экземпляра ядра, поэтому не закрывает более новую серию и не обращается
|
||||
к освобождённому ядру даже при повторном использовании его адреса.
|
||||
|
||||
TX ограничен 8 media-задачами, возраст — 200 мс. У рации дополнительно ограничено
|
||||
16 control-задач; FIN выделяется заранее вместе с BEGIN. Если control-очередь
|
||||
занята, новый BEGIN откладывается до освобождения; это логируется. Очередь PCM
|
||||
резервирует 8 слотов для управляющих сообщений; producer не меняет consumer и
|
||||
публикует слот только после копирования. Capture callback никогда не ждёт слот.
|
||||
Отказ постановки GUI-команды завершает capture; таймер запускает recovery.
|
||||
|
||||
`AudioRecorder` получает raw microphone без AEC. Его worker обрабатывает AGC,
|
||||
накапливает запись и публикует атомарный уровень/длительность. Переполнение или
|
||||
ошибка обработки отклоняют запись с логом. Meter-only использует тот же capture,
|
||||
но не сохраняет PCM и не создаёт голосовое сообщение. Stop: close device, drain
|
||||
и join worker, затем передача владельца записи задаче attachment. Настройки
|
||||
компрессора в ходе записи применяются к следующей.
|
||||
|
||||
PCM воспроизводится через независимые handles. Сообщение, видео и тест динамика
|
||||
не останавливают чужой PCM; sample rate принадлежит источнику. Seek пересоздаёт
|
||||
узел и очищает его read/resampler cache, сохраняя handle и владельца PCM. Cursor
|
||||
вычисляется по реально обработанным графом кадрам, а не по prefetched datasource.
|
||||
Pause отсоединяет узел и ждёт начатое чтение графа; resume подключает тот же узел,
|
||||
сохраняя его PCM и cache. Соседние проигрыватели продолжают работать.
|
||||
У асинхронного открытия видео/голоса проверяется generation.
|
||||
|
||||
Потоковый AGC использует фиксированные кольца истории и PCM, нулевой lookahead
|
||||
и непосредственный process_frame без растущего output. У записи lookahead явно
|
||||
100 мс; растёт только сохраняемый результат. Push/flush/configure возвращают
|
||||
ошибки; flush проверяется перед кодированием вложения.
|
||||
|
||||
Порядок остановки голосового направления: close capture → join capture worker →
|
||||
detach output → join RX worker → release core. При failed context синхронный сигнал
|
||||
сначала закрывает всех клиентов, затем освобождает context. Mixer, PCM handles,
|
||||
call ID и группа переживают recovery. Stop выключает recovery; таймер завершения
|
||||
звонка проверяет generation. MainWindow останавливает клиентов до SoundManager
|
||||
и сетевого ядра. Сетевые операции и регистрация audio ops выполняются в uasync.
|
||||
|
||||
PulseAudio control-waits ограничены 1 с на операцию; при timeout operation
|
||||
отменяется до возврата, исключая callback с просроченным stack pointer. Partial
|
||||
init освобождает контекст/mainloop/события. Failed stream/context содержит PA
|
||||
errno в логе. Zero writable/zero converter progress возвращают управление.
|
||||
Последовательность нескольких операций init/uninit всё ещё может задержать GUI
|
||||
дольше секунды; отдельный device owner-thread не добавлен.
|
||||
|
||||
## Проверки реализации
|
||||
|
||||
`vibechat` и аудиотесты собираются через CMake. Набор проверки:
|
||||
|
||||
```sh
|
||||
ctest --test-dir tools/chatgui/build --output-on-failure \
|
||||
-R 'test_(linux_audio|audio_timing|audio_diagnostics|audio_recovery|audio_event_queue|voice_audio_io|voice_tx|call_jitter|radio_audio|radio_jitter|voice_file)$'
|
||||
make -C tests test_audio_compressor test_speex_aec
|
||||
./tests/test_audio_compressor
|
||||
./tests/test_speex_aec
|
||||
```
|
||||
|
||||
Добавлены отдельные проверки MPSC (80000 сообщений, reserve/control, слот ещё не
|
||||
опубликован), точных PCM/PTT/mute границ, короткого хвоста, независимого RX при
|
||||
задержке capture worker, противофазного stereo AEC, старого физического timestamp
|
||||
при свежем enqueue, совпадения реальных Opus-пакетов с правильно дополненным
|
||||
хвостом, rapid PTT, capture restart, отмены старого прослушивания и bounded control.
|
||||
Проверяются manual/VAD handover
|
||||
без второй серии и продвижение настоящего сервисного RX при заблокированном VAD.
|
||||
Timing test проверяет read-index timestamp, несколько converter callbacks одного
|
||||
блока и метку hole. Recorder тестирует raw PCM и отсутствие вложения в meter-only.
|
||||
|
||||
Speex проверяет подавление эха и сохранение double-talk также для двух микрофонов
|
||||
с двумя независимыми каналами reference. В детерминированном синтетическом тесте
|
||||
остаточная энергия эха — около 0,009 исходной; корреляция near-end — около 0,95.
|
||||
Эти величины относятся только к тесту, а не к реальным акустическим условиям.
|
||||
|
||||
В `/tmp/utun-audio-asan` проверяются пять тестов нового тракта с ASAN/UBSAN и
|
||||
LeakSanitizer; LeakSanitizer запускается вне песочницы. MPSC отдельно проверяется
|
||||
ThreadSanitizer. Изолированный интеграционный запуск:
|
||||
|
||||
```sh
|
||||
dbus-run-session -- python3 tools/chatgui/tests/run_pulse_recovery.py
|
||||
```
|
||||
|
||||
Итоговый прогон 2026-10-02: сборка `vibechat` успешна, CTest — 11/11,
|
||||
ASAN/UBSAN/LeakSanitizer — 5/5, компрессор — 479/479, Speex — PASS,
|
||||
MPSC под ThreadSanitizer — PASS. Timing-стенд прошёл ещё 100 последовательных
|
||||
повторов. Пауза PCM проверяется также серией pause/resume при работающем соседнем
|
||||
проигрывателе; она ждёт чтение узла, а не только меняет его флаг.
|
||||
|
||||
Тест создаёт собственные PipeWire/PulseAudio/WirePlumber в `/tmp`, отключает
|
||||
аппаратные monitors и использует два виртуальных sink. Проверяет объединение
|
||||
voice с общим выходом после `move-sink-input`, сохранение desired выхода звонка
|
||||
при recovery общего выхода (включая server stream-restore), затем восстановление capture/PCM
|
||||
после остановки и запуска своего PulseAudio. Звонок и рация сохраняют идентичность;
|
||||
recorder и медиа продолжают работу. Системный аудиосервер не затрагивается.
|
||||
|
||||
Исторические проверки исходного recovery: полный `check.sh` — 119 passed,
|
||||
0 failed, 1 skipped; proxy/burst/load интеграции прошли. По указанию пользователя
|
||||
async-тесты исключены из дальнейших запусков. APK ранее собран offline; установка
|
||||
на телефон не выполнялась.
|
||||
|
||||
Физический USB/Bluetooth hotplug, сон ОС, внешние звуки других приложений и качество
|
||||
AEC в длительном реальном звонке этими проверками не подтверждены. Reference
|
||||
охватывает звук этого приложения на том же выходе, согласно выбранному контракту.
|
||||
|
||||
## Период Linux/PulseAudio и сравнительный замер
|
||||
|
||||
`AudioDevice::requestLowLatency()` задаёт запрос 10 мс для capture звонка/рации.
|
||||
Он применяется при каждом открытии, включая recovery и fallback.
|
||||
Остальные backends сохраняют прежние параметры.
|
||||
Общий и отдельный voice output сохраняют прежний период: запрос 10 мс
|
||||
на проверенной системе не снизил измеренную задержку воспроизведения.
|
||||
Кадры AEC/кодека остаются 20 мс; `noFixedSizedCallback` позволяет обслуживать
|
||||
фактические порции backend без дополнительного накопления в miniaudio.
|
||||
|
||||
Это запрос задержки, а не гарантия размера callback. PulseAudio согласует
|
||||
`tlength`, `fragsize`, `minreq`, `maxlength`, `prebuf`; PipeWire также ограничивает
|
||||
quantum и минимальные порции. `AudioDiag` при init выводит внутренний PCM-формат
|
||||
и согласованные параметры в миллисекундах (`-1` — значение по умолчанию).
|
||||
Пересчёт использует внутренние rate/channels/format, а не формат приложения.
|
||||
|
||||
Секундные clock-отчёты содержат `latency_us`/`backend_us` и `block_us` в виде
|
||||
`min/avg/max`. В задержку включены только измеренные timing snapshots;
|
||||
`estimated_blocks`/`measured` позволяют отличить их отсутствие от нулевой задержки.
|
||||
Для времени исполнения callback включать `call=trace`, для состояния AEC —
|
||||
`aec=debug`. Логи агрегируются вне аппаратного callback.
|
||||
|
||||
Ручной режим существующего стенда использует настоящие устройства и голосовые
|
||||
workers, не сохраняет микрофонный PCM и не отправляет его по сети. Выход звонка
|
||||
без удалённого собеседника обслуживается тишиной; акустическое подавление эха
|
||||
этот режим не измеряет. Аргументы: capture frames, playback frames, секунды,
|
||||
необязательный режим `call` (по умолчанию), `radio` или `output`, затем путь лога.
|
||||
Ноль frames оставляет стандартный период backend; override применяется только
|
||||
в тестовом executable, до настоящего `ma_device_init`.
|
||||
|
||||
```sh
|
||||
cmake --build tools/chatgui/build --target test_audio_recovery -j4
|
||||
tools/chatgui/build/test_audio_recovery --latency 960 0 60 > /tmp/audio-baseline.log 2>&1
|
||||
tools/chatgui/build/test_audio_recovery --latency 480 0 60 > /tmp/audio-capture10.log 2>&1
|
||||
tools/chatgui/build/test_audio_recovery --latency 960 480 60 > /tmp/audio-playback10.log 2>&1
|
||||
tools/chatgui/build/test_audio_recovery --latency 480 480 60 > /tmp/audio-both10.log 2>&1
|
||||
```
|
||||
|
||||
Запуски последовательные: несколько потоков могут менять общий quantum сервера.
|
||||
Сравнивать одинаковые маршруты и нагрузку, отдельно от старта/остановки и после
|
||||
стабилизации адаптивного PCM-резерва. Проверять backend latency, возраст первого
|
||||
сэмпла перед готовым TX-кадром, очередь после mix, starvation, holes, потери PCM,
|
||||
AEC reference и событий диагностики. Затем повторить baseline и проверить
|
||||
выбранный вариант под нагрузкой. Меньший возраст PCM в capture callback сам по
|
||||
себе не доказывает такой же выигрыш до кодирования: worker собирает кадры 20 мс.
|
||||
|
||||
`test_voice_audio_io` дополнительно проверяет порции по 256 samples: порядок PCM,
|
||||
временные метки, границы mute/PTT и позицию частичного playback. AEC проверяется
|
||||
при разных порциях capture/reference (256/480 и 480/256) с противофазным stereo
|
||||
reference. Очереди по-прежнему ограничены числом событий; уменьшение периода
|
||||
не увеличивает их ёмкость в миллисекундах.
|
||||
|
||||
Сравнение 2026-10-15 на встроенном HDA, PipeWire 1.6.2, 48 кГц. Каждый вариант
|
||||
работал 60 секунд; таблица содержит средние по callback/кадрам после 45 секунд
|
||||
прогрева (PCM-резерв к этому моменту уменьшился до одного блока).
|
||||
|
||||
| Запрос capture / playback | Capture backend, мс | Первый сэмпл готового TX-кадра, мс | Playback backend, мс | PCM после mix, мс |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 20 мс / default 25 мс | 30,69 | 34,53 | 16,32 | 18,67 |
|
||||
| 10 мс / default 25 мс | 16,02 | 29,79 | 16,15 | 18,67 |
|
||||
| 20 мс / 10 мс | 31,51 | 34,97 | 18,09 | 24,00 |
|
||||
| 10 мс / 10 мс | 16,13 | 29,66 | 18,46 | 24,00 |
|
||||
|
||||
Изменение capture даёт около 14,7 мс на границе backend и около 4,7 мс перед
|
||||
передачей полного кадра в голосовой стек. Для playback сервер согласовал
|
||||
`tlength=16ms`, `minreq=5.333ms` при запросе 10 мс; callback уменьшился с 512
|
||||
до 256 samples, но выигрыша по задержке не получилось. Поэтому в рабочем коде
|
||||
изменён только capture. Значения в столбцах не следует складывать как независимые
|
||||
участки общей задержки: capture backend входит в возраст TX-кадра, а очередь после
|
||||
mix снята уже после извлечения текущего выходного блока из очереди.
|
||||
|
||||
В этих четырёх прогретых окнах не было потерь capture/reference PCM, выходного
|
||||
starvation, backend underrun/overflow или потери событий диагностики.
|
||||
Отдельные отсутствующие AEC references были и в baseline (4 кадра за окно),
|
||||
и в остальных вариантах (4–5). Это существующее ограничение привязки reference,
|
||||
а не проверка акустического качества. Первоначальный output starvation и
|
||||
временное увеличение PCM-резерва учитываются отдельно от стабильного окна.
|
||||
|
||||
Повтор baseline дал 30,76 мс capture backend и 34,99 мс возраста TX-кадра.
|
||||
Двухминутный прогон выбранного capture 10 мс под нагрузкой четырёх CPU-процессов
|
||||
дал после прогрева 15,81 и 28,55 мс соответственно; PCM после mix — 18,67 мс.
|
||||
Потерь PCM, backend underrun/overflow и потери событий диагностики в этом окне
|
||||
не было. За 75 секунд окна зарегистрировано 25 отсутствующих AEC references.
|
||||
Отдельный минутный прогон рации: capture backend в среднем 15,48 мс, без потерь PCM.
|
||||
|
||||
Сборка `vibechat` и 6 аудиотестов прошли. Изолированный интеграционный тест
|
||||
подтвердил объединение выходных маршрутов и восстановление capture/playback после
|
||||
перезапуска тестового PulseAudio. Запускать весь стенд через `dbus-run-session`,
|
||||
чтобы имя его PulseAudio не конфликтовало с сервером пользовательской сессии.
|
||||
В fixture перед `radio_audio_init` создаётся `radio_instance`, освобождаемый после
|
||||
`radio_audio_destroy`; это исключает ложную ошибку регистрации mute callback.
|
||||
Полный `check.sh` вне sandbox: 156/156, 9/9 proxy-интеграций, burst и load — PASS.
|
||||
В sandbox сетевые тесты не проходят из-за запрета сокетов; этот запуск не является
|
||||
проверкой сетевых регрессий.
|
||||
|
||||
## Референсы
|
||||
|
||||
Первичные исходники и документация просмотрены 2026-10-01; ссылки на master могут
|
||||
изменяться. Используем принципы, а не копируем целиком архитектуру библиотеки.
|
||||
|
||||
- [PipeWire audio-src-ring.c](https://docs.pipewire.org/audio-src-ring_8c-example.html):
|
||||
callback читает доступные кадры, заполняет остаток тишиной и возвращает полный
|
||||
буфер; пополнение инициирует через событие управляющего потока. Хороший минимальный
|
||||
пример поведения при пустой PCM-очереди.
|
||||
- [PulseAudio stream API](https://github.com/pulseaudio/pulseaudio/blob/master/src/pulse/stream.h):
|
||||
отдельные underflow/overflow/started/suspended/moved callbacks; peek различает
|
||||
пустую очередь и hole с ненулевой длиной. Нужно разделять эти состояния в логах.
|
||||
- [PulseAudio pacat.c](https://github.com/pulseaudio/pulseaudio/blob/master/src/utils/pacat.c):
|
||||
underflow callback сообщает событие без пересоздания stream. При failed context
|
||||
утилита завершает работу — это пример диагностики, не готовая self-healing система.
|
||||
- [Mozilla cubeb PulseAudio backend](https://github.com/mozilla/cubeb/blob/master/src/cubeb_pulse.c):
|
||||
явные context/stream states, пересоздание ошибочного context при инициализации,
|
||||
согласованное отключение callbacks и доступ под mainloop lock; minimum latency
|
||||
25 мс. Это полезный референс владения и границ управления, а не основание
|
||||
автоматически заимствовать всю библиотеку.
|
||||
- [miniaudio](https://github.com/mackron/miniaudio/blob/master/miniaudio.h):
|
||||
init/start/stop/uninit запрещены внутри data callback из-за deadlock. Просмотренный
|
||||
upstream тоже содержит break при пустом duplex-ring: обновление библиотеки само
|
||||
по себе не решает этот найденный дефект.
|
||||
@@ -1,66 +0,0 @@
|
||||
# Важные нюансы работы встроенного lwIP TCP-стека
|
||||
|
||||
Краткая памятка по тонким местам `src/lwip_tcp/`, критичным для прокси.
|
||||
Полное описание модуля — в `src/lwip_tcp/lwip_tcp_doc.md`.
|
||||
|
||||
## Ключевые константы (`lwip_tcp_opts.h`)
|
||||
|
||||
| Константа | Значение | Смысл |
|
||||
|-----------|----------|-------|
|
||||
| `TCP_MSS` | 1460 | максимальный размер сегмента |
|
||||
| `TCP_WND` | 8×MSS = 11680 | окно приёма lwIP (объявляется пиру) |
|
||||
| `TCP_SND_BUF` | 16×MSS = 23360 | буфер отправки |
|
||||
| `TCP_TMR_INTERVAL` | 250 мс | базовый интервал таймера |
|
||||
| `tmr_interval_ms` | `TCP_TMR_INTERVAL/4` = 62.5 мс | фактический шаг `tcp_fasttmr` |
|
||||
| `TCP_WND_UPDATE_THRESHOLD` | `TCP_WND/4` = 2920 | порог немедленного ACK окна |
|
||||
|
||||
**Важно:** таймер uasync использует timebase 0.1 мс — `uasync_set_timeout(ua, N, …)`
|
||||
задаёт N×0.1 мс. Например, 5000 = 500 мс.
|
||||
|
||||
## Контракт recv_cb — главный источник багов
|
||||
|
||||
lwIP вызывает `recv_cb(arg, pcb, pbuf, err)` для данных, **уже извлечённых из pcb**.
|
||||
Дальше приложение само решает судьбу данных и окна:
|
||||
|
||||
| Действие | Результат |
|
||||
|----------|-----------|
|
||||
| скопировать + `tcp_recved(pcb, len)` | данные приняты, окно восстановлено (норма) |
|
||||
| `return != LERR_OK` | lwIP кладёт pbuf в `refused_data` и отдаст позже — **без потери**, окно закрывается |
|
||||
| `pbuf_free(p)` без `tcp_recved` | данные потеряны, окно не восстановлено → схлопывание окна |
|
||||
|
||||
`refused_data` — штатный backpressure lwIP: одноканальный (один pbuf), ре-доставка
|
||||
через `tcp_fasttmr` (62.5 мс) или при следующем входящем сегменте.
|
||||
|
||||
**Ошибка, из-за которой падал `tcp_proxy_full`:** при backpressure прокси делал
|
||||
`pbuf_free(p)` без `tcp_recved`. Каждый такой сброс уменьшал `rcv_wnd` на MSS; после
|
||||
~8 пакетов окно падало в 0, OS-TCP переставал слать, и передача умирала.
|
||||
|
||||
## Управление окном и «деградация 62.5 мс»
|
||||
|
||||
`tcp_recved(pcb, len)` увеличивает `rcv_wnd` и через `tcp_update_rcv_ann_wnd` решает,
|
||||
слать ли ACK окна немедленно. Немедленный ACK идёт только если прирост окна
|
||||
`wnd_inflation ≥ TCP_WND_UPDATE_THRESHOLD` (=2920 = 2×MSS); иначе ACK откладывается
|
||||
до `tcp_fasttmr` (62.5 мс).
|
||||
|
||||
Отсюда классическая ловушка: если после backpressure окно восстанавливать **по одному
|
||||
пакету** (`tcp_recved(1460)` за раз), прирост 1460 < 2920 → ACK уходит раз в 62.5 мс →
|
||||
пир шлёт 1 пакет за 62.5 мс ≈ 23 КБ/с. Это выглядит как «пропускная способность
|
||||
застряла», хотя сеть свободна.
|
||||
|
||||
**Правильно:** восстанавливать окно **пачкой** (несколько `tcp_recved` подряд либо один
|
||||
`tcp_recved(TCP_WND_MAX - rcv_wnd)`), тогда прирост ≥ 2920 → ACK немедленный → пир
|
||||
сразу возобновляет бурст.
|
||||
|
||||
## Рекомендуемый паттерн для relay-потребителя (как в `tcp_proxy_client.c`)
|
||||
|
||||
1. При получении данных — копировать в ограниченную очередь, **не вызывать**
|
||||
`tcp_recved` (окно само плавно закрывается; очередь ограничена `TCP_WND`).
|
||||
2. Дрейн очереди по сигналу освобождения нижележащего канала
|
||||
(`etcp_router_on_send_ready`) + retry-таймер (force=1) как safety-net.
|
||||
3. `tcp_recved(len)` вызывать **только после успешной отправки** — окно
|
||||
восстанавливается пачкой по мере дрена, ACK уходит немедленно.
|
||||
4. Никогда не сбрасывать pbuf без `tcp_recved`; не держать данные в `refused_data`
|
||||
дольше одного пакета (single-slot).
|
||||
|
||||
См. реализацию: `tcp_proxy_client_tx_queue_drain_cb` / `tcp_proxy_client_fin_flush`
|
||||
в `src/proxy/tcp_proxy_client.c`.
|
||||
@@ -1,375 +0,0 @@
|
||||
Файлы участников CHAT-группы
|
||||
===========================
|
||||
|
||||
Модуль: src/member_files/. Desktop-браузер: tools/chatgui/src/member_files/.
|
||||
Отдельный сервис router ETCP_RT_ID_MEMBER_FILES (0x39), отдельно от вложений
|
||||
сообщений media_delivery и E2E-файлов DM. Серверы выбираются явно; между
|
||||
серверными деревьями автоматической репликации нет.
|
||||
|
||||
Хранение и конфигурация
|
||||
----------------------
|
||||
|
||||
Корень по умолчанию: <db_path>/member_files/ (без db_path — ./member_files/).
|
||||
Публичное дерево: <корень>/<group_id>/<node_id>/<относительный путь>.
|
||||
Временные файлы: <корень>/.uploads/<upload UUID в hex>.
|
||||
При старте сервис создаёт/открывает корень в дисковом worker, удаляет старые
|
||||
незавершённые temp и сканирует опубликованные файлы. До готовности отвечает BUSY.
|
||||
Ошибка сканирования оставляет локальное хранилище недоступным до restart;
|
||||
частичные счётчики клиенту не выдаются. Клиентские передачи к другим серверам продолжают работать.
|
||||
Очистка кэша вложений media/ это дерево не обходит.
|
||||
|
||||
Настройки [chatserver] / [chat]:
|
||||
member_files_enabled=1
|
||||
member_files_user_quota_mb=1024
|
||||
member_files_upload_ttl_sec=3600
|
||||
member_files_path=/путь/к/хранилищу (опционально, до 255 байт UTF-8)
|
||||
|
||||
Путь, квота и TTL читаются при старте чат-сервиса; изменение требует его restart.
|
||||
enabled проверяется на каждом запросе. Лимит одинаков для каждого UID отдельно,
|
||||
размер UID суммируется по всем группам на этом сервере. Значение в МиБ (1024*1024 байт).
|
||||
Счётчики used/reserved/files/directories хранятся в памяти дискового worker и восстанавливаются с диска только при старте.
|
||||
Каталоги не занимают байтовую квоту. files/directories считают опубликованные файлы и каталоги внутри деревьев UID
|
||||
по всем группам; корни групп, корни UID и .uploads исключены. Пустой опубликованный файл считается файлом.
|
||||
Сохранённые файлы удалённых участников также занимают квоту.
|
||||
При начале загрузки резервируется размер нового файла или положительный прирост при замене.
|
||||
WRITE не изменяет used; COMMIT обновляет used на разницу размеров и освобождает резерв.
|
||||
ABORT/Cancel/TTL освобождают резерв после удаления temp; REMOVE вычитает размер файла.
|
||||
Переименование внутри UID не меняет размер и числа объектов. Новый COMMIT увеличивает files на 1;
|
||||
замена существующего файла, включая нулевой размер, сохраняет files. MKDIR увеличивает directories на 1,
|
||||
REMOVE уменьшает соответствующий счётчик. Для mkdir учитывается факт создания даже при последующей ошибке sync.
|
||||
BEGIN/WRITE/ABORT/Cancel/TTL не увеличивают files: temp не опубликован.
|
||||
Все изменения корректируют счётчики инкрементально, без обхода каталога пользователя;
|
||||
QUOTA только читает их из памяти. LIST выполняет свой обычный обход для отображения записей, не пересчитывая статистику.
|
||||
Повторы BEGIN/COMMIT/MKDIR/REMOVE не меняют учёт повторно.
|
||||
Уже превышенная квота не удаляет данные: чтение, удаление и уменьшение файла доступны,
|
||||
новый положительный прирост запрещён. Физический temp может занимать больше места,
|
||||
чем резерв прироста; нехватка места файловой системы остаётся отдельной I/O-ошибкой.
|
||||
Нехватка квоты/места возвращает NO_SPACE; опубликованные файлы не вытесняются.
|
||||
Общая member_files_quota_mb удалена. Корень обслуживается только файловым сервисом;
|
||||
ручные изменения требуют повторного сканирования через restart.
|
||||
|
||||
Сервер — действующий полный мембер группы с подписанным adm_tags.storage="yes".
|
||||
Колонка peers.storage — производный сетевой приоритет, для полномочий не используется.
|
||||
Каждый запрос и ответ подписаны Ed25519 и зашифрованы end-to-end средствами
|
||||
route_crypto. group/src берутся только из проверенного заголовка router;
|
||||
ETCP peer промежуточного хопа не считается автором запроса.
|
||||
Читать могут действующие участники той же группы. Изменять дерево может только
|
||||
src == owner. Удалённые записи и source=1 (плейсхолдеры) доступа не дают.
|
||||
Открытый fd/UUID загрузки не отменяют повторную проверку членства.
|
||||
Файлы удалённого участника сохраняются на диске, но через сервис не доступны.
|
||||
|
||||
Wire, версия 4
|
||||
--------------
|
||||
|
||||
Все числа LE, без сырых структур и padding. Фиксированный заголовок 125 байт:
|
||||
0 protocol_version:u8 = 4
|
||||
1 kind:u8 (1=request, 2=response)
|
||||
2 command:u8
|
||||
3 status:u16 (request=0)
|
||||
5 request_id:u64, ненулевой случайный ID
|
||||
13 owner:u64 (UID каталога; 0 только для LIST виртуального корня группы)
|
||||
21 offset:u64
|
||||
29 size:u64
|
||||
37 cursor:u64
|
||||
45 version:16
|
||||
61 upload:16
|
||||
77 hash:32
|
||||
109 mtime:u64, Unix seconds
|
||||
117 type:u8 (1=file, 2=directory)
|
||||
118 more:u8 (0/1)
|
||||
119 path_len:u16
|
||||
121 target_len:u16
|
||||
123 data_len:u16
|
||||
После заголовка: path[path_len], target[target_len], data[data_len], без NUL.
|
||||
Длина пакета обязана точно совпадать с указанными размерами.
|
||||
Путь максимум 1024 байта, компонент максимум 255 байт, максимум 60 компонентов.
|
||||
Данные максимум 8192 байт; вместе с полным router-путём и crypto укладываются
|
||||
в лимит normalizer 16384 байта. Размер файла/смещение ограничены INT64_MAX.
|
||||
Пути — строгий UTF-8, относительные, разделитель '/'. Запрещены пустые компоненты,
|
||||
компоненты с ведущей '.', завершающей точкой/пробелом, управляющие символы,
|
||||
Windows device names и символы \ : * ? " < > |. Корень внутри UID — пустой путь.
|
||||
POSIX разрешает путь через *at/O_NOFOLLOW; regular-файлы с hardlink отвергаются.
|
||||
Windows удерживает родительские каталоги без SHARE_DELETE и не принимает reparse points.
|
||||
|
||||
Команды
|
||||
---------------
|
||||
|
||||
1 LIST: owner/path/cursor/version. Первая страница cursor=0, version=0.
|
||||
Ответ data — записи: name_len:u16,type:u8,size:u64,mtime:u64,version:16,name.
|
||||
cursor — сколько записей уже выдано. more=1 требует следующую страницу с
|
||||
возвращёнными cursor/version. При изменении снимка — STALE, начать сначала.
|
||||
Размер каталога не ограничивает память страницы. Виртуальный корень группы
|
||||
выдаёт UID действующих полных участников, даже если каталог ещё не создан.
|
||||
Изменение namespace сервера консервативно инвалидирует дисковые LIST-курсоры.
|
||||
|
||||
2 STAT: owner/path. Ответ type/size/mtime/version. Не хеширует весь файл.
|
||||
version — непрозрачный снимок identity/size/mtime/ctime и эпохи сервера.
|
||||
Закрытие fd по таймауту токен не меняет; restart меняет дисковые токены.
|
||||
|
||||
3 READ: owner/path/version/offset/size (1..8192).
|
||||
Ответ size — полный размер файла, data_len — фактически прочитано,
|
||||
more — есть ли байты после ответа. offset==EOF возвращает 0 байт/more=0;
|
||||
offset>EOF — INVALID_RANGE. Изменение файла между блоками — STALE.
|
||||
Клиент не смешивает версии и публикует локальный файл только по завершении.
|
||||
|
||||
4 BEGIN_WRITE: owner/path/upload/size/hash/version.
|
||||
upload — 16 случайных байт, size — конечный размер, hash — SHA256 содержимого.
|
||||
version=0 означает создать новый файл, существующий возвращает EXISTS.
|
||||
Для замены требуется версия из STAT/LIST. В один путь одна активная загрузка.
|
||||
UUID привязан к group/src/owner/path; повтор BEGIN с теми же параметрами безопасен.
|
||||
|
||||
5 WRITE: owner/path/upload/offset/size/data. size==data_len, диапазон внутри
|
||||
конечного размера. Можно писать в произвольном порядке и повторять блок.
|
||||
ACK подтверждает запись в temp, а не durable публикацию всего файла.
|
||||
|
||||
6 COMMIT: owner/path/upload. Проверяет SHA256, fsync/_commit и ожидаемую
|
||||
версию назначения, затем атомарно заменяет файл и синхронизирует namespace.
|
||||
Прежний файл виден до публикации. Ответ содержит размер/версию/SHA256.
|
||||
Незаписанные sparse-области читаются как нули; неправильное содержимое не
|
||||
проходит SHA256. Пустые файлы поддерживаются. При I/O-ошибке во время
|
||||
публикации результат может быть неопределённым; нужно повторно проверить STAT.
|
||||
|
||||
7 ABORT: owner/path/upload. Удаляет temp, освобождает резервацию.
|
||||
Неизвестный UUID — OK; опубликованные файлы никогда не удаляет.
|
||||
|
||||
8 MKDIR: owner/path. Создаёт один каталог; родители должны существовать.
|
||||
9 RENAME: owner/path/target/version. Только внутри UID/группы, без замены
|
||||
существующего назначения. Активная загрузка в затрагиваемом пути — BUSY.
|
||||
10 REMOVE: owner/path/version. Файл или пустой каталог, без рекурсивного удаления.
|
||||
|
||||
11 STREAM_WRITE: открытие потоковой загрузки; поля как BEGIN_WRITE,
|
||||
cursor — ненулевой ID потока. Ответ подтверждает готовность temp и конечный размер.
|
||||
12 STREAM_READ: открытие потокового скачивания; owner/path/version/size/cursor,
|
||||
upload — случайный токен клиента. Сервер сверяет снимок и размер перед началом.
|
||||
13 STREAM_DATA: request_id — ID потока, owner/path/upload, offset/size/data.
|
||||
kind=request для upload, response для download. Ответа на блок нет.
|
||||
offset обязан равняться следующему принятому смещению; size==data_len, не более 8192.
|
||||
14 STREAM_END: ID потока, owner/path/upload, offset==size==размер файла, SHA256.
|
||||
Upload: клиент отправляет END после всех DATA, сервер отвечает END после COMMIT.
|
||||
Download: сервер отправляет END после DATA и повторной проверки версии; клиент
|
||||
проверяет SHA256 записанных данных, синхронизирует и публикует локальный файл.
|
||||
Ошибка сервера приходит END.status!=OK; ошибка открытия — ответом на STREAM_READ/WRITE.
|
||||
15 STREAM_CANCEL: клиент отменяет конкретный поток. Поздние DATA не публикуются.
|
||||
|
||||
16 QUOTA: owner=UID, path/target пустые, cursor=0, data_len=0.
|
||||
Права чтения те же, что у STAT/LIST выбранного участника. Ответ data_len=40:
|
||||
used:u64, limit:u64, reserved:u64, files:u64, directories:u64, LE.
|
||||
Первые три значения в байтах, последние два — числа объектов. Снимок общий для UID во всех группах.
|
||||
Не резервирует место и не занимает квитанцию мутации. used может превышать limit
|
||||
после уменьшения лимита; сервер не выдаёт фиктивный нулевой размер при ошибке учёта.
|
||||
|
||||
Открытие потока — обычный повторяемый запрос с отдельным request_id. cursor связывает
|
||||
его с DATA/END/CANCEL. Поток определяется group/peer/kind/ID; один ID не может
|
||||
одновременно обозначать разные открытия. Каждый пакет по-прежнему подписан и зашифрован.
|
||||
При закрытии/перезапуске router-канала его handles завершаются с IO_ERROR;
|
||||
байты разных эпох не смешиваются. Возобновление через новую эпоху пока не реализовано.
|
||||
|
||||
Статусы (0..13): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
|
||||
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT.
|
||||
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ на 120 секунд
|
||||
(64 квитанции); тот же request_id с другим содержимым — BAD_PACKET.
|
||||
COMMIT дополнительно узнаётся по upload UUID. При заполнении квитанций новая
|
||||
мутация возвращает BUSY до истечения старых. Клиент повторяет запрос с прежним
|
||||
request_id раз в 5 секунд, общий срок 120 секунд. Повторы после этого срока
|
||||
не дают гарантии того же результата; перед новой мутацией требуется STAT.
|
||||
|
||||
Потоки, таймауты и остановка
|
||||
---------------------------
|
||||
|
||||
На экземпляр один дисковый pthread с очередью до 64 обычных задач. Сеть,
|
||||
ACL, таймеры и state machines выполняются в uasync; open/stat/readdir/read/write,
|
||||
resize/hash/sync/rename/remove/close — в дисковом потоке. GUI только передаёт
|
||||
команды через bridge и получает события. Нет вызова дискового I/O из callback
|
||||
таймера. Completion резервируется до постановки задачи; без polling workers.
|
||||
Локальные файлы transfer и серверное дерево используют тот же дисковый поток.
|
||||
Для обязательной очистки резервируется отдельная задача до открытия файла.
|
||||
Обращение к своему серверу доставляется через отдельную ограниченную очередь
|
||||
call_soon в тот же обработчик, без сетевого канала. ACL и дисковый поток общие;
|
||||
сетевой recv не принимает unsigned даже при src == self.
|
||||
|
||||
Сервер держит до 64 открытых файлов. Каждый READ/WRITE/BEGIN продлевает idle.
|
||||
После 10 секунд простоя fd закрывается на очередном дисковом sweep (таймер 1 с);
|
||||
занятый дисковый поток откладывает sweep до освобождения. Закрытие не публикует
|
||||
и не удаляет файл. Temp можно открыть заново до истечения upload TTL.
|
||||
После restart незавершённые temp удаляются; опубликованные файлы остаются.
|
||||
|
||||
Лимиты: 16 клиентских запросов, 32 серверных задания/ответа, 16 временных
|
||||
загрузок, 16 клиентских и 16 серверных потоков на экземпляр. Каждый поток имеет
|
||||
кольцо 32 x 8192 байт (256 КиБ); worker обрабатывает до 16 блоков за задачу.
|
||||
Чтение следующей порции перекрывается с отправкой уже подготовленной.
|
||||
Все серверные и локальные I/O используют один FIFO worker, без блокировки uasync.
|
||||
|
||||
Отправитель добавляет пакеты, пока send_q router находится под нижним порогом
|
||||
(сейчас count<=0), затем ждёт etcp_router_on_send_ready. Файловый слой не устанавливает
|
||||
своё окно inflight и не повторяет DATA; транспортные ACK, retransmit и порядок — router.
|
||||
Callback отправляет не более 8 блоков за запуск, уступая очередь другим потокам.
|
||||
Параллельные файлы одного пира разделяют канал и FIFO/round-robin waiters.
|
||||
|
||||
Готовность приёмника проверяется router до доставки следующего seq. Заполненное
|
||||
кольцо или очередь заданий оставляют пакет в recv_q: rx_seq/ACK не продвигаются.
|
||||
Освобождение места вызывает отложенный resume; pump отменяется при reset/close.
|
||||
ACK RECEIVE_BUSY (flags bit0) раз в 100 мс отличает ожидание диска от потери транспорта.
|
||||
Отправитель приостанавливает новые DATA, а живые busy ACK продлевают таймаут router.
|
||||
Буфер router ограничен его receive window. Локальная доставка использует такую же
|
||||
проверку готовности и ограниченный FIFO, а не сеть или отдельные подтверждения.
|
||||
|
||||
При остановке сначала отменяются producers/requests и дожидаются все stream cleanup
|
||||
continuations; только затем worker освобождает storage. Отмена после уже выполненной
|
||||
публикации не удаляет опубликованный файл.
|
||||
|
||||
Transfer владеет CM handle до окончания/отмены; DOWN/TIMEOUT handle не освобождает.
|
||||
quiesce запрещает новые команды, отменяет таймеры, waiter и запросы; destroy
|
||||
вне callbacks дожидается только своих работ/completions и закрывает дисковый
|
||||
поток до уничтожения CHAT-групп. Контексты не освобождаются под работающим I/O.
|
||||
Отмена не прерывает текущий системный I/O или хеширование, а ждёт его completion.
|
||||
|
||||
Desktop
|
||||
-------
|
||||
|
||||
Контекстное меню канала: Group files. Меню участника: Member files.
|
||||
Окно: выбор сервера/UID, путь, Up/Refresh, таблица каталогов/файлов,
|
||||
Upload/Download/New folder/Rename/Delete, прогресс и Cancel transfer.
|
||||
Чужой UID доступен только для чтения. Автор может быть offline.
|
||||
Open скачивает в выбранный пользователем локальный файл и открывает его
|
||||
приложением ОС после завершения. Смена контекста отбрасывает поздние ответы.
|
||||
Для выбранного UID строка хранилища показывает занято/всего, число файлов/каталогов и резерв текущих загрузок.
|
||||
Точные байты доступны в tooltip. QUOTA запрашивается отдельно от LIST при выборе
|
||||
сервера/UID, обновлении каталога, завершении передачи и каждые две секунды видимого окна.
|
||||
Поздние ответы старого UID/сервера игнорируются. BUSY при стартовом сканировании
|
||||
сохраняет Loading directory и повторяет LIST через секунду; новый переход отменяет повтор.
|
||||
|
||||
API параллельных передач
|
||||
-----------------------
|
||||
|
||||
member_files_transfer_start(request, callback, arg) копирует request и возвращает
|
||||
отдельный mf_transfer* на каждый файл. upload задаёт направление, group/server/owner
|
||||
выбираются для каждой операции. id=0 выбирает случайный ID; id в callback — фактический.
|
||||
NULL означает отказ запуска без callback; причина пишется в лог.
|
||||
max_bytes=0 оставляет прежний лимит; ненулевое значение ограничивает размер файла.
|
||||
Upload проверяет размер источника до хеширования, download — ответ STAT до открытия
|
||||
локального temp. STREAM привязан к проверенным размеру/version; превышение — INVALID_RANGE.
|
||||
|
||||
callback(arg, mf_transfer_progress*) получает id/done/total/status/finished.
|
||||
Прогресс заимствован до возврата callback. done для отправителя означает байты,
|
||||
принятые router в очередь; успешный finished подтверждает завершение операции,
|
||||
для upload — публикацию на сервере, для download — публикацию локального файла.
|
||||
finished приходит ровно один раз, handle к этому моменту уже освобождён;
|
||||
вызывающий обнуляет свой указатель в callback. member_files_transfer_cancel(handle)
|
||||
отменяет только этот файл, ждёт его I/O и ставит CANCEL в очередь с backpressure.
|
||||
Stop сервиса/группы завершает все принадлежащие ему handles.
|
||||
|
||||
Все API/callbacks выполняются в uasync-потоке экземпляра. GUI trampolines используют
|
||||
тот же API и превращают callback в существующие события прогресса; Qt не делает I/O.
|
||||
Один локальный файл назначения не может одновременно принадлежать двум download.
|
||||
|
||||
Аватарки пользователей
|
||||
---------------------
|
||||
|
||||
Относительный путь avatars/<sha256>.<расширение> находится внутри обычного дерева UID:
|
||||
<корень>/<group>/<UID>/avatars/<sha256>.<расширение>. Дополнительного UID в API/пути нет.
|
||||
Аватарка по умолчанию копируется в каждую группу, где выбранный сервер имеет
|
||||
подписанную storage-роль. Ссылка каждого канала публикуется только после COMMIT
|
||||
соответствующей копии; отсутствующий сервер в другом канале не создаёт битую ссылку.
|
||||
Новое членство обнаруживается через снимки участников и повторяет публикацию.
|
||||
Между серверами автоматического копирования нет.
|
||||
|
||||
userinfo содержит строковые avatar_server (16 hex цифр) и avatar_file (64 hex SHA256 + .json).
|
||||
Оба поля отсутствуют при удалённой аватарке. Профиль подписан ключом участника;
|
||||
смена имени/адресов сохраняет остальные плоские JSON-атрибуты. userinfo ограничен
|
||||
1024 байтами UTF-8. Merkle, JOIN_INFO_RESP, CHANNEL_INVITE и JOIN_REQUEST используют
|
||||
длину userinfo:u16 BE вместо прежнего u8. Bootstrap передаёт существующий подписанный
|
||||
профиль инвайтера, не создавая более свежую запись только с именем. Обе стороны
|
||||
нужно обновить: обратной совместимости нет.
|
||||
|
||||
Снимок участника CHAT_MEMBER_DISPLAY_SIZE теперь 158 байт: прежние 79 байт,
|
||||
avatar_server:8LE, avatar_file:70 (с NUL), storage_role:1. storage_role обозначает
|
||||
подписанную роль сервера, независимо от существующей колонки storage — сетевого
|
||||
приоритета, который может быть 0 у сервера за NAT. Потребители используют общий
|
||||
chat_member.h; локальное изменение профиля также вызывает MEMBER_UPDATED.
|
||||
|
||||
В Settings → Profile доступны Upload avatar, выбор storage-сервера, предпросмотр
|
||||
и Remove avatar. Изменения применяются сразу, независимо от Save других настроек.
|
||||
Перед загрузкой desktop/Android показывают редактор: неподвижная квадратная рамка,
|
||||
перемещение фото и масштабирование (мышь/колесо или жесты). Отмена не публикует новую ссылку.
|
||||
Worker принимает JPEG/PNG/WebP до 32 МиБ и 16 мегапикселей. Исходные байты, включая EXIF,
|
||||
сохраняются без изменений. Выбранная область после EXIF-ориентации становится WebP lossy
|
||||
256×256, quality=85, максимум 1 МиБ. Описание JSON (максимум 4096 байт): version=1,
|
||||
original/preview — имена объектов; original_bytes/preview_bytes, width/height — размеры
|
||||
ориентированного оригинала; x/y/w/h — нормализованный квадрат в его координатах.
|
||||
Все объекты лежат в avatars/<SHA256 собственных байтов>.<jpg|png|webp|json>.
|
||||
Порядок COMMIT: оригинал, превью, описание; затем подписанная ссылка на описание.
|
||||
Повтор использует уже сохранённые неизменяемые объекты. Сервер проверяет имя/SHA256,
|
||||
лимит объекта и запрещает замену под прежним именем и rename в/из avatars.
|
||||
Файлы и каталог входят в обычные квоты. Прежние версии сохраняются на сервере;
|
||||
владелец может удалить их через файловый браузер. Старые PNG-ссылки не поддерживаются.
|
||||
|
||||
Клик по аватарке сообщения/участника открывает галерею пользователя: LIST каталога
|
||||
выбирает только описания, проходит все страницы cursor/version, сортирует mtime DESC,
|
||||
при равенстве — по имени. Изначально выбрана текущая подписанная ссылка. Большое фото —
|
||||
полный оригинал, до его загрузки показывается превью. Внизу пять квадратных позиций:
|
||||
две предыдущие, выбранная по центру, две следующие; за границами списка позиции пустые.
|
||||
Desktop: кнопки/клавиши ←/→; Android: кнопки/свайпы при исходном масштабе. Превью кликабельны.
|
||||
Загружаются выбранный оригинал и видимые превью; закрытие отменяет запросы без других владельцев.
|
||||
Для DM источник — общий канал, из которого берётся отображаемая аватарка.
|
||||
|
||||
AvatarManager общий для списка участников и сообщений, максимум две операции
|
||||
одновременно, одинаковые хеши не скачиваются параллельно. Изменение своего профиля
|
||||
имеет приоритет перед фоновыми загрузками. Для DM используются ссылки из общих
|
||||
каналов; при отсутствии доступной ссылки остаются инициалы. Онлайн-статус списка
|
||||
отображается отдельной точкой поверх фотографии. Поздний результат старой ссылки
|
||||
пополняет кеш, но не заменяет картинку новой ссылки.
|
||||
|
||||
Кеш аккаунта: <db_path>/avatar_cache/<local_UID_hex>/<sha256>.<расширение>; без db_path
|
||||
используется Qt AppLocalDataLocation. Проверяются SHA256 объектов, описание и WebP 256×256;
|
||||
превышение/битая картинка не публикуются. Публикация кеша атомарна через QSaveFile,
|
||||
незавершённые download/part очищаются при старте. Готовые файлы не требуют TTL/STAT:
|
||||
смена содержимого меняет имя. Дисковый LRU — 100 МиБ, кеш QImage — 32 МиБ;
|
||||
собственная аватарка и источники незавершённых upload сохраняются при вытеснении.
|
||||
Дисковое I/O и декодирование выполняются вне GUI/uasync. Повторы неудачных загрузок:
|
||||
5/10/20/40/80/160 секунд; новая ссылка сбрасывает задержку. RTT/online не меняют ссылку
|
||||
и не вызывают повторной загрузки. Желаемая собственная ссылка/удаление сохраняются
|
||||
атомарно одной строкой ui_state.avatar_default, незавершённая публикация повторяется
|
||||
после запуска при наличии локального источника.
|
||||
|
||||
test_avatars проверяет нормализацию, хеш/размеры, очистку temp, настоящее storage
|
||||
в двух каналах, публикацию, сохранение при смене имени, DM-кеш, restart без сети,
|
||||
автоматический download после потери кеша и удаление ссылок. Также проверяются
|
||||
max_bytes до открытия local temp и смена аватарки при задержанном download старой
|
||||
версии: поздний результат не заменяет новый рисунок. test_member_adapter проверяет
|
||||
подписанные профили 191/255/256/512/1023/1024 байт вместе с adm_tags до 8192.
|
||||
Категория media=debug показывает discovery, cache hit/eviction, направление,
|
||||
group/owner/server/hash, ошибки проверки и retry; media=info — передачи/публикации.
|
||||
|
||||
Проверки и диагностика
|
||||
---------------------
|
||||
|
||||
tests/test_member_files: wire/UTF-8, произвольные блоки, повторы, EOF, версии,
|
||||
короткая замена, SHA256, пустой файл, независимые квоты UID, общий учёт по группам,
|
||||
резервирование/повторы/TTL, инкрементальные files/directories, замены пустых файлов,
|
||||
создание/удаление/rename каталогов, отсутствие runtime-rescan, стартовый scan и его ошибки, ссылки, expiry/restart, 10-секундный
|
||||
fd idle и принадлежность work/done потокам.
|
||||
tests/test_member_files_network: настоящие A/B/M, подписанная storage-роль,
|
||||
параллельные upload/download одного пира, local loopback, пустые файлы, побайтная
|
||||
проверка, отмена одного handle, restart router и destroy с активным server I/O.
|
||||
Шестисекундная задержка worker проверяет receive backpressure и busy ACK.
|
||||
Также проверяются чужая запись, виртуальный корень и подписанное удаление читателя.
|
||||
QUOTA проверяется на настоящем подписанном/зашифрованном канале; удалённый читатель не получает счётчики.
|
||||
test_services — stop/start общего чат-сервиса.
|
||||
Текущая диагностика выделена в категорию debug. Настройка для полного трасса:
|
||||
[debug]
|
||||
debug=trace
|
||||
debug=debug показывает запрос/ответ, ACL, очереди, маршрут, ACK, crypto-решения,
|
||||
I/O, fd idle и состояние браузера. trace дополнительно показывает каждый
|
||||
read/write и передачу задач/завершений между дисковым потоком и uasync.
|
||||
stream progress раз в секунду показывает ID/направление, done/total, число блоков
|
||||
в кольце, активность I/O, причину ожидания и скорость B/s. Router логирует переходы
|
||||
receive paused/resumed и peer receive busy без повторного сообщения на каждый блок.
|
||||
quota scan started/ready показывают путь, число пользователей и время обхода;
|
||||
quota updated показывает UID/операцию/used/reserved/files/directories; quota exceeded — used/reserved/growth/limit.
|
||||
По request_id видны GUI post → core request → router seq → server rx → ACL →
|
||||
disk queued/start/done → response tx → GUI result/bridge → directory ready.
|
||||
PID создания worker, PID отправки задачи и startup PID позволяют однозначно
|
||||
обнаружить fork после создания потока; несовпадение PID логируется как ошибка.
|
||||
Ключи шифрования, plaintext файлов и сырые медиа в debug не выводятся.
|
||||
Старые VAD/рация/GUI/вложения сообщения перенесены в vad/radio/chat/media/config;
|
||||
ошибки и пользовательские info остаются в тематических категориях.
|
||||
@@ -1,36 +0,0 @@
|
||||
# Ссылки в сообщениях
|
||||
|
||||
Android и Qt распознают `http://`, `https://` и `utun://` в обычном тексте
|
||||
личных и групповых сообщений. HTTP/HTTPS нечувствительны к регистру схемы.
|
||||
Ссылки выделяются цветом и подчёркиванием; длинные веб-адреса переносятся по ширине.
|
||||
Сокращённая строка структурированного PM-приглашения сохраняет полный `utun://` адрес.
|
||||
|
||||
Пробелы, управляющие символы, кавычки и угловые скобки отделяют ссылку от текста.
|
||||
Конечные `. , ; : ! ?` и лишние закрывающие скобки удаляются из распознанного адреса.
|
||||
Сбалансированные скобки в адресе, параметры, fragment и Unicode сохраняются.
|
||||
Адреса без схемы не распознаются. Некорректные веб-адреса остаются обычным текстом.
|
||||
Исходное сообщение в БД и протоколе не меняется.
|
||||
|
||||
Разборщики возвращают диапазоны UTF-16 и полные адреса:
|
||||
`tools/chatgui/src/message_links.cpp`, Android `data/MessageLinks.kt`.
|
||||
Подсветка и нажатие используют эти диапазоны. Android подставляет emoji placeholders
|
||||
только в обычный текст между ссылками: emoji внутри адреса остаётся частью полного URL.
|
||||
Qt использует один QTextLayout для размеров, отрисовки, выделения и координат нажатия.
|
||||
Перетаскивание выделяет текст без открытия ссылки.
|
||||
|
||||
HTTP/HTTPS открываются из GUI системным обработчиком: Android `ACTION_VIEW`,
|
||||
Qt `QDesktopServices::openUrl`. `utun://` запускает существующий JOIN.
|
||||
Ошибки открытия показываются через перевод интерфейса и логируются.
|
||||
Диагностика открытия содержит схему и host, без полного пути/query.
|
||||
|
||||
Общие случаи: `tools/chatgui/tests/message_links_cases.tsv`.
|
||||
Qt `test_invite_links` проверяет разбор, перенесённые веб-ссылки, смесь схем,
|
||||
нажатие, drag/copy и сокращённые приглашения; `test_emoji_playback` — жесты приглашений.
|
||||
Android `tests/test_message_links.kt` проверяет тот же TSV на классах из APK-сборки:
|
||||
|
||||
```bash
|
||||
cd tools/chatgui-android
|
||||
./build.sh noinstall
|
||||
kotlinc tests/test_message_links.kt -classpath app/build/tmp/kotlin-classes/debug -d /tmp/message-links-tests.jar
|
||||
kotlin -classpath /tmp/message-links-tests.jar:app/build/tmp/kotlin-classes/debug Test_message_linksKt ../chatgui/tests/message_links_cases.tsv
|
||||
```
|
||||
@@ -1,109 +0,0 @@
|
||||
Как работает детектор NAT
|
||||
========================
|
||||
|
||||
Детектор выясняет две вещи: какой внешний IP:порт получает рабочий UDP-сокет
|
||||
при обращении к разным IP и проходят ли к нему пакеты от незнакомого IP.
|
||||
Проверяется именно сокет, через который работает приложение.
|
||||
|
||||
Есть быстрый путь без помощников: если рабочий сокет принял подключение
|
||||
и локальный IP и фактический IP подключившегося узла публичные,
|
||||
ему сразу ставится direct/open (UNCHANGED + OPEN). Это правило действует
|
||||
для UDP и TCP, даже если с IP пира раньше уже был исходящий обмен.
|
||||
Локальные подключения и SOCKS интернет-вход не подтверждают. Для TCP
|
||||
фактический локальный IP должен совпадать с IP интерфейса сокета.
|
||||
|
||||
Нужны три узла: A проверяет себя, B наблюдает его внешний адрес,
|
||||
C помогает проверить входящие пакеты. Команды и результаты передаются
|
||||
по уже работающей сети uTun, в том числе через TCP или промежуточные узлы.
|
||||
Сами тестовые пакеты идут напрямую по UDP.
|
||||
|
||||
Порядок проверки
|
||||
---------------
|
||||
|
||||
1. A отправляет пакет узлу B. Тот сообщает, с какого IP:порта его получил.
|
||||
Ответ идёт по управляющему соединению, поэтому блокировка прямого
|
||||
UDP-ответа не мешает узнать внешний адрес A.
|
||||
|
||||
2. Через B узел A просит C отправить несколько пакетов на этот внешний адрес.
|
||||
До этого A не должен отправлять ничего на IP узла C с проверяемого сокета.
|
||||
Приложение хранит историю таких отправок и учитывает новые во время проверки.
|
||||
На входящие тестовые пакеты A автоматически не отвечает.
|
||||
|
||||
3. Получатель проверяет настоящий адрес отправителя каждого тестового пакета.
|
||||
Локальный или другой непубличный IP не подходит для интернет-проверки.
|
||||
Для проверки незнакомого входа также не подходят IP узла B, собственный
|
||||
внешний IP узла A и IP, на который A уже отправлял пакеты.
|
||||
Если помощник не подходит, A выбирает другого. Разные узлы с одним IP
|
||||
не считаются разными источниками. Проверяются также подлинность пакета
|
||||
и принадлежность текущей попытке, чтобы чужой или старый пакет не влиял на результат.
|
||||
|
||||
4. Затем A сам отправляет пакет C и узнаёт, какой внешний IP:порт видит C.
|
||||
Если входящие до этого не проходили и внешний адрес совпал с увиденным B,
|
||||
C повторяет отправку с того же IP:порта на исходный внешний адрес A.
|
||||
Так проверяется, открыл ли исходящий контакт путь для входящих.
|
||||
Параллельный обмен с B подтверждает, что UDP-связь в целом работает.
|
||||
|
||||
5. A повторяет измерение внешнего адреса через B и C. Результат принимается,
|
||||
только если адрес для каждого направления остаётся стабильным.
|
||||
Сравниваются разные IP назначения; другие порты того же IP не проверяются.
|
||||
|
||||
Что означает результат
|
||||
----------------------
|
||||
|
||||
strict — B и C стабильно видят разные внешние IP:порты узла A.
|
||||
Единого внешнего адреса для подключения с любого IP нет.
|
||||
|
||||
eim open — B и C видят одинаковый внешний IP:порт, и пакет от C пришёл
|
||||
ещё до исходящего контакта A с его IP.
|
||||
|
||||
eim filtered — внешний IP:порт одинаковый, серия пакетов до контакта не прошла,
|
||||
а после исходящего контакта пакеты с того же адреса C начали приходить.
|
||||
Это оценка: потерю UDP-пакетов нельзя абсолютно отличить от блокировки.
|
||||
|
||||
EIM? — два направления видят одинаковый внешний IP:порт, но незнакомый вход
|
||||
проверить не удалось, например все доступные помощники уже были знакомы.
|
||||
Это EIM с неизвестной доступностью входящих, без обещания открытого входа.
|
||||
Такой же предварительный EIM? дают два STUN-сервера, видящие одинаковый внешний
|
||||
IP:порт тестового сокета на том же локальном IP и интерфейсе. Поддержка запроса
|
||||
с незнакомого IP для этого не нужна. Источник оценки — STUN, вход остаётся UNKNOWN.
|
||||
Измерение рабочего сокета имеет приоритет; STUN-оценка действует до пяти минут
|
||||
и обновляется автоматически. Публичный порт для подключения берётся только
|
||||
из наблюдения рабочего сокета: внешний порт STUN в него не подставляется.
|
||||
|
||||
Если внешний IP:порт совпадает с локальным, отдельно отмечается отсутствие
|
||||
трансляции адреса. Доступность входящих всё равно проверяется.
|
||||
|
||||
Если данных недостаточно, соответствующая часть результата остаётся UNKNOWN.
|
||||
Известный тип NAT можно сохранить, даже если входящие проверить не удалось.
|
||||
Таймаут и отсутствие подходящих узлов сами по себе не означают strict.
|
||||
|
||||
Если один пир уже увидел публичный IP:порт, отличный от локального, этот адрес
|
||||
сразу сохраняется как NAT с type=UNKNOWN. Он публикуется отдельно от адреса
|
||||
интерфейса и остаётся кандидатом для пробного подключения: одного наблюдения
|
||||
недостаточно, чтобы обещать тот же адрес для других IP или открытый вход.
|
||||
Уже знакомый помощник пригоден для измерения типа NAT, хотя проверить незнакомый
|
||||
вход через него нельзя. Потеря результата входящих не останавливает сравнение
|
||||
адресов через доступные публичные узлы; ожидание одного этапа ограничено по времени.
|
||||
Измеренный mapping сохраняется сразу, а проверка незнакомого входа продолжается
|
||||
через остальных помощников. Порядок обхода фиксирован на его начало; фактический
|
||||
source всегда проверяется по актуальной истории. При паузе по общему бюджету
|
||||
незавершённая попытка повторяется с новыми идентификаторами, без сброса обхода.
|
||||
Если попытка уже получила полный бюджет, его исчерпание считается таймаутом:
|
||||
обход переходит дальше, а не повторяет одного помощника бесконечно.
|
||||
Старый сохранённый результат входящих не считается успехом новой проверки.
|
||||
|
||||
Соединения по LAN работают независимо и не сбрасывают интернет-профиль.
|
||||
Если доступны только локальные узлы, измерить тип интернет-NAT для UDP нельзя.
|
||||
STUN на другом порту даёт предварительную оценку типа NAT для того же IP/интерфейса.
|
||||
Работа TCP не заменяет проверку UDP.
|
||||
|
||||
Для TCP определяется только наличие NAT. Непубличный локальный IP сразу означает
|
||||
NAT с неизвестным типом. Если локальный IP публичный, достаточно одного публичного
|
||||
TCP-пира: по готовому защищённому соединению он сообщает увиденный IP.
|
||||
IP совпали — трансляция IP не обнаружена; отличаются — NAT есть, тип UNKNOWN.
|
||||
Порты при сравнении не учитываются. Публичный внешний IP сохраняется с портом 0:
|
||||
входящий порт неизвестен, поэтому NCD не использует эту запись для подключения.
|
||||
Активных проб типа NAT и TCP-входа нет; OPEN устанавливается по принятому
|
||||
публичному подключению и сохраняется при последующих совпавших наблюдениях IP.
|
||||
Через SOCKS внешний IP прокси
|
||||
не записывается как собственный адрес.
|
||||
@@ -1,96 +0,0 @@
|
||||
# Подписанная запись узла
|
||||
|
||||
`TOPO_NODE.timestamp` — версия всей подписанной записи, а не отдельного адреса.
|
||||
Узел-владелец назначает Unix microseconds, строго больше предыдущей сохранённой
|
||||
версии. Имя, тип, активность, адреса, метаданные сокетов, REALITY и подпись
|
||||
заменяются вместе. Timestamp входит в подпись. Групповые пути и подсети
|
||||
остаются в `TOPO_GROUP_NODE` и не являются частью общей записи узла.
|
||||
|
||||
Получатель проверяет подпись до изменения реестра или маршрутов. Более свежая
|
||||
запись заменяет старую целиком, включая пустые списки адресов. Старые записи
|
||||
игнорируются; разные подписанные записи с одинаковым timestamp — конфликт.
|
||||
Повторное BGP-объявление может обновить путь, но не содержимое общей записи.
|
||||
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется
|
||||
только timestamp.
|
||||
|
||||
Живая телефонная занятость передаётся без изменения структуры NODEINFO: в существующем
|
||||
`flags` бит `0x04` означает известное состояние, `0x08` — занятость звонком.
|
||||
Это групповые флаги `TOPO_GROUP_NODE.phone_status`, как флаг рации `0x02`;
|
||||
они не входят в подпись общей записи и используются только для цвета трубки в GUI.
|
||||
Без разрешения Android, при выключенной публикации и после ухода узла статус неизвестен.
|
||||
Решение о начале вызова принимает адресат по локальному состоянию ОС, а не по BGP-подсказке.
|
||||
Изменения публикуются обычной новой версией NODEINFO во всех CHAT-группах; отдельного опроса и таймера нет.
|
||||
|
||||
Профиль endpoint в SOCKMETA содержит независимые `mapping` и `inbound`, источник `source`.
|
||||
ID и время измерения не передаются: последний успешный профиль действует до
|
||||
явного изменения endpoint или подтверждённого нового результата. Повторная
|
||||
проверка с тем же результатом не меняет NODEINFO. Все поля SOCKMETA — отдельные байты;
|
||||
границы списков metadata/адресов/REALITY также входят в каноническую подпись.
|
||||
Алгоритм и условия неопределённости описаны в `doc/nat_detection.txt`.
|
||||
`source=ETCP` обозначает результат детектора рабочего endpoint. Одно UDP-наблюдение может дать
|
||||
адрес `TOPO_ADDR_NAT` при `mapping=UNKNOWN`, `inbound=UNKNOWN`: это NAT-кандидат
|
||||
для пробного подключения, а не адрес интерфейса или подтверждённый открытый вход.
|
||||
Общий внешний адрес подтверждает EIM; strict не публикует универсальный NAT-адрес.
|
||||
Source входит в подпись; SOCKMETA занимает 5 байт. `source=STUN` означает предварительный
|
||||
EIM? для того же локального IP/netif с неизвестным входом; измерение ETCP приоритетно.
|
||||
Владелец снимает STUN-оценку после TTL. Внешний адрес тестового порта не публикуется:
|
||||
NAT-кандидат по-прежнему требует наблюдения рабочего сокета.
|
||||
Для TCP достаточно одного публичного пира: внешний IP с неизвестным входящим
|
||||
портом публикуется как `TOPO_ADDR_NAT` с `port=0`, `mapping=UNKNOWN`, `inbound=UNKNOWN`.
|
||||
NCD пропускает нулевые порты. Непубличный локальный TCP IP сразу означает NAT;
|
||||
совпавший с наблюдением публичный IP даёт UNCHANGED без подтверждения открытого входа.
|
||||
|
||||
Таблица `node_snapshots` хранит полную сериализованную запись без группового
|
||||
окружения. `node_addresses` остаётся проекцией для поиска и отображения, а не
|
||||
источником для восстановления подписанной записи. При наличии сохранённого
|
||||
snapshot загрузка использует его. Локальная публикация сохраняется до рассылки;
|
||||
ошибка сохранения не разрешает публикацию новой версии.
|
||||
|
||||
В проекции адресов сохраняются `nat_mapping`, `nat_inbound`, `nat_source`, `address_kind`.
|
||||
Уникальность определяется endpoint и видом адреса,
|
||||
а не отображаемым классом NAT. Старый производный формат пересоздаётся;
|
||||
bootstrap-загрузка без подписанного snapshot не подтверждает NAT-профиль.
|
||||
При удалении старых `nat_evidence`/`nat_checked` адреса и REALITY сохраняются для bootstrap,
|
||||
старые профили сбрасываются, сериализованные snapshots инвалидируются.
|
||||
Максимальные timestamp snapshots сохраняются, чтобы новая локальная публикация
|
||||
не откатывала версию даже при коррекции часов. Чат и ключи миграция не изменяет.
|
||||
|
||||
Timestamp 0 обозначает bootstrap-сведения без подписи (конфиг, приглашение,
|
||||
начальные адреса). Они могут помочь установить первое соединение, но не
|
||||
заменяют принятую подписанную запись. По BGP принимаются подписанные записи
|
||||
с ненулевым timestamp. Формат NODEINFO изменён без обратной совместимости.
|
||||
|
||||
После принятия BGP-записи `node_conn_direct_update_node` применяет адреса к
|
||||
существующему NCD-соединению. Отсутствующее соединение этот вызов не создаёт.
|
||||
Линки дополняются как при UP, так и до установления соединения; повторная
|
||||
доставка не создаёт дубликаты UDP/TCP. Работающие линки не закрываются только
|
||||
из-за смены списка адресов. NCD удерживает ссылку на общую запись, пока
|
||||
обслуживает соединение, и использует её при восстановлении.
|
||||
|
||||
`node_conn_direct_open_node` сверяет переданную запись с реестром/БД. Более
|
||||
свежая подписанная запись принимается целиком, старая уступает уже известной.
|
||||
Bootstrap-адреса не перекрывают подписанную запись даже при DOWN.
|
||||
|
||||
Конфигурационные подключения UTUN используют один адаптер
|
||||
`init_client_connections` для старта и reload. Все адреса, TCP и REALITY
|
||||
передаются NCD; адаптер не создаёт ETCP-линки. Новый набор handles открывается
|
||||
до освобождения старого; при ошибке старые handles сохраняются. Конфигурация
|
||||
удерживает один handle на клиента, независимо от количества его адресов.
|
||||
|
||||
Включение конфигурационного пира в UTUN-группу выполняет callback NCD UP.
|
||||
Транспортный UP и наличие callback старого `etcp_connect` больше не включают
|
||||
соединение в UTUN-группу автоматически. Входящий JOIN остаётся явным запросом
|
||||
участия. При пересоздании UTUN-группы учитываются её конфигурационные handles.
|
||||
Удаление клиента отправляет `LEAVE_GROUP` и освобождает удержание UTUN-группы
|
||||
с обеих сторон. Другие владельцы NCD сохраняют транспорт. Поздние NODEINFO и
|
||||
WITHDRAW от отсоединённого UTUN-пира игнорируются, чтобы не вернуть его маршруты.
|
||||
LEAVE ждёт backpressure через queue waiter и временно удерживает собственный
|
||||
NCD handle. Закрытие транспорта отменяет ожидание; новое участие в группе
|
||||
делает ожидающий LEAVE устаревшим, и он не отправляется.
|
||||
Причина удаления пира передаётся явно: только потеря транспорта запускает
|
||||
recovery каскадных маршрутов. Локальный/удалённый LEAVE и удаление битой
|
||||
member-записи не должны восстанавливать намеренно закрытое участие.
|
||||
|
||||
REALITY использует полное SNI до 255 символов; размер поля в записи узла
|
||||
согласован с транспортом. Если локального TCP listen-сокета нет, NCD может
|
||||
создать исходящий TCP-линк с выбором локального адреса операционной системой.
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
Работа лички
|
||||
|
||||
1. отдели логику лички от логики групповых чатов. не смешивай.
|
||||
|
||||
Работа лички. сообщения и медиа доставляется напрямую если узел доступен через bgp.
|
||||
если недоступен - то pending сообщения сохраняются на суперузле и pending media сохраняется на каком-либо медиаузле, а суперузлу сообщается где хранится медиа.
|
||||
при появлении пира суперузел организует доставку медиа до получателя. после успешной доставки медиа/сообщений они удаляются с промежуточных узлов.
|
||||
|
||||
время хранения медиафайлов на промежуточных узлах ограничено - 1 неделя по умолчанию (задачется в конфиге этих узлов).
|
||||
для доставки медиа поднимаем линк (conn_mgr).
|
||||
|
||||
и посмотри реализацию медиа для групповых чатов.
|
||||
@@ -1,66 +0,0 @@
|
||||
# Приглашение в группу через PM
|
||||
|
||||
В desktop chatgui и Android кнопка «+» доступна в строке другого участника
|
||||
и в шапке личной беседы. Она открывает один диалог с фиксированным получателем
|
||||
и выбором группы, в которой отправитель состоит. Группа не выбирается автоматически.
|
||||
|
||||
После нажатия «Пригласить» клиент создаёт свежую ссылку через `invite_build`,
|
||||
ожидает подтверждения регистрации ключа и отправляет PM с `ct="invite"`.
|
||||
Зашифрованное тело: `[group_name:UTF8][NUL][полная ссылка:UTF8]`, без конечного NUL.
|
||||
Снимок для GUI содержит полную ссылку в `data` и название в `tags.group_name`.
|
||||
GUI добавляет заголовок «Приглашение в группу «название»» из перевода интерфейса;
|
||||
язык отправителя не фиксирует язык сообщения у получателя.
|
||||
Успех означает сохранение сообщения и исходящей задачи в одной транзакции.
|
||||
Доставку, повтор и шифрование обеспечивает существующий механизм DM.
|
||||
После успешной отправки открывается личная беседа с получателем.
|
||||
|
||||
Срок действия ключа — **10 минут с момента создания**, а не получения PM.
|
||||
Он указан в диалогах отправки и подключения. Поздняя offline-доставка
|
||||
может принести уже просроченную ссылку; в этом случае нужно новое приглашение.
|
||||
Ссылка не содержит время истечения, поэтому клиент показывает длительность,
|
||||
без обратного отсчёта и вычисления оставшегося времени.
|
||||
|
||||
Нажатие на `utun://` в сообщении запускает существующий JOIN и показывает его результат.
|
||||
Если клиент уже состоит в группе, открывается эта группа. Ссылка приглашения отображается
|
||||
одной строкой с многоточием; нажатие и копирование используют полный адрес.
|
||||
В Android долгое нажатие копирует ссылку, в desktop выделение сокращённой строки
|
||||
копирует ссылку целиком без запуска JOIN. Обычные текстовые PM с вручную
|
||||
вставленными ссылками сохраняют текстовое отображение.
|
||||
|
||||
## Контракт операции
|
||||
|
||||
`dm_invite_trampoline` выполняется в uasync-потоке. Запрос содержит `request_id`,
|
||||
ID получателя, выбранную группу и исходную общую группу для создания PM.
|
||||
Из шапки существующей PM исходная группа может быть пустой.
|
||||
|
||||
На instance допускается одно ожидание регистрации приглашения в PM.
|
||||
Повтор во время ожидания возвращает BUSY. Закрытие диалога отменяет только
|
||||
соответствующий `request_id`; уже отправленное сообщение не отзывается.
|
||||
Отмена и остановка DM завершают принятый запрос ровно один раз.
|
||||
|
||||
`CHAT_EVT_DM_INVITE_RESULT` содержит 28 байт:
|
||||
`[request_id:u64][result:i32][peer:u64][conv_id:u64]` в порядке байтов ядра.
|
||||
Desktop и Android сопоставляют результат с запросом и получателем.
|
||||
Ошибки регистрации или сохранения сообщения оставляют диалог открытым для повторной попытки.
|
||||
|
||||
JOIN во вторую группу может использовать уже открытый транспорт общей группы.
|
||||
Поэтому обработчики JOIN_REQUEST и JOIN_READY явно планируют member_sync,
|
||||
а JOIN_READY обновляет кеш каналов до запуска синхронизации.
|
||||
|
||||
## Проверка и диагностика
|
||||
|
||||
- `tests/test_dm_invite`: настоящая UDP-доставка ссылки в другую группу, JOIN,
|
||||
репликация участника и отправка группового сообщения после вступления;
|
||||
отмена, BUSY, неверная группа, rollback при ошибке commit и остановка DM;
|
||||
передача названия с Unicode, кавычками и переводом строки.
|
||||
- `tests/test_chat_join`, `tests/test_chat_join_e2e`, `tests/test_dm_e2e`:
|
||||
существующие сценарии регистрации ключей, JOIN и PM-доставки.
|
||||
- Qt `test_invite_links`: одна строка приглашения на разных ширинах, полный адрес
|
||||
при нажатии и копировании, переносы двух обычных ссылок, их полное содержимое при нажатии,
|
||||
выделение текста и выбор получателя кнопкой в строке участника.
|
||||
- Qt `test_emoji_playback`: перетаскивание ссылки не активирует подключение.
|
||||
|
||||
Для сетевого теста подробности включаются через `UTUN_TEST_DEBUG=1`.
|
||||
В приложении полезны категории `dm=debug`, `chat_sync=debug`, `member_sync=debug`.
|
||||
Операция пишет request, peer, group, результат и TTL; сообщения диагностики
|
||||
новой операции не выводят саму invite-ссылку.
|
||||
@@ -1,61 +0,0 @@
|
||||
# Proxy: потоки TCP и датаграммы
|
||||
|
||||
Клиентские входы — TUN (lwIP), SOCKS5 CONNECT и HTTP proxy/CONNECT. Exit открывает
|
||||
обычный TCP-сокет к назначению. TCP-поток на exit определяется парой `(peer_node_id,
|
||||
stream_id)`. Клиент принимает ответы и уведомления restart только от настроенного
|
||||
exit в UTUN-группе. UDP/ICMP-контексты принадлежат конкретному `UTUN_INSTANCE`.
|
||||
|
||||
## TCP
|
||||
|
||||
Общие определения находятся в `src/proxy/proxy_protocol.h`. Передаваемый заголовок:
|
||||
`svc_id:1, command:1, stream_id:4`; поля маршрута добавляет ETCP-router.
|
||||
|
||||
- CONNECT (1): IPv4:4 и порт:2 в сетевом порядке.
|
||||
- CONNECTED (2): exit подтвердил успешный TCP connect. Только после этого клиент
|
||||
сообщает SOCKS success / HTTP 200 и начинает передавать накопленные данные.
|
||||
- DATA (3): от 1 до 4096 байт.
|
||||
- CLOSE (4), ERROR (5): прекращение потока.
|
||||
- FIN (6): конец одного направления; все предшествующие DATA должны быть переданы.
|
||||
- WINDOW (7): uint32_t, число дополнительно разрешённых байт.
|
||||
|
||||
У каждого направления начальное окно 65536 байт. DATA уменьшает окно отправителя
|
||||
и получателя. Получатель возвращает кредит после записи в локальный TCP-сокет
|
||||
либо принятия данных ограниченным send-buffer lwIP. Очередь ETCP сама по себе
|
||||
не является подтверждением потребления данных конечным TCP.
|
||||
|
||||
FIN ждёт освобождения исходящей очереди, включая данные, остановленные окном
|
||||
или backpressure роутера. Закрытие одного направления не прекращает другое.
|
||||
Окончательное освобождение происходит после обоих FIN и передачи оставшихся данных.
|
||||
Непереданные управляющие сообщения повторяются таймером. Ошибки и переходы
|
||||
состояния диагностируются категорией `proxy`; состояние сокетов — `socket`.
|
||||
|
||||
Парсеры сохраняют остаток после greeting/request/HTTP headers. HTTP CONNECT ждёт
|
||||
полного блока заголовков. Во время DNS/connect чтение ограничено очередью tcp_io;
|
||||
накопленные HTTP headers/body передаются частями размером DATA.
|
||||
|
||||
## UDP и ICMP
|
||||
|
||||
REQUEST и REPLY различаются явно. Ответ допускается только от настроенного exit.
|
||||
UDP-ответ восстанавливается с нулевой UDP checksum (допустимо для IPv4), IPv4 checksum
|
||||
пересчитывается. ICMP checksum учитывает нечётный последний байт без чтения за буфером.
|
||||
|
||||
Exit заменяет ICMP id/sequence уникальной среди ожидающих запросов парой и сопоставляет
|
||||
ответ также с IP назначения. Исходные id/sequence восстанавливаются для клиента.
|
||||
Это исключает смешивание ping-запросов разных клиентов с одинаковыми исходными ID.
|
||||
|
||||
## Проверки и ограничения
|
||||
|
||||
`test_proxy_packets` проверяет IPv4/UDP/ICMP и payload длиной 0..1501.
|
||||
`test_tcp_io_flush` проверяет уведомление после последнего синхронного send.
|
||||
`test_proxy_regressions` использует реальные локальные сокеты и управляемую доставку
|
||||
ETCP: совместные/раздельные handshake, ранние данные, отложенный CONNECTED, HTTP 502,
|
||||
большой POST, одинаковые stream ID разных пиров, FIN при закрытом окне, RST и IPv4 options.
|
||||
|
||||
На Linux полный `./check.sh`: 109 passed, 0 failed, 1 skipped; дополнительно прошли
|
||||
настоящий TUN, full-duplex burst и нагрузка 8 соединений / 128 МиБ.
|
||||
Windows/FreeBSD в этом прогоне не проверялись.
|
||||
|
||||
SOCKS IPv6 явно отклоняется кодом address type not supported. IPv4-фрагменты
|
||||
на входе proxy TUN отклоняются с диагностикой; сборка фрагментов не реализована.
|
||||
Формат TCP дополнен CONNECTED/WINDOW, UDP-команды разделены: клиент и exit необходимо
|
||||
обновлять вместе; совместимость со старым proxy-протоколом не предусмотрена.
|
||||
@@ -1,293 +0,0 @@
|
||||
# Жизненный цикл ядра и сервисов
|
||||
|
||||
CLI выполняет daemon fork до создания `UASYNC`, SQLite, сокетов и фоновых потоков.
|
||||
Сервисы и их workers создаются уже в дочернем процессе. Родитель ждёт подтверждения
|
||||
успешной инициализации и записи PID-файла; ошибка старта возвращается вызывающему.
|
||||
`test_daemon_worker` проверяет настоящий запуск без `-f`, наличие дискового потока,
|
||||
штатный SIGTERM и ненулевой статус launcher при ошибке PID-файла.
|
||||
|
||||
Все операции выполняются в потоке единственного `UASYNC` экземпляра, вне callbacks
|
||||
останавливаемых модулей. GUI передаёт команды в этот поток.
|
||||
|
||||
```c
|
||||
struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config);
|
||||
if (!inst || utun_core_start(inst) < 0) /* обработать ошибку */;
|
||||
if (chat_service_start(inst) < 0) /* обработать ошибку */;
|
||||
if (utun_service_start(inst) < 0) /* обработать ошибку */;
|
||||
|
||||
chat_service_stop(inst); /* UTUN продолжает работать */
|
||||
chat_service_start(inst); /* загружает сохранённые каналы */
|
||||
utun_service_stop(inst); /* чат продолжает работать */
|
||||
utun_instance_destroy(inst);
|
||||
```
|
||||
|
||||
`utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён
|
||||
в конфигурации. Desktop GUI запускает ядро, UTUN и чат явно; TUN создаётся при
|
||||
`[global] tun_enabled=yes`. Без TUN и NAT запуск чата не требует прав администратора.
|
||||
Desktop подтверждает start только после готовности всех сервисов и uasync poll;
|
||||
ошибка TUN откатывает запуск и возвращается в GUI. Android запускает ядро и чат без UTUN.
|
||||
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта
|
||||
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл
|
||||
приложения; освобождение ядра выполняет `utun_instance_destroy()`.
|
||||
|
||||
Для TUN в desktop `vibechat.cfg` задаются те же параметры, что и у CLI:
|
||||
|
||||
```ini
|
||||
[global]
|
||||
tun_enabled=yes
|
||||
tun_ifname=vibechat
|
||||
tun_ip=10.42.0.1
|
||||
|
||||
[routing]
|
||||
my_subnet=10.42.0.1/32
|
||||
route_subnet=10.42.0.0/24
|
||||
```
|
||||
|
||||
Адреса и маршруты выбираются под свою сеть; изменения применяются после перезапуска.
|
||||
На Windows нужна `wintun.dll` рядом с `vibechat.exe` и запуск с правами администратора.
|
||||
CMake копирует DLL и лицензии из `lib/`; Windows-упаковка явно включает DLL, проверяет
|
||||
её архитектуру x64 и зависимости, поскольку `LoadLibrary` не отражается в импортах EXE.
|
||||
На Linux необходимы доступ к `/dev/net/tun` и права на настройку интерфейса и маршрутов.
|
||||
|
||||
Успех запуска требует успешной инициализации всех обязательных ресурсов: заданных
|
||||
listen-сокетов, мониторинга, TUN и всех `route_subnet` при включённом TUN. Частичный
|
||||
набор сокетов или маршрутов — ошибка запуска. `utun_started` устанавливается только
|
||||
после завершения всех шагов. Системные маршруты хранятся отдельным списком успешно
|
||||
созданных записей; stop и откат удаляют только этот список, в обратном порядке.
|
||||
Маршрут, на добавлении которого ядро вернуло ошибку, не становится собственностью
|
||||
UTUN и при откате не удаляется. Ошибка одного удаления не прекращает очистку остальных.
|
||||
|
||||
`auto_sockets=android` выбирает внешнее управление сетью: frontend регистрирует
|
||||
ConnectivityManager до запроса запуска и передаёт конфигурацию сокетов через JNI.
|
||||
Оба нативных монитора ОС в этом режиме не создаются. Рабочие listen-сокеты по-прежнему
|
||||
должны открыться полностью. Android подтверждает запуск по `SERVICE_STARTED`, а не
|
||||
по успешному созданию потока; после события передаётся свежий снимок адресов.
|
||||
|
||||
| Владелец | Ресурсы |
|
||||
|---|---|
|
||||
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг |
|
||||
| UTUN | Группа UTUN, запросы соединений из конфигурации, DATA binding, TUN, системные маршруты, NAT transport |
|
||||
| Чат | CHAT-группы, запросы подключения и join, экземпляры синхронизации каналов, chat API, медиа, личные сообщения, звонки и headless API |
|
||||
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery |
|
||||
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки |
|
||||
|
||||
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
|
||||
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
|
||||
`:memory:` данные сохраняются только до уничтожения ядра.
|
||||
|
||||
Работающий общий транспорт сохраняется, пока он нужен хотя бы одному из пиров. Закрытие запроса одной группы
|
||||
не разрывает соединение, нужное другой группе. При остановке группы отменяются
|
||||
её планировщики, запросы, таймеры и ожидания очередей; отправляется LEAVE,
|
||||
закрываются логические каналы маршрутизатора и удаляется её ожидающий транзит
|
||||
на общих соединениях. Транзит других групп сохраняется. Отложенный LEAVE может временно
|
||||
удерживать транспорт самостоятельно, не обращаясь к уже удалённой группе.
|
||||
|
||||
ETCP удаляет транспорт после 30 секунд непрерывного DOWN только при отсутствии локальных NCD handles.
|
||||
Первоначальное ожидание UP тоже учитывает владение. Повторный DOWN не продлевает
|
||||
таймаут; восстановление UP отменяет его. Если срок истёк при наличии handles, транспорт
|
||||
сохраняется, а исходный deadline остаётся. Освобождение последнего handle во время DOWN
|
||||
оставляет транспорт до этого deadline либо запускает удаление в следующем callback,
|
||||
если срок уже истёк. Внешние ETCP refs задерживают только освобождение памяти.
|
||||
Явный force-close и отмена неудачной попытки последнего владельца закрывают сразу.
|
||||
|
||||
Входящий UP без локальных handles сохраняется: он может быть нужен пиру. CLOSE/KEEP_ALIVE
|
||||
привязан при запуске ядра, поэтому мягкое закрытие работает и на чисто входящем узле.
|
||||
На CLOSE при наличии handles отвечаем KEEP_ALIVE; без них закрываем транспорт.
|
||||
KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE обычно занимает 5 секунд,
|
||||
для спящего пира увеличивается до его RX deadline. Сам факт DOWN во время FIN_WAIT
|
||||
завершает ранее запрошенное мягкое закрытие.
|
||||
|
||||
Контроль RX независим от отправки keepalive. Объявленный сон не отключает проверку
|
||||
жизни: срок от последнего RX равен максимуму согласованного keepalive timeout и
|
||||
объявленного остатка ACTIVE+SLEEP с запасом normal timeout. Фазовые таймеры и наши
|
||||
отправки срок жизни пира не продлевают. Пробуждение сразу сокращает deadline до normal timeout.
|
||||
После истечения последнего линка транспорт
|
||||
становится DOWN, затем может удаляться через 30 секунд, когда локальных NCD handles нет. `test_etcp_server_lifecycle`
|
||||
проверяет сроки, несколько путей, восстановление, владение, сон и CLOSE/KEEP_ALIVE
|
||||
с управляемыми часами и контролем освобождения памяти/таймеров.
|
||||
|
||||
Остановка чата сначала прекращает внешние команды и фоновые задания, затем
|
||||
закрывает медиа и сервисы, синхронизацию и группы, после чего освобождает chat core.
|
||||
`media_async_destroy()` дожидается завершения native workers и вызывает каждый
|
||||
ожидающий callback с `MEDIA_ASYNC_CANCELLED`, пока его контекст ещё существует.
|
||||
Worker резервирует completion до запуска и передаёт его через `uasync_post_reserved`;
|
||||
периодического polling нет. Destroy выполняется вне callbacks uasync, после join
|
||||
отменяет pending post и завершает done ровно один раз. Uasync живёт до конца этой очистки.
|
||||
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное
|
||||
транскодирование или распознавание речи может задержать stop; принудительного
|
||||
прерывания этих библиотек нет.
|
||||
|
||||
Live media хранится в RS-кольце с общим индексом PTS; каждый локальный handle владеет
|
||||
отдельным cursor. Пауза player не останавливает сетевой ingest и не закрепляет старые
|
||||
части против вытеснения. После FIN publisher может снять своё владение, сохранив
|
||||
завершённый источник для оставшихся локальных readers. Последний close освобождает
|
||||
источник, индекс и кольцо. Новая публикация отсоединяет readers прежней эпохи.
|
||||
Android preview собственного эфира не создаёт reader: Camera2 пишет прямо в TextureView
|
||||
и encoder Surface. UI attach/detach меняет лишь camera session; audio/encoder/origin сохраняются.
|
||||
UI, ожидающие команды camera handler и каждая camera session удерживают preview target отдельно.
|
||||
TextureView возвращает false при destroy и снимает ссылку UI; Surface/SurfaceTexture освобождаются
|
||||
после закрытия последней session. CameraDevice.onClosed снимает также оставшиеся ссылки своих
|
||||
sessions, если Android не прислал отдельные session.onClosed. Повторные callbacks безопасны.
|
||||
Capture stop запрещает attach и снимает своё владение target; camera HandlerThread завершается
|
||||
после camera close и освобождения outputs всех созданных sessions.
|
||||
Отказ preview output возвращает encoder-only session и не завершает сам эфир.
|
||||
При stop/destroy отменяются delivery и media-index callbacks до освобождения store;
|
||||
destroy отсоединённого cursor не обращается к store. Frontend прекращает audio callback
|
||||
и worker до освобождения своего DSP; borrowed handle/RS pointers между потоками не передаются.
|
||||
|
||||
Чтение video cursor запрашивает следующий кадр по готовности decoder, независимо от
|
||||
PTS показа; audio cursor сохраняет временной lookahead. Bridges готовят один ответ
|
||||
на незавершённый запрос и не читают следующий кадр после получения ответа автоматически.
|
||||
Android worker держит один рабочий input и один готовый output MediaCodec; свободный
|
||||
input buffer получает лишь для уже готовых данных и сразу возвращает codec.
|
||||
Первый decoded frame разрешает движение clock/audio. Будущий output ждёт PTS и
|
||||
приостанавливает новые video requests, сохраняя независимую обработку audio/control.
|
||||
Если первый доступный звук позже KEY, cursor отсчитывает стартовый audio lookahead от
|
||||
первого аудиопакета, сохраняя это основание неизменным. Opus/SoundTouch получает PCM до
|
||||
запуска audio clock; неподвижные часы не разрешают читать дальше этого ограниченного окна.
|
||||
PTS поданных кадров служат диагностике/проверке запаздывания, а не лимиту заполнения codec.
|
||||
Смена revision заменяет только video decoder и его измеренный защитный guard;
|
||||
GAP/EXPIRED начинают новую точку KEY. Нехватка сети сохраняет decoder references.
|
||||
Desktop FFmpeg держит один рабочий AVPacket и два готовых изображения; EAGAIN оставляет
|
||||
packet до следующего прохода. FIN подаёт EOS и дренирует выход до decoder EOF;
|
||||
конец GUI требует также показа оставшихся изображений и завершения audio tail.
|
||||
Отсутствие прогресса decoder при наличии работы имеет отдельную диагностику и срок 10s.
|
||||
|
||||
ACK-история принадлежит ядру и обновляется только при первом найденном подтверждении
|
||||
пакета. REINIT/close соединения не стирает уже подтверждённые за последнюю секунду байты.
|
||||
Общий `stream_publisher` Android/desktop владеет отдельным 100ms таймером TX-снимков;
|
||||
close/detach отменяют его в uasync до уничтожения instance. Bridges копируют снимок
|
||||
под mutex, frontend не обращается к живым очередям транспорта. Android экран подписан
|
||||
на отдельный StateFlow статистики; desktop обновляет статистику отдельно от сырого превью.
|
||||
Publisher не регистрирует stream event callback и не подменяет callback viewer.
|
||||
Bridge удерживает внешний mutex на время producer/open/stop и destroy общего publisher;
|
||||
actor не вызывает frontend callbacks под своим mutex. Reserved post, media timer,
|
||||
stats timer и stream handle освобождаются до контекста publisher.
|
||||
|
||||
Desktop `MainWindow` владеет `StreamPublishEngine`, а окно управления лишь отображает
|
||||
его состояние. Закрытие окна сохраняет эфир, новый запуск запрещён до конца FIN drain.
|
||||
Qt capture callback сохраняет последний `QVideoFrame` для preview и ограниченно ставит кадр
|
||||
в очередь отдельного долгоживущего image worker. Он конвертирует `QVideoFrame` в `QImage`;
|
||||
video worker получает готовый image и не создаёт Qt Multimedia TLS, которое могло задержать его exit на Windows.
|
||||
Video/audio workers выполняют encode/DSP вне GUI и hardware callbacks. Отдельные поколения источника и encoder позволяют менять
|
||||
видеокодек/bitrate/rotation без повторного выбора экрана в портале. Ошибки Qt capture
|
||||
обрабатываются через queued callback, чтобы не уничтожать backend из собственного callback.
|
||||
Stop запрещает producers, закрывает Qt capture и подписку на аудиозахват. Отдельный join worker
|
||||
дожидается полного выхода video/audio threads, включая native TLS Qt; GUI продолжает обрабатывать события.
|
||||
После join отправляется FIN с drain 2s; закрытие последнего микрофона может продолжаться параллельно.
|
||||
Microphone lease и owner сохраняются до завершения обоих путей. Новый start ждёт также FIN drain.
|
||||
При завершении приложения engine уничтожается до SoundManager и ядра; detach в uasync
|
||||
закрывает оставшийся publisher. Destructor ждёт workers в локальном event loop без пользовательского ввода,
|
||||
чтобы thread cleanup мог обратиться к GUI; start и внешние сигналы engine в это время запрещены.
|
||||
Workers никогда не держат borrowed instance/handle.
|
||||
Microphone lease публикации исключает call/recorder; рация согласует вход через `AudioCapture`.
|
||||
Одинаковый выбранный вход имеет одну аппаратную сессию и профиль трансляции. Разные входы независимы.
|
||||
Подключение/отключение рации не останавливает устройство трансляции: атомарный слот и счётчик readers
|
||||
дожидаются только callback удаляемого потребителя. Последняя подписка закрывает устройство.
|
||||
Если первой работала рация, публикация заменяет профиль захвата своим; после остановки публикации
|
||||
оставшаяся рация продолжает тот же захват. Recovery останавливает callbacks, workers TX и один раз
|
||||
открывает общий вход; RX рации сохраняется. Каждый потребитель обрабатывает собственную копию PCM.
|
||||
На Android `MicrophoneCapture` владеет AudioRecord и reader-потоком, подписчики — ограниченными очередями.
|
||||
Stop публикации удаляет её подписку до join Opus worker и FIN; AudioRecord остаётся живым для рации.
|
||||
PTT и PCM рации проходят через одну очередь. Последний подписчик делает stop/read join/release;
|
||||
приоритет глобального AudioManager сохраняется до освобождения трансляционного захвата.
|
||||
Ограничения фонового захвата рации, её разрешения и foreground-сервис продолжают действовать.
|
||||
Fatal ошибка audio worker передаётся в GUI с session ID и завершает всю публикацию;
|
||||
откат видеоконфигурации применяется только к video/capture. Поздний отказ старого publisher
|
||||
не завершает новую сессию. Причина ошибки сохраняется в окне после окончания drain.
|
||||
|
||||
Desktop-звонок и рация проверяют поступление корректного PCM, включая первый callback.
|
||||
Секунда без данных отключает TX и фиксирует понятную ошибку в GUI; цифровая тишина является данными.
|
||||
RX продолжает работать, рация снимает PTT, автоматического переоткрытия неисправного входа нет.
|
||||
Выбор другого микрофона или новая сессия снимают ошибку. Неисправное устройство закрывается
|
||||
в отдельном потоке; stop не присоединяет уже запущенный close worker. Новый захват ждёт его завершения.
|
||||
В звонке microphone lease удерживается до конца закрытия даже после stop. Общий AudioCapture
|
||||
рации сначала отсоединяет callback потребителя; последний владелец освобождает backend в worker.
|
||||
Сброс контекста и уничтожение владельца дожидаются worker до освобождения miniaudio-контекста.
|
||||
Штатное закрытие исправного устройства сохраняет синхронный порядок.
|
||||
|
||||
`AudioDevice` подстраивает буфер по подтверждённым backend XRUN: WASAPI DATA_DISCONTINUITY
|
||||
на входе, ALSA EPIPE, PulseAudio capture hole/overflow и playback underflow. Notification
|
||||
только увеличивает атомарный счётчик, решение и лог выполняются в GUI recovery timer.
|
||||
Исходные параметры, включая Linux capture period=10мс, сохраняются до повторных сбоев.
|
||||
Первая секунда после start исключена; нужны ошибки в двух опросах за 5с. Период увеличивается
|
||||
на 5мс, не чаще раза в 5с, до общей запрошенной ёмкости 100мс с учётом числа периодов.
|
||||
Фактический размер согласует backend; исходный или уже достигнутый больший размер не уменьшается.
|
||||
При достижении предела повторные сбои логируются без новых перезапусков. Подобранный запрос
|
||||
сохраняется при recovery, сбрасывается при смене устройства/сессии; стабильный звук не вызывает reopen.
|
||||
Подстройка микрофона закрывает устройство асинхронно, останавливая TX worker и сохраняя RX.
|
||||
Общая capture-сессия отсоединяет callbacks до close и заново подготавливает потребителей после init.
|
||||
Новая подписка во время close откладывается, stop удаляет её без ожидания backend.
|
||||
Первое переоткрытие после подстройки не использует backoff; при ошибке открытия действует обычный retry.
|
||||
|
||||
В desktop-видеозвонке Windows/Linux камера живёт в отдельном процессе того же
|
||||
`vibechat --call-camera-worker`. Служебный вход выполняется до конфига, БД и
|
||||
single-instance lock. Перечисление устройств, Qt/V4L2, преобразование и освобождение
|
||||
аппаратных кадров происходят только там. GUI получает собственный `QImage`; TX не
|
||||
обращается к драйверу. Смена codec не перезапускает захват и не ждёт следующего GOP.
|
||||
|
||||
Пиксели одного кадра передаются через mapping (Windows — pagefile-backed объект ОС,
|
||||
Linux — временный файл), метаданные/команды — через локальный сокет.
|
||||
Worker записывает mapping до FRAME; следующий write разрешён
|
||||
только после ACK, который GUI посылает после копирования в свою память. Общего mutex
|
||||
нет. В worker ожидает только последний raw-кадр; поколения отсекают старые результаты.
|
||||
Preview обновляется по готовности, TX получает condition-variable wake. PTS сохраняет
|
||||
время захвата; кадры старше 250мс отбрасываются до encode и перед send. Отброшенный
|
||||
encoded frame требует следующего KEY, поскольку encoder уже продвинул reference chain.
|
||||
|
||||
Отключение камеры и удаление окна не ждут worker. Независимый Session сохраняет
|
||||
QProcess и mapping до finished, затем удаляет себя после отсоединения владельца.
|
||||
Зависший stop получает kill через 1.5с, отсутствие прогресса — через 5с; таймеры
|
||||
живут в родительском процессе. При выходе приложения kill отправляется без wait;
|
||||
GUI не уничтожает живой QProcess. При отказе новый процесс запускается лишь по
|
||||
явной команде пользователя и после finished прежнего. Лог категории call показывает
|
||||
последнюю стадию enumerate/start/convert/release/stop и возраст кадра.
|
||||
`test_call_camera_capture` проверяет настоящую IPC-цепочку с подставным устройством:
|
||||
блокировки start/map/stop, поздние кадры после смены поколения, задержку 720p,
|
||||
повтор после отказа, отсутствие рестарта камеры при смене codec, удаление окна,
|
||||
выход приложения и независимое освобождение mapping в каждом процессе.
|
||||
|
||||
Статус транспорта UP подтверждает восстановление физического пути. READY сессии
|
||||
группы означает согласованный JOIN/ACCEPT и завершённый обмен её таблицей; именно
|
||||
его ждёт recovery. REINIT меняет эпохи обмена, сохраняя рабочие маршруты. Эпохи
|
||||
защищают от сообщений старой сессии и не задают срок жизни маршрута.
|
||||
|
||||
Проверки: `test_services` проверяет независимый stop/start, общий NCD в CONNECTING,
|
||||
сохранение идентичности/сокетов/БД, повторную загрузку каналов, отмену workers,
|
||||
заблокированного медиа-потока, транзита и ожиданий заменённой очереди. `test_chat_join_e2e`
|
||||
проверяет join и распространение membership между узлами без UTUN-сервиса.
|
||||
|
||||
В Android звонковой сессией владеет `CallController` уровня приложения; события
|
||||
ядра и команды Telecom обрабатываются один раз в main thread, без повторного
|
||||
проигрывания событий при создании ViewModel. UI получает `StateFlow` и передаёт
|
||||
пользовательские команды. `CallService` владеет `CallAudioEngine` и частичным
|
||||
wake lock: перед исходящим вызовом или принятием входящего запускается foreground
|
||||
режим `phoneCall|microphone`, затем сигналинг и аудио. Завершение сначала
|
||||
останавливает I/O и дожидается аудиопотоков, затем освобождает native-аудио,
|
||||
wake lock и сервис. Перед `NativeLib.stop/restart/destroy` выполняется тот же
|
||||
порядок; новый звонок разрешается после `SERVICE_STARTED` нового ядра.
|
||||
|
||||
Активность ядра задаётся явно: видимая Activity или незавершённый звонок удерживают
|
||||
ACTIVE и выключают Android standby. Состояние не истекает по таймеру. После
|
||||
завершения звонка в фоне возвращается standby; экран может гаснуть во время
|
||||
разговора независимо от частичного wake lock сервиса.
|
||||
|
||||
Если работают рация, публикация или просмотр трансляции, исходящий Android-звонок
|
||||
требует подтверждения их остановки; входящий показывает предупреждение до принятия.
|
||||
После подтверждения/принятия `CallController` выключает участие в рации, завершает
|
||||
публикацию и просмотр, асинхронно ждёт освобождения их аудио и только затем запускает
|
||||
сигналинг и звонковое аудио. Foreground-сервис резервируется сразу из видимой Activity.
|
||||
Отмена во время освобождения не запускает устаревший звонок.
|
||||
До окончания звонка повторное включение рации/трансляции заблокировано.
|
||||
После закрытия звонкового аудио и Telecom Connection ранее включённая рация
|
||||
возвращается в прежний канал; удержания PTT сброшены. Удалённый канал и остановленное
|
||||
ядро не восстанавливаются. Публикация и просмотр трансляции автоматически не возобновляются.
|
||||
|
||||
`utun_set_client_activity()` в сборке с `UTUN_HAVE_STANDBY` применяет фактический
|
||||
режим duty-cycle до рассылки событий активности; JNI не переключает его отдельно.
|
||||
Повторная команда не перезапускает фазу и анонсы, но первый фоновый запуск включает
|
||||
standby даже при начальном `client_activity=STANDBY`. Keepalive подписан на режим
|
||||
и фазы самого standby; новые UDP/TCP-линки сразу анонсируют текущую фазу после
|
||||
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно
|
||||
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются
|
||||
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра.
|
||||
-1250
File diff suppressed because it is too large.
Load diff
-104
@@ -1,104 +0,0 @@
|
||||
# Независимая STUN-диагностика NAT
|
||||
|
||||
Модуль `src/transport_layer/stun_nat.c/h` измеряет NAT на уникальных путях существующих IPv4 UDP-сокетов.
|
||||
Ключ группы — семейство, фактический IP и netif; имена и рабочие порты не участвуют.
|
||||
Для каждой группы создаётся один собственный неблокирующий UDP-сокет с тем же локальным IP,
|
||||
привязкой к устройству и Linux SO_MARK / FreeBSD FIB. Тестовый порт выбирается случайно из 49152–65535.
|
||||
Модуль сам регистрирует чтение в uasync, принимает STUN и закрывает свой сокет после измерения.
|
||||
STUN не меняет исходный ETCP fd, его порт и обработчики. Через `stun_nat` API модуль
|
||||
`nat_detection` получает предварительную оценку для рабочих сокетов того же IP/netif
|
||||
с совпадающими mark/FIB/device. Если два разных IP видят одинаковый стабильный внешний
|
||||
IP:порт и есть трансляция адреса, рабочий профиль получает EIM? с source=STUN и inbound=UNKNOWN.
|
||||
Это действует и для обычных серверов без запроса с незнакомого IP. Filtering тестового порта
|
||||
не переносится; измерение mapping рабочего сокета всегда имеет приоритет.
|
||||
Оценка снимается после TTL, отключения STUN, смены пути или противоречащего внешнего IP.
|
||||
Тестовый mapped endpoint не копируется в `nat_addr` и NODEINFO: адрес для подключения
|
||||
должен быть получен наблюдением самого рабочего сокета. Детектор описан в `doc/nat_detection.txt`.
|
||||
|
||||
## Конфигурация
|
||||
|
||||
```ini
|
||||
[stun]
|
||||
enabled=yes
|
||||
server=stun-a.example.org:3478
|
||||
server=stun-b.example.org:3478
|
||||
# Дополнительные строки — резервные серверы.
|
||||
|
||||
[debug]
|
||||
nat=debug
|
||||
```
|
||||
|
||||
Без `enabled=yes` модуль выключен. Без порта используется 3478. Список сохраняет порядок конфига.
|
||||
При включении нужны минимум две строки `server`; для измерения выбираются два ответивших сервера с различными IP.
|
||||
Совпадающие IP после DNS пропускаются. DNS асинхронный, с использованием существующего IPv4 A-резолвера.
|
||||
IPv6, SOCKS UDP и `only_local` пропускаются с явным статусом; TCP не проверяется.
|
||||
|
||||
## Результат
|
||||
|
||||
`stun_nat_get_result(instance, "имя исходного сокета", &result)` копирует локальный снимок:
|
||||
|
||||
Имя сначала разрешается в текущий рабочий сокет, затем в группу. API ядра
|
||||
`stun_nat_get_socket_result(instance, socket, &result)` дополнительно проверяет
|
||||
текущие IP/netif и параметры маршрутизации. Несколько рабочих портов получают общий снимок.
|
||||
|
||||
- `local` — тестовый IP:port; `servers[2]` и `mapped[2]` — реальные серверы и наблюдавшиеся внешние адреса;
|
||||
- `translated`: -1 неизвестно, 0 трансляция не наблюдалась, 1 внешний адрес отличается;
|
||||
- `mapping`: UNKNOWN, MATCH, DEPENDENT, EIM, ADM, APDM;
|
||||
- `filtering`: UNKNOWN, EIF, ADF, APDF;
|
||||
- причины неполного результата, статус, время измерения и обнаруженная поддержка RFC 5780.
|
||||
- ключ группы, generation, evidence_id, measured_at_us, количество рабочих сокетов и признак refresh.
|
||||
|
||||
Обычный STUN даёт MATCH при одинаковых адресах и DEPENDENT при разных. MATCH не доказывает EIM,
|
||||
а обычные Binding-ответы не позволяют определить filtering. Первоначальный сервер опрашивается повторно:
|
||||
смена его mapping во время проверки инвалидирует классификацию как нестабильную.
|
||||
|
||||
## RFC 5780
|
||||
|
||||
Корректные OTHER-ADDRESS и RESPONSE-ORIGIN включают автоматическую попытку Behavior Discovery.
|
||||
Нужны две публичные IP и два порта на серверной стороне, доступные во всех четырёх комбинациях.
|
||||
|
||||
1. Обычный Binding получает внешний адрес и альтернативный endpoint.
|
||||
2. CHANGE-REQUEST с флагами IP+port проверяет EIF.
|
||||
3. Если ответ не пришёл, контрольный Binding проверяет живость сервера и стабильность mapping.
|
||||
4. CHANGE-REQUEST только port различает ADF и APDF; после таймаута снова выполняется контрольный Binding.
|
||||
5. Перед публикацией ADF/APDF альтернативные endpoints проверяются обычными Binding-запросами.
|
||||
Недоступный альтернативный endpoint даёт UNKNOWN, а не ложную строгую фильтрацию.
|
||||
6. После filtering проверяется mapping: альтернативный IP с первоначальным портом, затем при необходимости
|
||||
альтернативный IP с альтернативным портом. Сравнение различает EIM, ADM и APDM.
|
||||
7. Второй сервер и повтор первого проверяют независимую пару и стабильность результата.
|
||||
|
||||
Все filtering-тесты выполняются до открывающих фильтр отправок на альтернативные endpoints.
|
||||
Собственные предыдущие STUN-отправки учитываются: при пересечении адресов следующего сервера
|
||||
сомнительный тест filtering пропускается. ERROR 420 и игнорирование CHANGE-REQUEST дают UNKNOWN.
|
||||
Аутентификация STUN не реализована: требующий её сервер фиксируется как ошибка, затем используется резервный.
|
||||
|
||||
Запрос содержит случайный 96-битный transaction ID и FINGERPRINT. Проверяются источник ответа,
|
||||
transaction ID, границы/выравнивание атрибутов, XOR-MAPPED-ADDRESS, RESPONSE-ORIGIN и FINGERPRINT при его наличии.
|
||||
Повторы одной транзакции сохраняют ID: RTO от 500 мс удваивается, всего семь отправок, последнее ожидание 8 секунд.
|
||||
Новые транзакции всех проб ограничены десятью в секунду; ожидания не блокируют event loop.
|
||||
|
||||
## Жизненный цикл и проверки
|
||||
|
||||
Ядро запускает модуль при `utun_core_start`; остановка уничтожает его до сетевых ресурсов и config.
|
||||
Перезагрузка конфига отменяет старые DNS/таймеры и создаёт новый модуль.
|
||||
Раз в три секунды выполняется локальная сверка групп: новые/изменённые проверяются,
|
||||
группа удаляется после исчезновения последнего рабочего сокета. Смена имени/представителя
|
||||
или удаление дубликата не теряет кэш. Несовпадение mark/FIB/device внутри одного ключа
|
||||
даёт UNKNOWN с причиной incompatible routing policy.
|
||||
TTL результата 300 секунд, refresh 180 секунд; неполные/неудачные проверки повторяются через 30 секунд.
|
||||
Во время штатного refresh доступен прежний результат до TTL; его возраст не обновляется.
|
||||
Использованные тестовые порты не переиспользуются в течение жизни ядра, включая reload STUN.
|
||||
История портов освобождается только при окончательном destroy ядра.
|
||||
`stun_nat_recheck(instance)` явно повторяет измерение на новых портах, в том числе после смены маршрута без смены IP.
|
||||
Публичные операции выполняются в uasync-потоке экземпляра.
|
||||
|
||||
INFO содержит начало измерения и итог с адресами, классификацией и причинами UNKNOWN.
|
||||
DEBUG содержит этапы, transaction ID, повторы, RTT, DNS и очистку; WARN/ERROR описывают аномалии.
|
||||
|
||||
Linux-тест `test_stun_nat` использует реальные локальные UDP-сокеты и ускоренные часы:
|
||||
обычный STUN, все классы RFC 5780, недоступные альтернативы, потеря запроса, неверные/поздние ответы,
|
||||
резервные серверы, DNS, нестабильный mapping, отмена и повторная проверка, изменение состава сокетов.
|
||||
Сценарии проверяют сохранность рабочего fd/порта, предварительный EIM?, приоритет ETCP,
|
||||
снятие оценки после TTL, совпадение IP/netif и освобождение таймеров.
|
||||
|
||||
Ссылки: [RFC 5780](https://www.rfc-editor.org/rfc/rfc5780), [STUN RFC 8489](https://www.rfc-editor.org/rfc/rfc8489).
|
||||
-1211
File diff suppressed because it is too large.
Load diff
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-present Silero Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+2
-37
@@ -25,10 +25,6 @@ libuasync_a_SOURCES = \
|
||||
radix.h \
|
||||
serialize.c \
|
||||
serialize.h \
|
||||
video_plane.c \
|
||||
video_plane.h \
|
||||
reed_solomon.c \
|
||||
reed_solomon.h \
|
||||
json_flat.c \
|
||||
json_flat.h \
|
||||
tcp_io.c \
|
||||
@@ -44,48 +40,17 @@ libuasync_a_SOURCES = \
|
||||
dns.c \
|
||||
dns.h \
|
||||
async_dns.c \
|
||||
async_dns.h \
|
||||
speex_aec.c \
|
||||
speex_aec.h \
|
||||
speexdsp/mdf.c \
|
||||
speexdsp/fftwrap.c \
|
||||
speexdsp/kiss_fft.c \
|
||||
speexdsp/kiss_fftr.c
|
||||
|
||||
if HAVE_SILERO_VAD
|
||||
libuasync_a_SOURCES += \
|
||||
silero_vad.c \
|
||||
silero_vad.h
|
||||
endif
|
||||
async_dns.h
|
||||
|
||||
libuasync_a_CFLAGS = \
|
||||
-D_ISOC99_SOURCE \
|
||||
-DDEBUG_OUTPUT_STDERR \
|
||||
-DSQLITE_THREADSAFE=1 \
|
||||
-DDNS_RANDOM=RAND_bytes \
|
||||
-DFLOATING_POINT \
|
||||
-DUSE_KISS_FFT \
|
||||
-DEXPORT= \
|
||||
-g \
|
||||
-I$(top_srcdir)/src \
|
||||
-I$(top_srcdir)/lib \
|
||||
-I$(top_srcdir)/lib/libopus/include \
|
||||
-I$(top_srcdir)/lib/speexdsp \
|
||||
-I$(top_srcdir)/lib/speexdsp/include
|
||||
|
||||
if HAVE_SILERO_VAD
|
||||
libuasync_a_CFLAGS += -DHAVE_SILERO_VAD
|
||||
endif
|
||||
|
||||
# Вшитая модель Silero VAD (включается из silero_vad.c)
|
||||
EXTRA_DIST = silero_vad_model.inc \
|
||||
COPYING.silero-vad \
|
||||
wintun-LICENSE.txt \
|
||||
wintun-api-LICENSE.txt \
|
||||
speexdsp/COPYING \
|
||||
liblmdb/LICENSE \
|
||||
liblmdb/COPYRIGHT \
|
||||
soundtouch-master/COPYING.TXT
|
||||
-I$(top_srcdir)/lib/libopus/include
|
||||
|
||||
# Clean build directory
|
||||
clean-local:
|
||||
|
||||
+2
-119
@@ -20,7 +20,6 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <ctype.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <iphlpapi.h>
|
||||
@@ -38,9 +37,6 @@ struct adns_query {
|
||||
void* timer; /* handle таймаута (NULL когда нет) */
|
||||
int finished;
|
||||
char* name;
|
||||
int fast; /* 1 = готовый /etc/hosts-ответ, без libdns */
|
||||
struct adns_result fast_result;
|
||||
void* call_soon_id; /* отложенный вызов (fast-путь и первый драйв slow-пути) */
|
||||
};
|
||||
|
||||
static void adns_drive(struct adns_query* q);
|
||||
@@ -48,8 +44,6 @@ static void adns_finish(struct adns_query* q, int status, struct sockaddr_in* ad
|
||||
static void adns_read_cb(socket_t sock, void* arg);
|
||||
static void adns_write_cb(socket_t sock, void* arg);
|
||||
static void adns_timer_cb(void* arg);
|
||||
static void adns_fast_ready_cb(void* arg);
|
||||
static int adns_hosts_lookup(const char* name, struct sockaddr_in* out);
|
||||
|
||||
/* ─── system DNS servers discovery ─── */
|
||||
|
||||
@@ -130,49 +124,6 @@ int adns_system_servers(struct sockaddr_in* out, int max) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ─── синхронный /etc/hosts-лукап (быстрый путь, без libdns) ─── */
|
||||
|
||||
// Ищет name в /etc/hosts. Возвращает 1 если найден IPv4-адрес (out заполнен).
|
||||
// Блокирующее чтение небольшого файла — только для локальных имён (localhost и т.п.).
|
||||
static int adns_hosts_lookup(const char* name, struct sockaddr_in* out) {
|
||||
#ifndef _WIN32
|
||||
if (!name || !out) return 0;
|
||||
FILE* f = fopen("/etc/hosts", "r");
|
||||
if (!f) return 0;
|
||||
char line[512];
|
||||
int found = 0;
|
||||
while (!found && fgets(line, sizeof(line), f)) {
|
||||
char ip[64]; int n = 0;
|
||||
const char* p = line;
|
||||
while (*p && !isspace(*p) && n < 63) ip[n++] = *p++;
|
||||
ip[n] = '\0';
|
||||
if (!ip[0] || ip[0] == '#') continue;
|
||||
struct in_addr a;
|
||||
if (inet_pton(AF_INET, ip, &a) != 1) continue;
|
||||
while (*p && isspace(*p)) p++;
|
||||
while (*p && *p != '\n') {
|
||||
if (*p == '#') break;
|
||||
char h[256]; int hn = 0;
|
||||
while (*p && !isspace(*p) && *p != '#' && hn < 255) h[hn++] = *p++;
|
||||
h[hn] = '\0';
|
||||
if (h[0] && strcasecmp(h, name) == 0) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->sin_family = AF_INET;
|
||||
out->sin_addr = a;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
while (*p && isspace(*p)) p++;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return found;
|
||||
#else
|
||||
(void)name; (void)out;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ─── resolver setup ─── */
|
||||
|
||||
static struct dns_resolver* adns_open_resolver(const struct sockaddr_in* servers, int count,
|
||||
@@ -204,11 +155,6 @@ static struct dns_resolver* adns_open_resolver(const struct sockaddr_in* servers
|
||||
dns_resconf_close(rc);
|
||||
return NULL;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
// Локальные имена из /etc/hosts (localhost и т.п.). Не фатально, если файла нет.
|
||||
if (dns_hosts_loadpath(hosts, "/etc/hosts") != 0)
|
||||
DEBUG_DEBUG(ADNS_DEBUG_CAT, "adns: /etc/hosts not loaded");
|
||||
#endif
|
||||
|
||||
struct dns_hints* hints = dns_hints_local(rc, &error);
|
||||
if (!hints) {
|
||||
@@ -251,7 +197,7 @@ static void adns_rearm(struct adns_query* q) {
|
||||
}
|
||||
q->cur_fd = -1;
|
||||
if (fd >= 0) {
|
||||
q->socket_id = uasync_add_socket_t(q->ua, (socket_t)fd, adns_read_cb, adns_write_cb, NULL, "dns_query", q);
|
||||
q->socket_id = uasync_add_socket_t(q->ua, (socket_t)fd, adns_read_cb, adns_write_cb, NULL, q);
|
||||
if (q->socket_id) q->cur_fd = fd;
|
||||
else DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: uasync_add_socket_t(%d) failed", fd);
|
||||
}
|
||||
@@ -380,34 +326,6 @@ static void adns_finish(struct adns_query* q, int status, struct sockaddr_in* ad
|
||||
u_free(name);
|
||||
}
|
||||
|
||||
// Отложенный вызов коллбэка для fast-пути (/etc/hosts): сохраняет async-контракт
|
||||
// (adns_resolve возвращает handle, коллбэк вызывается в потоке event loop).
|
||||
static void adns_fast_ready_cb(void* arg) {
|
||||
struct adns_query* q = (struct adns_query*)arg;
|
||||
q->call_soon_id = NULL;
|
||||
if (q->finished) return; // отменён до срабатывания
|
||||
q->finished = 1;
|
||||
adns_done_cb cb = q->cb;
|
||||
void* a = q->arg;
|
||||
struct adns_result res = q->fast_result;
|
||||
char* nm = q->name;
|
||||
q->name = NULL;
|
||||
u_free(q);
|
||||
if (cb) cb(&res, a);
|
||||
u_free(nm);
|
||||
}
|
||||
|
||||
// Отложенный первый драйв slow-пути (libdns): adns_resolve возвращает живой handle,
|
||||
// а реальный запуск запроса происходит в event loop. Это исключает ситуацию, когда
|
||||
// dns_res_check завершается синхронно прямо внутри adns_resolve() и освобождает q
|
||||
// до того, как вызывающий код получил и сохранил handle.
|
||||
static void adns_drive_trampoline(void* arg) {
|
||||
struct adns_query* q = (struct adns_query*)arg;
|
||||
q->call_soon_id = NULL;
|
||||
if (q->finished) return; // отменён до срабатывания
|
||||
adns_drive(q);
|
||||
}
|
||||
|
||||
/* ─── public API ─── */
|
||||
|
||||
struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
|
||||
@@ -416,28 +334,6 @@ struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
|
||||
if (!ua || !name || !name[0] || !cb) return NULL;
|
||||
if (strlen(name) > 255) { DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: name too long"); return NULL; }
|
||||
|
||||
// Быстрый путь: имя из /etc/hosts (localhost и т.п.) — без libdns и сети.
|
||||
// libdns по умолчанию идёт в DNS первым (lookup="bf"), поэтому локальные имена
|
||||
// ждали бы таймаут DNS-запроса (2×2s); обрабатываем их сразу.
|
||||
{
|
||||
struct sockaddr_in ha;
|
||||
if (adns_hosts_lookup(name, &ha)) {
|
||||
struct adns_query* q = u_calloc(1, sizeof(*q));
|
||||
if (!q) { DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: calloc failed"); return NULL; }
|
||||
q->ua = ua; q->cb = cb; q->arg = arg;
|
||||
q->name = u_strdup(name);
|
||||
if (!q->name) { u_free(q); DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: strdup failed"); return NULL; }
|
||||
q->fast = 1;
|
||||
q->fast_result.status = ADNS_OK;
|
||||
q->fast_result.count = 1;
|
||||
q->fast_result.addrs[0] = ha;
|
||||
q->call_soon_id = uasync_call_soon(ua, q, adns_fast_ready_cb);
|
||||
if (!q->call_soon_id) { u_free(q->name); u_free(q); return NULL; }
|
||||
DEBUG_DEBUG(ADNS_DEBUG_CAT, "adns: '%s' resolved from /etc/hosts (fast)", name);
|
||||
return q;
|
||||
}
|
||||
}
|
||||
|
||||
struct sockaddr_in servers[3];
|
||||
int count;
|
||||
if (opts && opts->server.sin_family != 0) {
|
||||
@@ -477,26 +373,13 @@ struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
|
||||
DEBUG_DEBUG(ADNS_DEBUG_CAT, "adns: resolving '%s' via %d server(s), timeout=%dms attempts=%d",
|
||||
name, count, timeout_ms, attempts);
|
||||
|
||||
q->call_soon_id = uasync_call_soon(ua, q, adns_drive_trampoline);
|
||||
if (!q->call_soon_id) {
|
||||
DEBUG_ERROR(ADNS_DEBUG_CAT, "adns: uasync_call_soon failed for '%s'", name);
|
||||
dns_res_close(q->res);
|
||||
u_free(q->name);
|
||||
u_free(q);
|
||||
return NULL;
|
||||
}
|
||||
adns_drive(q);
|
||||
return q;
|
||||
}
|
||||
|
||||
void adns_cancel(struct adns_query* q) {
|
||||
if (!q || q->finished) return;
|
||||
q->finished = 1;
|
||||
if (q->call_soon_id) { uasync_call_soon_cancel(q->ua, q->call_soon_id); q->call_soon_id = NULL; }
|
||||
if (q->fast) {
|
||||
u_free(q->name);
|
||||
u_free(q);
|
||||
return;
|
||||
}
|
||||
adns_teardown(q);
|
||||
u_free(q->name);
|
||||
u_free(q);
|
||||
|
||||
+226
-187
@@ -3,144 +3,223 @@
|
||||
#include "mem.h"
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define BLOCK_LEVELS_CHUNK 64
|
||||
#define PENDING_CHUNK 32
|
||||
|
||||
/* История и задержанные блоки ограничены окном настройки. Только запись накапливает output. */
|
||||
struct audio_compressor {
|
||||
audio_compressor_config_t cfg;
|
||||
int enabled, configured, failed, block_samples, lookback_blocks, lookahead_blocks;
|
||||
float rise_factor_per_block, release_factor_per_block, max_gain, gain_smoothed;
|
||||
float* block_levels;
|
||||
size_t levels_cap, blocks_seen;
|
||||
int16_t* pending;
|
||||
size_t pending_cap, pending_head, pending_count, accum_count;
|
||||
int16_t* output;
|
||||
size_t output_size, output_cap;
|
||||
uint32_t clip_count;
|
||||
unsigned dbg_counter;
|
||||
};
|
||||
int enabled;
|
||||
|
||||
/* Начальное усиление в середине диапазона в дБ. */
|
||||
static float initial_gain(float max_gain) { return max_gain > 0.0f ? sqrtf(max_gain) : 1.0f; }
|
||||
int block_samples;
|
||||
int lookback_blocks;
|
||||
int lookahead_blocks;
|
||||
float rise_factor_per_block;
|
||||
float max_gain;
|
||||
float gain_smoothed;
|
||||
|
||||
float* block_levels;
|
||||
size_t block_levels_count;
|
||||
size_t block_levels_cap;
|
||||
|
||||
struct pending_block {
|
||||
int16_t* samples;
|
||||
size_t count;
|
||||
size_t level_index;
|
||||
}* pending;
|
||||
size_t pending_count;
|
||||
size_t pending_cap;
|
||||
|
||||
int16_t* accumulator;
|
||||
size_t accum_count;
|
||||
|
||||
int16_t* output;
|
||||
size_t output_size;
|
||||
size_t output_cap;
|
||||
|
||||
int dbg_counter;
|
||||
};
|
||||
|
||||
struct audio_compressor* audio_compressor_create(void) {
|
||||
struct audio_compressor* ac = u_calloc(1, sizeof(*ac));
|
||||
if (!ac) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: state allocation failed"); return NULL; }
|
||||
if (!ac) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: OOM");
|
||||
return NULL;
|
||||
}
|
||||
ac->enabled = 1;
|
||||
ac->gain_smoothed = 1.0f;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_create: ok");
|
||||
return ac;
|
||||
}
|
||||
|
||||
void audio_compressor_destroy(struct audio_compressor* ac) {
|
||||
if (!ac) return;
|
||||
u_free(ac->pending); u_free(ac->block_levels); u_free(ac->output); u_free(ac);
|
||||
for (size_t i = 0; i < ac->pending_count; i++)
|
||||
u_free(ac->pending[i].samples);
|
||||
u_free(ac->pending);
|
||||
u_free(ac->block_levels);
|
||||
u_free(ac->accumulator);
|
||||
u_free(ac->output);
|
||||
u_free(ac);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor_destroy");
|
||||
}
|
||||
|
||||
void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg) {
|
||||
if (!ac || !cfg) return;
|
||||
ac->cfg = *cfg;
|
||||
if (ac->cfg.sample_rate <= 0) ac->cfg.sample_rate = 48000;
|
||||
if (ac->cfg.channels <= 0) ac->cfg.channels = 1;
|
||||
if (ac->cfg.block_duration_ms <= 0) ac->cfg.block_duration_ms = 20;
|
||||
if (ac->cfg.lookback_ms <= 0) ac->cfg.lookback_ms = 200;
|
||||
if (ac->cfg.lookahead_ms <= 0) ac->cfg.lookahead_ms = 100;
|
||||
if (ac->cfg.target_level <= 0.0f) ac->cfg.target_level = 0.25f;
|
||||
|
||||
ac->block_samples = ac->cfg.sample_rate * ac->cfg.block_duration_ms / 1000 * ac->cfg.channels;
|
||||
ac->lookback_blocks = ac->cfg.lookback_ms / ac->cfg.block_duration_ms;
|
||||
ac->lookahead_blocks = ac->cfg.lookahead_ms / ac->cfg.block_duration_ms;
|
||||
ac->rise_factor_per_block = powf(ac->cfg.rise_rate_per_sec, (float)ac->cfg.block_duration_ms / 1000.0f);
|
||||
ac->max_gain = powf(10.0f, ac->cfg.max_gain_db / 20.0f);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL,
|
||||
"audio_compressor_configure: rate=%d ch=%d block=%dms lookback=%dms lookahead=%dms maxGain=%.0fdB riseRate=%.1f/sec target=%.0fdBFS",
|
||||
ac->cfg.sample_rate, ac->cfg.channels, ac->cfg.block_duration_ms,
|
||||
ac->cfg.lookback_ms, ac->cfg.lookahead_ms, ac->cfg.max_gain_db,
|
||||
ac->cfg.rise_rate_per_sec, 20.0f * log10f(ac->cfg.target_level));
|
||||
}
|
||||
|
||||
void audio_compressor_reset(struct audio_compressor* ac) {
|
||||
if (!ac) return;
|
||||
ac->pending_head = ac->pending_count = ac->accum_count = ac->blocks_seen = ac->output_size = 0;
|
||||
ac->gain_smoothed = initial_gain(ac->max_gain);
|
||||
ac->clip_count = ac->dbg_counter = 0;
|
||||
ac->failed = 0;
|
||||
for (size_t i = 0; i < ac->pending_count; i++)
|
||||
u_free(ac->pending[i].samples);
|
||||
ac->pending_count = 0;
|
||||
ac->block_levels_count = 0;
|
||||
ac->accum_count = 0;
|
||||
ac->output_size = 0;
|
||||
ac->gain_smoothed = 1.0f;
|
||||
ac->dbg_counter = 0;
|
||||
}
|
||||
|
||||
/* Ноль lookahead означает отсутствие задержки; defaults остальных полей задаются явно здесь. */
|
||||
int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* config) {
|
||||
if (!ac || !config) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: missing configuration"); return -1; }
|
||||
audio_compressor_config_t cfg = *config;
|
||||
if (!cfg.sample_rate) cfg.sample_rate = 48000;
|
||||
if (!cfg.channels) cfg.channels = 1;
|
||||
if (!cfg.block_duration_ms) cfg.block_duration_ms = 20;
|
||||
if (!cfg.lookback_ms) cfg.lookback_ms = 200;
|
||||
if (!cfg.target_level) cfg.target_level = 0.25f;
|
||||
if (!cfg.rise_rate_per_sec) cfg.rise_rate_per_sec = 4.0f;
|
||||
if (!cfg.release_rate_per_sec) cfg.release_rate_per_sec = cfg.rise_rate_per_sec * 5.0f;
|
||||
if (cfg.sample_rate < 8000 || cfg.sample_rate > 192000 || cfg.channels < 1 || cfg.channels > 2 ||
|
||||
cfg.block_duration_ms < 1 || cfg.block_duration_ms > 1000 || cfg.lookback_ms < 0 || cfg.lookback_ms > 5000 ||
|
||||
cfg.lookahead_ms < 0 || cfg.lookahead_ms > 5000 || !isfinite(cfg.target_level) || cfg.target_level <= 0 ||
|
||||
!isfinite(cfg.max_gain_db) || cfg.max_gain_db < 0 || cfg.max_gain_db > 60 ||
|
||||
!isfinite(cfg.rise_rate_per_sec) || cfg.rise_rate_per_sec <= 0 ||
|
||||
!isfinite(cfg.release_rate_per_sec) || cfg.release_rate_per_sec <= 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid rate=%d ch=%d block=%d back=%d ahead=%d",
|
||||
cfg.sample_rate, cfg.channels, cfg.block_duration_ms, cfg.lookback_ms, cfg.lookahead_ms);
|
||||
return -1;
|
||||
}
|
||||
int block = cfg.sample_rate * cfg.block_duration_ms / 1000 * cfg.channels;
|
||||
int back = cfg.lookback_ms / cfg.block_duration_ms, ahead = cfg.lookahead_ms / cfg.block_duration_ms;
|
||||
size_t levels_cap = (size_t)back + ahead + 2, pending_cap = (size_t)ahead + 2;
|
||||
float* levels = u_calloc(levels_cap, sizeof(float));
|
||||
int16_t* pending = u_calloc(pending_cap * (size_t)block, sizeof(int16_t));
|
||||
if (!levels || !pending) {
|
||||
u_free(levels); u_free(pending);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: buffers allocation failed");
|
||||
return -1;
|
||||
}
|
||||
u_free(ac->block_levels); u_free(ac->pending);
|
||||
ac->cfg = cfg; ac->block_samples = block;
|
||||
ac->lookback_blocks = back; ac->lookahead_blocks = ahead;
|
||||
ac->block_levels = levels; ac->levels_cap = levels_cap;
|
||||
ac->pending = pending; ac->pending_cap = pending_cap;
|
||||
ac->rise_factor_per_block = powf(cfg.rise_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f);
|
||||
ac->release_factor_per_block = powf(cfg.release_rate_per_sec, (float)cfg.block_duration_ms / 1000.0f);
|
||||
ac->max_gain = powf(10.0f, cfg.max_gain_db / 20.0f);
|
||||
ac->configured = 1;
|
||||
audio_compressor_reset(ac);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "compressor: configured rate=%d ch=%d ahead=%dms history=%zu pending=%zu",
|
||||
cfg.sample_rate, cfg.channels, cfg.lookahead_ms, levels_cap, pending_cap);
|
||||
return 0;
|
||||
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) {
|
||||
if (!ac) return;
|
||||
ac->enabled = enabled ? 1 : 0;
|
||||
}
|
||||
|
||||
struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source) {
|
||||
if (!source || !source->configured) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: cannot clone configuration"); return NULL; }
|
||||
struct audio_compressor* copy = audio_compressor_create();
|
||||
if (copy && audio_compressor_configure(copy, &source->cfg) != 0) { audio_compressor_destroy(copy); return NULL; }
|
||||
if (copy) copy->enabled = source->enabled;
|
||||
return copy;
|
||||
int audio_compressor_is_enabled(const struct audio_compressor* ac) {
|
||||
return ac ? ac->enabled : 0;
|
||||
}
|
||||
|
||||
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled) { if (ac) ac->enabled = !!enabled; }
|
||||
int audio_compressor_is_enabled(const struct audio_compressor* ac) { return ac && ac->enabled; }
|
||||
|
||||
/* Одна оценка для всех каналов сохраняет баланс стерео. */
|
||||
static float compute_block_level(const int16_t* samples, size_t count) {
|
||||
double sum_sq = 0.0;
|
||||
float peak = 0.0f;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
float v = (float)samples[i] / 32768.0f;
|
||||
sum_sq += (double)v * v;
|
||||
if (fabsf(v) > peak) peak = fabsf(v);
|
||||
float av = fabsf(v);
|
||||
if (av > peak) peak = av;
|
||||
}
|
||||
return (2.0f * sqrtf((float)(sum_sq / count)) + peak) / 2.0f;
|
||||
float rms = (float)sqrt(sum_sq / (double)count);
|
||||
return (rms * 2.0f + peak) / 2.0f;
|
||||
}
|
||||
|
||||
/* Выход записи растёт только вне аппаратного callback; OOM переводит запись в failed. */
|
||||
static int reserve_output(struct audio_compressor* ac, size_t count) {
|
||||
if (count > SIZE_MAX / sizeof(int16_t) - ac->output_size) goto failed;
|
||||
size_t want = ac->output_size + count;
|
||||
if (want <= ac->output_cap) return 0;
|
||||
size_t capacity = ac->output_cap ? ac->output_cap : 4096;
|
||||
while (capacity < want) {
|
||||
if (capacity > SIZE_MAX / sizeof(int16_t) / 2) { capacity = want; break; }
|
||||
capacity *= 2;
|
||||
}
|
||||
int16_t* samples = u_realloc(ac->output, capacity * sizeof(int16_t));
|
||||
if (!samples) goto failed;
|
||||
ac->output = samples; ac->output_cap = capacity;
|
||||
return 0;
|
||||
failed:
|
||||
ac->failed = 1;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: recording output allocation failed samples=%zu", count);
|
||||
return -1;
|
||||
static void ensure_output_cap(struct audio_compressor* ac, size_t needed) {
|
||||
size_t want = ac->output_size + needed;
|
||||
if (want <= ac->output_cap) return;
|
||||
size_t new_cap = ac->output_cap ? ac->output_cap * 2 : 4096;
|
||||
while (new_cap < want) new_cap *= 2;
|
||||
int16_t* tmp = u_realloc(ac->output, new_cap * sizeof(int16_t));
|
||||
if (!tmp) return;
|
||||
ac->output = tmp;
|
||||
ac->output_cap = new_cap;
|
||||
}
|
||||
|
||||
/* Общий алгоритм усиления для записи с lookahead и потокового кадра без lookahead. */
|
||||
static void apply_gain(struct audio_compressor* ac, int16_t* samples, size_t count, size_t idx) {
|
||||
size_t begin = idx >= (size_t)ac->lookback_blocks ? idx - ac->lookback_blocks : 0;
|
||||
size_t end = idx + ac->lookahead_blocks;
|
||||
if (end >= ac->blocks_seen) end = ac->blocks_seen - 1;
|
||||
float envelope = 0;
|
||||
for (size_t i = begin; i <= end; i++) {
|
||||
float level = ac->block_levels[i % ac->levels_cap];
|
||||
if (level > envelope) envelope = level;
|
||||
static void append_output(struct audio_compressor* ac, const int16_t* samples, size_t count) {
|
||||
ensure_output_cap(ac, count);
|
||||
memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
|
||||
ac->output_size += count;
|
||||
}
|
||||
|
||||
static void ensure_block_levels_cap(struct audio_compressor* ac) {
|
||||
if (ac->block_levels_count < ac->block_levels_cap) return;
|
||||
size_t new_cap = ac->block_levels_cap ? ac->block_levels_cap * 2 : BLOCK_LEVELS_CHUNK;
|
||||
float* tmp = u_realloc(ac->block_levels, new_cap * sizeof(float));
|
||||
if (!tmp) return;
|
||||
ac->block_levels = tmp;
|
||||
ac->block_levels_cap = new_cap;
|
||||
}
|
||||
|
||||
static void add_block_level(struct audio_compressor* ac, float level) {
|
||||
ensure_block_levels_cap(ac);
|
||||
ac->block_levels[ac->block_levels_count++] = level;
|
||||
}
|
||||
|
||||
static void ensure_pending_cap(struct audio_compressor* ac) {
|
||||
if (ac->pending_count < ac->pending_cap) return;
|
||||
size_t new_cap = ac->pending_cap ? ac->pending_cap * 2 : PENDING_CHUNK;
|
||||
struct pending_block* tmp = u_realloc(ac->pending, new_cap * sizeof(*tmp));
|
||||
if (!tmp) return;
|
||||
ac->pending = tmp;
|
||||
ac->pending_cap = new_cap;
|
||||
}
|
||||
|
||||
static void process_pending_block(struct audio_compressor* ac);
|
||||
|
||||
void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) {
|
||||
if (!ac || !samples || count == 0) return;
|
||||
|
||||
if (!ac->enabled) {
|
||||
append_output(ac, samples, count);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t remaining = count;
|
||||
const int16_t* src = samples;
|
||||
|
||||
while (remaining > 0) {
|
||||
size_t need = (size_t)ac->block_samples - ac->accum_count;
|
||||
size_t take = remaining < need ? remaining : need;
|
||||
|
||||
size_t new_cnt = ac->accum_count + take;
|
||||
int16_t* tmp = u_realloc(ac->accumulator, new_cnt * sizeof(int16_t));
|
||||
if (!tmp) return;
|
||||
ac->accumulator = tmp;
|
||||
memcpy(ac->accumulator + ac->accum_count, src, take * sizeof(int16_t));
|
||||
ac->accum_count = new_cnt;
|
||||
|
||||
src += take;
|
||||
remaining -= take;
|
||||
|
||||
if (ac->accum_count == (size_t)ac->block_samples) {
|
||||
float level = compute_block_level(ac->accumulator, ac->accum_count);
|
||||
add_block_level(ac, level);
|
||||
|
||||
ensure_pending_cap(ac);
|
||||
ac->pending[ac->pending_count].samples = ac->accumulator;
|
||||
ac->pending[ac->pending_count].count = ac->accum_count;
|
||||
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
|
||||
ac->pending_count++;
|
||||
|
||||
ac->accumulator = NULL;
|
||||
ac->accum_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
while (ac->pending_count > (size_t)ac->lookahead_blocks)
|
||||
process_pending_block(ac);
|
||||
}
|
||||
|
||||
static void process_pending_block(struct audio_compressor* ac) {
|
||||
if (ac->pending_count == 0) return;
|
||||
|
||||
size_t idx = ac->pending[0].level_index;
|
||||
|
||||
size_t win_start = idx >= (size_t)ac->lookback_blocks ? idx - (size_t)ac->lookback_blocks : 0;
|
||||
size_t win_end = idx + (size_t)ac->lookahead_blocks;
|
||||
if (win_end >= ac->block_levels_count) win_end = ac->block_levels_count - 1;
|
||||
|
||||
float envelope = 0.0f;
|
||||
for (size_t i = win_start; i <= win_end; i++) {
|
||||
if (ac->block_levels[i] > envelope) envelope = ac->block_levels[i];
|
||||
}
|
||||
if (envelope < 0.0001f) envelope = 0.0001f;
|
||||
|
||||
@@ -148,106 +227,66 @@ static void apply_gain(struct audio_compressor* ac, int16_t* samples, size_t cou
|
||||
float G_prev = ac->gain_smoothed;
|
||||
|
||||
if (G_raw > ac->gain_smoothed) {
|
||||
/* сигнал стал тише — плавно поднимаем gain (атака) */
|
||||
float max_rise = ac->gain_smoothed * ac->rise_factor_per_block;
|
||||
ac->gain_smoothed = G_raw < max_rise ? G_raw : max_rise;
|
||||
} else {
|
||||
/* сигнал стал громче — плавно опускаем gain (release), без мгновенной ступеньки
|
||||
* (мгновенный дроп давал клик на границе 20мс-блока). */
|
||||
float min_fall = ac->gain_smoothed / ac->release_factor_per_block;
|
||||
ac->gain_smoothed = G_raw > min_fall ? G_raw : min_fall;
|
||||
ac->gain_smoothed = G_raw;
|
||||
}
|
||||
if (ac->gain_smoothed > ac->max_gain) ac->gain_smoothed = ac->max_gain;
|
||||
|
||||
float gain_db = 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f);
|
||||
float prev_db = 20.0f * log10f(G_prev > 0.0001f ? G_prev : 0.0001f);
|
||||
if (ac->dbg_counter % 25 == 0 || fabsf(gain_db - prev_db) > 3.0f) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB clip=%u",
|
||||
idx, ac->block_levels[idx % ac->levels_cap], envelope, 20.0f * log10f(G_raw), gain_db, (unsigned)ac->clip_count);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "audio_compressor: block %zu level=%.4f envelope=%.4f gainRaw=%.1fdB gain=%.1fdB",
|
||||
idx, ac->block_levels[idx], envelope, 20.0f * log10f(G_raw), gain_db);
|
||||
}
|
||||
ac->dbg_counter++;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
float v = (float)samples[i] * ac->gain_smoothed;
|
||||
if (v > 32767.0f) { v = 32767.0f; ac->clip_count++; }
|
||||
if (v < -32768.0f) { v = -32768.0f; ac->clip_count++; }
|
||||
samples[i] = (int16_t)(int)v;
|
||||
for (size_t i = 0; i < ac->pending[0].count; i++) {
|
||||
float v = (float)ac->pending[0].samples[i] * ac->gain_smoothed;
|
||||
if (v > 32767.0f) v = 32767.0f;
|
||||
if (v < -32768.0f) v = -32768.0f;
|
||||
ac->pending[0].samples[i] = (int16_t)(int)v;
|
||||
}
|
||||
append_output(ac, ac->pending[0].samples, ac->pending[0].count);
|
||||
u_free(ac->pending[0].samples);
|
||||
|
||||
/* shift remaining pending blocks down */
|
||||
if (ac->pending_count > 1)
|
||||
memmove(ac->pending, ac->pending + 1, (ac->pending_count - 1) * sizeof(*ac->pending));
|
||||
ac->pending_count--;
|
||||
}
|
||||
|
||||
/* Слить один готовый блок записи. История хранится по абсолютному номеру в кольце. */
|
||||
static int drain_block(struct audio_compressor* ac, size_t count) {
|
||||
if (reserve_output(ac, count) != 0) return -1;
|
||||
int16_t* samples = ac->pending + ac->pending_head * ac->block_samples;
|
||||
apply_gain(ac, samples, count, ac->blocks_seen - ac->pending_count);
|
||||
memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
|
||||
ac->output_size += count;
|
||||
ac->pending_head = (ac->pending_head + 1) % ac->pending_cap;
|
||||
--ac->pending_count;
|
||||
return 0;
|
||||
void audio_compressor_flush(struct audio_compressor* ac) {
|
||||
if (!ac) return;
|
||||
if (!ac->enabled) return;
|
||||
|
||||
if (ac->accum_count > 0) {
|
||||
float level = compute_block_level(ac->accumulator, ac->accum_count);
|
||||
add_block_level(ac, level);
|
||||
|
||||
ensure_pending_cap(ac);
|
||||
ac->pending[ac->pending_count].samples = ac->accumulator;
|
||||
ac->pending[ac->pending_count].count = ac->accum_count;
|
||||
ac->pending[ac->pending_count].level_index = ac->block_levels_count - 1;
|
||||
ac->pending_count++;
|
||||
|
||||
ac->accumulator = NULL;
|
||||
ac->accum_count = 0;
|
||||
}
|
||||
|
||||
if (ac->pending_count > 0)
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "audio_compressor_flush: %zu pending blocks (gainSmoothed=%.1fdB)",
|
||||
ac->pending_count, 20.0f * log10f(ac->gain_smoothed > 0.0001f ? ac->gain_smoothed : 0.0001f));
|
||||
|
||||
while (ac->pending_count > 0)
|
||||
process_pending_block(ac);
|
||||
}
|
||||
|
||||
/* Ровно один кадр, без output-накопителя и выделений памяти. in/out могут совпадать. */
|
||||
int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count) {
|
||||
if (!ac || !ac->configured || ac->failed || !in || !out || count != (size_t)ac->block_samples ||
|
||||
ac->lookahead_blocks || ac->pending_count || ac->accum_count) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid streaming frame count=%zu", count);
|
||||
return -1;
|
||||
}
|
||||
memmove(out, in, count * sizeof(int16_t));
|
||||
if (ac->enabled) {
|
||||
size_t idx = ac->blocks_seen++;
|
||||
ac->block_levels[idx % ac->levels_cap] = compute_block_level(in, count);
|
||||
apply_gain(ac, out, count, idx);
|
||||
}
|
||||
return 0;
|
||||
const int16_t* audio_compressor_output(const struct audio_compressor* ac) {
|
||||
return ac ? ac->output : NULL;
|
||||
}
|
||||
|
||||
int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count) {
|
||||
if (!ac || !ac->configured || ac->failed || (!samples && count)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid recording input"); return -1;
|
||||
}
|
||||
if (!ac->enabled) {
|
||||
if (reserve_output(ac, count) != 0) return -1;
|
||||
if (count) memcpy(ac->output + ac->output_size, samples, count * sizeof(int16_t));
|
||||
ac->output_size += count;
|
||||
return 0;
|
||||
}
|
||||
while (count) {
|
||||
size_t take = (size_t)ac->block_samples - ac->accum_count;
|
||||
if (take > count) take = count;
|
||||
int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples;
|
||||
memcpy(block + ac->accum_count, samples, take * sizeof(int16_t));
|
||||
samples += take; count -= take; ac->accum_count += take;
|
||||
if (ac->accum_count == (size_t)ac->block_samples) {
|
||||
ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, ac->accum_count);
|
||||
ac->accum_count = 0; ++ac->pending_count;
|
||||
if (ac->pending_count > (size_t)ac->lookahead_blocks && drain_block(ac, ac->block_samples) != 0) return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int audio_compressor_flush(struct audio_compressor* ac) {
|
||||
if (!ac || !ac->configured || ac->failed) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "compressor: invalid flush"); return -1; }
|
||||
if (!ac->enabled) return 0;
|
||||
size_t tail = ac->accum_count;
|
||||
if (tail) {
|
||||
int16_t* block = ac->pending + ((ac->pending_head + ac->pending_count) % ac->pending_cap) * ac->block_samples;
|
||||
ac->block_levels[ac->blocks_seen++ % ac->levels_cap] = compute_block_level(block, tail);
|
||||
ac->accum_count = 0; ++ac->pending_count;
|
||||
}
|
||||
while (ac->pending_count) {
|
||||
size_t count = tail && ac->pending_count == 1 ? tail : (size_t)ac->block_samples;
|
||||
if (drain_block(ac, count) != 0) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int16_t* audio_compressor_output(const struct audio_compressor* ac) { return ac ? ac->output : NULL; }
|
||||
size_t audio_compressor_output_size(const struct audio_compressor* ac) { return ac ? ac->output_size : 0; }
|
||||
void audio_compressor_clear_output(struct audio_compressor* ac) { if (ac) ac->output_size = 0; }
|
||||
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac) { return ac ? ac->clip_count : 0; }
|
||||
float audio_compressor_gain_db(const struct audio_compressor* ac) {
|
||||
return ac && ac->configured && ac->enabled ? 20.0f * log10f(ac->gain_smoothed) : 0.0f;
|
||||
size_t audio_compressor_output_size(const struct audio_compressor* ac) {
|
||||
return ac ? ac->output_size : 0;
|
||||
}
|
||||
+4
-26
@@ -1,9 +1,3 @@
|
||||
/* audio_compressor — адаптивное усиление int16 PCM (моно/стерео) с общей огибающей каналов.
|
||||
* create → configure → обработка → destroy; объект использует один владелец, блокировок нет.
|
||||
* Запись: push принимает произвольные порции, flush завершает хвост и lookahead; output принадлежит объекту.
|
||||
* Поток: process_frame обрабатывает ровно sample_rate*block_duration_ms/1000*channels отсчётов,
|
||||
* требует нулевого lookahead и пустого входного накопителя; in/out могут совпадать.
|
||||
* count/output_size — interleaved отсчёты, не байты и не кадры на канал. Выключение даёт исходный PCM. */
|
||||
#ifndef AUDIO_COMPRESSOR_H
|
||||
#define AUDIO_COMPRESSOR_H
|
||||
|
||||
@@ -24,40 +18,24 @@ typedef struct {
|
||||
int lookahead_ms;
|
||||
float max_gain_db;
|
||||
float rise_rate_per_sec;
|
||||
float release_rate_per_sec; /* скорость плавного снижения gain (0 = авто: rise*5) */
|
||||
float target_level; /* Линейная амплитуда огибающей, не дБ; 0 задаёт default 0.25. */
|
||||
float target_level;
|
||||
} audio_compressor_config_t;
|
||||
|
||||
struct audio_compressor* audio_compressor_create(void);
|
||||
/* Новый пустой compressor с теми же настройками; позволяет передать запись worker без копии PCM. */
|
||||
struct audio_compressor* audio_compressor_clone_config(const struct audio_compressor* source);
|
||||
void audio_compressor_destroy(struct audio_compressor* ac);
|
||||
|
||||
int audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg);
|
||||
/* Потоковый кадр без lookahead: результат немедленный, память не выделяется; 0 / -1. */
|
||||
int audio_compressor_process_frame(struct audio_compressor* ac, const int16_t* in, int16_t* out, size_t count);
|
||||
void audio_compressor_configure(struct audio_compressor* ac, const audio_compressor_config_t* cfg);
|
||||
void audio_compressor_reset(struct audio_compressor* ac);
|
||||
|
||||
void audio_compressor_set_enabled(struct audio_compressor* ac, int enabled);
|
||||
int audio_compressor_is_enabled(const struct audio_compressor* ac);
|
||||
|
||||
/* push копирует вход; push/flush возвращают 0 / -1. Ошибка накопления требует reset перед повтором. */
|
||||
int audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count);
|
||||
int audio_compressor_flush(struct audio_compressor* ac);
|
||||
void audio_compressor_push(struct audio_compressor* ac, const int16_t* samples, size_t count);
|
||||
void audio_compressor_flush(struct audio_compressor* ac);
|
||||
|
||||
/* Указатель действителен до изменения/освобождения выходного буфера; вызывающий его не освобождает. */
|
||||
const int16_t* audio_compressor_output(const struct audio_compressor* ac);
|
||||
size_t audio_compressor_output_size(const struct audio_compressor* ac);
|
||||
|
||||
/* Сбросить накопленный выход (output_size=0) после слива — для потокового режима.
|
||||
* НЕ трогает gain_smoothed (адаптивная громкость сохраняется). */
|
||||
void audio_compressor_clear_output(struct audio_compressor* ac);
|
||||
|
||||
/* Число сэмплов, обрезанных по потолку int16 (клиппинг) с момента create/reset. */
|
||||
uint32_t audio_compressor_clip_count(const struct audio_compressor* ac);
|
||||
/* Текущее фактическое усиление в дБ; выключенный compressor возвращает 0. */
|
||||
float audio_compressor_gain_db(const struct audio_compressor* ac);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -129,15 +129,6 @@ static const struct {
|
||||
{"chat_sync", DEBUG_CATEGORY_CHAT_SYNC},
|
||||
{"member_sync", DEBUG_CATEGORY_MEMBER_SYNC},
|
||||
{"proxy", DEBUG_CATEGORY_PROXY},
|
||||
{"video", DEBUG_CATEGORY_VIDEO},
|
||||
{"reality", DEBUG_CATEGORY_REALITY},
|
||||
{"dm", DEBUG_CATEGORY_DM},
|
||||
{"call", DEBUG_CATEGORY_CALL},
|
||||
{"radio", DEBUG_CATEGORY_RADIO},
|
||||
{"vad", DEBUG_CATEGORY_VAD},
|
||||
{"aec", DEBUG_CATEGORY_AEC},
|
||||
{"stream", DEBUG_CATEGORY_STREAM},
|
||||
{"discovery", DEBUG_CATEGORY_DISCOVERY},
|
||||
{"all", DEBUG_CATEGORY_ALL},
|
||||
{NULL, DEBUG_CATEGORY_NONE}
|
||||
};
|
||||
|
||||
+8
-18
@@ -41,7 +41,7 @@ typedef int debug_category_t;
|
||||
#define DEBUG_CATEGORY_NONE 0
|
||||
#define DEBUG_CATEGORY_SYS 1 // System: u_async, ll_queue, memory, timing
|
||||
// #define DEBUG_CATEGORY_LL_QUEUE 2 (merged into SYS)
|
||||
#define DEBUG_CATEGORY_CONNECTION 3 // Connection: connect/reconnect/disconnect events only
|
||||
#define DEBUG_CATEGORY_CONNECTION 3 // Connection: links, keepalive, status events
|
||||
#define DEBUG_CATEGORY_ETCP 4 // ETCP: protocol, BBR, normalizer
|
||||
#define DEBUG_CATEGORY_CRYPTO 5 // crypto operations
|
||||
// #define DEBUG_CATEGORY_MEMORY 6 (merged into SYS)
|
||||
@@ -67,22 +67,13 @@ typedef int debug_category_t;
|
||||
#define DEBUG_CATEGORY_CHAT_SYNC 26 // Message DB sync (INIT_SYNC, PUSH, chain hash, recalc)
|
||||
#define DEBUG_CATEGORY_MEMBER_SYNC 27 // Member + address + merkle sync
|
||||
#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_DM 31 // DM — direct p2p chat between two users
|
||||
#define DEBUG_CATEGORY_CALL 32 // Call — P2P audio call (signaling + media)
|
||||
#define DEBUG_CATEGORY_RADIO 33 // Radio — PTT walkie-talkie multicast (voice)
|
||||
#define DEBUG_CATEGORY_VAD 34 // VAD — Silero voice activity detector (ONNX Runtime)
|
||||
#define DEBUG_CATEGORY_AEC 35 // AEC — acoustic echo cancellation (SpeexDSP)
|
||||
#define DEBUG_CATEGORY_STREAM 36 // Подписанные потоки, FEC, буферы и выбор поставщиков
|
||||
#define DEBUG_CATEGORY_DISCOVERY 37 // LAN UDP: discovery and delivery
|
||||
#define DEBUG_CATEGORY_COUNT 38 // Total number of categories
|
||||
#define DEBUG_CATEGORY_COUNT 29 // Total number of categories
|
||||
#define DEBUG_CATEGORY_ALL (-1) // special value for all categories
|
||||
|
||||
/* Debug configuration structure */
|
||||
typedef struct {
|
||||
debug_level_t level; // Global debug level (default: ERROR)
|
||||
debug_level_t category_levels[DEBUG_CATEGORY_COUNT]; // Уровень категории: NONE наследует общий, DISABLED выключает
|
||||
debug_level_t category_levels[DEBUG_CATEGORY_COUNT]; // Per-category levels (0 = disabled)
|
||||
int timestamp_enabled; // Include timestamps in output
|
||||
int function_name_enabled; // Include function names
|
||||
int file_line_enabled; // Include file:line info
|
||||
@@ -101,19 +92,18 @@ extern debug_config_t g_debug_config;
|
||||
void debug_config_init(void);
|
||||
|
||||
/* Set debug level */
|
||||
// Общий уровень применяется к категориям с уровнем NONE.
|
||||
// итоговый level = max (global level - здесь задается, category level)
|
||||
void debug_set_level(debug_level_t level);
|
||||
|
||||
/* Уровень категории заменяет общий; NONE наследует его, DISABLED подавляет вывод. */
|
||||
/* Set debug level for specific category (0 = disabled, otherwise uses that level) */
|
||||
void debug_set_category_level(debug_category_t category, debug_level_t level);
|
||||
|
||||
/* enable фиксирует текущий общий уровень; disable возвращает к наследованию (NONE).
|
||||
* Для полного выключения использовать debug_set_category_level(cat, DEBUG_LEVEL_DISABLED). */
|
||||
/* Enable/disable specific categories */
|
||||
void debug_enable_category(debug_category_t category);
|
||||
void debug_disable_category(debug_category_t category);
|
||||
void debug_set_categories(debug_category_t categories);
|
||||
|
||||
/* Установка уровня одной категории (аргумент categories — индекс, не битовая маска). */
|
||||
/* Set masks directly */
|
||||
void debug_set_masks(debug_category_t categories, debug_level_t level);
|
||||
|
||||
/* Configure output options */
|
||||
@@ -129,7 +119,7 @@ int debug_reopen_log(void);
|
||||
void debug_enable_console(int enable);
|
||||
|
||||
|
||||
// IP/sockaddr в строку: результат возвращается по значению, освобождать не требуется.
|
||||
// IP address to string (static buffer, single-threaded)
|
||||
typedef struct {
|
||||
char str[54]; // INET6_ADDRSTRLEN(45) + 6chars (:port) + \0
|
||||
} ip_str_t;
|
||||
|
||||
@@ -3403,33 +3403,33 @@ size_t dns_txt_print(void *dst_, size_t lim, struct dns_txt *txt) {
|
||||
} /* dns_txt_print() */
|
||||
|
||||
|
||||
typedef int (*dns_parse_fn)(union dns_any *, struct dns_rr *, struct dns_packet *);
|
||||
typedef int (*dns_push_fn)(struct dns_packet *, union dns_any *);
|
||||
typedef int (*dns_cmp_fn)(const union dns_any *, const union dns_any *);
|
||||
typedef size_t (*dns_print_fn)(void *, size_t, union dns_any *);
|
||||
typedef size_t (*dns_cname_fn)(void *, size_t, union dns_any *);
|
||||
typedef int (*dns_rrtype_parse_fn)(union dns_any *, struct dns_rr *, struct dns_packet *);
|
||||
typedef int (*dns_rrtype_push_fn)(struct dns_packet *, union dns_any *);
|
||||
typedef int (*dns_rrtype_cmp_fn)(const union dns_any *, const union dns_any *);
|
||||
typedef size_t (*dns_rrtype_print_fn)(void *, size_t, union dns_any *);
|
||||
typedef size_t (*dns_rrtype_cname_fn)(void *, size_t, union dns_any *);
|
||||
|
||||
static const struct {
|
||||
enum dns_type type;
|
||||
const char *name;
|
||||
dns_parse_fn parse;
|
||||
dns_push_fn push;
|
||||
dns_cmp_fn cmp;
|
||||
dns_print_fn print;
|
||||
dns_cname_fn cname;
|
||||
dns_rrtype_parse_fn parse;
|
||||
dns_rrtype_push_fn push;
|
||||
dns_rrtype_cmp_fn cmp;
|
||||
dns_rrtype_print_fn print;
|
||||
dns_rrtype_cname_fn cname;
|
||||
} dns_rrtypes[] = {
|
||||
{ DNS_T_A, "A", (dns_parse_fn)&dns_a_parse, (dns_push_fn)&dns_a_push, (dns_cmp_fn)&dns_a_cmp, (dns_print_fn)&dns_a_print, 0 },
|
||||
{ DNS_T_AAAA, "AAAA", (dns_parse_fn)&dns_aaaa_parse, (dns_push_fn)&dns_aaaa_push, (dns_cmp_fn)&dns_aaaa_cmp, (dns_print_fn)&dns_aaaa_print, 0 },
|
||||
{ DNS_T_MX, "MX", (dns_parse_fn)&dns_mx_parse, (dns_push_fn)&dns_mx_push, (dns_cmp_fn)&dns_mx_cmp, (dns_print_fn)&dns_mx_print, (dns_cname_fn)&dns_mx_cname },
|
||||
{ DNS_T_NS, "NS", (dns_parse_fn)&dns_ns_parse, (dns_push_fn)&dns_ns_push, (dns_cmp_fn)&dns_ns_cmp, (dns_print_fn)&dns_ns_print, (dns_cname_fn)&dns_ns_cname },
|
||||
{ DNS_T_CNAME, "CNAME", (dns_parse_fn)&dns_cname_parse, (dns_push_fn)&dns_cname_push, (dns_cmp_fn)&dns_cname_cmp, (dns_print_fn)&dns_cname_print, (dns_cname_fn)&dns_cname_cname },
|
||||
{ DNS_T_SOA, "SOA", (dns_parse_fn)&dns_soa_parse, (dns_push_fn)&dns_soa_push, (dns_cmp_fn)&dns_soa_cmp, (dns_print_fn)&dns_soa_print, 0 },
|
||||
{ DNS_T_SRV, "SRV", (dns_parse_fn)&dns_srv_parse, (dns_push_fn)&dns_srv_push, (dns_cmp_fn)&dns_srv_cmp, (dns_print_fn)&dns_srv_print, (dns_cname_fn)&dns_srv_cname },
|
||||
{ DNS_T_OPT, "OPT", (dns_parse_fn)&dns_opt_parse, (dns_push_fn)&dns_opt_push, (dns_cmp_fn)&dns_opt_cmp, (dns_print_fn)&dns_opt_print, 0 },
|
||||
{ DNS_T_PTR, "PTR", (dns_parse_fn)&dns_ptr_parse, (dns_push_fn)&dns_ptr_push, (dns_cmp_fn)&dns_ptr_cmp, (dns_print_fn)&dns_ptr_print, (dns_cname_fn)&dns_ptr_cname },
|
||||
{ DNS_T_TXT, "TXT", (dns_parse_fn)&dns_txt_parse, (dns_push_fn)&dns_txt_push, (dns_cmp_fn)&dns_txt_cmp, (dns_print_fn)&dns_txt_print, 0 },
|
||||
{ DNS_T_SPF, "SPF", (dns_parse_fn)&dns_txt_parse, (dns_push_fn)&dns_txt_push, (dns_cmp_fn)&dns_txt_cmp, (dns_print_fn)&dns_txt_print, 0 },
|
||||
{ DNS_T_SSHFP, "SSHFP", (dns_parse_fn)&dns_sshfp_parse, (dns_push_fn)&dns_sshfp_push, (dns_cmp_fn)&dns_sshfp_cmp, (dns_print_fn)&dns_sshfp_print, 0 },
|
||||
{ DNS_T_A, "A", (dns_rrtype_parse_fn)&dns_a_parse, (dns_rrtype_push_fn)&dns_a_push, (dns_rrtype_cmp_fn)&dns_a_cmp, (dns_rrtype_print_fn)&dns_a_print, 0 },
|
||||
{ DNS_T_AAAA, "AAAA", (dns_rrtype_parse_fn)&dns_aaaa_parse, (dns_rrtype_push_fn)&dns_aaaa_push, (dns_rrtype_cmp_fn)&dns_aaaa_cmp, (dns_rrtype_print_fn)&dns_aaaa_print, 0 },
|
||||
{ DNS_T_MX, "MX", (dns_rrtype_parse_fn)&dns_mx_parse, (dns_rrtype_push_fn)&dns_mx_push, (dns_rrtype_cmp_fn)&dns_mx_cmp, (dns_rrtype_print_fn)&dns_mx_print, (dns_rrtype_cname_fn)&dns_mx_cname },
|
||||
{ DNS_T_NS, "NS", (dns_rrtype_parse_fn)&dns_ns_parse, (dns_rrtype_push_fn)&dns_ns_push, (dns_rrtype_cmp_fn)&dns_ns_cmp, (dns_rrtype_print_fn)&dns_ns_print, (dns_rrtype_cname_fn)&dns_ns_cname },
|
||||
{ DNS_T_CNAME, "CNAME", (dns_rrtype_parse_fn)&dns_cname_parse, (dns_rrtype_push_fn)&dns_cname_push, (dns_rrtype_cmp_fn)&dns_cname_cmp, (dns_rrtype_print_fn)&dns_cname_print, (dns_rrtype_cname_fn)&dns_cname_cname },
|
||||
{ DNS_T_SOA, "SOA", (dns_rrtype_parse_fn)&dns_soa_parse, (dns_rrtype_push_fn)&dns_soa_push, (dns_rrtype_cmp_fn)&dns_soa_cmp, (dns_rrtype_print_fn)&dns_soa_print, 0 },
|
||||
{ DNS_T_SRV, "SRV", (dns_rrtype_parse_fn)&dns_srv_parse, (dns_rrtype_push_fn)&dns_srv_push, (dns_rrtype_cmp_fn)&dns_srv_cmp, (dns_rrtype_print_fn)&dns_srv_print, (dns_rrtype_cname_fn)&dns_srv_cname },
|
||||
{ DNS_T_OPT, "OPT", (dns_rrtype_parse_fn)&dns_opt_parse, (dns_rrtype_push_fn)&dns_opt_push, (dns_rrtype_cmp_fn)&dns_opt_cmp, (dns_rrtype_print_fn)&dns_opt_print, 0 },
|
||||
{ DNS_T_PTR, "PTR", (dns_rrtype_parse_fn)&dns_ptr_parse, (dns_rrtype_push_fn)&dns_ptr_push, (dns_rrtype_cmp_fn)&dns_ptr_cmp, (dns_rrtype_print_fn)&dns_ptr_print, (dns_rrtype_cname_fn)&dns_ptr_cname },
|
||||
{ DNS_T_TXT, "TXT", (dns_rrtype_parse_fn)&dns_txt_parse, (dns_rrtype_push_fn)&dns_txt_push, (dns_rrtype_cmp_fn)&dns_txt_cmp, (dns_rrtype_print_fn)&dns_txt_print, 0 },
|
||||
{ DNS_T_SPF, "SPF", (dns_rrtype_parse_fn)&dns_txt_parse, (dns_rrtype_push_fn)&dns_txt_push, (dns_rrtype_cmp_fn)&dns_txt_cmp, (dns_rrtype_print_fn)&dns_txt_print, 0 },
|
||||
{ DNS_T_SSHFP, "SSHFP", (dns_rrtype_parse_fn)&dns_sshfp_parse, (dns_rrtype_push_fn)&dns_sshfp_push, (dns_rrtype_cmp_fn)&dns_sshfp_cmp, (dns_rrtype_print_fn)&dns_sshfp_print, 0 },
|
||||
}; /* dns_rrtypes[] */
|
||||
|
||||
|
||||
|
||||
+7
-7
@@ -23,24 +23,24 @@ int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
|
||||
struct sockaddr_storage *result)
|
||||
{
|
||||
if (!local_addr || !remote_addr || !result) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: NULL argument");
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: NULL argument");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int family = local_addr->ss_family;
|
||||
if (family != AF_INET && family != AF_INET6) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: unsupported family %d", family);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: unsupported family %d", family);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (remote_addr->ss_family != family) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: family mismatch");
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: family mismatch");
|
||||
return -1;
|
||||
}
|
||||
|
||||
socket_t sock = socket_create_udp(family);
|
||||
if (sock == SOCKET_INVALID) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: socket_create_udp failed");
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: socket_create_udp failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
|
||||
socklen_t bind_len = (family == AF_INET) ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
|
||||
if (bind(sock, (struct sockaddr *)&bind_addr, bind_len) != 0) {
|
||||
int err = socket_get_error();
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: bind failed: %s", socket_strerror(err));
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: bind failed: %s", socket_strerror(err));
|
||||
socket_close_wrapper(sock);
|
||||
return -1;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
|
||||
socklen_t remote_len = (family == AF_INET) ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
|
||||
if (connect(sock, (struct sockaddr *)remote_addr, remote_len) != 0) {
|
||||
int err = socket_get_error();
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: connect failed: %s", socket_strerror(err));
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: connect failed: %s", socket_strerror(err));
|
||||
socket_close_wrapper(sock);
|
||||
return -1;
|
||||
}
|
||||
@@ -75,7 +75,7 @@ int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
|
||||
socklen_t local_len = sizeof(local);
|
||||
if (getsockname(sock, (struct sockaddr *)&local, &local_len) != 0) {
|
||||
int err = socket_get_error();
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "get_outgoing_local_ip: getsockname failed: %s", socket_strerror(err));
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "get_outgoing_local_ip: getsockname failed: %s", socket_strerror(err));
|
||||
socket_close_wrapper(sock);
|
||||
return -1;
|
||||
}
|
||||
|
||||
+10
-27
@@ -1,6 +1,4 @@
|
||||
#include "json_flat.h"
|
||||
#include "mem.h"
|
||||
#include "debug_config.h"
|
||||
#include <string.h>
|
||||
|
||||
#define SKIP_WS(p, end) while ((p) < (end) && (*(p) == ' ' || *(p) == '\t' || *(p) == '\n' || *(p) == '\r')) (p)++
|
||||
@@ -21,8 +19,6 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
|
||||
case 'n': ch = '\n'; break;
|
||||
case 'r': ch = '\r'; break;
|
||||
case 't': ch = '\t'; break;
|
||||
case 'b': ch = '\b'; break;
|
||||
case 'f': ch = '\f'; break;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
@@ -37,22 +33,20 @@ static int json_read_string(const char** pp, const char* end, char* out, size_t
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int json_parse_pair(const char** pp, const char* end, char* scratch, size_t capacity,
|
||||
static int json_parse_pair(const char** pp, const char* end,
|
||||
json_flat_cb cb, void* arg) {
|
||||
const char* p = *pp;
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (*p == '}') { *pp = p; return 0; }
|
||||
|
||||
char* key = scratch;
|
||||
if (json_read_string(&p, end, key, capacity) != 0) return -1;
|
||||
size_t key_size = strlen(key) + 1;
|
||||
char* val = scratch + key_size;
|
||||
char key[128], val[256];
|
||||
if (json_read_string(&p, end, key, sizeof(key)) != 0) return -1;
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (*p != ':') return -1; p++;
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (json_read_string(&p, end, val, capacity - key_size) != 0) return -1;
|
||||
if (json_read_string(&p, end, val, sizeof(val)) != 0) return -1;
|
||||
|
||||
if (cb) { int rc = cb(key, val, arg); if (rc != 0) { *pp = p; return rc < 0 ? -1 : 1; } }
|
||||
if (cb && cb(key, val, arg) != 0) { *pp = p; return 1; }
|
||||
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (*p == ',') p++;
|
||||
@@ -67,23 +61,13 @@ int json_flat_parse(const char* json, json_flat_cb cb, void* arg) {
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (*p != '{') return -1; p++;
|
||||
|
||||
/* Decoded key + value + two NULs fit within the encoded object size. */
|
||||
size_t capacity = strlen(json) + 1;
|
||||
if (capacity > UINT32_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: input too large"); return -1; }
|
||||
char local[384];
|
||||
char* scratch = capacity <= sizeof(local) ? local : u_malloc(capacity);
|
||||
if (!scratch) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "json_flat: allocation failed bytes=%zu", capacity); return -1; }
|
||||
int result = 0;
|
||||
for (;;) {
|
||||
int rc = json_parse_pair(&p, end, scratch, capacity, cb, arg);
|
||||
if (rc < 0) { result = -1; break; }
|
||||
if (rc > 0) break;
|
||||
SKIP_WS(p, end);
|
||||
if (p >= end) { result = -1; break; }
|
||||
if (*p == '}') break;
|
||||
int rc = json_parse_pair(&p, end, cb, arg);
|
||||
if (rc < 0) return -1;
|
||||
if (rc > 0) return 0; /* callback stopped */
|
||||
SKIP_WS(p, end); CHECK(p, end);
|
||||
if (*p == '}') return 0;
|
||||
}
|
||||
if (scratch != local) u_free(scratch);
|
||||
return result;
|
||||
}
|
||||
|
||||
struct get_ctx { const char* key; char* out; size_t sz; int found; };
|
||||
@@ -97,7 +81,6 @@ static int get_cb(const char* k, const char* v, void* arg) {
|
||||
}
|
||||
|
||||
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz) {
|
||||
if (!key || (value_out && !value_sz)) return -1;
|
||||
struct get_ctx g = { key, value_out, value_sz, 0 };
|
||||
if (json_flat_parse(json, get_cb, &g) != 0) return -1;
|
||||
return g.found ? 0 : -1;
|
||||
|
||||
+1
-5
@@ -2,10 +2,7 @@
|
||||
* @file json_flat.h
|
||||
* @brief Парсер плоского JSON без вложенности: {"key":"value",...}
|
||||
*
|
||||
* Только строковые значения и обычные escapes; \uXXXX не поддерживается.
|
||||
* Callback получает полные key/value, действительные только до его возврата.
|
||||
* parse возвращает 0 при успехе/остановке callback, -1 при ошибке; остановка не проверяет остаток ввода.
|
||||
* get ищет первый ключ и копирует значение с NUL, при малом буфере обрезает; 0 найден, -1 ошибка/нет ключа.
|
||||
* Только строковые значения. Без malloc, без зависимостей.
|
||||
*/
|
||||
|
||||
#ifndef JSON_FLAT_H
|
||||
@@ -18,7 +15,6 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
typedef int (*json_flat_cb)(const char* key, const char* value, void* arg);
|
||||
/* Callback: 0 = continue, >0 = stop successfully, <0 = error. */
|
||||
|
||||
int json_flat_parse(const char* json, json_flat_cb cb, void* arg);
|
||||
int json_flat_get(const char* json, const char* key, char* value_out, size_t value_sz);
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
Copyright 2001-2023 Xiph.Org, Skype Limited, Octasic,
|
||||
Jean-Marc Valin, Timothy B. Terriberry,
|
||||
CSIRO, Gregory Maxwell, Mark Borgerding,
|
||||
Erik de Castro Lopo, Mozilla, Amazon
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name of Internet Society, IETF or IETF Trust, nor the
|
||||
names of specific contributors, may be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
|
||||
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Opus is subject to the royalty-free patent licenses which are
|
||||
specified at:
|
||||
|
||||
Xiph.Org Foundation:
|
||||
https://datatracker.ietf.org/ipr/1524/
|
||||
|
||||
Microsoft Corporation:
|
||||
https://datatracker.ietf.org/ipr/1914/
|
||||
|
||||
Broadcom Corporation:
|
||||
https://datatracker.ietf.org/ipr/1526/
|
||||
@@ -1,7 +1,5 @@
|
||||
noinst_LIBRARIES = libopus_internal.a
|
||||
|
||||
EXTRA_DIST = COPYING
|
||||
|
||||
# Override global CFLAGS to disable ASAN (makes .a incompatible with PIE linking)
|
||||
CFLAGS = -g -Og -fno-omit-frame-pointer -fno-pie -fPIC
|
||||
|
||||
|
||||
+40
-125
@@ -52,23 +52,15 @@ static void on_get_trampoline(void* arg);
|
||||
static void empty_trampoline(void* arg) {
|
||||
struct ll_queue* q = (struct ll_queue*)arg;
|
||||
q->empty_call_soon_id = NULL;
|
||||
if (q->count != 0) return;
|
||||
queue_callback_fn cb = q->empty_callback;
|
||||
void* cb_arg = q->empty_callback_arg;
|
||||
q->empty_callback = NULL;
|
||||
q->empty_callback_arg = NULL;
|
||||
if (cb) cb(q, cb_arg);
|
||||
if (q->count == 0 && cb) cb(q, cb_arg);
|
||||
}
|
||||
|
||||
static void on_get_trampoline(void* arg) {
|
||||
struct ll_queue* q = (struct ll_queue*)arg;
|
||||
q->on_get_call_soon_id = NULL;
|
||||
if (!q->on_get_pending || !q->on_get) return;
|
||||
q->on_get_pending--;
|
||||
if (q->on_get_pending) {
|
||||
q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline);
|
||||
if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending);
|
||||
}
|
||||
if (q->on_get) q->on_get(q, q->on_get_arg);
|
||||
}
|
||||
|
||||
@@ -138,7 +130,6 @@ void queue_free(struct ll_queue* q) {
|
||||
// ВАЖНО: Не освобождаем элементы в очереди - они должны быть извлечены отдельно
|
||||
// Это упрощает архитектуру и предотвращает double-u_free
|
||||
|
||||
while (q->deferred_waiters) queue_waiter_cancel(q, q->deferred_waiters);
|
||||
// Освободить список waiters (сбросить handle->internal, отменить отложенные вызовы)
|
||||
struct queue_waiter* w = q->waiter_head;
|
||||
while (w) {
|
||||
@@ -168,10 +159,6 @@ void queue_free(struct ll_queue* q) {
|
||||
q->empty_callback_arg = NULL;
|
||||
q->on_get = NULL;
|
||||
q->on_get_arg = NULL;
|
||||
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
|
||||
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
|
||||
if (q->head) DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] freeing nonempty queue: count=%d; entries remain caller-owned", q->name, q->count);
|
||||
for (struct ll_entry* e = q->head; e; e = e->next) e->owner = NULL;
|
||||
|
||||
u_free(q);
|
||||
}
|
||||
@@ -207,19 +194,6 @@ void queue_resume_callback(struct ll_queue* q) {
|
||||
}
|
||||
}
|
||||
|
||||
// Общий запуск коллбэка автозабора: immediate (default) или deferred через uasync_call_soon.
|
||||
// Deferred-режим очищает стек производителя перед обработкой первого элемента.
|
||||
static void queue_trigger_callback(struct ll_queue* q) {
|
||||
if (!q->head || q->callback_suspended || !q->callback) return;
|
||||
|
||||
if (q->callback_defer) {
|
||||
if (!q->resume_timeout_id)
|
||||
q->resume_timeout_id = uasync_call_soon(q->ua, q, queue_resume_timeout_cb);
|
||||
} else {
|
||||
q->callback(q, q->callback_arg);
|
||||
}
|
||||
}
|
||||
|
||||
void queue_set_size_limit(struct ll_queue* q, int lim) {
|
||||
if (!q) return;
|
||||
q->size_limit = lim;
|
||||
@@ -329,42 +303,22 @@ static uint32_t make_hash(const void* data, uint16_t len) {// алгоритм F
|
||||
return hash;
|
||||
}
|
||||
|
||||
static void unlink_deferred_waiter(struct ll_queue* q, struct queue_waiter_handle* h) {
|
||||
struct queue_waiter_handle** p = &q->deferred_waiters;
|
||||
while (*p && *p != h) p = &(*p)->defer_next;
|
||||
if (*p) *p = h->defer_next;
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter not found: handle=%p", q->name, h);
|
||||
h->defer_next = NULL; h->defer_q = NULL;
|
||||
}
|
||||
|
||||
static void waiter_defer_cb(void* arg) {
|
||||
struct queue_waiter_handle* h = (struct queue_waiter_handle*)arg;
|
||||
struct ll_queue* q = h->defer_q;
|
||||
queue_threshold_callback_fn cb = h->defer_cb;
|
||||
void* cb_arg = h->defer_arg;
|
||||
h->call_soon_id = NULL;
|
||||
unlink_deferred_waiter(q, h);
|
||||
h->defer_cb = NULL; h->defer_arg = NULL;
|
||||
if (cb) cb(q, cb_arg);
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter has no callback", q->name);
|
||||
if (h->defer_cb) h->defer_cb(h->defer_q, h->defer_arg);
|
||||
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "waiter_defer_cb: NULL callback (stale?) q=%p", (void*)h->defer_q);
|
||||
}
|
||||
|
||||
static inline int queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg,
|
||||
static inline void queue_waiter_call(struct ll_queue* q, queue_threshold_callback_fn cb, void* arg,
|
||||
struct queue_waiter_handle* h) {
|
||||
if (q->waiter_defer) {
|
||||
if (h->call_soon_id) queue_waiter_cancel(h->defer_q, h);
|
||||
if (h->call_soon_id) { DEBUG_WARN(DEBUG_CATEGORY_SYS, "queue_waiter_call: overwriting existing call_soon_id=%p", h->call_soon_id); uasync_call_soon_cancel(q->ua, h->call_soon_id); }
|
||||
h->defer_q = q; h->defer_cb = cb; h->defer_arg = arg;
|
||||
h->call_soon_id = uasync_call_soon(q->ua, h, waiter_defer_cb);
|
||||
if (h->call_soon_id) { h->defer_next = q->deferred_waiters; q->deferred_waiters = h; }
|
||||
else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] deferred waiter scheduling failed: handle=%p", q->name, h);
|
||||
h->defer_q = NULL; h->defer_cb = NULL; h->defer_arg = NULL;
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
cb(q, arg);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Проверить и запустить первый ожидающий коллбэк (FIFO / round-robin)
|
||||
@@ -384,18 +338,13 @@ static void check_waiters(struct ll_queue* q) {
|
||||
|
||||
queue_threshold_callback_fn cb = waiter->callback;
|
||||
void* arg = waiter->callback_arg;
|
||||
u_free(waiter);
|
||||
|
||||
if (cb) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "check_waiters: waking head waiter, count=%d<=%d, bytes=%zu<=%zu",
|
||||
q->count, q->threshold_max_packets, q->total_bytes, q->threshold_max_bytes);
|
||||
if (queue_waiter_call(q, cb, arg, h) < 0) {
|
||||
// Отказ планирования не теряет ожидание: следующий get/remove повторит попытку.
|
||||
waiter->next = q->waiter_head; q->waiter_head = waiter;
|
||||
if (!q->waiter_tail) q->waiter_tail = waiter;
|
||||
h->internal = waiter;
|
||||
return;
|
||||
}
|
||||
queue_waiter_call(q, cb, arg, h);
|
||||
}
|
||||
u_free(waiter);
|
||||
}
|
||||
|
||||
int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
|
||||
@@ -404,7 +353,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
|
||||
queue_check_thread(q);
|
||||
#endif
|
||||
|
||||
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put detected! entry=%p prev=%p next=%p len=%u",
|
||||
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next, entry->len);
|
||||
abort();
|
||||
@@ -418,7 +367,6 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
|
||||
}
|
||||
|
||||
if (q->size_limit >= 0 && q->count >= q->size_limit) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
|
||||
queue_dgram_free(entry);
|
||||
queue_entry_free(entry);
|
||||
return -1;
|
||||
@@ -430,18 +378,19 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry) {
|
||||
if (q->tail) q->tail->next = entry; else q->head = entry;
|
||||
q->tail = entry;
|
||||
|
||||
entry->owner = q;
|
||||
q->count++;
|
||||
entry->int_len = entry->len;
|
||||
q->total_bytes += entry->int_len;
|
||||
|
||||
// НЕ вызываем add_to_hash
|
||||
|
||||
if (q->count == 1 && !q->callback_suspended && q->callback) {
|
||||
q->callback(q, q->callback_arg);
|
||||
}
|
||||
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);
|
||||
#endif
|
||||
// Пользовательский callback может освободить очередь.
|
||||
if (q->count == 1) queue_trigger_callback(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -451,7 +400,7 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
|
||||
queue_check_thread(q);
|
||||
#endif
|
||||
|
||||
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_with_index detected! entry=%p prev=%p next=%p",
|
||||
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
|
||||
abort();
|
||||
@@ -467,7 +416,6 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
|
||||
entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size);
|
||||
|
||||
if (q->size_limit >= 0 && q->count >= q->size_limit) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
|
||||
queue_dgram_free(entry);
|
||||
queue_entry_free(entry);
|
||||
return -1;
|
||||
@@ -479,18 +427,19 @@ int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry) {
|
||||
if (q->tail) q->tail->next = entry; else q->head = entry;
|
||||
q->tail = entry;
|
||||
|
||||
entry->owner = q;
|
||||
q->count++;
|
||||
entry->int_len = entry->len;
|
||||
q->total_bytes += entry->int_len;
|
||||
|
||||
add_to_hash(q, entry);
|
||||
|
||||
if (q->count == 1 && !q->callback_suspended && q->callback) {
|
||||
q->callback(q, q->callback_arg);
|
||||
}
|
||||
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);
|
||||
#endif
|
||||
// Пользовательский callback может освободить очередь.
|
||||
if (q->count == 1) queue_trigger_callback(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -500,7 +449,7 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) {
|
||||
queue_check_thread(q);
|
||||
#endif
|
||||
|
||||
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first detected! entry=%p prev=%p next=%p",
|
||||
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
|
||||
abort();
|
||||
@@ -513,25 +462,22 @@ int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (q->size_limit >= 0 && q->count >= q->size_limit) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
|
||||
queue_dgram_free(entry); queue_entry_free(entry); return -1;
|
||||
}
|
||||
entry->next = q->head;
|
||||
entry->prev = NULL;
|
||||
if (q->head) q->head->prev = entry; else q->tail = entry;
|
||||
q->head = entry;
|
||||
|
||||
entry->owner = q;
|
||||
q->count++;
|
||||
entry->int_len = entry->len;
|
||||
q->total_bytes += entry->int_len;
|
||||
|
||||
if (q->count == 1 && !q->callback_suspended && q->callback) {
|
||||
q->callback(q, q->callback_arg);
|
||||
}
|
||||
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);
|
||||
#endif
|
||||
// Пользовательский callback может освободить очередь.
|
||||
if (q->count == 1) queue_trigger_callback(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -542,7 +488,7 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
|
||||
queue_check_thread(q);
|
||||
#endif
|
||||
|
||||
if (entry->owner || entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
if (entry->prev || entry->next || q->head == entry || q->tail == entry) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] double put_first_with_index detected! entry=%p prev=%p next=%p",
|
||||
q->name, (void*)entry, (void*)entry->prev, (void*)entry->next);
|
||||
abort();
|
||||
@@ -558,7 +504,6 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
|
||||
entry->index_hash = make_hash(entry->data + q->index_offset, q->index_size);
|
||||
|
||||
if (q->size_limit >= 0 && q->count >= q->size_limit) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] queue full: count=%d limit=%d", q->name, q->count, q->size_limit);
|
||||
queue_dgram_free(entry);
|
||||
queue_entry_free(entry);
|
||||
return -1;
|
||||
@@ -569,18 +514,19 @@ int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry)
|
||||
if (q->head) q->head->prev = entry; else q->tail = entry;
|
||||
q->head = entry;
|
||||
|
||||
entry->owner = q;
|
||||
q->count++;
|
||||
entry->int_len = entry->len;
|
||||
q->total_bytes += entry->int_len;
|
||||
|
||||
add_to_hash(q, entry);
|
||||
|
||||
if (q->count == 1 && !q->callback_suspended && q->callback) {
|
||||
q->callback(q, q->callback_arg);
|
||||
}
|
||||
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);
|
||||
#endif
|
||||
// Пользовательский callback может освободить очередь.
|
||||
if (q->count == 1) queue_trigger_callback(q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -603,7 +549,6 @@ struct ll_entry* queue_data_get(struct ll_queue* q) {
|
||||
|
||||
entry->next = NULL;
|
||||
entry->prev = NULL;
|
||||
entry->owner = NULL;
|
||||
|
||||
remove_from_hash(q, entry);
|
||||
|
||||
@@ -612,21 +557,18 @@ struct ll_entry* queue_data_get(struct ll_queue* q) {
|
||||
// Приостановить коллбэки для предотвращения рекурсии
|
||||
q->callback_suspended = 1;
|
||||
|
||||
// Проверить ожидающие коллбэки
|
||||
check_waiters(q);
|
||||
|
||||
if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id)
|
||||
q->empty_call_soon_id = uasync_call_soon(q->ua, q, (timeout_callback_t)empty_trampoline);
|
||||
|
||||
if (q->on_get) {
|
||||
q->on_get_pending++;
|
||||
if (!q->on_get_call_soon_id) {
|
||||
q->on_get_call_soon_id = uasync_call_soon(q->ua, q, on_get_trampoline);
|
||||
if (!q->on_get_call_soon_id) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "[%s] on_get scheduling failed: pending=%zu", q->name, q->on_get_pending);
|
||||
}
|
||||
}
|
||||
if (q->on_get)
|
||||
uasync_call_soon(q->ua, q, (timeout_callback_t)on_get_trampoline);
|
||||
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);// !!!! for debug
|
||||
#endif
|
||||
check_waiters(q);
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -705,11 +647,6 @@ void queue_set_waiter_defer(struct ll_queue* q, int enable) {
|
||||
q->waiter_defer = enable;
|
||||
}
|
||||
|
||||
void queue_set_callback_defer(struct ll_queue* q, int enable) {
|
||||
if (!q) return;
|
||||
q->callback_defer = enable;
|
||||
}
|
||||
|
||||
void queue_set_empty_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) {
|
||||
if (!q) return;
|
||||
void* old = q->empty_call_soon_id;
|
||||
@@ -725,24 +662,18 @@ void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg) {
|
||||
if (!q) return;
|
||||
q->on_get = cbk_fn;
|
||||
q->on_get_arg = arg;
|
||||
if (!cbk_fn) {
|
||||
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
|
||||
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
|
||||
queue_threshold_callback_fn callback, void* arg) {
|
||||
if (!q || !h || !callback) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "queue_waiter_wait: invalid arguments q=%p handle=%p callback=%p", q, h, (void*)callback);
|
||||
return -1;
|
||||
}
|
||||
if (!q || !h) return -1;
|
||||
|
||||
if (h->internal || h->call_soon_id) queue_waiter_cancel(h->call_soon_id ? h->defer_q : q, h);
|
||||
if (h->internal) queue_waiter_cancel(q, h);
|
||||
|
||||
if (q->count <= q->threshold_max_packets
|
||||
&& (q->threshold_max_bytes == 0 || q->total_bytes <= q->threshold_max_bytes)) {
|
||||
return queue_waiter_call(q, callback, arg, h) < 0 ? -1 : 1;
|
||||
queue_waiter_call(q, callback, arg, h);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct queue_waiter* waiter = u_malloc(sizeof(struct queue_waiter));
|
||||
@@ -763,18 +694,9 @@ int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
|
||||
}
|
||||
|
||||
void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h) {
|
||||
if (!q || !h) return;
|
||||
/* Отложенный вызов (waiter_defer) может висеть и при internal==NULL:
|
||||
* queue_waiter_wait по ветке «очередь свободна» планирует call_soon_id,
|
||||
* не регистрируя waiter. Ранний выход по !internal оставлял бы висячий
|
||||
* callback с arg=handle, который после teardown разыменовывал бы freed-память. */
|
||||
if (h->call_soon_id) {
|
||||
struct ll_queue* deferred_q = h->defer_q;
|
||||
uasync_call_soon_cancel(deferred_q->ua, h->call_soon_id); h->call_soon_id = NULL;
|
||||
unlink_deferred_waiter(deferred_q, h); h->defer_cb = NULL; h->defer_arg = NULL;
|
||||
}
|
||||
if (!h->internal) return;
|
||||
|
||||
if (!q || !h || !h->internal) return;
|
||||
if (h->call_soon_id) { uasync_call_soon_cancel(q->ua, h->call_soon_id); h->call_soon_id = NULL; }
|
||||
|
||||
struct queue_waiter* waiter = h->internal;
|
||||
struct queue_waiter* prev = NULL;
|
||||
struct queue_waiter* curr = q->waiter_head;
|
||||
@@ -832,10 +754,6 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q, const void* index_
|
||||
|
||||
int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
|
||||
if (!q || !entry) return -1;
|
||||
if (entry->owner != q) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] cannot remove foreign entry=%p owner=%p", q->name, entry, entry->owner);
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifdef QUEUE_THREAD_CHECK
|
||||
queue_check_thread(q);
|
||||
@@ -859,7 +777,6 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
|
||||
|
||||
entry->next = NULL;
|
||||
entry->prev = NULL;
|
||||
entry->owner = NULL;
|
||||
|
||||
remove_from_hash(q, entry);
|
||||
|
||||
@@ -867,8 +784,6 @@ int queue_remove_data(struct ll_queue* q, struct ll_entry* entry) {
|
||||
#ifdef QUEUE_DEBUG
|
||||
queue_check_consistency(q);// !!!! for debug
|
||||
#endif
|
||||
if (q->count == 0 && q->empty_callback && !q->empty_call_soon_id)
|
||||
q->empty_call_soon_id = uasync_call_soon(q->ua, q, empty_trampoline);
|
||||
check_waiters(q);
|
||||
return 0;
|
||||
}
|
||||
+5
-25
@@ -1,10 +1,6 @@
|
||||
#ifndef LL_QUEUE_H
|
||||
#define LL_QUEUE_H
|
||||
|
||||
/* Очередь одного uasync-потока; наличие pthread-заголовков не делает её межпоточной.
|
||||
* put может синхронно вызвать consumer/waiter. Держать владельцев живыми до завершения callbacks,
|
||||
* waiter handle обнулять целиком и отменять до освобождения; entry и dgram освобождаются отдельно. */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -65,7 +61,6 @@ struct queue_waiter_handle;
|
||||
*/
|
||||
struct ll_entry {
|
||||
char* name;
|
||||
struct ll_queue* owner; // Владелец элемента; NULL после извлечения
|
||||
struct ll_entry* next; // Следующий элемент в очереди
|
||||
struct ll_entry* prev; // Предыдущий элемент в очереди
|
||||
uint16_t size; // Размер пользовательского буфера data[] (байт)
|
||||
@@ -128,7 +123,6 @@ struct queue_waiter_handle {
|
||||
queue_threshold_callback_fn defer_cb;
|
||||
void* defer_arg;
|
||||
void* call_soon_id; ///< хендл отложенного вызова (для отмены)
|
||||
struct queue_waiter_handle* defer_next; ///< список отложенных уведомлений очереди
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -152,11 +146,9 @@ struct ll_queue {
|
||||
|
||||
struct queue_waiter* waiter_head; // Голова списка ожидающих
|
||||
struct queue_waiter* waiter_tail; // Хвост списка ожидающих
|
||||
struct queue_waiter_handle* deferred_waiters; // Уже запланированные уведомления
|
||||
int threshold_max_packets; // Общий порог: макс. кол-во элементов (по умолчанию 0)
|
||||
size_t threshold_max_bytes; // Общий порог: макс. объём данных (0 = не проверять)
|
||||
int waiter_defer; // 0=immediate callback, 1=defer via uasync_call_soon
|
||||
int callback_defer; // 0=immediate callback из put (default), 1=defer via uasync_call_soon (очистка стека)
|
||||
|
||||
queue_callback_fn empty_callback; // одноразовый коллбэк при опустошении очереди
|
||||
void* empty_callback_arg;
|
||||
@@ -164,8 +156,6 @@ struct ll_queue {
|
||||
|
||||
queue_callback_fn on_get; // коллбэк после каждого queue_data_get (deferred)
|
||||
void* on_get_arg;
|
||||
void* on_get_call_soon_id;
|
||||
size_t on_get_pending;
|
||||
|
||||
struct ll_entry** hash_table; // Хеш-таблица для поиска по id (если hash_size > 0)
|
||||
size_t hash_size; // Размер хеш-таблицы
|
||||
@@ -251,17 +241,6 @@ void queue_set_threshold(struct ll_queue* q, int max_packets, size_t max_bytes);
|
||||
|
||||
void queue_set_waiter_defer(struct ll_queue* q, int enable);
|
||||
|
||||
/**
|
||||
* @brief Управляет способом вызова коллбэка автозабора при добавлении элемента.
|
||||
* @param q очередь
|
||||
* @param enable 0 — первый запуск коллбэка из queue_data_put* вызывается синхронно (по умолчанию);
|
||||
* 1 — первый запуск откладывается через uasync_call_soon (очищает стек вызывающего).
|
||||
*
|
||||
* @note Влияет только на первый запуск из put. Повторные запуски (queue_resume_callback)
|
||||
* всегда идут через uasync_call_soon.
|
||||
*/
|
||||
void queue_set_callback_defer(struct ll_queue* q, int enable);
|
||||
|
||||
/**
|
||||
* @brief Устанавливает одноразовый коллбэк при опустошении очереди (count==0).
|
||||
* @param q очередь
|
||||
@@ -284,13 +263,12 @@ void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
|
||||
/**
|
||||
* @brief Регистрирует ожидание освобождения очереди до общего порога.
|
||||
* @param q очередь
|
||||
* @param h полностью обнулённый handle: `struct queue_waiter_handle h = {0}`
|
||||
* @param h указатель на handle (создаётся как `struct queue_waiter_handle h = {0}` или `h.internal = NULL`)
|
||||
* @param callback функция, вызываемая при достижении порога
|
||||
* @param arg аргумент коллбэка
|
||||
* @return 1 — условие выполнено (callback вызван или отложен), 0 — ожидает порог, -1 — ошибка
|
||||
* @return 1 — условие уже выполнено (callback вызван немедленно), 0 — зарегистрирован в списке, -1 — ошибка
|
||||
*
|
||||
* Если handle уже ожидает или имеет отложенный callback — старое ожидание отменяется.
|
||||
* При queue_free отменяются также уже запланированные уведомления.
|
||||
* Если handle уже ожидает (h->internal != NULL) — старый waiter отменяется и создаётся новый.
|
||||
*/
|
||||
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
|
||||
queue_threshold_callback_fn callback, void* arg);
|
||||
@@ -310,6 +288,7 @@ void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h);
|
||||
* @brief Добавляет элемент в конец очереди (FIFO).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @param id идентификатор для поиска
|
||||
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
|
||||
*/
|
||||
int queue_data_put(struct ll_queue* q, struct ll_entry* entry);
|
||||
@@ -318,6 +297,7 @@ int queue_data_put(struct ll_queue* q, struct ll_entry* entry);
|
||||
* @brief Добавляет элемент в начало очереди (LIFO, высокий приоритет).
|
||||
* @param q очередь
|
||||
* @param entry элемент
|
||||
* @param id идентификатор
|
||||
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
|
||||
*/
|
||||
int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry);
|
||||
|
||||
@@ -195,7 +195,6 @@ queue_set_empty_callback(q, on_empty, my_data);
|
||||
| `queue_set_callback(q, cbk_fn, arg)` | Установить callback автозабора. Вызывается при наличии элементов. |
|
||||
| `queue_data_get(q)` | Извлечь элемент из головы. Приостанавливает callback до `queue_resume_callback()`. |
|
||||
| `queue_resume_callback(q)` | **Обязательно** вызвать после обработки элемента. Планирует отложенный вызов callback через uasync_call_soon. |
|
||||
| `queue_set_callback_defer(q, enable)` | `enable=1` — первый запуск callback из `put` откладывается через uasync_call_soon (очистка стека). По умолчанию `0` (синхронно). Повторные запуски всегда deferred. |
|
||||
|
||||
### Запись
|
||||
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
#define PREV_OFFSET LOCATION_MAX
|
||||
#define NEXT_OFFSET (LOCATION_MAX + POINTER_SIZE)
|
||||
|
||||
// Порог детекта аномально больших аллокаций (гигантские mmap-резервации, напр. VSZ ~4G)
|
||||
#define LARGE_ALLOC_LOG_THRESHOLD (8u * 1024u * 1024u) // 8 МБ
|
||||
|
||||
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
|
||||
#include <execinfo.h>
|
||||
#include <pthread.h>
|
||||
@@ -109,10 +106,9 @@ void u_check(void* ptr, const char* text, const char* location) {
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
uint32_t post_canary;
|
||||
memcpy(&post_canary, user + size, sizeof(post_canary));
|
||||
if (post_canary != CANARY) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: overflow canary corrupted at %p (expected 0x%X, got 0x%X)", location, text, ptr, CANARY, post_canary);
|
||||
uint32_t* post_canary = (uint32_t*)(user + size);
|
||||
if (*post_canary != CANARY) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: overflow canary corrupted at %p (expected 0x%X, got 0x%X)", location, text, ptr, CANARY, *post_canary);
|
||||
error = true;
|
||||
}
|
||||
if (error) {
|
||||
@@ -125,10 +121,6 @@ void u_check(void* ptr, const char* text, const char* location) {
|
||||
}
|
||||
|
||||
void* u_malloc_impl(uint32_t size, const char* location) {
|
||||
if (size >= LARGE_ALLOC_LOG_THRESHOLD) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_SYS, "LARGE ALLOC: %u bytes (%.1f MB) at %s",
|
||||
size, (double)size / (1024.0 * 1024.0), location);
|
||||
}
|
||||
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
|
||||
uint8_t* base = (uint8_t*)malloc(total);
|
||||
if (!base && size > 0) return NULL;
|
||||
@@ -142,8 +134,7 @@ void* u_malloc_impl(uint32_t size, const char* location) {
|
||||
uint8_t* prefix_start = base + METADATA_SIZE;
|
||||
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE) = CANARY;
|
||||
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 4) = size;
|
||||
const uint32_t post_canary = CANARY; /* размер пользователя не обязан быть кратен выравниванию uint32_t */
|
||||
memcpy(prefix_start + BOUNDARY_CHECK_SIZE + 8 + size, &post_canary, sizeof(post_canary));
|
||||
*(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 8 + size) = CANARY;
|
||||
// Add to list (thread-safe)
|
||||
LOCK();
|
||||
void** prev_ptr = (void**)(base + PREV_OFFSET);
|
||||
@@ -160,10 +151,6 @@ void* u_malloc_impl(uint32_t size, const char* location) {
|
||||
}
|
||||
|
||||
void* u_calloc_impl(uint32_t nmemb, uint32_t size, const char* location) {
|
||||
if (nmemb != 0 && size > UINT32_MAX / nmemb) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "CALLOC OVERFLOW: %u * %u (wraps uint32) at %s", nmemb, size, location);
|
||||
return NULL;
|
||||
}
|
||||
uint32_t total_size = nmemb * size;
|
||||
void* ptr = u_malloc_impl(total_size, location);
|
||||
if (ptr) memset(ptr, 0, total_size);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
/**
|
||||
* mem — выделение памяти с защитными полями, местом аллокации и учётом живых блоков.
|
||||
* Использовать u_malloc/u_calloc/u_realloc/u_strdup и парный u_free; обычный free к этим блокам неприменим.
|
||||
* Макросы передают LOCATION автоматически. OOM возвращает NULL; обнаруженное повреждение завершает процесс.
|
||||
* realloc при ошибке сохраняет старый блок, при size=0 освобождает его.
|
||||
* Отчёт u_report_unfreed_blocks вызывать после остановки потоков, меняющих список аллокаций.
|
||||
* Memory management layer
|
||||
* Provides wrappers for malloc/realloc/calloc/free with error handling
|
||||
*/
|
||||
#ifndef MEM_H
|
||||
#define MEM_H
|
||||
|
||||
+14
-26
@@ -5,59 +5,47 @@
|
||||
#include "memory_pool.h"
|
||||
#include "mem.h"
|
||||
|
||||
// Layout: [user_data] [canary: 4] [alloc_loc: pointer] [free_loc: pointer] [counter: 1]
|
||||
// Layout: [user_data: object_size] [canary: 4 bytes] [alloc_loc: pointer] [counter: 1 byte]
|
||||
#define POOL_CANARY_OFF (pool->object_size)
|
||||
#define POOL_LOC_OFF (pool->object_size + 4)
|
||||
#define POOL_FREE_LOC_OFF (pool->object_size + 4 + sizeof(const char*))
|
||||
#define POOL_COUNTER_OFF (pool->object_size + 4 + 2 * sizeof(const char*))
|
||||
#define POOL_ALLOC_SIZE (pool->object_size + 4 + 2 * sizeof(const char*) + 1)
|
||||
#define POOL_COUNTER_OFF (pool->object_size + 4 + sizeof(const char*))
|
||||
#define POOL_ALLOC_SIZE (pool->object_size + 4 + sizeof(const char*) + 1)
|
||||
#define POOL_CANARY_VAL 0xDEADBEAF
|
||||
|
||||
static void pool_init_tags(struct memory_pool* pool, void* obj, const char* location)
|
||||
{
|
||||
const uint32_t canary = POOL_CANARY_VAL;
|
||||
uint32_t* canary = (uint32_t*)((uint8_t*)obj + POOL_CANARY_OFF);
|
||||
const char** loc = (const char**)((uint8_t*)obj + POOL_LOC_OFF);
|
||||
uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF;
|
||||
/* Метаданные следуют за payload произвольного размера и могут быть невыровненными. */
|
||||
memcpy((uint8_t*)obj + POOL_CANARY_OFF, &canary, sizeof(canary));
|
||||
memcpy((uint8_t*)obj + POOL_LOC_OFF, &location, sizeof(location));
|
||||
const char* free_loc = NULL;
|
||||
memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &free_loc, sizeof(free_loc));
|
||||
*canary = POOL_CANARY_VAL;
|
||||
*loc = location;
|
||||
*counter = 1;
|
||||
}
|
||||
|
||||
static void pool_check_and_clear_tags(struct memory_pool* pool, void* obj, const char* location)
|
||||
{
|
||||
uint32_t canary;
|
||||
const char *loc, *free_loc;
|
||||
memcpy(&canary, (uint8_t*)obj + POOL_CANARY_OFF, sizeof(canary));
|
||||
memcpy(&loc, (uint8_t*)obj + POOL_LOC_OFF, sizeof(loc));
|
||||
memcpy(&free_loc, (uint8_t*)obj + POOL_FREE_LOC_OFF, sizeof(free_loc));
|
||||
uint32_t* canary = (uint32_t*)((uint8_t*)obj + POOL_CANARY_OFF);
|
||||
const char** loc = (const char**)((uint8_t*)obj + POOL_LOC_OFF);
|
||||
uint8_t* counter = (uint8_t*)obj + POOL_COUNTER_OFF;
|
||||
|
||||
if (canary != POOL_CANARY_VAL) {
|
||||
if (*canary != POOL_CANARY_VAL) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free BUFFER OVERFLOW pool=%p name=%s sz=%zu allocs=%zu reuse=%zu free=%d",
|
||||
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count, pool->free_count);
|
||||
/* Повреждённый хвост может содержать битые указатели; их нельзя читать как строки. */
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc_loc_ptr=%p free_loc_ptr=%p free_attempt=%s canary=0x%08x expected=0x%08x",
|
||||
obj, (void*)loc, (void*)free_loc, location, canary, POOL_CANARY_VAL);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " obj=%p alloc=%s free=%s canary=0x%08x expected=0x%08x",
|
||||
obj, *loc ? *loc : "(null)", location, *canary, POOL_CANARY_VAL);
|
||||
if (pool->object_size)
|
||||
log_dump(DEBUG_LEVEL_ERROR, DEBUG_CATEGORY_SYS, " user_data", obj, pool->object_size);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj);
|
||||
volatile int _halt = 1;
|
||||
while (_halt) {}
|
||||
}
|
||||
if (*counter == 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu obj=%p",
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free DOUBLE FREE pool=%p name=%s sz=%zu allocs=%zu reuse=%zu alloc=%s free=%s",
|
||||
pool, pool->name ? pool->name : "?", pool->object_size, pool->allocations, pool->reuse_count,
|
||||
obj);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, " alloc=%s first_free=%s repeated_free=%s",
|
||||
loc ? loc : "(null)", free_loc ? free_loc : "(null)", location);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "pool_free: corruption logged, stopping thread permanently obj=%p", obj);
|
||||
*loc ? *loc : "(null)", location);
|
||||
volatile int _halt = 1;
|
||||
while (_halt) {}
|
||||
}
|
||||
*counter = 0;
|
||||
memcpy((uint8_t*)obj + POOL_FREE_LOC_OFF, &location, sizeof(location));
|
||||
}
|
||||
|
||||
static size_t g_total_free = 0;
|
||||
|
||||
+1
-7
@@ -1,9 +1,4 @@
|
||||
/* memory_pool — повторное использование блоков фиксированного размера в одном потоке.
|
||||
* init создаёт пустой пул; alloc выделяет блок по требованию или берёт свободный и обнуляет payload.
|
||||
* free проверяет защитный хвост и возвращает не более MEMORY_POOL_MAX_FREE блоков в кеш.
|
||||
* object_size должен вмещать void*: свободный блок хранит next в начале payload.
|
||||
* Перед destroy вернуть все выданные блоки: destroy освобождает только кеш и сам пул.
|
||||
* name заимствован до destroy; блок возвращать только своему пулу, внутренней синхронизации нет. */
|
||||
// memory_pool.h
|
||||
#ifndef MEMORY_POOL_H
|
||||
#define MEMORY_POOL_H
|
||||
|
||||
@@ -45,7 +40,6 @@ size_t memory_pool_get_total_free_blocks(void);
|
||||
#define memory_pool_alloc(pool) memory_pool_alloc_impl(pool, PLOCATION)
|
||||
#define memory_pool_free(pool, obj) memory_pool_free_impl(pool, obj, PLOCATION)
|
||||
|
||||
/* Проверяет присутствие в кеше свободных блоков; не определяет состояние уже освобождённого через u_free блока. */
|
||||
int memory_pool_is_freed(struct memory_pool* pool, void* obj);
|
||||
|
||||
|
||||
|
||||
+48
-206
@@ -6780,9 +6780,7 @@ typedef enum
|
||||
ma_device_notification_type_rerouted,
|
||||
ma_device_notification_type_interruption_began,
|
||||
ma_device_notification_type_interruption_ended,
|
||||
ma_device_notification_type_unlocked,
|
||||
ma_device_notification_type_capture_overrun, /* uTun: подтверждённый backend разрыв входного PCM. */
|
||||
ma_device_notification_type_playback_underrun /* uTun: backend исчерпал буфер воспроизведения. */
|
||||
ma_device_notification_type_unlocked
|
||||
} ma_device_notification_type;
|
||||
|
||||
typedef struct
|
||||
@@ -7545,13 +7543,11 @@ struct ma_context
|
||||
ma_proc pa_context_disconnect;
|
||||
ma_proc pa_context_set_state_callback;
|
||||
ma_proc pa_context_get_state;
|
||||
ma_proc pa_context_errno;
|
||||
ma_proc pa_context_get_sink_info_list;
|
||||
ma_proc pa_context_get_source_info_list;
|
||||
ma_proc pa_context_get_sink_info_by_name;
|
||||
ma_proc pa_context_get_source_info_by_name;
|
||||
ma_proc pa_operation_unref;
|
||||
ma_proc pa_operation_cancel;
|
||||
ma_proc pa_operation_get_state;
|
||||
ma_proc pa_channel_map_init_extend;
|
||||
ma_proc pa_channel_map_valid;
|
||||
@@ -7567,7 +7563,6 @@ struct ma_context
|
||||
ma_proc pa_stream_get_buffer_attr;
|
||||
ma_proc pa_stream_set_buffer_attr;
|
||||
ma_proc pa_stream_get_device_name;
|
||||
ma_proc pa_stream_get_latency;
|
||||
ma_proc pa_stream_set_write_callback;
|
||||
ma_proc pa_stream_set_read_callback;
|
||||
ma_proc pa_stream_set_suspended_callback;
|
||||
@@ -7579,7 +7574,6 @@ struct ma_context
|
||||
ma_proc pa_stream_cork;
|
||||
ma_proc pa_stream_trigger;
|
||||
ma_proc pa_stream_begin_write;
|
||||
ma_proc pa_stream_cancel_write;
|
||||
ma_proc pa_stream_write;
|
||||
ma_proc pa_stream_peek;
|
||||
ma_proc pa_stream_drop;
|
||||
@@ -11559,17 +11553,6 @@ IMPLEMENTATION
|
||||
#ifndef miniaudio_c
|
||||
#define miniaudio_c
|
||||
|
||||
/* uTun: необязательные наблюдатели, без изменения ABI и поведения аудиотракта. */
|
||||
#ifndef MA_DIAGNOSTIC_EVENT
|
||||
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) ((void)0)
|
||||
#endif
|
||||
#ifndef MA_DIAGNOSTIC_DEVICE_INIT
|
||||
#define MA_DIAGNOSTIC_DEVICE_INIT(device) ((void)0)
|
||||
#endif
|
||||
#ifndef MA_DIAGNOSTIC_LOG
|
||||
#define MA_DIAGNOSTIC_LOG(log, level, message) ((void)0)
|
||||
#endif
|
||||
|
||||
#include <assert.h>
|
||||
#include <limits.h> /* For INT_MAX */
|
||||
#include <math.h> /* sin(), etc. */
|
||||
@@ -13769,7 +13752,6 @@ MA_API ma_result ma_log_post(ma_log* pLog, ma_uint32 level, const char* pMessage
|
||||
return MA_INVALID_ARGS;
|
||||
}
|
||||
|
||||
MA_DIAGNOSTIC_LOG(pLog, level, pMessage);
|
||||
ma_log_lock(pLog);
|
||||
{
|
||||
ma_uint32 iLog;
|
||||
@@ -20218,14 +20200,6 @@ static void ma_device__on_notification(ma_device_notification notification)
|
||||
{
|
||||
MA_ASSERT(notification.pDevice != NULL);
|
||||
|
||||
if (notification.type == ma_device_notification_type_capture_overrun) {
|
||||
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_BACKEND_OVERFLOW, AUDIO_DIAG_READ, 1, 0);
|
||||
} else if (notification.type == ma_device_notification_type_playback_underrun) {
|
||||
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_BACKEND_UNDERRUN, AUDIO_DIAG_WRITE, 1, 0);
|
||||
} else {
|
||||
MA_DIAGNOSTIC_EVENT(notification.pDevice, AUDIO_DIAG_NOTIFY, -1, notification.type, ma_device_get_state(notification.pDevice));
|
||||
}
|
||||
|
||||
if (notification.pDevice->onNotification != NULL) {
|
||||
notification.pDevice->onNotification(¬ification);
|
||||
}
|
||||
@@ -20386,7 +20360,6 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut
|
||||
{
|
||||
float masterVolumeFactor;
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_CALLBACK, frameCount, (pFramesIn != NULL) | ((pFramesOut != NULL) << 1));
|
||||
ma_device_get_master_volume(pDevice, &masterVolumeFactor); /* Use ma_device_get_master_volume() to ensure the volume is loaded atomically. */
|
||||
|
||||
if (pDevice->onData) {
|
||||
@@ -20434,7 +20407,6 @@ static void ma_device__handle_data_callback(ma_device* pDevice, void* pFramesOut
|
||||
ma_silence_pcm_frames(pFramesOut, frameCount, pDevice->playback.format, pDevice->playback.channels);
|
||||
}
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_CALLBACK, frameCount, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -20618,7 +20590,6 @@ static ma_result ma_device__handle_duplex_callback_capture(ma_device* pDevice, m
|
||||
|
||||
if (framesToProcessInClientFormat == 0) {
|
||||
if (ma_pcm_rb_pointer_distance(pRB) == (ma_int32)ma_pcm_rb_get_subbuffer_size(pRB)) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CAPTURE_DROP, AUDIO_DIAG_READ, framesToProcessInDeviceFormat, ma_pcm_rb_available_read(pRB));
|
||||
break; /* Overrun. Not enough room in the ring buffer for input frame. Excess frames are dropped. */
|
||||
}
|
||||
}
|
||||
@@ -20654,7 +20625,6 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
|
||||
ma_result result;
|
||||
ma_uint8 silentInputFrames[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
|
||||
ma_uint32 totalFramesReadOut = 0;
|
||||
ma_silence_pcm_frames(pFramesInInternalFormat, frameCount, pDevice->playback.internalFormat, pDevice->playback.internalChannels);
|
||||
|
||||
MA_ASSERT(pDevice != NULL);
|
||||
MA_ASSERT(frameCount > 0);
|
||||
@@ -20666,7 +20636,6 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
|
||||
Sitting in the ring buffer should be captured data from the capture callback in external format. If there's not enough data in there for
|
||||
the whole frameCount frames we just use silence instead for the input data.
|
||||
*/
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_DUPLEX, frameCount, ma_pcm_rb_available_read(pRB));
|
||||
MA_ZERO_MEMORY(silentInputFrames, sizeof(silentInputFrames));
|
||||
|
||||
while (totalFramesReadOut < frameCount && ma_device_is_started(pDevice)) {
|
||||
@@ -20677,12 +20646,8 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
|
||||
if (pDevice->playback.inputCacheRemaining > 0) {
|
||||
ma_uint64 framesConvertedIn = pDevice->playback.inputCacheRemaining;
|
||||
ma_uint64 framesConvertedOut = (frameCount - totalFramesReadOut);
|
||||
result = ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut);
|
||||
ma_data_converter_process_pcm_frames(&pDevice->playback.converter, ma_offset_pcm_frames_ptr(pDevice->playback.pInputCache, pDevice->playback.inputCacheConsumed, pDevice->playback.format, pDevice->playback.channels), &framesConvertedIn, pFramesInInternalFormat, &framesConvertedOut);
|
||||
|
||||
if (result != MA_SUCCESS || (framesConvertedIn == 0 && framesConvertedOut == 0)) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
|
||||
break;
|
||||
}
|
||||
pDevice->playback.inputCacheConsumed += framesConvertedIn;
|
||||
pDevice->playback.inputCacheRemaining -= framesConvertedIn;
|
||||
|
||||
@@ -20697,34 +20662,30 @@ static ma_result ma_device__handle_duplex_callback_playback(ma_device* pDevice,
|
||||
|
||||
inputFrameCount = (ma_uint32)pDevice->playback.inputCacheCap;
|
||||
result = ma_pcm_rb_acquire_read(pRB, &inputFrameCount, &pInputFrames);
|
||||
if (result != MA_SUCCESS) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
|
||||
break;
|
||||
}
|
||||
if (inputFrameCount > 0) {
|
||||
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount);
|
||||
result = ma_pcm_rb_commit_read(pRB, inputFrameCount);
|
||||
if (result != MA_SUCCESS) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_DUPLEX, 1, result);
|
||||
break;
|
||||
if (result == MA_SUCCESS) {
|
||||
if (inputFrameCount > 0) {
|
||||
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, pInputFrames, inputFrameCount);
|
||||
} else {
|
||||
if (ma_pcm_rb_pointer_distance(pRB) == 0) {
|
||||
break; /* Underrun. */
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Отсутствие микрофонных кадров не останавливает рендер; синтетический ввод не сдвигает capture-ring. */
|
||||
inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap,
|
||||
sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels));
|
||||
/* No capture data available. Feed in silence. */
|
||||
inputFrameCount = (ma_uint32)ma_min(pDevice->playback.inputCacheCap, sizeof(silentInputFrames) / ma_get_bytes_per_frame(pDevice->capture.format, pDevice->capture.channels));
|
||||
ma_device__handle_data_callback(pDevice, pDevice->playback.pInputCache, silentInputFrames, inputFrameCount);
|
||||
}
|
||||
|
||||
pDevice->playback.inputCacheConsumed = 0;
|
||||
pDevice->playback.inputCacheRemaining = inputFrameCount;
|
||||
|
||||
result = ma_pcm_rb_commit_read(pRB, inputFrameCount);
|
||||
if (result != MA_SUCCESS) {
|
||||
return result; /* Should never happen. */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalFramesReadOut < frameCount) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_UNFILLED, AUDIO_DIAG_DUPLEX, frameCount - totalFramesReadOut, ma_pcm_rb_available_read(pRB));
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_DUPLEX, totalFramesReadOut, ma_pcm_rb_available_read(pRB));
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -24832,7 +24793,6 @@ static ma_result ma_device_read__wasapi(ma_device* pDevice, void* pFrames, ma_ui
|
||||
|
||||
/* Overrun detection. */
|
||||
if ((flags & MA_AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) != 0) {
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
|
||||
/* Glitched. Probably due to an overrun. */
|
||||
|
||||
/*
|
||||
@@ -24963,7 +24923,6 @@ static ma_result ma_device_write__wasapi(ma_device* pDevice, const void* pFrames
|
||||
{
|
||||
ma_result result = MA_SUCCESS;
|
||||
ma_uint32 totalFramesProcessed = 0;
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_WRITE, frameCount, pDevice->wasapi.mappedBufferPlaybackLen);
|
||||
|
||||
/* Keep writing to the device until it's stopped or we've consumed all of our input. */
|
||||
while (ma_device_get_state(pDevice) == ma_device_state_started && totalFramesProcessed < frameCount) {
|
||||
@@ -25050,7 +25009,6 @@ static ma_result ma_device_write__wasapi(ma_device* pDevice, const void* pFrames
|
||||
*pFramesWritten = totalFramesProcessed;
|
||||
}
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_WRITE, totalFramesProcessed, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -29911,7 +29869,6 @@ static ma_result ma_device_read__alsa(ma_device* pDevice, void* pFramesOut, ma_u
|
||||
continue; /* Try again. */
|
||||
} else if (resultALSA == -EPIPE) {
|
||||
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "EPIPE (read)\n");
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
|
||||
|
||||
/* Overrun. Recover and try again. If this fails we need to return an error. */
|
||||
resultALSA = ((ma_snd_pcm_recover_proc)pDevice->pContext->alsa.snd_pcm_recover)((ma_snd_pcm_t*)pDevice->alsa.pPCMCapture, resultALSA, MA_TRUE);
|
||||
@@ -29967,7 +29924,6 @@ static ma_result ma_device_write__alsa(ma_device* pDevice, const void* pFrames,
|
||||
continue; /* Try again. */
|
||||
} else if (resultALSA == -EPIPE) {
|
||||
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "EPIPE (write)\n");
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_playback_underrun));
|
||||
|
||||
/* Underrun. Recover and try again. If this fails we need to return an error. */
|
||||
resultALSA = ((ma_snd_pcm_recover_proc)pDevice->pContext->alsa.snd_pcm_recover)((ma_snd_pcm_t*)pDevice->alsa.pPCMPlayback, resultALSA, MA_TRUE); /* MA_TRUE=silent (don't print anything on error). */
|
||||
@@ -30952,13 +30908,11 @@ typedef int (* ma_pa_context_connect_proc) (
|
||||
typedef void (* ma_pa_context_disconnect_proc) (ma_pa_context* c);
|
||||
typedef void (* ma_pa_context_set_state_callback_proc) (ma_pa_context* c, ma_pa_context_notify_cb_t cb, void* userdata);
|
||||
typedef ma_pa_context_state_t (* ma_pa_context_get_state_proc) (const ma_pa_context* c);
|
||||
typedef int (* ma_pa_context_errno_proc) (const ma_pa_context* c);
|
||||
typedef ma_pa_operation* (* ma_pa_context_get_sink_info_list_proc) (ma_pa_context* c, ma_pa_sink_info_cb_t cb, void* userdata);
|
||||
typedef ma_pa_operation* (* ma_pa_context_get_source_info_list_proc) (ma_pa_context* c, ma_pa_source_info_cb_t cb, void* userdata);
|
||||
typedef ma_pa_operation* (* ma_pa_context_get_sink_info_by_name_proc) (ma_pa_context* c, const char* name, ma_pa_sink_info_cb_t cb, void* userdata);
|
||||
typedef ma_pa_operation* (* ma_pa_context_get_source_info_by_name_proc)(ma_pa_context* c, const char* name, ma_pa_source_info_cb_t cb, void* userdata);
|
||||
typedef void (* ma_pa_operation_unref_proc) (ma_pa_operation* o);
|
||||
typedef void (* ma_pa_operation_cancel_proc) (ma_pa_operation* o);
|
||||
typedef ma_pa_operation_state_t (* ma_pa_operation_get_state_proc) (const ma_pa_operation* o);
|
||||
typedef ma_pa_channel_map* (* ma_pa_channel_map_init_extend_proc) (ma_pa_channel_map* m, unsigned channels, ma_pa_channel_map_def_t def);
|
||||
typedef int (* ma_pa_channel_map_valid_proc) (const ma_pa_channel_map* m);
|
||||
@@ -30973,7 +30927,6 @@ typedef const ma_pa_sample_spec* (* ma_pa_stream_get_sample_spec_proc) (
|
||||
typedef const ma_pa_channel_map* (* ma_pa_stream_get_channel_map_proc) (ma_pa_stream* s);
|
||||
typedef const ma_pa_buffer_attr* (* ma_pa_stream_get_buffer_attr_proc) (ma_pa_stream* s);
|
||||
typedef ma_pa_operation* (* ma_pa_stream_set_buffer_attr_proc) (ma_pa_stream* s, const ma_pa_buffer_attr* attr, ma_pa_stream_success_cb_t cb, void* userdata);
|
||||
typedef int (* ma_pa_stream_get_latency_proc)(const ma_pa_stream* s, ma_uint64* usec, int* negative);
|
||||
typedef const char* (* ma_pa_stream_get_device_name_proc) (const ma_pa_stream* s);
|
||||
typedef void (* ma_pa_stream_set_write_callback_proc) (ma_pa_stream* s, ma_pa_stream_request_cb_t cb, void* userdata);
|
||||
typedef void (* ma_pa_stream_set_read_callback_proc) (ma_pa_stream* s, ma_pa_stream_request_cb_t cb, void* userdata);
|
||||
@@ -30986,7 +30939,6 @@ typedef int (* ma_pa_stream_is_corked_proc) (
|
||||
typedef ma_pa_operation* (* ma_pa_stream_cork_proc) (ma_pa_stream* s, int b, ma_pa_stream_success_cb_t cb, void* userdata);
|
||||
typedef ma_pa_operation* (* ma_pa_stream_trigger_proc) (ma_pa_stream* s, ma_pa_stream_success_cb_t cb, void* userdata);
|
||||
typedef int (* ma_pa_stream_begin_write_proc) (ma_pa_stream* s, void** data, size_t* nbytes);
|
||||
typedef int (* ma_pa_stream_cancel_write_proc) (ma_pa_stream* s);
|
||||
typedef int (* ma_pa_stream_write_proc) (ma_pa_stream* s, const void* data, size_t nbytes, ma_pa_free_cb_t free_cb, int64_t offset, ma_pa_seek_mode_t seek);
|
||||
typedef int (* ma_pa_stream_peek_proc) (ma_pa_stream* s, const void** data, size_t* nbytes);
|
||||
typedef int (* ma_pa_stream_drop_proc) (ma_pa_stream* s);
|
||||
@@ -31174,49 +31126,23 @@ static ma_pa_channel_position_t ma_channel_position_to_pulse(ma_channel position
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef MA_PULSE_WAIT_TIMEOUT_MS
|
||||
#define MA_PULSE_WAIT_TIMEOUT_MS 1000
|
||||
#endif
|
||||
|
||||
static ma_result ma_pulse_wait_step(ma_context* pContext, ma_ptr pMainLoop, ma_timer* pTimer, const char* operation)
|
||||
{
|
||||
int resultPA;
|
||||
if (ma_timer_get_time_in_seconds(pTimer) * 1000 >= MA_PULSE_WAIT_TIMEOUT_MS) {
|
||||
ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR,
|
||||
"[PulseAudio] %s timed out after %d ms.\n", operation, MA_PULSE_WAIT_TIMEOUT_MS);
|
||||
return MA_TIMEOUT;
|
||||
}
|
||||
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 0, NULL);
|
||||
if (resultPA < 0) {
|
||||
ma_log_postf(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] %s iterate failed: %d.\n", operation, resultPA);
|
||||
return ma_result_from_pulse(resultPA);
|
||||
}
|
||||
if (resultPA == 0) ma_sleep(1);
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
|
||||
static ma_result ma_wait_for_operation__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_pa_operation* pOP)
|
||||
{
|
||||
ma_result result;
|
||||
ma_timer timer;
|
||||
int resultPA;
|
||||
ma_pa_operation_state_t state;
|
||||
|
||||
MA_ASSERT(pContext != NULL);
|
||||
MA_ASSERT(pOP != NULL);
|
||||
|
||||
ma_timer_init(&timer);
|
||||
for (;;) {
|
||||
state = ((ma_pa_operation_get_state_proc)pContext->pulse.pa_operation_get_state)(pOP);
|
||||
if (state == MA_PA_OPERATION_DONE) return MA_SUCCESS;
|
||||
if (state != MA_PA_OPERATION_RUNNING) {
|
||||
ma_log_post(ma_context_get_log(pContext), MA_LOG_LEVEL_ERROR, "[PulseAudio] Operation cancelled.\n");
|
||||
return MA_ERROR;
|
||||
break; /* Done. */
|
||||
}
|
||||
|
||||
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "wait");
|
||||
if (result != MA_SUCCESS) {
|
||||
((ma_pa_operation_cancel_proc)pContext->pulse.pa_operation_cancel)(pOP);
|
||||
return result;
|
||||
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
|
||||
if (resultPA < 0) {
|
||||
return ma_result_from_pulse(resultPA);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31239,11 +31165,9 @@ static ma_result ma_wait_for_operation_and_unref__pulse(ma_context* pContext, ma
|
||||
|
||||
static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pPulseContext)
|
||||
{
|
||||
ma_result result;
|
||||
ma_timer timer;
|
||||
int resultPA;
|
||||
ma_pa_context_state_t state;
|
||||
|
||||
ma_timer_init(&timer);
|
||||
for (;;) {
|
||||
state = ((ma_pa_context_get_state_proc)pContext->pulse.pa_context_get_state)((ma_pa_context*)pPulseContext);
|
||||
if (state == MA_PA_CONTEXT_READY) {
|
||||
@@ -31255,8 +31179,10 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext,
|
||||
return MA_ERROR;
|
||||
}
|
||||
|
||||
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "context connect");
|
||||
if (result != MA_SUCCESS) return result;
|
||||
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
|
||||
if (resultPA < 0) {
|
||||
return ma_result_from_pulse(resultPA);
|
||||
}
|
||||
}
|
||||
|
||||
/* Should never get here. */
|
||||
@@ -31265,11 +31191,9 @@ static ma_result ma_wait_for_pa_context_to_connect__pulse(ma_context* pContext,
|
||||
|
||||
static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, ma_ptr pMainLoop, ma_ptr pStream)
|
||||
{
|
||||
ma_result result;
|
||||
ma_timer timer;
|
||||
int resultPA;
|
||||
ma_pa_stream_state_t state;
|
||||
|
||||
ma_timer_init(&timer);
|
||||
for (;;) {
|
||||
state = ((ma_pa_stream_get_state_proc)pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pStream);
|
||||
if (state == MA_PA_STREAM_READY) {
|
||||
@@ -31281,8 +31205,10 @@ static ma_result ma_wait_for_pa_stream_to_connect__pulse(ma_context* pContext, m
|
||||
return MA_ERROR;
|
||||
}
|
||||
|
||||
result = ma_pulse_wait_step(pContext, pMainLoop, &timer, "stream connect");
|
||||
if (result != MA_SUCCESS) return result;
|
||||
resultPA = ((ma_pa_mainloop_iterate_proc)pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pMainLoop, 1, NULL);
|
||||
if (resultPA < 0) {
|
||||
return ma_result_from_pulse(resultPA);
|
||||
}
|
||||
}
|
||||
|
||||
return MA_SUCCESS;
|
||||
@@ -31456,13 +31382,10 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext
|
||||
|
||||
if (deviceType == ma_device_type_playback) {
|
||||
ma_pa_sink_info sinkInfo;
|
||||
MA_ZERO_OBJECT(&sinkInfo);
|
||||
sinkInfo.index = (ma_uint32)-1;
|
||||
result = ma_context_get_sink_info__pulse(pContext, NULL, &sinkInfo);
|
||||
if (result != MA_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
if (sinkInfo.index == (ma_uint32)-1) return MA_NO_DEVICE;
|
||||
|
||||
if (pIndex != NULL) {
|
||||
*pIndex = sinkInfo.index;
|
||||
@@ -31471,13 +31394,10 @@ static ma_result ma_context_get_default_device_index__pulse(ma_context* pContext
|
||||
|
||||
if (deviceType == ma_device_type_capture) {
|
||||
ma_pa_source_info sourceInfo;
|
||||
MA_ZERO_OBJECT(&sourceInfo);
|
||||
sourceInfo.index = (ma_uint32)-1;
|
||||
result = ma_context_get_source_info__pulse(pContext, NULL, &sourceInfo);
|
||||
if (result != MA_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
if (sourceInfo.index == (ma_uint32)-1) return MA_NO_DEVICE;
|
||||
|
||||
if (pIndex != NULL) {
|
||||
*pIndex = sourceInfo.index;
|
||||
@@ -31631,7 +31551,7 @@ static void ma_context_get_device_info_sink_callback__pulse(ma_pa_context* pPuls
|
||||
{
|
||||
ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData;
|
||||
|
||||
if (endOfList != 0 || pInfo == NULL) {
|
||||
if (endOfList > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31668,7 +31588,7 @@ static void ma_context_get_device_info_source_callback__pulse(ma_pa_context* pPu
|
||||
{
|
||||
ma_context_get_device_info_callback_data__pulse* pData = (ma_context_get_device_info_callback_data__pulse*)pUserData;
|
||||
|
||||
if (endOfList != 0 || pInfo == NULL) {
|
||||
if (endOfList > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31720,7 +31640,6 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi
|
||||
}
|
||||
|
||||
result = ma_context_get_default_device_index__pulse(pContext, deviceType, &callbackData.defaultDeviceIndex);
|
||||
if (result != MA_SUCCESS) return result;
|
||||
|
||||
if (deviceType == ma_device_type_playback) {
|
||||
pOP = ((ma_pa_context_get_sink_info_by_name_proc)pContext->pulse.pa_context_get_sink_info_by_name)((ma_pa_context*)(pContext->pulse.pPulseContext), pDeviceName, ma_context_get_device_info_sink_callback__pulse, &callbackData);
|
||||
@@ -31729,8 +31648,7 @@ static ma_result ma_context_get_device_info__pulse(ma_context* pContext, ma_devi
|
||||
}
|
||||
|
||||
if (pOP != NULL) {
|
||||
result = ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP);
|
||||
if (result != MA_SUCCESS) goto done;
|
||||
ma_wait_for_operation_and_unref__pulse(pContext, pContext->pulse.pMainLoop, pOP);
|
||||
} else {
|
||||
result = MA_ERROR;
|
||||
goto done;
|
||||
@@ -31829,7 +31747,6 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
|
||||
MA_ASSERT(bpf > 0);
|
||||
|
||||
frameCount = byteCount / bpf;
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_READ, frameCount, deviceState);
|
||||
framesProcessed = 0;
|
||||
|
||||
while (ma_device_get_state(pDevice) == ma_device_state_started && framesProcessed < frameCount) {
|
||||
@@ -31839,7 +31756,6 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
|
||||
|
||||
int pulseResult = ((ma_pa_stream_peek_proc)pDevice->pContext->pulse.pa_stream_peek)(pStream, &pMappedPCMFrames, &bytesMapped);
|
||||
if (pulseResult < 0) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult);
|
||||
break; /* Failed to map. Abort. */
|
||||
}
|
||||
|
||||
@@ -31848,25 +31764,12 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
|
||||
if (pMappedPCMFrames != NULL) {
|
||||
ma_device_handle_backend_data_callback(pDevice, NULL, pMappedPCMFrames, framesMapped);
|
||||
} else {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_HOLE, AUDIO_DIAG_READ, framesMapped, 0);
|
||||
ma_uint8 silence[MA_DATA_CONVERTER_STACK_BUFFER_SIZE];
|
||||
ma_uint64 remaining = framesMapped;
|
||||
ma_uint32 capacity = sizeof(silence) / bpf;
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_capture_overrun));
|
||||
ma_silence_pcm_frames(silence, capacity, pDevice->capture.internalFormat, pDevice->capture.internalChannels);
|
||||
while (remaining > 0 && ma_device_get_state(pDevice) == ma_device_state_started) {
|
||||
ma_uint32 n = (ma_uint32)ma_min(remaining, capacity);
|
||||
#ifdef MA_AUDIO_CAPTURE_HOLE
|
||||
MA_AUDIO_CAPTURE_HOLE(framesMapped - remaining);
|
||||
#endif
|
||||
ma_device_handle_backend_data_callback(pDevice, NULL, silence, n);
|
||||
remaining -= n;
|
||||
}
|
||||
/* It's a hole. */
|
||||
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[PulseAudio] ma_device_on_read__pulse: Hole.\n");
|
||||
}
|
||||
|
||||
pulseResult = ((ma_pa_stream_drop_proc)pDevice->pContext->pulse.pa_stream_drop)(pStream);
|
||||
if (pulseResult < 0) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_READ, 1, pulseResult);
|
||||
break; /* Failed to drop the buffer. */
|
||||
}
|
||||
|
||||
@@ -31877,7 +31780,6 @@ static void ma_device_on_read__pulse(ma_pa_stream* pStream, size_t byteCount, vo
|
||||
break;
|
||||
}
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_READ, framesProcessed, 0);
|
||||
}
|
||||
|
||||
static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stream* pStream, ma_uint64* pFramesProcessed)
|
||||
@@ -31896,7 +31798,6 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
|
||||
|
||||
deviceState = ma_device_get_state(pDevice);
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_MAP, 0, deviceState);
|
||||
bytesMapped = ((ma_pa_stream_writable_size_proc)pDevice->pContext->pulse.pa_stream_writable_size)(pStream);
|
||||
if (bytesMapped != (size_t)-1) {
|
||||
if (bytesMapped > 0) {
|
||||
@@ -31909,23 +31810,15 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
|
||||
}
|
||||
|
||||
framesMapped = bytesMapped / bpf;
|
||||
if (framesMapped == 0 || pMappedPCMFrames == NULL) {
|
||||
((ma_pa_stream_cancel_write_proc)pDevice->pContext->pulse.pa_stream_cancel_write)(pStream);
|
||||
result = MA_ERROR;
|
||||
goto done;
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesMapped, 0);
|
||||
|
||||
if (deviceState == ma_device_state_started || deviceState == ma_device_state_starting) { /* Check for starting state just in case this is being used to do the initial fill. */
|
||||
ma_device_handle_backend_data_callback(pDevice, pMappedPCMFrames, NULL, framesMapped);
|
||||
} else {
|
||||
/* Device is not started. Write silence. */
|
||||
ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.internalFormat, pDevice->playback.internalChannels);
|
||||
ma_silence_pcm_frames(pMappedPCMFrames, framesMapped, pDevice->playback.format, pDevice->playback.channels);
|
||||
}
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_SUBMIT, framesMapped, 0);
|
||||
pulseResult = ((ma_pa_stream_write_proc)pDevice->pContext->pulse.pa_stream_write)(pStream, pMappedPCMFrames, bytesMapped, NULL, 0, MA_PA_SEEK_RELATIVE);
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_SUBMIT, framesMapped, pulseResult);
|
||||
if (pulseResult < 0) {
|
||||
result = ma_result_from_pulse(pulseResult);
|
||||
goto done; /* Failed to write data to stream. */
|
||||
@@ -31942,10 +31835,6 @@ static ma_result ma_device_write_to_stream__pulse(ma_device* pDevice, ma_pa_stre
|
||||
}
|
||||
|
||||
done:
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_MAP, framesProcessed, result);
|
||||
if (result != MA_SUCCESS) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_WRITE, 1, result);
|
||||
}
|
||||
if (pFramesProcessed != NULL) {
|
||||
*pFramesProcessed = framesProcessed;
|
||||
}
|
||||
@@ -31961,7 +31850,6 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
|
||||
ma_uint64 framesProcessed;
|
||||
ma_uint32 deviceState;
|
||||
ma_result result;
|
||||
ma_bool32 diagnosticNoProgress = MA_FALSE;
|
||||
|
||||
MA_ASSERT(pDevice != NULL);
|
||||
|
||||
@@ -31978,7 +31866,6 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
|
||||
MA_ASSERT(bpf > 0);
|
||||
|
||||
frameCount = byteCount / bpf;
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_WRITE, frameCount, deviceState);
|
||||
framesProcessed = 0;
|
||||
|
||||
while (framesProcessed < frameCount) {
|
||||
@@ -31995,14 +31882,8 @@ static void ma_device_on_write__pulse(ma_pa_stream* pStream, size_t byteCount, v
|
||||
break;
|
||||
}
|
||||
|
||||
if (framesProcessedThisIteration == 0 && !diagnosticNoProgress) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_NO_PROGRESS, AUDIO_DIAG_WRITE, 1, frameCount - framesProcessed);
|
||||
diagnosticNoProgress = MA_TRUE;
|
||||
}
|
||||
if (framesProcessedThisIteration == 0) break;
|
||||
framesProcessed += framesProcessedThisIteration;
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_WRITE, framesProcessed, 0);
|
||||
}
|
||||
|
||||
static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData)
|
||||
@@ -32021,10 +31902,10 @@ static void ma_device_on_suspended__pulse(ma_pa_stream* pStream, void* pUserData
|
||||
|
||||
if (suspended == 1) {
|
||||
ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Suspended.\n");
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_began));
|
||||
ma_device__on_notification_stopped(pDevice);
|
||||
} else {
|
||||
ma_log_post(ma_device_get_log(pDevice), MA_LOG_LEVEL_DEBUG, "[Pulse] Device suspended state changed. Resumed.\n");
|
||||
ma_device__on_notification(ma_device_notification_init(pDevice, ma_device_notification_type_interruption_ended));
|
||||
ma_device__on_notification_started(pDevice);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32163,7 +32044,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi
|
||||
if (pDescriptorCapture->sampleRate != 0) {
|
||||
ss.rate = pDescriptorCapture->sampleRate;
|
||||
}
|
||||
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY | MA_PA_STREAM_AUTO_TIMING_UPDATE | MA_PA_STREAM_INTERPOLATE_TIMING;
|
||||
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY;
|
||||
|
||||
if (ma_format_from_pulse(ss.format) == ma_format_unknown) {
|
||||
if (ma_is_little_endian()) {
|
||||
@@ -32322,7 +32203,7 @@ static ma_result ma_device_init__pulse(ma_device* pDevice, const ma_device_confi
|
||||
ss.rate = pDescriptorPlayback->sampleRate;
|
||||
}
|
||||
|
||||
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY | MA_PA_STREAM_AUTO_TIMING_UPDATE | MA_PA_STREAM_INTERPOLATE_TIMING;
|
||||
streamFlags = MA_PA_STREAM_START_CORKED | MA_PA_STREAM_ADJUST_LATENCY;
|
||||
if (ma_format_from_pulse(ss.format) == ma_format_unknown) {
|
||||
if (ma_is_little_endian()) {
|
||||
ss.format = MA_PA_SAMPLE_FLOAT32LE;
|
||||
@@ -32494,10 +32375,6 @@ on_error1:
|
||||
((ma_pa_stream_unref_proc)pDevice->pContext->pulse.pa_stream_unref)((ma_pa_stream*)pDevice->pulse.pStreamCapture);
|
||||
}
|
||||
on_error0:
|
||||
((ma_pa_context_disconnect_proc)pDevice->pContext->pulse.pa_context_disconnect)((ma_pa_context*)pDevice->pulse.pPulseContext);
|
||||
((ma_pa_context_unref_proc)pDevice->pContext->pulse.pa_context_unref)((ma_pa_context*)pDevice->pulse.pPulseContext);
|
||||
((ma_pa_mainloop_free_proc)pDevice->pContext->pulse.pa_mainloop_free)((ma_pa_mainloop*)pDevice->pulse.pMainLoop);
|
||||
MA_ZERO_OBJECT(&pDevice->pulse);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -32627,24 +32504,8 @@ static ma_result ma_device_data_loop__pulse(ma_device* pDevice)
|
||||
the callbacks deal with it.
|
||||
*/
|
||||
while (ma_device_get_state(pDevice) == ma_device_state_started) {
|
||||
ma_pa_context_state_t contextState = ((ma_pa_context_get_state_proc)pDevice->pContext->pulse.pa_context_get_state)((ma_pa_context*)pDevice->pulse.pPulseContext);
|
||||
ma_pa_stream_state_t captureState = pDevice->pulse.pStreamCapture ?
|
||||
((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamCapture) : MA_PA_STREAM_READY;
|
||||
ma_pa_stream_state_t playbackState = pDevice->pulse.pStreamPlayback ?
|
||||
((ma_pa_stream_get_state_proc)pDevice->pContext->pulse.pa_stream_get_state)((ma_pa_stream*)pDevice->pulse.pStreamPlayback) : MA_PA_STREAM_READY;
|
||||
if (contextState == MA_PA_CONTEXT_FAILED || contextState == MA_PA_CONTEXT_TERMINATED ||
|
||||
captureState == MA_PA_STREAM_FAILED || captureState == MA_PA_STREAM_TERMINATED ||
|
||||
playbackState == MA_PA_STREAM_FAILED || playbackState == MA_PA_STREAM_TERMINATED) {
|
||||
ma_log_postf(ma_device_get_log(pDevice), MA_LOG_LEVEL_ERROR,
|
||||
"[PulseAudio] Device failed context=%d capture=%d playback=%d errno=%d.\n", contextState, captureState,
|
||||
playbackState, ((ma_pa_context_errno_proc)pDevice->pContext->pulse.pa_context_errno)((ma_pa_context*)pDevice->pulse.pPulseContext));
|
||||
return MA_ERROR;
|
||||
}
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ENTER, AUDIO_DIAG_LOOP, 0, 0);
|
||||
resultPA = ((ma_pa_mainloop_iterate_proc)pDevice->pContext->pulse.pa_mainloop_iterate)((ma_pa_mainloop*)pDevice->pulse.pMainLoop, 1, NULL);
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_LEAVE, AUDIO_DIAG_LOOP, 0, resultPA);
|
||||
if (resultPA < 0) {
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_ERROR, AUDIO_DIAG_LOOP, 1, resultPA);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -32727,13 +32588,11 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_context_disconnect = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_disconnect");
|
||||
pContext->pulse.pa_context_set_state_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_set_state_callback");
|
||||
pContext->pulse.pa_context_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_state");
|
||||
pContext->pulse.pa_context_errno = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_errno");
|
||||
pContext->pulse.pa_context_get_sink_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_list");
|
||||
pContext->pulse.pa_context_get_source_info_list = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_list");
|
||||
pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_sink_info_by_name");
|
||||
pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_context_get_source_info_by_name");
|
||||
pContext->pulse.pa_operation_unref = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_unref");
|
||||
pContext->pulse.pa_operation_cancel = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_cancel");
|
||||
pContext->pulse.pa_operation_get_state = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_operation_get_state");
|
||||
pContext->pulse.pa_channel_map_init_extend = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_init_extend");
|
||||
pContext->pulse.pa_channel_map_valid = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_channel_map_valid");
|
||||
@@ -32748,7 +32607,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_stream_get_channel_map = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_channel_map");
|
||||
pContext->pulse.pa_stream_get_buffer_attr = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_buffer_attr");
|
||||
pContext->pulse.pa_stream_set_buffer_attr = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_buffer_attr");
|
||||
pContext->pulse.pa_stream_get_latency = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_latency");
|
||||
pContext->pulse.pa_stream_get_device_name = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_get_device_name");
|
||||
pContext->pulse.pa_stream_set_write_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_write_callback");
|
||||
pContext->pulse.pa_stream_set_read_callback = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_set_read_callback");
|
||||
@@ -32761,7 +32619,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_stream_cork = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cork");
|
||||
pContext->pulse.pa_stream_trigger = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_trigger");
|
||||
pContext->pulse.pa_stream_begin_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_begin_write");
|
||||
pContext->pulse.pa_stream_cancel_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_cancel_write");
|
||||
pContext->pulse.pa_stream_write = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_write");
|
||||
pContext->pulse.pa_stream_peek = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_peek");
|
||||
pContext->pulse.pa_stream_drop = (ma_proc)ma_dlsym(ma_context_get_log(pContext), pContext->pulse.pulseSO, "pa_stream_drop");
|
||||
@@ -32794,13 +32651,11 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
ma_pa_context_disconnect_proc _pa_context_disconnect = pa_context_disconnect;
|
||||
ma_pa_context_set_state_callback_proc _pa_context_set_state_callback = pa_context_set_state_callback;
|
||||
ma_pa_context_get_state_proc _pa_context_get_state = pa_context_get_state;
|
||||
ma_pa_context_errno_proc _pa_context_errno = pa_context_errno;
|
||||
ma_pa_context_get_sink_info_list_proc _pa_context_get_sink_info_list = pa_context_get_sink_info_list;
|
||||
ma_pa_context_get_source_info_list_proc _pa_context_get_source_info_list = pa_context_get_source_info_list;
|
||||
ma_pa_context_get_sink_info_by_name_proc _pa_context_get_sink_info_by_name = pa_context_get_sink_info_by_name;
|
||||
ma_pa_context_get_source_info_by_name_proc _pa_context_get_source_info_by_name= pa_context_get_source_info_by_name;
|
||||
ma_pa_operation_unref_proc _pa_operation_unref = pa_operation_unref;
|
||||
ma_pa_operation_cancel_proc _pa_operation_cancel = pa_operation_cancel;
|
||||
ma_pa_operation_get_state_proc _pa_operation_get_state = pa_operation_get_state;
|
||||
ma_pa_channel_map_init_extend_proc _pa_channel_map_init_extend = pa_channel_map_init_extend;
|
||||
ma_pa_channel_map_valid_proc _pa_channel_map_valid = pa_channel_map_valid;
|
||||
@@ -32815,7 +32670,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
ma_pa_stream_get_channel_map_proc _pa_stream_get_channel_map = pa_stream_get_channel_map;
|
||||
ma_pa_stream_get_buffer_attr_proc _pa_stream_get_buffer_attr = pa_stream_get_buffer_attr;
|
||||
ma_pa_stream_set_buffer_attr_proc _pa_stream_set_buffer_attr = pa_stream_set_buffer_attr;
|
||||
ma_pa_stream_get_latency_proc _pa_stream_get_latency = pa_stream_get_latency;
|
||||
ma_pa_stream_get_device_name_proc _pa_stream_get_device_name = pa_stream_get_device_name;
|
||||
ma_pa_stream_set_write_callback_proc _pa_stream_set_write_callback = pa_stream_set_write_callback;
|
||||
ma_pa_stream_set_read_callback_proc _pa_stream_set_read_callback = pa_stream_set_read_callback;
|
||||
@@ -32828,7 +32682,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
ma_pa_stream_cork_proc _pa_stream_cork = pa_stream_cork;
|
||||
ma_pa_stream_trigger_proc _pa_stream_trigger = pa_stream_trigger;
|
||||
ma_pa_stream_begin_write_proc _pa_stream_begin_write = pa_stream_begin_write;
|
||||
ma_pa_stream_cancel_write_proc _pa_stream_cancel_write = pa_stream_cancel_write;
|
||||
ma_pa_stream_write_proc _pa_stream_write = pa_stream_write;
|
||||
ma_pa_stream_peek_proc _pa_stream_peek = pa_stream_peek;
|
||||
ma_pa_stream_drop_proc _pa_stream_drop = pa_stream_drop;
|
||||
@@ -32860,13 +32713,11 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_context_disconnect = (ma_proc)_pa_context_disconnect;
|
||||
pContext->pulse.pa_context_set_state_callback = (ma_proc)_pa_context_set_state_callback;
|
||||
pContext->pulse.pa_context_get_state = (ma_proc)_pa_context_get_state;
|
||||
pContext->pulse.pa_context_errno = (ma_proc)_pa_context_errno;
|
||||
pContext->pulse.pa_context_get_sink_info_list = (ma_proc)_pa_context_get_sink_info_list;
|
||||
pContext->pulse.pa_context_get_source_info_list = (ma_proc)_pa_context_get_source_info_list;
|
||||
pContext->pulse.pa_context_get_sink_info_by_name = (ma_proc)_pa_context_get_sink_info_by_name;
|
||||
pContext->pulse.pa_context_get_source_info_by_name = (ma_proc)_pa_context_get_source_info_by_name;
|
||||
pContext->pulse.pa_operation_unref = (ma_proc)_pa_operation_unref;
|
||||
pContext->pulse.pa_operation_cancel = (ma_proc)_pa_operation_cancel;
|
||||
pContext->pulse.pa_operation_get_state = (ma_proc)_pa_operation_get_state;
|
||||
pContext->pulse.pa_channel_map_init_extend = (ma_proc)_pa_channel_map_init_extend;
|
||||
pContext->pulse.pa_channel_map_valid = (ma_proc)_pa_channel_map_valid;
|
||||
@@ -32881,7 +32732,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_stream_get_channel_map = (ma_proc)_pa_stream_get_channel_map;
|
||||
pContext->pulse.pa_stream_get_buffer_attr = (ma_proc)_pa_stream_get_buffer_attr;
|
||||
pContext->pulse.pa_stream_set_buffer_attr = (ma_proc)_pa_stream_set_buffer_attr;
|
||||
pContext->pulse.pa_stream_get_latency = (ma_proc)_pa_stream_get_latency;
|
||||
pContext->pulse.pa_stream_get_device_name = (ma_proc)_pa_stream_get_device_name;
|
||||
pContext->pulse.pa_stream_set_write_callback = (ma_proc)_pa_stream_set_write_callback;
|
||||
pContext->pulse.pa_stream_set_read_callback = (ma_proc)_pa_stream_set_read_callback;
|
||||
@@ -32894,7 +32744,6 @@ static ma_result ma_context_init__pulse(ma_context* pContext, const ma_context_c
|
||||
pContext->pulse.pa_stream_cork = (ma_proc)_pa_stream_cork;
|
||||
pContext->pulse.pa_stream_trigger = (ma_proc)_pa_stream_trigger;
|
||||
pContext->pulse.pa_stream_begin_write = (ma_proc)_pa_stream_begin_write;
|
||||
pContext->pulse.pa_stream_cancel_write = (ma_proc)_pa_stream_cancel_write;
|
||||
pContext->pulse.pa_stream_write = (ma_proc)_pa_stream_write;
|
||||
pContext->pulse.pa_stream_peek = (ma_proc)_pa_stream_peek;
|
||||
pContext->pulse.pa_stream_drop = (ma_proc)_pa_stream_drop;
|
||||
@@ -43918,7 +43767,6 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC
|
||||
|
||||
result = pContext->callbacks.onDeviceInit(pDevice, pConfig, &descriptorPlayback, &descriptorCapture);
|
||||
if (result != MA_SUCCESS) {
|
||||
ma_event_uninit(&pDevice->stopEvent);
|
||||
ma_event_uninit(&pDevice->startEvent);
|
||||
ma_event_uninit(&pDevice->wakeupEvent);
|
||||
ma_mutex_uninit(&pDevice->startStopLock);
|
||||
@@ -44160,8 +44008,6 @@ MA_API ma_result ma_device_init(ma_context* pContext, const ma_device_config* pC
|
||||
}
|
||||
}
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_OPEN, -1, pDevice->type, pDevice->sampleRate);
|
||||
MA_DIAGNOSTIC_DEVICE_INIT(pDevice);
|
||||
MA_ASSERT(ma_device_get_state(pDevice) == ma_device_state_stopped);
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
@@ -44329,7 +44175,6 @@ MA_API void ma_device_uninit(ma_device* pDevice)
|
||||
ma_free(pDevice->pContext, &allocationCallbacks);
|
||||
}
|
||||
|
||||
MA_DIAGNOSTIC_EVENT(pDevice, AUDIO_DIAG_CLOSE, -1, 0, 0);
|
||||
MA_ZERO_OBJECT(pDevice);
|
||||
}
|
||||
|
||||
@@ -44661,14 +44506,6 @@ MA_API ma_result ma_device_handle_backend_data_callback(ma_device* pDevice, void
|
||||
if (frameCount == 0) {
|
||||
return MA_INVALID_ARGS;
|
||||
}
|
||||
if (((pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_loopback) && pInput == NULL) ||
|
||||
(pDevice->type == ma_device_type_playback && pOutput == NULL)) {
|
||||
return MA_INVALID_ARGS;
|
||||
}
|
||||
|
||||
#ifdef MA_AUDIO_TIMING_BEGIN
|
||||
MA_AUDIO_TIMING_BEGIN(pDevice, frameCount, pInput != NULL);
|
||||
#endif
|
||||
|
||||
if (pDevice->type == ma_device_type_duplex) {
|
||||
if (pInput != NULL) {
|
||||
@@ -44680,17 +44517,22 @@ MA_API ma_result ma_device_handle_backend_data_callback(ma_device* pDevice, void
|
||||
}
|
||||
} else {
|
||||
if (pDevice->type == ma_device_type_capture || pDevice->type == ma_device_type_loopback) {
|
||||
if (pInput == NULL) {
|
||||
return MA_INVALID_ARGS;
|
||||
}
|
||||
|
||||
ma_device__send_frames_to_client(pDevice, frameCount, pInput);
|
||||
}
|
||||
|
||||
if (pDevice->type == ma_device_type_playback) {
|
||||
if (pOutput == NULL) {
|
||||
return MA_INVALID_ARGS;
|
||||
}
|
||||
|
||||
ma_device__read_frames_from_client(pDevice, frameCount, pOutput);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef MA_AUDIO_TIMING_END
|
||||
MA_AUDIO_TIMING_END();
|
||||
#endif
|
||||
return MA_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -146,23 +146,6 @@ int opus_codec_encoder_signal_set(opus_codec_encoder_t* enc, int signal_type) {
|
||||
return opus_encoder_ctl(enc->opus_enc, OPUS_SET_SIGNAL(signal_type));
|
||||
}
|
||||
|
||||
int opus_codec_encoder_application_set(opus_codec_encoder_t* enc, int application) {
|
||||
if (!enc || !enc->opus_enc) return -1;
|
||||
return opus_encoder_ctl(enc->opus_enc, OPUS_SET_APPLICATION(application));
|
||||
}
|
||||
|
||||
int opus_codec_encoder_delay(opus_codec_encoder_t* enc, int* samples) {
|
||||
if (!enc || !samples) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Opus delay invalid arguments"); return -1; }
|
||||
opus_int32 delay = 0;
|
||||
int result = opus_encoder_ctl(enc->opus_enc, OPUS_GET_LOOKAHEAD(&delay));
|
||||
if (result != OPUS_OK || delay < 0 || delay > enc->sample_rate) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Opus delay query failed result=%d samples=%d", result, (int)delay); return -1;
|
||||
}
|
||||
*samples = delay;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "Opus capture delay samples=%d rate=%d", (int)delay, enc->sample_rate);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* opus_codec_strerror(int err) {
|
||||
switch (err) {
|
||||
case OPUS_OK: return "OK";
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
/* opus_codec — владеющие обёртки encoder/decoder libopus для int16 interleaved PCM.
|
||||
* create принимает частоту и 1/2 канала; неположительная частота → 48000, неверные каналы → моно.
|
||||
* Encoder/decoder имеют состояние потока; каждому нужен один последовательный владелец.
|
||||
* frame_samples — отсчёты НА КАНАЛ: PCM вмещает frame_samples*channels элементов.
|
||||
* encode возвращает байты Opus, decode — отсчёты на канал; отрицательное значение означает ошибку.
|
||||
* out_cap — байты; decode с data=NULL/len=0 использует PLC libopus. Освобождение — соответствующим destroy. */
|
||||
#ifndef OPUS_CODEC_H
|
||||
#define OPUS_CODEC_H
|
||||
|
||||
@@ -16,10 +10,6 @@ extern "C" {
|
||||
typedef struct opus_codec_encoder opus_codec_encoder_t;
|
||||
typedef struct opus_codec_decoder opus_codec_decoder_t;
|
||||
|
||||
/* Режим кодирования Opus: голос (VOIP, default) или музыка (AUDIO). */
|
||||
#define OPUS_CODEC_APP_VOIP 2048 /* OPUS_APPLICATION_VOIP */
|
||||
#define OPUS_CODEC_APP_AUDIO 2049 /* OPUS_APPLICATION_AUDIO */
|
||||
|
||||
opus_codec_encoder_t* opus_codec_encoder_create(int sample_rate, int channels);
|
||||
void opus_codec_encoder_destroy(opus_codec_encoder_t* enc);
|
||||
int opus_codec_encode(opus_codec_encoder_t* enc, const int16_t* pcm, int frame_samples,
|
||||
@@ -33,9 +23,6 @@ int opus_codec_decode(opus_codec_decoder_t* dec, const uint8_t* data, int len,
|
||||
int opus_codec_encoder_bitrate_set(opus_codec_encoder_t* enc, int bitrate_bps);
|
||||
int opus_codec_encoder_complexity_set(opus_codec_encoder_t* enc, int complexity);
|
||||
int opus_codec_encoder_signal_set(opus_codec_encoder_t* enc, int signal_type);
|
||||
int opus_codec_encoder_application_set(opus_codec_encoder_t* enc, int application);
|
||||
/* Задержка codec в сэмплах на канал; запрашивать после настройки encoder, не задавать константой. */
|
||||
int opus_codec_encoder_delay(opus_codec_encoder_t* enc, int* samples);
|
||||
|
||||
const char* opus_codec_strerror(int err);
|
||||
|
||||
|
||||
+22
-60
@@ -39,25 +39,6 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Классифицирует IPv4-адрес (network byte order):
|
||||
* 1 = глобальный/публичный (маршрутизируемый в интернете),
|
||||
* 0 = приватный, CGNAT, loopback, link-local, multicast или зарезервированный.
|
||||
*/
|
||||
int ip_is_public(uint32_t addr_be) {
|
||||
uint32_t a = ntohl(addr_be);
|
||||
if ((a & 0xFF000000u) == 0x0A000000u) return 0; /* 10.0.0.0/8 */
|
||||
if ((a & 0xFFF00000u) == 0xAC100000u) return 0; /* 172.16.0.0/12 */
|
||||
if ((a & 0xFFFF0000u) == 0xC0A80000u) return 0; /* 192.168.0.0/16 */
|
||||
if ((a & 0xFF000000u) == 0x7F000000u) return 0; /* 127.0.0.0/8 */
|
||||
if ((a & 0xFFFF0000u) == 0xA9FE0000u) return 0; /* 169.254.0.0/16 */
|
||||
if ((a & 0xFF000000u) == 0x00000000u) return 0; /* 0.0.0.0/8 */
|
||||
if ((a & 0xF0000000u) == 0xE0000000u) return 0; /* 224.0.0.0/4 */
|
||||
if ((a & 0xF0000000u) == 0xF0000000u) return 0; /* 240.0.0.0/4 */
|
||||
if ((a & 0xFFC00000u) == 0x64400000u) return 0; /* 100.64.0.0/10 (CGNAT) */
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Generate cryptographically secure random bytes
|
||||
* Returns 0 on success, -1 on error
|
||||
@@ -79,12 +60,6 @@ int random_bytes(uint8_t *buffer, size_t len) {
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Глобальный unicast IPv6 (2000::/3). При выборе адреса интерфейса предпочитаем его
|
||||
* ULA (fc00::/7) и link-local (fe80::/10). */
|
||||
static int ipv6_is_global_unicast(const uint8_t* a) {
|
||||
return (a[0] & 0xe0) == 0x20;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
uint32_t get_interface_ip_by_index(uint32_t netif_index) {
|
||||
struct ifaddrs *ifaddr, *ifa;
|
||||
@@ -114,8 +89,7 @@ int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* ad
|
||||
struct ifaddrs *ifaddr, *ifa;
|
||||
if (getifaddrs(&ifaddr) == -1) return -1;
|
||||
|
||||
uint8_t global_match[16], local_match[16];
|
||||
int has_global = 0, has_local = 0;
|
||||
int found = 0;
|
||||
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
|
||||
if (ifa->ifa_addr == NULL) continue;
|
||||
if (ifa->ifa_name == NULL) continue;
|
||||
@@ -126,18 +100,20 @@ int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* ad
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)ifa->ifa_addr;
|
||||
if ((sin6->sin6_addr.s6_addr[0] & 0xfe) == 0xfe && sin6->sin6_addr.s6_addr[1] == 0x80) continue;
|
||||
int is_temporary = (ifa->ifa_flags & IFA_F_TEMPORARY) ? 1 : 0;
|
||||
if ((temporary && !is_temporary) || (!temporary && is_temporary)) continue;
|
||||
if (ipv6_is_global_unicast(sin6->sin6_addr.s6_addr)) {
|
||||
if (!has_global) { memcpy(global_match, &sin6->sin6_addr, 16); has_global = 1; }
|
||||
} else {
|
||||
if (!has_local) { memcpy(local_match, &sin6->sin6_addr, 16); has_local = 1; }
|
||||
if (temporary && is_temporary) {
|
||||
memcpy(addr_out, &sin6->sin6_addr, 16);
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
if (!temporary && !is_temporary) {
|
||||
memcpy(addr_out, &sin6->sin6_addr, 16);
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
freeifaddrs(ifaddr);
|
||||
if (has_global) { memcpy(addr_out, global_match, 16); return 0; }
|
||||
if (has_local) { memcpy(addr_out, local_match, 16); return 0; }
|
||||
return -1;
|
||||
return found ? 0 : -1;
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
@@ -175,8 +151,8 @@ int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t*
|
||||
|
||||
if (send(nl_sock, &req, req.nlh.nlmsg_len, 0) < 0) { close(nl_sock); return -1; }
|
||||
|
||||
uint8_t fallback[16], g_stable[16], g_temp[16], l_stable[16], l_temp[16];
|
||||
int has_fallback = 0, has_g_stable = 0, has_g_temp = 0, has_l_stable = 0, has_l_temp = 0;
|
||||
uint8_t fallback[16], stable_addr[16], temp_addr[16];
|
||||
int has_fallback = 0, has_stable = 0, has_temp = 0;
|
||||
|
||||
char buf[8192];
|
||||
int done = 0;
|
||||
@@ -205,32 +181,19 @@ int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t*
|
||||
|
||||
int is_temp = (ifa_flags & IFA_F_TEMPORARY) != 0;
|
||||
int is_mngtmp = (ifa_flags & IFA_F_MANAGETEMPADDR) != 0;
|
||||
int is_global = ipv6_is_global_unicast(addr);
|
||||
|
||||
if (!has_fallback) { memcpy(fallback, addr, 16); has_fallback = 1; }
|
||||
|
||||
if (is_global) {
|
||||
if (!has_g_stable && (!is_temp || is_mngtmp)) { memcpy(g_stable, addr, 16); has_g_stable = 1; }
|
||||
if (!has_g_temp && is_temp && !is_mngtmp) { memcpy(g_temp, addr, 16); has_g_temp = 1; }
|
||||
} else {
|
||||
if (!has_l_stable && (!is_temp || is_mngtmp)) { memcpy(l_stable, addr, 16); has_l_stable = 1; }
|
||||
if (!has_l_temp && is_temp && !is_mngtmp) { memcpy(l_temp, addr, 16); has_l_temp = 1; }
|
||||
}
|
||||
if (!has_stable && (!is_temp || is_mngtmp)) { memcpy(stable_addr, addr, 16); has_stable = 1; }
|
||||
if (!has_temp && is_temp && !is_mngtmp) { memcpy(temp_addr, addr, 16); has_temp = 1; }
|
||||
}
|
||||
}
|
||||
close(nl_sock);
|
||||
|
||||
if (prefer_stable) {
|
||||
if (has_g_stable) { memcpy(addr_out, g_stable, 16); return 0; }
|
||||
if (has_g_temp) { memcpy(addr_out, g_temp, 16); return 0; }
|
||||
if (has_l_stable) { memcpy(addr_out, l_stable, 16); return 0; }
|
||||
if (has_l_temp) { memcpy(addr_out, l_temp, 16); return 0; }
|
||||
} else {
|
||||
if (has_g_temp) { memcpy(addr_out, g_temp, 16); return 0; }
|
||||
if (has_g_stable) { memcpy(addr_out, g_stable, 16); return 0; }
|
||||
if (has_l_temp) { memcpy(addr_out, l_temp, 16); return 0; }
|
||||
if (has_l_stable) { memcpy(addr_out, l_stable, 16); return 0; }
|
||||
}
|
||||
if (prefer_stable && has_stable) { memcpy(addr_out, stable_addr, 16); return 0; }
|
||||
if (!prefer_stable && has_temp) { memcpy(addr_out, temp_addr, 16); return 0; }
|
||||
if (has_stable) { memcpy(addr_out, stable_addr, 16); return 0; }
|
||||
if (has_temp) { memcpy(addr_out, temp_addr, 16); return 0; }
|
||||
if (has_fallback) { memcpy(addr_out, fallback, 16); return 0; }
|
||||
return -1;
|
||||
}
|
||||
@@ -372,7 +335,6 @@ int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t*
|
||||
PIP_ADAPTER_ADDRESSES adapters = plat_win_get_adapters(AF_INET6);
|
||||
if (!adapters) return -1;
|
||||
int found = 0;
|
||||
uint8_t local_match[16]; int has_local = 0;
|
||||
PIP_ADAPTER_ADDRESSES a = plat_win_find_adapter(adapters, netif_index);
|
||||
if (a) {
|
||||
for (PIP_ADAPTER_UNICAST_ADDRESS u = a->FirstUnicastAddress; u; u = u->Next) {
|
||||
@@ -382,12 +344,12 @@ int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t*
|
||||
{ static const uint8_t lb[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1};
|
||||
if (memcmp(b, lb, 16) == 0) continue; } /* loopback */
|
||||
{ static const uint8_t z[16]; if (memcmp(b, z, 16) == 0) continue; } /* :: */
|
||||
if (ipv6_is_global_unicast(b)) { memcpy(addr_out, b, 16); found = 1; break; }
|
||||
if (!has_local) { memcpy(local_match, b, 16); has_local = 1; }
|
||||
memcpy(addr_out, b, 16);
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
free(adapters);
|
||||
if (!found && has_local) { memcpy(addr_out, local_match, 16); found = 1; }
|
||||
return found ? 0 : -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/**
|
||||
* platform_compat — общие заголовки POSIX/Windows и адаптеры отсутствующих POSIX-функций.
|
||||
* Также предоставляет криптографический random_bytes, адреса интерфейсов, выбор default route
|
||||
* и классификацию публичных IPv4. Сокетные типы/ошибки и nonblocking API — в socket_compat.h.
|
||||
* Platform compatibility layer for POSIX functions on Windows
|
||||
*/
|
||||
|
||||
#ifndef PLATFORM_COMPAT_H
|
||||
@@ -203,11 +201,6 @@ int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t*
|
||||
// family: AF_INET or AF_INET6, returns ifindex (0 on failure)
|
||||
uint32_t get_default_route_netif_index(int family);
|
||||
|
||||
// Классифицирует IPv4-адрес (network byte order):
|
||||
// 1 = глобальный/публичный (маршрутизируемый в интернете),
|
||||
// 0 = приватный, CGNAT, loopback, link-local, multicast или зарезервированный.
|
||||
int ip_is_public(uint32_t addr_be);
|
||||
|
||||
|
||||
// Cross-platform mkdir
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "mem.h"
|
||||
#include "debug_config.h"
|
||||
#include "reed_solomon.h"
|
||||
|
||||
struct rs_codec {
|
||||
unsigned n;
|
||||
unsigned k;
|
||||
size_t max_block_size;
|
||||
uint8_t gf_log[256];
|
||||
uint8_t gf_exp[510];
|
||||
uint8_t *generator;
|
||||
uint8_t *work;
|
||||
uint8_t storage[];
|
||||
};
|
||||
|
||||
/* Умножение в GF(256) с примитивным полиномом x^8+x^4+x^3+x^2+1 (0x11d). */
|
||||
static uint8_t rs_multiply(const struct rs_codec *ctx, uint8_t a, uint8_t b) {
|
||||
return a && b ? ctx->gf_exp[ctx->gf_log[a] + ctx->gf_log[b]] : 0;
|
||||
}
|
||||
|
||||
/* Обратить k строк матрицы; результат — правая половина рабочей матрицы [A|I]. */
|
||||
static int rs_invert(struct rs_codec *ctx, const uint8_t *const rows[]) {
|
||||
unsigned k = ctx->k;
|
||||
unsigned stride = 2 * k;
|
||||
for (unsigned i = 0; i < k; i++) {
|
||||
uint8_t *row = ctx->work + i * stride;
|
||||
memcpy(row, rows[i], k);
|
||||
memset(row + k, 0, k);
|
||||
row[k + i] = 1;
|
||||
}
|
||||
for (unsigned col = 0; col < k; col++) {
|
||||
unsigned pivot = col;
|
||||
while (pivot < k && !ctx->work[pivot * stride + col]) pivot++;
|
||||
if (pivot == k) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: singular matrix n=%u k=%u column=%u", ctx->n, k, col);
|
||||
return RS_ERR_MATRIX;
|
||||
}
|
||||
uint8_t *row = ctx->work + col * stride;
|
||||
if (pivot != col) {
|
||||
uint8_t *other = ctx->work + pivot * stride;
|
||||
for (unsigned j = 0; j < stride; j++) {
|
||||
uint8_t tmp = row[j];
|
||||
row[j] = other[j];
|
||||
other[j] = tmp;
|
||||
}
|
||||
}
|
||||
uint8_t inverse = ctx->gf_exp[255 - ctx->gf_log[row[col]]];
|
||||
for (unsigned j = col; j < stride; j++) row[j] = rs_multiply(ctx, row[j], inverse);
|
||||
for (unsigned i = 0; i < k; i++) {
|
||||
if (i == col) continue;
|
||||
uint8_t *other = ctx->work + i * stride;
|
||||
uint8_t factor = other[col];
|
||||
if (!factor) continue;
|
||||
for (unsigned j = col; j < stride; j++) other[j] ^= rs_multiply(ctx, factor, row[j]);
|
||||
}
|
||||
}
|
||||
return RS_OK;
|
||||
}
|
||||
|
||||
/* Построить систематическую матрицу G=V*inverse(V[0:k]) в точках 2^i поля GF(256). */
|
||||
static int rs_build_generator(struct rs_codec *ctx) {
|
||||
unsigned k = ctx->k;
|
||||
if (ctx->n != k) {
|
||||
const uint8_t *rows[RS_MAX_BLOCKS];
|
||||
for (unsigned i = 0; i < ctx->n; i++) {
|
||||
uint8_t value = 1;
|
||||
for (unsigned j = 0; j < k; j++) {
|
||||
ctx->generator[i * k + j] = value;
|
||||
value = rs_multiply(ctx, value, ctx->gf_exp[i]);
|
||||
}
|
||||
}
|
||||
for (unsigned i = 0; i < k; i++) rows[i] = ctx->generator + i * k;
|
||||
int rc = rs_invert(ctx, rows);
|
||||
if (rc != RS_OK) return rc;
|
||||
for (unsigned i = k; i < ctx->n; i++) {
|
||||
uint8_t result[RS_MAX_BLOCKS];
|
||||
for (unsigned j = 0; j < k; j++) {
|
||||
uint8_t value = 0;
|
||||
for (unsigned t = 0; t < k; t++) {
|
||||
value ^= rs_multiply(ctx, ctx->generator[i * k + t], ctx->work[t * 2 * k + k + j]);
|
||||
}
|
||||
result[j] = value;
|
||||
}
|
||||
memcpy(ctx->generator + i * k, result, k);
|
||||
}
|
||||
}
|
||||
memset(ctx->generator, 0, k * k);
|
||||
for (unsigned i = 0; i < k; i++) ctx->generator[i * k + i] = 1;
|
||||
return RS_OK;
|
||||
}
|
||||
|
||||
/* Инициализация параметров, таблиц поля и матрицы; все рабочие данные в одной аллокации. */
|
||||
int rs_init(struct rs_codec **out, unsigned n, unsigned r, size_t block_size) {
|
||||
if (out) *out = NULL;
|
||||
if (!out || !n || n > RS_MAX_BLOCKS || r >= n || block_size <= RS_HEADER_SIZE || block_size > RS_MAX_BLOCK_SIZE) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid init ctx=%p n=%u r=%u block_size=%zu", (void *)out, n, r, block_size);
|
||||
return RS_ERR_INVALID_ARG;
|
||||
}
|
||||
unsigned k = n - r;
|
||||
size_t bytes = sizeof(struct rs_codec) + n * k + 2 * k * k;
|
||||
struct rs_codec *ctx = u_calloc(1, (uint32_t)bytes);
|
||||
if (!ctx) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: allocation failed n=%u r=%u block_size=%zu bytes=%zu", n, r, block_size, bytes);
|
||||
return RS_ERR_MEMORY;
|
||||
}
|
||||
ctx->n = n;
|
||||
ctx->k = k;
|
||||
ctx->max_block_size = block_size;
|
||||
ctx->generator = ctx->storage;
|
||||
ctx->work = ctx->storage + n * k;
|
||||
unsigned value = 1;
|
||||
for (unsigned i = 0; i < 255; i++) {
|
||||
ctx->gf_exp[i] = (uint8_t)value;
|
||||
ctx->gf_log[value] = (uint8_t)i;
|
||||
value <<= 1;
|
||||
if (value & 256) value ^= 0x11d;
|
||||
}
|
||||
for (unsigned i = 255; i < 510; i++) ctx->gf_exp[i] = ctx->gf_exp[i - 255];
|
||||
int rc = rs_build_generator(ctx);
|
||||
if (rc != RS_OK) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: generator failed n=%u k=%u rc=%d", n, k, rc);
|
||||
u_free(ctx);
|
||||
return rc;
|
||||
}
|
||||
*out = ctx;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "reed_solomon: initialized n=%u r=%u max_block_size=%zu input_capacity=%zu memory=%zu",
|
||||
n, r, block_size, k * (block_size - RS_HEADER_SIZE), bytes);
|
||||
return RS_OK;
|
||||
}
|
||||
|
||||
void rs_free(struct rs_codec *ctx) {
|
||||
if (!ctx) return;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "reed_solomon: free n=%u r=%u max_block_size=%zu", ctx->n, ctx->n - ctx->k, ctx->max_block_size);
|
||||
u_free(ctx);
|
||||
}
|
||||
|
||||
size_t rs_input_capacity(const struct rs_codec *ctx) {
|
||||
if (!ctx) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: input_capacity requires a context");
|
||||
return 0;
|
||||
}
|
||||
return ctx->k * (ctx->max_block_size - RS_HEADER_SIZE);
|
||||
}
|
||||
|
||||
/* Записать заголовки и исходные субблоки, затем вычислить r проверочных субблоков. */
|
||||
int rs_encode(struct rs_codec *ctx, uint64_t seq, const uint8_t *input, size_t length,
|
||||
uint8_t *const blocks[], size_t *block_size) {
|
||||
if (!ctx || !input || !blocks || !block_size || !length || length > rs_input_capacity(ctx)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid encode ctx=%p input=%p blocks=%p length=%zu size_out=%p",
|
||||
(void *)ctx, (const void *)input, (const void *)blocks, length, (void *)block_size);
|
||||
return RS_ERR_INVALID_ARG;
|
||||
}
|
||||
size_t payload_size = (length + ctx->k - 1) / ctx->k;
|
||||
for (unsigned i = 0; i < ctx->n; i++) {
|
||||
if (!blocks[i]) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: missing encode buffer seq=%llu subblock=%u", (unsigned long long)seq, i);
|
||||
return RS_ERR_INVALID_ARG;
|
||||
}
|
||||
}
|
||||
for (unsigned i = 0; i < ctx->n; i++) {
|
||||
for (unsigned j = 0; j < 8; j++) blocks[i][j] = (uint8_t)(seq >> (56 - 8 * j));
|
||||
blocks[i][8] = (uint8_t)i;
|
||||
uint8_t *payload = blocks[i] + RS_HEADER_SIZE;
|
||||
memset(payload, 0, payload_size);
|
||||
if (i < ctx->k) {
|
||||
size_t offset = i * payload_size;
|
||||
size_t take = offset < length ? length - offset : 0;
|
||||
if (take > payload_size) take = payload_size;
|
||||
if (take) memcpy(payload, input + offset, take);
|
||||
continue;
|
||||
}
|
||||
for (unsigned j = 0; j < ctx->k; j++) {
|
||||
uint8_t factor = ctx->generator[i * ctx->k + j];
|
||||
if (!factor) continue;
|
||||
const uint8_t *source = blocks[j] + RS_HEADER_SIZE;
|
||||
for (size_t b = 0; b < payload_size; b++) payload[b] ^= rs_multiply(ctx, factor, source[b]);
|
||||
}
|
||||
}
|
||||
*block_size = RS_HEADER_SIZE + payload_size;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "reed_solomon: encoded seq=%llu n=%u r=%u length=%zu padding=%zu block_size=%zu max=%zu",
|
||||
(unsigned long long)seq, ctx->n, ctx->n - ctx->k, length, ctx->k * payload_size - length,
|
||||
*block_size, ctx->max_block_size);
|
||||
return RS_OK;
|
||||
}
|
||||
|
||||
/* Проверить всю пачку и восстановить только отсутствующие исходные субблоки. */
|
||||
int rs_decode(struct rs_codec *ctx, const uint8_t *const blocks[], size_t block_size,
|
||||
uint8_t *output, size_t output_capacity) {
|
||||
if (!ctx || !blocks || !output || block_size <= RS_HEADER_SIZE || block_size > ctx->max_block_size
|
||||
|| output_capacity < ctx->k * (block_size - RS_HEADER_SIZE)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid decode ctx=%p blocks=%p output=%p block_size=%zu capacity=%zu",
|
||||
(void *)ctx, (const void *)blocks, (void *)output, block_size, output_capacity);
|
||||
return RS_ERR_INVALID_ARG;
|
||||
}
|
||||
size_t payload_size = block_size - RS_HEADER_SIZE;
|
||||
const uint8_t *rows[RS_MAX_BLOCKS];
|
||||
unsigned selected[RS_MAX_BLOCKS];
|
||||
unsigned count = 0;
|
||||
unsigned missing = 0;
|
||||
uint64_t seq = 0;
|
||||
for (unsigned i = 0; i < ctx->n; i++) {
|
||||
if (!blocks[i]) {
|
||||
if (i < ctx->k) missing++;
|
||||
continue;
|
||||
}
|
||||
uint64_t block_seq = 0;
|
||||
for (unsigned j = 0; j < 8; j++) block_seq = (block_seq << 8) | blocks[i][j];
|
||||
if (blocks[i][8] != i || (count && block_seq != seq)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: invalid header slot=%u subblock=%u seq=%llu expected_seq=%llu",
|
||||
i, blocks[i][8], (unsigned long long)block_seq, (unsigned long long)(count ? seq : block_seq));
|
||||
return RS_ERR_HEADER;
|
||||
}
|
||||
seq = block_seq;
|
||||
if (count < ctx->k) {
|
||||
selected[count] = i;
|
||||
rows[count] = ctx->generator + i * ctx->k;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
if (count < ctx->k) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "reed_solomon: insufficient blocks seq=%llu received=%u required=%u n=%u",
|
||||
(unsigned long long)seq, count, ctx->k, ctx->n);
|
||||
return RS_ERR_INSUFFICIENT;
|
||||
}
|
||||
if (missing) {
|
||||
int rc = rs_invert(ctx, rows);
|
||||
if (rc != RS_OK) return rc;
|
||||
}
|
||||
for (unsigned i = 0; i < ctx->k; i++) {
|
||||
uint8_t *target = output + i * payload_size;
|
||||
if (blocks[i]) {
|
||||
memcpy(target, blocks[i] + RS_HEADER_SIZE, payload_size);
|
||||
continue;
|
||||
}
|
||||
memset(target, 0, payload_size);
|
||||
for (unsigned j = 0; j < ctx->k; j++) {
|
||||
uint8_t factor = ctx->work[i * 2 * ctx->k + ctx->k + j];
|
||||
if (!factor) continue;
|
||||
const uint8_t *source = blocks[selected[j]] + RS_HEADER_SIZE;
|
||||
for (size_t b = 0; b < payload_size; b++) target[b] ^= rs_multiply(ctx, factor, source[b]);
|
||||
}
|
||||
DEBUG_TRACE(DEBUG_CATEGORY_GENERAL, "reed_solomon: recovered seq=%llu subblock=%u", (unsigned long long)seq, i);
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "reed_solomon: decoded seq=%llu received=%u recovered=%u output_size=%zu",
|
||||
(unsigned long long)seq, count, missing, ctx->k * payload_size);
|
||||
return RS_OK;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/**
|
||||
* Reed–Solomon: кодирование и восстановление одной пачки по известным пропускам.
|
||||
* rs_init задаёт n кодограмм, r проверочных и максимальный размер каждой кодограммы.
|
||||
* Первые n-r кодограмм содержат исходные данные, остальные — проверочные.
|
||||
* Формат: seq (8 байт, big-endian), subblock (1 байт), полезные данные.
|
||||
* blocks[i] всегда соответствует subblock=i; при decode NULL обозначает пропуск.
|
||||
* Любые n-r присутствующих кодограмм восстанавливают всю исходную пачку.
|
||||
* Полученные данные и заголовки должны быть проверены транспортом на повреждения.
|
||||
* Все входные/выходные буферы принадлежат вызывающему коду и не перекрываются.
|
||||
* Кодек выделяет рабочую память только при init; один контекст используют последовательно.
|
||||
* encode принимает действительную длину, выбирает минимальный одинаковый размер частей
|
||||
* и дополняет хвост максимум n-r-1 нулями. Размер и исходную длину хранит вызывающий код.
|
||||
*/
|
||||
#ifndef REED_SOLOMON_H
|
||||
#define REED_SOLOMON_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define RS_HEADER_SIZE 9
|
||||
#define RS_MAX_BLOCK_SIZE 4096
|
||||
#define RS_MAX_BLOCKS 255
|
||||
|
||||
enum rs_result {
|
||||
RS_OK = 0,
|
||||
RS_ERR_INVALID_ARG = -1,
|
||||
RS_ERR_MEMORY = -2,
|
||||
RS_ERR_INSUFFICIENT = -3,
|
||||
RS_ERR_HEADER = -4,
|
||||
RS_ERR_MATRIX = -5
|
||||
};
|
||||
|
||||
struct rs_codec;
|
||||
|
||||
/* Создать кодек: 1 <= n <= 255, 0 <= r < n, 10 <= max_block_size <= 4096.
|
||||
* ctx — выходной указатель; при ошибке *ctx=NULL. Старый контекст сначала освобождают.
|
||||
* Возвращает RS_OK или отрицательный rs_result.
|
||||
*/
|
||||
int rs_init(struct rs_codec **ctx, unsigned n, unsigned r, size_t max_block_size);
|
||||
|
||||
/* Освободить контекст и рабочую память; NULL допустим. */
|
||||
void rs_free(struct rs_codec *ctx);
|
||||
|
||||
/* Максимальная длина входа encode: (n-r)*(max_block_size-RS_HEADER_SIZE).
|
||||
* Для NULL возвращает 0 и логирует ошибку.
|
||||
*/
|
||||
size_t rs_input_capacity(const struct rs_codec *ctx);
|
||||
|
||||
/* Читать 0 < length <= rs_input_capacity(ctx) байт input, записать n кодограмм.
|
||||
* blocks — n ненулевых указателей на буферы ёмкостью max_block_size каждый.
|
||||
* block_size получает RS_HEADER_SIZE+ceil(length/(n-r)); хвост за этим размером не меняется.
|
||||
* При ошибке аргументов выходные буферы и block_size не изменяются.
|
||||
*/
|
||||
int rs_encode(struct rs_codec *ctx, uint64_t seq, const uint8_t *input, size_t length,
|
||||
uint8_t *const blocks[], size_t *block_size);
|
||||
|
||||
/* Восстановить (n-r)*(block_size-RS_HEADER_SIZE) байт, включая нулевой хвост.
|
||||
* Ненулевые буферы содержат block_size байт; NULL обозначает пропуск. Размер общий для пачки.
|
||||
* output_capacity проверяется до записи; block_size не превышает максимум контекста.
|
||||
* Проверяет одинаковый seq и subblock=i у всех присутствующих кодограмм.
|
||||
* Входные буферы не изменяются. При ошибке output не изменяется.
|
||||
*/
|
||||
int rs_decode(struct rs_codec *ctx, const uint8_t *const blocks[], size_t block_size,
|
||||
uint8_t *output, size_t output_capacity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
+4
-7
@@ -22,20 +22,17 @@
|
||||
* • Все динамические данные (строки, массивы, списки) выделяются через u_malloc.
|
||||
* • При encode длина переменных полей кодируется всегда 2 байтами (uint16_t, big-endian).
|
||||
* • Для linked list в буфере сохраняется только количество узлов + сырые данные узлов
|
||||
* (включая байты next/padding; decode заменяет next восстановленными ссылками).
|
||||
* (next-указатели НЕ сериализуются, они восстанавливаются при decode).
|
||||
* • serialize_decode принимает буфер БЕЗ заголовка.
|
||||
* • serialize_free освобождает ВСЮ структуру и все вложенные динамические объекты.
|
||||
* • Поля кодируются в порядке schema.fields; фиксированные данные копируются без смены endian.
|
||||
* • max_size (если > 0) — лимит encode; header при header_len > 0 обязателен.
|
||||
* • Переменные counts ограничены 65535; elem_size=1 добавляет NUL и для byte-массивов.
|
||||
* • Linked nodes копируются целиком, без рекурсивного кодирования вложенных указателей.
|
||||
* • Этот формат зависит от ABI сырых полей; сериализация не нормализует размер/выравнивание структур.
|
||||
* • Поля в schema.fields должны идти в порядке возрастания offset.
|
||||
* • max_size (если > 0) — жёсткий лимит размера выходного буфера.
|
||||
*
|
||||
* @example
|
||||
* // 1. Описание структуры
|
||||
* typedef struct Node {
|
||||
* uint32_t value;
|
||||
* struct Node* next; // положение задаётся offsetof(Node, next)
|
||||
* struct Node* next; // next по offset = 4
|
||||
* } Node;
|
||||
*
|
||||
* typedef struct {
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
#include "silero_vad.h"
|
||||
#include "debug_config.h"
|
||||
#include "mem.h"
|
||||
|
||||
#ifdef HAVE_SILERO_VAD
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <onnxruntime/onnxruntime_c_api.h>
|
||||
|
||||
#include "silero_vad_model.inc"
|
||||
|
||||
/* Число float-элементов рекуррентного состояния модели: 2 * 1 * 128 */
|
||||
#define SILERO_VAD_STATE_COUNT 256
|
||||
/* ONNX-вход: 64 отсчёта предыдущего окна + 512 новых отсчётов @16кГц. */
|
||||
#define SILERO_VAD_CONTEXT_SAMPLES 64
|
||||
#define SILERO_VAD_INPUT_SAMPLES (SILERO_VAD_CONTEXT_SAMPLES + SILERO_VAD_WINDOW_SAMPLES)
|
||||
|
||||
struct silero_vad {
|
||||
const OrtApi* ort;
|
||||
OrtEnv* env;
|
||||
OrtSession* session;
|
||||
OrtMemoryInfo* mem_info;
|
||||
float state[SILERO_VAD_STATE_COUNT]; /* рекуррентное состояние между вызовами */
|
||||
float context[SILERO_VAD_CONTEXT_SAMPLES]; /* звуковой контекст предыдущего окна */
|
||||
uint8_t* model_data; /* байты .onnx, загруженные в память (для file-версии) */
|
||||
size_t model_len;
|
||||
};
|
||||
|
||||
/* Освободить OrtStatus и залогировать его сообщение как ошибку */
|
||||
static void vad_report_ort_error(const OrtApi* ort, OrtStatus* status, const char* what) {
|
||||
const char* msg = ort->GetErrorMessage(status);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "%s: %s", what, msg ? msg : "unknown onnxruntime error");
|
||||
ort->ReleaseStatus(status);
|
||||
}
|
||||
|
||||
/* Прочитать файл целиком в буфер. 0 при успехе: буфер пишется в out/out_len (u_malloc). */
|
||||
static int vad_read_file(const char* path, uint8_t** out, size_t* out_len) {
|
||||
FILE* f;
|
||||
long size;
|
||||
uint8_t* buf;
|
||||
|
||||
*out = NULL;
|
||||
*out_len = 0;
|
||||
|
||||
f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: cannot open model '%s'", path);
|
||||
return -1;
|
||||
}
|
||||
if (fseek(f, 0, SEEK_END) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: fseek failed on '%s'", path);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
size = ftell(f);
|
||||
if (size <= 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: empty or unreadable model '%s'", path);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
rewind(f);
|
||||
|
||||
buf = (uint8_t*)u_malloc((size_t)size);
|
||||
if (!buf) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: OOM for %ld bytes", size);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
if (fread(buf, 1, (size_t)size, f) != (size_t)size) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: short read on '%s'", path);
|
||||
u_free(buf);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
*out = buf;
|
||||
*out_len = (size_t)size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Общая инициализация детектора из байтов модели (файл или вшитая модель).
|
||||
* data может указывать на внешний буфер (вшитая модель) — тогда не освобождается. */
|
||||
static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t len,
|
||||
const uint8_t* owned_buf, const char* label) {
|
||||
silero_vad_t* vad = NULL;
|
||||
const OrtApi* ort;
|
||||
OrtStatus* status;
|
||||
OrtSessionOptions* opts = NULL;
|
||||
|
||||
if (!data || len == 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): empty model", label);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ort = OrtGetApiBase() ? OrtGetApiBase()->GetApi(ORT_API_VERSION) : NULL;
|
||||
if (!ort) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): onnxruntime C API unavailable", label);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
vad = (silero_vad_t*)u_calloc(1, sizeof(*vad));
|
||||
if (!vad) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): OOM", label);
|
||||
return NULL;
|
||||
}
|
||||
vad->ort = ort;
|
||||
vad->model_data = (uint8_t*)owned_buf;
|
||||
vad->model_len = len;
|
||||
|
||||
status = ort->CreateEnv(ORT_LOGGING_LEVEL_WARNING, "utun_silero_vad", &vad->env);
|
||||
if (status) { vad_report_ort_error(ort, status, "CreateEnv"); goto fail; }
|
||||
|
||||
status = ort->CreateSessionOptions(&opts);
|
||||
if (status) { vad_report_ort_error(ort, status, "CreateSessionOptions"); goto fail; }
|
||||
status = ort->SetIntraOpNumThreads(opts, 1);
|
||||
if (status) { vad_report_ort_error(ort, status, "SetIntraOpNumThreads"); }
|
||||
status = ort->SetSessionGraphOptimizationLevel(opts, ORT_ENABLE_ALL);
|
||||
if (status) { vad_report_ort_error(ort, status, "SetSessionGraphOptimizationLevel"); }
|
||||
|
||||
status = ort->CreateSessionFromArray(vad->env, data, len, opts, &vad->session);
|
||||
if (status) { vad_report_ort_error(ort, status, "CreateSessionFromArray"); goto fail; }
|
||||
|
||||
status = ort->CreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeDefault, &vad->mem_info);
|
||||
if (status) { vad_report_ort_error(ort, status, "CreateCpuMemoryInfo"); goto fail; }
|
||||
|
||||
ort->ReleaseSessionOptions(opts);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): ok bytes=%zu window=%d context=%d input=%d", label, len,
|
||||
SILERO_VAD_WINDOW_SAMPLES, SILERO_VAD_CONTEXT_SAMPLES, SILERO_VAD_INPUT_SAMPLES);
|
||||
return vad;
|
||||
|
||||
fail:
|
||||
if (opts) ort->ReleaseSessionOptions(opts);
|
||||
silero_vad_destroy(vad);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
silero_vad_t* silero_vad_create(const char* model_path) {
|
||||
uint8_t* buf = NULL;
|
||||
size_t len = 0;
|
||||
silero_vad_t* vad;
|
||||
|
||||
if (!model_path || !*model_path) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create: empty model path");
|
||||
return NULL;
|
||||
}
|
||||
if (vad_read_file(model_path, &buf, &len) != 0) return NULL;
|
||||
vad = silero_vad_create_from_bytes(buf, len, buf, model_path);
|
||||
if (!vad) u_free(buf);
|
||||
return vad;
|
||||
}
|
||||
|
||||
silero_vad_t* silero_vad_create_default(void) {
|
||||
return silero_vad_create_from_bytes(silero_vad_model_data, silero_vad_model_len, NULL, "embedded");
|
||||
}
|
||||
|
||||
void silero_vad_destroy(silero_vad_t* vad) {
|
||||
if (!vad) return;
|
||||
const OrtApi* ort = vad->ort;
|
||||
if (ort) {
|
||||
if (vad->mem_info) ort->ReleaseMemoryInfo(vad->mem_info);
|
||||
if (vad->session) ort->ReleaseSession(vad->session);
|
||||
if (vad->env) ort->ReleaseEnv(vad->env);
|
||||
}
|
||||
if (vad->model_data) u_free(vad->model_data);
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "silero_vad_destroy");
|
||||
u_free(vad);
|
||||
}
|
||||
|
||||
void silero_vad_reset(silero_vad_t* vad) {
|
||||
if (!vad) return;
|
||||
memset(vad->state, 0, sizeof(vad->state));
|
||||
memset(vad->context, 0, sizeof(vad->context));
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "silero_vad_reset: recurrent state and audio context cleared");
|
||||
}
|
||||
|
||||
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob) {
|
||||
const OrtApi* ort;
|
||||
OrtStatus* status;
|
||||
OrtValue* in_input = NULL;
|
||||
OrtValue* in_state = NULL;
|
||||
OrtValue* in_sr = NULL;
|
||||
OrtValue* out_prob = NULL;
|
||||
OrtValue* out_state = NULL;
|
||||
const char* input_names[3];
|
||||
const char* output_names[2];
|
||||
const OrtValue* inputs[3];
|
||||
OrtValue* outputs[2];
|
||||
int64_t shape_input[2] = {1, SILERO_VAD_INPUT_SAMPLES};
|
||||
int64_t shape_state[3] = {2, 1, 128};
|
||||
int64_t shape_prob[2] = {1, 1};
|
||||
int64_t sr_value = SILERO_VAD_SAMPLE_RATE;
|
||||
float prob_local = 0.0f;
|
||||
float next_state[SILERO_VAD_STATE_COUNT];
|
||||
float input[SILERO_VAD_INPUT_SAMPLES];
|
||||
int rc = -1;
|
||||
|
||||
if (!vad || !samples || !prob) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_process: invalid args");
|
||||
return -1;
|
||||
}
|
||||
ort = vad->ort;
|
||||
memcpy(input, vad->context, sizeof(vad->context));
|
||||
memcpy(input + SILERO_VAD_CONTEXT_SAMPLES, samples, SILERO_VAD_WINDOW_SAMPLES * sizeof(float));
|
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, input,
|
||||
sizeof(input), shape_input, 2,
|
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &in_input);
|
||||
if (status) { vad_report_ort_error(ort, status, "input tensor"); return -1; }
|
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, vad->state,
|
||||
SILERO_VAD_STATE_COUNT * sizeof(float), shape_state, 3,
|
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &in_state);
|
||||
if (status) { vad_report_ort_error(ort, status, "state tensor"); goto done; }
|
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, &sr_value,
|
||||
sizeof(sr_value), NULL, 0,
|
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64, &in_sr);
|
||||
if (status) { vad_report_ort_error(ort, status, "sr tensor"); goto done; }
|
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, &prob_local,
|
||||
sizeof(float), shape_prob, 2,
|
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &out_prob);
|
||||
if (status) { vad_report_ort_error(ort, status, "output tensor"); goto done; }
|
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, next_state,
|
||||
SILERO_VAD_STATE_COUNT * sizeof(float), shape_state, 3,
|
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &out_state);
|
||||
if (status) { vad_report_ort_error(ort, status, "stateN tensor"); goto done; }
|
||||
|
||||
input_names[0] = "input";
|
||||
input_names[1] = "state";
|
||||
input_names[2] = "sr";
|
||||
inputs[0] = in_input;
|
||||
inputs[1] = in_state;
|
||||
inputs[2] = in_sr;
|
||||
|
||||
output_names[0] = "output";
|
||||
output_names[1] = "stateN";
|
||||
outputs[0] = out_prob;
|
||||
outputs[1] = out_state;
|
||||
|
||||
status = ort->Run(vad->session, NULL, input_names, inputs, 3, output_names, 2, outputs);
|
||||
if (status) {
|
||||
vad_report_ort_error(ort, status, "Run");
|
||||
goto done;
|
||||
}
|
||||
|
||||
memcpy(vad->state, next_state, sizeof(next_state));
|
||||
memcpy(vad->context, input + SILERO_VAD_INPUT_SAMPLES - SILERO_VAD_CONTEXT_SAMPLES, sizeof(vad->context));
|
||||
*prob = prob_local;
|
||||
rc = 0;
|
||||
|
||||
done:
|
||||
if (in_input) ort->ReleaseValue(in_input);
|
||||
if (in_state) ort->ReleaseValue(in_state);
|
||||
if (in_sr) ort->ReleaseValue(in_sr);
|
||||
if (out_prob) ort->ReleaseValue(out_prob);
|
||||
if (out_state) ort->ReleaseValue(out_state);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob) {
|
||||
float samples[SILERO_VAD_WINDOW_SAMPLES];
|
||||
int i;
|
||||
|
||||
if (!pcm) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_process_pcm16: pcm == NULL");
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; i < SILERO_VAD_WINDOW_SAMPLES; i++) {
|
||||
samples[i] = (float)pcm[i] / 32768.0f;
|
||||
}
|
||||
return silero_vad_process(vad, samples, prob);
|
||||
}
|
||||
|
||||
#else /* !HAVE_SILERO_VAD — пустые стабы (сборки без onnxruntime) */
|
||||
|
||||
silero_vad_t* silero_vad_create(const char* model_path) {
|
||||
(void)model_path;
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create: built without onnxruntime (HAVE_SILERO_VAD not defined)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
silero_vad_t* silero_vad_create_default(void) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create_default: built without onnxruntime (HAVE_SILERO_VAD not defined)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void silero_vad_destroy(silero_vad_t* vad) {
|
||||
(void)vad;
|
||||
}
|
||||
|
||||
void silero_vad_reset(silero_vad_t* vad) {
|
||||
(void)vad;
|
||||
}
|
||||
|
||||
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob) {
|
||||
(void)vad; (void)samples; (void)prob;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob) {
|
||||
(void)vad; (void)pcm; (void)prob;
|
||||
return -1;
|
||||
}
|
||||
|
||||
#endif /* HAVE_SILERO_VAD */
|
||||
@@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Silero VAD (Voice Activity Detector) — C-обёртка над ONNX Runtime.
|
||||
*
|
||||
* Запускает официальную стриминговую модель silero_vad.onnx (v5, ~2.3 МБ),
|
||||
* которая по окну аудио возвращает вероятность наличия речи [0..1].
|
||||
* Рекуррентное состояние модели хранится внутри объекта и переносится между
|
||||
* вызовами. Обёртка также хранит 64 последних отсчёта аудио и добавляет их
|
||||
* перед новым окном: ONNX получает 576 отсчётов, публичный API принимает 512.
|
||||
*
|
||||
* Требования к входному аудио:
|
||||
* - частота дискретизации 16 кГц (модель поддерживает только 16k);
|
||||
* - моно;
|
||||
* - окно ровно SILERO_VAD_WINDOW_SAMPLES = 512 сэмплов (32 мс);
|
||||
* - float-сэмплы в диапазоне примерно [-1;1] (для int16 есть
|
||||
* silero_vad_process_pcm16, который сам делит на 32768).
|
||||
*
|
||||
* Порог и гистерезис (задержки начала/конца речи) намеренно оставлены
|
||||
* вызывающему — радио/звонок сами решают, как интерпретировать вероятность.
|
||||
*
|
||||
* Зависимость: libonnxruntime (C API). Сборка включается опцией
|
||||
* `--with-silero-vad` (см. configure.ac) или макросом HAVE_SILERO_VAD в
|
||||
* CMake-сборках. Без HAVE_SILERO_VAD модуль компилируется в пустые стабы
|
||||
* (все create-функции возвращают NULL, process — ошибку), чтобы исходники lib/
|
||||
* можно было GLOB-ить во всех сборках (Android/chatgui) без onnxruntime.
|
||||
*
|
||||
* Лицензия модели: MIT (Silero Team, https://github.com/snakers4/silero-vad).
|
||||
*/
|
||||
#ifndef SILERO_VAD_H
|
||||
#define SILERO_VAD_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Фиксированное окно стриминговой модели: 512 сэмплов @ 16 кГц = 32 мс */
|
||||
#define SILERO_VAD_WINDOW_SAMPLES 512
|
||||
#define SILERO_VAD_SAMPLE_RATE 16000
|
||||
|
||||
typedef struct silero_vad silero_vad_t;
|
||||
|
||||
/**
|
||||
* Создать детектор, загрузив ONNX-модель из файла model_path.
|
||||
* Возвращает NULL при ошибке (подробности — в лог категории "vad").
|
||||
*/
|
||||
silero_vad_t* silero_vad_create(const char* model_path);
|
||||
|
||||
/**
|
||||
* Создать детектор из модели, вшитой в бинарник (silero_vad_model_data в
|
||||
* silero_vad.c). Не требует файла на диске. NULL при ошибке.
|
||||
*/
|
||||
silero_vad_t* silero_vad_create_default(void);
|
||||
|
||||
/* Освободить детектор. NULL безопасен. */
|
||||
void silero_vad_destroy(silero_vad_t* vad);
|
||||
|
||||
/* Сбросить рекуррентное состояние и звуковой контекст (начало нового потока / разрыв захвата). */
|
||||
void silero_vad_reset(silero_vad_t* vad);
|
||||
|
||||
/**
|
||||
* Обработать окно из 512 float-сэмплов @ 16 кГц (примерно [-1;1]).
|
||||
* При успехе возвращает 0 и заполняет *prob вероятностью речи [0..1];
|
||||
* при ошибке — отрицательное значение (проб не трогается).
|
||||
*/
|
||||
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob);
|
||||
|
||||
/**
|
||||
* То же, но вход — int16 PCM: сэмплы преобразуются к float делением на 32768.
|
||||
*/
|
||||
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SILERO_VAD_H */
|
||||
Binary file not shown.
-136918
File diff suppressed because it is too large.
Load diff
@@ -246,16 +246,3 @@ uint16_t ss_get_port(const struct sockaddr_storage *addr) {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t socket_get_bound_port(socket_t sock) {
|
||||
struct sockaddr_storage address; socklen_t size = sizeof(address);
|
||||
if (getsockname(sock, (struct sockaddr*)&address, &size) != 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "getsockname fd=%llu failed: %s",
|
||||
(unsigned long long)sock, socket_strerror(socket_get_error()));
|
||||
return 0;
|
||||
}
|
||||
uint16_t port = ss_get_port(&address);
|
||||
if (!port) DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "socket fd=%llu family=%d has no bound port",
|
||||
(unsigned long long)sock, address.ss_family);
|
||||
return port;
|
||||
}
|
||||
+2
-5
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Единые socket_t, коды ошибок и wrappers сокетов POSIX/Windows (включая MSYS2 UCRT64).
|
||||
* platform_init/cleanup обслуживают Winsock; UDP/nonblocking/options/sendto/recvfrom — операции ОС.
|
||||
* Созданные сокеты закрывает владелец через socket_close_wrapper; event loop находится в u_async.h.
|
||||
* Socket compatibility layer for cross-platform support (POSIX / Windows)
|
||||
* MSYS2 UCRT64 compatible
|
||||
*/
|
||||
|
||||
#ifndef SOCKET_COMPAT_H
|
||||
@@ -78,8 +77,6 @@ const char* socket_strerror(int err);
|
||||
|
||||
// взять порт из sockaddr_storage
|
||||
uint16_t ss_get_port(const struct sockaddr_storage *addr);
|
||||
// Фактический IPv4/IPv6-порт уже привязанного сокета; 0 при ошибке.
|
||||
uint16_t socket_get_bound_port(socket_t sock);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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
|
||||
@@ -1,131 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(SoundTouch VERSION 2.3.3 LANGUAGES CXX)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set(COMPILE_OPTIONS)
|
||||
|
||||
if(MSVC)
|
||||
list(APPEND COMPILE_OPTIONS /O2 /fp:fast)
|
||||
else()
|
||||
list(APPEND COMPILE_OPTIONS -Wall -Wextra -Wzero-as-null-pointer-constant -Wno-unknown-pragmas)
|
||||
if(EMSCRIPTEN)
|
||||
list(APPEND COMPILE_OPTIONS -O3)
|
||||
else()
|
||||
list(APPEND COMPILE_OPTIONS -Ofast)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(CMAKE_C_VISIBILITY_PRESET hidden)
|
||||
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
|
||||
|
||||
#####################
|
||||
# SoundTouch library
|
||||
|
||||
add_library(SoundTouch STATIC
|
||||
source/SoundTouch/AAFilter.cpp
|
||||
source/SoundTouch/BPMDetect.cpp
|
||||
source/SoundTouch/cpu_detect_x86.cpp
|
||||
source/SoundTouch/FIFOSampleBuffer.cpp
|
||||
source/SoundTouch/FIRFilter.cpp
|
||||
source/SoundTouch/InterpolateCubic.cpp
|
||||
source/SoundTouch/InterpolateLinear.cpp
|
||||
source/SoundTouch/InterpolateShannon.cpp
|
||||
source/SoundTouch/mmx_optimized.cpp
|
||||
source/SoundTouch/PeakFinder.cpp
|
||||
source/SoundTouch/RateTransposer.cpp
|
||||
source/SoundTouch/SoundTouch.cpp
|
||||
source/SoundTouch/sse_optimized.cpp
|
||||
source/SoundTouch/TDStretch.cpp
|
||||
)
|
||||
target_include_directories(SoundTouch PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
|
||||
)
|
||||
target_include_directories(SoundTouch PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/soundtouch
|
||||
)
|
||||
|
||||
target_compile_definitions(SoundTouch PRIVATE ${COMPILE_DEFINITIONS})
|
||||
target_compile_options(SoundTouch PRIVATE ${COMPILE_OPTIONS})
|
||||
set_target_properties(SoundTouch PROPERTIES OUTPUT_NAME "soundtouch-static")
|
||||
|
||||
option(INTEGER_SAMPLES "Use integers instead of floats for samples" OFF)
|
||||
if(INTEGER_SAMPLES)
|
||||
target_compile_definitions(SoundTouch PRIVATE SOUNDTOUCH_INTEGER_SAMPLES)
|
||||
set(SOUNDTOUCH_INTEGER_SAMPLES TRUE)
|
||||
else()
|
||||
target_compile_definitions(SoundTouch PRIVATE SOUNDTOUCH_FLOAT_SAMPLES)
|
||||
set(SOUNDTOUCH_FLOAT_SAMPLES TRUE)
|
||||
endif()
|
||||
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv7.*)$")
|
||||
target_compile_options(SoundTouch PRIVATE -mfpu=neon)
|
||||
endif()
|
||||
|
||||
install(
|
||||
FILES
|
||||
include/soundtouch/BPMDetect.h
|
||||
include/soundtouch/FIFOSampleBuffer.h
|
||||
include/soundtouch/FIFOSamplePipe.h
|
||||
include/soundtouch/STTypes.h
|
||||
include/soundtouch/SoundTouch.h
|
||||
DESTINATION
|
||||
"${CMAKE_INSTALL_INCLUDEDIR}/soundtouch"
|
||||
)
|
||||
|
||||
|
||||
########################
|
||||
# SoundTouchDll library
|
||||
|
||||
option(SOUNDTOUCH_DLL "Build SoundTouchDLL C wrapper library" ON)
|
||||
if(SOUNDTOUCH_DLL)
|
||||
add_library(SoundTouchDLL SHARED
|
||||
source/SoundTouchDLL/SoundTouchDLL.cpp
|
||||
source/SoundTouchDLL/SoundTouchDLL.rc
|
||||
)
|
||||
set_target_properties(SoundTouch PROPERTIES POSITION_INDEPENDENT_CODE TRUE)
|
||||
target_compile_options(SoundTouchDLL PRIVATE ${COMPILE_OPTIONS})
|
||||
set_target_properties(SoundTouchDLL PROPERTIES CXX_VISIBILITY_PRESET hidden)
|
||||
target_compile_definitions(SoundTouchDLL PRIVATE DLL_EXPORTS)
|
||||
target_include_directories(SoundTouchDLL INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
|
||||
target_include_directories(SoundTouchDLL PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include/soundtouch")
|
||||
target_link_libraries(SoundTouchDLL PRIVATE SoundTouch)
|
||||
set_target_properties(SoundTouchDLL PROPERTIES OUTPUT_NAME "soundtouch")
|
||||
if(NOT ANDROID)
|
||||
set_target_properties(SoundTouchDLL PROPERTIES VERSION ${CMAKE_PROJECT_VERSION})
|
||||
if(NOT WIN32)
|
||||
set_target_properties(SoundTouchDLL PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR})
|
||||
endif()
|
||||
endif()
|
||||
install(FILES include/soundtouch/SoundTouchDLL.h DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/soundtouch")
|
||||
install(TARGETS SoundTouchDLL EXPORT SoundTouchTargets)
|
||||
endif()
|
||||
|
||||
########################
|
||||
|
||||
# CMake config
|
||||
include(CMakePackageConfigHelpers)
|
||||
set(SOUNDTOUCH_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/SoundTouch")
|
||||
install(
|
||||
EXPORT SoundTouchTargets
|
||||
FILE SoundTouchTargets.cmake
|
||||
NAMESPACE SoundTouch::
|
||||
DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}"
|
||||
)
|
||||
configure_package_config_file(SoundTouchConfig.cmake.in
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfig.cmake"
|
||||
INSTALL_DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}"
|
||||
)
|
||||
write_basic_package_version_file(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfigVersion.cmake"
|
||||
VERSION "${CMAKE_PROJECT_VERSION}"
|
||||
COMPATIBILITY SameMajorVersion
|
||||
)
|
||||
install(
|
||||
FILES
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfig.cmake"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfigVersion.cmake"
|
||||
DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}"
|
||||
)
|
||||
@@ -1,458 +0,0 @@
|
||||
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
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,11 +0,0 @@
|
||||
@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()
|
||||
@@ -1,205 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Beats-per-minute (BPM) detection routine.
|
||||
///
|
||||
/// The beat detection algorithm works as follows:
|
||||
/// - Use function 'inputSamples' to input a chunks of samples to the class for
|
||||
/// analysis. It's a good idea to enter a large sound file or stream in smallish
|
||||
/// chunks of around few kilosamples in order not to extinguish too much RAM memory.
|
||||
/// - Input sound data is decimated to approx 500 Hz to reduce calculation burden,
|
||||
/// which is basically ok as low (bass) frequencies mostly determine the beat rate.
|
||||
/// Simple averaging is used for anti-alias filtering because the resulting signal
|
||||
/// quality isn't of that high importance.
|
||||
/// - Decimated sound data is enveloped, i.e. the amplitude shape is detected by
|
||||
/// taking absolute value that's smoothed by sliding average. Signal levels that
|
||||
/// are below a couple of times the general RMS amplitude level are cut away to
|
||||
/// leave only notable peaks there.
|
||||
/// - Repeating sound patterns (e.g. beats) are detected by calculating short-term
|
||||
/// autocorrelation function of the enveloped signal.
|
||||
/// - After whole sound data file has been analyzed as above, the bpm level is
|
||||
/// detected by function 'getBpm' that finds the highest peak of the autocorrelation
|
||||
/// function, calculates it's precise location and converts this reading to bpm's.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef _BPMDetect_H_
|
||||
#define _BPMDetect_H_
|
||||
|
||||
#include <vector>
|
||||
#include "STTypes.h"
|
||||
#include "FIFOSampleBuffer.h"
|
||||
|
||||
namespace soundtouch
|
||||
{
|
||||
|
||||
/// Minimum allowed BPM rate. Used to restrict accepted result above a reasonable limit.
|
||||
#define MIN_BPM 45
|
||||
|
||||
/// Maximum allowed BPM rate range. Used for calculating algorithm parametrs
|
||||
#define MAX_BPM_RANGE 200
|
||||
|
||||
/// Maximum allowed BPM rate range. Used to restrict accepted result below a reasonable limit.
|
||||
#define MAX_BPM_VALID 190
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
typedef struct
|
||||
{
|
||||
float pos;
|
||||
float strength;
|
||||
} BEAT;
|
||||
|
||||
|
||||
class IIR2_filter
|
||||
{
|
||||
double coeffs[5];
|
||||
double prev[5];
|
||||
|
||||
public:
|
||||
IIR2_filter(const double *lpf_coeffs);
|
||||
float update(float x);
|
||||
};
|
||||
|
||||
|
||||
/// Class for calculating BPM rate for audio data.
|
||||
class BPMDetect
|
||||
{
|
||||
protected:
|
||||
/// Auto-correlation accumulator bins.
|
||||
float *xcorr;
|
||||
|
||||
/// Sample average counter.
|
||||
int decimateCount;
|
||||
|
||||
/// Sample average accumulator for FIFO-like decimation.
|
||||
soundtouch::LONG_SAMPLETYPE decimateSum;
|
||||
|
||||
/// Decimate sound by this coefficient to reach approx. 500 Hz.
|
||||
int decimateBy;
|
||||
|
||||
/// Auto-correlation window length
|
||||
int windowLen;
|
||||
|
||||
/// Number of channels (1 = mono, 2 = stereo)
|
||||
int channels;
|
||||
|
||||
/// sample rate
|
||||
int sampleRate;
|
||||
|
||||
/// Beginning of auto-correlation window: Autocorrelation isn't being updated for
|
||||
/// the first these many correlation bins.
|
||||
int windowStart;
|
||||
|
||||
/// window functions for data preconditioning
|
||||
float *hamw;
|
||||
float *hamw2;
|
||||
|
||||
// beat detection variables
|
||||
int pos;
|
||||
int peakPos;
|
||||
int beatcorr_ringbuffpos;
|
||||
int init_scaler;
|
||||
float peakVal;
|
||||
float *beatcorr_ringbuff;
|
||||
|
||||
/// FIFO-buffer for decimated processing samples.
|
||||
soundtouch::FIFOSampleBuffer *buffer;
|
||||
|
||||
/// Collection of detected beat positions
|
||||
//BeatCollection beats;
|
||||
std::vector<BEAT> beats;
|
||||
|
||||
// 2nd order low-pass-filter
|
||||
IIR2_filter beat_lpf;
|
||||
|
||||
/// Updates auto-correlation function for given number of decimated samples that
|
||||
/// are read from the internal 'buffer' pipe (samples aren't removed from the pipe
|
||||
/// though).
|
||||
void updateXCorr(int process_samples /// How many samples are processed.
|
||||
);
|
||||
|
||||
/// Decimates samples to approx. 500 Hz.
|
||||
///
|
||||
/// \return Number of output samples.
|
||||
int decimate(soundtouch::SAMPLETYPE *dest, ///< Destination buffer
|
||||
const soundtouch::SAMPLETYPE *src, ///< Source sample buffer
|
||||
int numsamples ///< Number of source samples.
|
||||
);
|
||||
|
||||
/// Calculates amplitude envelope for the buffer of samples.
|
||||
/// Result is output to 'samples'.
|
||||
void calcEnvelope(soundtouch::SAMPLETYPE *samples, ///< Pointer to input/output data buffer
|
||||
int numsamples ///< Number of samples in buffer
|
||||
);
|
||||
|
||||
/// remove constant bias from xcorr data
|
||||
void removeBias();
|
||||
|
||||
// Detect individual beat positions
|
||||
void updateBeatPos(int process_samples);
|
||||
|
||||
|
||||
public:
|
||||
/// Constructor.
|
||||
BPMDetect(int numChannels, ///< Number of channels in sample data.
|
||||
int sampleRate ///< Sample rate in Hz.
|
||||
);
|
||||
|
||||
/// Destructor.
|
||||
virtual ~BPMDetect();
|
||||
|
||||
/// Inputs a block of samples for analyzing: Envelopes the samples and then
|
||||
/// updates the autocorrelation estimation. When whole song data has been input
|
||||
/// in smaller blocks using this function, read the resulting bpm with 'getBpm'
|
||||
/// function.
|
||||
///
|
||||
/// Notice that data in 'samples' array can be disrupted in processing.
|
||||
void inputSamples(const soundtouch::SAMPLETYPE *samples, ///< Pointer to input/working data buffer
|
||||
int numSamples ///< Number of samples in buffer
|
||||
);
|
||||
|
||||
/// Analyzes the results and returns the BPM rate. Use this function to read result
|
||||
/// after whole song data has been input to the class by consecutive calls of
|
||||
/// 'inputSamples' function.
|
||||
///
|
||||
/// \return Beats-per-minute rate, or zero if detection failed.
|
||||
float getBpm();
|
||||
|
||||
/// Get beat position arrays. Note: The array includes also really low beat detection values
|
||||
/// in absence of clear strong beats. Consumer may wish to filter low values away.
|
||||
/// - "pos" receive array of beat positions
|
||||
/// - "values" receive array of beat detection strengths
|
||||
/// - max_num indicates max.size of "pos" and "values" array.
|
||||
///
|
||||
/// You can query a suitable array sized by calling this with nullptr in "pos" & "values".
|
||||
///
|
||||
/// \return number of beats in the arrays.
|
||||
int getBeats(float *pos, float *strength, int max_num);
|
||||
};
|
||||
}
|
||||
#endif // _BPMDetect_H_
|
||||
@@ -1,180 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// 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
|
||||
@@ -1,230 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// 'FIFOSamplePipe' : An abstract base class for classes that manipulate sound
|
||||
/// samples by operating like a first-in-first-out pipe: New samples are fed
|
||||
/// into one end of the pipe with the 'putSamples' function, and the processed
|
||||
/// samples are received from the other end with the 'receiveSamples' function.
|
||||
///
|
||||
/// 'FIFOProcessor' : A base class for classes the do signal processing with
|
||||
/// the samples while operating like a first-in-first-out pipe. When samples
|
||||
/// are input with the 'putSamples' function, the class processes them
|
||||
/// and moves the processed samples to the given 'output' pipe object, which
|
||||
/// may be either another processing stage, or a fifo sample buffer object.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef FIFOSamplePipe_H
|
||||
#define FIFOSamplePipe_H
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include "STTypes.h"
|
||||
|
||||
namespace soundtouch
|
||||
{
|
||||
|
||||
/// Abstract base class for FIFO (first-in-first-out) sample processing classes.
|
||||
class FIFOSamplePipe
|
||||
{
|
||||
protected:
|
||||
|
||||
bool verifyNumberOfChannels(int nChannels) const
|
||||
{
|
||||
if ((nChannels > 0) && (nChannels <= SOUNDTOUCH_MAX_CHANNELS))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
ST_THROW_RT_ERROR("Error: Illegal number of channels");
|
||||
return false;
|
||||
}
|
||||
|
||||
public:
|
||||
// virtual default destructor
|
||||
virtual ~FIFOSamplePipe() {}
|
||||
|
||||
|
||||
/// Returns a pointer to the beginning of the output samples.
|
||||
/// This function is provided for accessing the output samples directly.
|
||||
/// Please be careful for not to corrupt the book-keeping!
|
||||
///
|
||||
/// When using this function to output samples, also remember to 'remove' the
|
||||
/// output samples from the buffer by calling the
|
||||
/// 'receiveSamples(numSamples)' function
|
||||
virtual SAMPLETYPE *ptrBegin() = 0;
|
||||
|
||||
/// Adds 'numSamples' pcs of samples from the 'samples' memory position to
|
||||
/// the sample buffer.
|
||||
virtual void putSamples(const SAMPLETYPE *samples, ///< Pointer to samples.
|
||||
uint numSamples ///< Number of samples to insert.
|
||||
) = 0;
|
||||
|
||||
|
||||
// Moves samples from the 'other' pipe instance to this instance.
|
||||
void moveSamples(FIFOSamplePipe &other ///< Other pipe instance where from the receive the data.
|
||||
)
|
||||
{
|
||||
const uint oNumSamples = other.numSamples();
|
||||
|
||||
putSamples(other.ptrBegin(), oNumSamples);
|
||||
other.receiveSamples(oNumSamples);
|
||||
}
|
||||
|
||||
/// Output samples from beginning of the sample buffer. Copies requested samples to
|
||||
/// output buffer and removes them from the sample buffer. If there are less than
|
||||
/// 'numsample' samples in the buffer, returns all that available.
|
||||
///
|
||||
/// \return Number of samples returned.
|
||||
virtual uint receiveSamples(SAMPLETYPE *output, ///< Buffer where to copy output samples.
|
||||
uint maxSamples ///< How many samples to receive at max.
|
||||
) = 0;
|
||||
|
||||
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
|
||||
/// sample buffer without copying them anywhere.
|
||||
///
|
||||
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
|
||||
/// with 'ptrBegin' function.
|
||||
virtual uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
|
||||
) = 0;
|
||||
|
||||
/// Returns number of samples currently available.
|
||||
virtual uint numSamples() const = 0;
|
||||
|
||||
// Returns nonzero if there aren't any samples available for outputting.
|
||||
virtual int isEmpty() const = 0;
|
||||
|
||||
/// Clears all the samples.
|
||||
virtual void clear() = 0;
|
||||
|
||||
/// allow trimming (downwards) amount of samples in pipeline.
|
||||
/// Returns adjusted amount of samples
|
||||
virtual uint adjustAmountOfSamples(uint numSamples) = 0;
|
||||
|
||||
};
|
||||
|
||||
|
||||
/// Base-class for sound processing routines working in FIFO principle. With this base
|
||||
/// class it's easy to implement sound processing stages that can be chained together,
|
||||
/// so that samples that are fed into beginning of the pipe automatically go through
|
||||
/// all the processing stages.
|
||||
///
|
||||
/// When samples are input to this class, they're first processed and then put to
|
||||
/// the FIFO pipe that's defined as output of this class. This output pipe can be
|
||||
/// either other processing stage or a FIFO sample buffer.
|
||||
class FIFOProcessor :public FIFOSamplePipe
|
||||
{
|
||||
protected:
|
||||
/// Internal pipe where processed samples are put.
|
||||
FIFOSamplePipe *output;
|
||||
|
||||
/// Sets output pipe.
|
||||
void setOutPipe(FIFOSamplePipe *pOutput)
|
||||
{
|
||||
assert(output == nullptr);
|
||||
assert(pOutput != nullptr);
|
||||
output = pOutput;
|
||||
}
|
||||
|
||||
/// Constructor. Doesn't define output pipe; it has to be set be
|
||||
/// 'setOutPipe' function.
|
||||
FIFOProcessor()
|
||||
{
|
||||
output = nullptr;
|
||||
}
|
||||
|
||||
/// Constructor. Configures output pipe.
|
||||
FIFOProcessor(FIFOSamplePipe *pOutput ///< Output pipe.
|
||||
)
|
||||
{
|
||||
output = pOutput;
|
||||
}
|
||||
|
||||
/// Destructor.
|
||||
virtual ~FIFOProcessor() override
|
||||
{
|
||||
}
|
||||
|
||||
/// Returns a pointer to the beginning of the output samples.
|
||||
/// This function is provided for accessing the output samples directly.
|
||||
/// Please be careful for not to corrupt the book-keeping!
|
||||
///
|
||||
/// When using this function to output samples, also remember to 'remove' the
|
||||
/// output samples from the buffer by calling the
|
||||
/// 'receiveSamples(numSamples)' function
|
||||
virtual SAMPLETYPE *ptrBegin() override
|
||||
{
|
||||
return output->ptrBegin();
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
/// Output samples from beginning of the sample buffer. Copies requested samples to
|
||||
/// output buffer and removes them from the sample buffer. If there are less than
|
||||
/// 'numsample' samples in the buffer, returns all that available.
|
||||
///
|
||||
/// \return Number of samples returned.
|
||||
virtual uint receiveSamples(SAMPLETYPE *outBuffer, ///< Buffer where to copy output samples.
|
||||
uint maxSamples ///< How many samples to receive at max.
|
||||
) override
|
||||
{
|
||||
return output->receiveSamples(outBuffer, maxSamples);
|
||||
}
|
||||
|
||||
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
|
||||
/// sample buffer without copying them anywhere.
|
||||
///
|
||||
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
|
||||
/// with 'ptrBegin' function.
|
||||
virtual uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
|
||||
) override
|
||||
{
|
||||
return output->receiveSamples(maxSamples);
|
||||
}
|
||||
|
||||
/// Returns number of samples currently available.
|
||||
virtual uint numSamples() const override
|
||||
{
|
||||
return output->numSamples();
|
||||
}
|
||||
|
||||
/// Returns nonzero if there aren't any samples available for outputting.
|
||||
virtual int isEmpty() const override
|
||||
{
|
||||
return output->isEmpty();
|
||||
}
|
||||
|
||||
/// allow trimming (downwards) amount of samples in pipeline.
|
||||
/// Returns adjusted amount of samples
|
||||
virtual uint adjustAmountOfSamples(uint numSamples) override
|
||||
{
|
||||
return output->adjustAmountOfSamples(numSamples);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,176 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Common type definitions for SoundTouch audio processing library.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef STTypes_H
|
||||
#define STTypes_H
|
||||
|
||||
typedef unsigned int uint;
|
||||
typedef unsigned long ulong;
|
||||
|
||||
// Patch for MinGW: on Win64 long is 32-bit
|
||||
#ifdef _WIN64
|
||||
typedef unsigned long long ulongptr;
|
||||
#else
|
||||
typedef ulong ulongptr;
|
||||
#endif
|
||||
|
||||
|
||||
// Helper macro for aligning pointer up to next 16-byte boundary
|
||||
#define SOUNDTOUCH_ALIGN_POINTER_16(x) ( ( (ulongptr)(x) + 15 ) & ~(ulongptr)15 )
|
||||
|
||||
|
||||
namespace soundtouch
|
||||
{
|
||||
/// Max allowed number of channels
|
||||
#define SOUNDTOUCH_MAX_CHANNELS 16
|
||||
|
||||
/// Activate these undef's to overrule the possible sampletype
|
||||
/// setting inherited from some other header file:
|
||||
//#undef SOUNDTOUCH_INTEGER_SAMPLES
|
||||
//#undef SOUNDTOUCH_FLOAT_SAMPLES
|
||||
|
||||
/// If following flag is defined, always uses multichannel processing
|
||||
/// routines also for mono and stero sound. This is for routine testing
|
||||
/// purposes; output should be same with either routines, yet disabling
|
||||
/// the dedicated mono/stereo processing routines will result in slower
|
||||
/// runtime performance so recommendation is to keep this off.
|
||||
// #define USE_MULTICH_ALWAYS
|
||||
|
||||
#if !(SOUNDTOUCH_INTEGER_SAMPLES || SOUNDTOUCH_FLOAT_SAMPLES)
|
||||
|
||||
/// Choose either 32bit floating point or 16bit integer sampletype
|
||||
/// by choosing one of the following defines, unless this selection
|
||||
/// has already been done in some other file.
|
||||
////
|
||||
/// Notes:
|
||||
/// - In Windows environment, choose the sample format with the
|
||||
/// following defines.
|
||||
/// - In GNU environment, the floating point samples are used by
|
||||
/// default, but integer samples can be chosen by giving the
|
||||
/// following switch to the configure script:
|
||||
/// ./configure --enable-integer-samples
|
||||
/// However, if you still prefer to select the sample format here
|
||||
/// also in GNU environment, then please #undef the INTEGER_SAMPLE
|
||||
/// and FLOAT_SAMPLE defines first as in comments above.
|
||||
//#define SOUNDTOUCH_INTEGER_SAMPLES 1 //< 16bit integer samples
|
||||
#define SOUNDTOUCH_FLOAT_SAMPLES 1 //< 32bit float samples
|
||||
|
||||
#endif
|
||||
|
||||
#if (_M_IX86 || __i386__ || __x86_64__ || _M_X64)
|
||||
/// Define this to allow X86-specific assembler/intrinsic optimizations.
|
||||
/// Notice that library contains also usual C++ versions of each of these
|
||||
/// these routines, so if you're having difficulties getting the optimized
|
||||
/// routines compiled for whatever reason, you may disable these optimizations
|
||||
/// to make the library compile.
|
||||
|
||||
#define SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS 1
|
||||
#undef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS
|
||||
#elif defined(__arm__) || defined(__aarch64__)
|
||||
#undef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
|
||||
#define SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS 1
|
||||
#else
|
||||
/// Always disable optimizations when not using a x86 systems.
|
||||
#undef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
|
||||
#undef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS
|
||||
|
||||
#endif
|
||||
|
||||
// If defined, allows the SIMD-optimized routines to skip unevenly aligned
|
||||
// memory offsets that can cause performance penalty in some SIMD implementations.
|
||||
// Causes slight compromise in sound quality.
|
||||
// #define SOUNDTOUCH_ALLOW_NONEXACT_SIMD_OPTIMIZATION 1
|
||||
|
||||
|
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
|
||||
// 16bit integer sample type
|
||||
typedef short SAMPLETYPE;
|
||||
// data type for sample accumulation: Use 32bit integer to prevent overflows
|
||||
typedef long LONG_SAMPLETYPE;
|
||||
|
||||
#ifdef SOUNDTOUCH_FLOAT_SAMPLES
|
||||
// check that only one sample type is defined
|
||||
#error "conflicting sample types defined"
|
||||
#endif // SOUNDTOUCH_FLOAT_SAMPLES
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
|
||||
// Allow MMX optimizations (not available in X64 mode)
|
||||
#if (!_M_X64)
|
||||
#define SOUNDTOUCH_ALLOW_MMX 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
// floating point samples
|
||||
typedef float SAMPLETYPE;
|
||||
// data type for sample accumulation: Use float also here to enable
|
||||
// efficient autovectorization
|
||||
typedef float LONG_SAMPLETYPE;
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS
|
||||
// Allow SSE optimizations
|
||||
#define SOUNDTOUCH_ALLOW_SSE 1
|
||||
#endif
|
||||
#ifdef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS
|
||||
#define SOUNDTOUCH_USE_NEON 1
|
||||
#endif
|
||||
|
||||
#endif // SOUNDTOUCH_INTEGER_SAMPLES
|
||||
|
||||
#if ((SOUNDTOUCH_ALLOW_SSE) || (__SSE__) || (SOUNDTOUCH_USE_NEON))
|
||||
#if SOUNDTOUCH_ALLOW_NONEXACT_SIMD_OPTIMIZATION
|
||||
#define ST_SIMD_AVOID_UNALIGNED
|
||||
#endif
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
// define ST_NO_EXCEPTION_HANDLING switch to disable throwing std exceptions:
|
||||
#define ST_NO_EXCEPTION_HANDLING 1
|
||||
#ifdef ST_NO_EXCEPTION_HANDLING
|
||||
// Exceptions disabled. Throw asserts instead if enabled.
|
||||
#include <assert.h>
|
||||
#define ST_THROW_RT_ERROR(x) {assert((const char *)x);}
|
||||
#else
|
||||
// use c++ standard exceptions
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#define ST_THROW_RT_ERROR(x) {throw std::runtime_error(x);}
|
||||
#endif
|
||||
|
||||
// When this #define is active, eliminates a clicking sound when the "rate" or "pitch"
|
||||
// parameter setting crosses from value <1 to >=1 or vice versa during processing.
|
||||
// Default is off as such crossover is untypical case and involves a slight sound
|
||||
// quality compromise.
|
||||
//#define SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER 1
|
||||
|
||||
#endif
|
||||
@@ -1,358 +0,0 @@
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// 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"
|
||||
#include "SourceSpan.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();
|
||||
|
||||
bool sourceProfileSupported() const;
|
||||
|
||||
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;
|
||||
|
||||
/* uTun: подключать после настройки, до первого putSamples либо после clear.
|
||||
* Поддерживается float PCM, unity pitch/rate, cubic interpolation, AA off, mono/stereo.
|
||||
* Ошибка сообщает Invalid и возвращает false. Изменение профиля после начала и flush
|
||||
* инвалидируют карту; clear начинает новую. nullptr отключает события в любой момент.
|
||||
* События описывают произведённый выход, включая ещё не прочитанные receiveSamples данные. */
|
||||
bool setSourceCallback(SourceCallback callback, void* context);
|
||||
|
||||
/// 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
|
||||
@@ -1,177 +0,0 @@
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// 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_
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/* uTun: происхождение PCM без изменения DSP. Индексы — сэмплы НА КАНАЛ от clear().
|
||||
* Copy: out[i] происходит из input0+i. Mix: источники input0+i и input1+i,
|
||||
* вес второго i/count, первого 1-i/count. Это описание происхождения, не PCM.
|
||||
* Reset начинает новую шкалу. Invalid запрещает её использование до clear().
|
||||
* Callback синхронный: нельзя вызывать SoundTouch повторно, блокировать поток или
|
||||
* сохранять ссылку на событие. Ошибки передаются через Invalid; логирует потребитель. */
|
||||
#ifndef SOUNDTOUCH_SOURCE_SPAN_H
|
||||
#define SOUNDTOUCH_SOURCE_SPAN_H
|
||||
#include <cstdint>
|
||||
|
||||
namespace soundtouch {
|
||||
enum class SourceKind { Reset, Copy, Mix, Invalid };
|
||||
enum class SourceError { None, Profile, Reconfigure, Flush, LateAttach };
|
||||
struct SourceSpan {
|
||||
SourceKind kind;
|
||||
SourceError error;
|
||||
uint64_t output, input0, input1;
|
||||
uint32_t count;
|
||||
SourceSpan(SourceKind k, SourceError e = SourceError::None, uint64_t out = 0,
|
||||
uint64_t first = 0, uint64_t second = 0, uint32_t length = 0)
|
||||
: kind(k), error(e), output(out), input0(first), input1(second), count(length) {}
|
||||
};
|
||||
using SourceCallback = void (*)(void*, const SourceSpan&);
|
||||
}
|
||||
#endif
|
||||
@@ -1,66 +0,0 @@
|
||||
# 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
|
||||
@@ -1,222 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// FIR low-pass (anti-alias) filter with filter coefficient design routine and
|
||||
/// MMX optimization.
|
||||
///
|
||||
/// Anti-alias filter is used to prevent folding of high frequencies when
|
||||
/// transposing the sample rate with interpolation.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <memory.h>
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include "AAFilter.h"
|
||||
#include "FIRFilter.h"
|
||||
|
||||
using namespace soundtouch;
|
||||
|
||||
#define PI 3.14159265358979323846
|
||||
#define TWOPI (2 * PI)
|
||||
|
||||
// define this to save AA filter coefficients to a file
|
||||
// #define _DEBUG_SAVE_AAFILTER_COEFFICIENTS 1
|
||||
|
||||
#ifdef _DEBUG_SAVE_AAFILTER_COEFFICIENTS
|
||||
#include <stdio.h>
|
||||
|
||||
static void _DEBUG_SAVE_AAFIR_COEFFS(SAMPLETYPE *coeffs, int len)
|
||||
{
|
||||
FILE *fptr = fopen("aa_filter_coeffs.txt", "wt");
|
||||
if (fptr == nullptr) return;
|
||||
|
||||
for (int i = 0; i < len; i ++)
|
||||
{
|
||||
double temp = coeffs[i];
|
||||
fprintf(fptr, "%lf\n", temp);
|
||||
}
|
||||
fclose(fptr);
|
||||
}
|
||||
|
||||
#else
|
||||
#define _DEBUG_SAVE_AAFIR_COEFFS(x, y)
|
||||
#endif
|
||||
|
||||
/*****************************************************************************
|
||||
*
|
||||
* Implementation of the class 'AAFilter'
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
AAFilter::AAFilter(uint len)
|
||||
{
|
||||
pFIR = FIRFilter::newInstance();
|
||||
cutoffFreq = 0.5;
|
||||
setLength(len);
|
||||
}
|
||||
|
||||
|
||||
AAFilter::~AAFilter()
|
||||
{
|
||||
delete pFIR;
|
||||
}
|
||||
|
||||
|
||||
// Sets new anti-alias filter cut-off edge frequency, scaled to
|
||||
// sampling frequency (nyquist frequency = 0.5).
|
||||
// The filter will cut frequencies higher than the given frequency.
|
||||
void AAFilter::setCutoffFreq(double newCutoffFreq)
|
||||
{
|
||||
cutoffFreq = newCutoffFreq;
|
||||
calculateCoeffs();
|
||||
}
|
||||
|
||||
|
||||
// Sets number of FIR filter taps
|
||||
void AAFilter::setLength(uint newLength)
|
||||
{
|
||||
length = newLength;
|
||||
calculateCoeffs();
|
||||
}
|
||||
|
||||
|
||||
// Calculates coefficients for a low-pass FIR filter using Hamming window
|
||||
void AAFilter::calculateCoeffs()
|
||||
{
|
||||
uint i;
|
||||
double cntTemp, temp, tempCoeff,h, w;
|
||||
double wc;
|
||||
double scaleCoeff, sum;
|
||||
double *work;
|
||||
SAMPLETYPE *coeffs;
|
||||
|
||||
assert(length >= 2);
|
||||
assert(length % 4 == 0);
|
||||
assert(cutoffFreq >= 0);
|
||||
assert(cutoffFreq <= 0.5);
|
||||
|
||||
work = new double[length];
|
||||
coeffs = new SAMPLETYPE[length];
|
||||
|
||||
wc = 2.0 * PI * cutoffFreq;
|
||||
tempCoeff = TWOPI / (double)length;
|
||||
|
||||
sum = 0;
|
||||
for (i = 0; i < length; i ++)
|
||||
{
|
||||
cntTemp = (double)i - (double)(length / 2);
|
||||
|
||||
temp = cntTemp * wc;
|
||||
if (temp != 0)
|
||||
{
|
||||
h = sin(temp) / temp; // sinc function
|
||||
}
|
||||
else
|
||||
{
|
||||
h = 1.0;
|
||||
}
|
||||
w = 0.54 + 0.46 * cos(tempCoeff * cntTemp); // hamming window
|
||||
|
||||
temp = w * h;
|
||||
work[i] = temp;
|
||||
|
||||
// calc net sum of coefficients
|
||||
sum += temp;
|
||||
}
|
||||
|
||||
// ensure the sum of coefficients is larger than zero
|
||||
assert(sum > 0);
|
||||
|
||||
// ensure we've really designed a lowpass filter...
|
||||
assert(work[length/2] > 0);
|
||||
assert(work[length/2 + 1] > -1e-6);
|
||||
assert(work[length/2 - 1] > -1e-6);
|
||||
|
||||
// Calculate a scaling coefficient in such a way that the result can be
|
||||
// divided by 16384
|
||||
scaleCoeff = 16384.0f / sum;
|
||||
|
||||
for (i = 0; i < length; i ++)
|
||||
{
|
||||
temp = work[i] * scaleCoeff;
|
||||
// scale & round to nearest integer
|
||||
temp += (temp >= 0) ? 0.5 : -0.5;
|
||||
// ensure no overfloods
|
||||
assert(temp >= -32768 && temp <= 32767);
|
||||
coeffs[i] = (SAMPLETYPE)temp;
|
||||
}
|
||||
|
||||
// Set coefficients. Use divide factor 14 => divide result by 2^14 = 16384
|
||||
pFIR->setCoefficients(coeffs, length, 14);
|
||||
|
||||
_DEBUG_SAVE_AAFIR_COEFFS(coeffs, length);
|
||||
|
||||
delete[] work;
|
||||
delete[] coeffs;
|
||||
}
|
||||
|
||||
|
||||
// Applies the filter to the given sequence of samples.
|
||||
// Note : The amount of outputted samples is by value of 'filter length'
|
||||
// smaller than the amount of input samples.
|
||||
uint AAFilter::evaluate(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels) const
|
||||
{
|
||||
return pFIR->evaluate(dest, src, numSamples, numChannels);
|
||||
}
|
||||
|
||||
|
||||
/// Applies the filter to the given src & dest pipes, so that processed amount of
|
||||
/// samples get removed from src, and produced amount added to dest
|
||||
/// Note : The amount of outputted samples is by value of 'filter length'
|
||||
/// smaller than the amount of input samples.
|
||||
uint AAFilter::evaluate(FIFOSampleBuffer &dest, FIFOSampleBuffer &src) const
|
||||
{
|
||||
SAMPLETYPE *pdest;
|
||||
const SAMPLETYPE *psrc;
|
||||
uint numSrcSamples;
|
||||
uint result;
|
||||
int numChannels = src.getChannels();
|
||||
|
||||
assert(numChannels == dest.getChannels());
|
||||
|
||||
numSrcSamples = src.numSamples();
|
||||
psrc = src.ptrBegin();
|
||||
pdest = dest.ptrEnd(numSrcSamples);
|
||||
result = pFIR->evaluate(pdest, psrc, numSrcSamples, numChannels);
|
||||
src.receiveSamples(result);
|
||||
dest.putSamples(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
uint AAFilter::getLength() const
|
||||
{
|
||||
return pFIR->getLength();
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// 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
|
||||
@@ -1,569 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Beats-per-minute (BPM) detection routine.
|
||||
///
|
||||
/// The beat detection algorithm works as follows:
|
||||
/// - Use function 'inputSamples' to input a chunks of samples to the class for
|
||||
/// analysis. It's a good idea to enter a large sound file or stream in smallish
|
||||
/// chunks of around few kilosamples in order not to extinguish too much RAM memory.
|
||||
/// - Inputted sound data is decimated to approx 500 Hz to reduce calculation burden,
|
||||
/// which is basically ok as low (bass) frequencies mostly determine the beat rate.
|
||||
/// Simple averaging is used for anti-alias filtering because the resulting signal
|
||||
/// quality isn't of that high importance.
|
||||
/// - Decimated sound data is enveloped, i.e. the amplitude shape is detected by
|
||||
/// taking absolute value that's smoothed by sliding average. Signal levels that
|
||||
/// are below a couple of times the general RMS amplitude level are cut away to
|
||||
/// leave only notable peaks there.
|
||||
/// - Repeating sound patterns (e.g. beats) are detected by calculating short-term
|
||||
/// autocorrelation function of the enveloped signal.
|
||||
/// - After whole sound data file has been analyzed as above, the bpm level is
|
||||
/// detected by function 'getBpm' that finds the highest peak of the autocorrelation
|
||||
/// function, calculates it's precise location and converts this reading to bpm's.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#define _USE_MATH_DEFINES
|
||||
|
||||
#include <math.h>
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <cfloat>
|
||||
#include "FIFOSampleBuffer.h"
|
||||
#include "PeakFinder.h"
|
||||
#include "BPMDetect.h"
|
||||
|
||||
using namespace soundtouch;
|
||||
|
||||
// algorithm input sample block size
|
||||
static const int INPUT_BLOCK_SIZE = 2048;
|
||||
|
||||
// decimated sample block size
|
||||
static const int DECIMATED_BLOCK_SIZE = 256;
|
||||
|
||||
/// Target sample rate after decimation
|
||||
static const int TARGET_SRATE = 1000;
|
||||
|
||||
/// XCorr update sequence size, update in about 200msec chunks
|
||||
static const int XCORR_UPDATE_SEQUENCE = (int)(TARGET_SRATE / 5);
|
||||
|
||||
/// Moving average N size
|
||||
static const int MOVING_AVERAGE_N = 15;
|
||||
|
||||
/// XCorr decay time constant, decay to half in 30 seconds
|
||||
/// If it's desired to have the system adapt quicker to beat rate
|
||||
/// changes within a continuing music stream, then the
|
||||
/// 'xcorr_decay_time_constant' value can be reduced, yet that
|
||||
/// can increase possibility of glitches in bpm detection.
|
||||
static const double XCORR_DECAY_TIME_CONSTANT = 30.0;
|
||||
|
||||
/// Data overlap factor for beat detection algorithm
|
||||
static const int OVERLAP_FACTOR = 4;
|
||||
|
||||
static const double TWOPI = (2 * M_PI);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Enable following define to create bpm analysis file:
|
||||
|
||||
//#define _CREATE_BPM_DEBUG_FILE
|
||||
|
||||
#ifdef _CREATE_BPM_DEBUG_FILE
|
||||
|
||||
static void _SaveDebugData(const char *name, const float *data, int minpos, int maxpos, double coeff)
|
||||
{
|
||||
FILE *fptr = fopen(name, "wt");
|
||||
int i;
|
||||
|
||||
if (fptr)
|
||||
{
|
||||
printf("\nWriting BPM debug data into file %s\n", name);
|
||||
for (i = minpos; i < maxpos; i ++)
|
||||
{
|
||||
fprintf(fptr, "%d\t%.1lf\t%f\n", i, coeff / (double)i, data[i]);
|
||||
}
|
||||
fclose(fptr);
|
||||
}
|
||||
}
|
||||
|
||||
void _SaveDebugBeatPos(const char *name, const std::vector<BEAT> &beats)
|
||||
{
|
||||
printf("\nWriting beat detections data into file %s\n", name);
|
||||
|
||||
FILE *fptr = fopen(name, "wt");
|
||||
if (fptr)
|
||||
{
|
||||
for (uint i = 0; i < beats.size(); i++)
|
||||
{
|
||||
BEAT b = beats[i];
|
||||
fprintf(fptr, "%lf\t%lf\n", b.pos, b.strength);
|
||||
}
|
||||
fclose(fptr);
|
||||
}
|
||||
}
|
||||
#else
|
||||
#define _SaveDebugData(name, a,b,c,d)
|
||||
#define _SaveDebugBeatPos(name, b)
|
||||
#endif
|
||||
|
||||
// Hamming window
|
||||
void hamming(float *w, int N)
|
||||
{
|
||||
for (int i = 0; i < N; i++)
|
||||
{
|
||||
w[i] = (float)(0.54 - 0.46 * cos(TWOPI * i / (N - 1)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// IIR2_filter - 2nd order IIR filter
|
||||
|
||||
IIR2_filter::IIR2_filter(const double *lpf_coeffs)
|
||||
{
|
||||
memcpy(coeffs, lpf_coeffs, 5 * sizeof(double));
|
||||
memset(prev, 0, sizeof(prev));
|
||||
}
|
||||
|
||||
|
||||
float IIR2_filter::update(float x)
|
||||
{
|
||||
prev[0] = x;
|
||||
double y = x * coeffs[0];
|
||||
|
||||
for (int i = 4; i >= 1; i--)
|
||||
{
|
||||
y += coeffs[i] * prev[i];
|
||||
prev[i] = prev[i - 1];
|
||||
}
|
||||
|
||||
prev[3] = y;
|
||||
return (float)y;
|
||||
}
|
||||
|
||||
|
||||
// IIR low-pass filter coefficients, calculated with matlab/octave cheby2(2,40,0.05)
|
||||
const double _LPF_coeffs[5] = { 0.00996655391939, -0.01944529148401, 0.00996655391939, 1.96867605796247, -0.96916387431724 };
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
BPMDetect::BPMDetect(int numChannels, int aSampleRate) :
|
||||
beat_lpf(_LPF_coeffs)
|
||||
{
|
||||
beats.reserve(250); // initial reservation to prevent frequent reallocation
|
||||
|
||||
this->sampleRate = aSampleRate;
|
||||
this->channels = numChannels;
|
||||
|
||||
decimateSum = 0;
|
||||
decimateCount = 0;
|
||||
|
||||
// choose decimation factor so that result is approx. 1000 Hz
|
||||
decimateBy = sampleRate / TARGET_SRATE;
|
||||
if ((decimateBy <= 0) || (decimateBy * DECIMATED_BLOCK_SIZE < INPUT_BLOCK_SIZE))
|
||||
{
|
||||
ST_THROW_RT_ERROR("Too small samplerate");
|
||||
}
|
||||
|
||||
// Calculate window length & starting item according to desired min & max bpms
|
||||
windowLen = (60 * sampleRate) / (decimateBy * MIN_BPM);
|
||||
windowStart = (60 * sampleRate) / (decimateBy * MAX_BPM_RANGE);
|
||||
|
||||
assert(windowLen > windowStart);
|
||||
|
||||
// allocate new working objects
|
||||
xcorr = new float[windowLen];
|
||||
memset(xcorr, 0, windowLen * sizeof(float));
|
||||
|
||||
pos = 0;
|
||||
peakPos = 0;
|
||||
peakVal = 0;
|
||||
init_scaler = 1;
|
||||
beatcorr_ringbuffpos = 0;
|
||||
beatcorr_ringbuff = new float[windowLen];
|
||||
memset(beatcorr_ringbuff, 0, windowLen * sizeof(float));
|
||||
|
||||
// allocate processing buffer
|
||||
buffer = new FIFOSampleBuffer();
|
||||
// we do processing in mono mode
|
||||
buffer->setChannels(1);
|
||||
buffer->clear();
|
||||
|
||||
// calculate hamming windows
|
||||
hamw = new float[XCORR_UPDATE_SEQUENCE];
|
||||
hamming(hamw, XCORR_UPDATE_SEQUENCE);
|
||||
hamw2 = new float[XCORR_UPDATE_SEQUENCE / 2];
|
||||
hamming(hamw2, XCORR_UPDATE_SEQUENCE / 2);
|
||||
}
|
||||
|
||||
|
||||
BPMDetect::~BPMDetect()
|
||||
{
|
||||
delete[] xcorr;
|
||||
delete[] beatcorr_ringbuff;
|
||||
delete[] hamw;
|
||||
delete[] hamw2;
|
||||
delete buffer;
|
||||
}
|
||||
|
||||
|
||||
/// convert to mono, low-pass filter & decimate to about 500 Hz.
|
||||
/// return number of outputted samples.
|
||||
///
|
||||
/// Decimation is used to remove the unnecessary frequencies and thus to reduce
|
||||
/// the amount of data needed to be processed as calculating autocorrelation
|
||||
/// function is a very-very heavy operation.
|
||||
///
|
||||
/// Anti-alias filtering is done simply by averaging the samples. This is really a
|
||||
/// poor-man's anti-alias filtering, but it's not so critical in this kind of application
|
||||
/// (it'd also be difficult to design a high-quality filter with steep cut-off at very
|
||||
/// narrow band)
|
||||
int BPMDetect::decimate(SAMPLETYPE *dest, const SAMPLETYPE *src, int numsamples)
|
||||
{
|
||||
int count, outcount;
|
||||
LONG_SAMPLETYPE out;
|
||||
|
||||
assert(channels > 0);
|
||||
assert(decimateBy > 0);
|
||||
outcount = 0;
|
||||
for (count = 0; count < numsamples; count ++)
|
||||
{
|
||||
int j;
|
||||
|
||||
// convert to mono and accumulate
|
||||
for (j = 0; j < channels; j ++)
|
||||
{
|
||||
decimateSum += src[j];
|
||||
}
|
||||
src += j;
|
||||
|
||||
decimateCount ++;
|
||||
if (decimateCount >= decimateBy)
|
||||
{
|
||||
// Store every Nth sample only
|
||||
out = (LONG_SAMPLETYPE)(decimateSum / (decimateBy * channels));
|
||||
decimateSum = 0;
|
||||
decimateCount = 0;
|
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
|
||||
// check ranges for sure (shouldn't actually be necessary)
|
||||
if (out > 32767)
|
||||
{
|
||||
out = 32767;
|
||||
}
|
||||
else if (out < -32768)
|
||||
{
|
||||
out = -32768;
|
||||
}
|
||||
#endif // SOUNDTOUCH_INTEGER_SAMPLES
|
||||
dest[outcount] = (SAMPLETYPE)out;
|
||||
outcount ++;
|
||||
}
|
||||
}
|
||||
return outcount;
|
||||
}
|
||||
|
||||
|
||||
// Calculates autocorrelation function of the sample history buffer
|
||||
void BPMDetect::updateXCorr(int process_samples)
|
||||
{
|
||||
int offs;
|
||||
SAMPLETYPE *pBuffer;
|
||||
|
||||
assert(buffer->numSamples() >= (uint)(process_samples + windowLen));
|
||||
assert(process_samples == XCORR_UPDATE_SEQUENCE);
|
||||
|
||||
pBuffer = buffer->ptrBegin();
|
||||
|
||||
// calculate decay factor for xcorr filtering
|
||||
float xcorr_decay = (float)pow(0.5, 1.0 / (XCORR_DECAY_TIME_CONSTANT * TARGET_SRATE / process_samples));
|
||||
|
||||
// prescale pbuffer
|
||||
float tmp[XCORR_UPDATE_SEQUENCE];
|
||||
for (int i = 0; i < process_samples; i++)
|
||||
{
|
||||
tmp[i] = hamw[i] * hamw[i] * pBuffer[i];
|
||||
}
|
||||
|
||||
for (offs = windowStart; offs < windowLen; offs ++)
|
||||
{
|
||||
float sum;
|
||||
int i;
|
||||
|
||||
sum = 0;
|
||||
for (i = 0; i < process_samples; i ++)
|
||||
{
|
||||
sum += tmp[i] * pBuffer[i + offs]; // scaling the sub-result shouldn't be necessary
|
||||
}
|
||||
xcorr[offs] *= xcorr_decay; // decay 'xcorr' here with suitable time constant.
|
||||
|
||||
xcorr[offs] += (float)fabs(sum);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Detect individual beat positions
|
||||
void BPMDetect::updateBeatPos(int process_samples)
|
||||
{
|
||||
SAMPLETYPE *pBuffer;
|
||||
|
||||
assert(buffer->numSamples() >= (uint)(process_samples + windowLen));
|
||||
|
||||
pBuffer = buffer->ptrBegin();
|
||||
assert(process_samples == XCORR_UPDATE_SEQUENCE / 2);
|
||||
|
||||
// static double thr = 0.0003;
|
||||
double posScale = (double)this->decimateBy / (double)this->sampleRate;
|
||||
int resetDur = (int)(0.12 / posScale + 0.5);
|
||||
|
||||
// prescale pbuffer
|
||||
float tmp[XCORR_UPDATE_SEQUENCE / 2];
|
||||
for (int i = 0; i < process_samples; i++)
|
||||
{
|
||||
tmp[i] = hamw2[i] * hamw2[i] * pBuffer[i];
|
||||
}
|
||||
|
||||
for (int offs = windowStart; offs < windowLen; offs++)
|
||||
{
|
||||
float sum = 0;
|
||||
for (int i = 0; i < process_samples; i++)
|
||||
{
|
||||
sum += tmp[i] * pBuffer[offs + i];
|
||||
}
|
||||
beatcorr_ringbuff[(beatcorr_ringbuffpos + offs) % windowLen] += (float)((sum > 0) ? sum : 0); // accumulate only positive correlations
|
||||
}
|
||||
|
||||
int skipstep = XCORR_UPDATE_SEQUENCE / OVERLAP_FACTOR;
|
||||
|
||||
// compensate empty buffer at beginning by scaling coefficient
|
||||
float scale = (float)windowLen / (float)(skipstep * init_scaler);
|
||||
if (scale > 1.0f)
|
||||
{
|
||||
init_scaler++;
|
||||
}
|
||||
else
|
||||
{
|
||||
scale = 1.0f;
|
||||
}
|
||||
|
||||
// detect beats
|
||||
for (int i = 0; i < skipstep; i++)
|
||||
{
|
||||
float sum = beatcorr_ringbuff[beatcorr_ringbuffpos];
|
||||
sum -= beat_lpf.update(sum);
|
||||
|
||||
if (sum > peakVal)
|
||||
{
|
||||
// found new local largest value
|
||||
peakVal = sum;
|
||||
peakPos = pos;
|
||||
}
|
||||
if (pos > peakPos + resetDur)
|
||||
{
|
||||
// largest value not updated for 200msec => accept as beat
|
||||
peakPos += skipstep;
|
||||
if (peakVal > 0)
|
||||
{
|
||||
// add detected beat to end of "beats" vector
|
||||
BEAT temp = { (float)(peakPos * posScale), (float)(peakVal * scale) };
|
||||
beats.push_back(temp);
|
||||
}
|
||||
|
||||
peakVal = 0;
|
||||
peakPos = pos;
|
||||
}
|
||||
|
||||
beatcorr_ringbuff[beatcorr_ringbuffpos] = 0;
|
||||
pos++;
|
||||
beatcorr_ringbuffpos = (beatcorr_ringbuffpos + 1) % windowLen;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#define max(x,y) ((x) > (y) ? (x) : (y))
|
||||
|
||||
void BPMDetect::inputSamples(const SAMPLETYPE *samples, int numSamples)
|
||||
{
|
||||
SAMPLETYPE decimated[DECIMATED_BLOCK_SIZE];
|
||||
|
||||
// iterate so that max INPUT_BLOCK_SAMPLES processed per iteration
|
||||
while (numSamples > 0)
|
||||
{
|
||||
int block;
|
||||
int decSamples;
|
||||
|
||||
block = (numSamples > INPUT_BLOCK_SIZE) ? INPUT_BLOCK_SIZE : numSamples;
|
||||
|
||||
// decimate. note that converts to mono at the same time
|
||||
decSamples = decimate(decimated, samples, block);
|
||||
samples += block * channels;
|
||||
numSamples -= block;
|
||||
|
||||
buffer->putSamples(decimated, decSamples);
|
||||
}
|
||||
|
||||
// when the buffer has enough samples for processing...
|
||||
int req = max(windowLen + XCORR_UPDATE_SEQUENCE, 2 * XCORR_UPDATE_SEQUENCE);
|
||||
while ((int)buffer->numSamples() >= req)
|
||||
{
|
||||
// ... update autocorrelations...
|
||||
updateXCorr(XCORR_UPDATE_SEQUENCE);
|
||||
// ...update beat position calculation...
|
||||
updateBeatPos(XCORR_UPDATE_SEQUENCE / 2);
|
||||
// ... and remove proceessed samples from the buffer
|
||||
int n = XCORR_UPDATE_SEQUENCE / OVERLAP_FACTOR;
|
||||
buffer->receiveSamples(n);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void BPMDetect::removeBias()
|
||||
{
|
||||
int i;
|
||||
|
||||
// Remove linear bias: calculate linear regression coefficient
|
||||
// 1. calc mean of 'xcorr' and 'i'
|
||||
double mean_i = 0;
|
||||
double mean_x = 0;
|
||||
for (i = windowStart; i < windowLen; i++)
|
||||
{
|
||||
mean_x += xcorr[i];
|
||||
}
|
||||
mean_x /= (windowLen - windowStart);
|
||||
mean_i = 0.5 * (windowLen - 1 + windowStart);
|
||||
|
||||
// 2. calculate linear regression coefficient
|
||||
double b = 0;
|
||||
double div = 0;
|
||||
for (i = windowStart; i < windowLen; i++)
|
||||
{
|
||||
double xt = xcorr[i] - mean_x;
|
||||
double xi = i - mean_i;
|
||||
b += xt * xi;
|
||||
div += xi * xi;
|
||||
}
|
||||
b /= div;
|
||||
|
||||
// subtract linear regression and resolve min. value bias
|
||||
float minval = FLT_MAX; // arbitrary large number
|
||||
for (i = windowStart; i < windowLen; i ++)
|
||||
{
|
||||
xcorr[i] -= (float)(b * i);
|
||||
if (xcorr[i] < minval)
|
||||
{
|
||||
minval = xcorr[i];
|
||||
}
|
||||
}
|
||||
|
||||
// subtract min.value
|
||||
for (i = windowStart; i < windowLen; i ++)
|
||||
{
|
||||
xcorr[i] -= minval;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calculate N-point moving average for "source" values
|
||||
void MAFilter(float *dest, const float *source, int start, int end, int N)
|
||||
{
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
int i1 = i - N / 2;
|
||||
int i2 = i + N / 2 + 1;
|
||||
if (i1 < start) i1 = start;
|
||||
if (i2 > end) i2 = end;
|
||||
|
||||
double sum = 0;
|
||||
for (int j = i1; j < i2; j ++)
|
||||
{
|
||||
sum += source[j];
|
||||
}
|
||||
dest[i] = (float)(sum / (i2 - i1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
float BPMDetect::getBpm()
|
||||
{
|
||||
double peakPos;
|
||||
double coeff;
|
||||
PeakFinder peakFinder;
|
||||
|
||||
// remove bias from xcorr data
|
||||
removeBias();
|
||||
|
||||
coeff = 60.0 * ((double)sampleRate / (double)decimateBy);
|
||||
|
||||
// save bpm debug data if debug data writing enabled
|
||||
_SaveDebugData("soundtouch-bpm-xcorr.txt", xcorr, windowStart, windowLen, coeff);
|
||||
|
||||
// Smoothen by N-point moving-average
|
||||
float *data = new float[windowLen];
|
||||
memset(data, 0, sizeof(float) * windowLen);
|
||||
MAFilter(data, xcorr, windowStart, windowLen, MOVING_AVERAGE_N);
|
||||
|
||||
// find peak position
|
||||
peakPos = peakFinder.detectPeak(data, windowStart, windowLen);
|
||||
|
||||
// save bpm debug data if debug data writing enabled
|
||||
_SaveDebugData("soundtouch-bpm-smoothed.txt", data, windowStart, windowLen, coeff);
|
||||
|
||||
delete[] data;
|
||||
|
||||
assert(decimateBy != 0);
|
||||
if (peakPos < 1e-9) return 0.0; // detection failed.
|
||||
|
||||
_SaveDebugBeatPos("soundtouch-detected-beats.txt", beats);
|
||||
|
||||
// calculate BPM
|
||||
float bpm = (float)(coeff / peakPos);
|
||||
return (bpm >= MIN_BPM && bpm <= MAX_BPM_VALID) ? bpm : 0;
|
||||
}
|
||||
|
||||
|
||||
/// Get beat position arrays. Note: The array includes also really low beat detection values
|
||||
/// in absence of clear strong beats. Consumer may wish to filter low values away.
|
||||
/// - "pos" receive array of beat positions
|
||||
/// - "values" receive array of beat detection strengths
|
||||
/// - max_num indicates max.size of "pos" and "values" array.
|
||||
///
|
||||
/// You can query a suitable array sized by calling this with nullptr in "pos" & "values".
|
||||
///
|
||||
/// \return number of beats in the arrays.
|
||||
int BPMDetect::getBeats(float *pos, float *values, int max_num)
|
||||
{
|
||||
int num = (int)beats.size();
|
||||
if ((!pos) || (!values)) return num; // pos or values nullptr, return just size
|
||||
|
||||
for (int i = 0; (i < num) && (i < max_num); i++)
|
||||
{
|
||||
pos[i] = beats[i].pos;
|
||||
values[i] = beats[i].strength;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// A buffer class for temporarily storaging sound samples, operates as a
|
||||
/// first-in-first-out pipe.
|
||||
///
|
||||
/// Samples are added to the end of the sample buffer with the 'putSamples'
|
||||
/// function, and are received from the beginning of the buffer by calling
|
||||
/// the 'receiveSamples' function. The class automatically removes the
|
||||
/// outputted samples from the buffer, as well as grows the buffer size
|
||||
/// whenever necessary.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <memory.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "FIFOSampleBuffer.h"
|
||||
|
||||
using namespace soundtouch;
|
||||
|
||||
// Constructor
|
||||
FIFOSampleBuffer::FIFOSampleBuffer(int numChannels)
|
||||
{
|
||||
assert(numChannels > 0);
|
||||
sizeInBytes = 0; // reasonable initial value
|
||||
buffer = nullptr;
|
||||
bufferUnaligned = nullptr;
|
||||
samplesInBuffer = 0;
|
||||
bufferPos = 0;
|
||||
channels = (uint)numChannels;
|
||||
ensureCapacity(32); // allocate initial capacity
|
||||
}
|
||||
|
||||
|
||||
// destructor
|
||||
FIFOSampleBuffer::~FIFOSampleBuffer()
|
||||
{
|
||||
delete[] bufferUnaligned;
|
||||
bufferUnaligned = nullptr;
|
||||
buffer = nullptr;
|
||||
}
|
||||
|
||||
|
||||
// Sets number of channels, 1 = mono, 2 = stereo
|
||||
void FIFOSampleBuffer::setChannels(int numChannels)
|
||||
{
|
||||
uint usedBytes;
|
||||
|
||||
if (!verifyNumberOfChannels(numChannels)) return;
|
||||
|
||||
usedBytes = channels * samplesInBuffer;
|
||||
channels = (uint)numChannels;
|
||||
samplesInBuffer = usedBytes / channels;
|
||||
}
|
||||
|
||||
|
||||
// if output location pointer 'bufferPos' isn't zero, 'rewinds' the buffer and
|
||||
// zeroes this pointer by copying samples from the 'bufferPos' pointer
|
||||
// location on to the beginning of the buffer.
|
||||
void FIFOSampleBuffer::rewind()
|
||||
{
|
||||
if (buffer && bufferPos)
|
||||
{
|
||||
memmove(buffer, ptrBegin(), sizeof(SAMPLETYPE) * channels * samplesInBuffer);
|
||||
bufferPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Adds 'numSamples' pcs of samples from the 'samples' memory position to
|
||||
// the sample buffer.
|
||||
void FIFOSampleBuffer::putSamples(const SAMPLETYPE *samples, uint nSamples)
|
||||
{
|
||||
memcpy(ptrEnd(nSamples), samples, sizeof(SAMPLETYPE) * nSamples * channels);
|
||||
samplesInBuffer += nSamples;
|
||||
}
|
||||
|
||||
|
||||
// Increases the number of samples in the buffer without copying any actual
|
||||
// samples.
|
||||
//
|
||||
// This function is used to update the number of samples in the sample buffer
|
||||
// when accessing the buffer directly with 'ptrEnd' function. Please be
|
||||
// careful though!
|
||||
void FIFOSampleBuffer::putSamples(uint nSamples)
|
||||
{
|
||||
uint req;
|
||||
|
||||
req = samplesInBuffer + nSamples;
|
||||
ensureCapacity(req);
|
||||
samplesInBuffer += nSamples;
|
||||
}
|
||||
|
||||
|
||||
// Returns a pointer to the end of the used part of the sample buffer (i.e.
|
||||
// where the new samples are to be inserted). This function may be used for
|
||||
// inserting new samples into the sample buffer directly. Please be careful!
|
||||
//
|
||||
// Parameter 'slackCapacity' tells the function how much free capacity (in
|
||||
// terms of samples) there _at least_ should be, in order to the caller to
|
||||
// successfully insert all the required samples to the buffer. When necessary,
|
||||
// the function grows the buffer size to comply with this requirement.
|
||||
//
|
||||
// When using this function as means for inserting new samples, also remember
|
||||
// to increase the sample count afterwards, by calling the
|
||||
// 'putSamples(numSamples)' function.
|
||||
SAMPLETYPE *FIFOSampleBuffer::ptrEnd(uint slackCapacity)
|
||||
{
|
||||
ensureCapacity(samplesInBuffer + slackCapacity);
|
||||
return buffer + samplesInBuffer * channels;
|
||||
}
|
||||
|
||||
|
||||
// Returns a pointer to the beginning of the currently non-outputted samples.
|
||||
// This function is provided for accessing the output samples directly.
|
||||
// Please be careful!
|
||||
//
|
||||
// When using this function to output samples, also remember to 'remove' the
|
||||
// outputted samples from the buffer by calling the
|
||||
// 'receiveSamples(numSamples)' function
|
||||
SAMPLETYPE *FIFOSampleBuffer::ptrBegin()
|
||||
{
|
||||
assert(buffer);
|
||||
return buffer + bufferPos * channels;
|
||||
}
|
||||
|
||||
|
||||
// Ensures that the buffer has enough capacity, i.e. space for _at least_
|
||||
// 'capacityRequirement' number of samples. The buffer is grown in steps of
|
||||
// 4 kilobytes to eliminate the need for frequently growing up the buffer,
|
||||
// as well as to round the buffer size up to the virtual memory page size.
|
||||
void FIFOSampleBuffer::ensureCapacity(uint capacityRequirement)
|
||||
{
|
||||
SAMPLETYPE *tempUnaligned, *temp;
|
||||
|
||||
if (capacityRequirement > getCapacity())
|
||||
{
|
||||
// enlarge the buffer in 4kbyte steps (round up to next 4k boundary)
|
||||
sizeInBytes = (capacityRequirement * channels * sizeof(SAMPLETYPE) + 4095) & (uint)-4096;
|
||||
assert(sizeInBytes % 2 == 0);
|
||||
tempUnaligned = new SAMPLETYPE[sizeInBytes / sizeof(SAMPLETYPE) + 16 / sizeof(SAMPLETYPE)];
|
||||
if (tempUnaligned == nullptr)
|
||||
{
|
||||
ST_THROW_RT_ERROR("Couldn't allocate memory!\n");
|
||||
}
|
||||
// Align the buffer to begin at 16byte cache line boundary for optimal performance
|
||||
temp = (SAMPLETYPE *)SOUNDTOUCH_ALIGN_POINTER_16(tempUnaligned);
|
||||
if (samplesInBuffer)
|
||||
{
|
||||
memcpy(temp, ptrBegin(), samplesInBuffer * channels * sizeof(SAMPLETYPE));
|
||||
}
|
||||
delete[] bufferUnaligned;
|
||||
buffer = temp;
|
||||
bufferUnaligned = tempUnaligned;
|
||||
bufferPos = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// simply rewind the buffer (if necessary)
|
||||
rewind();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Returns the current buffer capacity in terms of samples
|
||||
uint FIFOSampleBuffer::getCapacity() const
|
||||
{
|
||||
return sizeInBytes / (channels * sizeof(SAMPLETYPE));
|
||||
}
|
||||
|
||||
|
||||
// Returns the number of samples currently in the buffer
|
||||
uint FIFOSampleBuffer::numSamples() const
|
||||
{
|
||||
return samplesInBuffer;
|
||||
}
|
||||
|
||||
|
||||
// Output samples from beginning of the sample buffer. Copies demanded number
|
||||
// of samples to output and removes them from the sample buffer. If there
|
||||
// are less than 'numsample' samples in the buffer, returns all available.
|
||||
//
|
||||
// Returns number of samples copied.
|
||||
uint FIFOSampleBuffer::receiveSamples(SAMPLETYPE *output, uint maxSamples)
|
||||
{
|
||||
uint num;
|
||||
|
||||
num = (maxSamples > samplesInBuffer) ? samplesInBuffer : maxSamples;
|
||||
|
||||
memcpy(output, ptrBegin(), channels * sizeof(SAMPLETYPE) * num);
|
||||
return receiveSamples(num);
|
||||
}
|
||||
|
||||
|
||||
// Removes samples from the beginning of the sample buffer without copying them
|
||||
// anywhere. Used to reduce the number of samples in the buffer, when accessing
|
||||
// the sample buffer with the 'ptrBegin' function.
|
||||
uint FIFOSampleBuffer::receiveSamples(uint maxSamples)
|
||||
{
|
||||
if (maxSamples >= samplesInBuffer)
|
||||
{
|
||||
uint temp;
|
||||
|
||||
temp = samplesInBuffer;
|
||||
samplesInBuffer = 0;
|
||||
return temp;
|
||||
}
|
||||
|
||||
samplesInBuffer -= maxSamples;
|
||||
bufferPos += maxSamples;
|
||||
|
||||
return maxSamples;
|
||||
}
|
||||
|
||||
|
||||
// Returns nonzero if the sample buffer is empty
|
||||
int FIFOSampleBuffer::isEmpty() const
|
||||
{
|
||||
return (samplesInBuffer == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
|
||||
// Clears the sample buffer
|
||||
void FIFOSampleBuffer::clear()
|
||||
{
|
||||
samplesInBuffer = 0;
|
||||
bufferPos = 0;
|
||||
}
|
||||
|
||||
|
||||
/// allow trimming (downwards) amount of samples in pipeline.
|
||||
/// Returns adjusted amount of samples
|
||||
uint FIFOSampleBuffer::adjustAmountOfSamples(uint numSamples)
|
||||
{
|
||||
if (numSamples < samplesInBuffer)
|
||||
{
|
||||
samplesInBuffer = numSamples;
|
||||
}
|
||||
return samplesInBuffer;
|
||||
}
|
||||
|
||||
|
||||
/// Add silence to end of buffer
|
||||
void FIFOSampleBuffer::addSilent(uint nSamples)
|
||||
{
|
||||
memset(ptrEnd(nSamples), 0, sizeof(SAMPLETYPE) * nSamples * channels);
|
||||
samplesInBuffer += nSamples;
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// General FIR digital filter routines with MMX optimization.
|
||||
///
|
||||
/// Notes : MMX optimized functions reside in a separate, platform-specific file,
|
||||
/// e.g. 'mmx_win.cpp' or 'mmx_gcc.cpp'
|
||||
///
|
||||
/// This source file contains OpenMP optimizations that allow speeding up the
|
||||
/// corss-correlation algorithm by executing it in several threads / CPU cores
|
||||
/// in parallel. See the following article link for more detailed discussion
|
||||
/// about SoundTouch OpenMP optimizations:
|
||||
/// http://www.softwarecoven.com/parallel-computing-in-embedded-mobile-devices
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <memory.h>
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include "FIRFilter.h"
|
||||
#include "cpu_detect.h"
|
||||
|
||||
using namespace soundtouch;
|
||||
|
||||
/*****************************************************************************
|
||||
*
|
||||
* Implementation of the class 'FIRFilter'
|
||||
*
|
||||
*****************************************************************************/
|
||||
|
||||
FIRFilter::FIRFilter()
|
||||
{
|
||||
resultDivFactor = 0;
|
||||
resultDivider = 0;
|
||||
length = 0;
|
||||
lengthDiv8 = 0;
|
||||
filterCoeffs = nullptr;
|
||||
filterCoeffsStereo = nullptr;
|
||||
}
|
||||
|
||||
|
||||
FIRFilter::~FIRFilter()
|
||||
{
|
||||
delete[] filterCoeffs;
|
||||
delete[] filterCoeffsStereo;
|
||||
}
|
||||
|
||||
|
||||
// Usual C-version of the filter routine for stereo sound
|
||||
uint FIRFilter::evaluateFilterStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples) const
|
||||
{
|
||||
int j, end;
|
||||
// hint compiler autovectorization that loop length is divisible by 8
|
||||
uint ilength = length & -8;
|
||||
|
||||
assert((length != 0) && (length == ilength) && (src != nullptr) && (dest != nullptr) && (filterCoeffs != nullptr));
|
||||
assert(numSamples > ilength);
|
||||
|
||||
end = 2 * (numSamples - ilength);
|
||||
|
||||
for (j = 0; j < end; j += 2)
|
||||
{
|
||||
const SAMPLETYPE *ptr;
|
||||
LONG_SAMPLETYPE suml, sumr;
|
||||
|
||||
suml = sumr = 0;
|
||||
ptr = src + j;
|
||||
|
||||
for (uint i = 0; i < ilength; i ++)
|
||||
{
|
||||
suml += ptr[2 * i] * filterCoeffsStereo[2 * i];
|
||||
sumr += ptr[2 * i + 1] * filterCoeffsStereo[2 * i + 1];
|
||||
}
|
||||
|
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
|
||||
suml >>= resultDivFactor;
|
||||
sumr >>= resultDivFactor;
|
||||
// saturate to 16 bit integer limits
|
||||
suml = (suml < -32768) ? -32768 : (suml > 32767) ? 32767 : suml;
|
||||
// saturate to 16 bit integer limits
|
||||
sumr = (sumr < -32768) ? -32768 : (sumr > 32767) ? 32767 : sumr;
|
||||
#endif // SOUNDTOUCH_INTEGER_SAMPLES
|
||||
dest[j] = (SAMPLETYPE)suml;
|
||||
dest[j + 1] = (SAMPLETYPE)sumr;
|
||||
}
|
||||
return numSamples - ilength;
|
||||
}
|
||||
|
||||
|
||||
// Usual C-version of the filter routine for mono sound
|
||||
uint FIRFilter::evaluateFilterMono(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples) const
|
||||
{
|
||||
int j, end;
|
||||
|
||||
// hint compiler autovectorization that loop length is divisible by 8
|
||||
int ilength = length & -8;
|
||||
|
||||
assert(ilength != 0);
|
||||
|
||||
end = numSamples - ilength;
|
||||
for (j = 0; j < end; j ++)
|
||||
{
|
||||
const SAMPLETYPE *pSrc = src + j;
|
||||
LONG_SAMPLETYPE sum;
|
||||
int i;
|
||||
|
||||
sum = 0;
|
||||
for (i = 0; i < ilength; i ++)
|
||||
{
|
||||
sum += pSrc[i] * filterCoeffs[i];
|
||||
}
|
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
|
||||
sum >>= resultDivFactor;
|
||||
// saturate to 16 bit integer limits
|
||||
sum = (sum < -32768) ? -32768 : (sum > 32767) ? 32767 : sum;
|
||||
#endif // SOUNDTOUCH_INTEGER_SAMPLES
|
||||
dest[j] = (SAMPLETYPE)sum;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
|
||||
uint FIRFilter::evaluateFilterMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels)
|
||||
{
|
||||
int j, end;
|
||||
|
||||
assert(length != 0);
|
||||
assert(src != nullptr);
|
||||
assert(dest != nullptr);
|
||||
assert(filterCoeffs != nullptr);
|
||||
assert(numChannels < 16);
|
||||
|
||||
// hint compiler autovectorization that loop length is divisible by 8
|
||||
int ilength = length & -8;
|
||||
|
||||
end = numChannels * (numSamples - ilength);
|
||||
|
||||
for (j = 0; j < end; j += numChannels)
|
||||
{
|
||||
const SAMPLETYPE *ptr;
|
||||
LONG_SAMPLETYPE sums[16];
|
||||
uint c;
|
||||
int i;
|
||||
|
||||
for (c = 0; c < numChannels; c ++)
|
||||
{
|
||||
sums[c] = 0;
|
||||
}
|
||||
|
||||
ptr = src + j;
|
||||
|
||||
for (i = 0; i < ilength; i ++)
|
||||
{
|
||||
SAMPLETYPE coef=filterCoeffs[i];
|
||||
for (c = 0; c < numChannels; c ++)
|
||||
{
|
||||
sums[c] += ptr[0] * coef;
|
||||
ptr ++;
|
||||
}
|
||||
}
|
||||
|
||||
for (c = 0; c < numChannels; c ++)
|
||||
{
|
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES
|
||||
sums[c] >>= resultDivFactor;
|
||||
#endif // SOUNDTOUCH_INTEGER_SAMPLES
|
||||
dest[j+c] = (SAMPLETYPE)sums[c];
|
||||
}
|
||||
}
|
||||
return numSamples - ilength;
|
||||
}
|
||||
|
||||
|
||||
// Set filter coeffiecients and length.
|
||||
//
|
||||
// Throws an exception if filter length isn't divisible by 8
|
||||
void FIRFilter::setCoefficients(const SAMPLETYPE *coeffs, uint newLength, uint uResultDivFactor)
|
||||
{
|
||||
assert(newLength > 0);
|
||||
if (newLength % 8) ST_THROW_RT_ERROR("FIR filter length not divisible by 8");
|
||||
|
||||
#ifdef SOUNDTOUCH_FLOAT_SAMPLES
|
||||
// scale coefficients already here if using floating samples
|
||||
double scale = 1.0 / resultDivider;
|
||||
#else
|
||||
short scale = 1;
|
||||
#endif
|
||||
|
||||
lengthDiv8 = newLength / 8;
|
||||
length = lengthDiv8 * 8;
|
||||
assert(length == newLength);
|
||||
|
||||
resultDivFactor = uResultDivFactor;
|
||||
resultDivider = (SAMPLETYPE)::pow(2.0, (int)resultDivFactor);
|
||||
|
||||
delete[] filterCoeffs;
|
||||
filterCoeffs = new SAMPLETYPE[length];
|
||||
delete[] filterCoeffsStereo;
|
||||
filterCoeffsStereo = new SAMPLETYPE[length*2];
|
||||
for (uint i = 0; i < length; i ++)
|
||||
{
|
||||
filterCoeffs[i] = (SAMPLETYPE)(coeffs[i] * scale);
|
||||
// create also stereo set of filter coefficients: this allows compiler
|
||||
// to autovectorize filter evaluation much more efficiently
|
||||
filterCoeffsStereo[2 * i] = (SAMPLETYPE)(coeffs[i] * scale);
|
||||
filterCoeffsStereo[2 * i + 1] = (SAMPLETYPE)(coeffs[i] * scale);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
uint FIRFilter::getLength() const
|
||||
{
|
||||
return length;
|
||||
}
|
||||
|
||||
|
||||
// Applies the filter to the given sequence of samples.
|
||||
//
|
||||
// Note : The amount of outputted samples is by value of 'filter_length'
|
||||
// smaller than the amount of input samples.
|
||||
uint FIRFilter::evaluate(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels)
|
||||
{
|
||||
assert(length > 0);
|
||||
assert(lengthDiv8 * 8 == length);
|
||||
|
||||
if (numSamples < length) return 0;
|
||||
|
||||
#ifndef USE_MULTICH_ALWAYS
|
||||
if (numChannels == 1)
|
||||
{
|
||||
return evaluateFilterMono(dest, src, numSamples);
|
||||
}
|
||||
else if (numChannels == 2)
|
||||
{
|
||||
return evaluateFilterStereo(dest, src, numSamples);
|
||||
}
|
||||
else
|
||||
#endif // USE_MULTICH_ALWAYS
|
||||
{
|
||||
assert(numChannels > 0);
|
||||
return evaluateFilterMulti(dest, src, numSamples, numChannels);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Operator 'new' is overloaded so that it automatically creates a suitable instance
|
||||
// depending on if we've a MMX-capable CPU available or not.
|
||||
void * FIRFilter::operator new(size_t)
|
||||
{
|
||||
// Notice! don't use "new FIRFilter" directly, use "newInstance" to create a new instance instead!
|
||||
ST_THROW_RT_ERROR("Error in FIRFilter::new: Don't use 'new FIRFilter', use 'newInstance' member instead!");
|
||||
return newInstance();
|
||||
}
|
||||
|
||||
|
||||
FIRFilter * FIRFilter::newInstance()
|
||||
{
|
||||
uint uExtensions;
|
||||
|
||||
uExtensions = detectCPUextensions();
|
||||
(void)uExtensions;
|
||||
|
||||
// Check if MMX/SSE instruction set extensions supported by CPU
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_MMX
|
||||
// MMX routines available only with integer sample types
|
||||
if (uExtensions & SUPPORT_MMX)
|
||||
{
|
||||
return ::new FIRFilterMMX;
|
||||
}
|
||||
else
|
||||
#endif // SOUNDTOUCH_ALLOW_MMX
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_SSE
|
||||
if (uExtensions & SUPPORT_SSE)
|
||||
{
|
||||
// SSE support
|
||||
return ::new FIRFilterSSE;
|
||||
}
|
||||
else
|
||||
#endif // SOUNDTOUCH_ALLOW_SSE
|
||||
|
||||
{
|
||||
// ISA optimizations not supported, use plain C version
|
||||
return ::new FIRFilter;
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// General FIR digital filter routines with MMX optimization.
|
||||
///
|
||||
/// Note : MMX optimized functions reside in a separate, platform-specific file,
|
||||
/// e.g. 'mmx_win.cpp' or 'mmx_gcc.cpp'
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef FIRFilter_H
|
||||
#define FIRFilter_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include "STTypes.h"
|
||||
|
||||
namespace soundtouch
|
||||
{
|
||||
|
||||
class FIRFilter
|
||||
{
|
||||
protected:
|
||||
// Number of FIR filter taps
|
||||
uint length;
|
||||
// Number of FIR filter taps divided by 8
|
||||
uint lengthDiv8;
|
||||
|
||||
// Result divider factor in 2^k format
|
||||
uint resultDivFactor;
|
||||
|
||||
// Result divider value.
|
||||
SAMPLETYPE resultDivider;
|
||||
|
||||
// Memory for filter coefficients
|
||||
SAMPLETYPE *filterCoeffs;
|
||||
SAMPLETYPE *filterCoeffsStereo;
|
||||
|
||||
virtual uint evaluateFilterStereo(SAMPLETYPE *dest,
|
||||
const SAMPLETYPE *src,
|
||||
uint numSamples) const;
|
||||
virtual uint evaluateFilterMono(SAMPLETYPE *dest,
|
||||
const SAMPLETYPE *src,
|
||||
uint numSamples) const;
|
||||
virtual uint evaluateFilterMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels);
|
||||
|
||||
public:
|
||||
FIRFilter();
|
||||
virtual ~FIRFilter();
|
||||
|
||||
/// Operator 'new' is overloaded so that it automatically creates a suitable instance
|
||||
/// depending on if we've a MMX-capable CPU available or not.
|
||||
static void * operator new(size_t s);
|
||||
|
||||
static FIRFilter *newInstance();
|
||||
|
||||
/// Applies the filter to the given sequence of samples.
|
||||
/// Note : The amount of outputted samples is by value of 'filter_length'
|
||||
/// smaller than the amount of input samples.
|
||||
///
|
||||
/// \return Number of samples copied to 'dest'.
|
||||
uint evaluate(SAMPLETYPE *dest,
|
||||
const SAMPLETYPE *src,
|
||||
uint numSamples,
|
||||
uint numChannels);
|
||||
|
||||
uint getLength() const;
|
||||
|
||||
virtual void setCoefficients(const SAMPLETYPE *coeffs,
|
||||
uint newLength,
|
||||
uint uResultDivFactor);
|
||||
};
|
||||
|
||||
|
||||
// Optional subclasses that implement CPU-specific optimizations:
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_MMX
|
||||
|
||||
/// Class that implements MMX optimized functions exclusive for 16bit integer samples type.
|
||||
class FIRFilterMMX : public FIRFilter
|
||||
{
|
||||
protected:
|
||||
short *filterCoeffsUnalign;
|
||||
short *filterCoeffsAlign;
|
||||
|
||||
virtual uint evaluateFilterStereo(short *dest, const short *src, uint numSamples) const override;
|
||||
public:
|
||||
FIRFilterMMX();
|
||||
~FIRFilterMMX();
|
||||
|
||||
virtual void setCoefficients(const short *coeffs, uint newLength, uint uResultDivFactor) override;
|
||||
};
|
||||
|
||||
#endif // SOUNDTOUCH_ALLOW_MMX
|
||||
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_SSE
|
||||
/// Class that implements SSE optimized functions exclusive for floating point samples type.
|
||||
class FIRFilterSSE : public FIRFilter
|
||||
{
|
||||
protected:
|
||||
float *filterCoeffsUnalign;
|
||||
float *filterCoeffsAlign;
|
||||
|
||||
virtual uint evaluateFilterStereo(float *dest, const float *src, uint numSamples) const override;
|
||||
public:
|
||||
FIRFilterSSE();
|
||||
~FIRFilterSSE();
|
||||
|
||||
virtual void setCoefficients(const float *coeffs, uint newLength, uint uResultDivFactor) override;
|
||||
};
|
||||
|
||||
#endif // SOUNDTOUCH_ALLOW_SSE
|
||||
|
||||
}
|
||||
|
||||
#endif // FIRFilter_H
|
||||
@@ -1,196 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Cubic interpolation routine.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <stddef.h>
|
||||
#include <math.h>
|
||||
#include "InterpolateCubic.h"
|
||||
#include "STTypes.h"
|
||||
|
||||
using namespace soundtouch;
|
||||
|
||||
// cubic interpolation coefficients
|
||||
static const float _coeffs[]=
|
||||
{ -0.5f, 1.0f, -0.5f, 0.0f,
|
||||
1.5f, -2.5f, 0.0f, 1.0f,
|
||||
-1.5f, 2.0f, 0.5f, 0.0f,
|
||||
0.5f, -0.5f, 0.0f, 0.0f};
|
||||
|
||||
|
||||
InterpolateCubic::InterpolateCubic()
|
||||
{
|
||||
fract = 0;
|
||||
}
|
||||
|
||||
|
||||
void InterpolateCubic::resetRegisters()
|
||||
{
|
||||
fract = 0;
|
||||
}
|
||||
|
||||
|
||||
/// Transpose mono audio. Returns number of produced output samples, and
|
||||
/// updates "srcSamples" to amount of consumed source samples
|
||||
int InterpolateCubic::transposeMono(SAMPLETYPE *pdest,
|
||||
const SAMPLETYPE *psrc,
|
||||
int &srcSamples)
|
||||
{
|
||||
int i;
|
||||
int srcSampleEnd = srcSamples - 4;
|
||||
int srcCount = 0;
|
||||
|
||||
i = 0;
|
||||
while (srcCount < srcSampleEnd)
|
||||
{
|
||||
float out;
|
||||
const float x3 = 1.0f;
|
||||
const float x2 = (float)fract; // x
|
||||
const float x1 = x2*x2; // x^2
|
||||
const float x0 = x1*x2; // x^3
|
||||
float y0, y1, y2, y3;
|
||||
|
||||
assert(fract < 1.0);
|
||||
|
||||
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3;
|
||||
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3;
|
||||
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3;
|
||||
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3;
|
||||
|
||||
out = y0 * psrc[0] + y1 * psrc[1] + y2 * psrc[2] + y3 * psrc[3];
|
||||
|
||||
pdest[i] = (SAMPLETYPE)out;
|
||||
i ++;
|
||||
|
||||
// update position fraction
|
||||
fract += rate;
|
||||
// update whole positions
|
||||
int whole = (int)fract;
|
||||
fract -= whole;
|
||||
psrc += whole;
|
||||
srcCount += whole;
|
||||
}
|
||||
srcSamples = srcCount;
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
/// Transpose stereo audio. Returns number of produced output samples, and
|
||||
/// updates "srcSamples" to amount of consumed source samples
|
||||
int InterpolateCubic::transposeStereo(SAMPLETYPE *pdest,
|
||||
const SAMPLETYPE *psrc,
|
||||
int &srcSamples)
|
||||
{
|
||||
int i;
|
||||
int srcSampleEnd = srcSamples - 4;
|
||||
int srcCount = 0;
|
||||
|
||||
i = 0;
|
||||
while (srcCount < srcSampleEnd)
|
||||
{
|
||||
const float x3 = 1.0f;
|
||||
const float x2 = (float)fract; // x
|
||||
const float x1 = x2*x2; // x^2
|
||||
const float x0 = x1*x2; // x^3
|
||||
float y0, y1, y2, y3;
|
||||
float out0, out1;
|
||||
|
||||
assert(fract < 1.0);
|
||||
|
||||
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3;
|
||||
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3;
|
||||
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3;
|
||||
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3;
|
||||
|
||||
out0 = y0 * psrc[0] + y1 * psrc[2] + y2 * psrc[4] + y3 * psrc[6];
|
||||
out1 = y0 * psrc[1] + y1 * psrc[3] + y2 * psrc[5] + y3 * psrc[7];
|
||||
|
||||
pdest[2*i] = (SAMPLETYPE)out0;
|
||||
pdest[2*i+1] = (SAMPLETYPE)out1;
|
||||
i ++;
|
||||
|
||||
// update position fraction
|
||||
fract += rate;
|
||||
// update whole positions
|
||||
int whole = (int)fract;
|
||||
fract -= whole;
|
||||
psrc += 2*whole;
|
||||
srcCount += whole;
|
||||
}
|
||||
srcSamples = srcCount;
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
/// Transpose multi-channel audio. Returns number of produced output samples, and
|
||||
/// updates "srcSamples" to amount of consumed source samples
|
||||
int InterpolateCubic::transposeMulti(SAMPLETYPE *pdest,
|
||||
const SAMPLETYPE *psrc,
|
||||
int &srcSamples)
|
||||
{
|
||||
int i;
|
||||
int srcSampleEnd = srcSamples - 4;
|
||||
int srcCount = 0;
|
||||
|
||||
i = 0;
|
||||
while (srcCount < srcSampleEnd)
|
||||
{
|
||||
const float x3 = 1.0f;
|
||||
const float x2 = (float)fract; // x
|
||||
const float x1 = x2*x2; // x^2
|
||||
const float x0 = x1*x2; // x^3
|
||||
float y0, y1, y2, y3;
|
||||
|
||||
assert(fract < 1.0);
|
||||
|
||||
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3;
|
||||
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3;
|
||||
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3;
|
||||
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3;
|
||||
|
||||
for (int c = 0; c < numChannels; c ++)
|
||||
{
|
||||
float out;
|
||||
out = y0 * psrc[c] + y1 * psrc[c + numChannels] + y2 * psrc[c + 2 * numChannels] + y3 * psrc[c + 3 * numChannels];
|
||||
pdest[0] = (SAMPLETYPE)out;
|
||||
pdest ++;
|
||||
}
|
||||
i ++;
|
||||
|
||||
// update position fraction
|
||||
fract += rate;
|
||||
// update whole positions
|
||||
int whole = (int)fract;
|
||||
fract -= whole;
|
||||
psrc += numChannels*whole;
|
||||
srcCount += whole;
|
||||
}
|
||||
srcSamples = srcCount;
|
||||
return i;
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// 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;
|
||||
|
||||
// При fract=0 кубический интерполятор копирует psrc[1], сохраняя четыре входных сэмпла.
|
||||
int unitySourceOffset() const override { return rate == 1.0 && fract == 0.0 ? 1 : -1; }
|
||||
|
||||
virtual int getLatency() const override
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,296 +0,0 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Linear interpolation algorithm.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include "InterpolateLinear.h"
|
||||
|
||||
using namespace soundtouch;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// InterpolateLinearInteger - integer arithmetic implementation
|
||||
//
|
||||
|
||||
/// fixed-point interpolation routine precision
|
||||
#define SCALE 65536
|
||||
|
||||
|
||||
// Constructor
|
||||
InterpolateLinearInteger::InterpolateLinearInteger() : TransposerBase()
|
||||
{
|
||||
// Notice: use local function calling syntax for sake of clarity,
|
||||
// to indicate the fact that C++ constructor can't call virtual functions.
|
||||
resetRegisters();
|
||||
setRate(1.0f);
|
||||
}
|
||||
|
||||
|
||||
void InterpolateLinearInteger::resetRegisters()
|
||||
{
|
||||
iFract = 0;
|
||||
}
|
||||
|
||||
|
||||
// Transposes the sample rate of the given samples using linear interpolation.
|
||||
// 'Mono' version of the routine. Returns the number of samples returned in
|
||||
// the "dest" buffer
|
||||
int InterpolateLinearInteger::transposeMono(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
|
||||
{
|
||||
int i;
|
||||
int srcSampleEnd = srcSamples - 1;
|
||||
int srcCount = 0;
|
||||
|
||||
i = 0;
|
||||
while (srcCount < srcSampleEnd)
|
||||
{
|
||||
LONG_SAMPLETYPE temp;
|
||||
|
||||
assert(iFract < SCALE);
|
||||
|
||||
temp = (SCALE - iFract) * src[0] + iFract * src[1];
|
||||
dest[i] = (SAMPLETYPE)(temp / SCALE);
|
||||
i++;
|
||||
|
||||
iFract += iRate;
|
||||
|
||||
int iWhole = iFract / SCALE;
|
||||
iFract -= iWhole * SCALE;
|
||||
srcCount += iWhole;
|
||||
src += iWhole;
|
||||
}
|
||||
srcSamples = srcCount;
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
// Transposes the sample rate of the given samples using linear interpolation.
|
||||
// 'Stereo' version of the routine. Returns the number of samples returned in
|
||||
// the "dest" buffer
|
||||
int InterpolateLinearInteger::transposeStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
|
||||
{
|
||||
int i;
|
||||
int srcSampleEnd = srcSamples - 1;
|
||||
int srcCount = 0;
|
||||
|
||||
i = 0;
|
||||
while (srcCount < srcSampleEnd)
|
||||
{
|
||||
LONG_SAMPLETYPE temp0;
|
||||
LONG_SAMPLETYPE temp1;
|
||||
|
||||
assert(iFract < SCALE);
|
||||
|
||||
temp0 = (SCALE - iFract) * src[0] + iFract * src[2];
|
||||
temp1 = (SCALE - iFract) * src[1] + iFract * src[3];
|
||||
dest[0] = (SAMPLETYPE)(temp0 / SCALE);
|
||||
dest[1] = (SAMPLETYPE)(temp1 / SCALE);
|
||||
dest += 2;
|
||||
i++;
|
||||
|
||||
iFract += iRate;
|
||||
|
||||
int iWhole = iFract / SCALE;
|
||||
iFract -= iWhole * SCALE;
|
||||
srcCount += iWhole;
|
||||
src += 2*iWhole;
|
||||
}
|
||||
srcSamples = srcCount;
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
int InterpolateLinearInteger::transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
|
||||
{
|
||||
int i;
|
||||
int srcSampleEnd = srcSamples - 1;
|
||||
int srcCount = 0;
|
||||
|
||||
i = 0;
|
||||
while (srcCount < srcSampleEnd)
|
||||
{
|
||||
LONG_SAMPLETYPE temp, vol1;
|
||||
|
||||
assert(iFract < SCALE);
|
||||
vol1 = (LONG_SAMPLETYPE)(SCALE - iFract);
|
||||
for (int c = 0; c < numChannels; c ++)
|
||||
{
|
||||
temp = vol1 * src[c] + iFract * src[c + numChannels];
|
||||
dest[0] = (SAMPLETYPE)(temp / SCALE);
|
||||
dest ++;
|
||||
}
|
||||
i++;
|
||||
|
||||
iFract += iRate;
|
||||
|
||||
int iWhole = iFract / SCALE;
|
||||
iFract -= iWhole * SCALE;
|
||||
srcCount += iWhole;
|
||||
src += iWhole * numChannels;
|
||||
}
|
||||
srcSamples = srcCount;
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
// Sets new target iRate. Normal iRate = 1.0, smaller values represent slower
|
||||
// iRate, larger faster iRates.
|
||||
void InterpolateLinearInteger::setRate(double newRate)
|
||||
{
|
||||
iRate = (int)(newRate * SCALE + 0.5);
|
||||
TransposerBase::setRate(newRate);
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// InterpolateLinearFloat - floating point arithmetic implementation
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
// Constructor
|
||||
InterpolateLinearFloat::InterpolateLinearFloat() : TransposerBase()
|
||||
{
|
||||
// Notice: use local function calling syntax for sake of clarity,
|
||||
// to indicate the fact that C++ constructor can't call virtual functions.
|
||||
resetRegisters();
|
||||
setRate(1.0);
|
||||
}
|
||||
|
||||
|
||||
void InterpolateLinearFloat::resetRegisters()
|
||||
{
|
||||
fract = 0;
|
||||
}
|
||||
|
||||
|
||||
// Transposes the sample rate of the given samples using linear interpolation.
|
||||
// 'Mono' version of the routine. Returns the number of samples returned in
|
||||
// the "dest" buffer
|
||||
int InterpolateLinearFloat::transposeMono(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
|
||||
{
|
||||
int i;
|
||||
int srcSampleEnd = srcSamples - 1;
|
||||
int srcCount = 0;
|
||||
|
||||
i = 0;
|
||||
while (srcCount < srcSampleEnd)
|
||||
{
|
||||
double out;
|
||||
assert(fract < 1.0);
|
||||
|
||||
out = (1.0 - fract) * src[0] + fract * src[1];
|
||||
dest[i] = (SAMPLETYPE)out;
|
||||
i ++;
|
||||
|
||||
// update position fraction
|
||||
fract += rate;
|
||||
// update whole positions
|
||||
int whole = (int)fract;
|
||||
fract -= whole;
|
||||
src += whole;
|
||||
srcCount += whole;
|
||||
}
|
||||
srcSamples = srcCount;
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
// Transposes the sample rate of the given samples using linear interpolation.
|
||||
// 'Mono' version of the routine. Returns the number of samples returned in
|
||||
// the "dest" buffer
|
||||
int InterpolateLinearFloat::transposeStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
|
||||
{
|
||||
int i;
|
||||
int srcSampleEnd = srcSamples - 1;
|
||||
int srcCount = 0;
|
||||
|
||||
i = 0;
|
||||
while (srcCount < srcSampleEnd)
|
||||
{
|
||||
double out0, out1;
|
||||
assert(fract < 1.0);
|
||||
|
||||
out0 = (1.0 - fract) * src[0] + fract * src[2];
|
||||
out1 = (1.0 - fract) * src[1] + fract * src[3];
|
||||
dest[2*i] = (SAMPLETYPE)out0;
|
||||
dest[2*i+1] = (SAMPLETYPE)out1;
|
||||
i ++;
|
||||
|
||||
// update position fraction
|
||||
fract += rate;
|
||||
// update whole positions
|
||||
int whole = (int)fract;
|
||||
fract -= whole;
|
||||
src += 2*whole;
|
||||
srcCount += whole;
|
||||
}
|
||||
srcSamples = srcCount;
|
||||
return i;
|
||||
}
|
||||
|
||||
|
||||
int InterpolateLinearFloat::transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples)
|
||||
{
|
||||
int i;
|
||||
int srcSampleEnd = srcSamples - 1;
|
||||
int srcCount = 0;
|
||||
|
||||
i = 0;
|
||||
while (srcCount < srcSampleEnd)
|
||||
{
|
||||
float temp, vol1, fract_float;
|
||||
|
||||
vol1 = (float)(1.0 - fract);
|
||||
fract_float = (float)fract;
|
||||
for (int c = 0; c < numChannels; c ++)
|
||||
{
|
||||
temp = vol1 * src[c] + fract_float * src[c + numChannels];
|
||||
*dest = (SAMPLETYPE)temp;
|
||||
dest ++;
|
||||
}
|
||||
i++;
|
||||
|
||||
fract += rate;
|
||||
|
||||
int iWhole = (int)fract;
|
||||
fract -= iWhole;
|
||||
srcCount += iWhole;
|
||||
src += iWhole * numChannels;
|
||||
}
|
||||
srcSamples = srcCount;
|
||||
|
||||
return i;
|
||||
}
|
||||
Loaded 100 of 1550 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user