Compare commits
156 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
1c51df039f | 20 hours ago |
|
|
9e3a417059 | 20 hours ago |
|
|
e914a1bdc1 | 21 hours ago |
|
|
b36c685cfa | 21 hours ago |
|
|
3c5c9604b3 | 22 hours ago |
|
|
fb37ca747a | 23 hours ago |
|
|
2d21c53b4b | 23 hours ago |
|
|
752c1f140c | 1 day ago |
|
|
db3cacd33e | 1 day ago |
|
|
7ebc282eb2 | 1 day ago |
|
|
0d44ada922 | 1 day ago |
|
|
52ffb91101 | 1 day ago |
|
|
745ec3dbd2 | 2 days ago |
|
|
15958a078e | 2 days ago |
|
|
61bf235ef4 | 2 days ago |
|
|
556b0bc7b7 | 2 days ago |
|
|
f12f61d7ee | 2 days ago |
|
|
1176411b79 | 2 days ago |
|
|
74ca98b38f | 2 days ago |
|
|
961a234d7d | 2 days ago |
|
|
a73ae13df4 | 2 days ago |
|
|
d8cbe2e589 | 2 days ago |
|
|
e1946b208d | 3 days ago |
|
|
09bc8c35c5 | 3 days ago |
|
|
f19ff11b36 | 3 days ago |
|
|
268243feeb | 3 days ago |
|
|
4be1e62251 | 3 days ago |
|
|
10b66e2ad0 | 3 days ago |
|
|
7a41fcf512 | 3 days ago |
|
|
518df33b0e | 3 days ago |
|
|
fdec5b65c5 | 3 days ago |
|
|
ce1f23fe46 | 3 days ago |
|
|
331970a121 | 3 days ago |
|
|
e3caf15f32 | 3 days ago |
|
|
fa9e102271 | 3 days ago |
|
|
39ce249361 | 3 days ago |
|
|
b76e6553bc | 3 days ago |
|
|
546e0c185d | 3 days ago |
|
|
4503bdfa7e | 3 days ago |
|
|
c63e93715f | 3 days ago |
|
|
904a9fbda1 | 3 days ago |
|
|
01fcefdec5 | 3 days ago |
|
|
72fee9e245 | 3 days ago |
|
|
5838e63cde | 3 days ago |
|
|
0756a6e020 | 4 days ago |
|
|
792776a1f6 | 4 days ago |
|
|
52ef7a4b3c | 4 days ago |
|
|
66c59501fc | 4 days ago |
|
|
bce17d9cfc | 4 days ago |
|
|
9745a987ac | 4 days ago |
|
|
1a8072e9b2 | 4 days ago |
|
|
26e6c05313 | 4 days ago |
|
|
ec51b2d61f | 4 days ago |
|
|
7374d1648c | 4 days ago |
|
|
de714f5023 | 4 days ago |
|
|
3e25bcef02 | 4 days ago |
|
|
1b92b4885b | 4 days ago |
|
|
10d3ecfda4 | 4 days ago |
|
|
03f71fd94e | 4 days ago |
|
|
59f4802904 | 4 days ago |
|
|
3131dcbb7c | 5 days ago |
|
|
018c8e795d | 5 days ago |
|
|
0209bcf5d3 | 5 days ago |
|
|
44d4265084 | 5 days ago |
|
|
f6e6e2c73f | 5 days ago |
|
|
8aaac45020 | 5 days ago |
|
|
cc2d2b5f78 | 5 days ago |
|
|
265421eb96 | 6 days ago |
|
|
a20d73d2be | 6 days ago |
|
|
4353daf913 | 6 days ago |
|
|
3a4daf6e77 | 6 days ago |
|
|
ab5d1c5f91 | 6 days ago |
|
|
08e8b2f1cc | 6 days ago |
|
|
3f7b4351cd | 6 days ago |
|
|
ceaf466b1c | 1 week ago |
|
|
fb4343fca6 | 1 week ago |
|
|
cc269e810c | 1 week ago |
|
|
b90dc752d2 | 1 week ago |
|
|
7198154106 | 1 week ago |
|
|
d237dfec7c | 1 week ago |
|
|
4f29839de5 | 1 week ago |
|
|
b46fd599fa | 1 week ago |
|
|
0e625da109 | 1 week ago |
|
|
93becd36e6 | 1 week ago |
|
|
a3e890b711 | 1 week ago |
|
|
a3df1720b4 | 1 week ago |
|
|
331c7849f0 | 1 week ago |
|
|
945c6a82b0 | 1 week ago |
|
|
cc0dcb071e | 1 week ago |
|
|
3d9c79e752 | 1 week ago |
|
|
b39b85db13 | 1 week ago |
|
|
9f89fd0f7a | 1 week ago |
|
|
0a1ba79d6f | 1 week ago |
|
|
e4e9fca243 | 1 week ago |
|
|
5628138d16 | 1 week ago |
|
|
8a9d8d0097 | 1 week ago |
|
|
2f3001458f | 1 week ago |
|
|
845af9200d | 1 week ago |
|
|
328d36376c | 1 week ago |
|
|
6e2a2f0011 | 1 week ago |
|
|
9793733cd5 | 1 week ago |
|
|
19dc076528 | 1 week ago |
|
|
6a0373ee73 | 1 week ago |
|
|
570b421ba0 | 1 week ago |
|
|
ee54bd52b3 | 1 week ago |
|
|
e885fe30f5 | 1 week ago |
|
|
cd0757e272 | 1 week ago |
|
|
f5fece4959 | 1 week ago |
|
|
01987a75d0 | 1 week ago |
|
|
9d4538935d | 1 week ago |
|
|
6ee9bad17d | 2 weeks ago |
|
|
a5047c29f7 | 2 weeks ago |
|
|
2f1545de38 | 2 weeks ago |
|
|
de850db548 | 2 weeks ago |
|
|
39a1da4c28 | 2 weeks ago |
|
|
d1432b7d19 | 2 weeks ago |
|
|
a7a3666d85 | 2 weeks ago |
|
|
ba1c2d85d3 | 2 weeks ago |
|
|
738cfe7835 | 2 weeks ago |
|
|
27e66f1092 | 2 weeks ago |
|
|
1bde2eca35 | 2 weeks ago |
|
|
16237b0df8 | 2 weeks ago |
|
|
fae0a7c069 | 2 weeks ago |
|
|
b33caa56b8 | 2 weeks ago |
|
|
c36cebe357 | 2 weeks ago |
|
|
90520a83ff | 2 weeks ago |
|
|
6b92964794 | 2 weeks ago |
|
|
688785fc6e | 2 weeks ago |
|
|
5e955168d2 | 2 weeks ago |
|
|
841b3042cf | 2 weeks ago |
|
|
4a785772be | 2 weeks ago |
|
|
8a9184675a | 2 weeks ago |
|
|
b96ed8bd00 | 2 weeks ago |
|
|
398078a28f | 2 weeks ago |
|
|
6c42390fc2 | 2 weeks ago |
|
|
1de1409699 | 2 weeks ago |
|
|
734a414186 | 2 weeks ago |
|
|
6d3ad965ee | 2 weeks ago |
|
|
8bfb456161 | 2 weeks ago |
|
|
1340ab123b | 2 weeks ago |
|
|
e7fc69bb1d | 2 weeks ago |
|
|
1fce405204 | 2 weeks ago |
|
|
e6a4ffd418 | 2 weeks ago |
|
|
1f1df4f437 | 2 weeks ago |
|
|
51a5c5bcbf | 2 weeks ago |
|
|
51d6d22a27 | 2 weeks ago |
|
|
21d9a4e5b7 | 2 weeks ago |
|
|
5293fdd3a9 | 2 weeks ago |
|
|
2de554f8b9 | 2 weeks ago |
|
|
de930b21a2 | 2 weeks ago |
|
|
e4557b0473 | 2 weeks ago |
|
|
b483335c06 | 2 weeks ago |
|
|
b11e77e4c0 | 2 weeks ago |
|
|
0157f7f96b | 2 weeks ago |
|
|
21ec740d82 | 2 weeks ago |
|
|
b085715654 | 2 weeks ago |
462 changed files with 198330 additions and 8592 deletions
@ -1,89 +0,0 @@
|
||||
# Задача: BGP NODEINFO теряется при флапающем реконнекте (chatgui member online не возвращается) |
||||
|
||||
## Кратко |
||||
|
||||
После рестарта Android-пира и реконнекта локальный chatgui показывает мембера **offline** и не возвращает **online**. |
||||
Корневая причина — **не в chat/member коде**, а в ETCP: BGP-пакет NODEINFO (347 байт) расшифровывается, но **не доходит до BGP-обработчика** `topo_group_receive_cbk()`. |
||||
|
||||
Нужно: **точно локализовать точку потери NODEINFO в receive-пути ETCP и починить её.** |
||||
|
||||
## Что уже сделано (исправлено и работает) |
||||
|
||||
1. `tools/chatgui/src/mainwindow.cpp` — **исправлен** `onMemberUpdatedCallback`/`onMemberRemovedCallback`: проверка `len < 1+64+1+79` отбрасывала все события с channel_id короче 64 симв. Теперь `len < 1` + точная проверка. (Это была причина «не обновляется вообще».) |
||||
2. `src/chat/chat_member.c/h` — добавлен `chat_core_member_online(ch_id,node_id)` = присутствие в `topo_group` (`topo_node_find_by_id`). Используется в `chat_core_get_member_list`/`get_single_member`. Убран `nodes.online` из SQL и блок `connected→2`. |
||||
3. `src/chat/chat_core.c`, `src/chat/chat_sync.c`, `src/chat/member_sync.c/h` — удалён онлайн-статус из БД + merkle-online gossip (`member_sync_set_online`, `_ms_apply_update`, `push_update`). |
||||
4. `src/chat/chat_status.c` + `tools/chatgui/src/accountlist.cpp` — флаг «bgp present» (0x08) для detail-панели. |
||||
5. `tools/chatgui/src/nodespage.cpp` — убрана колонка Online из диагностики. |
||||
|
||||
Эти правки корректны; статус работает на чистом connect/disconnect/стабильном реконнекте. |
||||
|
||||
## Найденная причина (подтверждено логами) |
||||
|
||||
- Android **шлёт** свой NODEINFO: `send_nodeinfo: node 1657b3ea8281c0a8 ver=2` (bgp-debug на Android). |
||||
- Локально пакет **расшифровывается**: `decrypt: code=01 dlen=347 plen=350` (347-байтный BGP-пакет, `code=0x01` = `ETCP_ID_TOPO_ENTRY`). |
||||
- Но `BGP recv NODEINFO nid=1657` (строка 193 в `topo_group.c`) **не появляется** — до обработчика пакет не доходит. |
||||
- При этом маленький `TABLE_COMPLETE` (10 байт) **доходит** (виден в логе как «mislabeled» `nid=aaaaaaaaaadeadbe` — это строка 193 логирует не-NODEINFO пакет как NODEINFO, garbage-поля). |
||||
- Признак: в стабильных рестартах (18:18/18:20) узел **добавляется** каждый раз, в флапающем (18:07) — нет. Т.е. потеря привязана к флапу (TCP-линки + ETCP reinit). |
||||
|
||||
## Receive-путь ETCP (где искать потерю) |
||||
|
||||
``` |
||||
decrypt (etcp_connections.c:2262, лог "decrypt: code=.. dlen=..") |
||||
→ if (link_state==CONNECTED) etcp_conn_input(pkt) |
||||
else memory_pool_free(pkt) ← добавлен WARN "decrypted pkt DROPPED" |
||||
→ etcp_conn_input (etcp.c:1506, "RX pkt dlen=%d") |
||||
→ нормализатор (reassembly) → int_queue |
||||
→ etcp_int_recv (etcp_api.c) ← добавлен WARN "int_recv BGP id=0x01 len=.." |
||||
→ dispatch по route id (0x01) → topo_group_receive_cbk (topo_group.c:193 "BGP recv NODEINFO") |
||||
``` |
||||
|
||||
## Уже добавленный дебаг (для локализации) |
||||
|
||||
1. `src/transport_layer/etcp_connections.c` (после дешифровки): |
||||
```c |
||||
} else { |
||||
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "[%s] decrypted pkt DROPPED: link_state=%d code=0x%02x dlen=%u", |
||||
link->etcp->log_name, link->link_state, pkt_code, pkt->data_len); |
||||
memory_pool_free(e_sock->instance->pkt_pool, pkt); |
||||
} |
||||
``` |
||||
2. `src/transport_layer/etcp_api.c` (`etcp_int_recv`, после чтения `id`): |
||||
```c |
||||
uint8_t id = e->dgram[0]; |
||||
if (id == 0x01) DEBUG_WARN(DEBUG_CATEGORY_ETCP, "int_recv BGP id=0x01 conn=%s len=%zu", conn->log_name, e->len); |
||||
``` |
||||
3. Android: `tools/chatgui-android/libutun_lite/instance_lite.c` добавлено |
||||
`debug_set_category_level(DEBUG_CATEGORY_BGP, DEBUG_LEVEL_DEBUG);` (пересобрать APK: `cd tools/chatgui-android && ./build.sh`). |
||||
4. chatgui конфиг `tools/chatgui/build/vibechat.cfg` — включены `bgp=debug`, `chat_sync=debug`, `member_sync=debug`, `debug=debug`. |
||||
|
||||
Как читать (по таймстампу флапа): |
||||
- есть `decrypt code=01 dlen=..` + `DROPPED` → потеря в проверке `link_state==CONNECTED`; |
||||
- есть `decrypt` + нет `DROPPED` + нет `int_recv BGP` → потеря в `etcp_conn_input`/нормализаторе; |
||||
- есть `int_recv BGP` + нет `BGP recv NODEINFO` → потеря в BGP-обработчике. |
||||
|
||||
## Задача для агента |
||||
|
||||
1. Пересобрать chatgui (`cd tools/chatgui && cmake --build build -j4`) и Android APK (`cd tools/chatgui-android && ./build.sh`). |
||||
2. Запустить chatgui (DISPLAY=:0, `./vibechat`), воспроизвести флап реконнекта: |
||||
```bash |
||||
adb shell am force-stop com.utun.chat && adb shell am start -n com.utun.chat/.MainActivity |
||||
``` |
||||
(повторить несколько раз; флап ловится не каждый раз). |
||||
3. В логах (`tools/chatgui/build/chatgui.log`) найти потерю NODEINFO и **точно определить точку** по таблице выше. |
||||
4. Починить первопричину (варианты): |
||||
- буферизовать расшифрованный пакет до `LINK_STATE_CONNECTED` вместо `memory_pool_free`; |
||||
- либо гарантированный BGP-retry (после conn UP, если узел пира не в `group->nodes` через N мс — повторно `topo_group_send_table_request`). |
||||
5. Убрать весь диагностический мусор: |
||||
- WARN "DROPPED" (etcp_connections.c), WARN "int_recv BGP" (etcp_api.c); |
||||
- DEBUG-логи в `chat_member.c` (`on_member_props_changed`), `chat_sync.c` (`cs_on_peer_status_changed`), `memberlistmodel.cpp`; |
||||
- `instance_lite.c` BGP-debug; |
||||
- `vibechat.cfg` — вернуть категории в закомментированное состояние. |
||||
6. Прогнать: connect→online, disconnect→offline, реконнект (флап)→online. `./check.sh` (или `cd src && make -j4`). |
||||
|
||||
## Воспроизведение / ключевые артефакты |
||||
|
||||
- Пир: Android SM-A525F (`com.utun.chat`), node_id `0x1657b3ea8281c0a8`. |
||||
- Локальный: chatgui (`tools/chatgui/build/vibechat`), node_id `0x24cd036a6e659b9a`. |
||||
- Канал: `ch_id=7206723622466219923`, `group_id=0x64036cf3a783ef93`. |
||||
- Лог chatgui: `tools/chatgui/build/chatgui.log` (debug_file из конфига). |
||||
- Лог Android: `adb logcat -d | grep -iE "send_nodeinfo|NOT in registry|NODEINFO|DROPPED|int_recv"`. |
||||
@ -0,0 +1,178 @@
|
||||
# Разбор полного прогона 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()` напрямую. |
||||
@ -0,0 +1,60 @@
|
||||
# Подписанная запись узла |
||||
|
||||
`TOPO_NODE.timestamp` — версия всей подписанной записи, а не отдельного адреса. |
||||
Узел-владелец назначает Unix microseconds, строго больше предыдущей сохранённой |
||||
версии. Имя, тип, активность, адреса, метаданные сокетов, REALITY и подпись |
||||
заменяются вместе. Timestamp входит в подпись. Групповые пути и подсети |
||||
остаются в `TOPO_GROUP_NODE` и не являются частью общей записи узла. |
||||
|
||||
Получатель проверяет подпись до изменения реестра или маршрутов. Более свежая |
||||
запись заменяет старую целиком, включая пустые списки адресов. Старые записи |
||||
игнорируются; разные подписанные записи с одинаковым timestamp — конфликт. |
||||
Повторное BGP-объявление может обновить путь, но не содержимое общей записи. |
||||
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется |
||||
только timestamp. |
||||
|
||||
Таблица `node_snapshots` хранит полную сериализованную запись без группового |
||||
окружения. `node_addresses` остаётся проекцией для поиска и отображения, а не |
||||
источником для восстановления подписанной записи. При наличии сохранённого |
||||
snapshot загрузка использует его. Локальная публикация сохраняется до рассылки; |
||||
ошибка сохранения не разрешает публикацию новой версии. |
||||
|
||||
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-линк с выбором локального адреса операционной системой. |
||||
@ -0,0 +1,61 @@
|
||||
# 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-протоколом не предусмотрена. |
||||
@ -0,0 +1,83 @@
|
||||
# Жизненный цикл ядра и сервисов |
||||
|
||||
Все операции выполняются в потоке единственного `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 и Android запускают ядро и чат явно, без UTUN. |
||||
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта |
||||
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл |
||||
приложения; освобождение ядра выполняет `utun_instance_destroy()`. |
||||
|
||||
| Владелец | Ресурсы | |
||||
|---|---| |
||||
| Ядро | Идентичность узла, реестр узлов и групп, 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 может временно |
||||
удерживать транспорт самостоятельно, не обращаясь к уже удалённой группе. |
||||
|
||||
Остановка чата сначала прекращает внешние команды и фоновые задания, затем |
||||
закрывает медиа и сервисы, синхронизацию и группы, после чего освобождает chat core. |
||||
`media_async_destroy()` дожидается завершения native workers и вызывает каждый |
||||
ожидающий callback с `MEDIA_ASYNC_CANCELLED`, пока его контекст ещё существует. |
||||
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное |
||||
транскодирование или распознавание речи может задержать stop; принудительного |
||||
прерывания этих библиотек нет. |
||||
|
||||
Статус транспорта 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 сервиса. |
||||
|
||||
`utun_set_client_activity()` в сборке с `UTUN_HAVE_STANDBY` применяет фактический |
||||
режим duty-cycle до рассылки событий активности; JNI не переключает его отдельно. |
||||
Повторная команда не перезапускает фазу и анонсы, но первый фоновый запуск включает |
||||
standby даже при начальном `client_activity=STANDBY`. Keepalive подписан на режим |
||||
и фазы самого standby; новые UDP/TCP-линки сразу анонсируют текущую фазу после |
||||
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно |
||||
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются |
||||
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра. |
||||
@ -0,0 +1,301 @@
|
||||
#include "silero_vad.h" |
||||
#include "debug_config.h" |
||||
#include "mem.h" |
||||
|
||||
#ifdef HAVE_SILERO_VAD |
||||
|
||||
#include <stdio.h> |
||||
#include <string.h> |
||||
|
||||
#include <onnxruntime/onnxruntime_c_api.h> |
||||
|
||||
#include "silero_vad_model.inc" |
||||
|
||||
/* Число float-элементов рекуррентного состояния модели: 2 * 1 * 128 */ |
||||
#define SILERO_VAD_STATE_COUNT 256 |
||||
|
||||
struct silero_vad { |
||||
const OrtApi* ort; |
||||
OrtEnv* env; |
||||
OrtSession* session; |
||||
OrtMemoryInfo* mem_info; |
||||
float state[SILERO_VAD_STATE_COUNT]; /* рекуррентное состояние между вызовами */ |
||||
uint8_t* model_data; /* байты .onnx, загруженные в память (для file-версии) */ |
||||
size_t model_len; |
||||
}; |
||||
|
||||
/* Освободить OrtStatus и залогировать его сообщение как ошибку */ |
||||
static void vad_report_ort_error(const OrtApi* ort, OrtStatus* status, const char* what) { |
||||
const char* msg = ort->GetErrorMessage(status); |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "%s: %s", what, msg ? msg : "unknown onnxruntime error"); |
||||
ort->ReleaseStatus(status); |
||||
} |
||||
|
||||
/* Прочитать файл целиком в буфер. 0 при успехе: буфер пишется в out/out_len (u_malloc). */ |
||||
static int vad_read_file(const char* path, uint8_t** out, size_t* out_len) { |
||||
FILE* f; |
||||
long size; |
||||
uint8_t* buf; |
||||
|
||||
*out = NULL; |
||||
*out_len = 0; |
||||
|
||||
f = fopen(path, "rb"); |
||||
if (!f) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: cannot open model '%s'", path); |
||||
return -1; |
||||
} |
||||
if (fseek(f, 0, SEEK_END) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: fseek failed on '%s'", path); |
||||
fclose(f); |
||||
return -1; |
||||
} |
||||
size = ftell(f); |
||||
if (size <= 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: empty or unreadable model '%s'", path); |
||||
fclose(f); |
||||
return -1; |
||||
} |
||||
rewind(f); |
||||
|
||||
buf = (uint8_t*)u_malloc((size_t)size); |
||||
if (!buf) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: OOM for %ld bytes", size); |
||||
fclose(f); |
||||
return -1; |
||||
} |
||||
if (fread(buf, 1, (size_t)size, f) != (size_t)size) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "vad_read_file: short read on '%s'", path); |
||||
u_free(buf); |
||||
fclose(f); |
||||
return -1; |
||||
} |
||||
fclose(f); |
||||
|
||||
*out = buf; |
||||
*out_len = (size_t)size; |
||||
return 0; |
||||
} |
||||
|
||||
/* Общая инициализация детектора из байтов модели (файл или вшитая модель).
|
||||
* data может указывать на внешний буфер (вшитая модель) — тогда не освобождается. */ |
||||
static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t len, |
||||
const uint8_t* owned_buf, const char* label) { |
||||
silero_vad_t* vad = NULL; |
||||
const OrtApi* ort; |
||||
OrtStatus* status; |
||||
OrtSessionOptions* opts = NULL; |
||||
|
||||
if (!data || len == 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): empty model", label); |
||||
return NULL; |
||||
} |
||||
|
||||
ort = OrtGetApiBase() ? OrtGetApiBase()->GetApi(ORT_API_VERSION) : NULL; |
||||
if (!ort) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): onnxruntime C API unavailable", label); |
||||
return NULL; |
||||
} |
||||
|
||||
vad = (silero_vad_t*)u_calloc(1, sizeof(*vad)); |
||||
if (!vad) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): OOM", label); |
||||
return NULL; |
||||
} |
||||
vad->ort = ort; |
||||
vad->model_data = (uint8_t*)owned_buf; |
||||
vad->model_len = len; |
||||
|
||||
status = ort->CreateEnv(ORT_LOGGING_LEVEL_WARNING, "utun_silero_vad", &vad->env); |
||||
if (status) { vad_report_ort_error(ort, status, "CreateEnv"); goto fail; } |
||||
|
||||
status = ort->CreateSessionOptions(&opts); |
||||
if (status) { vad_report_ort_error(ort, status, "CreateSessionOptions"); goto fail; } |
||||
status = ort->SetIntraOpNumThreads(opts, 1); |
||||
if (status) { vad_report_ort_error(ort, status, "SetIntraOpNumThreads"); } |
||||
status = ort->SetSessionGraphOptimizationLevel(opts, ORT_ENABLE_ALL); |
||||
if (status) { vad_report_ort_error(ort, status, "SetSessionGraphOptimizationLevel"); } |
||||
|
||||
status = ort->CreateSessionFromArray(vad->env, data, len, opts, &vad->session); |
||||
if (status) { vad_report_ort_error(ort, status, "CreateSessionFromArray"); goto fail; } |
||||
|
||||
status = ort->CreateCpuMemoryInfo(OrtArenaAllocator, OrtMemTypeDefault, &vad->mem_info); |
||||
if (status) { vad_report_ort_error(ort, status, "CreateCpuMemoryInfo"); goto fail; } |
||||
|
||||
ort->ReleaseSessionOptions(opts); |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): ok bytes=%zu", label, len); |
||||
return vad; |
||||
|
||||
fail: |
||||
if (opts) ort->ReleaseSessionOptions(opts); |
||||
silero_vad_destroy(vad); |
||||
return NULL; |
||||
} |
||||
|
||||
silero_vad_t* silero_vad_create(const char* model_path) { |
||||
uint8_t* buf = NULL; |
||||
size_t len = 0; |
||||
silero_vad_t* vad; |
||||
|
||||
if (!model_path || !*model_path) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create: empty model path"); |
||||
return NULL; |
||||
} |
||||
if (vad_read_file(model_path, &buf, &len) != 0) return NULL; |
||||
vad = silero_vad_create_from_bytes(buf, len, buf, model_path); |
||||
if (!vad) u_free(buf); |
||||
return vad; |
||||
} |
||||
|
||||
silero_vad_t* silero_vad_create_default(void) { |
||||
return silero_vad_create_from_bytes(silero_vad_model_data, silero_vad_model_len, NULL, "embedded"); |
||||
} |
||||
|
||||
void silero_vad_destroy(silero_vad_t* vad) { |
||||
if (!vad) return; |
||||
const OrtApi* ort = vad->ort; |
||||
if (ort) { |
||||
if (vad->mem_info) ort->ReleaseMemoryInfo(vad->mem_info); |
||||
if (vad->session) ort->ReleaseSession(vad->session); |
||||
if (vad->env) ort->ReleaseEnv(vad->env); |
||||
} |
||||
if (vad->model_data) u_free(vad->model_data); |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "silero_vad_destroy"); |
||||
u_free(vad); |
||||
} |
||||
|
||||
void silero_vad_reset(silero_vad_t* vad) { |
||||
if (!vad) return; |
||||
memset(vad->state, 0, sizeof(vad->state)); |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_VAD, "silero_vad_reset"); |
||||
} |
||||
|
||||
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob) { |
||||
const OrtApi* ort; |
||||
OrtStatus* status; |
||||
OrtValue* in_input = NULL; |
||||
OrtValue* in_state = NULL; |
||||
OrtValue* in_sr = NULL; |
||||
OrtValue* out_prob = NULL; |
||||
OrtValue* out_state = NULL; |
||||
const char* input_names[3]; |
||||
const char* output_names[2]; |
||||
const OrtValue* inputs[3]; |
||||
OrtValue* outputs[2]; |
||||
int64_t shape_input[2] = {1, SILERO_VAD_WINDOW_SAMPLES}; |
||||
int64_t shape_state[3] = {2, 1, 128}; |
||||
int64_t shape_prob[2] = {1, 1}; |
||||
int64_t sr_value = SILERO_VAD_SAMPLE_RATE; |
||||
float prob_local = 0.0f; |
||||
float next_state[SILERO_VAD_STATE_COUNT]; |
||||
int rc = -1; |
||||
|
||||
if (!vad || !samples || !prob) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_process: invalid args"); |
||||
return -1; |
||||
} |
||||
ort = vad->ort; |
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, (void*)samples, |
||||
SILERO_VAD_WINDOW_SAMPLES * sizeof(float), shape_input, 2, |
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &in_input); |
||||
if (status) { vad_report_ort_error(ort, status, "input tensor"); return -1; } |
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, vad->state, |
||||
SILERO_VAD_STATE_COUNT * sizeof(float), shape_state, 3, |
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &in_state); |
||||
if (status) { vad_report_ort_error(ort, status, "state tensor"); goto done; } |
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, &sr_value, |
||||
sizeof(sr_value), NULL, 0, |
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64, &in_sr); |
||||
if (status) { vad_report_ort_error(ort, status, "sr tensor"); goto done; } |
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, &prob_local, |
||||
sizeof(float), shape_prob, 2, |
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &out_prob); |
||||
if (status) { vad_report_ort_error(ort, status, "output tensor"); goto done; } |
||||
|
||||
status = ort->CreateTensorWithDataAsOrtValue(vad->mem_info, next_state, |
||||
SILERO_VAD_STATE_COUNT * sizeof(float), shape_state, 3, |
||||
ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT, &out_state); |
||||
if (status) { vad_report_ort_error(ort, status, "stateN tensor"); goto done; } |
||||
|
||||
input_names[0] = "input"; |
||||
input_names[1] = "state"; |
||||
input_names[2] = "sr"; |
||||
inputs[0] = in_input; |
||||
inputs[1] = in_state; |
||||
inputs[2] = in_sr; |
||||
|
||||
output_names[0] = "output"; |
||||
output_names[1] = "stateN"; |
||||
outputs[0] = out_prob; |
||||
outputs[1] = out_state; |
||||
|
||||
status = ort->Run(vad->session, NULL, input_names, inputs, 3, output_names, 2, outputs); |
||||
if (status) { |
||||
vad_report_ort_error(ort, status, "Run"); |
||||
goto done; |
||||
} |
||||
|
||||
memcpy(vad->state, next_state, sizeof(next_state)); |
||||
*prob = prob_local; |
||||
rc = 0; |
||||
|
||||
done: |
||||
if (in_input) ort->ReleaseValue(in_input); |
||||
if (in_state) ort->ReleaseValue(in_state); |
||||
if (in_sr) ort->ReleaseValue(in_sr); |
||||
if (out_prob) ort->ReleaseValue(out_prob); |
||||
if (out_state) ort->ReleaseValue(out_state); |
||||
return rc; |
||||
} |
||||
|
||||
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob) { |
||||
float samples[SILERO_VAD_WINDOW_SAMPLES]; |
||||
int i; |
||||
|
||||
if (!pcm) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_process_pcm16: pcm == NULL"); |
||||
return -1; |
||||
} |
||||
for (i = 0; i < SILERO_VAD_WINDOW_SAMPLES; i++) { |
||||
samples[i] = (float)pcm[i] / 32768.0f; |
||||
} |
||||
return silero_vad_process(vad, samples, prob); |
||||
} |
||||
|
||||
#else /* !HAVE_SILERO_VAD — пустые стабы (сборки без onnxruntime) */ |
||||
|
||||
silero_vad_t* silero_vad_create(const char* model_path) { |
||||
(void)model_path; |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create: built without onnxruntime (HAVE_SILERO_VAD not defined)"); |
||||
return NULL; |
||||
} |
||||
|
||||
silero_vad_t* silero_vad_create_default(void) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_VAD, "silero_vad_create_default: built without onnxruntime (HAVE_SILERO_VAD not defined)"); |
||||
return NULL; |
||||
} |
||||
|
||||
void silero_vad_destroy(silero_vad_t* vad) { |
||||
(void)vad; |
||||
} |
||||
|
||||
void silero_vad_reset(silero_vad_t* vad) { |
||||
(void)vad; |
||||
} |
||||
|
||||
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob) { |
||||
(void)vad; (void)samples; (void)prob; |
||||
return -1; |
||||
} |
||||
|
||||
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob) { |
||||
(void)vad; (void)pcm; (void)prob; |
||||
return -1; |
||||
} |
||||
|
||||
#endif /* HAVE_SILERO_VAD */ |
||||
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Silero VAD (Voice Activity Detector) — C-обёртка над ONNX Runtime. |
||||
* |
||||
* Запускает официальную стриминговую модель silero_vad.onnx (v5, ~2.3 МБ), |
||||
* которая по окну аудио возвращает вероятность наличия речи [0..1]. |
||||
* Рекуррентное состояние (GRU) хранится внутри объекта и переносится между |
||||
* вызовами, поэтому детектор работает в потоковом (real-time) режиме. |
||||
* |
||||
* Требования к входному аудио: |
||||
* - частота дискретизации 16 кГц (модель поддерживает только 16k); |
||||
* - моно; |
||||
* - окно ровно SILERO_VAD_WINDOW_SAMPLES = 512 сэмплов (32 мс); |
||||
* - float-сэмплы в диапазоне примерно [-1;1] (для int16 есть |
||||
* silero_vad_process_pcm16, который сам делит на 32768). |
||||
* |
||||
* Порог и гистерезис (задержки начала/конца речи) намеренно оставлены |
||||
* вызывающему — радио/звонок сами решают, как интерпретировать вероятность. |
||||
* |
||||
* Зависимость: libonnxruntime (C API). Сборка включается опцией |
||||
* `--with-silero-vad` (см. configure.ac) или макросом HAVE_SILERO_VAD в |
||||
* CMake-сборках. Без HAVE_SILERO_VAD модуль компилируется в пустые стабы |
||||
* (все create-функции возвращают NULL, process — ошибку), чтобы исходники lib/ |
||||
* можно было GLOB-ить во всех сборках (Android/chatgui) без onnxruntime. |
||||
* |
||||
* Лицензия модели: MIT (Silero Team, https://github.com/snakers4/silero-vad).
|
||||
*/ |
||||
#ifndef SILERO_VAD_H |
||||
#define SILERO_VAD_H |
||||
|
||||
#include <stdint.h> |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
/* Фиксированное окно стриминговой модели: 512 сэмплов @ 16 кГц = 32 мс */ |
||||
#define SILERO_VAD_WINDOW_SAMPLES 512 |
||||
#define SILERO_VAD_SAMPLE_RATE 16000 |
||||
|
||||
typedef struct silero_vad silero_vad_t; |
||||
|
||||
/**
|
||||
* Создать детектор, загрузив ONNX-модель из файла model_path. |
||||
* Возвращает NULL при ошибке (подробности — в лог категории "vad"). |
||||
*/ |
||||
silero_vad_t* silero_vad_create(const char* model_path); |
||||
|
||||
/**
|
||||
* Создать детектор из модели, вшитой в бинарник (silero_vad_model_data в |
||||
* silero_vad.c). Не требует файла на диске. NULL при ошибке. |
||||
*/ |
||||
silero_vad_t* silero_vad_create_default(void); |
||||
|
||||
/* Освободить детектор. NULL безопасен. */ |
||||
void silero_vad_destroy(silero_vad_t* vad); |
||||
|
||||
/* Сбросить рекуррентное состояние (начало нового аудиопотока / после длительной тишины). */ |
||||
void silero_vad_reset(silero_vad_t* vad); |
||||
|
||||
/**
|
||||
* Обработать окно из 512 float-сэмплов @ 16 кГц (примерно [-1;1]). |
||||
* При успехе возвращает 0 и заполняет *prob вероятностью речи [0..1]; |
||||
* при ошибке — отрицательное значение (проб не трогается). |
||||
*/ |
||||
int silero_vad_process(silero_vad_t* vad, const float* samples, float* prob); |
||||
|
||||
/**
|
||||
* То же, но вход — int16 PCM: сэмплы преобразуются к float делением на 32768. |
||||
*/ |
||||
int silero_vad_process_pcm16(silero_vad_t* vad, const int16_t* pcm, float* prob); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
|
||||
#endif /* SILERO_VAD_H */ |
||||
Binary file not shown.
@ -0,0 +1,169 @@
|
||||
// speex_aec.c — C-обёртка над SpeexDSP mdf.c (акустическое эхоподавление).
|
||||
//
|
||||
// API см. в speex_aec.h. Ключевые детали реализации:
|
||||
// - собственная линия задержки рендера на depth кадров (SpeexDSP даёт только
|
||||
// фиксированные 2 кадра — PLAYBACK_DELAY в mdf.c);
|
||||
// - синхронный speex_echo_cancellation(rec, play_delayed, out) с выравниванием
|
||||
// рендер↔захват по линии задержки;
|
||||
// - дрейф независимых потоков: переполнение линии → отбрасываем старый кадр,
|
||||
// недозаполнение (старт/underflow) → passthrough без канселлера.
|
||||
//
|
||||
// Захват и рендер приходят чанками разного размера (480/960), поэтому обе
|
||||
// стороны накапливаются до полного кадра (frame_samples) внутри обёртки.
|
||||
|
||||
#include "speex_aec.h" |
||||
#include "debug_config.h" |
||||
#include "mem.h" |
||||
|
||||
#include <string.h> |
||||
|
||||
#include <speex/speex_echo.h> |
||||
|
||||
#define AEC_ID "speex_aec" |
||||
|
||||
struct speex_aec { |
||||
SpeexEchoState* st; |
||||
int frame_samples; /* сэмплов в кадре (960 @48кГц) */ |
||||
int depth; /* глубина линии задержки в кадрах (>=1) */ |
||||
int16_t* line; /* кольцо depth*frame_samples */ |
||||
int head; /* индекс самого старого кадра в line */ |
||||
int count; /* кадров в линии (0..depth) */ |
||||
int16_t* play_acc; /* аккумулятор рендера до кадра */ |
||||
int play_len; |
||||
int16_t* cap_acc; /* аккумулятор захвата до кадра */ |
||||
int cap_len; |
||||
uint32_t overruns; /* отброшено рендер-кадров (дрейф: рендер быстрее) */ |
||||
uint32_t underruns; /* passthrough-кадров (дрейф: рендер медленнее/старт) */ |
||||
}; |
||||
|
||||
/* Положить полный рендер-кадр в линию задержки (вытесняя старейший при переполнении). */ |
||||
static void aec_enqueue(speex_aec_t* a, const int16_t* frame) { |
||||
if (a->count == a->depth) { |
||||
a->head = (a->head + 1) % a->depth; |
||||
a->count--; |
||||
a->overruns++; |
||||
if (a->overruns == 1 || (a->overruns & 0xff) == 0) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_AEC, |
||||
"%s: delay line overflow (render faster than capture) overruns=%u", AEC_ID, a->overruns); |
||||
} |
||||
} |
||||
memcpy(a->line + (size_t)((a->head + a->count) % a->depth) * a->frame_samples, |
||||
frame, (size_t)a->frame_samples * sizeof(int16_t)); |
||||
a->count++; |
||||
} |
||||
|
||||
speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames) { |
||||
speex_aec_t* a; |
||||
int rate; |
||||
|
||||
if (sample_rate <= 0 || frame_samples <= 0 || filter_samples < frame_samples) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: bad args rate=%d frame=%d filter=%d delay=%d", |
||||
AEC_ID, sample_rate, frame_samples, filter_samples, delay_frames); |
||||
return NULL; |
||||
} |
||||
|
||||
a = (speex_aec_t*)u_calloc(1, sizeof(*a)); |
||||
if (!a) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: OOM for state", AEC_ID); |
||||
return NULL; |
||||
} |
||||
|
||||
a->st = speex_echo_state_init(frame_samples, filter_samples); |
||||
rate = sample_rate; |
||||
if (speex_echo_ctl(a->st, SPEEX_ECHO_SET_SAMPLING_RATE, &rate) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: SET_SAMPLING_RATE(%d) failed", AEC_ID, sample_rate); |
||||
speex_echo_state_destroy(a->st); |
||||
u_free(a); |
||||
return NULL; |
||||
} |
||||
|
||||
a->frame_samples = frame_samples; |
||||
a->depth = delay_frames > 0 ? delay_frames : 1; |
||||
a->line = (int16_t*)u_calloc((uint32_t)(a->depth * frame_samples), sizeof(int16_t)); |
||||
a->play_acc = (int16_t*)u_calloc((uint32_t)frame_samples, sizeof(int16_t)); |
||||
a->cap_acc = (int16_t*)u_calloc((uint32_t)frame_samples, sizeof(int16_t)); |
||||
if (!a->line || !a->play_acc || !a->cap_acc) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_AEC, "%s: OOM for buffers", AEC_ID); |
||||
if (a->line) u_free(a->line); |
||||
if (a->play_acc) u_free(a->play_acc); |
||||
if (a->cap_acc) u_free(a->cap_acc); |
||||
speex_echo_state_destroy(a->st); |
||||
u_free(a); |
||||
return NULL; |
||||
} |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_AEC, "%s: created rate=%d frame=%d filter=%d delay=%d frames", |
||||
AEC_ID, sample_rate, frame_samples, filter_samples, a->depth); |
||||
return a; |
||||
} |
||||
|
||||
void speex_aec_destroy(speex_aec_t* a) { |
||||
if (!a) return; |
||||
speex_echo_state_destroy(a->st); |
||||
u_free(a->line); |
||||
u_free(a->play_acc); |
||||
u_free(a->cap_acc); |
||||
u_free(a); |
||||
} |
||||
|
||||
void speex_aec_reset(speex_aec_t* a) { |
||||
if (!a) return; |
||||
speex_echo_state_reset(a->st); |
||||
a->head = 0; |
||||
a->count = 0; |
||||
a->play_len = 0; |
||||
a->cap_len = 0; |
||||
DEBUG_INFO(DEBUG_CATEGORY_AEC, "%s: reset (filter + delay line)", AEC_ID); |
||||
} |
||||
|
||||
void speex_aec_feed_playback(speex_aec_t* a, const int16_t* pcm, int count) { |
||||
int fs, take; |
||||
|
||||
if (!a || !pcm || count <= 0) return; |
||||
fs = a->frame_samples; |
||||
|
||||
take = count; |
||||
if (a->play_len + count > fs) take = fs - a->play_len; |
||||
memcpy(a->play_acc + a->play_len, pcm, (size_t)take * sizeof(int16_t)); |
||||
a->play_len += take; |
||||
if (a->play_len < fs) return; |
||||
|
||||
aec_enqueue(a, a->play_acc); |
||||
a->play_len = 0; |
||||
} |
||||
|
||||
int speex_aec_process_capture(speex_aec_t* a, const int16_t* pcm, int count, int16_t* out) { |
||||
int fs, take; |
||||
|
||||
if (!a || !pcm || !out || count <= 0) return 0; |
||||
fs = a->frame_samples; |
||||
|
||||
take = count; |
||||
if (a->cap_len + count > fs) take = fs - a->cap_len; |
||||
memcpy(a->cap_acc + a->cap_len, pcm, (size_t)take * sizeof(int16_t)); |
||||
a->cap_len += take; |
||||
if (a->cap_len < fs) return 0; |
||||
|
||||
if (a->count == a->depth) { |
||||
const int16_t* ref = a->line + (size_t)a->head * fs; |
||||
speex_echo_cancellation(a->st, a->cap_acc, ref, out); |
||||
a->head = (a->head + 1) % a->depth; |
||||
a->count--; |
||||
} else { |
||||
/* линия не наполнена (старт или рендер отстаёт): passthrough без канселлера */ |
||||
if (a->underruns == 0 || (a->underruns & 0xff) == 0) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_AEC, |
||||
"%s: delay line underflow (render slower than capture) underruns=%u fill=%d/%d", |
||||
AEC_ID, a->underruns, a->count, a->depth); |
||||
} |
||||
memcpy(out, a->cap_acc, (size_t)fs * sizeof(int16_t)); |
||||
a->underruns++; |
||||
} |
||||
|
||||
a->cap_len = 0; |
||||
return fs; |
||||
} |
||||
|
||||
int speex_aec_delay_fill(const speex_aec_t* a) { |
||||
return a ? a->count : 0; |
||||
} |
||||
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* speex_aec.h — акустическое эхоподавление (AEC), C-обёртка над SpeexDSP mdf.c. |
||||
* |
||||
* Подавляет эхо дальнего конца (то, что играем в динамик) в сигнале микрофона |
||||
* перед кодированием. Работает на int16 PCM, один канал, частота 48000 (можно |
||||
* 8000/16000/32000/48000), кадр 20 мс (960 сэмплов @48 кГц). |
||||
* |
||||
* Модель использования (duplex-контур звонка): |
||||
* - рендер (far-end, то что пошло в динамик) → speex_aec_feed_playback(); |
||||
* - захват (near-end, микрофон) → speex_aec_process_capture(). |
||||
* |
||||
* Обёртка держит собственную линию задержки рендера на `delay_frames` кадров и |
||||
* зовёт синхронный speex_echo_cancellation(rec, play_delayed, out). Это делает |
||||
* выравнивание рендер↔захват предсказуемым и настраиваемым (встроенный буфер |
||||
* SpeexDSP фиксирован в 2 кадра и для Android-задержки не годится). |
||||
* |
||||
* Дрейф двух независимых потоков (capture/play на Android) компенсируется |
||||
* ограниченной глубиной линии: переполнение → отбрасываем старый рендер-кадр, |
||||
* недозаполнение → passthrough без канселлера (с подробным логом категории "aec"). |
||||
* |
||||
* Зависимости: SpeexDSP (mdf.c/fftwrap.c/kiss_fft*) из lib/speexdsp, флаги |
||||
* FLOATING_POINT + USE_KISS_FFT. Собирается всегда (без внешних зависимостей). |
||||
* |
||||
* Лицензия SpeexDSP: 3-clause BSD (Xiph) — см. lib/speexdsp/COPYING. |
||||
*/ |
||||
#ifndef SPEEX_AEC_H |
||||
#define SPEEX_AEC_H |
||||
|
||||
#include <stdint.h> |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
typedef struct speex_aec speex_aec_t; |
||||
|
||||
/**
|
||||
* Создать эхоканселлер. |
||||
* sample_rate — 48000 (также 8000/16000/32000); |
||||
* frame_samples — сэмплов в кадре (960 = 20 мс @48 кГц); |
||||
* filter_samples— длина эхо-хвоста в сэмплах (14400 = 300 мс, кратно кадру); |
||||
* delay_frames — задержка рендер→захват в кадрах (>=0; 0 = без линии задержки). |
||||
* Возвращает NULL при ошибке (лог категории "aec"). |
||||
*/ |
||||
speex_aec_t* speex_aec_create(int sample_rate, int frame_samples, int filter_samples, int delay_frames); |
||||
|
||||
/* Освободить канселлер. NULL безопасен. */ |
||||
void speex_aec_destroy(speex_aec_t* aec); |
||||
|
||||
/* Сбросить адаптивный фильтр и линию задержки (смена устройства/роута). */ |
||||
void speex_aec_reset(speex_aec_t* aec); |
||||
|
||||
/**
|
||||
* Подать рендер (дальний конец). Накопление до кадра внутри; полный кадр |
||||
* кладётся в линию задержки. count может быть любым (обычно 480 или 960). |
||||
*/ |
||||
void speex_aec_feed_playback(speex_aec_t* aec, const int16_t* pcm, int count); |
||||
|
||||
/**
|
||||
* Обработать захват (микрофон). Накопление до кадра внутри. Когда кадр собран — |
||||
* подавляет эхо (с учётом линии задержки) и пишет результат в out. |
||||
* Возвращает число сэмплов, записанных в out (frame_samples, если кадр готов; |
||||
* 0 — кадр ещё накапливается). out не должен алиаситься с pcm. |
||||
*/ |
||||
int speex_aec_process_capture(speex_aec_t* aec, const int16_t* pcm, int count, int16_t* out); |
||||
|
||||
/* Актуальная глубина линии задержки (кадров) — для диагностики дрейфа. */ |
||||
int speex_aec_delay_fill(const speex_aec_t* aec); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
|
||||
#endif /* SPEEX_AEC_H */ |
||||
@ -0,0 +1,35 @@
|
||||
Copyright 2002-2008 Xiph.org Foundation |
||||
Copyright 2002-2008 Jean-Marc Valin |
||||
Copyright 2005-2007 Analog Devices Inc. |
||||
Copyright 2005-2008 Commonwealth Scientific and Industrial Research |
||||
Organisation (CSIRO) |
||||
Copyright 1993, 2002, 2006 David Rowe |
||||
Copyright 2003 EpicGames |
||||
Copyright 1992-1994 Jutta Degener, Carsten Bormann |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions |
||||
are met: |
||||
|
||||
- Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
|
||||
- Redistributions in binary form must reproduce the above copyright |
||||
notice, this list of conditions and the following disclaimer in the |
||||
documentation and/or other materials provided with the distribution. |
||||
|
||||
- Neither the name of the Xiph.org Foundation nor the names of its |
||||
contributors may be used to endorse or promote products derived from |
||||
this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR |
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
@ -0,0 +1,160 @@
|
||||
/*
|
||||
Copyright (c) 2003-2004, Mark Borgerding |
||||
|
||||
All rights reserved. |
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: |
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. |
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. |
||||
* Neither the author nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
#define MIN(a,b) ((a)<(b) ? (a):(b)) |
||||
#define MAX(a,b) ((a)>(b) ? (a):(b)) |
||||
|
||||
/* kiss_fft.h
|
||||
defines kiss_fft_scalar as either short or a float type |
||||
and defines |
||||
typedef struct { kiss_fft_scalar r; kiss_fft_scalar i; }kiss_fft_cpx; */ |
||||
#include "kiss_fft.h" |
||||
#include "math_approx.h" |
||||
|
||||
#define MAXFACTORS 32 |
||||
/* e.g. an fft of length 128 has 4 factors
|
||||
as far as kissfft is concerned |
||||
4*4*4*2 |
||||
*/ |
||||
|
||||
struct kiss_fft_state{ |
||||
int nfft; |
||||
int inverse; |
||||
int factors[2*MAXFACTORS]; |
||||
kiss_fft_cpx twiddles[1]; |
||||
}; |
||||
|
||||
/*
|
||||
Explanation of macros dealing with complex math: |
||||
|
||||
C_MUL(m,a,b) : m = a*b |
||||
C_FIXDIV( c , div ) : if a fixed point impl., c /= div. noop otherwise |
||||
C_SUB( res, a,b) : res = a - b |
||||
C_SUBFROM( res , a) : res -= a |
||||
C_ADDTO( res , a) : res += a |
||||
* */ |
||||
#ifdef FIXED_POINT |
||||
#include "arch.h" |
||||
# define FRACBITS 15 |
||||
# define SAMPPROD spx_int32_t |
||||
#define SAMP_MAX 32767 |
||||
|
||||
#define SAMP_MIN -SAMP_MAX |
||||
|
||||
#if defined(CHECK_OVERFLOW) |
||||
# define CHECK_OVERFLOW_OP(a,op,b) \ |
||||
if ( (SAMPPROD)(a) op (SAMPPROD)(b) > SAMP_MAX || (SAMPPROD)(a) op (SAMPPROD)(b) < SAMP_MIN ) { \
|
||||
fprintf(stderr,"WARNING:overflow @ " __FILE__ "(%d): (%d " #op" %d) = %ld\n",__LINE__,(a),(b),(SAMPPROD)(a) op (SAMPPROD)(b) ); } |
||||
#endif |
||||
|
||||
|
||||
# define smul(a,b) ( (SAMPPROD)(a)*(b) ) |
||||
# define sround( x ) (kiss_fft_scalar)( ( (x) + (1<<(FRACBITS-1)) ) >> FRACBITS ) |
||||
|
||||
# define S_MUL(a,b) sround( smul(a,b) ) |
||||
|
||||
# define C_MUL(m,a,b) \ |
||||
do{ (m).r = sround( smul((a).r,(b).r) - smul((a).i,(b).i) ); \
|
||||
(m).i = sround( smul((a).r,(b).i) + smul((a).i,(b).r) ); }while(0) |
||||
|
||||
# define C_MUL4(m,a,b) \ |
||||
do{ (m).r = PSHR32( smul((a).r,(b).r) - smul((a).i,(b).i),17 ); \
|
||||
(m).i = PSHR32( smul((a).r,(b).i) + smul((a).i,(b).r),17 ); }while(0) |
||||
|
||||
# define DIVSCALAR(x,k) \ |
||||
(x) = sround( smul( x, SAMP_MAX/k ) ) |
||||
|
||||
# define C_FIXDIV(c,div) \ |
||||
do { DIVSCALAR( (c).r , div); \
|
||||
DIVSCALAR( (c).i , div); }while (0) |
||||
|
||||
# define C_MULBYSCALAR( c, s ) \ |
||||
do{ (c).r = sround( smul( (c).r , s ) ) ;\
|
||||
(c).i = sround( smul( (c).i , s ) ) ; }while(0) |
||||
|
||||
#else /* not FIXED_POINT*/ |
||||
|
||||
# define S_MUL(a,b) ( (a)*(b) ) |
||||
#define C_MUL(m,a,b) \ |
||||
do{ (m).r = (a).r*(b).r - (a).i*(b).i;\
|
||||
(m).i = (a).r*(b).i + (a).i*(b).r; }while(0) |
||||
|
||||
#define C_MUL4(m,a,b) C_MUL(m,a,b) |
||||
|
||||
# define C_FIXDIV(c,div) /* NOOP */ |
||||
# define C_MULBYSCALAR( c, s ) \ |
||||
do{ (c).r *= (s);\
|
||||
(c).i *= (s); }while(0) |
||||
#endif |
||||
|
||||
#ifndef CHECK_OVERFLOW_OP |
||||
# define CHECK_OVERFLOW_OP(a,op,b) /* noop */ |
||||
#endif |
||||
|
||||
#define C_ADD( res, a,b)\ |
||||
do { \
|
||||
CHECK_OVERFLOW_OP((a).r,+,(b).r)\
|
||||
CHECK_OVERFLOW_OP((a).i,+,(b).i)\
|
||||
(res).r=(a).r+(b).r; (res).i=(a).i+(b).i; \
|
||||
}while(0) |
||||
#define C_SUB( res, a,b)\ |
||||
do { \
|
||||
CHECK_OVERFLOW_OP((a).r,-,(b).r)\
|
||||
CHECK_OVERFLOW_OP((a).i,-,(b).i)\
|
||||
(res).r=(a).r-(b).r; (res).i=(a).i-(b).i; \
|
||||
}while(0) |
||||
#define C_ADDTO( res , a)\ |
||||
do { \
|
||||
CHECK_OVERFLOW_OP((res).r,+,(a).r)\
|
||||
CHECK_OVERFLOW_OP((res).i,+,(a).i)\
|
||||
(res).r += (a).r; (res).i += (a).i;\
|
||||
}while(0) |
||||
|
||||
#define C_SUBFROM( res , a)\ |
||||
do {\
|
||||
CHECK_OVERFLOW_OP((res).r,-,(a).r)\
|
||||
CHECK_OVERFLOW_OP((res).i,-,(a).i)\
|
||||
(res).r -= (a).r; (res).i -= (a).i; \
|
||||
}while(0) |
||||
|
||||
|
||||
#ifdef FIXED_POINT |
||||
# define KISS_FFT_COS(phase) floor(MIN(32767,MAX(-32767,.5+32768 * cos (phase)))) |
||||
# define KISS_FFT_SIN(phase) floor(MIN(32767,MAX(-32767,.5+32768 * sin (phase)))) |
||||
# define HALF_OF(x) ((x)>>1) |
||||
#elif defined(USE_SIMD) |
||||
# define KISS_FFT_COS(phase) _mm_set1_ps( cos(phase) ) |
||||
# define KISS_FFT_SIN(phase) _mm_set1_ps( sin(phase) ) |
||||
# define HALF_OF(x) ((x)*_mm_set1_ps(.5)) |
||||
#else |
||||
# define KISS_FFT_COS(phase) (kiss_fft_scalar) cos(phase) |
||||
# define KISS_FFT_SIN(phase) (kiss_fft_scalar) sin(phase) |
||||
# define HALF_OF(x) ((x)*.5) |
||||
#endif |
||||
|
||||
#define kf_cexp(x,phase) \ |
||||
do{ \
|
||||
(x)->r = KISS_FFT_COS(phase);\
|
||||
(x)->i = KISS_FFT_SIN(phase);\
|
||||
}while(0) |
||||
#define kf_cexp2(x,phase) \ |
||||
do{ \
|
||||
(x)->r = spx_cos_norm((phase));\
|
||||
(x)->i = spx_cos_norm((phase)-32768);\
|
||||
}while(0) |
||||
|
||||
|
||||
/* a debugging function */ |
||||
#define pcpx(c)\ |
||||
fprintf(stderr,"%g + %gi\n",(double)((c)->r),(double)((c)->i) ) |
||||
@ -0,0 +1,232 @@
|
||||
/* Copyright (C) 2003 Jean-Marc Valin */ |
||||
/**
|
||||
@file arch.h |
||||
@brief Various architecture definitions Speex |
||||
*/ |
||||
/*
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions |
||||
are met: |
||||
|
||||
- Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
|
||||
- Redistributions in binary form must reproduce the above copyright |
||||
notice, this list of conditions and the following disclaimer in the |
||||
documentation and/or other materials provided with the distribution. |
||||
|
||||
- Neither the name of the Xiph.org Foundation nor the names of its |
||||
contributors may be used to endorse or promote products derived from |
||||
this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR |
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
#ifndef ARCH_H |
||||
#define ARCH_H |
||||
|
||||
/* A couple test to catch stupid option combinations */ |
||||
#ifdef FIXED_POINT |
||||
|
||||
#ifdef FLOATING_POINT |
||||
#error You cannot compile as floating point and fixed point at the same time |
||||
#endif |
||||
#ifdef USE_SSE |
||||
#error SSE is only for floating-point |
||||
#endif |
||||
#if defined(ARM4_ASM) + defined(ARM5E_ASM) + defined(BFIN_ASM) > 1 |
||||
#error Make up your mind. What CPU do you have? |
||||
#endif |
||||
#ifdef VORBIS_PSYCHO |
||||
#error Vorbis-psy model currently not implemented in fixed-point |
||||
#endif |
||||
|
||||
#else |
||||
|
||||
#ifndef FLOATING_POINT |
||||
#error You now need to define either FIXED_POINT or FLOATING_POINT |
||||
#endif |
||||
#if defined(ARM4_ASM) || defined(ARM5E_ASM) || defined(BFIN_ASM) |
||||
#error I suppose you can have a [ARM4/ARM5E/Blackfin] that has float instructions? |
||||
#endif |
||||
#ifdef FIXED_DEBUG |
||||
#error "Don't you think enabling fixed-point is a good thing to do if you want to debug that?" |
||||
#endif |
||||
|
||||
|
||||
#endif |
||||
|
||||
#ifndef OUTSIDE_SPEEX |
||||
#include "speex/speexdsp_types.h" |
||||
#endif |
||||
|
||||
#define ABS(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute integer value. */ |
||||
#define ABS16(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute 16-bit value. */ |
||||
#define MIN16(a,b) ((a) < (b) ? (a) : (b)) /**< Maximum 16-bit value. */ |
||||
#define MAX16(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 16-bit value. */ |
||||
#define ABS32(x) ((x) < 0 ? (-(x)) : (x)) /**< Absolute 32-bit value. */ |
||||
#define MIN32(a,b) ((a) < (b) ? (a) : (b)) /**< Maximum 32-bit value. */ |
||||
#define MAX32(a,b) ((a) > (b) ? (a) : (b)) /**< Maximum 32-bit value. */ |
||||
|
||||
#ifdef FIXED_POINT |
||||
|
||||
typedef spx_int16_t spx_word16_t; |
||||
typedef spx_int32_t spx_word32_t; |
||||
typedef spx_word32_t spx_mem_t; |
||||
typedef spx_word16_t spx_coef_t; |
||||
typedef spx_word16_t spx_lsp_t; |
||||
typedef spx_word32_t spx_sig_t; |
||||
|
||||
#define Q15ONE 32767 |
||||
|
||||
#define LPC_SCALING 8192 |
||||
#define SIG_SCALING 16384 |
||||
#define LSP_SCALING 8192. |
||||
#define GAMMA_SCALING 32768. |
||||
#define GAIN_SCALING 64 |
||||
#define GAIN_SCALING_1 0.015625 |
||||
|
||||
#define LPC_SHIFT 13 |
||||
#define LSP_SHIFT 13 |
||||
#define SIG_SHIFT 14 |
||||
#define GAIN_SHIFT 6 |
||||
|
||||
#define WORD2INT(x) ((x) < -32767 ? -32768 : ((x) > 32766 ? 32767 : (x))) |
||||
|
||||
#define VERY_SMALL 0 |
||||
#define VERY_LARGE32 ((spx_word32_t)2147483647) |
||||
#define VERY_LARGE16 ((spx_word16_t)32767) |
||||
#define Q15_ONE ((spx_word16_t)32767) |
||||
|
||||
|
||||
#ifdef FIXED_DEBUG |
||||
#include "fixed_debug.h" |
||||
#else |
||||
|
||||
#include "fixed_generic.h" |
||||
|
||||
#ifdef ARM5E_ASM |
||||
#include "fixed_arm5e.h" |
||||
#elif defined(ARM4_ASM) |
||||
#include "fixed_arm4.h" |
||||
#elif defined(BFIN_ASM) |
||||
#include "fixed_bfin.h" |
||||
#endif |
||||
|
||||
#endif |
||||
|
||||
|
||||
#else |
||||
|
||||
typedef float spx_mem_t; |
||||
typedef float spx_coef_t; |
||||
typedef float spx_lsp_t; |
||||
typedef float spx_sig_t; |
||||
typedef float spx_word16_t; |
||||
typedef float spx_word32_t; |
||||
|
||||
#define Q15ONE 1.0f |
||||
#define LPC_SCALING 1.f |
||||
#define SIG_SCALING 1.f |
||||
#define LSP_SCALING 1.f |
||||
#define GAMMA_SCALING 1.f |
||||
#define GAIN_SCALING 1.f |
||||
#define GAIN_SCALING_1 1.f |
||||
|
||||
|
||||
#define VERY_SMALL 1e-15f |
||||
#define VERY_LARGE32 1e15f |
||||
#define VERY_LARGE16 1e15f |
||||
#define Q15_ONE ((spx_word16_t)1.f) |
||||
|
||||
#define QCONST16(x,bits) (x) |
||||
#define QCONST32(x,bits) (x) |
||||
|
||||
#define NEG16(x) (-(x)) |
||||
#define NEG32(x) (-(x)) |
||||
#define EXTRACT16(x) (x) |
||||
#define EXTEND32(x) (x) |
||||
#define SHR16(a,shift) (a) |
||||
#define SHL16(a,shift) (a) |
||||
#define SHR32(a,shift) (a) |
||||
#define SHL32(a,shift) (a) |
||||
#define PSHR16(a,shift) (a) |
||||
#define PSHR32(a,shift) (a) |
||||
#define VSHR32(a,shift) (a) |
||||
#define SATURATE16(x,a) (x) |
||||
#define SATURATE32(x,a) (x) |
||||
#define SATURATE32PSHR(x,shift,a) (x) |
||||
|
||||
#define PSHR(a,shift) (a) |
||||
#define SHR(a,shift) (a) |
||||
#define SHL(a,shift) (a) |
||||
#define SATURATE(x,a) (x) |
||||
|
||||
#define ADD16(a,b) ((a)+(b)) |
||||
#define SUB16(a,b) ((a)-(b)) |
||||
#define ADD32(a,b) ((a)+(b)) |
||||
#define SUB32(a,b) ((a)-(b)) |
||||
#define MULT16_16_16(a,b) ((a)*(b)) |
||||
#define MULT16_32_32(a,b) ((a)*(b)) |
||||
#define MULT16_16(a,b) ((spx_word32_t)(a)*(spx_word32_t)(b)) |
||||
#define MAC16_16(c,a,b) ((c)+(spx_word32_t)(a)*(spx_word32_t)(b)) |
||||
|
||||
#define MULT16_32_Q15(a,b) ((a)*(b)) |
||||
#define MULT16_32_P15(a,b) ((a)*(b)) |
||||
|
||||
#define MAC16_32_Q15(c,a,b) ((c)+(a)*(b)) |
||||
|
||||
#define MAC16_16_Q11(c,a,b) ((c)+(a)*(b)) |
||||
#define MAC16_16_Q13(c,a,b) ((c)+(a)*(b)) |
||||
#define MAC16_16_P13(c,a,b) ((c)+(a)*(b)) |
||||
#define MULT16_16_Q11_32(a,b) ((a)*(b)) |
||||
#define MULT16_16_Q13(a,b) ((a)*(b)) |
||||
#define MULT16_16_Q14(a,b) ((a)*(b)) |
||||
#define MULT16_16_Q15(a,b) ((a)*(b)) |
||||
#define MULT16_16_P15(a,b) ((a)*(b)) |
||||
#define MULT16_16_P13(a,b) ((a)*(b)) |
||||
#define MULT16_16_P14(a,b) ((a)*(b)) |
||||
|
||||
#define DIV32_16(a,b) (((spx_word32_t)(a))/(spx_word16_t)(b)) |
||||
#define PDIV32_16(a,b) (((spx_word32_t)(a))/(spx_word16_t)(b)) |
||||
#define DIV32(a,b) (((spx_word32_t)(a))/(spx_word32_t)(b)) |
||||
#define PDIV32(a,b) (((spx_word32_t)(a))/(spx_word32_t)(b)) |
||||
|
||||
#define WORD2INT(x) ((x) < -32767.5f ? -32768 : \ |
||||
((x) > 32766.5f ? 32767 : (spx_int16_t)floor(.5 + (x)))) |
||||
#endif |
||||
|
||||
|
||||
#if defined(CONFIG_TI_C54X) || defined(CONFIG_TI_C55X) |
||||
|
||||
/* 2 on TI C5x DSP */ |
||||
#define BYTES_PER_CHAR 2 |
||||
#define BITS_PER_CHAR 16 |
||||
#define LOG2_BITS_PER_CHAR 4 |
||||
|
||||
#else |
||||
|
||||
#define BYTES_PER_CHAR 1 |
||||
#define BITS_PER_CHAR 8 |
||||
#define LOG2_BITS_PER_CHAR 3 |
||||
|
||||
#endif |
||||
|
||||
|
||||
|
||||
#ifdef FIXED_DEBUG |
||||
extern long long spx_mips; |
||||
#endif |
||||
|
||||
|
||||
#endif |
||||
@ -0,0 +1,448 @@
|
||||
/* Copyright (C) 2005-2006 Jean-Marc Valin
|
||||
File: fftwrap.c |
||||
|
||||
Wrapper for various FFTs |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions |
||||
are met: |
||||
|
||||
- Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
|
||||
- Redistributions in binary form must reproduce the above copyright |
||||
notice, this list of conditions and the following disclaimer in the |
||||
documentation and/or other materials provided with the distribution. |
||||
|
||||
- Neither the name of the Xiph.org Foundation nor the names of its |
||||
contributors may be used to endorse or promote products derived from |
||||
this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR |
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
|
||||
*/ |
||||
|
||||
#ifdef HAVE_CONFIG_H |
||||
#include "config.h" |
||||
#endif |
||||
|
||||
#include "arch.h" |
||||
#include "os_support.h" |
||||
|
||||
#define MAX_FFT_SIZE 2048 |
||||
|
||||
#ifdef FIXED_POINT |
||||
static int maximize_range(spx_word16_t *in, spx_word16_t *out, spx_word16_t bound, int len) |
||||
{ |
||||
int i, shift; |
||||
spx_word16_t max_val = 0; |
||||
for (i=0;i<len;i++) |
||||
{ |
||||
if (in[i]>max_val) |
||||
max_val = in[i]; |
||||
if (-in[i]>max_val) |
||||
max_val = -in[i]; |
||||
} |
||||
shift=0; |
||||
while (max_val <= (bound>>1) && max_val != 0) |
||||
{ |
||||
max_val <<= 1; |
||||
shift++; |
||||
} |
||||
for (i=0;i<len;i++) |
||||
{ |
||||
out[i] = SHL16(in[i], shift); |
||||
} |
||||
return shift; |
||||
} |
||||
|
||||
static void renorm_range(spx_word16_t *in, spx_word16_t *out, int shift, int len) |
||||
{ |
||||
int i; |
||||
for (i=0;i<len;i++) |
||||
{ |
||||
out[i] = PSHR16(in[i], shift); |
||||
} |
||||
} |
||||
#endif |
||||
|
||||
#ifdef USE_SMALLFT |
||||
|
||||
#include "smallft.h" |
||||
#include <math.h> |
||||
|
||||
void *spx_fft_init(int size) |
||||
{ |
||||
struct drft_lookup *table; |
||||
table = speex_alloc(sizeof(struct drft_lookup)); |
||||
spx_drft_init((struct drft_lookup *)table, size); |
||||
return (void*)table; |
||||
} |
||||
|
||||
void spx_fft_destroy(void *table) |
||||
{ |
||||
spx_drft_clear(table); |
||||
speex_free(table); |
||||
} |
||||
|
||||
void spx_fft(void *table, float *in, float *out) |
||||
{ |
||||
if (in==out) |
||||
{ |
||||
int i; |
||||
float scale = 1./((struct drft_lookup *)table)->n; |
||||
speex_warning("FFT should not be done in-place"); |
||||
for (i=0;i<((struct drft_lookup *)table)->n;i++) |
||||
out[i] = scale*in[i]; |
||||
} else { |
||||
int i; |
||||
float scale = 1./((struct drft_lookup *)table)->n; |
||||
for (i=0;i<((struct drft_lookup *)table)->n;i++) |
||||
out[i] = scale*in[i]; |
||||
} |
||||
spx_drft_forward((struct drft_lookup *)table, out); |
||||
} |
||||
|
||||
void spx_ifft(void *table, float *in, float *out) |
||||
{ |
||||
if (in==out) |
||||
{ |
||||
speex_warning("FFT should not be done in-place"); |
||||
} else { |
||||
int i; |
||||
for (i=0;i<((struct drft_lookup *)table)->n;i++) |
||||
out[i] = in[i]; |
||||
} |
||||
spx_drft_backward((struct drft_lookup *)table, out); |
||||
} |
||||
|
||||
#elif defined(USE_INTEL_MKL) |
||||
#include <mkl.h> |
||||
|
||||
struct mkl_config { |
||||
DFTI_DESCRIPTOR_HANDLE desc; |
||||
int N; |
||||
}; |
||||
|
||||
void *spx_fft_init(int size) |
||||
{ |
||||
struct mkl_config *table = (struct mkl_config *) speex_alloc(sizeof(struct mkl_config)); |
||||
table->N = size; |
||||
DftiCreateDescriptor(&table->desc, DFTI_SINGLE, DFTI_REAL, 1, size); |
||||
DftiSetValue(table->desc, DFTI_PACKED_FORMAT, DFTI_PACK_FORMAT); |
||||
DftiSetValue(table->desc, DFTI_PLACEMENT, DFTI_NOT_INPLACE); |
||||
DftiSetValue(table->desc, DFTI_FORWARD_SCALE, 1.0f / size); |
||||
DftiCommitDescriptor(table->desc); |
||||
return table; |
||||
} |
||||
|
||||
void spx_fft_destroy(void *table) |
||||
{ |
||||
struct mkl_config *t = (struct mkl_config *) table; |
||||
DftiFreeDescriptor(t->desc); |
||||
speex_free(table); |
||||
} |
||||
|
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out) |
||||
{ |
||||
struct mkl_config *t = (struct mkl_config *) table; |
||||
DftiComputeForward(t->desc, in, out); |
||||
} |
||||
|
||||
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out) |
||||
{ |
||||
struct mkl_config *t = (struct mkl_config *) table; |
||||
DftiComputeBackward(t->desc, in, out); |
||||
} |
||||
|
||||
#elif defined(USE_INTEL_IPP) |
||||
|
||||
#include <ipps.h> |
||||
|
||||
struct ipp_fft_config |
||||
{ |
||||
IppsDFTSpec_R_32f *dftSpec; |
||||
Ipp8u *buffer; |
||||
}; |
||||
|
||||
void *spx_fft_init(int size) |
||||
{ |
||||
int bufferSize = 0; |
||||
int hint; |
||||
struct ipp_fft_config *table; |
||||
|
||||
table = (struct ipp_fft_config *)speex_alloc(sizeof(struct ipp_fft_config)); |
||||
|
||||
/* there appears to be no performance difference between ippAlgHintFast and
|
||||
ippAlgHintAccurate when using the with the floating point version |
||||
of the fft. */ |
||||
hint = ippAlgHintAccurate; |
||||
|
||||
ippsDFTInitAlloc_R_32f(&table->dftSpec, size, IPP_FFT_DIV_FWD_BY_N, hint); |
||||
|
||||
ippsDFTGetBufSize_R_32f(table->dftSpec, &bufferSize); |
||||
table->buffer = ippsMalloc_8u(bufferSize); |
||||
|
||||
return table; |
||||
} |
||||
|
||||
void spx_fft_destroy(void *table) |
||||
{ |
||||
struct ipp_fft_config *t = (struct ipp_fft_config *)table; |
||||
ippsFree(t->buffer); |
||||
ippsDFTFree_R_32f(t->dftSpec); |
||||
speex_free(t); |
||||
} |
||||
|
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out) |
||||
{ |
||||
struct ipp_fft_config *t = (struct ipp_fft_config *)table; |
||||
ippsDFTFwd_RToPack_32f(in, out, t->dftSpec, t->buffer); |
||||
} |
||||
|
||||
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out) |
||||
{ |
||||
struct ipp_fft_config *t = (struct ipp_fft_config *)table; |
||||
ippsDFTInv_PackToR_32f(in, out, t->dftSpec, t->buffer); |
||||
} |
||||
|
||||
#elif defined(USE_GPL_FFTW3) |
||||
|
||||
#include <fftw3.h> |
||||
|
||||
struct fftw_config { |
||||
float *in; |
||||
float *out; |
||||
fftwf_plan fft; |
||||
fftwf_plan ifft; |
||||
int N; |
||||
}; |
||||
|
||||
void *spx_fft_init(int size) |
||||
{ |
||||
struct fftw_config *table = (struct fftw_config *) speex_alloc(sizeof(struct fftw_config)); |
||||
table->in = fftwf_malloc(sizeof(float) * (size+2)); |
||||
table->out = fftwf_malloc(sizeof(float) * (size+2)); |
||||
|
||||
table->fft = fftwf_plan_dft_r2c_1d(size, table->in, (fftwf_complex *) table->out, FFTW_PATIENT); |
||||
table->ifft = fftwf_plan_dft_c2r_1d(size, (fftwf_complex *) table->in, table->out, FFTW_PATIENT); |
||||
|
||||
table->N = size; |
||||
return table; |
||||
} |
||||
|
||||
void spx_fft_destroy(void *table) |
||||
{ |
||||
struct fftw_config *t = (struct fftw_config *) table; |
||||
fftwf_destroy_plan(t->fft); |
||||
fftwf_destroy_plan(t->ifft); |
||||
fftwf_free(t->in); |
||||
fftwf_free(t->out); |
||||
speex_free(table); |
||||
} |
||||
|
||||
|
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out) |
||||
{ |
||||
int i; |
||||
struct fftw_config *t = (struct fftw_config *) table; |
||||
const int N = t->N; |
||||
float *iptr = t->in; |
||||
float *optr = t->out; |
||||
const float m = 1.0 / N; |
||||
for(i=0;i<N;++i) |
||||
iptr[i]=in[i] * m; |
||||
|
||||
fftwf_execute(t->fft); |
||||
|
||||
out[0] = optr[0]; |
||||
for(i=1;i<N;++i) |
||||
out[i] = optr[i+1]; |
||||
} |
||||
|
||||
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out) |
||||
{ |
||||
int i; |
||||
struct fftw_config *t = (struct fftw_config *) table; |
||||
const int N = t->N; |
||||
float *iptr = t->in; |
||||
float *optr = t->out; |
||||
|
||||
iptr[0] = in[0]; |
||||
iptr[1] = 0.0f; |
||||
for(i=1;i<N;++i) |
||||
iptr[i+1] = in[i]; |
||||
iptr[N+1] = 0.0f; |
||||
|
||||
fftwf_execute(t->ifft); |
||||
|
||||
for(i=0;i<N;++i) |
||||
out[i] = optr[i]; |
||||
} |
||||
|
||||
#elif defined(USE_KISS_FFT) |
||||
|
||||
#include "kiss_fftr.h" |
||||
#include "kiss_fft.h" |
||||
|
||||
struct kiss_config { |
||||
kiss_fftr_cfg forward; |
||||
kiss_fftr_cfg backward; |
||||
int N; |
||||
}; |
||||
|
||||
void *spx_fft_init(int size) |
||||
{ |
||||
struct kiss_config *table; |
||||
table = (struct kiss_config*)speex_alloc(sizeof(struct kiss_config)); |
||||
table->forward = kiss_fftr_alloc(size,0,NULL,NULL); |
||||
table->backward = kiss_fftr_alloc(size,1,NULL,NULL); |
||||
table->N = size; |
||||
return table; |
||||
} |
||||
|
||||
void spx_fft_destroy(void *table) |
||||
{ |
||||
struct kiss_config *t = (struct kiss_config *)table; |
||||
kiss_fftr_free(t->forward); |
||||
kiss_fftr_free(t->backward); |
||||
speex_free(table); |
||||
} |
||||
|
||||
#ifdef FIXED_POINT |
||||
|
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out) |
||||
{ |
||||
int shift; |
||||
struct kiss_config *t = (struct kiss_config *)table; |
||||
shift = maximize_range(in, in, 32000, t->N); |
||||
kiss_fftr2(t->forward, in, out); |
||||
renorm_range(in, in, shift, t->N); |
||||
renorm_range(out, out, shift, t->N); |
||||
} |
||||
|
||||
#else |
||||
|
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out) |
||||
{ |
||||
int i; |
||||
float scale; |
||||
struct kiss_config *t = (struct kiss_config *)table; |
||||
scale = 1./t->N; |
||||
kiss_fftr2(t->forward, in, out); |
||||
for (i=0;i<t->N;i++) |
||||
out[i] *= scale; |
||||
} |
||||
#endif |
||||
|
||||
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out) |
||||
{ |
||||
struct kiss_config *t = (struct kiss_config *)table; |
||||
kiss_fftri2(t->backward, in, out); |
||||
} |
||||
|
||||
|
||||
#else |
||||
|
||||
#error No other FFT implemented |
||||
|
||||
#endif |
||||
|
||||
|
||||
#ifdef FIXED_POINT |
||||
/*#include "smallft.h"*/ |
||||
|
||||
|
||||
void spx_fft_float(void *table, float *in, float *out) |
||||
{ |
||||
int i; |
||||
#ifdef USE_SMALLFT |
||||
int N = ((struct drft_lookup *)table)->n; |
||||
#elif defined(USE_KISS_FFT) |
||||
int N = ((struct kiss_config *)table)->N; |
||||
#else |
||||
#endif |
||||
#ifdef VAR_ARRAYS |
||||
spx_word16_t _in[N]; |
||||
spx_word16_t _out[N]; |
||||
#else |
||||
spx_word16_t _in[MAX_FFT_SIZE]; |
||||
spx_word16_t _out[MAX_FFT_SIZE]; |
||||
#endif |
||||
for (i=0;i<N;i++) |
||||
_in[i] = (int)floor(.5+in[i]); |
||||
spx_fft(table, _in, _out); |
||||
for (i=0;i<N;i++) |
||||
out[i] = _out[i]; |
||||
#if 0 |
||||
if (!fixed_point) |
||||
{ |
||||
float scale; |
||||
struct drft_lookup t; |
||||
spx_drft_init(&t, ((struct kiss_config *)table)->N); |
||||
scale = 1./((struct kiss_config *)table)->N; |
||||
for (i=0;i<((struct kiss_config *)table)->N;i++) |
||||
out[i] = scale*in[i]; |
||||
spx_drft_forward(&t, out); |
||||
spx_drft_clear(&t); |
||||
} |
||||
#endif |
||||
} |
||||
|
||||
void spx_ifft_float(void *table, float *in, float *out) |
||||
{ |
||||
int i; |
||||
#ifdef USE_SMALLFT |
||||
int N = ((struct drft_lookup *)table)->n; |
||||
#elif defined(USE_KISS_FFT) |
||||
int N = ((struct kiss_config *)table)->N; |
||||
#else |
||||
#endif |
||||
#ifdef VAR_ARRAYS |
||||
spx_word16_t _in[N]; |
||||
spx_word16_t _out[N]; |
||||
#else |
||||
spx_word16_t _in[MAX_FFT_SIZE]; |
||||
spx_word16_t _out[MAX_FFT_SIZE]; |
||||
#endif |
||||
for (i=0;i<N;i++) |
||||
_in[i] = (int)floor(.5+in[i]); |
||||
spx_ifft(table, _in, _out); |
||||
for (i=0;i<N;i++) |
||||
out[i] = _out[i]; |
||||
#if 0 |
||||
if (!fixed_point) |
||||
{ |
||||
int i; |
||||
struct drft_lookup t; |
||||
spx_drft_init(&t, ((struct kiss_config *)table)->N); |
||||
for (i=0;i<((struct kiss_config *)table)->N;i++) |
||||
out[i] = in[i]; |
||||
spx_drft_backward(&t, out); |
||||
spx_drft_clear(&t); |
||||
} |
||||
#endif |
||||
} |
||||
|
||||
#else |
||||
|
||||
void spx_fft_float(void *table, float *in, float *out) |
||||
{ |
||||
spx_fft(table, in, out); |
||||
} |
||||
void spx_ifft_float(void *table, float *in, float *out) |
||||
{ |
||||
spx_ifft(table, in, out); |
||||
} |
||||
|
||||
#endif |
||||
@ -0,0 +1,58 @@
|
||||
/* Copyright (C) 2005 Jean-Marc Valin
|
||||
File: fftwrap.h |
||||
|
||||
Wrapper for various FFTs |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions |
||||
are met: |
||||
|
||||
- Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
|
||||
- Redistributions in binary form must reproduce the above copyright |
||||
notice, this list of conditions and the following disclaimer in the |
||||
documentation and/or other materials provided with the distribution. |
||||
|
||||
- Neither the name of the Xiph.org Foundation nor the names of its |
||||
contributors may be used to endorse or promote products derived from |
||||
this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR |
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
|
||||
*/ |
||||
|
||||
#ifndef FFTWRAP_H |
||||
#define FFTWRAP_H |
||||
|
||||
#include "arch.h" |
||||
|
||||
/** Compute tables for an FFT */ |
||||
void *spx_fft_init(int size); |
||||
|
||||
/** Destroy tables for an FFT */ |
||||
void spx_fft_destroy(void *table); |
||||
|
||||
/** Forward (real to half-complex) transform */ |
||||
void spx_fft(void *table, spx_word16_t *in, spx_word16_t *out); |
||||
|
||||
/** Backward (half-complex to real) transform */ |
||||
void spx_ifft(void *table, spx_word16_t *in, spx_word16_t *out); |
||||
|
||||
/** Forward (real to half-complex) transform of float data */ |
||||
void spx_fft_float(void *table, float *in, float *out); |
||||
|
||||
/** Backward (half-complex to real) transform of float data */ |
||||
void spx_ifft_float(void *table, float *in, float *out); |
||||
|
||||
#endif |
||||
@ -0,0 +1,170 @@
|
||||
/* Copyright (C) Jean-Marc Valin */ |
||||
/**
|
||||
@file speex_echo.h |
||||
@brief Echo cancellation |
||||
*/ |
||||
/*
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions are |
||||
met: |
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, |
||||
this list of conditions and the following disclaimer. |
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright |
||||
notice, this list of conditions and the following disclaimer in the |
||||
documentation and/or other materials provided with the distribution. |
||||
|
||||
3. The name of the author may not be used to endorse or promote products |
||||
derived from this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
||||
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, |
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
||||
POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
#ifndef SPEEX_ECHO_H |
||||
#define SPEEX_ECHO_H |
||||
/** @defgroup SpeexEchoState SpeexEchoState: Acoustic echo canceller
|
||||
* This is the acoustic echo canceller module. |
||||
* @{ |
||||
*/ |
||||
#include "speexdsp_types.h" |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
/** Obtain frame size used by the AEC */ |
||||
#define SPEEX_ECHO_GET_FRAME_SIZE 3 |
||||
|
||||
/** Set sampling rate */ |
||||
#define SPEEX_ECHO_SET_SAMPLING_RATE 24 |
||||
/** Get sampling rate */ |
||||
#define SPEEX_ECHO_GET_SAMPLING_RATE 25 |
||||
|
||||
/* Can't set window sizes */ |
||||
/** Get size of impulse response (int32) */ |
||||
#define SPEEX_ECHO_GET_IMPULSE_RESPONSE_SIZE 27 |
||||
|
||||
/* Can't set window content */ |
||||
/** Get impulse response (int32[]) */ |
||||
#define SPEEX_ECHO_GET_IMPULSE_RESPONSE 29 |
||||
|
||||
/** Internal echo canceller state. Should never be accessed directly. */ |
||||
struct SpeexEchoState_; |
||||
|
||||
/** @class SpeexEchoState
|
||||
* This holds the state of the echo canceller. You need one per channel. |
||||
*/ |
||||
|
||||
/** Internal echo canceller state. Should never be accessed directly. */ |
||||
typedef struct SpeexEchoState_ SpeexEchoState; |
||||
|
||||
/** Creates a new echo canceller state
|
||||
* @param frame_size Number of samples to process at one time (should correspond to 10-20 ms) |
||||
* @param filter_length Number of samples of echo to cancel (should generally correspond to 100-500 ms) |
||||
* @return Newly-created echo canceller state |
||||
*/ |
||||
SpeexEchoState *speex_echo_state_init(int frame_size, int filter_length); |
||||
|
||||
/** Creates a new multi-channel echo canceller state
|
||||
* @param frame_size Number of samples to process at one time (should correspond to 10-20 ms) |
||||
* @param filter_length Number of samples of echo to cancel (should generally correspond to 100-500 ms) |
||||
* @param nb_mic Number of microphone channels |
||||
* @param nb_speakers Number of speaker channels |
||||
* @return Newly-created echo canceller state |
||||
*/ |
||||
SpeexEchoState *speex_echo_state_init_mc(int frame_size, int filter_length, int nb_mic, int nb_speakers); |
||||
|
||||
/** Destroys an echo canceller state
|
||||
* @param st Echo canceller state |
||||
*/ |
||||
void speex_echo_state_destroy(SpeexEchoState *st); |
||||
|
||||
/** Performs echo cancellation a frame, based on the audio sent to the speaker (no delay is added
|
||||
* to playback in this form) |
||||
* |
||||
* @param st Echo canceller state |
||||
* @param rec Signal from the microphone (near end + far end echo) |
||||
* @param play Signal played to the speaker (received from far end) |
||||
* @param out Returns near-end signal with echo removed |
||||
*/ |
||||
void speex_echo_cancellation(SpeexEchoState *st, const spx_int16_t *rec, const spx_int16_t *play, spx_int16_t *out); |
||||
|
||||
/** Performs echo cancellation a frame (deprecated) */ |
||||
void speex_echo_cancel(SpeexEchoState *st, const spx_int16_t *rec, const spx_int16_t *play, spx_int16_t *out, spx_int32_t *Yout); |
||||
|
||||
/** Perform echo cancellation using internal playback buffer, which is delayed by two frames
|
||||
* to account for the delay introduced by most soundcards (but it could be off!) |
||||
* @param st Echo canceller state |
||||
* @param rec Signal from the microphone (near end + far end echo) |
||||
* @param out Returns near-end signal with echo removed |
||||
*/ |
||||
void speex_echo_capture(SpeexEchoState *st, const spx_int16_t *rec, spx_int16_t *out); |
||||
|
||||
/** Let the echo canceller know that a frame was just queued to the soundcard
|
||||
* @param st Echo canceller state |
||||
* @param play Signal played to the speaker (received from far end) |
||||
*/ |
||||
void speex_echo_playback(SpeexEchoState *st, const spx_int16_t *play); |
||||
|
||||
/** Reset the echo canceller to its original state
|
||||
* @param st Echo canceller state |
||||
*/ |
||||
void speex_echo_state_reset(SpeexEchoState *st); |
||||
|
||||
/** Used like the ioctl function to control the echo canceller parameters
|
||||
* |
||||
* @param st Echo canceller state |
||||
* @param request ioctl-type request (one of the SPEEX_ECHO_* macros) |
||||
* @param ptr Data exchanged to-from function |
||||
* @return 0 if no error, -1 if request in unknown |
||||
*/ |
||||
int speex_echo_ctl(SpeexEchoState *st, int request, void *ptr); |
||||
|
||||
|
||||
|
||||
struct SpeexDecorrState_; |
||||
|
||||
typedef struct SpeexDecorrState_ SpeexDecorrState; |
||||
|
||||
|
||||
/** Create a state for the channel decorrelation algorithm
|
||||
This is useful for multi-channel echo cancellation only |
||||
* @param rate Sampling rate |
||||
* @param channels Number of channels (it's a bit pointless if you don't have at least 2) |
||||
* @param frame_size Size of the frame to process at ones (counting samples *per* channel) |
||||
*/ |
||||
SpeexDecorrState *speex_decorrelate_new(int rate, int channels, int frame_size); |
||||
|
||||
/** Remove correlation between the channels by modifying the phase and possibly
|
||||
adding noise in a way that is not (or little) perceptible. |
||||
* @param st Decorrelator state |
||||
* @param in Input audio in interleaved format |
||||
* @param out Result of the decorrelation (out *may* alias in) |
||||
* @param strength How much alteration of the audio to apply from 0 to 100. |
||||
*/ |
||||
void speex_decorrelate(SpeexDecorrState *st, const spx_int16_t *in, spx_int16_t *out, int strength); |
||||
|
||||
/** Destroy a Decorrelation state
|
||||
* @param st State to destroy |
||||
*/ |
||||
void speex_decorrelate_destroy(SpeexDecorrState *st); |
||||
|
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
|
||||
|
||||
/** @}*/ |
||||
#endif |
||||
@ -0,0 +1,14 @@
|
||||
/* speexdsp_config_types.h — статически сгенерировано для POSIX (Linux/FreeBSD/Android).
|
||||
* В оригинале генерируется configure из speexdsp_config_types.h.in. На Windows ветка |
||||
* _WIN32 в speexdsp_types.h использует нативные типы и этот файл не инклудится. */ |
||||
#ifndef __SPEEX_TYPES_H__ |
||||
#define __SPEEX_TYPES_H__ |
||||
|
||||
#include <stdint.h> |
||||
|
||||
typedef int16_t spx_int16_t; |
||||
typedef uint16_t spx_uint16_t; |
||||
typedef int32_t spx_int32_t; |
||||
typedef uint32_t spx_uint32_t; |
||||
|
||||
#endif |
||||
@ -0,0 +1,126 @@
|
||||
/* speexdsp_types.h taken from libogg */ |
||||
/********************************************************************
|
||||
* * |
||||
* THIS FILE IS PART OF THE OggVorbis SOFTWARE CODEC SOURCE CODE. * |
||||
* USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS * |
||||
* GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE * |
||||
* IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. * |
||||
* * |
||||
* THE OggVorbis SOURCE CODE IS (C) COPYRIGHT 1994-2002 * |
||||
* by the Xiph.Org Foundation http://www.xiph.org/ *
|
||||
* * |
||||
******************************************************************** |
||||
|
||||
function: #ifdef jail to whip a few platforms into the UNIX ideal. |
||||
last mod: $Id: os_types.h 7524 2004-08-11 04:20:36Z conrad $ |
||||
|
||||
********************************************************************/ |
||||
/**
|
||||
@file speexdsp_types.h |
||||
@brief Speex types |
||||
*/ |
||||
#ifndef _SPEEX_TYPES_H |
||||
#define _SPEEX_TYPES_H |
||||
|
||||
#if defined(_WIN32) |
||||
|
||||
# if defined(__CYGWIN__) |
||||
# include <_G_config.h> |
||||
typedef _G_int32_t spx_int32_t; |
||||
typedef _G_uint32_t spx_uint32_t; |
||||
typedef _G_int16_t spx_int16_t; |
||||
typedef _G_uint16_t spx_uint16_t; |
||||
# elif defined(__MINGW32__) |
||||
typedef short spx_int16_t; |
||||
typedef unsigned short spx_uint16_t; |
||||
typedef int spx_int32_t; |
||||
typedef unsigned int spx_uint32_t; |
||||
# elif defined(__MWERKS__) |
||||
typedef int spx_int32_t; |
||||
typedef unsigned int spx_uint32_t; |
||||
typedef short spx_int16_t; |
||||
typedef unsigned short spx_uint16_t; |
||||
# else |
||||
/* MSVC/Borland */ |
||||
typedef __int32 spx_int32_t; |
||||
typedef unsigned __int32 spx_uint32_t; |
||||
typedef __int16 spx_int16_t; |
||||
typedef unsigned __int16 spx_uint16_t; |
||||
# endif |
||||
|
||||
#elif defined(__MACOS__) |
||||
|
||||
# include <sys/types.h> |
||||
typedef SInt16 spx_int16_t; |
||||
typedef UInt16 spx_uint16_t; |
||||
typedef SInt32 spx_int32_t; |
||||
typedef UInt32 spx_uint32_t; |
||||
|
||||
#elif (defined(__APPLE__) && defined(__MACH__)) /* MacOS X Framework build */ |
||||
|
||||
# include <sys/types.h> |
||||
typedef int16_t spx_int16_t; |
||||
typedef u_int16_t spx_uint16_t; |
||||
typedef int32_t spx_int32_t; |
||||
typedef u_int32_t spx_uint32_t; |
||||
|
||||
#elif defined(__BEOS__) |
||||
|
||||
/* Be */ |
||||
# include <inttypes.h> |
||||
typedef int16_t spx_int16_t; |
||||
typedef u_int16_t spx_uint16_t; |
||||
typedef int32_t spx_int32_t; |
||||
typedef u_int32_t spx_uint32_t; |
||||
|
||||
#elif defined (__EMX__) |
||||
|
||||
/* OS/2 GCC */ |
||||
typedef short spx_int16_t; |
||||
typedef unsigned short spx_uint16_t; |
||||
typedef int spx_int32_t; |
||||
typedef unsigned int spx_uint32_t; |
||||
|
||||
#elif defined (DJGPP) |
||||
|
||||
/* DJGPP */ |
||||
typedef short spx_int16_t; |
||||
typedef int spx_int32_t; |
||||
typedef unsigned int spx_uint32_t; |
||||
|
||||
#elif defined(R5900) |
||||
|
||||
/* PS2 EE */ |
||||
typedef int spx_int32_t; |
||||
typedef unsigned spx_uint32_t; |
||||
typedef short spx_int16_t; |
||||
|
||||
#elif defined(__SYMBIAN32__) |
||||
|
||||
/* Symbian GCC */ |
||||
typedef signed short spx_int16_t; |
||||
typedef unsigned short spx_uint16_t; |
||||
typedef signed int spx_int32_t; |
||||
typedef unsigned int spx_uint32_t; |
||||
|
||||
#elif defined(CONFIG_TI_C54X) || defined (CONFIG_TI_C55X) |
||||
|
||||
typedef short spx_int16_t; |
||||
typedef unsigned short spx_uint16_t; |
||||
typedef long spx_int32_t; |
||||
typedef unsigned long spx_uint32_t; |
||||
|
||||
#elif defined(CONFIG_TI_C6X) |
||||
|
||||
typedef short spx_int16_t; |
||||
typedef unsigned short spx_uint16_t; |
||||
typedef int spx_int32_t; |
||||
typedef unsigned int spx_uint32_t; |
||||
|
||||
#else |
||||
|
||||
#include "speexdsp_config_types.h" |
||||
|
||||
#endif |
||||
|
||||
#endif /* _SPEEX_TYPES_H */ |
||||
@ -0,0 +1,523 @@
|
||||
/*
|
||||
Copyright (c) 2003-2004, Mark Borgerding |
||||
Copyright (c) 2005-2007, Jean-Marc Valin |
||||
|
||||
All rights reserved. |
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: |
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. |
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. |
||||
* Neither the author nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
|
||||
#ifdef HAVE_CONFIG_H |
||||
#include "config.h" |
||||
#endif |
||||
|
||||
#include "_kiss_fft_guts.h" |
||||
#include "arch.h" |
||||
#include "os_support.h" |
||||
|
||||
/* The guts header contains all the multiplication and addition macros that are defined for
|
||||
fixed or floating point complex numbers. It also declares the kf_ internal functions. |
||||
*/ |
||||
|
||||
static void kf_bfly2( |
||||
kiss_fft_cpx * Fout, |
||||
const size_t fstride, |
||||
const kiss_fft_cfg st, |
||||
int m, |
||||
int N, |
||||
int mm |
||||
) |
||||
{ |
||||
kiss_fft_cpx * Fout2; |
||||
kiss_fft_cpx * tw1; |
||||
kiss_fft_cpx t; |
||||
if (!st->inverse) { |
||||
int i,j; |
||||
kiss_fft_cpx * Fout_beg = Fout; |
||||
for (i=0;i<N;i++) |
||||
{ |
||||
Fout = Fout_beg + i*mm; |
||||
Fout2 = Fout + m; |
||||
tw1 = st->twiddles; |
||||
for(j=0;j<m;j++) |
||||
{ |
||||
/* Almost the same as the code path below, except that we divide the input by two
|
||||
(while keeping the best accuracy possible) */ |
||||
spx_word32_t tr, ti; |
||||
tr = SHR32(SUB32(MULT16_16(Fout2->r , tw1->r),MULT16_16(Fout2->i , tw1->i)), 1); |
||||
ti = SHR32(ADD32(MULT16_16(Fout2->i , tw1->r),MULT16_16(Fout2->r , tw1->i)), 1); |
||||
tw1 += fstride; |
||||
Fout2->r = PSHR32(SUB32(SHL32(EXTEND32(Fout->r), 14), tr), 15); |
||||
Fout2->i = PSHR32(SUB32(SHL32(EXTEND32(Fout->i), 14), ti), 15); |
||||
Fout->r = PSHR32(ADD32(SHL32(EXTEND32(Fout->r), 14), tr), 15); |
||||
Fout->i = PSHR32(ADD32(SHL32(EXTEND32(Fout->i), 14), ti), 15); |
||||
++Fout2; |
||||
++Fout; |
||||
} |
||||
} |
||||
} else { |
||||
int i,j; |
||||
kiss_fft_cpx * Fout_beg = Fout; |
||||
for (i=0;i<N;i++) |
||||
{ |
||||
Fout = Fout_beg + i*mm; |
||||
Fout2 = Fout + m; |
||||
tw1 = st->twiddles; |
||||
for(j=0;j<m;j++) |
||||
{ |
||||
C_MUL (t, *Fout2 , *tw1); |
||||
tw1 += fstride; |
||||
C_SUB( *Fout2 , *Fout , t ); |
||||
C_ADDTO( *Fout , t ); |
||||
++Fout2; |
||||
++Fout; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
static void kf_bfly4( |
||||
kiss_fft_cpx * Fout, |
||||
const size_t fstride, |
||||
const kiss_fft_cfg st, |
||||
int m, |
||||
int N, |
||||
int mm |
||||
) |
||||
{ |
||||
kiss_fft_cpx *tw1,*tw2,*tw3; |
||||
kiss_fft_cpx scratch[6]; |
||||
const size_t m2=2*m; |
||||
const size_t m3=3*m; |
||||
int i, j; |
||||
|
||||
if (st->inverse) |
||||
{ |
||||
kiss_fft_cpx * Fout_beg = Fout; |
||||
for (i=0;i<N;i++) |
||||
{ |
||||
Fout = Fout_beg + i*mm; |
||||
tw3 = tw2 = tw1 = st->twiddles; |
||||
for (j=0;j<m;j++) |
||||
{ |
||||
C_MUL(scratch[0],Fout[m] , *tw1 ); |
||||
C_MUL(scratch[1],Fout[m2] , *tw2 ); |
||||
C_MUL(scratch[2],Fout[m3] , *tw3 ); |
||||
|
||||
C_SUB( scratch[5] , *Fout, scratch[1] ); |
||||
C_ADDTO(*Fout, scratch[1]); |
||||
C_ADD( scratch[3] , scratch[0] , scratch[2] ); |
||||
C_SUB( scratch[4] , scratch[0] , scratch[2] ); |
||||
C_SUB( Fout[m2], *Fout, scratch[3] ); |
||||
tw1 += fstride; |
||||
tw2 += fstride*2; |
||||
tw3 += fstride*3; |
||||
C_ADDTO( *Fout , scratch[3] ); |
||||
|
||||
Fout[m].r = scratch[5].r - scratch[4].i; |
||||
Fout[m].i = scratch[5].i + scratch[4].r; |
||||
Fout[m3].r = scratch[5].r + scratch[4].i; |
||||
Fout[m3].i = scratch[5].i - scratch[4].r; |
||||
++Fout; |
||||
} |
||||
} |
||||
} else |
||||
{ |
||||
kiss_fft_cpx * Fout_beg = Fout; |
||||
for (i=0;i<N;i++) |
||||
{ |
||||
Fout = Fout_beg + i*mm; |
||||
tw3 = tw2 = tw1 = st->twiddles; |
||||
for (j=0;j<m;j++) |
||||
{ |
||||
C_MUL4(scratch[0],Fout[m] , *tw1 ); |
||||
C_MUL4(scratch[1],Fout[m2] , *tw2 ); |
||||
C_MUL4(scratch[2],Fout[m3] , *tw3 ); |
||||
|
||||
Fout->r = PSHR16(Fout->r, 2); |
||||
Fout->i = PSHR16(Fout->i, 2); |
||||
C_SUB( scratch[5] , *Fout, scratch[1] ); |
||||
C_ADDTO(*Fout, scratch[1]); |
||||
C_ADD( scratch[3] , scratch[0] , scratch[2] ); |
||||
C_SUB( scratch[4] , scratch[0] , scratch[2] ); |
||||
Fout[m2].r = PSHR16(Fout[m2].r, 2); |
||||
Fout[m2].i = PSHR16(Fout[m2].i, 2); |
||||
C_SUB( Fout[m2], *Fout, scratch[3] ); |
||||
tw1 += fstride; |
||||
tw2 += fstride*2; |
||||
tw3 += fstride*3; |
||||
C_ADDTO( *Fout , scratch[3] ); |
||||
|
||||
Fout[m].r = scratch[5].r + scratch[4].i; |
||||
Fout[m].i = scratch[5].i - scratch[4].r; |
||||
Fout[m3].r = scratch[5].r - scratch[4].i; |
||||
Fout[m3].i = scratch[5].i + scratch[4].r; |
||||
++Fout; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
static void kf_bfly3( |
||||
kiss_fft_cpx * Fout, |
||||
const size_t fstride, |
||||
const kiss_fft_cfg st, |
||||
size_t m |
||||
) |
||||
{ |
||||
size_t k=m; |
||||
const size_t m2 = 2*m; |
||||
kiss_fft_cpx *tw1,*tw2; |
||||
kiss_fft_cpx scratch[5]; |
||||
kiss_fft_cpx epi3; |
||||
epi3 = st->twiddles[fstride*m]; |
||||
|
||||
tw1=tw2=st->twiddles; |
||||
|
||||
do{ |
||||
if (!st->inverse) { |
||||
C_FIXDIV(*Fout,3); C_FIXDIV(Fout[m],3); C_FIXDIV(Fout[m2],3); |
||||
} |
||||
|
||||
C_MUL(scratch[1],Fout[m] , *tw1); |
||||
C_MUL(scratch[2],Fout[m2] , *tw2); |
||||
|
||||
C_ADD(scratch[3],scratch[1],scratch[2]); |
||||
C_SUB(scratch[0],scratch[1],scratch[2]); |
||||
tw1 += fstride; |
||||
tw2 += fstride*2; |
||||
|
||||
Fout[m].r = Fout->r - HALF_OF(scratch[3].r); |
||||
Fout[m].i = Fout->i - HALF_OF(scratch[3].i); |
||||
|
||||
C_MULBYSCALAR( scratch[0] , epi3.i ); |
||||
|
||||
C_ADDTO(*Fout,scratch[3]); |
||||
|
||||
Fout[m2].r = Fout[m].r + scratch[0].i; |
||||
Fout[m2].i = Fout[m].i - scratch[0].r; |
||||
|
||||
Fout[m].r -= scratch[0].i; |
||||
Fout[m].i += scratch[0].r; |
||||
|
||||
++Fout; |
||||
}while(--k); |
||||
} |
||||
|
||||
static void kf_bfly5( |
||||
kiss_fft_cpx * Fout, |
||||
const size_t fstride, |
||||
const kiss_fft_cfg st, |
||||
int m |
||||
) |
||||
{ |
||||
kiss_fft_cpx *Fout0,*Fout1,*Fout2,*Fout3,*Fout4; |
||||
int u; |
||||
kiss_fft_cpx scratch[13]; |
||||
kiss_fft_cpx * twiddles = st->twiddles; |
||||
kiss_fft_cpx *tw; |
||||
kiss_fft_cpx ya,yb; |
||||
ya = twiddles[fstride*m]; |
||||
yb = twiddles[fstride*2*m]; |
||||
|
||||
Fout0=Fout; |
||||
Fout1=Fout0+m; |
||||
Fout2=Fout0+2*m; |
||||
Fout3=Fout0+3*m; |
||||
Fout4=Fout0+4*m; |
||||
|
||||
tw=st->twiddles; |
||||
for ( u=0; u<m; ++u ) { |
||||
if (!st->inverse) { |
||||
C_FIXDIV( *Fout0,5); C_FIXDIV( *Fout1,5); C_FIXDIV( *Fout2,5); C_FIXDIV( *Fout3,5); C_FIXDIV( *Fout4,5); |
||||
} |
||||
scratch[0] = *Fout0; |
||||
|
||||
C_MUL(scratch[1] ,*Fout1, tw[u*fstride]); |
||||
C_MUL(scratch[2] ,*Fout2, tw[2*u*fstride]); |
||||
C_MUL(scratch[3] ,*Fout3, tw[3*u*fstride]); |
||||
C_MUL(scratch[4] ,*Fout4, tw[4*u*fstride]); |
||||
|
||||
C_ADD( scratch[7],scratch[1],scratch[4]); |
||||
C_SUB( scratch[10],scratch[1],scratch[4]); |
||||
C_ADD( scratch[8],scratch[2],scratch[3]); |
||||
C_SUB( scratch[9],scratch[2],scratch[3]); |
||||
|
||||
Fout0->r += scratch[7].r + scratch[8].r; |
||||
Fout0->i += scratch[7].i + scratch[8].i; |
||||
|
||||
scratch[5].r = scratch[0].r + S_MUL(scratch[7].r,ya.r) + S_MUL(scratch[8].r,yb.r); |
||||
scratch[5].i = scratch[0].i + S_MUL(scratch[7].i,ya.r) + S_MUL(scratch[8].i,yb.r); |
||||
|
||||
scratch[6].r = S_MUL(scratch[10].i,ya.i) + S_MUL(scratch[9].i,yb.i); |
||||
scratch[6].i = -S_MUL(scratch[10].r,ya.i) - S_MUL(scratch[9].r,yb.i); |
||||
|
||||
C_SUB(*Fout1,scratch[5],scratch[6]); |
||||
C_ADD(*Fout4,scratch[5],scratch[6]); |
||||
|
||||
scratch[11].r = scratch[0].r + S_MUL(scratch[7].r,yb.r) + S_MUL(scratch[8].r,ya.r); |
||||
scratch[11].i = scratch[0].i + S_MUL(scratch[7].i,yb.r) + S_MUL(scratch[8].i,ya.r); |
||||
scratch[12].r = - S_MUL(scratch[10].i,yb.i) + S_MUL(scratch[9].i,ya.i); |
||||
scratch[12].i = S_MUL(scratch[10].r,yb.i) - S_MUL(scratch[9].r,ya.i); |
||||
|
||||
C_ADD(*Fout2,scratch[11],scratch[12]); |
||||
C_SUB(*Fout3,scratch[11],scratch[12]); |
||||
|
||||
++Fout0;++Fout1;++Fout2;++Fout3;++Fout4; |
||||
} |
||||
} |
||||
|
||||
/* perform the butterfly for one stage of a mixed radix FFT */ |
||||
static void kf_bfly_generic( |
||||
kiss_fft_cpx * Fout, |
||||
const size_t fstride, |
||||
const kiss_fft_cfg st, |
||||
int m, |
||||
int p |
||||
) |
||||
{ |
||||
int u,k,q1,q; |
||||
kiss_fft_cpx * twiddles = st->twiddles; |
||||
kiss_fft_cpx t; |
||||
kiss_fft_cpx scratchbuf[17]; |
||||
int Norig = st->nfft; |
||||
|
||||
/*CHECKBUF(scratchbuf,nscratchbuf,p);*/ |
||||
if (p>17) |
||||
speex_fatal("KissFFT: max radix supported is 17"); |
||||
|
||||
for ( u=0; u<m; ++u ) { |
||||
k=u; |
||||
for ( q1=0 ; q1<p ; ++q1 ) { |
||||
scratchbuf[q1] = Fout[ k ]; |
||||
if (!st->inverse) { |
||||
C_FIXDIV(scratchbuf[q1],p); |
||||
} |
||||
k += m; |
||||
} |
||||
|
||||
k=u; |
||||
for ( q1=0 ; q1<p ; ++q1 ) { |
||||
int twidx=0; |
||||
Fout[ k ] = scratchbuf[0]; |
||||
for (q=1;q<p;++q ) { |
||||
twidx += fstride * k; |
||||
if (twidx>=Norig) twidx-=Norig; |
||||
C_MUL(t,scratchbuf[q] , twiddles[twidx] ); |
||||
C_ADDTO( Fout[ k ] ,t); |
||||
} |
||||
k += m; |
||||
} |
||||
} |
||||
} |
||||
|
||||
static |
||||
void kf_shuffle( |
||||
kiss_fft_cpx * Fout, |
||||
const kiss_fft_cpx * f, |
||||
const size_t fstride, |
||||
int in_stride, |
||||
int * factors, |
||||
const kiss_fft_cfg st |
||||
) |
||||
{ |
||||
const int p=*factors++; /* the radix */ |
||||
const int m=*factors++; /* stage's fft length/p */ |
||||
|
||||
/*printf ("fft %d %d %d %d %d %d\n", p*m, m, p, s2, fstride*in_stride, N);*/ |
||||
if (m==1) |
||||
{ |
||||
int j; |
||||
for (j=0;j<p;j++) |
||||
{ |
||||
Fout[j] = *f; |
||||
f += fstride*in_stride; |
||||
} |
||||
} else { |
||||
int j; |
||||
for (j=0;j<p;j++) |
||||
{ |
||||
kf_shuffle( Fout , f, fstride*p, in_stride, factors,st); |
||||
f += fstride*in_stride; |
||||
Fout += m; |
||||
} |
||||
} |
||||
} |
||||
|
||||
static |
||||
void kf_work( |
||||
kiss_fft_cpx * Fout, |
||||
const kiss_fft_cpx * f, |
||||
const size_t fstride, |
||||
int in_stride, |
||||
int * factors, |
||||
const kiss_fft_cfg st, |
||||
int N, |
||||
int s2, |
||||
int m2 |
||||
) |
||||
{ |
||||
int i; |
||||
kiss_fft_cpx * Fout_beg=Fout; |
||||
const int p=*factors++; /* the radix */ |
||||
const int m=*factors++; /* stage's fft length/p */ |
||||
#if 0 |
||||
/*printf ("fft %d %d %d %d %d %d\n", p*m, m, p, s2, fstride*in_stride, N);*/ |
||||
if (m==1) |
||||
{ |
||||
/* int j;
|
||||
for (j=0;j<p;j++) |
||||
{ |
||||
Fout[j] = *f; |
||||
f += fstride*in_stride; |
||||
}*/ |
||||
} else { |
||||
int j; |
||||
for (j=0;j<p;j++) |
||||
{ |
||||
kf_work( Fout , f, fstride*p, in_stride, factors,st, N*p, fstride*in_stride, m); |
||||
f += fstride*in_stride; |
||||
Fout += m; |
||||
} |
||||
} |
||||
|
||||
Fout=Fout_beg; |
||||
|
||||
switch (p) { |
||||
case 2: kf_bfly2(Fout,fstride,st,m); break; |
||||
case 3: kf_bfly3(Fout,fstride,st,m); break; |
||||
case 4: kf_bfly4(Fout,fstride,st,m); break; |
||||
case 5: kf_bfly5(Fout,fstride,st,m); break; |
||||
default: kf_bfly_generic(Fout,fstride,st,m,p); break; |
||||
} |
||||
#else |
||||
/*printf ("fft %d %d %d %d %d %d %d\n", p*m, m, p, s2, fstride*in_stride, N, m2);*/ |
||||
if (m==1) |
||||
{ |
||||
/*for (i=0;i<N;i++)
|
||||
{ |
||||
int j; |
||||
Fout = Fout_beg+i*m2; |
||||
const kiss_fft_cpx * f2 = f+i*s2; |
||||
for (j=0;j<p;j++) |
||||
{ |
||||
*Fout++ = *f2; |
||||
f2 += fstride*in_stride; |
||||
} |
||||
}*/ |
||||
}else{ |
||||
kf_work( Fout , f, fstride*p, in_stride, factors,st, N*p, fstride*in_stride, m); |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
switch (p) { |
||||
case 2: kf_bfly2(Fout,fstride,st,m, N, m2); break; |
||||
case 3: for (i=0;i<N;i++){Fout=Fout_beg+i*m2; kf_bfly3(Fout,fstride,st,m);} break; |
||||
case 4: kf_bfly4(Fout,fstride,st,m, N, m2); break; |
||||
case 5: for (i=0;i<N;i++){Fout=Fout_beg+i*m2; kf_bfly5(Fout,fstride,st,m);} break; |
||||
default: for (i=0;i<N;i++){Fout=Fout_beg+i*m2; kf_bfly_generic(Fout,fstride,st,m,p);} break; |
||||
} |
||||
#endif |
||||
} |
||||
|
||||
/* facbuf is populated by p1,m1,p2,m2, ...
|
||||
where |
||||
p[i] * m[i] = m[i-1] |
||||
m0 = n */ |
||||
static |
||||
void kf_factor(int n,int * facbuf) |
||||
{ |
||||
int p=4; |
||||
|
||||
/*factor out powers of 4, powers of 2, then any remaining primes */ |
||||
do { |
||||
while (n % p) { |
||||
switch (p) { |
||||
case 4: p = 2; break; |
||||
case 2: p = 3; break; |
||||
default: p += 2; break; |
||||
} |
||||
if (p>32000 || (spx_int32_t)p*(spx_int32_t)p > n) |
||||
p = n; /* no more factors, skip to end */ |
||||
} |
||||
n /= p; |
||||
*facbuf++ = p; |
||||
*facbuf++ = n; |
||||
} while (n > 1); |
||||
} |
||||
/*
|
||||
* |
||||
* User-callable function to allocate all necessary storage space for the fft. |
||||
* |
||||
* The return value is a contiguous block of memory, allocated with malloc. As such, |
||||
* It can be freed with free(), rather than a kiss_fft-specific function. |
||||
* */ |
||||
kiss_fft_cfg kiss_fft_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem ) |
||||
{ |
||||
kiss_fft_cfg st=NULL; |
||||
size_t memneeded = sizeof(struct kiss_fft_state) |
||||
+ sizeof(kiss_fft_cpx)*(nfft-1); /* twiddle factors*/ |
||||
|
||||
if ( lenmem==NULL ) { |
||||
st = ( kiss_fft_cfg)KISS_FFT_MALLOC( memneeded ); |
||||
}else{ |
||||
if (mem != NULL && *lenmem >= memneeded) |
||||
st = (kiss_fft_cfg)mem; |
||||
*lenmem = memneeded; |
||||
} |
||||
if (st) { |
||||
int i; |
||||
st->nfft=nfft; |
||||
st->inverse = inverse_fft; |
||||
#ifdef FIXED_POINT |
||||
for (i=0;i<nfft;++i) { |
||||
spx_word32_t phase = i; |
||||
if (!st->inverse) |
||||
phase = -phase; |
||||
kf_cexp2(st->twiddles+i, DIV32(SHL32(phase,17),nfft)); |
||||
} |
||||
#else |
||||
for (i=0;i<nfft;++i) { |
||||
const double pi=3.14159265358979323846264338327; |
||||
double phase = ( -2*pi /nfft ) * i; |
||||
if (st->inverse) |
||||
phase *= -1; |
||||
kf_cexp(st->twiddles+i, phase ); |
||||
} |
||||
#endif |
||||
kf_factor(nfft,st->factors); |
||||
} |
||||
return st; |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
void kiss_fft_stride(kiss_fft_cfg st,const kiss_fft_cpx *fin,kiss_fft_cpx *fout,int in_stride) |
||||
{ |
||||
if (fin == fout) |
||||
{ |
||||
speex_fatal("In-place FFT not supported"); |
||||
/*CHECKBUF(tmpbuf,ntmpbuf,st->nfft);
|
||||
kf_work(tmpbuf,fin,1,in_stride, st->factors,st); |
||||
SPEEX_MOVE(fout,tmpbuf,st->nfft);*/ |
||||
} else { |
||||
kf_shuffle( fout, fin, 1,in_stride, st->factors,st); |
||||
kf_work( fout, fin, 1,in_stride, st->factors,st, 1, in_stride, 1); |
||||
} |
||||
} |
||||
|
||||
void kiss_fft(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout) |
||||
{ |
||||
kiss_fft_stride(cfg,fin,fout,1); |
||||
} |
||||
|
||||
@ -0,0 +1,108 @@
|
||||
#ifndef KISS_FFT_H |
||||
#define KISS_FFT_H |
||||
|
||||
#include <stdlib.h> |
||||
#include <math.h> |
||||
#include "arch.h" |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
/*
|
||||
ATTENTION! |
||||
If you would like a : |
||||
-- a utility that will handle the caching of fft objects |
||||
-- real-only (no imaginary time component ) FFT |
||||
-- a multi-dimensional FFT |
||||
-- a command-line utility to perform ffts |
||||
-- a command-line utility to perform fast-convolution filtering |
||||
|
||||
Then see kfc.h kiss_fftr.h kiss_fftnd.h fftutil.c kiss_fastfir.c |
||||
in the tools/ directory. |
||||
*/ |
||||
|
||||
#ifdef USE_SIMD |
||||
# include <xmmintrin.h> |
||||
# define kiss_fft_scalar __m128 |
||||
#define KISS_FFT_MALLOC(nbytes) memalign(16,nbytes) |
||||
#else |
||||
#define KISS_FFT_MALLOC speex_alloc |
||||
#endif |
||||
|
||||
|
||||
#ifdef FIXED_POINT |
||||
#include "arch.h" |
||||
# define kiss_fft_scalar spx_int16_t |
||||
#else |
||||
# ifndef kiss_fft_scalar |
||||
/* default is float */ |
||||
# define kiss_fft_scalar float |
||||
# endif |
||||
#endif |
||||
|
||||
typedef struct { |
||||
kiss_fft_scalar r; |
||||
kiss_fft_scalar i; |
||||
}kiss_fft_cpx; |
||||
|
||||
typedef struct kiss_fft_state* kiss_fft_cfg; |
||||
|
||||
/*
|
||||
* kiss_fft_alloc |
||||
* |
||||
* Initialize a FFT (or IFFT) algorithm's cfg/state buffer. |
||||
* |
||||
* typical usage: kiss_fft_cfg mycfg=kiss_fft_alloc(1024,0,NULL,NULL); |
||||
* |
||||
* The return value from fft_alloc is a cfg buffer used internally |
||||
* by the fft routine or NULL. |
||||
* |
||||
* If lenmem is NULL, then kiss_fft_alloc will allocate a cfg buffer using malloc. |
||||
* The returned value should be free()d when done to avoid memory leaks. |
||||
* |
||||
* The state can be placed in a user supplied buffer 'mem': |
||||
* If lenmem is not NULL and mem is not NULL and *lenmem is large enough, |
||||
* then the function places the cfg in mem and the size used in *lenmem |
||||
* and returns mem. |
||||
* |
||||
* If lenmem is not NULL and ( mem is NULL or *lenmem is not large enough), |
||||
* then the function returns NULL and places the minimum cfg |
||||
* buffer size in *lenmem. |
||||
* */ |
||||
|
||||
kiss_fft_cfg kiss_fft_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem); |
||||
|
||||
/*
|
||||
* kiss_fft(cfg,in_out_buf) |
||||
* |
||||
* Perform an FFT on a complex input buffer. |
||||
* for a forward FFT, |
||||
* fin should be f[0] , f[1] , ... ,f[nfft-1] |
||||
* fout will be F[0] , F[1] , ... ,F[nfft-1] |
||||
* Note that each element is complex and can be accessed like |
||||
f[k].r and f[k].i |
||||
* */ |
||||
void kiss_fft(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout); |
||||
|
||||
/*
|
||||
A more generic version of the above function. It reads its input from every Nth sample. |
||||
* */ |
||||
void kiss_fft_stride(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout,int fin_stride); |
||||
|
||||
/* If kiss_fft_alloc allocated a buffer, it is one contiguous
|
||||
buffer and can be simply free()d when no longer needed*/ |
||||
#define kiss_fft_free speex_free |
||||
|
||||
/*
|
||||
Cleans up some memory that gets managed internally. Not necessary to call, but it might clean up |
||||
your compiler output to call this before you exit. |
||||
*/ |
||||
void kiss_fft_cleanup(void); |
||||
|
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
|
||||
#endif |
||||
@ -0,0 +1,297 @@
|
||||
/*
|
||||
Copyright (c) 2003-2004, Mark Borgerding |
||||
|
||||
All rights reserved. |
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: |
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. |
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. |
||||
* Neither the author nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
#ifdef HAVE_CONFIG_H |
||||
#include "config.h" |
||||
#endif |
||||
|
||||
#include "os_support.h" |
||||
#include "kiss_fftr.h" |
||||
#include "_kiss_fft_guts.h" |
||||
|
||||
struct kiss_fftr_state{ |
||||
kiss_fft_cfg substate; |
||||
kiss_fft_cpx * tmpbuf; |
||||
kiss_fft_cpx * super_twiddles; |
||||
#ifdef USE_SIMD |
||||
long pad; |
||||
#endif |
||||
}; |
||||
|
||||
kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem) |
||||
{ |
||||
int i; |
||||
kiss_fftr_cfg st = NULL; |
||||
size_t subsize, memneeded; |
||||
|
||||
if (nfft & 1) { |
||||
speex_warning("Real FFT optimization must be even.\n"); |
||||
return NULL; |
||||
} |
||||
nfft >>= 1; |
||||
|
||||
kiss_fft_alloc (nfft, inverse_fft, NULL, &subsize); |
||||
memneeded = sizeof(struct kiss_fftr_state) + subsize + sizeof(kiss_fft_cpx) * ( nfft * 2); |
||||
|
||||
if (lenmem == NULL) { |
||||
st = (kiss_fftr_cfg) KISS_FFT_MALLOC (memneeded); |
||||
} else { |
||||
if (*lenmem >= memneeded) |
||||
st = (kiss_fftr_cfg) mem; |
||||
*lenmem = memneeded; |
||||
} |
||||
if (!st) |
||||
return NULL; |
||||
|
||||
st->substate = (kiss_fft_cfg) (st + 1); /*just beyond kiss_fftr_state struct */ |
||||
st->tmpbuf = (kiss_fft_cpx *) (((char *) st->substate) + subsize); |
||||
st->super_twiddles = st->tmpbuf + nfft; |
||||
kiss_fft_alloc(nfft, inverse_fft, st->substate, &subsize); |
||||
|
||||
#ifdef FIXED_POINT |
||||
for (i=0;i<nfft;++i) { |
||||
spx_word32_t phase = i+(nfft>>1); |
||||
if (!inverse_fft) |
||||
phase = -phase; |
||||
kf_cexp2(st->super_twiddles+i, DIV32(SHL32(phase,16),nfft)); |
||||
} |
||||
#else |
||||
for (i=0;i<nfft;++i) { |
||||
const double pi=3.14159265358979323846264338327; |
||||
double phase = pi*(((double)i) /nfft + .5); |
||||
if (!inverse_fft) |
||||
phase = -phase; |
||||
kf_cexp(st->super_twiddles+i, phase ); |
||||
} |
||||
#endif |
||||
return st; |
||||
} |
||||
|
||||
void kiss_fftr(kiss_fftr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata) |
||||
{ |
||||
/* input buffer timedata is stored row-wise */ |
||||
int k,ncfft; |
||||
kiss_fft_cpx fpnk,fpk,f1k,f2k,tw,tdc; |
||||
|
||||
if ( st->substate->inverse) { |
||||
speex_fatal("kiss fft usage error: improper alloc\n"); |
||||
} |
||||
|
||||
ncfft = st->substate->nfft; |
||||
|
||||
/*perform the parallel fft of two real signals packed in real,imag*/ |
||||
kiss_fft( st->substate , (const kiss_fft_cpx*)timedata, st->tmpbuf ); |
||||
/* The real part of the DC element of the frequency spectrum in st->tmpbuf
|
||||
* contains the sum of the even-numbered elements of the input time sequence |
||||
* The imag part is the sum of the odd-numbered elements |
||||
* |
||||
* The sum of tdc.r and tdc.i is the sum of the input time sequence. |
||||
* yielding DC of input time sequence |
||||
* The difference of tdc.r - tdc.i is the sum of the input (dot product) [1,-1,1,-1... |
||||
* yielding Nyquist bin of input time sequence |
||||
*/ |
||||
|
||||
tdc.r = st->tmpbuf[0].r; |
||||
tdc.i = st->tmpbuf[0].i; |
||||
C_FIXDIV(tdc,2); |
||||
CHECK_OVERFLOW_OP(tdc.r ,+, tdc.i); |
||||
CHECK_OVERFLOW_OP(tdc.r ,-, tdc.i); |
||||
freqdata[0].r = tdc.r + tdc.i; |
||||
freqdata[ncfft].r = tdc.r - tdc.i; |
||||
#ifdef USE_SIMD |
||||
freqdata[ncfft].i = freqdata[0].i = _mm_set1_ps(0); |
||||
#else |
||||
freqdata[ncfft].i = freqdata[0].i = 0; |
||||
#endif |
||||
|
||||
for ( k=1;k <= ncfft/2 ; ++k ) { |
||||
fpk = st->tmpbuf[k]; |
||||
fpnk.r = st->tmpbuf[ncfft-k].r; |
||||
fpnk.i = - st->tmpbuf[ncfft-k].i; |
||||
C_FIXDIV(fpk,2); |
||||
C_FIXDIV(fpnk,2); |
||||
|
||||
C_ADD( f1k, fpk , fpnk ); |
||||
C_SUB( f2k, fpk , fpnk ); |
||||
C_MUL( tw , f2k , st->super_twiddles[k]); |
||||
|
||||
freqdata[k].r = HALF_OF(f1k.r + tw.r); |
||||
freqdata[k].i = HALF_OF(f1k.i + tw.i); |
||||
freqdata[ncfft-k].r = HALF_OF(f1k.r - tw.r); |
||||
freqdata[ncfft-k].i = HALF_OF(tw.i - f1k.i); |
||||
} |
||||
} |
||||
|
||||
void kiss_fftri(kiss_fftr_cfg st,const kiss_fft_cpx *freqdata, kiss_fft_scalar *timedata) |
||||
{ |
||||
/* input buffer timedata is stored row-wise */ |
||||
int k, ncfft; |
||||
|
||||
if (st->substate->inverse == 0) { |
||||
speex_fatal("kiss fft usage error: improper alloc\n"); |
||||
} |
||||
|
||||
ncfft = st->substate->nfft; |
||||
|
||||
st->tmpbuf[0].r = freqdata[0].r + freqdata[ncfft].r; |
||||
st->tmpbuf[0].i = freqdata[0].r - freqdata[ncfft].r; |
||||
/*C_FIXDIV(st->tmpbuf[0],2);*/ |
||||
|
||||
for (k = 1; k <= ncfft / 2; ++k) { |
||||
kiss_fft_cpx fk, fnkc, fek, fok, tmp; |
||||
fk = freqdata[k]; |
||||
fnkc.r = freqdata[ncfft - k].r; |
||||
fnkc.i = -freqdata[ncfft - k].i; |
||||
/*C_FIXDIV( fk , 2 );
|
||||
C_FIXDIV( fnkc , 2 );*/ |
||||
|
||||
C_ADD (fek, fk, fnkc); |
||||
C_SUB (tmp, fk, fnkc); |
||||
C_MUL (fok, tmp, st->super_twiddles[k]); |
||||
C_ADD (st->tmpbuf[k], fek, fok); |
||||
C_SUB (st->tmpbuf[ncfft - k], fek, fok); |
||||
#ifdef USE_SIMD |
||||
st->tmpbuf[ncfft - k].i *= _mm_set1_ps(-1.0); |
||||
#else |
||||
st->tmpbuf[ncfft - k].i *= -1; |
||||
#endif |
||||
} |
||||
kiss_fft (st->substate, st->tmpbuf, (kiss_fft_cpx *) timedata); |
||||
} |
||||
|
||||
void kiss_fftr2(kiss_fftr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_scalar *freqdata) |
||||
{ |
||||
/* input buffer timedata is stored row-wise */ |
||||
int k,ncfft; |
||||
kiss_fft_cpx f2k,tdc; |
||||
spx_word32_t f1kr, f1ki, twr, twi; |
||||
|
||||
if ( st->substate->inverse) { |
||||
speex_fatal("kiss fft usage error: improper alloc\n"); |
||||
} |
||||
|
||||
ncfft = st->substate->nfft; |
||||
|
||||
/*perform the parallel fft of two real signals packed in real,imag*/ |
||||
kiss_fft( st->substate , (const kiss_fft_cpx*)timedata, st->tmpbuf ); |
||||
/* The real part of the DC element of the frequency spectrum in st->tmpbuf
|
||||
* contains the sum of the even-numbered elements of the input time sequence |
||||
* The imag part is the sum of the odd-numbered elements |
||||
* |
||||
* The sum of tdc.r and tdc.i is the sum of the input time sequence. |
||||
* yielding DC of input time sequence |
||||
* The difference of tdc.r - tdc.i is the sum of the input (dot product) [1,-1,1,-1... |
||||
* yielding Nyquist bin of input time sequence |
||||
*/ |
||||
|
||||
tdc.r = st->tmpbuf[0].r; |
||||
tdc.i = st->tmpbuf[0].i; |
||||
C_FIXDIV(tdc,2); |
||||
CHECK_OVERFLOW_OP(tdc.r ,+, tdc.i); |
||||
CHECK_OVERFLOW_OP(tdc.r ,-, tdc.i); |
||||
freqdata[0] = tdc.r + tdc.i; |
||||
freqdata[2*ncfft-1] = tdc.r - tdc.i; |
||||
|
||||
for ( k=1;k <= ncfft/2 ; ++k ) |
||||
{ |
||||
/*fpk = st->tmpbuf[k];
|
||||
fpnk.r = st->tmpbuf[ncfft-k].r; |
||||
fpnk.i = - st->tmpbuf[ncfft-k].i; |
||||
C_FIXDIV(fpk,2); |
||||
C_FIXDIV(fpnk,2); |
||||
|
||||
C_ADD( f1k, fpk , fpnk ); |
||||
C_SUB( f2k, fpk , fpnk ); |
||||
|
||||
C_MUL( tw , f2k , st->super_twiddles[k]); |
||||
|
||||
freqdata[2*k-1] = HALF_OF(f1k.r + tw.r); |
||||
freqdata[2*k] = HALF_OF(f1k.i + tw.i); |
||||
freqdata[2*(ncfft-k)-1] = HALF_OF(f1k.r - tw.r); |
||||
freqdata[2*(ncfft-k)] = HALF_OF(tw.i - f1k.i); |
||||
*/ |
||||
|
||||
/*f1k.r = PSHR32(ADD32(EXTEND32(st->tmpbuf[k].r), EXTEND32(st->tmpbuf[ncfft-k].r)),1);
|
||||
f1k.i = PSHR32(SUB32(EXTEND32(st->tmpbuf[k].i), EXTEND32(st->tmpbuf[ncfft-k].i)),1); |
||||
f2k.r = PSHR32(SUB32(EXTEND32(st->tmpbuf[k].r), EXTEND32(st->tmpbuf[ncfft-k].r)),1); |
||||
f2k.i = SHR32(ADD32(EXTEND32(st->tmpbuf[k].i), EXTEND32(st->tmpbuf[ncfft-k].i)),1); |
||||
|
||||
C_MUL( tw , f2k , st->super_twiddles[k]); |
||||
|
||||
freqdata[2*k-1] = HALF_OF(f1k.r + tw.r); |
||||
freqdata[2*k] = HALF_OF(f1k.i + tw.i); |
||||
freqdata[2*(ncfft-k)-1] = HALF_OF(f1k.r - tw.r); |
||||
freqdata[2*(ncfft-k)] = HALF_OF(tw.i - f1k.i); |
||||
*/ |
||||
f2k.r = SHR32(SUB32(EXTEND32(st->tmpbuf[k].r), EXTEND32(st->tmpbuf[ncfft-k].r)),1); |
||||
f2k.i = PSHR32(ADD32(EXTEND32(st->tmpbuf[k].i), EXTEND32(st->tmpbuf[ncfft-k].i)),1); |
||||
|
||||
f1kr = SHL32(ADD32(EXTEND32(st->tmpbuf[k].r), EXTEND32(st->tmpbuf[ncfft-k].r)),13); |
||||
f1ki = SHL32(SUB32(EXTEND32(st->tmpbuf[k].i), EXTEND32(st->tmpbuf[ncfft-k].i)),13); |
||||
|
||||
twr = SHR32(SUB32(MULT16_16(f2k.r,st->super_twiddles[k].r),MULT16_16(f2k.i,st->super_twiddles[k].i)), 1); |
||||
twi = SHR32(ADD32(MULT16_16(f2k.i,st->super_twiddles[k].r),MULT16_16(f2k.r,st->super_twiddles[k].i)), 1); |
||||
|
||||
#ifdef FIXED_POINT |
||||
freqdata[2*k-1] = PSHR32(f1kr + twr, 15); |
||||
freqdata[2*k] = PSHR32(f1ki + twi, 15); |
||||
freqdata[2*(ncfft-k)-1] = PSHR32(f1kr - twr, 15); |
||||
freqdata[2*(ncfft-k)] = PSHR32(twi - f1ki, 15); |
||||
#else |
||||
freqdata[2*k-1] = .5f*(f1kr + twr); |
||||
freqdata[2*k] = .5f*(f1ki + twi); |
||||
freqdata[2*(ncfft-k)-1] = .5f*(f1kr - twr); |
||||
freqdata[2*(ncfft-k)] = .5f*(twi - f1ki); |
||||
|
||||
#endif |
||||
} |
||||
} |
||||
|
||||
void kiss_fftri2(kiss_fftr_cfg st,const kiss_fft_scalar *freqdata,kiss_fft_scalar *timedata) |
||||
{ |
||||
/* input buffer timedata is stored row-wise */ |
||||
int k, ncfft; |
||||
|
||||
if (st->substate->inverse == 0) { |
||||
speex_fatal ("kiss fft usage error: improper alloc\n"); |
||||
} |
||||
|
||||
ncfft = st->substate->nfft; |
||||
|
||||
st->tmpbuf[0].r = freqdata[0] + freqdata[2*ncfft-1]; |
||||
st->tmpbuf[0].i = freqdata[0] - freqdata[2*ncfft-1]; |
||||
/*C_FIXDIV(st->tmpbuf[0],2);*/ |
||||
|
||||
for (k = 1; k <= ncfft / 2; ++k) { |
||||
kiss_fft_cpx fk, fnkc, fek, fok, tmp; |
||||
fk.r = freqdata[2*k-1]; |
||||
fk.i = freqdata[2*k]; |
||||
fnkc.r = freqdata[2*(ncfft - k)-1]; |
||||
fnkc.i = -freqdata[2*(ncfft - k)]; |
||||
/*C_FIXDIV( fk , 2 );
|
||||
C_FIXDIV( fnkc , 2 );*/ |
||||
|
||||
C_ADD (fek, fk, fnkc); |
||||
C_SUB (tmp, fk, fnkc); |
||||
C_MUL (fok, tmp, st->super_twiddles[k]); |
||||
C_ADD (st->tmpbuf[k], fek, fok); |
||||
C_SUB (st->tmpbuf[ncfft - k], fek, fok); |
||||
#ifdef USE_SIMD |
||||
st->tmpbuf[ncfft - k].i *= _mm_set1_ps(-1.0); |
||||
#else |
||||
st->tmpbuf[ncfft - k].i *= -1; |
||||
#endif |
||||
} |
||||
kiss_fft (st->substate, st->tmpbuf, (kiss_fft_cpx *) timedata); |
||||
} |
||||
@ -0,0 +1,51 @@
|
||||
#ifndef KISS_FTR_H |
||||
#define KISS_FTR_H |
||||
|
||||
#include "kiss_fft.h" |
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
|
||||
/*
|
||||
|
||||
Real optimized version can save about 45% cpu time vs. complex fft of a real seq. |
||||
|
||||
|
||||
|
||||
*/ |
||||
|
||||
typedef struct kiss_fftr_state *kiss_fftr_cfg; |
||||
|
||||
|
||||
kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem, size_t * lenmem); |
||||
/*
|
||||
nfft must be even |
||||
|
||||
If you don't care to allocate space, use mem = lenmem = NULL |
||||
*/ |
||||
|
||||
|
||||
void kiss_fftr(kiss_fftr_cfg cfg,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata); |
||||
/*
|
||||
input timedata has nfft scalar points |
||||
output freqdata has nfft/2+1 complex points |
||||
*/ |
||||
|
||||
void kiss_fftr2(kiss_fftr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_scalar *freqdata); |
||||
|
||||
void kiss_fftri(kiss_fftr_cfg cfg,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata); |
||||
|
||||
void kiss_fftri2(kiss_fftr_cfg st,const kiss_fft_scalar *freqdata, kiss_fft_scalar *timedata); |
||||
|
||||
/*
|
||||
input freqdata has nfft/2+1 complex points |
||||
output timedata has nfft scalar points |
||||
*/ |
||||
|
||||
#define kiss_fftr_free speex_free |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif |
||||
@ -0,0 +1,332 @@
|
||||
/* Copyright (C) 2002 Jean-Marc Valin */ |
||||
/**
|
||||
@file math_approx.h |
||||
@brief Various math approximation functions for Speex |
||||
*/ |
||||
/*
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions |
||||
are met: |
||||
|
||||
- Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
|
||||
- Redistributions in binary form must reproduce the above copyright |
||||
notice, this list of conditions and the following disclaimer in the |
||||
documentation and/or other materials provided with the distribution. |
||||
|
||||
- Neither the name of the Xiph.org Foundation nor the names of its |
||||
contributors may be used to endorse or promote products derived from |
||||
this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR |
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
#ifndef MATH_APPROX_H |
||||
#define MATH_APPROX_H |
||||
|
||||
#include "arch.h" |
||||
|
||||
#ifndef FIXED_POINT |
||||
|
||||
#define spx_sqrt sqrt |
||||
#define spx_acos acos |
||||
#define spx_exp exp |
||||
#define spx_cos_norm(x) (cos((.5f*M_PI)*(x))) |
||||
#define spx_atan atan |
||||
|
||||
/** Generate a pseudo-random number */ |
||||
static inline spx_word16_t speex_rand(spx_word16_t std, spx_int32_t *seed) |
||||
{ |
||||
const unsigned int jflone = 0x3f800000; |
||||
const unsigned int jflmsk = 0x007fffff; |
||||
union {int i; float f;} ran; |
||||
*seed = 1664525 * *seed + 1013904223; |
||||
ran.i = jflone | (jflmsk & *seed); |
||||
ran.f -= 1.5; |
||||
return 3.4642*std*ran.f; |
||||
} |
||||
|
||||
|
||||
#endif |
||||
|
||||
|
||||
static inline spx_int16_t spx_ilog2(spx_uint32_t x) |
||||
{ |
||||
int r=0; |
||||
if (x>=(spx_int32_t)65536) |
||||
{ |
||||
x >>= 16; |
||||
r += 16; |
||||
} |
||||
if (x>=256) |
||||
{ |
||||
x >>= 8; |
||||
r += 8; |
||||
} |
||||
if (x>=16) |
||||
{ |
||||
x >>= 4; |
||||
r += 4; |
||||
} |
||||
if (x>=4) |
||||
{ |
||||
x >>= 2; |
||||
r += 2; |
||||
} |
||||
if (x>=2) |
||||
{ |
||||
r += 1; |
||||
} |
||||
return r; |
||||
} |
||||
|
||||
static inline spx_int16_t spx_ilog4(spx_uint32_t x) |
||||
{ |
||||
int r=0; |
||||
if (x>=(spx_int32_t)65536) |
||||
{ |
||||
x >>= 16; |
||||
r += 8; |
||||
} |
||||
if (x>=256) |
||||
{ |
||||
x >>= 8; |
||||
r += 4; |
||||
} |
||||
if (x>=16) |
||||
{ |
||||
x >>= 4; |
||||
r += 2; |
||||
} |
||||
if (x>=4) |
||||
{ |
||||
r += 1; |
||||
} |
||||
return r; |
||||
} |
||||
|
||||
#ifdef FIXED_POINT |
||||
|
||||
/** Generate a pseudo-random number */ |
||||
static inline spx_word16_t speex_rand(spx_word16_t std, spx_int32_t *seed) |
||||
{ |
||||
spx_word32_t res; |
||||
*seed = 1664525 * *seed + 1013904223; |
||||
res = MULT16_16(EXTRACT16(SHR32(*seed,16)),std); |
||||
return EXTRACT16(PSHR32(SUB32(res, SHR32(res, 3)),14)); |
||||
} |
||||
|
||||
/* sqrt(x) ~= 0.22178 + 1.29227*x - 0.77070*x^2 + 0.25723*x^3 (for .25 < x < 1) */ |
||||
/*#define C0 3634
|
||||
#define C1 21173 |
||||
#define C2 -12627 |
||||
#define C3 4215*/ |
||||
|
||||
/* sqrt(x) ~= 0.22178 + 1.29227*x - 0.77070*x^2 + 0.25659*x^3 (for .25 < x < 1) */ |
||||
#define C0 3634 |
||||
#define C1 21173 |
||||
#define C2 -12627 |
||||
#define C3 4204 |
||||
|
||||
static inline spx_word16_t spx_sqrt(spx_word32_t x) |
||||
{ |
||||
int k; |
||||
spx_word32_t rt; |
||||
k = spx_ilog4(x)-6; |
||||
x = VSHR32(x, (k<<1)); |
||||
rt = ADD16(C0, MULT16_16_Q14(x, ADD16(C1, MULT16_16_Q14(x, ADD16(C2, MULT16_16_Q14(x, (C3))))))); |
||||
rt = VSHR32(rt,7-k); |
||||
return rt; |
||||
} |
||||
|
||||
/* log(x) ~= -2.18151 + 4.20592*x - 2.88938*x^2 + 0.86535*x^3 (for .5 < x < 1) */ |
||||
|
||||
|
||||
#define A1 16469 |
||||
#define A2 2242 |
||||
#define A3 1486 |
||||
|
||||
static inline spx_word16_t spx_acos(spx_word16_t x) |
||||
{ |
||||
int s=0; |
||||
spx_word16_t ret; |
||||
spx_word16_t sq; |
||||
if (x<0) |
||||
{ |
||||
s=1; |
||||
x = NEG16(x); |
||||
} |
||||
x = SUB16(16384,x); |
||||
|
||||
x = x >> 1; |
||||
sq = MULT16_16_Q13(x, ADD16(A1, MULT16_16_Q13(x, ADD16(A2, MULT16_16_Q13(x, (A3)))))); |
||||
ret = spx_sqrt(SHL32(EXTEND32(sq),13)); |
||||
|
||||
/*ret = spx_sqrt(67108864*(-1.6129e-04 + 2.0104e+00*f + 2.7373e-01*f*f + 1.8136e-01*f*f*f));*/ |
||||
if (s) |
||||
ret = SUB16(25736,ret); |
||||
return ret; |
||||
} |
||||
|
||||
|
||||
#define K1 8192 |
||||
#define K2 -4096 |
||||
#define K3 340 |
||||
#define K4 -10 |
||||
|
||||
static inline spx_word16_t spx_cos(spx_word16_t x) |
||||
{ |
||||
spx_word16_t x2; |
||||
|
||||
if (x<12868) |
||||
{ |
||||
x2 = MULT16_16_P13(x,x); |
||||
return ADD32(K1, MULT16_16_P13(x2, ADD32(K2, MULT16_16_P13(x2, ADD32(K3, MULT16_16_P13(K4, x2)))))); |
||||
} else { |
||||
x = SUB16(25736,x); |
||||
x2 = MULT16_16_P13(x,x); |
||||
return SUB32(-K1, MULT16_16_P13(x2, ADD32(K2, MULT16_16_P13(x2, ADD32(K3, MULT16_16_P13(K4, x2)))))); |
||||
} |
||||
} |
||||
|
||||
#define L1 32767 |
||||
#define L2 -7651 |
||||
#define L3 8277 |
||||
#define L4 -626 |
||||
|
||||
static inline spx_word16_t _spx_cos_pi_2(spx_word16_t x) |
||||
{ |
||||
spx_word16_t x2; |
||||
|
||||
x2 = MULT16_16_P15(x,x); |
||||
return ADD16(1,MIN16(32766,ADD32(SUB16(L1,x2), MULT16_16_P15(x2, ADD32(L2, MULT16_16_P15(x2, ADD32(L3, MULT16_16_P15(L4, x2)))))))); |
||||
} |
||||
|
||||
static inline spx_word16_t spx_cos_norm(spx_word32_t x) |
||||
{ |
||||
x = x&0x0001ffff; |
||||
if (x>SHL32(EXTEND32(1), 16)) |
||||
x = SUB32(SHL32(EXTEND32(1), 17),x); |
||||
if (x&0x00007fff) |
||||
{ |
||||
if (x<SHL32(EXTEND32(1), 15)) |
||||
{ |
||||
return _spx_cos_pi_2(EXTRACT16(x)); |
||||
} else { |
||||
return NEG32(_spx_cos_pi_2(EXTRACT16(65536-x))); |
||||
} |
||||
} else { |
||||
if (x&0x0000ffff) |
||||
return 0; |
||||
else if (x&0x0001ffff) |
||||
return -32767; |
||||
else |
||||
return 32767; |
||||
} |
||||
} |
||||
|
||||
/*
|
||||
K0 = 1 |
||||
K1 = log(2) |
||||
K2 = 3-4*log(2) |
||||
K3 = 3*log(2) - 2 |
||||
*/ |
||||
#define D0 16384 |
||||
#define D1 11356 |
||||
#define D2 3726 |
||||
#define D3 1301 |
||||
/* Input in Q11 format, output in Q16 */ |
||||
static inline spx_word32_t spx_exp2(spx_word16_t x) |
||||
{ |
||||
int integer; |
||||
spx_word16_t frac; |
||||
integer = SHR16(x,11); |
||||
if (integer>14) |
||||
return 0x7fffffff; |
||||
else if (integer < -15) |
||||
return 0; |
||||
frac = SHL16(x-SHL16(integer,11),3); |
||||
frac = ADD16(D0, MULT16_16_Q14(frac, ADD16(D1, MULT16_16_Q14(frac, ADD16(D2 , MULT16_16_Q14(D3,frac)))))); |
||||
return VSHR32(EXTEND32(frac), -integer-2); |
||||
} |
||||
|
||||
/* Input in Q11 format, output in Q16 */ |
||||
static inline spx_word32_t spx_exp(spx_word16_t x) |
||||
{ |
||||
if (x>21290) |
||||
return 0x7fffffff; |
||||
else if (x<-21290) |
||||
return 0; |
||||
else |
||||
return spx_exp2(MULT16_16_P14(23637,x)); |
||||
} |
||||
#define M1 32767 |
||||
#define M2 -21 |
||||
#define M3 -11943 |
||||
#define M4 4936 |
||||
|
||||
static inline spx_word16_t spx_atan01(spx_word16_t x) |
||||
{ |
||||
return MULT16_16_P15(x, ADD32(M1, MULT16_16_P15(x, ADD32(M2, MULT16_16_P15(x, ADD32(M3, MULT16_16_P15(M4, x))))))); |
||||
} |
||||
|
||||
#undef M1 |
||||
#undef M2 |
||||
#undef M3 |
||||
#undef M4 |
||||
|
||||
/* Input in Q15, output in Q14 */ |
||||
static inline spx_word16_t spx_atan(spx_word32_t x) |
||||
{ |
||||
if (x <= 32767) |
||||
{ |
||||
return SHR16(spx_atan01(x),1); |
||||
} else { |
||||
int e = spx_ilog2(x); |
||||
if (e>=29) |
||||
return 25736; |
||||
x = DIV32_16(SHL32(EXTEND32(32767),29-e), EXTRACT16(SHR32(x, e-14))); |
||||
return SUB16(25736, SHR16(spx_atan01(x),1)); |
||||
} |
||||
} |
||||
#else |
||||
|
||||
#ifndef M_PI |
||||
#define M_PI 3.14159265358979323846 /* pi */ |
||||
#endif |
||||
|
||||
#define C1 0.9999932946f |
||||
#define C2 -0.4999124376f |
||||
#define C3 0.0414877472f |
||||
#define C4 -0.0012712095f |
||||
|
||||
|
||||
#define SPX_PI_2 1.5707963268 |
||||
static inline spx_word16_t spx_cos(spx_word16_t x) |
||||
{ |
||||
if (x<SPX_PI_2) |
||||
{ |
||||
x *= x; |
||||
return C1 + x*(C2+x*(C3+C4*x)); |
||||
} else { |
||||
x = M_PI-x; |
||||
x *= x; |
||||
return NEG16(C1 + x*(C2+x*(C3+C4*x))); |
||||
} |
||||
} |
||||
|
||||
#endif |
||||
|
||||
|
||||
#endif |
||||
@ -0,0 +1,169 @@
|
||||
/* Copyright (C) 2007 Jean-Marc Valin
|
||||
|
||||
File: os_support.h |
||||
This is the (tiny) OS abstraction layer. Aside from math.h, this is the |
||||
only place where system headers are allowed. |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions are |
||||
met: |
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, |
||||
this list of conditions and the following disclaimer. |
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright |
||||
notice, this list of conditions and the following disclaimer in the |
||||
documentation and/or other materials provided with the distribution. |
||||
|
||||
3. The name of the author may not be used to endorse or promote products |
||||
derived from this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
||||
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, |
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
||||
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
||||
POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
#ifndef OS_SUPPORT_H |
||||
#define OS_SUPPORT_H |
||||
|
||||
#include <string.h> |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
|
||||
#ifdef HAVE_CONFIG_H |
||||
#include "config.h" |
||||
#endif |
||||
#ifdef OS_SUPPORT_CUSTOM |
||||
#include "os_support_custom.h" |
||||
#endif |
||||
|
||||
/** Speex wrapper for calloc. To do your own dynamic allocation, all you need to do is replace this function, speex_realloc and speex_free
|
||||
NOTE: speex_alloc needs to CLEAR THE MEMORY */ |
||||
#ifndef OVERRIDE_SPEEX_ALLOC |
||||
static inline void *speex_alloc (int size) |
||||
{ |
||||
/* WARNING: this is not equivalent to malloc(). If you want to use malloc()
|
||||
or your own allocator, YOU NEED TO CLEAR THE MEMORY ALLOCATED. Otherwise |
||||
you will experience strange bugs */ |
||||
return calloc(size,1); |
||||
} |
||||
#endif |
||||
|
||||
/** Same as speex_alloc, except that the area is only needed inside a Speex call (might cause problem with wideband though) */ |
||||
#ifndef OVERRIDE_SPEEX_ALLOC_SCRATCH |
||||
static inline void *speex_alloc_scratch (int size) |
||||
{ |
||||
/* Scratch space doesn't need to be cleared */ |
||||
return calloc(size,1); |
||||
} |
||||
#endif |
||||
|
||||
/** Speex wrapper for realloc. To do your own dynamic allocation, all you need to do is replace this function, speex_alloc and speex_free */ |
||||
#ifndef OVERRIDE_SPEEX_REALLOC |
||||
static inline void *speex_realloc (void *ptr, int size) |
||||
{ |
||||
return realloc(ptr, size); |
||||
} |
||||
#endif |
||||
|
||||
/** Speex wrapper for calloc. To do your own dynamic allocation, all you need to do is replace this function, speex_realloc and speex_alloc */ |
||||
#ifndef OVERRIDE_SPEEX_FREE |
||||
static inline void speex_free (void *ptr) |
||||
{ |
||||
free(ptr); |
||||
} |
||||
#endif |
||||
|
||||
/** Same as speex_free, except that the area is only needed inside a Speex call (might cause problem with wideband though) */ |
||||
#ifndef OVERRIDE_SPEEX_FREE_SCRATCH |
||||
static inline void speex_free_scratch (void *ptr) |
||||
{ |
||||
free(ptr); |
||||
} |
||||
#endif |
||||
|
||||
/** Copy n elements from src to dst. The 0* term provides compile-time type checking */ |
||||
#ifndef OVERRIDE_SPEEX_COPY |
||||
#define SPEEX_COPY(dst, src, n) (memcpy((dst), (src), (n)*sizeof(*(dst)) + 0*((dst)-(src)) )) |
||||
#endif |
||||
|
||||
/** Copy n elements from src to dst, allowing overlapping regions. The 0* term
|
||||
provides compile-time type checking */ |
||||
#ifndef OVERRIDE_SPEEX_MOVE |
||||
#define SPEEX_MOVE(dst, src, n) (memmove((dst), (src), (n)*sizeof(*(dst)) + 0*((dst)-(src)) )) |
||||
#endif |
||||
|
||||
/** For n elements worth of memory, set every byte to the value of c, starting at address dst */ |
||||
#ifndef OVERRIDE_SPEEX_MEMSET |
||||
#define SPEEX_MEMSET(dst, c, n) (memset((dst), (c), (n)*sizeof(*(dst)))) |
||||
#endif |
||||
|
||||
|
||||
#ifndef OVERRIDE_SPEEX_FATAL |
||||
static inline void _speex_fatal(const char *str, const char *file, int line) |
||||
{ |
||||
fprintf (stderr, "Fatal (internal) error in %s, line %d: %s\n", file, line, str); |
||||
exit(1); |
||||
} |
||||
#endif |
||||
|
||||
#ifndef OVERRIDE_SPEEX_WARNING |
||||
static inline void speex_warning(const char *str) |
||||
{ |
||||
#ifndef DISABLE_WARNINGS |
||||
fprintf (stderr, "warning: %s\n", str); |
||||
#endif |
||||
} |
||||
#endif |
||||
|
||||
#ifndef OVERRIDE_SPEEX_WARNING_INT |
||||
static inline void speex_warning_int(const char *str, int val) |
||||
{ |
||||
#ifndef DISABLE_WARNINGS |
||||
fprintf (stderr, "warning: %s %d\n", str, val); |
||||
#endif |
||||
} |
||||
#endif |
||||
|
||||
#ifndef OVERRIDE_SPEEX_NOTIFY |
||||
static inline void speex_notify(const char *str) |
||||
{ |
||||
#ifndef DISABLE_NOTIFICATIONS |
||||
fprintf (stderr, "notification: %s\n", str); |
||||
#endif |
||||
} |
||||
#endif |
||||
|
||||
#ifndef OVERRIDE_SPEEX_PUTC |
||||
/** Speex wrapper for putc */ |
||||
static inline void _speex_putc(int ch, void *file) |
||||
{ |
||||
FILE *f = (FILE *)file; |
||||
fprintf(f, "%c", ch); |
||||
} |
||||
#endif |
||||
|
||||
#define speex_fatal(str) _speex_fatal(str, __FILE__, __LINE__); |
||||
#define speex_assert(cond) {if (!(cond)) {speex_fatal("assertion failed: " #cond);}} |
||||
|
||||
#ifndef RELEASE |
||||
static inline void print_vec(float *vec, int len, char *name) |
||||
{ |
||||
int i; |
||||
printf ("%s ", name); |
||||
for (i=0;i<len;i++) |
||||
printf (" %f", vec[i]); |
||||
printf ("\n"); |
||||
} |
||||
#endif |
||||
|
||||
#endif |
||||
|
||||
@ -0,0 +1,379 @@
|
||||
/* Copyright (C) 2005 Jean-Marc Valin */ |
||||
/**
|
||||
@file pseudofloat.h |
||||
@brief Pseudo-floating point |
||||
* This header file provides a lightweight floating point type for |
||||
* use on fixed-point platforms when a large dynamic range is |
||||
* required. The new type is not compatible with the 32-bit IEEE format, |
||||
* it is not even remotely as accurate as 32-bit floats, and is not |
||||
* even guaranteed to produce even remotely correct results for code |
||||
* other than Speex. It makes all kinds of shortcuts that are acceptable |
||||
* for Speex, but may not be acceptable for your application. You're |
||||
* quite welcome to reuse this code and improve it, but don't assume |
||||
* it works out of the box. Most likely, it doesn't. |
||||
*/ |
||||
/*
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions |
||||
are met: |
||||
|
||||
- Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
|
||||
- Redistributions in binary form must reproduce the above copyright |
||||
notice, this list of conditions and the following disclaimer in the |
||||
documentation and/or other materials provided with the distribution. |
||||
|
||||
- Neither the name of the Xiph.org Foundation nor the names of its |
||||
contributors may be used to endorse or promote products derived from |
||||
this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR |
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
*/ |
||||
|
||||
#ifndef PSEUDOFLOAT_H |
||||
#define PSEUDOFLOAT_H |
||||
|
||||
#include "arch.h" |
||||
#include "os_support.h" |
||||
#include "math_approx.h" |
||||
#include <math.h> |
||||
|
||||
#ifdef FIXED_POINT |
||||
|
||||
typedef struct { |
||||
spx_int16_t m; |
||||
spx_int16_t e; |
||||
} spx_float_t; |
||||
|
||||
static const spx_float_t FLOAT_ZERO = {0,0}; |
||||
static const spx_float_t FLOAT_ONE = {16384,-14}; |
||||
static const spx_float_t FLOAT_HALF = {16384,-15}; |
||||
|
||||
#define MIN(a,b) ((a)<(b)?(a):(b)) |
||||
static inline spx_float_t PSEUDOFLOAT(spx_int32_t x) |
||||
{ |
||||
int e=0; |
||||
int sign=0; |
||||
if (x<0) |
||||
{ |
||||
sign = 1; |
||||
x = -x; |
||||
} |
||||
if (x==0) |
||||
{ |
||||
spx_float_t r = {0,0}; |
||||
return r; |
||||
} |
||||
e = spx_ilog2(ABS32(x))-14; |
||||
x = VSHR32(x, e); |
||||
if (sign) |
||||
{ |
||||
spx_float_t r; |
||||
r.m = -x; |
||||
r.e = e; |
||||
return r; |
||||
} |
||||
else |
||||
{ |
||||
spx_float_t r; |
||||
r.m = x; |
||||
r.e = e; |
||||
return r; |
||||
} |
||||
} |
||||
|
||||
|
||||
static inline spx_float_t FLOAT_ADD(spx_float_t a, spx_float_t b) |
||||
{ |
||||
spx_float_t r; |
||||
if (a.m==0) |
||||
return b; |
||||
else if (b.m==0) |
||||
return a; |
||||
if ((a).e > (b).e) |
||||
{ |
||||
r.m = ((a).m>>1) + ((b).m>>MIN(15,(a).e-(b).e+1)); |
||||
r.e = (a).e+1; |
||||
} |
||||
else |
||||
{ |
||||
r.m = ((b).m>>1) + ((a).m>>MIN(15,(b).e-(a).e+1)); |
||||
r.e = (b).e+1; |
||||
} |
||||
if (r.m>0) |
||||
{ |
||||
if (r.m<16384) |
||||
{ |
||||
r.m<<=1; |
||||
r.e-=1; |
||||
} |
||||
} else { |
||||
if (r.m>-16384) |
||||
{ |
||||
r.m<<=1; |
||||
r.e-=1; |
||||
} |
||||
} |
||||
/*printf ("%f + %f = %f\n", REALFLOAT(a), REALFLOAT(b), REALFLOAT(r));*/ |
||||
return r; |
||||
} |
||||
|
||||
static inline spx_float_t FLOAT_SUB(spx_float_t a, spx_float_t b) |
||||
{ |
||||
spx_float_t r; |
||||
if (a.m==0) |
||||
return b; |
||||
else if (b.m==0) |
||||
return a; |
||||
if ((a).e > (b).e) |
||||
{ |
||||
r.m = ((a).m>>1) - ((b).m>>MIN(15,(a).e-(b).e+1)); |
||||
r.e = (a).e+1; |
||||
} |
||||
else |
||||
{ |
||||
r.m = ((a).m>>MIN(15,(b).e-(a).e+1)) - ((b).m>>1); |
||||
r.e = (b).e+1; |
||||
} |
||||
if (r.m>0) |
||||
{ |
||||
if (r.m<16384) |
||||
{ |
||||
r.m<<=1; |
||||
r.e-=1; |
||||
} |
||||
} else { |
||||
if (r.m>-16384) |
||||
{ |
||||
r.m<<=1; |
||||
r.e-=1; |
||||
} |
||||
} |
||||
/*printf ("%f + %f = %f\n", REALFLOAT(a), REALFLOAT(b), REALFLOAT(r));*/ |
||||
return r; |
||||
} |
||||
|
||||
static inline int FLOAT_LT(spx_float_t a, spx_float_t b) |
||||
{ |
||||
if (a.m==0) |
||||
return b.m>0; |
||||
else if (b.m==0) |
||||
return a.m<0; |
||||
if ((a).e > (b).e) |
||||
return ((a).m>>1) < ((b).m>>MIN(15,(a).e-(b).e+1)); |
||||
else |
||||
return ((b).m>>1) > ((a).m>>MIN(15,(b).e-(a).e+1)); |
||||
|
||||
} |
||||
|
||||
static inline int FLOAT_GT(spx_float_t a, spx_float_t b) |
||||
{ |
||||
return FLOAT_LT(b,a); |
||||
} |
||||
|
||||
static inline spx_float_t FLOAT_MULT(spx_float_t a, spx_float_t b) |
||||
{ |
||||
spx_float_t r; |
||||
r.m = (spx_int16_t)((spx_int32_t)(a).m*(b).m>>15); |
||||
r.e = (a).e+(b).e+15; |
||||
if (r.m>0) |
||||
{ |
||||
if (r.m<16384) |
||||
{ |
||||
r.m<<=1; |
||||
r.e-=1; |
||||
} |
||||
} else { |
||||
if (r.m>-16384) |
||||
{ |
||||
r.m<<=1; |
||||
r.e-=1; |
||||
} |
||||
} |
||||
/*printf ("%f * %f = %f\n", REALFLOAT(a), REALFLOAT(b), REALFLOAT(r));*/ |
||||
return r; |
||||
} |
||||
|
||||
static inline spx_float_t FLOAT_AMULT(spx_float_t a, spx_float_t b) |
||||
{ |
||||
spx_float_t r; |
||||
r.m = (spx_int16_t)((spx_int32_t)(a).m*(b).m>>15); |
||||
r.e = (a).e+(b).e+15; |
||||
return r; |
||||
} |
||||
|
||||
|
||||
static inline spx_float_t FLOAT_SHL(spx_float_t a, int b) |
||||
{ |
||||
spx_float_t r; |
||||
r.m = a.m; |
||||
r.e = a.e+b; |
||||
return r; |
||||
} |
||||
|
||||
static inline spx_int16_t FLOAT_EXTRACT16(spx_float_t a) |
||||
{ |
||||
if (a.e<0) |
||||
return EXTRACT16((EXTEND32(a.m)+(EXTEND32(1)<<(-a.e-1)))>>-a.e); |
||||
else |
||||
return a.m<<a.e; |
||||
} |
||||
|
||||
static inline spx_int32_t FLOAT_EXTRACT32(spx_float_t a) |
||||
{ |
||||
if (a.e<0) |
||||
return (EXTEND32(a.m)+(EXTEND32(1)<<(-a.e-1)))>>-a.e; |
||||
else |
||||
return EXTEND32(a.m)<<a.e; |
||||
} |
||||
|
||||
static inline spx_int32_t FLOAT_MUL32(spx_float_t a, spx_word32_t b) |
||||
{ |
||||
return VSHR32(MULT16_32_Q15(a.m, b),-a.e-15); |
||||
} |
||||
|
||||
static inline spx_float_t FLOAT_MUL32U(spx_word32_t a, spx_word32_t b) |
||||
{ |
||||
int e1, e2; |
||||
spx_float_t r; |
||||
if (a==0 || b==0) |
||||
{ |
||||
return FLOAT_ZERO; |
||||
} |
||||
e1 = spx_ilog2(ABS32(a)); |
||||
a = VSHR32(a, e1-14); |
||||
e2 = spx_ilog2(ABS32(b)); |
||||
b = VSHR32(b, e2-14); |
||||
r.m = MULT16_16_Q15(a,b); |
||||
r.e = e1+e2-13; |
||||
return r; |
||||
} |
||||
|
||||
/* Do NOT attempt to divide by a negative number */ |
||||
static inline spx_float_t FLOAT_DIV32_FLOAT(spx_word32_t a, spx_float_t b) |
||||
{ |
||||
int e=0; |
||||
spx_float_t r; |
||||
if (a==0) |
||||
{ |
||||
return FLOAT_ZERO; |
||||
} |
||||
e = spx_ilog2(ABS32(a))-spx_ilog2(b.m-1)-15; |
||||
a = VSHR32(a, e); |
||||
if (ABS32(a)>=SHL32(EXTEND32(b.m-1),15)) |
||||
{ |
||||
a >>= 1; |
||||
e++; |
||||
} |
||||
r.m = DIV32_16(a,b.m); |
||||
r.e = e-b.e; |
||||
return r; |
||||
} |
||||
|
||||
|
||||
/* Do NOT attempt to divide by a negative number */ |
||||
static inline spx_float_t FLOAT_DIV32(spx_word32_t a, spx_word32_t b) |
||||
{ |
||||
int e0=0,e=0; |
||||
spx_float_t r; |
||||
if (a==0) |
||||
{ |
||||
return FLOAT_ZERO; |
||||
} |
||||
if (b>32767) |
||||
{ |
||||
e0 = spx_ilog2(b)-14; |
||||
b = VSHR32(b, e0); |
||||
e0 = -e0; |
||||
} |
||||
e = spx_ilog2(ABS32(a))-spx_ilog2(b-1)-15; |
||||
a = VSHR32(a, e); |
||||
if (ABS32(a)>=SHL32(EXTEND32(b-1),15)) |
||||
{ |
||||
a >>= 1; |
||||
e++; |
||||
} |
||||
e += e0; |
||||
r.m = DIV32_16(a,b); |
||||
r.e = e; |
||||
return r; |
||||
} |
||||
|
||||
/* Do NOT attempt to divide by a negative number */ |
||||
static inline spx_float_t FLOAT_DIVU(spx_float_t a, spx_float_t b) |
||||
{ |
||||
int e=0; |
||||
spx_int32_t num; |
||||
spx_float_t r; |
||||
if (b.m<=0) |
||||
{ |
||||
speex_warning_int("Attempted to divide by", b.m); |
||||
return FLOAT_ONE; |
||||
} |
||||
num = a.m; |
||||
a.m = ABS16(a.m); |
||||
while (a.m >= b.m) |
||||
{ |
||||
e++; |
||||
a.m >>= 1; |
||||
} |
||||
num = num << (15-e); |
||||
r.m = DIV32_16(num,b.m); |
||||
r.e = a.e-b.e-15+e; |
||||
return r; |
||||
} |
||||
|
||||
static inline spx_float_t FLOAT_SQRT(spx_float_t a) |
||||
{ |
||||
spx_float_t r; |
||||
spx_int32_t m; |
||||
m = SHL32(EXTEND32(a.m), 14); |
||||
r.e = a.e - 14; |
||||
if (r.e & 1) |
||||
{ |
||||
r.e -= 1; |
||||
m <<= 1; |
||||
} |
||||
r.e >>= 1; |
||||
r.m = spx_sqrt(m); |
||||
return r; |
||||
} |
||||
|
||||
#else |
||||
|
||||
#define spx_float_t float |
||||
#define FLOAT_ZERO 0.f |
||||
#define FLOAT_ONE 1.f |
||||
#define FLOAT_HALF 0.5f |
||||
#define PSEUDOFLOAT(x) (x) |
||||
#define FLOAT_MULT(a,b) ((a)*(b)) |
||||
#define FLOAT_AMULT(a,b) ((a)*(b)) |
||||
#define FLOAT_MUL32(a,b) ((a)*(b)) |
||||
#define FLOAT_DIV32(a,b) ((a)/(b)) |
||||
#define FLOAT_EXTRACT16(a) (a) |
||||
#define FLOAT_EXTRACT32(a) (a) |
||||
#define FLOAT_ADD(a,b) ((a)+(b)) |
||||
#define FLOAT_SUB(a,b) ((a)-(b)) |
||||
#define REALFLOAT(x) (x) |
||||
#define FLOAT_DIV32_FLOAT(a,b) ((a)/(b)) |
||||
#define FLOAT_MUL32U(a,b) ((a)*(b)) |
||||
#define FLOAT_SHL(a,b) (a) |
||||
#define FLOAT_LT(a,b) ((a)<(b)) |
||||
#define FLOAT_GT(a,b) ((a)>(b)) |
||||
#define FLOAT_DIVU(a,b) ((a)/(b)) |
||||
#define FLOAT_SQRT(a) (spx_sqrt(a)) |
||||
|
||||
#endif |
||||
|
||||
#endif |
||||
@ -0,0 +1,977 @@
|
||||
// call.c — P2P аудио-звонок: сигналинг + медиа (per-instance)
|
||||
//
|
||||
// Полный цикл: INVITE → RINGING → ACCEPT → conn_mgr (прямой линк) → ACTIVE →
|
||||
// медиа (CALL_MEDIA) → HANGUP/ENDED. Звонок из CHAT-группы или DM — модуль
|
||||
// оперирует (group_id, peer_node_id), маршрутизация через etcp_router.
|
||||
//
|
||||
// Сигналинг идёт с ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT, медиа — без подписи.
|
||||
// Реестр сессий: ll_queue с hash-индексом по call_id (8 байт).
|
||||
//
|
||||
// Thread-модель: всё в uasync-потоке (single-threaded). Освобождение сессии
|
||||
// отложено через uasync_call_soon, чтобы не было UAF при завершении из
|
||||
// коллбэков conn_mgr/таймеров.
|
||||
|
||||
#include "call.h" |
||||
#include "call_proto.h" |
||||
#include "../utun_instance.h" |
||||
#include "../chat/chat_event.h" |
||||
#include "../routing_layer/etcp_router.h" |
||||
#include "../routing_layer/route_crypto.h" |
||||
#include "../routing_layer/conn_mgr.h" |
||||
#include "../routing_layer/topo_group.h" |
||||
#include "../routing_layer/topo_node.h" |
||||
#include "../transport_layer/etcp_api.h" |
||||
#include "../transport_layer/etcp.h" |
||||
#include "../../lib/mem.h" |
||||
#include "../../lib/ll_queue.h" |
||||
#include "../../lib/debug_config.h" |
||||
#include "../../lib/u_async.h" |
||||
#include "../../lib/platform_compat.h" |
||||
|
||||
#include <string.h> |
||||
#include <stdlib.h> |
||||
#include <stdio.h> |
||||
|
||||
#define CALL_ID "call" |
||||
|
||||
enum call_state { |
||||
CALL_ST_IDLE = 0, |
||||
CALL_ST_OUTGOING, /* caller: INVITE отправлен */ |
||||
CALL_ST_RINGING, /* caller: получил RINGING; callee: отправил RINGING */ |
||||
CALL_ST_ACTIVE, /* вызов принят; доставка через роутер независимо от готовности CM */ |
||||
CALL_ST_ENDED, |
||||
}; |
||||
|
||||
enum call_role { CALL_ROLE_CALLER = 0, CALL_ROLE_CALLEE }; |
||||
|
||||
struct call_session { |
||||
struct ll_entry ll; /* dummy-заголовок; структура хранится в entry->data */ |
||||
uint64_t call_id; /* hash-индекс (8 байт) */ |
||||
uint64_t group_id; |
||||
uint64_t peer_node_id; |
||||
uint8_t role; /* CALL_ROLE_* */ |
||||
uint8_t state; /* CALL_ST_* */ |
||||
uint8_t reason; /* CALL_REASON_* (при ENDED) */ |
||||
struct call_ctx* ctx; /* обратная ссылка (для коллбэков conn_mgr/таймеров) */ |
||||
struct CONN_MGR_HANDLE* cm_handle; |
||||
void* ring_timer; /* caller: 45с ожидания ответа; callee: 2с watchdog кодограмм */ |
||||
void* invite_timer; /* caller: кодограмма INVITE каждые 300мс (OUTGOING+RINGING) */ |
||||
uint8_t no_route_strikes; /* caller: подряд ретрансмитов без маршрута (fail-fast) */ |
||||
void* traffic_timer; /* ACTIVE: 20с без медиа (запускается по первому медиа) */ |
||||
void* teardown_timer; /* отложенный разрыв conn_mgr (500мс после завершения) */ |
||||
void* stats_timer; /* ACTIVE: раз в 1с — CALL_STATS (rtt/buffer/tempo) */ |
||||
uint64_t last_media_tb; /* время последнего CALL_MEDIA (0.1ms) */ |
||||
uint16_t tx_media_seq; /* seq следующего исходящего CALL_MEDIA */ |
||||
struct ll_queue* tx_q; /* FIFO закодированных CALL_MEDIA (backpressure) */ |
||||
struct queue_waiter_handle send_waiter; /* waiter на send_q роутера */ |
||||
uint32_t c_tx_frames; /* отправлено медиа-кадров */ |
||||
uint32_t c_rx_frames; /* принято медиа-кадров */ |
||||
uint32_t c_tx_dropped; /* дропнуто при переполнении tx_q */ |
||||
uint32_t c_rx_dropped; /* дропнуто при переполнении jitter-буфера */ |
||||
}; |
||||
|
||||
struct call_ctx { |
||||
struct UTUN_INSTANCE* inst; |
||||
int initialized; |
||||
struct ll_queue* sessions; /* struct call_session, hash по call_id */ |
||||
uint32_t c_calls_started; /* суммарно начато звонков */ |
||||
uint32_t c_calls_ended; /* суммарно завершено звонков */ |
||||
call_media_recv_fn media_recv_fn; /* приём Opus-кадров пира */ |
||||
void* media_recv_arg; |
||||
const struct call_audio_ops* audio_ops; /* аудио-движок (GUI-сборки), NULL в демоне */ |
||||
}; |
||||
|
||||
/* ───────────────────────────────────────────────────────────────────────── */ |
||||
|
||||
static struct call_ctx* call_of(struct UTUN_INSTANCE* inst) { |
||||
return inst ? inst->call : NULL; |
||||
} |
||||
|
||||
static const char* call_state_name(uint8_t st) { |
||||
switch (st) { |
||||
case CALL_ST_IDLE: return "IDLE"; |
||||
case CALL_ST_OUTGOING: return "OUTGOING"; |
||||
case CALL_ST_RINGING: return "RINGING"; |
||||
case CALL_ST_ACTIVE: return "ACTIVE"; |
||||
case CALL_ST_ENDED: return "ENDED"; |
||||
default: return "?"; |
||||
} |
||||
} |
||||
|
||||
static const char* call_subcmd_name(uint8_t sc) { |
||||
switch (sc) { |
||||
case CALL_SUBCMD_INVITE: return "INVITE"; |
||||
case CALL_SUBCMD_RINGING: return "RINGING"; |
||||
case CALL_SUBCMD_ACCEPT: return "ACCEPT"; |
||||
case CALL_SUBCMD_DECLINE: return "DECLINE"; |
||||
case CALL_SUBCMD_BUSY: return "BUSY"; |
||||
case CALL_SUBCMD_CANCEL: return "CANCEL"; |
||||
case CALL_SUBCMD_HANGUP: return "HANGUP"; |
||||
case CALL_SUBCMD_MEDIA: return "MEDIA"; |
||||
default: return "?"; |
||||
} |
||||
} |
||||
|
||||
/* ─────────────────── отправка ─────────────────── */ |
||||
|
||||
static void call_stats_timer_cb(void* arg); |
||||
|
||||
/* data[0] = subcmd. etcp_route_send забирает владение entry. */ |
||||
static int call_send(struct call_ctx* ctx, uint64_t group_id, uint64_t dst, |
||||
const uint8_t* data, size_t len, int mode) { |
||||
struct ll_entry* e = queue_entry_new(0); |
||||
if (!e) return -1; |
||||
e->dgram = u_malloc(1 + len); |
||||
if (!e->dgram) { queue_entry_free(e); return -1; } |
||||
e->dgram[0] = ETCP_RT_ID_CALL; |
||||
if (len) memcpy(e->dgram + 1, data, len); |
||||
e->len = (uint16_t)(1 + len); |
||||
int rc = etcp_route_send(ctx->inst, group_id, dst, e, 1, mode); |
||||
if (rc != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: send FAIL rc=%d subcmd=%s(0x%02x) dst=0x%016llx group=0x%016llx", |
||||
CALL_ID, rc, call_subcmd_name(len ? data[0] : 0), len ? data[0] : 0, |
||||
(unsigned long long)dst, (unsigned long long)group_id); |
||||
} |
||||
return rc; |
||||
} |
||||
|
||||
/* Собрать и отправить INVITE для сессии вызывающего. */ |
||||
static int call_send_invite(struct call_ctx* ctx, struct call_session* s) { |
||||
uint8_t buf[sizeof(struct call_pkt_invite)]; |
||||
struct call_pkt_invite* inv = (struct call_pkt_invite*)buf; |
||||
inv->subcmd = CALL_SUBCMD_INVITE; |
||||
inv->call_id = s->call_id; |
||||
inv->group_id = s->group_id; |
||||
inv->num_tracks = 1; |
||||
inv->tracks[0].type = CALL_TRACK_AUDIO; |
||||
inv->tracks[0].codec = CALL_CODEC_OPUS; |
||||
inv->tracks[0].sample_rate = CALL_OPUS_SAMPLE_RATE; |
||||
inv->tracks[0].channels = 1; |
||||
inv->tracks[0].bitrate = 32000; |
||||
return call_send(ctx, s->group_id, s->peer_node_id, buf, sizeof(buf), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); |
||||
} |
||||
|
||||
/* ─────────────────── реестр сессий ─────────────────── */ |
||||
|
||||
static struct call_session* call_session_find(struct call_ctx* ctx, uint64_t call_id) { |
||||
if (!ctx || !ctx->sessions) return NULL; |
||||
struct ll_entry* e = queue_find_data_by_index(ctx->sessions, &call_id); |
||||
return e ? (struct call_session*)e->data : NULL; |
||||
} |
||||
|
||||
static struct call_session* call_session_new(struct call_ctx* ctx, uint64_t call_id, |
||||
uint64_t group_id, uint64_t peer, uint8_t role) { |
||||
if (!ctx || !ctx->sessions) return NULL; |
||||
struct ll_entry* qe = queue_entry_new(sizeof(struct call_session)); |
||||
if (!qe) return NULL; |
||||
struct call_session* s = (struct call_session*)qe->data; |
||||
memset(s, 0, sizeof(*s)); |
||||
s->call_id = call_id; |
||||
s->group_id = group_id; |
||||
s->peer_node_id = peer; |
||||
s->role = role; |
||||
s->state = CALL_ST_IDLE; |
||||
s->ctx = ctx; |
||||
s->tx_q = queue_new(ctx->inst->ua, 0, 0, 0, "call_txq"); |
||||
if (!s->tx_q) { queue_entry_free(qe); return NULL; } |
||||
// При провале queue_data_put_with_index сам освобождает qe (вместе с s), поэтому
|
||||
// сохраняем tx_q заранее: queue_entry_free(qe) повторно дал бы double-free.
|
||||
struct ll_queue* tx_q = s->tx_q; |
||||
if (queue_data_put_with_index(ctx->sessions, qe) != 0) { |
||||
queue_free(tx_q); |
||||
return NULL; |
||||
} |
||||
return s; |
||||
} |
||||
|
||||
static void call_session_free_cb(void* arg) { |
||||
struct ll_entry* e = (struct ll_entry*)arg; |
||||
queue_entry_free(e); |
||||
} |
||||
|
||||
/* Освободить медиа-объекты сессии: отменить waiter на send_q роутера и
|
||||
* дренировать tx_q (без отправки). Безопасно вызывать из uasync-потока. */ |
||||
static void call_session_cleanup(struct call_ctx* ctx, struct call_session* s) { |
||||
etcp_router_cancel_send_ready(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL, &s->send_waiter); |
||||
if (s->tx_q) { |
||||
struct ll_entry* e; |
||||
while ((e = queue_data_get(s->tx_q)) != NULL) { |
||||
queue_dgram_free(e); |
||||
queue_entry_free(e); |
||||
} |
||||
queue_free(s->tx_q); |
||||
s->tx_q = NULL; |
||||
} |
||||
} |
||||
|
||||
/* Убрать сессию из реестра и освободить отложенно (не из коллбэков conn_mgr/таймеров). */ |
||||
static void call_session_remove_and_free(struct call_ctx* ctx, struct call_session* s) { |
||||
call_session_cleanup(ctx, s); |
||||
struct ll_entry* e = queue_find_data_by_index(ctx->sessions, &s->call_id); |
||||
if (e) { |
||||
queue_remove_data(ctx->sessions, e); |
||||
uasync_call_soon(ctx->inst->ua, e, call_session_free_cb); |
||||
} |
||||
} |
||||
|
||||
/* ─────────────────── события в GUI ─────────────────── */ |
||||
|
||||
static void call_event_id(struct call_ctx* ctx, int type, uint64_t call_id, uint8_t reason) { |
||||
uint8_t buf[16]; |
||||
size_t off = 0; |
||||
memcpy(buf + off, &call_id, 8); off += 8; |
||||
buf[off++] = reason; |
||||
chat_event_post(ctx->inst, type, buf, (int)off); |
||||
} |
||||
|
||||
static void call_event_incoming(struct call_ctx* ctx, uint64_t group_id, uint64_t caller, uint64_t call_id) { |
||||
char ch[32]; |
||||
snprintf(ch, sizeof(ch), "%llu", (unsigned long long)group_id); |
||||
size_t ch_len = strlen(ch); |
||||
uint8_t buf[1 + 32 + 8 + 8]; |
||||
size_t off = 0; |
||||
buf[off++] = (uint8_t)ch_len; |
||||
memcpy(buf + off, ch, ch_len); off += ch_len; |
||||
memcpy(buf + off, &caller, 8); off += 8; |
||||
memcpy(buf + off, &call_id, 8); off += 8; |
||||
chat_event_post(ctx->inst, CHAT_EVT_CALL_INCOMING, buf, (int)off); |
||||
} |
||||
|
||||
/* ─────────────────── BGP-маршрут до пира ─────────────────── */ |
||||
|
||||
/* Формирует строку BGP-пути до peer_node_id и постит CHAT_EVT_CALL_PATH ([call_id:8][text:var]).
|
||||
* hop_list = [peer, m1, ..., mN] (mN — наш прямой сосед), поэтому идём от нас к пиру (в обратном порядке). |
||||
* Формат hop'а: "[%04X] имя" (короткий адрес + имя, как в chat_status). */ |
||||
static void call_post_bgp_path(struct call_ctx* ctx, struct call_session* s) { |
||||
if (!ctx || !s || !ctx->inst || !ctx->inst->topo_groups) return; |
||||
struct TOPO_GROUP* g = topo_groups_find(ctx->inst->topo_groups, s->group_id); |
||||
if (!g) return; |
||||
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, s->peer_node_id); |
||||
if (!nq) return; |
||||
|
||||
uint8_t hc = 0; |
||||
uint64_t* hops = topo_node_best_hop_list(nq, &hc, NULL); |
||||
uint16_t cr = topo_get_chain_rtt(nq); |
||||
|
||||
char text[512]; |
||||
int off = 0; |
||||
if (hc <= 1) { |
||||
off = snprintf(text, sizeof(text), "маршрут: прямой"); |
||||
} else { |
||||
off = snprintf(text, sizeof(text), "маршрут: через "); |
||||
for (int i = hc - 1; i >= 1 && off < (int)sizeof(text) - 48; i--) { |
||||
char hop[48]; |
||||
struct TOPO_NODE* ni = topo_node_registry_find(ctx->inst->topo_groups, hops[i]); |
||||
if (ni && ni->node_name && ni->node_name[0]) |
||||
snprintf(hop, sizeof(hop), "[%04X] %s", (unsigned)(hops[i] & 0xFFFF), ni->node_name); |
||||
else |
||||
snprintf(hop, sizeof(hop), "[%04X]", (unsigned)(hops[i] & 0xFFFF)); |
||||
if (i != hc - 1) off += snprintf(text + off, sizeof(text) - (size_t)off, " → "); |
||||
off += snprintf(text + off, sizeof(text) - (size_t)off, "%s", hop); |
||||
} |
||||
} |
||||
if (cr != 0xFFFF) |
||||
off += snprintf(text + off, sizeof(text) - (size_t)off, " (rtt %ums)", cr / 10); |
||||
|
||||
uint8_t buf[8 + 512]; |
||||
size_t boff = 0; |
||||
memcpy(buf + boff, &s->call_id, 8); boff += 8; |
||||
memcpy(buf + boff, text, (size_t)off); boff += (size_t)off; |
||||
chat_event_post(ctx->inst, CHAT_EVT_CALL_PATH, buf, (int)boff); |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: path id=%016llx hc=%u rtt=%u: %s", |
||||
CALL_ID, (unsigned long long)s->call_id, hc, cr, text); |
||||
} |
||||
|
||||
/* ─────────────────── завершение ─────────────────── */ |
||||
|
||||
/* Отложенный разрыв: единственная точка, где закрывается conn_mgr (после того,
|
||||
* как финальный сигналинг — HANGUP/DECLINE — гарантированно долетел). */ |
||||
static void call_teardown_timer_cb(void* arg) { |
||||
struct call_session* s = (struct call_session*)arg; |
||||
if (!s || !s->ctx) return; |
||||
struct call_ctx* ctx = s->ctx; |
||||
s->teardown_timer = NULL; |
||||
if (s->cm_handle) { conn_mgr_close(s->cm_handle); s->cm_handle = NULL; } |
||||
call_session_remove_and_free(ctx, s); |
||||
} |
||||
|
||||
static void call_end(struct call_ctx* ctx, struct call_session* s, uint8_t reason) { |
||||
if (!ctx || !s || s->state == CALL_ST_ENDED) return; |
||||
|
||||
s->state = CALL_ST_ENDED; |
||||
s->reason = reason; |
||||
ctx->c_calls_ended++; |
||||
|
||||
if (s->ring_timer) { uasync_cancel_timeout(ctx->inst->ua, s->ring_timer); s->ring_timer = NULL; } |
||||
if (s->invite_timer) { uasync_cancel_timeout(ctx->inst->ua, s->invite_timer); s->invite_timer = NULL; } |
||||
if (s->traffic_timer) { uasync_cancel_timeout(ctx->inst->ua, s->traffic_timer); s->traffic_timer = NULL; } |
||||
if (s->stats_timer) { uasync_cancel_timeout(ctx->inst->ua, s->stats_timer); s->stats_timer = NULL; } |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: call ENDED id=%016llx reason=%d role=%s rx=%u tx=%u tx_dropped=%u", |
||||
CALL_ID, (unsigned long long)s->call_id, reason, |
||||
s->role == CALL_ROLE_CALLER ? "caller" : "callee", |
||||
s->c_rx_frames, s->c_tx_frames, s->c_tx_dropped); |
||||
|
||||
call_event_id(ctx, CHAT_EVT_CALL_ENDED, s->call_id, reason); |
||||
|
||||
/* Закрытие запроса CM откладываем на 500мс для отправки финального сигналинга. */ |
||||
if (s->cm_handle) { |
||||
s->teardown_timer = uasync_set_timeout(ctx->inst->ua, CALL_TEARDOWN_DELAY_TB, s, call_teardown_timer_cb, "call_teardown"); |
||||
} else { |
||||
call_session_remove_and_free(ctx, s); |
||||
} |
||||
} |
||||
|
||||
/* ─────────────────── conn_mgr ─────────────────── */ |
||||
|
||||
static void call_traffic_timeout_cb(void* arg); |
||||
|
||||
/* CM готовит оптимальный путь; звонок всегда отправляет через роутер с автоматическим fallback. */ |
||||
static void call_conn_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id, |
||||
enum conn_mgr_event event, void* arg) { |
||||
struct call_session* s = arg; |
||||
if (!s || s->state == CALL_ST_ENDED) return; |
||||
if (event == CONN_EVENT_UP) { |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: CM path ready id=%016llx peer=%016llx", CALL_ID, |
||||
(unsigned long long)s->call_id, (unsigned long long)node_id); |
||||
if (s->state == CALL_ST_ACTIVE) call_post_bgp_path(s->ctx, s); |
||||
} else { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: CM event=%d id=%016llx peer=%016llx; delivery continues via router", |
||||
CALL_ID, event, (unsigned long long)s->call_id, (unsigned long long)node_id); |
||||
} |
||||
(void)h; (void)group_id; |
||||
} |
||||
|
||||
static void call_open_cm(struct call_session* s) { |
||||
if (s->cm_handle) return; |
||||
if (conn_mgr_open(s->ctx->inst, s->group_id, s->peer_node_id, call_conn_cb, s, &s->cm_handle) != 0) |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: CM request failed id=%016llx; using existing routing", CALL_ID, |
||||
(unsigned long long)s->call_id); |
||||
} |
||||
|
||||
static void call_activate(struct call_session* s) { |
||||
if (s->state == CALL_ST_ACTIVE || s->state == CALL_ST_ENDED) return; |
||||
struct call_ctx* ctx = s->ctx; |
||||
s->state = CALL_ST_ACTIVE; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: accepted id=%016llx -> ACTIVE (router delivery)", CALL_ID, |
||||
(unsigned long long)s->call_id); |
||||
call_event_id(ctx, CHAT_EVT_CALL_ACCEPTED, s->call_id, 0); |
||||
call_post_bgp_path(ctx, s); |
||||
s->traffic_timer = uasync_set_timeout(ctx->inst->ua, CALL_NO_TRAFFIC_TB, s, call_traffic_timeout_cb, "call_traffic"); |
||||
s->stats_timer = uasync_set_timeout(ctx->inst->ua, 10000, s, call_stats_timer_cb, "call_stats"); |
||||
} |
||||
|
||||
/* ─────────────────── таймеры ─────────────────── */ |
||||
|
||||
static void call_ring_timeout_cb(void* arg) { |
||||
struct call_session* s = (struct call_session*)arg; |
||||
if (!s || !s->ctx) return; |
||||
struct call_ctx* ctx = s->ctx; |
||||
s->ring_timer = NULL; |
||||
if (s->state == CALL_ST_ENDED) return; |
||||
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: ring timeout id=%016llx -> CANCEL", |
||||
CALL_ID, (unsigned long long)s->call_id); |
||||
uint8_t pkt[sizeof(struct call_pkt_cancel)]; |
||||
struct call_pkt_cancel* c = (struct call_pkt_cancel*)pkt; |
||||
c->subcmd = CALL_SUBCMD_CANCEL; |
||||
c->call_id = s->call_id; |
||||
call_send(ctx, s->group_id, s->peer_node_id, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); |
||||
call_end(ctx, s, CALL_REASON_RING_TIMEOUT); |
||||
} |
||||
|
||||
/* Периодическая кодограмма INVITE (caller) каждые 300мс на всём протяжении вызова
|
||||
* (OUTGOING + RINGING) — «сердцебиение»: покрывает потерю INVITE/RINGING, держит |
||||
* маршрут и кормит watchdog вызываемого (2с без кодограммы → вызов сорван). |
||||
* При стойком отсутствии маршрута — fail-fast CONNECT_FAIL. */ |
||||
static void call_invite_retry_cb(void* arg) { |
||||
struct call_session* s = (struct call_session*)arg; |
||||
if (!s || !s->ctx) return; |
||||
struct call_ctx* ctx = s->ctx; |
||||
s->invite_timer = NULL; |
||||
if (s->state == CALL_ST_ENDED) return; |
||||
if (s->state != CALL_ST_OUTGOING && s->state != CALL_ST_RINGING) return; /* ACTIVE — звонок идёт */ |
||||
|
||||
if (etcp_router_has_route(ctx->inst, s->group_id, s->peer_node_id)) { |
||||
s->no_route_strikes = 0; |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE codegram id=%016llx (route ok)", CALL_ID, (unsigned long long)s->call_id); |
||||
call_send_invite(ctx, s); |
||||
} else if (++s->no_route_strikes >= CALL_NO_ROUTE_MAX_STRIKES) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: no route after %d strikes id=%016llx -> CONNECT_FAIL", |
||||
CALL_ID, s->no_route_strikes, (unsigned long long)s->call_id); |
||||
uint8_t buf[9]; |
||||
memcpy(buf, &s->call_id, 8); |
||||
buf[8] = 2; /* err: peer unreachable (no route) */ |
||||
chat_event_post(ctx->inst, CHAT_EVT_CALL_ERROR, buf, sizeof(buf)); |
||||
call_end(ctx, s, CALL_REASON_CONNECT_FAIL); |
||||
return; |
||||
} else { |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE codegram id=%016llx no route (strike %d/%d)", |
||||
CALL_ID, (unsigned long long)s->call_id, s->no_route_strikes, CALL_NO_ROUTE_MAX_STRIKES); |
||||
} |
||||
|
||||
s->invite_timer = uasync_set_timeout(ctx->inst->ua, CALL_INVITE_RETRY_TB, s, call_invite_retry_cb, "call_invite_retry"); |
||||
} |
||||
|
||||
/* Вызываемый: 2с без кодограммы INVITE → вызывающий пропал (отмена потерялась) — закрываем. */ |
||||
static void call_ring_dead_cb(void* arg) { |
||||
struct call_session* s = (struct call_session*)arg; |
||||
if (!s || !s->ctx) return; |
||||
struct call_ctx* ctx = s->ctx; |
||||
s->ring_timer = NULL; |
||||
if (s->state != CALL_ST_RINGING) return; |
||||
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: ring dead (2с без кодограммы) id=%016llx -> end", |
||||
CALL_ID, (unsigned long long)s->call_id); |
||||
call_end(ctx, s, CALL_REASON_CANCEL); |
||||
} |
||||
|
||||
static void call_traffic_timeout_cb(void* arg) { |
||||
struct call_session* s = (struct call_session*)arg; |
||||
if (!s || !s->ctx) return; |
||||
struct call_ctx* ctx = s->ctx; |
||||
s->traffic_timer = NULL; |
||||
if (s->state == CALL_ST_ENDED) return; |
||||
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: no traffic 20s id=%016llx -> HANGUP", |
||||
CALL_ID, (unsigned long long)s->call_id); |
||||
uint8_t pkt[sizeof(struct call_pkt_hangup)]; |
||||
struct call_pkt_hangup* hu = (struct call_pkt_hangup*)pkt; |
||||
hu->subcmd = CALL_SUBCMD_HANGUP; |
||||
hu->call_id = s->call_id; |
||||
hu->reason = CALL_REASON_NO_TRAFFIC; |
||||
call_send(ctx, s->group_id, s->peer_node_id, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); |
||||
call_end(ctx, s, CALL_REASON_NO_TRAFFIC); |
||||
} |
||||
|
||||
/* Периодическое событие CALL_STATS (раз в 1с): rtt_ms, buffer_ms, tempo_x100. */ |
||||
static void call_stats_timer_cb(void* arg) { |
||||
struct call_session* s = (struct call_session*)arg; |
||||
if (!s || !s->ctx) return; |
||||
struct call_ctx* ctx = s->ctx; |
||||
s->stats_timer = NULL; |
||||
if (s->state != CALL_ST_ACTIVE) return; |
||||
|
||||
int buffer_ms = 0, tempo_x100 = 100; |
||||
struct call_jitter_range range = {0, 0, 60}; |
||||
uint32_t dropped = 0, underruns = 0; |
||||
if (ctx->audio_ops && ctx->audio_ops->get_stats) |
||||
ctx->audio_ops->get_stats(s->call_id, &buffer_ms, &tempo_x100, &dropped, &underruns, &range); |
||||
|
||||
uint16_t rtt = 0; |
||||
struct ETCP_ROUTER_CONN* rconn = |
||||
etcp_router_conn_get(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL); |
||||
if (rconn && rconn->recv_conn) rtt = rconn->recv_conn->rtt_last; |
||||
|
||||
uint8_t buf[24]; |
||||
size_t off = 0; |
||||
memcpy(buf + off, &s->call_id, 8); off += 8; |
||||
buf[off++] = (uint8_t)(rtt >> 8); buf[off++] = (uint8_t)(rtt & 0xFF); |
||||
uint16_t bms = (uint16_t)(buffer_ms < 0 ? 0 : (buffer_ms > 0xFFFF ? 0xFFFF : buffer_ms)); |
||||
buf[off++] = (uint8_t)(bms >> 8); buf[off++] = (uint8_t)(bms & 0xFF); |
||||
uint16_t t = (uint16_t)(tempo_x100 < 0 ? 0 : (tempo_x100 > 0xFFFF ? 0xFFFF : tempo_x100)); |
||||
buf[off++] = (uint8_t)(t >> 8); buf[off++] = (uint8_t)(t & 0xFF); |
||||
uint16_t dr = (uint16_t)(dropped > 0xFFFF ? 0xFFFF : dropped); |
||||
buf[off++] = (uint8_t)(dr >> 8); buf[off++] = (uint8_t)(dr & 0xFF); |
||||
uint16_t un = (uint16_t)(underruns > 0xFFFF ? 0xFFFF : underruns); |
||||
buf[off++] = (uint8_t)(un >> 8); buf[off++] = (uint8_t)(un & 0xFF); |
||||
int values[] = {range.min_ms, range.max_ms, range.reserve_ms}; |
||||
for (size_t i = 0; i < 3; i++) { |
||||
uint16_t value = (uint16_t)values[i]; |
||||
buf[off++] = (uint8_t)(value >> 8); buf[off++] = (uint8_t)value; |
||||
} |
||||
chat_event_post(ctx->inst, CHAT_EVT_CALL_STATS, buf, (int)off); |
||||
|
||||
s->stats_timer = uasync_set_timeout(ctx->inst->ua, 10000, s, call_stats_timer_cb, "call_stats"); |
||||
} |
||||
|
||||
/* ─────────────────── обработчики приёма ─────────────────── */ |
||||
|
||||
static void call_handle_invite(struct call_ctx* ctx, uint64_t from, uint64_t group, |
||||
const uint8_t* b, size_t n) { |
||||
if (n < sizeof(struct call_pkt_invite) - sizeof(struct call_pkt_track)) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: INVITE too short n=%zu", CALL_ID, n); |
||||
return; |
||||
} |
||||
uint64_t call_id, inv_group; |
||||
memcpy(&call_id, b + 1, 8); |
||||
memcpy(&inv_group, b + 9, 8); |
||||
|
||||
/* кодограмма-дубликат (тот же call_id): пока звоним — пере-взводим watchdog и
|
||||
* идемпотентно отвечаем RINGING; в разговоре (ACTIVE) — игнорируем. */ |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (s) { |
||||
if (s->role == CALL_ROLE_CALLEE && s->state == CALL_ST_RINGING) { |
||||
if (s->ring_timer) { uasync_cancel_timeout(ctx->inst->ua, s->ring_timer); s->ring_timer = NULL; } |
||||
s->ring_timer = uasync_set_timeout(ctx->inst->ua, CALL_RING_DEAD_TB, s, call_ring_dead_cb, "call_ring_dead"); |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE codegram id=%016llx -> re-RINGING + re-arm dead", CALL_ID, (unsigned long long)call_id); |
||||
uint8_t pkt[sizeof(struct call_pkt_ringing)]; |
||||
struct call_pkt_ringing* r = (struct call_pkt_ringing*)pkt; |
||||
r->subcmd = CALL_SUBCMD_RINGING; |
||||
r->call_id = call_id; |
||||
call_send(ctx, s->group_id, from, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); |
||||
} else { |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: INVITE codegram id=%016llx ignored (state=%s)", |
||||
CALL_ID, (unsigned long long)call_id, call_state_name(s->state)); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
/* занятость: есть другая активная сессия */ |
||||
struct ll_entry* e = ctx->sessions ? ctx->sessions->head : NULL; |
||||
while (e) { |
||||
struct call_session* other = (struct call_session*)e->data; |
||||
if (other->state != CALL_ST_ENDED) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: busy — active call id=%016llx, reply BUSY to id=%016llx", |
||||
CALL_ID, (unsigned long long)other->call_id, (unsigned long long)call_id); |
||||
uint8_t pkt[sizeof(struct call_pkt_busy)]; |
||||
struct call_pkt_busy* bz = (struct call_pkt_busy*)pkt; |
||||
bz->subcmd = CALL_SUBCMD_BUSY; |
||||
bz->call_id = call_id; |
||||
call_send(ctx, group, from, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); |
||||
return; |
||||
} |
||||
e = e->next; |
||||
} |
||||
|
||||
s = call_session_new(ctx, call_id, group, from, CALL_ROLE_CALLEE); |
||||
if (!s) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: session alloc failed id=%016llx", CALL_ID, (unsigned long long)call_id); |
||||
return; |
||||
} |
||||
s->state = CALL_ST_RINGING; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: INVITE id=%016llx from=0x%016llx group=0x%016llx (declared group=0x%016llx)", |
||||
CALL_ID, (unsigned long long)call_id, (unsigned long long)from, |
||||
(unsigned long long)group, (unsigned long long)inv_group); |
||||
|
||||
call_event_incoming(ctx, group, from, call_id); |
||||
call_post_bgp_path(ctx, s); |
||||
|
||||
/* watchdog: вызывающий шлёт кодограммы INVITE каждые 300мс; 2с тишины → вызов сорван */ |
||||
s->ring_timer = uasync_set_timeout(ctx->inst->ua, CALL_RING_DEAD_TB, s, call_ring_dead_cb, "call_ring_dead"); |
||||
|
||||
uint8_t pkt[sizeof(struct call_pkt_ringing)]; |
||||
struct call_pkt_ringing* r = (struct call_pkt_ringing*)pkt; |
||||
r->subcmd = CALL_SUBCMD_RINGING; |
||||
r->call_id = call_id; |
||||
call_send(ctx, group, from, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); |
||||
} |
||||
|
||||
static void call_handle_ringing(struct call_ctx* ctx, uint64_t from, const uint8_t* b, size_t n) { |
||||
if (n < 9) return; |
||||
uint64_t call_id; memcpy(&call_id, b + 1, 8); |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (!s || s->role != CALL_ROLE_CALLER || s->state == CALL_ST_ENDED) return; |
||||
if (s->state == CALL_ST_OUTGOING) { |
||||
s->state = CALL_ST_RINGING; |
||||
/* invite_timer не трогаем — кодограмма INVITE продолжается и в RINGING */ |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: RINGING id=%016llx", CALL_ID, (unsigned long long)call_id); |
||||
call_event_id(ctx, CHAT_EVT_CALL_RINGING, call_id, 0); |
||||
} |
||||
(void)from; |
||||
} |
||||
|
||||
static void call_handle_accept(struct call_ctx* ctx, uint64_t from, const uint8_t* b, size_t n) { |
||||
if (n < 9) return; |
||||
uint64_t call_id; memcpy(&call_id, b + 1, 8); |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (!s || s->role != CALL_ROLE_CALLER || s->state == CALL_ST_ENDED || s->state == CALL_ST_ACTIVE) return; |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: ACCEPT id=%016llx", CALL_ID, (unsigned long long)call_id); |
||||
if (s->ring_timer) { uasync_cancel_timeout(ctx->inst->ua, s->ring_timer); s->ring_timer = NULL; } |
||||
if (s->invite_timer) { uasync_cancel_timeout(ctx->inst->ua, s->invite_timer); s->invite_timer = NULL; } |
||||
call_open_cm(s); |
||||
call_activate(s); |
||||
(void)from; |
||||
} |
||||
|
||||
static void call_handle_decline(struct call_ctx* ctx, uint64_t from, const uint8_t* b, size_t n) { |
||||
if (n < 10) return; |
||||
uint64_t call_id; memcpy(&call_id, b + 1, 8); |
||||
uint8_t reason = b[9]; |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (!s || s->state == CALL_ST_ENDED) return; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: DECLINE id=%016llx reason=%d", CALL_ID, (unsigned long long)call_id, reason); |
||||
call_event_id(ctx, CHAT_EVT_CALL_DECLINED, call_id, reason); |
||||
call_end(ctx, s, CALL_REASON_DECLINE); |
||||
(void)from; |
||||
} |
||||
|
||||
static void call_handle_busy(struct call_ctx* ctx, uint64_t from, const uint8_t* b, size_t n) { |
||||
if (n < 9) return; |
||||
uint64_t call_id; memcpy(&call_id, b + 1, 8); |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (!s || s->state == CALL_ST_ENDED) return; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: BUSY id=%016llx", CALL_ID, (unsigned long long)call_id); |
||||
call_end(ctx, s, CALL_REASON_BUSY); |
||||
(void)from; |
||||
} |
||||
|
||||
static void call_handle_cancel(struct call_ctx* ctx, uint64_t from, const uint8_t* b, size_t n) { |
||||
if (n < 9) return; |
||||
uint64_t call_id; memcpy(&call_id, b + 1, 8); |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (!s || s->state == CALL_ST_ENDED) return; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: CANCEL id=%016llx", CALL_ID, (unsigned long long)call_id); |
||||
call_end(ctx, s, CALL_REASON_CANCEL); |
||||
(void)from; |
||||
} |
||||
|
||||
static void call_handle_hangup(struct call_ctx* ctx, uint64_t from, const uint8_t* b, size_t n) { |
||||
if (n < 10) return; |
||||
uint64_t call_id; memcpy(&call_id, b + 1, 8); |
||||
uint8_t reason = b[9]; |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (!s || s->state == CALL_ST_ENDED) return; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: HANGUP id=%016llx reason=%d", CALL_ID, (unsigned long long)call_id, reason); |
||||
call_end(ctx, s, CALL_REASON_REMOTE_HANGUP); |
||||
(void)from; |
||||
} |
||||
|
||||
static void call_handle_media(struct call_ctx* ctx, uint64_t from, const uint8_t* b, size_t n) { |
||||
if (n < CALL_MEDIA_HDR_SIZE) return; |
||||
uint64_t call_id; memcpy(&call_id, b + 1, 8); |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (!s || s->state != CALL_ST_ACTIVE) { |
||||
/* медиа вне ACTIVE — игнорируем (гонка с завершением) */ |
||||
return; |
||||
} |
||||
s->c_rx_frames++; |
||||
s->last_media_tb = get_time_tb(); |
||||
|
||||
uint16_t seq = 0; |
||||
uint32_t ts_ms = 0; |
||||
memcpy(&seq, b + 10, 2); |
||||
memcpy(&ts_ms, b + 12, 4); |
||||
|
||||
/* аудио-движок (декод + джиттер-буфер) — если зарегистрирован */ |
||||
if (ctx->audio_ops && ctx->audio_ops->on_media) |
||||
ctx->audio_ops->on_media(ctx->inst, call_id, seq, ts_ms, |
||||
b + CALL_MEDIA_HDR_SIZE, (int)(n - CALL_MEDIA_HDR_SIZE)); |
||||
|
||||
/* релей Opus-кадра потребителю (headless-аудио-сокет) */ |
||||
if (ctx->media_recv_fn) |
||||
ctx->media_recv_fn(ctx->inst, call_id, b + CALL_MEDIA_HDR_SIZE, |
||||
(int)(n - CALL_MEDIA_HDR_SIZE), ctx->media_recv_arg); |
||||
|
||||
/* watchdog: пере-взводим таймер no-traffic на каждый принятый кадр */ |
||||
if (s->traffic_timer) { uasync_cancel_timeout(ctx->inst->ua, s->traffic_timer); s->traffic_timer = NULL; } |
||||
s->traffic_timer = uasync_set_timeout(ctx->inst->ua, CALL_NO_TRAFFIC_TB, s, call_traffic_timeout_cb, "call_traffic"); |
||||
(void)from; |
||||
} |
||||
|
||||
/* ─────────────────── приём (etcp_router) ─────────────────── */ |
||||
|
||||
static void call_etcp_recv(struct ETCP_CONN* conn, struct ll_entry* entry) { |
||||
if (!entry || !conn || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 1) { |
||||
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } |
||||
return; |
||||
} |
||||
struct call_ctx* ctx = call_of(conn->instance); |
||||
if (!ctx || !ctx->initialized) { queue_dgram_free(entry); queue_entry_free(entry); return; } |
||||
|
||||
const uint8_t* d = entry->dgram; |
||||
uint64_t from; memcpy(&from, d + ROUTER_SVC_SRC_OFF, 8); |
||||
uint64_t group; memcpy(&group, d + ROUTER_SVC_GROUP_OFF, 8); |
||||
uint8_t subcmd = d[ROUTER_SVC_PAYLOAD_OFF]; |
||||
const uint8_t* body = d + ROUTER_SVC_PAYLOAD_OFF; |
||||
size_t blen = entry->len - ROUTER_SVC_PAYLOAD_OFF; |
||||
|
||||
switch (subcmd) { |
||||
case CALL_SUBCMD_INVITE: call_handle_invite(ctx, from, group, body, blen); break; |
||||
case CALL_SUBCMD_RINGING: call_handle_ringing(ctx, from, body, blen); break; |
||||
case CALL_SUBCMD_ACCEPT: call_handle_accept(ctx, from, body, blen); break; |
||||
case CALL_SUBCMD_DECLINE: call_handle_decline(ctx, from, body, blen); break; |
||||
case CALL_SUBCMD_BUSY: call_handle_busy(ctx, from, body, blen); break; |
||||
case CALL_SUBCMD_CANCEL: call_handle_cancel(ctx, from, body, blen); break; |
||||
case CALL_SUBCMD_HANGUP: call_handle_hangup(ctx, from, body, blen); break; |
||||
case CALL_SUBCMD_MEDIA: call_handle_media(ctx, from, body, blen); break; |
||||
default: |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: unknown subcmd=0x%02x from=0x%016llx len=%zu", |
||||
CALL_ID, subcmd, (unsigned long long)from, blen); |
||||
break; |
||||
} |
||||
queue_dgram_free(entry); |
||||
queue_entry_free(entry); |
||||
} |
||||
|
||||
/* ─────────────────── публичный API ─────────────────── */ |
||||
|
||||
int call_init(struct UTUN_INSTANCE* inst) { |
||||
if (!inst) return -1; |
||||
if (inst->call && inst->call->initialized) return 0; |
||||
|
||||
struct call_ctx* ctx = u_calloc(1, sizeof(*ctx)); |
||||
if (!ctx) return -1; |
||||
ctx->inst = inst; |
||||
inst->call = ctx; |
||||
|
||||
ctx->sessions = queue_new(inst->ua, 256, offsetof(struct call_session, call_id), 8, "call_sessions"); |
||||
if (!ctx->sessions) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: queue_new(sessions) failed", CALL_ID); |
||||
u_free(ctx); inst->call = NULL; |
||||
return -1; |
||||
} |
||||
if (etcp_router_bind(inst, ETCP_RT_ID_CALL, call_etcp_recv) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: etcp_router_bind(0x%02x) failed", CALL_ID, ETCP_RT_ID_CALL); |
||||
queue_free(ctx->sessions); |
||||
u_free(ctx); inst->call = NULL; |
||||
return -1; |
||||
} |
||||
|
||||
ctx->initialized = 1; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized (svc=0x%02x)", CALL_ID, ETCP_RT_ID_CALL); |
||||
return 0; |
||||
} |
||||
|
||||
void call_destroy(struct UTUN_INSTANCE* inst) { |
||||
struct call_ctx* ctx = call_of(inst); |
||||
if (!ctx || !ctx->initialized) return; |
||||
|
||||
/* завершить все активные сессии (без отправки — мы выключаемся) */ |
||||
while (ctx->sessions && ctx->sessions->head) { |
||||
struct ll_entry* e = ctx->sessions->head; |
||||
struct call_session* s = (struct call_session*)e->data; |
||||
s->state = CALL_ST_ENDED; |
||||
if (s->ring_timer) { uasync_cancel_timeout(inst->ua, s->ring_timer); s->ring_timer = NULL; } |
||||
if (s->invite_timer) { uasync_cancel_timeout(inst->ua, s->invite_timer); s->invite_timer = NULL; } |
||||
if (s->traffic_timer) { uasync_cancel_timeout(inst->ua, s->traffic_timer); s->traffic_timer = NULL; } |
||||
if (s->stats_timer) { uasync_cancel_timeout(inst->ua, s->stats_timer); s->stats_timer = NULL; } |
||||
if (s->teardown_timer) { uasync_cancel_timeout(inst->ua, s->teardown_timer); s->teardown_timer = NULL; } |
||||
if (s->cm_handle) { conn_mgr_close(s->cm_handle); s->cm_handle = NULL; } |
||||
call_session_cleanup(ctx, s); |
||||
queue_remove_data(ctx->sessions, e); |
||||
queue_entry_free(e); |
||||
} |
||||
queue_free(ctx->sessions); |
||||
etcp_router_unbind(inst, ETCP_RT_ID_CALL); |
||||
inst->call = NULL; |
||||
uint32_t started = ctx->c_calls_started, ended = ctx->c_calls_ended; |
||||
u_free(ctx); |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed (started=%u ended=%u)", CALL_ID, started, ended); |
||||
} |
||||
|
||||
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id) { |
||||
if (!inst || !ch_id || !ch_id[0] || !inst->topo_groups) return 0; |
||||
if (peer_node_id == inst->node_id) return 1; |
||||
uint64_t group_id = strtoull(ch_id, NULL, 10); |
||||
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, group_id); |
||||
if (!g) return 0; |
||||
return topo_node_find_by_id(g, peer_node_id) != NULL; |
||||
} |
||||
|
||||
void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, void* arg) { |
||||
struct call_ctx* ctx = call_of(inst); |
||||
if (!ctx) return; |
||||
ctx->media_recv_fn = fn; |
||||
ctx->media_recv_arg = arg; |
||||
} |
||||
|
||||
void call_set_audio_ops(struct UTUN_INSTANCE* inst, const struct call_audio_ops* ops) { |
||||
struct call_ctx* ctx = call_of(inst); |
||||
if (!ctx) return; |
||||
ctx->audio_ops = ops; |
||||
} |
||||
|
||||
/* ─────────────────── TX медиа: tx_q + backpressure ─────────────────── */ |
||||
|
||||
static void call_tx_flush(struct call_session* s); |
||||
|
||||
static void call_tx_drain_cb(struct ll_queue* q, void* arg) { |
||||
(void)q; |
||||
struct call_session* s = (struct call_session*)arg; |
||||
if (!s || !s->ctx) return; |
||||
if (s->state == CALL_ST_ENDED) return; |
||||
call_tx_flush(s); |
||||
} |
||||
|
||||
/* Дренируем tx_q, пока роутер готов; иначе — одноразовый waiter (re-arm в drain_cb). */ |
||||
static void call_tx_flush(struct call_session* s) { |
||||
struct call_ctx* ctx = s->ctx; |
||||
if (s->state == CALL_ST_ENDED) return; |
||||
/* гарантируем router conn — иначе has_room/on_send_ready не видят его */ |
||||
etcp_router_conn_get(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL); |
||||
while (s->tx_q && s->tx_q->head) { |
||||
if (!etcp_router_send_q_has_room(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL)) { |
||||
etcp_router_on_send_ready(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL, |
||||
&s->send_waiter, call_tx_drain_cb, s); |
||||
return; |
||||
} |
||||
struct ll_entry* e = queue_data_get(s->tx_q); |
||||
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, e, 1, 0) == 0) |
||||
s->c_tx_frames++; |
||||
else DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: media enqueue failed id=%016llx", CALL_ID, (unsigned long long)s->call_id); |
||||
} |
||||
} |
||||
|
||||
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len) { |
||||
struct call_ctx* ctx = call_of(inst); |
||||
if (!ctx || !ctx->initialized || !opus || len <= 0) return -1; |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (!s || s->state != CALL_ST_ACTIVE) return -1; |
||||
if (CALL_MEDIA_HDR_SIZE + (size_t)len > 1024) return -1; |
||||
|
||||
uint8_t buf[1024]; |
||||
struct call_pkt_media* m = (struct call_pkt_media*)buf; |
||||
m->subcmd = CALL_SUBCMD_MEDIA; |
||||
m->call_id = call_id; |
||||
m->track_id = CALL_TRACK_AUDIO; |
||||
m->seq = s->tx_media_seq++; |
||||
m->ts_ms = (uint32_t)(get_time_tb() / 10); |
||||
memcpy(buf + CALL_MEDIA_HDR_SIZE, opus, (size_t)len); |
||||
size_t mlen = CALL_MEDIA_HDR_SIZE + (size_t)len; |
||||
|
||||
/* entry: [svc_id][media-пакет] — кладём в tx_q (переполнение → дроп старейшего) */ |
||||
struct ll_entry* e = queue_entry_new(0); |
||||
if (!e) return -1; |
||||
e->dgram = u_malloc(1 + mlen); |
||||
if (!e->dgram) { queue_entry_free(e); return -1; } |
||||
e->dgram[0] = ETCP_RT_ID_CALL; |
||||
memcpy(e->dgram + 1, buf, mlen); |
||||
e->len = (uint16_t)(1 + mlen); |
||||
|
||||
if (queue_entry_count(s->tx_q) >= CALL_MAX_TX_FRAMES) { |
||||
struct ll_entry* old = queue_data_get(s->tx_q); |
||||
if (old) { |
||||
queue_dgram_free(old); queue_entry_free(old); s->c_tx_dropped++; |
||||
if (s->c_tx_dropped == 1 || s->c_tx_dropped % 50 == 0) |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: TX overflow id=%016llx queued=%zu dropped=%u", CALL_ID, |
||||
(unsigned long long)call_id, (size_t)queue_entry_count(s->tx_q), s->c_tx_dropped); |
||||
} |
||||
} |
||||
queue_data_put(s->tx_q, e); |
||||
|
||||
call_tx_flush(s); |
||||
return 0; |
||||
} |
||||
|
||||
void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id) { |
||||
struct call_ctx* ctx = call_of(inst); |
||||
if (!ctx || !ctx->initialized || !ch_id || !ch_id[0]) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: call_start bad args (ctx=%p ch_id=%s)", CALL_ID, (void*)ctx, ch_id ? ch_id : "(null)"); |
||||
return; |
||||
} |
||||
if (peer_node_id == inst->node_id) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: call_start self-call rejected", CALL_ID); |
||||
return; |
||||
} |
||||
|
||||
uint64_t group_id = strtoull(ch_id, NULL, 10); |
||||
if (!call_is_online(inst, ch_id, peer_node_id)) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: peer 0x%016llx offline in group 0x%016llx — call not started", |
||||
CALL_ID, (unsigned long long)peer_node_id, (unsigned long long)group_id); |
||||
uint8_t buf[9]; |
||||
memcpy(buf, &out_call_id, 8); |
||||
buf[8] = 1; /* err: peer offline */ |
||||
chat_event_post(inst, CHAT_EVT_CALL_ERROR, buf, sizeof(buf)); |
||||
return; |
||||
} |
||||
|
||||
struct call_session* s = call_session_new(ctx, out_call_id, group_id, peer_node_id, CALL_ROLE_CALLER); |
||||
if (!s) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: session alloc failed id=%016llx", CALL_ID, (unsigned long long)out_call_id); |
||||
return; |
||||
} |
||||
s->state = CALL_ST_OUTGOING; |
||||
ctx->c_calls_started++; |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: call_start id=%016llx peer=0x%016llx group=0x%016llx", |
||||
CALL_ID, (unsigned long long)out_call_id, (unsigned long long)peer_node_id, (unsigned long long)group_id); |
||||
|
||||
call_open_cm(s); |
||||
if (call_send_invite(ctx, s) != 0) { |
||||
call_end(ctx, s, CALL_REASON_CONNECT_FAIL); |
||||
return; |
||||
} |
||||
call_post_bgp_path(ctx, s); |
||||
s->ring_timer = uasync_set_timeout(inst->ua, CALL_RING_TIMEOUT_TB, s, call_ring_timeout_cb, "call_ring"); |
||||
s->invite_timer = uasync_set_timeout(inst->ua, CALL_INVITE_RETRY_TB, s, call_invite_retry_cb, "call_invite_retry"); |
||||
} |
||||
|
||||
void call_accept(struct UTUN_INSTANCE* inst, uint64_t call_id) { |
||||
struct call_ctx* ctx = call_of(inst); |
||||
if (!ctx || !ctx->initialized) return; |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (!s || s->role != CALL_ROLE_CALLEE || s->state == CALL_ST_ENDED || s->state == CALL_ST_ACTIVE) return; |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: call_accept id=%016llx -> ACCEPT + conn_mgr_open", |
||||
CALL_ID, (unsigned long long)call_id); |
||||
if (s->ring_timer) { uasync_cancel_timeout(ctx->inst->ua, s->ring_timer); s->ring_timer = NULL; } /* watchdog кодограмм */ |
||||
|
||||
uint8_t pkt[sizeof(struct call_pkt_accept)]; |
||||
struct call_pkt_accept* a = (struct call_pkt_accept*)pkt; |
||||
a->subcmd = CALL_SUBCMD_ACCEPT; |
||||
a->call_id = call_id; |
||||
call_send(ctx, s->group_id, s->peer_node_id, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); |
||||
|
||||
call_open_cm(s); |
||||
call_activate(s); |
||||
} |
||||
|
||||
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id) { |
||||
struct call_ctx* ctx = call_of(inst); |
||||
if (!ctx || !ctx->initialized) return; |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (!s || s->role != CALL_ROLE_CALLEE || s->state == CALL_ST_ENDED) return; |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: call_decline id=%016llx", CALL_ID, (unsigned long long)call_id); |
||||
uint8_t pkt[sizeof(struct call_pkt_decline)]; |
||||
struct call_pkt_decline* d = (struct call_pkt_decline*)pkt; |
||||
d->subcmd = CALL_SUBCMD_DECLINE; |
||||
d->call_id = call_id; |
||||
d->reason = CALL_REASON_DECLINE; |
||||
call_send(ctx, s->group_id, s->peer_node_id, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); |
||||
|
||||
call_event_id(ctx, CHAT_EVT_CALL_DECLINED, call_id, CALL_REASON_DECLINE); |
||||
call_end(ctx, s, CALL_REASON_DECLINE); |
||||
} |
||||
|
||||
void call_hangup(struct UTUN_INSTANCE* inst, uint64_t call_id) { |
||||
struct call_ctx* ctx = call_of(inst); |
||||
if (!ctx || !ctx->initialized) return; |
||||
struct call_session* s = call_session_find(ctx, call_id); |
||||
if (!s || s->state == CALL_ST_ENDED) return; |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: call_hangup id=%016llx", CALL_ID, (unsigned long long)call_id); |
||||
uint8_t pkt[sizeof(struct call_pkt_hangup)]; |
||||
struct call_pkt_hangup* hu = (struct call_pkt_hangup*)pkt; |
||||
hu->subcmd = CALL_SUBCMD_HANGUP; |
||||
hu->call_id = call_id; |
||||
hu->reason = CALL_REASON_LOCAL_HANGUP; |
||||
call_send(ctx, s->group_id, s->peer_node_id, pkt, sizeof(pkt), ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT); |
||||
|
||||
call_end(ctx, s, CALL_REASON_LOCAL_HANGUP); |
||||
} |
||||
|
||||
/* ─────────────────── trampoline-функции (GUI → uasync) ─────────────────── */ |
||||
|
||||
void call_start_trampoline(void* arg) { |
||||
struct call_start_arg* a = (struct call_start_arg*)arg; |
||||
call_start(a->inst, a->ch_id, a->peer_node_id, a->call_id); |
||||
u_free(a); |
||||
} |
||||
|
||||
void call_accept_trampoline(void* arg) { |
||||
struct call_id_arg* a = (struct call_id_arg*)arg; |
||||
call_accept(a->inst, a->call_id); |
||||
u_free(a); |
||||
} |
||||
|
||||
void call_decline_trampoline(void* arg) { |
||||
struct call_id_arg* a = (struct call_id_arg*)arg; |
||||
call_decline(a->inst, a->call_id); |
||||
u_free(a); |
||||
} |
||||
|
||||
void call_hangup_trampoline(void* arg) { |
||||
struct call_id_arg* a = (struct call_id_arg*)arg; |
||||
call_hangup(a->inst, a->call_id); |
||||
u_free(a); |
||||
} |
||||
|
||||
void call_send_media_trampoline(void* arg) { |
||||
struct call_media_arg* a = (struct call_media_arg*)arg; |
||||
call_send_media(a->inst, a->call_id, a->opus, a->len); |
||||
u_free(a); |
||||
} |
||||
@ -0,0 +1,99 @@
|
||||
// call.h — P2P аудио-звонок (сигналинг + медиа), per-instance
|
||||
//
|
||||
// Модуль оперирует (group_id, peer_node_id): звонок можно инициировать из
|
||||
// CHAT-группы или из DM (group_id — группа маршрутизации, как в dm_core).
|
||||
// Сигналинг идёт через etcp_router (ROUTE_CRYPTO_SIGN|ENCRYPT), соединение
|
||||
// устанавливается через conn_mgr (3-фазное DIRECT→REVERSE→INDIRECT).
|
||||
//
|
||||
// Все функции — в uasync-потоке. GUI/headless вызывают через trampoline
|
||||
// (gui_bridge_post_uasync_fn / uasync_post).
|
||||
#ifndef CALL_H |
||||
#define CALL_H |
||||
|
||||
#include <stdint.h> |
||||
#include "call_jitter.h" |
||||
|
||||
struct UTUN_INSTANCE; |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
/* ── жизненный цикл ── */ |
||||
|
||||
int call_init(struct UTUN_INSTANCE* inst); /* из utun_instance.c после dm_core_init */ |
||||
void call_destroy(struct UTUN_INSTANCE* inst); /* перед dm_core_destroy */ |
||||
|
||||
/* ── сигналинг (uasync-поток) ── */ |
||||
|
||||
/* Caller: начать звонок. out_call_id — случайный uint64, генерит вызывающий. */ |
||||
void call_start (struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id); |
||||
|
||||
/* Callee: ответить/отклонить. hangup — любая сторона в любой момент. */ |
||||
void call_accept (struct UTUN_INSTANCE* inst, uint64_t call_id); |
||||
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id); |
||||
void call_hangup (struct UTUN_INSTANCE* inst, uint64_t call_id); |
||||
|
||||
/* Read-only онлайн-проверка (GUI может звать из любого потока). */ |
||||
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id); |
||||
|
||||
/* ── медиа (релей Opus-кадров, uasync-поток) ── */ |
||||
|
||||
/* Отправить закодированный Opus-кадр пиру (оборачивает в CALL_MEDIA).
|
||||
* Только когда звонок ACTIVE. seq/ts выставляются автоматически. */ |
||||
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len); |
||||
|
||||
/* Коллбэк приёма медиа: вызывается в uasync-потоке на каждый Opus-кадр пира.
|
||||
* Используется headless-аудио-сокетом (и в фазе 3 — декодером GUI). */ |
||||
typedef void (*call_media_recv_fn)(struct UTUN_INSTANCE* inst, uint64_t call_id, |
||||
const uint8_t* opus, int len, void* arg); |
||||
void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, void* arg); |
||||
|
||||
/* ── аудио-движок (опционально, GUI-сборки) ──
|
||||
* Сервисный аудио-контур (codec + джиттер-буфер + тоны) регистрируется через |
||||
* call_set_audio_ops. call.c зовёт on_media на каждый принятый медиа-кадр (с seq/ts) |
||||
* и get_stats для события CALL_STATS. Демон без аудио (headless) — ops не ставит. */ |
||||
|
||||
struct call_audio_ops { |
||||
/* uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер). */ |
||||
void (*on_media)(struct UTUN_INSTANCE* inst, uint64_t call_id, |
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len); |
||||
/* uasync-поток: текущие метрики джиттер-буфера (buffer_ms, tempo_x100,
|
||||
* dropped/underruns — опционально, NULL допустим). */ |
||||
int (*get_stats)(uint64_t call_id, int* buffer_ms, int* tempo_x100, |
||||
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range); |
||||
}; |
||||
void call_set_audio_ops(struct UTUN_INSTANCE* inst, const struct call_audio_ops* ops); |
||||
|
||||
/* Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline. */ |
||||
struct call_media_arg { |
||||
struct UTUN_INSTANCE* inst; |
||||
uint64_t call_id; |
||||
uint16_t len; |
||||
uint8_t opus[256]; |
||||
}; |
||||
void call_send_media_trampoline(void* arg); |
||||
|
||||
/* ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ── */ |
||||
|
||||
struct call_start_arg { |
||||
struct UTUN_INSTANCE* inst; |
||||
char ch_id[64]; |
||||
uint64_t peer_node_id; |
||||
uint64_t call_id; |
||||
}; |
||||
|
||||
struct call_id_arg { |
||||
struct UTUN_INSTANCE* inst; |
||||
uint64_t call_id; |
||||
}; |
||||
|
||||
void call_start_trampoline (void* arg); |
||||
void call_accept_trampoline (void* arg); |
||||
void call_decline_trampoline(void* arg); |
||||
void call_hangup_trampoline (void* arg); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif /* CALL_H */ |
||||
@ -0,0 +1,429 @@
|
||||
// call_audio.c — сервисный аудио-движок P2P-звонка (Opus + джиттер + тоны).
|
||||
//
|
||||
// Один активный звонок: encoder (аудио-поток), decoder + адаптивный джиттер-буфер
|
||||
// vj (кодированные кадры + SoundTouch stretch на pull) и генератор тонов.
|
||||
//
|
||||
// RX: call.c → call_audio_on_media (uasync) → call_jitter_push (кодированный кадр);
|
||||
// аудио-поток call_audio_pull_pcm → call_jitter_pull (декод+stretch) (+ глитч/завершение).
|
||||
// TX: аудио-поток call_audio_feed_pcm → encode → uasync_post(call_send_media).
|
||||
|
||||
#include "call_audio.h" |
||||
#include "call.h" |
||||
#include "call_jitter.h" |
||||
#include "call_tones.h" |
||||
#include "../chat/chat_setting.h" |
||||
#include "../utun_instance.h" |
||||
#include "../../lib/opus_codec.h" |
||||
#include "../../lib/debug_config.h" |
||||
#include "../../lib/mem.h" |
||||
#include "../../lib/u_async.h" |
||||
#include "../../lib/speex_aec.h" |
||||
#include "../../lib/audio_compressor.h" |
||||
|
||||
#include <pthread.h> |
||||
#include <string.h> |
||||
|
||||
#define CALL_AUDIO_ID "call_audio" |
||||
#define CALL_AUDIO_BITRATE 32000 |
||||
#define CALL_STALL_SAMPLES (CALL_AUDIO_SAMPLE_RATE / 2) /* 500мс без PCM из jitter → глитч-тон */ |
||||
#define CALL_AEC_FILTER_SAMPLES 14400 /* хвост эха 300мс @48кГц */ |
||||
#define CALL_AEC_DELAY_FRAMES 2 /* desktop: минимальная задержка duplex */ |
||||
|
||||
static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER; |
||||
static struct UTUN_INSTANCE* g_inst = NULL; |
||||
static int g_active = 0; |
||||
static int g_ending = 0; |
||||
static int g_glitch_started = 0; /* только аудиопоток */ |
||||
static int g_ended_started = 0; /* только аудио-поток: тон завершения запущен */ |
||||
static uint64_t g_call_id = 0; |
||||
static int g_gap_samples = 0; /* только аудиопоток; насыщение на пороге */ |
||||
static opus_codec_encoder_t* g_encoder = NULL; |
||||
static opus_codec_decoder_t* g_decoder = NULL; |
||||
static struct call_jitter* g_vj = NULL; |
||||
static struct call_tones* g_tones = NULL; |
||||
static speex_aec_t* g_aec = NULL; |
||||
static int g_aec_delay_frames = CALL_AEC_DELAY_FRAMES; |
||||
static int g_aec_enabled = 1; /* по умолчанию: настройка aec_enabled */ |
||||
static struct audio_compressor* g_compressor = NULL; |
||||
|
||||
static const struct call_audio_ops g_ops = { |
||||
.on_media = call_audio_on_media, |
||||
.get_stats = call_audio_get_stats, |
||||
}; |
||||
|
||||
/* Создать AEC с текущей задержкой (объявление; определение ниже). */ |
||||
static speex_aec_t* call_audio_aec_create(void); |
||||
|
||||
/* vj decode-коллбэк: дёргается только из аудио-потока (call_jitter_pull). */ |
||||
static int call_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples) { |
||||
opus_codec_decoder_t* dec = (opus_codec_decoder_t*)arg; |
||||
return opus_codec_decode(dec, enc, len, pcm, max_samples); |
||||
} |
||||
|
||||
int call_audio_init(struct UTUN_INSTANCE* inst) { |
||||
if (!inst) return -1; |
||||
if (g_inst == inst) return 0; |
||||
g_inst = inst; |
||||
call_set_audio_ops(inst, &g_ops); |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized", CALL_AUDIO_ID); |
||||
return 0; |
||||
} |
||||
|
||||
void call_audio_destroy(struct UTUN_INSTANCE* inst) { |
||||
if (!inst || g_inst != inst) return; |
||||
call_set_audio_ops(inst, NULL); |
||||
g_inst = NULL; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed", CALL_AUDIO_ID); |
||||
} |
||||
|
||||
/* ── uasync-поток: приём медиа (кладём кодированный кадр в jitter-буфер) ── */ |
||||
|
||||
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id, |
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len) { |
||||
(void)inst; (void)seq; (void)ts_ms; |
||||
|
||||
pthread_mutex_lock(&g_mtx); |
||||
if (g_active && call_id == g_call_id && g_vj && opus && len > 0) { |
||||
call_jitter_push(g_vj, opus, len); |
||||
} |
||||
pthread_mutex_unlock(&g_mtx); |
||||
} |
||||
|
||||
/* ── GUI-поток: lifecycle ── */ |
||||
|
||||
int call_audio_start(uint64_t call_id) { |
||||
if (call_id == 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: start bad call_id=0", CALL_AUDIO_ID); |
||||
return -1; |
||||
} |
||||
|
||||
pthread_mutex_lock(&g_mtx); |
||||
if (g_active && g_call_id == call_id) { |
||||
pthread_mutex_unlock(&g_mtx); |
||||
return 0; |
||||
} |
||||
if (g_active) { |
||||
pthread_mutex_unlock(&g_mtx); |
||||
call_audio_release(g_call_id); |
||||
pthread_mutex_lock(&g_mtx); |
||||
} |
||||
|
||||
opus_codec_encoder_t* enc = opus_codec_encoder_create(CALL_AUDIO_SAMPLE_RATE, 1); |
||||
if (!enc) { |
||||
pthread_mutex_unlock(&g_mtx); |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder create failed", CALL_AUDIO_ID); |
||||
return -1; |
||||
} |
||||
opus_codec_encoder_bitrate_set(enc, CALL_AUDIO_BITRATE); |
||||
|
||||
opus_codec_decoder_t* dec = opus_codec_decoder_create(CALL_AUDIO_SAMPLE_RATE, 1); |
||||
if (!dec) { |
||||
opus_codec_encoder_destroy(enc); |
||||
pthread_mutex_unlock(&g_mtx); |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: decoder create failed", CALL_AUDIO_ID); |
||||
return -1; |
||||
} |
||||
|
||||
int max_reserve_ms = chat_setting_get_int(g_inst, "call_jitter_max_reserve_ms", 1000); |
||||
struct call_jitter* j = call_jitter_create(call_audio_decode_cb, dec, max_reserve_ms); |
||||
if (!j) { |
||||
opus_codec_decoder_destroy(dec); |
||||
opus_codec_encoder_destroy(enc); |
||||
pthread_mutex_unlock(&g_mtx); |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: vj create failed", CALL_AUDIO_ID); |
||||
return -1; |
||||
} |
||||
|
||||
struct call_tones* tones = call_tones_create(); |
||||
if (!tones) { |
||||
call_jitter_destroy(j); |
||||
opus_codec_decoder_destroy(dec); |
||||
opus_codec_encoder_destroy(enc); |
||||
pthread_mutex_unlock(&g_mtx); |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: tones create failed", CALL_AUDIO_ID); |
||||
return -1; |
||||
} |
||||
|
||||
g_aec_enabled = g_inst ? chat_setting_get_int(g_inst, "aec_enabled", 1) : 1; |
||||
speex_aec_t* aec = NULL; |
||||
if (g_aec_enabled) { |
||||
aec = call_audio_aec_create(); |
||||
if (!aec) { |
||||
g_aec_enabled = 0; |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: AEC create failed — call without echo cancellation", CALL_AUDIO_ID); |
||||
} |
||||
} |
||||
|
||||
/* компрессор микрофона (AGC) — как в рации: выравнивает тихий проводной микрофон
|
||||
* (HAL не применяет AGC на проводном входе). lookahead=0 — без задержки. */ |
||||
struct audio_compressor* ac = audio_compressor_create(); |
||||
if (ac) { |
||||
audio_compressor_config_t accfg = {0}; |
||||
accfg.sample_rate = CALL_AUDIO_SAMPLE_RATE; |
||||
accfg.channels = 1; |
||||
accfg.block_duration_ms = CALL_AUDIO_FRAME_MS; |
||||
accfg.lookback_ms = 200; |
||||
accfg.lookahead_ms = 0; |
||||
accfg.max_gain_db = (float)chat_setting_get_int(g_inst, "call_compressor_max_gain_db", 15); |
||||
accfg.rise_rate_per_sec = (float)chat_setting_get_int(g_inst, "compressor_rise_rate", 2); |
||||
accfg.target_level = 0.25f; /* -12 dBFS — как у голосовых сообщений */ |
||||
audio_compressor_configure(ac, &accfg); |
||||
audio_compressor_set_enabled(ac, chat_setting_get_int(g_inst, "call_compressor_enabled", 1)); |
||||
} else { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: compressor create failed — call without AGC", CALL_AUDIO_ID); |
||||
} |
||||
|
||||
g_encoder = enc; |
||||
g_decoder = dec; |
||||
g_vj = j; |
||||
g_tones = tones; |
||||
g_aec = aec; |
||||
g_compressor = ac; |
||||
g_call_id = call_id; |
||||
g_active = 1; |
||||
g_ending = 0; |
||||
g_ended_started = 0; |
||||
g_glitch_started = 0; |
||||
g_gap_samples = 0; |
||||
pthread_mutex_unlock(&g_mtx); |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: started call=0x%016llx aec=%d delay=%d compressor=%s gain=%d", CALL_AUDIO_ID, |
||||
(unsigned long long)call_id, g_aec ? 1 : 0, g_aec_delay_frames, |
||||
ac ? (audio_compressor_is_enabled(ac) ? "on" : "off") : "unavailable", |
||||
chat_setting_get_int(g_inst, "call_compressor_max_gain_db", 15)); |
||||
return 0; |
||||
} |
||||
|
||||
void call_audio_begin_end(uint64_t call_id) { |
||||
pthread_mutex_lock(&g_mtx); |
||||
if (g_active && call_id == g_call_id && !g_ending) { |
||||
g_ending = 1; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: begin_end call=0x%016llx (ended tone)", CALL_AUDIO_ID, (unsigned long long)call_id); |
||||
} |
||||
pthread_mutex_unlock(&g_mtx); |
||||
} |
||||
|
||||
void call_audio_release(uint64_t call_id) { |
||||
opus_codec_encoder_t* enc = NULL; |
||||
opus_codec_decoder_t* dec = NULL; |
||||
struct call_jitter* j = NULL; |
||||
struct call_tones* tones = NULL; |
||||
speex_aec_t* aec = NULL; |
||||
struct audio_compressor* ac = NULL; |
||||
int was_active = 0; |
||||
|
||||
pthread_mutex_lock(&g_mtx); |
||||
if (g_active && g_call_id == call_id) { |
||||
was_active = 1; |
||||
g_active = 0; |
||||
g_ending = 0; |
||||
g_ended_started = 0; |
||||
g_call_id = 0; |
||||
g_gap_samples = 0; |
||||
enc = g_encoder; g_encoder = NULL; |
||||
dec = g_decoder; g_decoder = NULL; |
||||
j = g_vj; g_vj = NULL; |
||||
tones = g_tones; g_tones = NULL; |
||||
aec = g_aec; g_aec = NULL; |
||||
ac = g_compressor; g_compressor = NULL; |
||||
} |
||||
pthread_mutex_unlock(&g_mtx); |
||||
|
||||
if (!was_active) return; |
||||
|
||||
/* дальше on_media не тронет объекты (active=0, указатели NULL) */ |
||||
if (enc) opus_codec_encoder_destroy(enc); |
||||
if (dec) opus_codec_decoder_destroy(dec); |
||||
if (j) call_jitter_destroy(j); |
||||
if (tones) call_tones_destroy(tones); |
||||
if (aec) speex_aec_destroy(aec); |
||||
if (ac) audio_compressor_destroy(ac); |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: released call=0x%016llx", CALL_AUDIO_ID, (unsigned long long)call_id); |
||||
} |
||||
|
||||
void call_audio_stop(void) { |
||||
uint64_t cid = 0; |
||||
pthread_mutex_lock(&g_mtx); |
||||
if (g_active) cid = g_call_id; |
||||
pthread_mutex_unlock(&g_mtx); |
||||
if (cid) call_audio_release(cid); |
||||
} |
||||
|
||||
/* ── AEC (SpeexDSP): рантайм-управление ── */ |
||||
|
||||
/* Создать AEC с текущей задержкой. Вызывается под g_mtx. */ |
||||
static speex_aec_t* call_audio_aec_create(void) { |
||||
return speex_aec_create(CALL_AUDIO_SAMPLE_RATE, CALL_AUDIO_FRAME_SAMPLES, |
||||
CALL_AEC_FILTER_SAMPLES, g_aec_delay_frames); |
||||
} |
||||
|
||||
void call_audio_set_aec_enabled(int enabled) { |
||||
pthread_mutex_lock(&g_mtx); |
||||
g_aec_enabled = enabled ? 1 : 0; |
||||
if (g_active) { |
||||
if (g_aec_enabled && !g_aec) { |
||||
g_aec = call_audio_aec_create(); |
||||
if (!g_aec) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: AEC create failed (enabled)", CALL_AUDIO_ID); |
||||
else DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC enabled (delay=%d frames)", CALL_AUDIO_ID, g_aec_delay_frames); |
||||
} else if (!g_aec_enabled && g_aec) { |
||||
speex_aec_destroy(g_aec); |
||||
g_aec = NULL; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC disabled", CALL_AUDIO_ID); |
||||
} |
||||
} |
||||
pthread_mutex_unlock(&g_mtx); |
||||
} |
||||
|
||||
void call_audio_set_aec_delay_frames(int frames) { |
||||
if (frames < 1) frames = 1; |
||||
pthread_mutex_lock(&g_mtx); |
||||
g_aec_delay_frames = frames; |
||||
if (g_active && g_aec_enabled && g_aec) { |
||||
speex_aec_destroy(g_aec); |
||||
g_aec = call_audio_aec_create(); |
||||
if (!g_aec) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: AEC recreate failed (delay=%d)", CALL_AUDIO_ID, frames); |
||||
else DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: AEC delay -> %d frames", CALL_AUDIO_ID, frames); |
||||
} |
||||
pthread_mutex_unlock(&g_mtx); |
||||
} |
||||
|
||||
/* ── аудио-поток: TX / RX ── */ |
||||
|
||||
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) { |
||||
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES) return -1; |
||||
|
||||
uint8_t opus[256]; |
||||
int16_t cleaned[CALL_AUDIO_FRAME_SAMPLES]; |
||||
struct UASYNC* ua = NULL; |
||||
struct UTUN_INSTANCE* inst = NULL; |
||||
int len = -1; |
||||
|
||||
pthread_mutex_lock(&g_mtx); |
||||
if (!g_active || call_id != g_call_id || !g_encoder) { |
||||
pthread_mutex_unlock(&g_mtx); |
||||
return -1; |
||||
} |
||||
const int16_t* tx = pcm; |
||||
if (g_aec) { |
||||
if (speex_aec_process_capture(g_aec, pcm, count, cleaned) == count) |
||||
tx = cleaned; |
||||
} |
||||
/* компрессор (AGC): push → кодируем прямо из выхода до clear_output.
|
||||
* lookahead=0 ⇒ выход ровно count сэмплов. */ |
||||
if (g_compressor) { |
||||
audio_compressor_push(g_compressor, tx, (size_t)count); |
||||
const int16_t* out = audio_compressor_output(g_compressor); |
||||
if (out && audio_compressor_output_size(g_compressor) >= (size_t)count) |
||||
tx = out; |
||||
len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus)); |
||||
audio_compressor_clear_output(g_compressor); |
||||
} else { |
||||
len = opus_codec_encode(g_encoder, tx, count, opus, (int)sizeof(opus)); |
||||
} |
||||
ua = g_inst ? g_inst->ua : NULL; |
||||
inst = g_inst; |
||||
pthread_mutex_unlock(&g_mtx); |
||||
|
||||
if (len <= 0) return -1; |
||||
if (!ua || !inst) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: feed_pcm: instance not set (call_audio_init not called)", CALL_AUDIO_ID); |
||||
return -1; |
||||
} |
||||
|
||||
struct call_media_arg* a = (struct call_media_arg*)u_malloc(sizeof(*a)); |
||||
if (!a) return -1; |
||||
a->inst = inst; |
||||
a->call_id = call_id; |
||||
a->len = (uint16_t)len; |
||||
memcpy(a->opus, opus, (size_t)len); |
||||
uasync_post(ua, call_send_media_trampoline, a); |
||||
return 0; |
||||
} |
||||
|
||||
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) { |
||||
if (!out || max_samples <= 0) return 0; |
||||
|
||||
int active = 0, ending = 0; |
||||
struct call_jitter* j = NULL; |
||||
struct call_tones* tones = NULL; |
||||
|
||||
pthread_mutex_lock(&g_mtx); |
||||
if (g_active && call_id == g_call_id) { |
||||
active = 1; |
||||
ending = g_ending; |
||||
j = g_vj; |
||||
tones = g_tones; |
||||
} |
||||
pthread_mutex_unlock(&g_mtx); |
||||
|
||||
if (!active || !j || !tones) return 0; |
||||
|
||||
int n = 0; |
||||
|
||||
/* завершение: доигрываем тон (450мс), затем тишина. Тон стартует лениво
|
||||
* здесь (аудио-поток) — begin_end лишь выставляет флаг, чтобы не гонять |
||||
* генератор тонов с GUI-потоком. */ |
||||
if (ending) { |
||||
if (!g_ended_started) { |
||||
call_tones_ended_start(tones); |
||||
g_ended_started = 1; |
||||
} |
||||
if (call_tones_ended_pending(tones)) |
||||
n = call_tones_render(tones, out, max_samples); |
||||
} else { |
||||
n = call_jitter_pull(j, out, max_samples); |
||||
int previous_gap = g_gap_samples; |
||||
if (n > 0) g_gap_samples = 0; |
||||
int missing = max_samples - n; |
||||
int quiet = CALL_STALL_SAMPLES - g_gap_samples; |
||||
if (quiet > missing) quiet = missing; |
||||
int tone_samples = missing - quiet; |
||||
if (n > 0 && previous_gap > 0) |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: PCM resumed call=%016llx gap=%dms pcm=%d/%d", CALL_AUDIO_ID, |
||||
(unsigned long long)call_id, previous_gap * 1000 / CALL_AUDIO_SAMPLE_RATE, n, max_samples); |
||||
if (missing > 0 && g_gap_samples == 0) |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "%s: PCM gap started call=%016llx pcm=%d/%d", CALL_AUDIO_ID, |
||||
(unsigned long long)call_id, n, max_samples); |
||||
g_gap_samples += quiet; |
||||
/* Новый PCM прерывает предыдущий цикл тона, даже если хвост блока снова пуст. */ |
||||
if (g_glitch_started && (n > 0 || tone_samples == 0)) { |
||||
g_glitch_started = 0; |
||||
call_tones_glitch_stop(tones); |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone stopped call=%016llx pcm=%d/%d", CALL_AUDIO_ID, |
||||
(unsigned long long)call_id, n, max_samples); |
||||
} |
||||
memset(out + n, 0, (size_t)quiet * sizeof(*out)); |
||||
if (tone_samples > 0) { |
||||
if (!g_glitch_started) { |
||||
g_glitch_started = 1; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: gap tone started call=%016llx pcm_gap=500ms pcm=%d/%d", CALL_AUDIO_ID, |
||||
(unsigned long long)call_id, n, max_samples); |
||||
} |
||||
call_tones_glitch_start(tones); |
||||
call_tones_render(tones, out + n + quiet, tone_samples); |
||||
} |
||||
n = max_samples; |
||||
} |
||||
|
||||
/* рендер-референс для AEC: то, что реально пошло в динамик */ |
||||
if (n > 0) { |
||||
pthread_mutex_lock(&g_mtx); |
||||
if (g_aec) speex_aec_feed_playback(g_aec, out, n); |
||||
pthread_mutex_unlock(&g_mtx); |
||||
} |
||||
return n; |
||||
} |
||||
|
||||
/* ── uasync-поток: метрики ── */ |
||||
|
||||
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100, |
||||
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range) { |
||||
int ok = 0; |
||||
pthread_mutex_lock(&g_mtx); |
||||
if (g_active && call_id == g_call_id && g_vj) { |
||||
call_jitter_get_stats(g_vj, buffer_ms, tempo_x100, dropped, underruns, range); |
||||
ok = 1; |
||||
} |
||||
pthread_mutex_unlock(&g_mtx); |
||||
return ok ? 0 : -1; |
||||
} |
||||
@ -0,0 +1,79 @@
|
||||
#ifndef CALL_AUDIO_H |
||||
#define CALL_AUDIO_H |
||||
|
||||
// call_audio.h — сервисный аудио-движок P2P-звонка (единый для desktop/Android/headless).
|
||||
//
|
||||
// Владеет Opus codec, адаптивным джиттер-буфером (call_jitter: кодированные
|
||||
// кадры + SoundTouch time-stretch на pull) и тонами (глитч при сталле / завершение).
|
||||
// Излишек голоса держится до декодирования — декодируется только то, что готово
|
||||
// к воспроизведению. Потребитель (desktop miniaudio / Android AudioTrack) получает
|
||||
// финальный PCM через call_audio_pull_pcm и отдаёт захваченный PCM через
|
||||
// call_audio_feed_pcm.
|
||||
//
|
||||
// Потоки:
|
||||
// - call_audio_start / begin_end / release — GUI-поток (по ACCEPTED/ENDED);
|
||||
// - call_audio_feed_pcm / pull_pcm — аудио-поток (single-writer);
|
||||
// - call_audio_on_media / get_stats — uasync-поток (из call.c).
|
||||
// Общие указатели (encoder/decoder/vj/tones) защищены мьютексом; encoder и
|
||||
// decoder — только аудио-поток (decoder дёргается из call_jitter_pull через коллбэк),
|
||||
// call_jitter_push — только uasync-поток.
|
||||
|
||||
#include <stdint.h> |
||||
#include "call_jitter.h" |
||||
|
||||
struct UTUN_INSTANCE; |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
#define CALL_AUDIO_SAMPLE_RATE 48000 |
||||
#define CALL_AUDIO_FRAME_MS 20 |
||||
#define CALL_AUDIO_FRAME_SAMPLES (CALL_AUDIO_SAMPLE_RATE * CALL_AUDIO_FRAME_MS / 1000) /* 960 */ |
||||
|
||||
/* Зарегистрировать аудио-движок в call.c (идемпотентно). */ |
||||
int call_audio_init(struct UTUN_INSTANCE* inst); |
||||
void call_audio_destroy(struct UTUN_INSTANCE* inst); |
||||
|
||||
/* GUI-поток: создать codec + jitter + tones для звонка. 0 = успех. */ |
||||
int call_audio_start(uint64_t call_id); |
||||
|
||||
/* GUI-поток: по CALL_ENDED — начать тон завершения (pull_pcm выдаст 450мс тона). */ |
||||
void call_audio_begin_end(uint64_t call_id); |
||||
|
||||
/* GUI-поток (после остановки аудиоустройства): освободить все объекты. */ |
||||
void call_audio_release(uint64_t call_id); |
||||
|
||||
/* GUI-поток: остановить текущий активный звонок (удобная обёртка release). */ |
||||
void call_audio_stop(void); |
||||
|
||||
/* аудио-поток: закодировать PCM-кадр (ровно CALL_AUDIO_FRAME_SAMPLES) и отправить пиру. */ |
||||
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count); |
||||
|
||||
/* аудио-поток: выдать до max_samples финального PCM (jitter + stretch + тоны).
|
||||
* Возвращает число сэмплов (может быть < max — андерфлоу/конец тона). */ |
||||
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples); |
||||
|
||||
/* uasync-поток (из call.c): принятый Opus-кадр — кладём в джиттер-буфер (call_jitter_push,
|
||||
* декодирование произойдёт на pull в аудио-потоке). */ |
||||
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id, |
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len); |
||||
|
||||
/* uasync-поток: метрики джиттер-буфера для CALL_STATS (dropped/underruns — необязательные). */ |
||||
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100, |
||||
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range); |
||||
|
||||
/* Включить/выключить AEC (SpeexDSP) в рантайме. desktop — по настройке GUI,
|
||||
* Android — по спикерфону (route == SPEAKER). Работает и в активном звонке. */ |
||||
void call_audio_set_aec_enabled(int enabled); |
||||
|
||||
/* Задать задержку рендер→захват AEC в кадрах (20 мс). Android измеряет через
|
||||
* AudioTrack/AudioRecord getTimestamp, desktop использует дефолт. Если AEC уже |
||||
* активен — пересоздаётся с новой задержкой. */ |
||||
void call_audio_set_aec_delay_frames(int frames); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
|
||||
#endif /* CALL_AUDIO_H */ |
||||
@ -0,0 +1,372 @@
|
||||
// call_headless.c — headless-аудио-сокет для P2P-звонков (PCM, без тактирования)
|
||||
//
|
||||
// TCP-сокет с бинарным фреймингом [type:1][call_id:8][len:2][data:len].
|
||||
// Клиент привязывается к звонку (HELLO) и обменивается ГОТОВЫМ PCM (int16 mono 48k):
|
||||
// клиент → ядро: PCM-кадр (960 сэмплов) — кодируется в Opus и уходит пиру;
|
||||
// ядро → клиент: Opus пира декодируется в PCM и отдаётся клиенту.
|
||||
// Тактирование/джиттер — на стороне клиента (ядро лишь кодирует/декодирует).
|
||||
// Сигналинг — на control-сокете (chat_headless_control), медиа — здесь.
|
||||
#include "call_headless.h" |
||||
#include "call.h" |
||||
#include "../utun_instance.h" |
||||
#include "../../lib/opus_codec.h" |
||||
#include "../../lib/u_async.h" |
||||
#include "../../lib/mem.h" |
||||
#include "../../lib/socket_compat.h" |
||||
#include "../../lib/debug_config.h" |
||||
#include "../../lib/platform_compat.h" |
||||
|
||||
#include <string.h> |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <errno.h> |
||||
|
||||
#define CH_ID "call_audio" |
||||
|
||||
#define AUDIO_RECV_BUF_SIZE 65536 |
||||
#define AUDIO_SEND_BUF_SIZE 65536 |
||||
#define AUDIO_MAX_CLIENTS 16 |
||||
#define AUDIO_MAX_FRAME 2048 |
||||
|
||||
#define CA_SAMPLE_RATE 48000 |
||||
#define CA_FRAME_SAMPLES 960 /* 20мс @48k */ |
||||
#define CA_PCM_FRAME_BYTES (CA_FRAME_SAMPLES * (int)sizeof(int16_t)) /* 1920 */ |
||||
#define CA_BITRATE 32000 |
||||
|
||||
struct call_audio_client { |
||||
socket_t fd; |
||||
void* socket_id; |
||||
uint64_t call_id; /* 0 = не привязан */ |
||||
uint8_t recv_buf[AUDIO_RECV_BUF_SIZE]; |
||||
int recv_len; |
||||
uint8_t send_buf[AUDIO_SEND_BUF_SIZE]; |
||||
int send_len; |
||||
int send_offset; |
||||
int closing; |
||||
struct call_audio* ca; |
||||
struct call_audio_client* next; |
||||
}; |
||||
|
||||
struct call_audio { |
||||
struct UASYNC* ua; |
||||
struct UTUN_INSTANCE* inst; |
||||
struct call_audio_client* clients; |
||||
int client_count; |
||||
socket_t listen_fd; |
||||
void* listen_sock_id; |
||||
int running; |
||||
void* cleanup_timer; |
||||
/* Opus-кодеки (одна активная звонка за раз — busy в call.c) */ |
||||
uint64_t enc_call_id; |
||||
opus_codec_encoder_t* enc; |
||||
uint64_t dec_call_id; |
||||
opus_codec_decoder_t* dec; |
||||
/* диагностика */ |
||||
uint32_t c_enc_frames; |
||||
uint32_t c_dec_frames; |
||||
uint32_t c_enc_dropped; |
||||
uint32_t c_dec_dropped; |
||||
}; |
||||
|
||||
static struct call_audio* ca_of(struct UTUN_INSTANCE* inst) { |
||||
return inst ? (struct call_audio*)inst->call_audio : NULL; |
||||
} |
||||
|
||||
static void ca_close_client(struct call_audio_client* cli); |
||||
|
||||
/* ── отправка фрейма клиенту (с буферизацией как в control-сокете) ── */ |
||||
|
||||
static void ca_send_raw(struct call_audio_client* cli, const uint8_t* data, size_t len) { |
||||
struct call_audio* ca = cli->ca; |
||||
if (!cli || cli->closing || cli->fd == SOCKET_INVALID || !data || len == 0) return; |
||||
if (cli->send_len > 0) { |
||||
size_t space = sizeof(cli->send_buf) - (size_t)cli->send_len; |
||||
if (len > space) return; |
||||
memcpy(cli->send_buf + cli->send_len, data, len); |
||||
cli->send_len += (int)len; |
||||
return; |
||||
} |
||||
ssize_t r = send(cli->fd, data, (int)len, 0); |
||||
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) { |
||||
if (len <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data, len); cli->send_len = (int)len; cli->send_offset = 0; } |
||||
if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 1); |
||||
return; |
||||
} |
||||
if (r >= 0 && (size_t)r < len) { |
||||
size_t remain = len - (size_t)r; |
||||
if (remain <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data + r, remain); cli->send_len = (int)remain; cli->send_offset = 0; } |
||||
if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 1); |
||||
} |
||||
if (r < 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: send failed err=%d, closing client", CH_ID, socket_get_error()); |
||||
ca_close_client(cli); |
||||
} |
||||
} |
||||
|
||||
static void ca_write_frame(struct call_audio_client* cli, uint8_t type, uint64_t call_id, |
||||
const uint8_t* data, uint16_t len) { |
||||
uint8_t hdr[CALL_AUDIO_HDR_SIZE]; |
||||
hdr[0] = type; |
||||
memcpy(hdr + 1, &call_id, 8); |
||||
hdr[9] = (uint8_t)(len >> 8); |
||||
hdr[10] = (uint8_t)(len & 0xFF); |
||||
ca_send_raw(cli, hdr, sizeof(hdr)); |
||||
if (len) ca_send_raw(cli, data, len); |
||||
} |
||||
|
||||
/* ── Opus-кодеки (одна активная звонка за раз) ── */ |
||||
|
||||
static opus_codec_encoder_t* ca_encoder(struct call_audio* ca, uint64_t call_id) { |
||||
if (ca->enc && ca->enc_call_id == call_id) return ca->enc; |
||||
if (ca->enc) { opus_codec_encoder_destroy(ca->enc); ca->enc = NULL; } |
||||
ca->enc = opus_codec_encoder_create(CA_SAMPLE_RATE, 1); |
||||
if (ca->enc) { |
||||
opus_codec_encoder_bitrate_set(ca->enc, CA_BITRATE); |
||||
ca->enc_call_id = call_id; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: encoder created for call_id=%016llx", CH_ID, (unsigned long long)call_id); |
||||
} else { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder create failed", CH_ID); |
||||
} |
||||
return ca->enc; |
||||
} |
||||
|
||||
static opus_codec_decoder_t* ca_decoder(struct call_audio* ca, uint64_t call_id) { |
||||
if (ca->dec && ca->dec_call_id == call_id) return ca->dec; |
||||
if (ca->dec) { opus_codec_decoder_destroy(ca->dec); ca->dec = NULL; } |
||||
ca->dec = opus_codec_decoder_create(CA_SAMPLE_RATE, 1); |
||||
if (ca->dec) { |
||||
ca->dec_call_id = call_id; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: decoder created for call_id=%016llx", CH_ID, (unsigned long long)call_id); |
||||
} else { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: decoder create failed", CH_ID); |
||||
} |
||||
return ca->dec; |
||||
} |
||||
|
||||
/* ── приём медиа от пира → PCM клиенту ── */ |
||||
|
||||
static void ca_media_recv(struct UTUN_INSTANCE* inst, uint64_t call_id, |
||||
const uint8_t* opus, int len, void* arg) { |
||||
struct call_audio* ca = (struct call_audio*)arg; |
||||
(void)inst; |
||||
if (!ca || !opus || len <= 0) return; |
||||
|
||||
opus_codec_decoder_t* dec = ca_decoder(ca, call_id); |
||||
if (!dec) return; |
||||
|
||||
int16_t pcm[CA_FRAME_SAMPLES]; |
||||
int n = opus_codec_decode(dec, opus, len, pcm, CA_FRAME_SAMPLES); |
||||
if (n <= 0) { |
||||
ca->c_dec_dropped++; |
||||
return; |
||||
} |
||||
ca->c_dec_frames++; |
||||
|
||||
for (struct call_audio_client* c = ca->clients; c; c = c->next) { |
||||
if (c->call_id == call_id && !c->closing) { |
||||
ca_write_frame(c, CALL_AUDIO_FRAME, call_id, (const uint8_t*)pcm, (uint16_t)(n * (int)sizeof(int16_t))); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
/* ── разбор входящих фреймов ── */ |
||||
|
||||
static void ca_parse_frames(struct call_audio_client* cli) { |
||||
struct call_audio* ca = cli->ca; |
||||
uint8_t* p = cli->recv_buf; |
||||
int avail = cli->recv_len; |
||||
int consumed = 0; |
||||
while (avail - consumed >= CALL_AUDIO_HDR_SIZE) { |
||||
uint8_t type = p[consumed]; |
||||
uint64_t call_id; memcpy(&call_id, p + consumed + 1, 8); |
||||
uint16_t flen = (uint16_t)((p[consumed + 9] << 8) | p[consumed + 10]); |
||||
if (flen > AUDIO_MAX_FRAME) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: frame too big %u — closing", CH_ID, flen); |
||||
ca_close_client(cli); |
||||
return; |
||||
} |
||||
if (avail - consumed < (int)(CALL_AUDIO_HDR_SIZE + flen)) break; /* нужен ещё кусок */ |
||||
const uint8_t* data = p + consumed + CALL_AUDIO_HDR_SIZE; |
||||
if (type == CALL_AUDIO_HELLO) { |
||||
cli->call_id = call_id; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client bound to call_id=%016llx", CH_ID, (unsigned long long)call_id); |
||||
} else if (type == CALL_AUDIO_FRAME) { |
||||
if (flen != CA_PCM_FRAME_BYTES) { |
||||
ca->c_enc_dropped++; |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: bad PCM frame size=%u (expect %d) — dropped", |
||||
CH_ID, flen, CA_PCM_FRAME_BYTES); |
||||
} else { |
||||
opus_codec_encoder_t* enc = ca_encoder(ca, call_id); |
||||
uint8_t opus[256]; |
||||
int olen = enc ? opus_codec_encode(enc, (const int16_t*)data, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1; |
||||
if (olen <= 0) { |
||||
ca->c_enc_dropped++; |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: encode failed (olen=%d) call_id=%016llx", |
||||
CH_ID, olen, (unsigned long long)call_id); |
||||
} else { |
||||
ca->c_enc_frames++; |
||||
if (call_send_media(ca->inst, call_id, opus, olen) != 0) |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: send_media failed call_id=%016llx len=%d", |
||||
CH_ID, (unsigned long long)call_id, olen); |
||||
} |
||||
} |
||||
} else { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: unknown frame type=0x%02x — closing", CH_ID, type); |
||||
ca_close_client(cli); |
||||
return; |
||||
} |
||||
consumed += CALL_AUDIO_HDR_SIZE + flen; |
||||
} |
||||
if (consumed > 0) { |
||||
memmove(cli->recv_buf, cli->recv_buf + consumed, (size_t)(cli->recv_len - consumed)); |
||||
cli->recv_len -= consumed; |
||||
} |
||||
} |
||||
|
||||
/* ── socket callbacks ── */ |
||||
|
||||
static void ca_write_callback(socket_t fd, void* arg) { |
||||
struct call_audio_client* cli = (struct call_audio_client*)arg; |
||||
struct call_audio* ca = cli ? cli->ca : NULL; |
||||
if (!cli || cli->closing || fd != cli->fd) return; |
||||
if (cli->send_len <= 0) { if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 0); return; } |
||||
ssize_t r = send(cli->fd, cli->send_buf + cli->send_offset, cli->send_len - cli->send_offset, 0); |
||||
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) return; |
||||
if (r <= 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: send failed (r=%zd err=%d) — closing client", CH_ID, r, socket_get_error()); |
||||
ca_close_client(cli); |
||||
return; |
||||
} |
||||
cli->send_offset += (int)r; |
||||
if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (ca && ca->ua) uasync_set_socket_write(ca->ua, cli->socket_id, 0); } |
||||
} |
||||
|
||||
static void ca_close_client(struct call_audio_client* cli) { |
||||
struct call_audio* ca = cli ? cli->ca : NULL; |
||||
if (!cli || cli->closing) return; |
||||
cli->closing = 1; |
||||
if (ca && ca->ua && cli->socket_id) uasync_remove_socket_t(ca->ua, cli->fd); |
||||
if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; } |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client closed call_id=%016llx", CH_ID, (unsigned long long)cli->call_id); |
||||
} |
||||
|
||||
static void ca_read_callback(socket_t fd, void* arg) { |
||||
struct call_audio_client* cli = (struct call_audio_client*)arg; |
||||
if (!cli || cli->closing) return; |
||||
uint8_t tmp[4096]; |
||||
ssize_t r = recv(fd, tmp, (int)sizeof(tmp), 0); |
||||
if (r <= 0) { ca_close_client(cli); return; } |
||||
if (cli->recv_len + (int)r > AUDIO_RECV_BUF_SIZE) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: recv buffer overflow — closing", CH_ID); |
||||
ca_close_client(cli); |
||||
return; |
||||
} |
||||
memcpy(cli->recv_buf + cli->recv_len, tmp, (size_t)r); |
||||
cli->recv_len += (int)r; |
||||
ca_parse_frames(cli); |
||||
} |
||||
|
||||
static void ca_accept_callback(socket_t fd, void* arg) { |
||||
struct call_audio* ca = (struct call_audio*)arg; |
||||
struct sockaddr_storage addr; socklen_t alen = sizeof(addr); |
||||
socket_t cfd = accept(fd, (struct sockaddr*)&addr, &alen); |
||||
if (cfd == SOCKET_INVALID) { |
||||
int e = socket_get_error(); |
||||
if (e != ERR_AGAIN && e != ERR_WOULDBLOCK) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: accept failed: %s", CH_ID, socket_strerror(e)); |
||||
return; |
||||
} |
||||
socket_set_nonblocking(cfd); |
||||
if (ca->client_count >= AUDIO_MAX_CLIENTS) { socket_close_wrapper(cfd); DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: max clients reached", CH_ID); return; } |
||||
|
||||
struct call_audio_client* cli = u_calloc(1, sizeof(*cli)); |
||||
if (!cli) { socket_close_wrapper(cfd); return; } |
||||
cli->fd = cfd; |
||||
cli->ca = ca; |
||||
cli->socket_id = uasync_add_socket_t(ca->ua, cfd, ca_read_callback, ca_write_callback, NULL, "call_client", cli); |
||||
if (!cli->socket_id) { u_free(cli); socket_close_wrapper(cfd); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: failed to register client fd", CH_ID); return; } |
||||
|
||||
cli->next = ca->clients; ca->clients = cli; ca->client_count++; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client connected fd=%d total=%d", CH_ID, (int)cfd, ca->client_count); |
||||
} |
||||
|
||||
/* ── периодическая очистка закрытых клиентов ── */ |
||||
|
||||
static void ca_cleanup(void* arg) { |
||||
struct call_audio* ca = (struct call_audio*)arg; |
||||
ca->cleanup_timer = NULL; |
||||
if (!ca->running) return; |
||||
struct call_audio_client** prev = &ca->clients; |
||||
while (*prev) { |
||||
if ((*prev)->closing) { |
||||
struct call_audio_client* dead = *prev; *prev = dead->next; |
||||
if (dead->fd != SOCKET_INVALID) socket_close_wrapper(dead->fd); |
||||
u_free(dead); ca->client_count--; |
||||
} else prev = &(*prev)->next; |
||||
} |
||||
ca->cleanup_timer = uasync_set_timeout(ca->ua, 5000, ca, ca_cleanup, "ca_cleanup"); |
||||
if (!ca->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID); |
||||
} |
||||
|
||||
/* ── init / destroy ── */ |
||||
|
||||
int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst, |
||||
const char* bind_ip, int port) { |
||||
if (!ua || !inst || !bind_ip || port <= 0 || port > 65535) return -1; |
||||
struct call_audio* ca = ca_of(inst); |
||||
if (ca && ca->running) return 0; |
||||
|
||||
if (!ca) { |
||||
ca = u_calloc(1, sizeof(*ca)); |
||||
if (!ca) return -1; |
||||
inst->call_audio = ca; |
||||
} |
||||
ca->ua = ua; ca->inst = inst; |
||||
|
||||
ca->listen_fd = socket(AF_INET, SOCK_STREAM, 0); |
||||
if (ca->listen_fd == SOCKET_INVALID) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: socket() failed: %s", CH_ID, socket_strerror(socket_get_error())); return -1; } |
||||
socket_set_reuseaddr(ca->listen_fd, 1); |
||||
socket_set_nonblocking(ca->listen_fd); |
||||
|
||||
struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); |
||||
sin.sin_family = AF_INET; sin.sin_port = htons((uint16_t)port); |
||||
if (inet_pton(AF_INET, bind_ip, &sin.sin_addr) != 1) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1; } |
||||
if (bind(ca->listen_fd, (struct sockaddr*)&sin, sizeof(sin)) < 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: bind(%s:%d) failed: %s", CH_ID, bind_ip, port, socket_strerror(socket_get_error())); |
||||
socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1; |
||||
} |
||||
if (listen(ca->listen_fd, 5) < 0) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1; } |
||||
|
||||
ca->listen_sock_id = uasync_add_socket_t(ua, ca->listen_fd, ca_accept_callback, NULL, NULL, "call_listen", ca); |
||||
if (!ca->listen_sock_id) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; return -1; } |
||||
|
||||
call_set_media_recv_cb(inst, ca_media_recv, ca); |
||||
ca->cleanup_timer = uasync_set_timeout(ua, 5000, ca, ca_cleanup, "ca_cleanup"); |
||||
if (!ca->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID); |
||||
|
||||
ca->running = 1; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket listening on %s:%d", CH_ID, bind_ip, port); |
||||
return 0; |
||||
} |
||||
|
||||
void call_headless_destroy(struct UTUN_INSTANCE* inst) { |
||||
struct call_audio* ca = ca_of(inst); |
||||
if (!ca || !ca->running) return; |
||||
ca->running = 0; |
||||
if (ca->cleanup_timer) { uasync_cancel_timeout(ca->ua, ca->cleanup_timer); ca->cleanup_timer = NULL; } |
||||
call_set_media_recv_cb(inst, NULL, NULL); |
||||
if (ca->listen_sock_id && ca->ua) uasync_remove_socket_t(ca->ua, ca->listen_fd); |
||||
if (ca->listen_fd != SOCKET_INVALID) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; } |
||||
if (ca->enc) { opus_codec_encoder_destroy(ca->enc); ca->enc = NULL; } |
||||
if (ca->dec) { opus_codec_decoder_destroy(ca->dec); ca->dec = NULL; } |
||||
struct call_audio_client* c = ca->clients; |
||||
while (c) { |
||||
struct call_audio_client* n = c->next; |
||||
ca_close_client(c); |
||||
u_free(c); |
||||
c = n; |
||||
} |
||||
ca->clients = NULL; ca->client_count = 0; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u)", |
||||
CH_ID, ca->c_enc_frames, ca->c_dec_frames, ca->c_enc_dropped, ca->c_dec_dropped); |
||||
} |
||||
@ -0,0 +1,38 @@
|
||||
// call_headless.h — headless-аудио-сокет для P2P-звонков (PCM, без тактирования)
|
||||
//
|
||||
// Отдельный TCP-сокет, через который headless-клиент (CLI/скрипт) обменивается
|
||||
// ГОТОВЫМ PCM с ядром звонка. Ядро кодирует PCM→Opus (TX) и декодирует Opus→PCM
|
||||
// (RX); тактирование и джиттер — на стороне клиента. Сигналинг идёт по
|
||||
// control-сокету (chat_headless_control), аудио — по этому сокету.
|
||||
//
|
||||
// Бинарный фрейм (обе стороны):
|
||||
// [type:1][call_id:8][len:2][data:len]
|
||||
// type = 0x01 HELLO (клиент → ядро: привязать соединение к call_id, data пусто)
|
||||
// type = 0x02 FRAME (PCM int16 mono 48k, 960 сэмплов = 1920 байт)
|
||||
// клиент → ядро = PCM захвата (кодируется и шлётся пиру);
|
||||
// ядро → клиент = PCM воспроизведения (декодированный Opus пира)
|
||||
#ifndef CALL_HEADLESS_H |
||||
#define CALL_HEADLESS_H |
||||
|
||||
#include <stdint.h> |
||||
|
||||
struct UASYNC; |
||||
struct UTUN_INSTANCE; |
||||
|
||||
#define CALL_AUDIO_HELLO 0x01 |
||||
#define CALL_AUDIO_FRAME 0x02 |
||||
#define CALL_AUDIO_HDR_SIZE 11 /* type(1) + call_id(8) + len(2) */ |
||||
#define CALL_AUDIO_PCM_FRAME_BYTES (960 * (int)sizeof(int16_t)) /* 1920 — PCM int16 mono 48k 20мс */ |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst, |
||||
const char* bind_ip, int port); |
||||
void call_headless_destroy(struct UTUN_INSTANCE* inst); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif /* CALL_HEADLESS_H */ |
||||
@ -0,0 +1,172 @@
|
||||
// Буфер звонка: храним речь в порядке доставки, ускоряем накопление без изменения высоты голоса.
|
||||
#include "call_jitter.h" |
||||
#include "call_jitter_window.h" |
||||
#include <chrono> |
||||
#include "../../lib/debug_config.h" |
||||
#include "../../lib/mem.h" |
||||
#include "soundtouch/SoundTouch.h" |
||||
|
||||
#include <algorithm> |
||||
#include <cmath> |
||||
#include <cstring> |
||||
#include <mutex> |
||||
#include <new> |
||||
#include <vector> |
||||
|
||||
static constexpr int kSampleRate = 48000; |
||||
static constexpr int kFrameSamples = 960; |
||||
static constexpr int kMaxEncoded = 512; |
||||
static constexpr int kTargetMs = 60; |
||||
static constexpr double kMaxTempo = 1.6; |
||||
static constexpr int kTempoRampMs = 500; |
||||
|
||||
template<typename T> static T clamp_value(T value, T low, T high) { |
||||
return std::max(low, std::min(value, high)); |
||||
} |
||||
|
||||
struct call_jitter { |
||||
std::mutex mtx; |
||||
uint8_t ring[CALL_JITTER_MAX_FRAMES][kMaxEncoded]; |
||||
int lens[CALL_JITTER_MAX_FRAMES]; |
||||
int head = 0, count = 0; |
||||
uint32_t dropped = 0, underruns = 0; |
||||
int processing_ms = 0, tempo_x100 = 100; /* снимок аудиопотока под mtx */ |
||||
call_jitter_window window; |
||||
call_jitter_range range = {0, 0, 60}; /* опубликованный снимок под mtx */ |
||||
call_jitter_decode_fn decode; |
||||
void* arg; |
||||
soundtouch::SoundTouch st; |
||||
int16_t pcm[kFrameSamples]; |
||||
float input[kFrameSamples]; |
||||
std::vector<float> output; |
||||
bool primed = false, catching_up = false; |
||||
double tempo = 1.0, ema_ms = kTargetMs; |
||||
}; |
||||
|
||||
extern "C" { |
||||
struct call_jitter* call_jitter_create(call_jitter_decode_fn decode, void* arg, int max_reserve_ms) { |
||||
if (max_reserve_ms < 500 || max_reserve_ms > 2000) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_jitter: invalid max reserve=%dms", max_reserve_ms); |
||||
return nullptr; |
||||
} |
||||
if (!decode) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_jitter: missing decoder"); |
||||
return nullptr; |
||||
} |
||||
void* memory = u_malloc(sizeof(call_jitter)); |
||||
if (!memory) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_jitter: allocation failed"); |
||||
return nullptr; |
||||
} |
||||
auto* j = new (memory) call_jitter; |
||||
j->window.max_reserve = max_reserve_ms; |
||||
j->decode = decode; j->arg = arg; |
||||
j->st.setSampleRate(kSampleRate); j->st.setChannels(1); j->st.setTempo(1.0); |
||||
j->st.setSetting(SETTING_USE_QUICKSEEK, 1); |
||||
j->st.setSetting(SETTING_SEQUENCE_MS, 20); |
||||
j->st.setSetting(SETTING_SEEKWINDOW_MS, 10); |
||||
j->st.setSetting(SETTING_OVERLAP_MS, 4); |
||||
j->st.setSetting(SETTING_USE_AA_FILTER, 0); |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: created capacity=%dms target=%dms max_reserve=%dms tempo=1.0..%.1f ramp=%dms", |
||||
CALL_JITTER_MAX_FRAMES * CALL_JITTER_FRAME_MS, kTargetMs, max_reserve_ms, kMaxTempo, kTempoRampMs); |
||||
return j; |
||||
} |
||||
|
||||
void call_jitter_destroy(struct call_jitter* j) { |
||||
if (!j) return; |
||||
j->~call_jitter(); u_free(j); |
||||
} |
||||
|
||||
void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len) { |
||||
if (!j || !enc || len <= 0 || len > kMaxEncoded) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: invalid frame len=%d", len); |
||||
return; |
||||
} |
||||
std::lock_guard<std::mutex> lock(j->mtx); |
||||
if (j->count == CALL_JITTER_MAX_FRAMES) { |
||||
j->head = (j->head + 1) % CALL_JITTER_MAX_FRAMES; j->count--; j->dropped++; |
||||
if (j->dropped == 1 || j->dropped % 50 == 0) |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: overflow capacity=15000ms dropped=%u", j->dropped); |
||||
} |
||||
int idx = (j->head + j->count) % CALL_JITTER_MAX_FRAMES; |
||||
std::memcpy(j->ring[idx], enc, (size_t)len); j->lens[idx] = len; j->count++; |
||||
} |
||||
|
||||
int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples) { |
||||
if (!j || !out || max_samples <= 0) return 0; |
||||
int count; |
||||
{ std::lock_guard<std::mutex> lock(j->mtx); count = j->count; } |
||||
// Готовый PCM учитываем в длительности исходной речи.
|
||||
double depth = count * CALL_JITTER_FRAME_MS |
||||
+ (j->st.numUnprocessedSamples() + j->st.numSamples() * j->tempo) * 1000.0 / kSampleRate; |
||||
int64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>( |
||||
std::chrono::steady_clock::now().time_since_epoch()).count(); |
||||
bool log_stats = j->window.observe(now_ms, (int)std::lround(depth)); |
||||
{ |
||||
std::lock_guard<std::mutex> lock(j->mtx); |
||||
j->range = {j->window.low, j->window.high, j->window.reserve}; |
||||
} |
||||
if (log_stats) |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: depth=%.0fms min=%dms max=%dms reserve=%dms tempo=%.2f", |
||||
depth, j->window.low, j->window.high, j->window.reserve, j->tempo); |
||||
if (!j->primed) { |
||||
if (depth < j->window.reserve) return 0; |
||||
j->primed = true; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: playback resumed depth=%.0fms reserve=%dms", depth, j->window.reserve); |
||||
} |
||||
j->ema_ms += 0.2 * (depth - j->ema_ms); |
||||
double target = 1.0 + (kMaxTempo - 1.0) * clamp_value((std::min(depth, j->ema_ms) - j->window.reserve - 15.0) / 925.0, 0.0, 1.0); |
||||
// Линейный предел 1.0→1.6 за 500мс PCM. Снижение следует за глубиной буфера с тем же пределом.
|
||||
double step = (kMaxTempo - 1.0) * max_samples * 1000.0 / (kSampleRate * kTempoRampMs); |
||||
j->tempo += clamp_value(target - j->tempo, -step, step); |
||||
j->st.setTempo(j->tempo); |
||||
bool catching = j->tempo > 1.05; |
||||
if (catching != j->catching_up) { |
||||
j->catching_up = catching; |
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_jitter: catch-up %s depth=%.0fms tempo=%.2f", |
||||
catching ? "started" : "finished", depth, j->tempo); |
||||
} |
||||
uint8_t enc[kMaxEncoded]; |
||||
while (j->st.numSamples() < (uint)max_samples) { |
||||
int len; |
||||
{ |
||||
std::lock_guard<std::mutex> lock(j->mtx); |
||||
if (!j->count) break; |
||||
len = j->lens[j->head]; std::memcpy(enc, j->ring[j->head], (size_t)len); |
||||
j->head = (j->head + 1) % CALL_JITTER_MAX_FRAMES; j->count--; |
||||
} |
||||
int n = j->decode(j->arg, enc, len, j->pcm, kFrameSamples); |
||||
if (n <= 0 || n > kFrameSamples) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "call_jitter: decode failed len=%d result=%d", len, n); |
||||
continue; |
||||
} |
||||
for (int i = 0; i < n; i++) j->input[i] = j->pcm[i] / 32768.0f; |
||||
j->st.putSamples(j->input, (uint)n); |
||||
} |
||||
j->output.resize((size_t)max_samples); |
||||
int n = (int)j->st.receiveSamples(j->output.data(), (uint)max_samples); |
||||
for (int i = 0; i < n; i++) out[i] = (int16_t)std::lrintf(clamp_value(j->output[i] * 32767.0f, -32768.0f, 32767.0f)); |
||||
int processing_ms = (int)std::lround((j->st.numUnprocessedSamples() + j->st.numSamples() * j->tempo) * 1000.0 / kSampleRate); |
||||
{ |
||||
std::lock_guard<std::mutex> lock(j->mtx); |
||||
j->processing_ms = processing_ms; j->tempo_x100 = (int)std::lround(j->tempo * 100); |
||||
if (n < max_samples) j->underruns++; |
||||
} |
||||
if (n < max_samples) { |
||||
j->primed = false; |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "call_jitter: waiting for refill pcm=%d/%d processing=%dms", n, max_samples, processing_ms); |
||||
} |
||||
return n; |
||||
} |
||||
|
||||
void call_jitter_get_stats(struct call_jitter* j, int* depth_ms, int* tempo_x100, |
||||
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range) { |
||||
if (!j) return; |
||||
std::lock_guard<std::mutex> lock(j->mtx); |
||||
if (depth_ms) *depth_ms = j->count * CALL_JITTER_FRAME_MS + j->processing_ms; |
||||
if (tempo_x100) *tempo_x100 = j->tempo_x100; |
||||
if (range) *range = j->range; |
||||
if (dropped) *dropped = j->dropped; |
||||
if (underruns) *underruns = j->underruns; |
||||
} |
||||
} |
||||
@ -0,0 +1,28 @@
|
||||
#ifndef CALL_JITTER_H |
||||
#define CALL_JITTER_H |
||||
|
||||
#include <stdint.h> |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
/* Звонок: FIFO Opus, адаптивный запас 60мс..max_reserve_ms (max-min за 10 секунд) и догон без сброса после паузы устройства.
|
||||
* push — сетевой поток, pull/decode/SoundTouch — единственный аудиопоток. |
||||
* destroy — после остановки обоих потребителей. get_stats — любой поток. */ |
||||
#define CALL_JITTER_FRAME_MS 20 |
||||
#define CALL_JITTER_MAX_FRAMES 750 /* 10с перебоя + 5с запаса на накопление и восстановление */ |
||||
typedef int (*call_jitter_decode_fn)(void*, const uint8_t*, int, int16_t*, int); |
||||
struct call_jitter_range { int min_ms, max_ms, reserve_ms; }; |
||||
struct call_jitter; |
||||
struct call_jitter* call_jitter_create(call_jitter_decode_fn decode, void* arg, int max_reserve_ms); |
||||
void call_jitter_destroy(struct call_jitter* j); |
||||
void call_jitter_push(struct call_jitter* j, const uint8_t* enc, int len); |
||||
int call_jitter_pull(struct call_jitter* j, int16_t* out, int max_samples); |
||||
void call_jitter_get_stats(struct call_jitter* j, int* depth_ms, int* tempo_x100, |
||||
uint32_t* dropped, uint32_t* underruns, struct call_jitter_range* range); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif |
||||
@ -0,0 +1,24 @@
|
||||
#pragma once |
||||
#include <algorithm> |
||||
#include <cstdint> |
||||
|
||||
// Десять секундных интервалов. Время передаёт аудиопоток из монотонных часов.
|
||||
struct call_jitter_window { |
||||
struct bucket { int64_t second = -1; int low = 0, high = 0; } buckets[10]; |
||||
int low = 0, high = 0, reserve = 60; |
||||
int max_reserve = 1000; |
||||
bool observe(int64_t now_ms, int depth) { |
||||
int64_t second = now_ms / 1000; |
||||
auto& b = buckets[second % 10]; |
||||
bool changed_second = b.second != second; |
||||
if (changed_second) { b.second = second; b.low = b.high = depth; } |
||||
else { b.low = std::min(b.low, depth); b.high = std::max(b.high, depth); } |
||||
low = high = depth; |
||||
for (const auto& item : buckets) { |
||||
if (item.second < 0 || item.second > second || second - item.second >= 10) continue; |
||||
low = std::min(low, item.low); high = std::max(high, item.high); |
||||
} |
||||
reserve = std::max(60, std::min(max_reserve, high - low)); |
||||
return changed_second; |
||||
} |
||||
}; |
||||
@ -0,0 +1,98 @@
|
||||
// call_proto.h — wire-формат и константы P2P аудио-звонка (svc_id ETCP_RT_ID_CALL 0x36)
|
||||
//
|
||||
// Приём/отправка через etcp_router. На проводе первый байт payload = subcmd.
|
||||
// Сигналинг идёт с ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT, медиа — без подписи
|
||||
// (уже внутри шифрованного канала etcp_router).
|
||||
#ifndef CALL_PROTO_H |
||||
#define CALL_PROTO_H |
||||
|
||||
#include <stdint.h> |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
/* ── подкоманды (первый байт payload после svc_id) ── */ |
||||
enum { |
||||
CALL_SUBCMD_INVITE = 0x01, /* {call_id:8, group_id:8, tracks:[]} */ |
||||
CALL_SUBCMD_RINGING = 0x02, /* {call_id:8} */ |
||||
CALL_SUBCMD_ACCEPT = 0x03, /* {call_id:8} */ |
||||
CALL_SUBCMD_DECLINE = 0x04, /* {call_id:8, reason:1} */ |
||||
CALL_SUBCMD_BUSY = 0x05, /* {call_id:8} */ |
||||
CALL_SUBCMD_CANCEL = 0x06, /* {call_id:8} */ |
||||
CALL_SUBCMD_HANGUP = 0x07, /* {call_id:8, reason:1} */ |
||||
CALL_SUBCMD_MEDIA = 0x10, /* {call_id:8, track_id:1, seq:2, ts_ms:4, payload[]} */ |
||||
}; |
||||
|
||||
/* ── треки и кодеки (модель расширяема под видео) ── */ |
||||
enum { CALL_TRACK_AUDIO = 0x01, CALL_TRACK_VIDEO = 0x02 }; |
||||
enum { CALL_CODEC_OPUS = 0x01 }; |
||||
|
||||
/* ── причины завершения (ENDED) ── */ |
||||
enum { |
||||
CALL_REASON_LOCAL_HANGUP = 0, |
||||
CALL_REASON_REMOTE_HANGUP = 1, |
||||
CALL_REASON_DECLINE = 2, |
||||
CALL_REASON_BUSY = 3, |
||||
CALL_REASON_CANCEL = 4, |
||||
CALL_REASON_NO_TRAFFIC = 5, |
||||
CALL_REASON_RING_TIMEOUT = 6, |
||||
CALL_REASON_CONNECT_FAIL = 7, |
||||
}; |
||||
|
||||
#pragma pack(push, 1) |
||||
|
||||
struct call_pkt_track { |
||||
uint8_t type; /* CALL_TRACK_AUDIO / CALL_TRACK_VIDEO */ |
||||
uint8_t codec; /* CALL_CODEC_OPUS */ |
||||
uint32_t sample_rate; |
||||
uint8_t channels; |
||||
uint32_t bitrate; |
||||
}; |
||||
|
||||
struct call_pkt_invite { |
||||
uint8_t subcmd; /* CALL_SUBCMD_INVITE */ |
||||
uint64_t call_id; |
||||
uint64_t group_id; |
||||
uint8_t num_tracks; |
||||
struct call_pkt_track tracks[1]; /* tracks[num_tracks] */ |
||||
}; |
||||
|
||||
struct call_pkt_ringing { uint8_t subcmd; uint64_t call_id; }; |
||||
struct call_pkt_accept { uint8_t subcmd; uint64_t call_id; }; |
||||
struct call_pkt_decline { uint8_t subcmd; uint64_t call_id; uint8_t reason; }; |
||||
struct call_pkt_busy { uint8_t subcmd; uint64_t call_id; }; |
||||
struct call_pkt_cancel { uint8_t subcmd; uint64_t call_id; }; |
||||
struct call_pkt_hangup { uint8_t subcmd; uint64_t call_id; uint8_t reason; }; |
||||
|
||||
struct call_pkt_media { |
||||
uint8_t subcmd; /* CALL_SUBCMD_MEDIA */ |
||||
uint64_t call_id; |
||||
uint8_t track_id; |
||||
uint16_t seq; |
||||
uint32_t ts_ms; |
||||
/* payload[] */ |
||||
}; |
||||
|
||||
#pragma pack(pop) |
||||
|
||||
#define CALL_MEDIA_HDR_SIZE sizeof(struct call_pkt_media) /* 16 */ |
||||
|
||||
/* ── параметры аудио (Opus 48kHz mono, 20ms) ── */ |
||||
#define CALL_OPUS_SAMPLE_RATE 48000 |
||||
#define CALL_OPUS_FRAME_MS 20 |
||||
#define CALL_OPUS_FRAME_SAMPLES (CALL_OPUS_SAMPLE_RATE * CALL_OPUS_FRAME_MS / 1000) /* 960 */ |
||||
|
||||
/* ── таймауты и ёмкости (timebase 0.1ms) ── */ |
||||
#define CALL_RING_TIMEOUT_TB 450000 /* 45с — вызывающий: нет ответа → CANCEL */ |
||||
#define CALL_INVITE_RETRY_TB 3000 /* 300мс — кодограмма INVITE (OUTGOING + RINGING) */ |
||||
#define CALL_RING_DEAD_TB 20000 /* 2с — вызываемый: нет кодограммы → вызов сорван */ |
||||
#define CALL_NO_ROUTE_MAX_STRIKES 20 /* 20 × 300мс ≈ 6с без маршрута → fail-fast CONNECT_FAIL */ |
||||
#define CALL_NO_TRAFFIC_TB 200000 /* 20с — нет входящих медиа → HANGUP */ |
||||
#define CALL_TEARDOWN_DELAY_TB 5000 /* 500мс — задержка разрыва conn_mgr после завершения */ |
||||
#define CALL_MAX_TX_FRAMES 750 /* 15с: 10с перебоя + 5с запаса */ |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif /* CALL_PROTO_H */ |
||||
@ -0,0 +1,165 @@
|
||||
// call_tones.c — генератор тонов звонка (48kHz mono int16).
|
||||
//
|
||||
// Глитч (сталл >500мс): 1000Гц, паттерн из doc/chat_call_arch.md §8 —
|
||||
// пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс) пауза(200мс) пи(200мс),
|
||||
// цикл 2с, повторяется пока длится сталл.
|
||||
// Завершение: 3 нисходящих тона 700/500/400 Гц × 150мс (один раз, 450мс).
|
||||
//
|
||||
// Генерация — синус с коротким fade in/out (без кликов). Только аудио-поток.
|
||||
|
||||
#include "call_tones.h" |
||||
|
||||
#include <math.h> |
||||
#include <string.h> |
||||
#include <stdlib.h> |
||||
|
||||
#define TONES_SAMPLE_RATE 48000 |
||||
#define TONES_AMPLITUDE 0.25f /* от полной шкалы int16 */ |
||||
#define TONES_FADE_SAMPLES (TONES_SAMPLE_RATE * 5 / 1000) /* 5мс */ |
||||
|
||||
enum tones_mode { TONES_NONE = 0, TONES_GLITCH, TONES_ENDED }; |
||||
|
||||
struct tone_seg { |
||||
int tone; /* 1 = звук, 0 = тишина */ |
||||
int dur_ms; |
||||
int freq_hz; |
||||
}; |
||||
|
||||
static const struct tone_seg GLITCH_SEGS[] = { |
||||
{1, 150, 1000}, {0, 100, 0}, {1, 150, 1000}, {0, 600, 0}, |
||||
{1, 200, 1000}, {0, 200, 0}, {1, 200, 1000}, {0, 400, 0}, |
||||
}; |
||||
#define GLITCH_SEG_COUNT ((int)(sizeof(GLITCH_SEGS) / sizeof(GLITCH_SEGS[0]))) |
||||
|
||||
static const struct tone_seg ENDED_SEGS[] = { |
||||
{1, 150, 700}, {1, 150, 500}, {1, 150, 400}, |
||||
}; |
||||
#define ENDED_SEG_COUNT ((int)(sizeof(ENDED_SEGS) / sizeof(ENDED_SEGS[0]))) |
||||
|
||||
struct call_tones { |
||||
enum tones_mode mode; |
||||
const struct tone_seg* segs; |
||||
int seg_count; |
||||
int seg_idx; |
||||
int seg_pos; /* сэмплов отыграно в текущем сегменте */ |
||||
int ended_done; |
||||
double phase; /* фаза синуса (радианы) */ |
||||
}; |
||||
|
||||
struct call_tones* call_tones_create(void) { |
||||
struct call_tones* t = (struct call_tones*)calloc(1, sizeof(*t)); |
||||
if (!t) return NULL; |
||||
t->mode = TONES_NONE; |
||||
t->segs = NULL; |
||||
t->seg_count = 0; |
||||
t->seg_idx = 0; |
||||
t->seg_pos = 0; |
||||
t->ended_done = 0; |
||||
t->phase = 0.0; |
||||
return t; |
||||
} |
||||
|
||||
void call_tones_destroy(struct call_tones* t) { |
||||
free(t); |
||||
} |
||||
|
||||
void call_tones_reset(struct call_tones* t) { |
||||
if (!t) return; |
||||
t->mode = TONES_NONE; |
||||
t->segs = NULL; |
||||
t->seg_count = 0; |
||||
t->seg_idx = 0; |
||||
t->seg_pos = 0; |
||||
t->ended_done = 0; |
||||
t->phase = 0.0; |
||||
} |
||||
|
||||
void call_tones_glitch_start(struct call_tones* t) { |
||||
if (!t || t->mode == TONES_GLITCH) return; |
||||
t->mode = TONES_GLITCH; |
||||
t->segs = GLITCH_SEGS; |
||||
t->seg_count = GLITCH_SEG_COUNT; |
||||
t->seg_idx = 0; |
||||
t->seg_pos = 0; |
||||
t->phase = 0.0; |
||||
} |
||||
|
||||
void call_tones_glitch_stop(struct call_tones* t) { |
||||
if (!t || t->mode != TONES_GLITCH) return; |
||||
t->mode = TONES_NONE; |
||||
t->segs = NULL; |
||||
t->seg_count = 0; |
||||
t->seg_idx = 0; |
||||
t->seg_pos = 0; |
||||
t->phase = 0.0; |
||||
} |
||||
|
||||
void call_tones_ended_start(struct call_tones* t) { |
||||
if (!t) return; |
||||
t->mode = TONES_ENDED; |
||||
t->segs = ENDED_SEGS; |
||||
t->seg_count = ENDED_SEG_COUNT; |
||||
t->seg_idx = 0; |
||||
t->seg_pos = 0; |
||||
t->ended_done = 0; |
||||
t->phase = 0.0; |
||||
} |
||||
|
||||
int call_tones_ended_pending(struct call_tones* t) { |
||||
return t && t->mode == TONES_ENDED && !t->ended_done; |
||||
} |
||||
|
||||
/* Перейти к следующему сегменту (или завершить тон). */ |
||||
static void tones_advance(struct call_tones* t) { |
||||
t->seg_idx++; |
||||
t->seg_pos = 0; |
||||
t->phase = 0.0; |
||||
if (t->seg_idx >= t->seg_count) { |
||||
if (t->mode == TONES_GLITCH) { |
||||
t->seg_idx = 0; /* цикл глитча */ |
||||
} else if (t->mode == TONES_ENDED) { |
||||
t->ended_done = 1; |
||||
t->mode = TONES_NONE; |
||||
t->segs = NULL; |
||||
} |
||||
} |
||||
} |
||||
|
||||
int call_tones_render(struct call_tones* t, int16_t* out, int max_samples) { |
||||
if (!t || !out || max_samples <= 0) return 0; |
||||
|
||||
int written = 0; |
||||
while (written < max_samples) { |
||||
if (t->mode == TONES_NONE) { |
||||
out[written++] = 0; /* тишина до конца запроса */ |
||||
continue; |
||||
} |
||||
|
||||
const struct tone_seg* s = &t->segs[t->seg_idx]; |
||||
int seg_len = s->dur_ms * TONES_SAMPLE_RATE / 1000; |
||||
int remain = seg_len - t->seg_pos; |
||||
int n = max_samples - written; |
||||
if (n > remain) n = remain; |
||||
|
||||
for (int i = 0; i < n; i++) { |
||||
int pos = t->seg_pos + i; |
||||
float v = 0.0f; |
||||
if (s->tone) { |
||||
v = (float)sin(t->phase) * TONES_AMPLITUDE * 32767.0f; |
||||
t->phase += 2.0 * M_PI * (double)s->freq_hz / TONES_SAMPLE_RATE; |
||||
/* fade in/out по краям сегмента (без кликов) */ |
||||
int fade = pos < TONES_FADE_SAMPLES ? pos |
||||
: (seg_len - pos - 1) < TONES_FADE_SAMPLES ? (seg_len - pos - 1) |
||||
: TONES_FADE_SAMPLES; |
||||
if (fade < TONES_FADE_SAMPLES) |
||||
v *= (float)fade / (float)TONES_FADE_SAMPLES; |
||||
} |
||||
int iv = (int)v; |
||||
out[written + i] = (int16_t)(iv > 32767 ? 32767 : (iv < -32768 ? -32768 : iv)); |
||||
} |
||||
written += n; |
||||
t->seg_pos += n; |
||||
if (t->seg_pos >= seg_len) tones_advance(t); |
||||
} |
||||
return written; |
||||
} |
||||
@ -0,0 +1,32 @@
|
||||
#ifndef CALL_TONES_H |
||||
#define CALL_TONES_H |
||||
|
||||
// call_tones.h — генератор тонов звонка (48kHz mono int16): глитч при сталле
|
||||
// и тон завершения. Используется только аудио-потоком (single-writer).
|
||||
|
||||
#include <stdint.h> |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
struct call_tones; |
||||
|
||||
struct call_tones* call_tones_create(void); |
||||
void call_tones_destroy(struct call_tones* t); |
||||
void call_tones_reset(struct call_tones* t); /* в тишину */ |
||||
|
||||
void call_tones_glitch_start(struct call_tones* t); /* запустить/продолжить цикл глитча */ |
||||
void call_tones_glitch_stop(struct call_tones* t); /* прекратить глитч (тишина) */ |
||||
|
||||
void call_tones_ended_start(struct call_tones* t); /* 3 нисходящих тона, один раз */ |
||||
int call_tones_ended_pending(struct call_tones* t); /* 1 = тон завершения ещё звучит */ |
||||
|
||||
/* Выдать до max_samples сэмплов (тон или тишина). Возвращает записанное число. */ |
||||
int call_tones_render(struct call_tones* t, int16_t* out, int max_samples); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
|
||||
#endif /* CALL_TONES_H */ |
||||
@ -0,0 +1,54 @@
|
||||
# voice_sources.cmake — голосовой стек звонка (единый для desktop/Android/headless). |
||||
# |
||||
# SoundTouch (time-stretch) + call_jitter + radio_jitter (кодированные кадры + decode-on-pull) |
||||
# + call_audio (Opus/тоны) + call_tones. C++ внутри, наружу — C-интерфейс |
||||
# (call_audio.h). Экспортирует UTUN_VOICE_SOURCES и UTUN_VOICE_INCLUDE_DIRS. |
||||
# |
||||
# Использование: |
||||
# set(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../lib) |
||||
# set(CALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/call) |
||||
# include(${CALL_DIR}/voice_sources.cmake) |
||||
# utun_setup_voice("${LIB_DIR}" "${CALL_DIR}") |
||||
# add_library(utun_voice STATIC ${UTUN_VOICE_SOURCES}) |
||||
# target_include_directories(utun_voice PUBLIC ${UTUN_VOICE_INCLUDE_DIRS}) |
||||
# target_link_libraries(utun_voice PUBLIC opus) # opus_codec |
||||
|
||||
function(utun_setup_voice LIB_DIR CALL_DIR) |
||||
set(_st_dir "${LIB_DIR}/soundtouch-master") |
||||
set(_src_dir "${CALL_DIR}/..") |
||||
set(UTUN_VOICE_SOURCES |
||||
${CALL_DIR}/call_audio.c |
||||
${CALL_DIR}/call_tones.c |
||||
${CALL_DIR}/call_jitter.cpp |
||||
${_src_dir}/radio/radio_jitter.cpp |
||||
${_src_dir}/radio/radio_audio.c |
||||
${_st_dir}/source/SoundTouch/SoundTouch.cpp |
||||
${_st_dir}/source/SoundTouch/TDStretch.cpp |
||||
${_st_dir}/source/SoundTouch/RateTransposer.cpp |
||||
${_st_dir}/source/SoundTouch/FIFOSampleBuffer.cpp |
||||
${_st_dir}/source/SoundTouch/AAFilter.cpp |
||||
${_st_dir}/source/SoundTouch/FIRFilter.cpp |
||||
${_st_dir}/source/SoundTouch/InterpolateLinear.cpp |
||||
${_st_dir}/source/SoundTouch/InterpolateCubic.cpp |
||||
${_st_dir}/source/SoundTouch/InterpolateShannon.cpp |
||||
${_st_dir}/source/SoundTouch/cpu_detect_x86.cpp |
||||
${_st_dir}/source/SoundTouch/mmx_optimized.cpp |
||||
${_st_dir}/source/SoundTouch/sse_optimized.cpp |
||||
PARENT_SCOPE |
||||
) |
||||
set(UTUN_VOICE_INCLUDE_DIRS |
||||
${CALL_DIR} |
||||
${_src_dir} |
||||
${_src_dir}/transport_layer |
||||
${_src_dir}/routing_layer |
||||
${_src_dir}/chat |
||||
${_src_dir}/proxy |
||||
${_src_dir}/media_delivery |
||||
${_src_dir}/media_async |
||||
${LIB_DIR} |
||||
${LIB_DIR}/libopus/include |
||||
${_st_dir}/include |
||||
${_st_dir}/include/soundtouch |
||||
PARENT_SCOPE |
||||
) |
||||
endfunction() |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue