Browse Source
- src/dm/: dm_core/dm_crypto/dm_mailbox — прямой обмен сообщениями (DM) - lib/soundtouch-master/: vendored SoundTouch (изменение темпа/высоты аудио) - doc/dm_arch.md, doc/chat_call_arch.md: архитектура DM и звонков - tests: test_dm.c, test_dm_e2e.c - chat: рефакторинг chat_* модулей, member_sync, invite_build - chatgui/chatgui-android: синхронизация с новым API чата - lib/u_async, debug_config, utun_instance: сопутствующие измененияv2
123 changed files with 14095 additions and 1270 deletions
@ -0,0 +1,199 @@
|
||||
# P2P аудио-звонок пользователю — архитектура |
||||
|
||||
Статус: спроектировано, ждёт реализации. |
||||
|
||||
## 1. Область |
||||
|
||||
Голос 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)** — модуль |
||||
не привязан к типу группы: он оперирует `group_id` + `peer_node_id` (для DM `group_id` — |
||||
DM-группа, маршрутизация через `etcp_router` идентична). Протокол расширяем под видео |
||||
(track-модель), реализуется только аудио. Работает desktop↔desktop и desktop↔android |
||||
(обе стороны компилируют общий стек `etcp_router` / `topo_group` (BGP) / `conn_mgr`). |
||||
|
||||
Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE → |
||||
установка соединения (conn_mgr) → аудиопоток → HANGUP. |
||||
|
||||
## 2. Разбиение файлов (отдельный каталог `src/call/`) |
||||
|
||||
| Файл | Язык | Сборки | Назначение | |
||||
|---|---|---|---| |
||||
| `src/call/call_proto.h` | C | везде | wire-формат, subcmd, svc_id `ETCP_RT_ID_CALL 0x34` | |
||||
| `src/call/call.h` / `call.c` | C | везде (autotools + GUI) | сигналинг, реестр сессий, таймеры, watchdog (20с по приёму), релей медиа с backpressure, счётчики | |
||||
| `src/call/call_audio.h/c` | C | только GUI-сборки | Opus encode/decode, фрейминг, pull-обвязка с тонами; регистрация через `call_audio_ops` | |
||||
| `src/call/call_tones.h/c` | C | только GUI-сборки | генератор тонов (glitch/ended), синус | |
||||
| `src/call/audio_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API | |
||||
|
||||
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c` |
||||
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI. |
||||
|
||||
Новая категория логирования: `DEBUG_CATEGORY_CALL = 31` (добавить в `debug_config.h`, |
||||
`DEBUG_CATEGORY_COUNT` → 32). |
||||
|
||||
## 3. Протокол (svc_id 0x34, первый байт payload = subcmd) |
||||
|
||||
Формат приёма (как media_delivery, одноуровневый): `entry->dgram[0]=svc_id`, |
||||
`entry->dgram[1]=subcmd`, `entry->dgram[2..]=payload`. |
||||
|
||||
``` |
||||
CALL_INVITE 0x01 {call_id:8, group_id:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]} |
||||
CALL_RINGING 0x02 {call_id:8} |
||||
CALL_ACCEPT 0x03 {call_id:8, agreed_tracks} |
||||
CALL_DECLINE 0x04 {call_id:8, reason:1} |
||||
CALL_BUSY 0x05 {call_id:8} |
||||
CALL_CANCEL 0x06 {call_id:8} |
||||
CALL_HANGUP 0x07 {call_id:8, reason:1} |
||||
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, payload} |
||||
``` |
||||
|
||||
- `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах. |
||||
- `group_id` — CHAT-группа или DM-группа (общая маршрутизация). |
||||
- Сигналинг: `etcp_route_send(..., ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)`. |
||||
- Медиа: без подписи (уже внутри шифрованного канала etcp_router). |
||||
- `CALL_MEDIA` — общий контейнер; `track_id` оставляет место видео (`type=VIDEO`). |
||||
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с. |
||||
|
||||
## 4. Машина состояний |
||||
|
||||
``` |
||||
Caller: IDLE → OUTGOING(INVITE) → RINGING(RINGING) → CONNECTING(ACCEPT, conn_mgr_open) → ACTIVE → ENDED |
||||
Callee: IDLE → INCOMING(INVITE) → RINGING(sent RINGING) → ACCEPTING(accept, conn_mgr_open) → ACTIVE → ENDED |
||||
``` |
||||
|
||||
`ENDED` reasons: `LOCAL_HANGUP / REMOTE_HANGUP / DECLINE / BUSY / CANCEL / |
||||
NO_TRAFFIC / RING_TIMEOUT / CONNECT_FAIL`. |
||||
|
||||
Таймауты: |
||||
- ring-timeout 45с (нет ответа → CANCEL); |
||||
- connect: `conn_mgr` `CONN_EVENT_TIMEOUT` → fail; |
||||
- **NO_TRAFFIC 20с** — нет входящих медиа-кадров → авто HANGUP. |
||||
|
||||
Онлайн-проверка: `topo_node_find_by_id(group, peer) == NULL` → «не в сети» (event). |
||||
|
||||
Соединение: на ACCEPT обе стороны `conn_mgr_open(inst, group_id, peer, cb, ...)`; |
||||
`CONN_EVENT_UP` → ACTIVE. Медиа идёт `etcp_route_send` (после подъёма прямого линка |
||||
роутер идёт напрямую — RTT минимален). |
||||
|
||||
## 5. Отправка медиа — backpressure через локальный буфер |
||||
|
||||
Требование: не дропать аудио при переполнении роутера — держим в локальном буфере |
||||
отправки, дренируем при появлении места. |
||||
|
||||
Механика (`etcp_router` уже предоставляет): |
||||
- `etcp_route_send(force=0)` возвращает `-1`, когда `send_q` роутера полон |
||||
(`count >= ROUTER_MAX_SEND_Q_PACKETS`). |
||||
- `etcp_router_send_q_has_room(...)` — есть ли место (порог `ROUTER_MAX_SEND_Q_PACKETS-1`). |
||||
- `etcp_router_on_send_ready(..., &waiter, drain_cb, arg)` — одноразовый waiter: |
||||
колбэк вызывается, когда `send_q->count <= threshold` (освободилось место). |
||||
|
||||
В сессии: |
||||
- `tx_q` — локальная FIFO очередь закодированных кадров (ll_entry с dgram), ёмкостью |
||||
~1с (≈50 кадров). При переполнении — дроп старейшего + счётчик `c_tx_dropped`. |
||||
- Алгоритм (`call_tx_flush`): пока `tx_q` не пуст: |
||||
- если `send_q_has_room` → `queue_data_get` + `etcp_route_send(force=0)`; |
||||
- иначе — если waiter не зарегистрирован → `etcp_router_on_send_ready(... drain_cb)` и выход. |
||||
- `drain_cb(q, arg)` → снова `call_tx_flush(session)` (re-arm при необходимости). |
||||
- Весь код — в uasync-потоке (single-threaded, реентерабельность как в |
||||
`media_delivery.c:stream_send_chunk_cb`). |
||||
|
||||
## 6. Thread-модель (single-writer) |
||||
|
||||
| Объект | Владелец | |
||||
|---|---| |
||||
| `call_session` (состояние, encoder, decoder, tx_q, таймеры) | uasync-поток | |
||||
| Opus encoder (send) | аудио-поток (encode) | |
||||
| Opus decoder (recv) | uasync-поток (`decode → ajb_push`) | |
||||
| `ajb` push | uasync-поток | |
||||
| `ajb` pull + SoundTouch | аудио-поток | |
||||
| `call_tones` | аудио-поток | |
||||
|
||||
Поток данных: |
||||
- TX: аудио-поток кодирует → `call_send_encoded(call_id, opus, len)` → `uasync_post` → |
||||
uasync: `tx_q` + `call_tx_flush` (backpressure). |
||||
- RX: uasync принимает `CALL_MEDIA` → decode → `ajb_push`; аудио-поток `call_pull_pcm` → `ajb_pull` (+ тоны). |
||||
|
||||
`ajb` = mutex-кольцо PCM (гранулярность 20ms/50 ops в сек — мьютекс бесплатен); |
||||
SoundTouch инстанс заперт на pull-стороне. |
||||
|
||||
**Контракт жизненного цикла:** GUI стартует аудио-устройство по `CALL_ACCEPTED`, |
||||
останавливает по `CALL_ENDED` и только после остановки (join аудио-потока) вызывает |
||||
`call_release(call_id)` (post в uasync) → uasync освобождает session/encoder/decoder/jb. |
||||
Пока устройство работает — `call_pull_pcm`/`call_send_encoded` не гоняются с освобождением. |
||||
|
||||
## 7. Адаптивный джиттер-буфер (SoundTouch) |
||||
|
||||
Буфер ≤ **1000 мс**, target playout ~60 мс; при росте буфера плавно tempo → **1.6x**. |
||||
SoundTouch: `USE_QUICKSEEK=1, SEQUENCE_MS=20, SEEKWINDOW_MS=10, OVERLAP_MS=4`. |
||||
API: `ajb_create / ajb_push / ajb_pull / ajb_stats / ajb_destroy`. |
||||
|
||||
## 8. Тоны и диагностика |
||||
|
||||
- **Сталл >500мс** (нет входящих медиа) → в `call_pull_pcm` микшируется |
||||
`CALL_TONE_GLITCH`: `пи-пи (2×150мс, gap 100мс) … тишина до 1.0с … пи(200мс) |
||||
пауза(200мс) пи(200мс)`, 1000 Гц, цикл пока длится сталл. |
||||
- **Завершение звонка → всегда** `CALL_TONE_ENDED`: 3 нисходящих 700/500/400 Гц по 150 мс. |
||||
- Счётчики: tx/rx кадры, `c_tx_dropped`/`c_rx_dropped`, дропы буфера, сталлы, |
||||
tempo-события, причины завершения. Логи `DEBUG_CATEGORY_CALL` (+стата раз в 1с). |
||||
|
||||
## 9. GUI-события (extend `chat_event.h` + `gui_bridge.h` + android) |
||||
|
||||
``` |
||||
CHAT_EVT_CALL_INCOMING 32 [ch_id_len][ch_id][caller_node_id:8][call_id:8] |
||||
CHAT_EVT_CALL_RINGING 33 [call_id:8] |
||||
CHAT_EVT_CALL_ACCEPTED 34 [call_id:8] |
||||
CHAT_EVT_CALL_ENDED 35 [call_id:8][reason:1] |
||||
CHAT_EVT_CALL_DECLINED 36 [call_id:8][reason:1] |
||||
CHAT_EVT_CALL_STATS 37 [call_id:8][rtt_ms:2][buffer_ms:2][tempo_x100:2] |
||||
CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var] |
||||
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры) |
||||
``` |
||||
|
||||
## 10. Публичный API (call.h) |
||||
|
||||
``` |
||||
int call_start(ch_id, peer_node_id, out_call_id); // caller (post в uasync) |
||||
void call_accept(call_id); // callee |
||||
void call_decline(call_id); |
||||
void call_hangup(call_id); |
||||
int call_is_online(ch_id, peer_node_id); // BGP-проверка для UI |
||||
// аудио-поток (общий процессинг в C-ядре): |
||||
int call_encode_pcm(call_id, pcm, n, opus_out, cap); // single-writer: audio thread |
||||
int call_send_encoded(call_id, opus, len); // post → uasync → tx_q + flush |
||||
int call_pull_pcm(call_id, pcm_out, max); // single-writer: audio thread (ajb+tones) |
||||
void call_release(call_id); // post → uasync: освободить аудио-объекты |
||||
``` |
||||
|
||||
`call_init / call_destroy` — вызываются из `chat_core_init / chat_core_destroy`. |
||||
|
||||
## 11. Платформенный I/O |
||||
|
||||
- **Desktop:** `tools/chatgui/src/call_audio_engine.cpp` — miniaudio full-duplex |
||||
(capture → `call_encode_pcm`, `call_pull_pcm` → playback) + окно звонка (UI) |
||||
+ события в `gui_bridge_impl.cpp`. |
||||
- **Android:** JNI `nativeCall*` + Kotlin `AudioRecord`/`AudioTrack`. |
||||
**Переключение источника:** `AudioManager` (API31+ `setCommunicationDevice` + |
||||
`availableCommunicationDevices`; fallback `setSpeakerphoneOn` + `startBluetoothSco`), |
||||
детект гарнитуры (`ACTION_HEADSET_PLUG`, BT-профиль, `ACTION_SCO_AUDIO_STATE_UPDATED`), |
||||
кнопка-цикл трубка/громкая/гарнитура, авто-выбор гарнитуры. `nativeCallSetAudioRoute(route)`. |
||||
|
||||
## 12. Сборка |
||||
|
||||
- `src/Makefile.am` + `tests/Makefile.am`: `call.c` (C, без `.cpp`). `src/call/` в include path. |
||||
- `tools/chatgui/libutun/CMakeLists.txt`: +`call_audio.c`, `call_tones.c`, |
||||
`audio_jitter.cpp`, soundtouch sources (AAFilter, FIFOSampleBuffer, FIRFilter, |
||||
InterpolateCubic/Linear/Shannon, RateTransposer, SoundTouch, TDStretch, cpu_detect_x86, |
||||
mmx_optimized, sse_optimized) как C++. `call.c` попадёт через GLOB `src/*.c`. |
||||
- `tools/chatgui-android/libutun_lite/CMakeLists.txt` + `utun_sources.cmake`: |
||||
`LANGUAGES C CXX`, те же файлы. |
||||
- `src/transport_layer/etcp_api.h`: `#define ETCP_RT_ID_CALL 0x34`. |
||||
- `lib/debug_config.h`: `DEBUG_CATEGORY_CALL 31`, `DEBUG_CATEGORY_COUNT` → 32. |
||||
|
||||
## 13. Порядок задач |
||||
|
||||
1. `call_proto.h` + svc_id + `DEBUG_CATEGORY_CALL`. |
||||
2. `call.c` (машина состояний, backpressure-media, watchdog, события, API) + `chat_core` хуки. |
||||
3. `audio_jitter.cpp` (SoundTouch) + `test_audio_jitter.c`. |
||||
4. `call_tones.c` + `call_audio.c` (Opus + фрейминг + тон-обвязка). |
||||
5. Desktop: `call_audio_engine.cpp` + UI + события. |
||||
6. Android: JNI + Kotlin AudioRecord/Track + переключение источника + UI. |
||||
7. Сборка (Makefile.am / CMake обе). |
||||
8. `test_call.c` (два инстанса), интеграция desktop↔desktop и desktop↔android. |
||||
@ -0,0 +1,93 @@
|
||||
# DM — прямой p2p чат между двумя пользователями группы |
||||
|
||||
## 1. Цель |
||||
|
||||
Возможность начать личный чат с любым пользователем доступных групп. |
||||
Пока target offline сообщения временно хранятся на storage-узлах. |
||||
|
||||
## 2. Ключевые решения |
||||
|
||||
- Отдельная DM-подсистема (НЕ канал: без TOPO_GROUP/member_sync/merkle). |
||||
- Идентичность детерминированная: одна ветка на пару, согласование без handshake. |
||||
- E2E-шифрование содержимого сразу. |
||||
- Offline-хранение: dm_mailbox на storage-узлах с флагом dm_storage. |
||||
- Доставка: fallback — сперва проверка наличия узла в BGP-группе; доступен → |
||||
прямой send; нет связи → storage. |
||||
|
||||
## 3. Идентичность и ключи (детерминированные) |
||||
|
||||
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63] |
||||
shared = X25519(my_x25519_priv, peer_x25519_pub) |
||||
content_key = SHA256(shared || "utun_dm_content") |
||||
|
||||
Обе стороны выводят одинаковые значения из своего privkey и pubkey пира |
||||
(известен из общей группы). Смена x25519 у пира = новая ветка. |
||||
|
||||
## 4. Модель данных (SQLite, общий chats.db) |
||||
|
||||
dm_conversations(conv_id TEXT PK, peer_node_id, peer_x25519 BLOB, peer_ed25519 BLOB, |
||||
peer_name TEXT, last_out_seq INT, last_in_seq INT, created_at INT, last_ts INT) |
||||
dm_messages(conv_id TEXT, dir INT, seq INT, ts INT, author INT, ct TEXT, |
||||
data BLOB, sig BLOB, PRIMARY KEY(conv_id,dir,seq)) |
||||
dm_mail(recipient INT, sender INT, conv_id TEXT, seq INT, ts INT, data BLOB, sig BLOB, |
||||
ttl INT, PRIMARY KEY(recipient,sender,seq)) |
||||
|
||||
Два независимых направленных потока (A→B, B→A) с монотонным seq. |
||||
Без chain-хеша: для 2 пиров достаточно seq + catch-up по диапазону. |
||||
|
||||
## 5. Доставка |
||||
|
||||
1. Проверка доступности: peer присутствует в BGP-группе (общая группа). |
||||
2. Доступен → прямой send (сервис ETCP_RT_ID_DM = 0x34), ждём DM_ACK{conv_id,seq}. |
||||
Нет ACK за таймаут → fallback в mailbox. |
||||
3. Нет связи → MAIL_PUT на 1..N storage-узлов. |
||||
4. Catch-up при connect: DM_HELLO{conv_id,last_out_seq,last_in_seq} → пир досылает |
||||
(in_seq+1 .. out_seq). |
||||
5. Dedup: вставка только при seq > last_in_seq. |
||||
|
||||
## 6. E2E-шифрование |
||||
|
||||
data_enc = nonce(13B, счётчик по seq) || AES-CCM_ct || tag |
||||
|
||||
secure_channel уже умеет AES-CCM. Storage/relay видят только шифротекст. |
||||
|
||||
## 7. Storage-узлы (dm_storage) |
||||
|
||||
- Новый флаг dm_storage=yes в adm_tags (блок B владельца), распространяется |
||||
существующим member_sync без нового синка. |
||||
- DM читает adm_tags мемберов общих групп (peers_<ch>) и кэширует storage-узлы. |
||||
- Протокол mailbox (сервис ETCP_RT_ID_DM_MAILBOX = 0x35): |
||||
|
||||
MAIL_PUT {recipient,sender,conv_id,seq,ts,data,sig} |
||||
MAIL_PULL {recipient, per-sender cursor} |
||||
MAIL_ACK {up_to_seq} |
||||
|
||||
TTL 7 дней + лимит объёма, dedup по (recipient,sender,seq). |
||||
|
||||
## 8. Старт и anti-spam |
||||
|
||||
- dm_start(peer): создать беседу (вывести conv_id/ключи), обеспечить прямое |
||||
соединение (переиспользовать chat_sync_connect_node/node_conn_direct). |
||||
- Пир автопринимает на первом DM_HELLO/сообщении, только если отправитель в |
||||
общей группе. |
||||
|
||||
## 9. Файлы и интеграция |
||||
|
||||
| Файл | Назначение | |
||||
|------|-----------| |
||||
| src/dm/dm_core.c/h | беседы, send/recv, таблицы, bind 0x34, conn-события, fallback | |
||||
| src/dm/dm_crypto.c/h | derive + AES-CCM | |
||||
| src/dm/dm_mailbox.c/h | storage-узел, push/pull/ack, bind 0x35 | |
||||
|
||||
Плюс: |
||||
|
||||
- src/chat/chat_member.c — бит CHAT_MEMBER_FLAG_DM_STORAGE + парсинг dm_storage. |
||||
- utun_instance.c — dm_core_init/destroy. |
||||
- chat_core — API dm_start/send/list + события CHAT_EVT_DM_*. |
||||
- headless CLI — dm list/send/messages. |
||||
- src/Makefile.am — новые источники. |
||||
|
||||
## 10. Сервисы ETCP |
||||
|
||||
ETCP_RT_ID_DM = 0x34 |
||||
ETCP_RT_ID_DM_MAILBOX = 0x35 |
||||
@ -1,6 +1,137 @@
|
||||
# Задачи по проекту |
||||
|
||||
Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ. |
||||
|
||||
## Текущая задача |
||||
[ ] **DM: прямой p2p чат с любым пользователем группы** — отдельная DM-подсистема. |
||||
ТЗ и архитектура: `/doc/dm_arch.md`. Статус: реализовано (dm_core/dm_mailbox/dm_crypto), |
||||
тесты `test_dm` (крипто) и `test_dm_e2e` (интеграция). Остались задачи ниже. |
||||
|
||||
[ ] **Звонок (P2P аудио)** — голос 1:1, из CHAT-группы или p2p-чата (DM). Отдельный |
||||
каталог `src/call/`. Сигналинг + conn_mgr + аудиопоток (Opus) с backpressure |
||||
(локальный буфер отправки при переполнении роутера) + адаптивный джиттер-буфер |
||||
(SoundTouch, ускорение до 1.6x) + тоны (глитч/завершение) + watchdog 20с. |
||||
Android: переключение источника (трубка/громкая/гарнитура). |
||||
Архитектура: `/doc/chat_call_arch.md`. Статус: спроектировано, ждёт реализации. |
||||
|
||||
## Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e) |
||||
|
||||
Сделано (глобальные синглтоны переведены на `UTUN_INSTANCE`): |
||||
- `chat_setting` → `inst->chat_settings` (`struct chat_setting_state`, state-level API для парсера конфига + instance-level для runtime). |
||||
- `chat_event` → `inst->chat_event_handler` (обработчик теперь получает `inst`). |
||||
- `chat_core` (`g_cc`) → `inst->chat_core` (`struct chat_core_ctx*`). |
||||
- `chat_sync` (`g_cs`) → `inst->chat_sync`. |
||||
- `member_sync` callback-списки (`g_props_cbks`/`g_apply_cbks`) → в `chat_core_ctx`. |
||||
- `chat_join` (`g_keys`) → в `chat_core_ctx`. |
||||
- `chat_msg` media-счётчики/backfill → в `chat_core_ctx`. |
||||
- `dm_core` (`g_dm`) → `inst->dm`; `dm_mailbox` (`g_mb`) → `inst->dm_mailbox`. |
||||
- `chat_headless_control` (`g_hc`) → `inst->headless`. |
||||
- `chat_whisper` — сигнатуры `init/available/trigger` принимают `inst`. |
||||
- API протащено через `inst` (chat_core_*/dm_*), обновлены call-сайты в src/ (headless, topo, media_delivery, auto_socket, config_parser). |
||||
- `test_dm_e2e.c` переписан на однопоточный: один `uasync`, три `utun_instance` (A,B,C), master state-machine, без fork. Фаза 2 (B offline) — `utun_instance_destroy(B1)` + пересоздание B2. |
||||
- Попутно исправлен 1-байтовый overflow в `dm_mailbox.c: mb_route_send` (`u_malloc(1+body_len)` → `1+1+body_len`). |
||||
|
||||
Статус сборки: `make -C src` (libutun.a + бинарник utun) и `make -C tests` — OK. |
||||
|
||||
### Осталось (по порядку) |
||||
|
||||
[+] **test_dm_e2e: зависание в utun_instance_destroy(B1)** (фаза P_B_OFFLINE) — сделано. |
||||
Причин было несколько (не одна): |
||||
1. `lib/u_async.c`: если ближайший таймер уже истёк на входе в `uasync_poll`, |
||||
`get_next_timeout` возвращал {0,0}, `timeout_ms=-1` → `epoll_wait(-1)` блокировался |
||||
навсегда, а `process_timeouts` (вызывается только после epoll_wait) не запускался. |
||||
Фикс: `if (timeout_ms == -1 && heap не пуст) timeout_ms = 0`. |
||||
2. `routing_layer/etcp_router.c`: `router_send_conn` с group_id=0 (глобальная |
||||
маршрутизация DM/mailbox) не находил маршрут (`topo_groups_find(0)==NULL`). |
||||
Фикс: fallback на `instance_find_conn(inst, remote)` при group==NULL. |
||||
3. `dm/dm_core.c`: `dm_on_conn_status` PULL-ил только беседы с подключившимся пиром; |
||||
B2 подключается к storage (C), а беседа — с A. Фикс: на подъём любого соединения |
||||
PULL для всех бесед, чей пир не подключён напрямую. |
||||
4. `dm/dm_mailbox.c`: не было ACK после PULL_RESP → storage не чистил dm_mail. |
||||
Фикс: PULL_RESP теперь несёт sender, получатель шлёт MB_SUBCMD_ACK. |
||||
5. `utun_instance.c`: `dm_core_init` ставил deliver-cb до `dm_mailbox_init` → |
||||
mailbox не находился, deliver-cb не регистрировался. Фикс: mailbox init раньше core. |
||||
`test_dm_e2e` проходит (несколько прогонов), `test_dm` тоже. |
||||
|
||||
[+] **chatgui-android: переезд на per-instance API** — сделано. |
||||
`instance_lite.c/h` (+`instance_lite_get_instance()`), `jni_bridge.c`, `standby.c`, |
||||
`headless_control.c`, `voice_recorder.c`, `attachment_sender.c`, `video_sender.c`, |
||||
`photo_sender.c` — все call-сайты переведены на `inst`. `libutun_lite` компилируется, |
||||
`jni_bridge.c` — синтакс-чисто. (headless-линковка сломана ПРЕДЫДУЩЕ — instance_lite.c |
||||
ссылается на jni_bridge-функции, не включённые в headless-сборку; отдельная задача.) |
||||
|
||||
[+] **test_chat_join: segfault** (регресс рефакторинга) — сделано. |
||||
`chat_join` хранил `join_keys` в `chat_core_ctx` (CC(inst)), а тест использует chat_join |
||||
без chat_core → NULL-deref. Фикс: `join_keys` вынесен в `UTUN_INSTANCE.join_keys` |
||||
(у chat_join собственный init/destroy). |
||||
|
||||
[ ] **BGP-гонка: дропнутый REQUEST_TABLE** (отдельная задача, как договорились). |
||||
|
||||
**Место:** `topo_group_new_conn()` в `src/routing_layer/topo_group.c:571`. Дедуп-ветка |
||||
«conn already in senders_list» (строки 579–587) делает `return` БЕЗ повторной отправки |
||||
`topo_group_send_table_request()`. |
||||
|
||||
**Зачем дедуп:** тот же `ETCP_CONN` стреляет `ETCP_CONN_STATUS_UP` дважды (UDP-линк, |
||||
затем TCP-линк). Повторная обработка задваивает `active_conn_count`, из-за чего |
||||
переподключение не стартует. Поэтому `return` в дедупе — правильный. |
||||
|
||||
**В чём гонка:** REQUEST_TABLE — это то, что заставляет пира отдать свою таблицу |
||||
(NODEINFO/узлы группы). Сценарий «разнесённого старта»: |
||||
1. A↔C соединение поднято. A шлёт C `REQUEST_TABLE` для группы G (`topo_group_new_conn` |
||||
→ `topo_group_send_table_request`, `topo_group.c:602`). |
||||
2. У C группа G ещё **не создана** (создаётся позже, по мере загрузки каналов/синка). |
||||
C получает `REQUEST_TABLE`, но `topo_group_handle_request_table` не находит G → |
||||
дропает запрос. |
||||
3. C наконец создаёт G (`topo_groups_create_group` → обход connections → |
||||
`topo_group_new_conn(G, conn_A)` на стороне C) → C шлёт A свой `REQUEST_TABLE`. |
||||
A отвечает своей таблицей → **C узнаёт A**. |
||||
4. Но A больше не шлёт C `REQUEST_TABLE` → **A так и не узнаёт C** в группе G. |
||||
Итог: при разнесённом старте A не видит C в CHAT-группе (односторонняя видимость). |
||||
|
||||
**Фикс (предполагаемый):** в дедуп-ветке не просто `return`, а предварительно |
||||
идемпотентно `topo_group_send_table_request(group, conn)`: |
||||
```c |
||||
if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) { |
||||
topo_group_send_table_request(group, conn); /* повторно, идемпотентно */ |
||||
DEBUG_INFO(..., "conn already in senders_list, re-request table (%s)", conn->log_name); |
||||
return; |
||||
} |
||||
``` |
||||
`topo_group_send_table_request` не ведёт состояния — просто шлёт REQUEST_TABLE, |
||||
пир отвечает снимком таблицы (идемпотентно). Лишний дубль таблицы безвреден. |
||||
|
||||
**Замечание:** в однопоточном `test_dm_e2e` (и `test_chat_join_e2e`) из-за |
||||
детерминированного порядка группа успевает создаться до прихода REQUEST_TABLE — |
||||
поэтому тест проходит. Реальная гонка остаётся для асинхронных сетей. Отдельный |
||||
воспроизводимый тест для этой гонки пока не написан. |
||||
|
||||
[ ] **«packet undecryptable» на линках C** (вторичное). A, узнав B через BGP, через |
||||
`topo_group_connect` пытается поднять прямой A↔B линк (`seskey 4777737b`), но B не |
||||
настроен принимать A → crypto-шум каждые ~1с. Разобраться с авто-подключением |
||||
(не пытаться соединяться с узлами, для которых нет `[client]`/ncd-конфигурации, |
||||
либо обрабатывать неудачу без спама). |
||||
|
||||
[+] **Обновить GUI call-сайты `tools/chatgui` (Qt, desktop)** — сделано. |
||||
- В `chat_core.h` вынесены trampoline-структуры из `.c`: `update_my_name_arg`, |
||||
`save_ui_state_arg`, `chat_setting_arg` (были в chat_profile.c/chat_core.c). |
||||
- `gui_bridge.h/impl`: `gui_bridge_set_inst()`/`gui_bridge_get_inst()` (+`g_inst`). |
||||
- `utun_node.cpp`: `gui_bridge_set_inst(m_instance)`, `chat_event_set_handler(m_instance,…)`, |
||||
`chat_core_sync_my_addresses(m_instance)`. |
||||
- Обновлены ~45 call-сайтов в 16 файлах (`mainwindow`, `messagelist`, `messagedelegate`, |
||||
`memberlistmodel`, `memberpropsdialog`, `accountlist`, `invitedialog`, `inviteby`, |
||||
`joindialog`, `settingsdialog`, `channelsettingsdialog`, `soundsettingspage`, |
||||
`audiodevicesettingspage`, `storagesettingspage`, `statuspage`, `connmonitorwindow`). |
||||
- Сборка `cmake --build . --target vibechat` — OK. |
||||
Попутно: `chat_sync_connect_from_invite` писал `cs->pending_*` на вызывающем (GUI) потоке — |
||||
перенёс в `cm_invite_trampoline` (uasync-поток), убрав гонку; заодно исправил утечку |
||||
`inv->addrs_data` и добавил null-check `cs` в трамплине. |
||||
|
||||
[+] **Прогон всех тестов** — сделано. `make clean && make -j4` OK; все тесты проходят |
||||
(кроме `test_auto_socket_dynamic` — пропуск «requires root»). `test_dm_e2e` стабилен |
||||
в нескольких прогонах. В `test_dm_e2e` при teardown остаётся timer-leak (~68 узлов, |
||||
`router_no_route`), не влияет на результат — стоит разобрать отдельно. |
||||
|
||||
## Открытые флаки |
||||
- **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`: |
||||
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном |
||||
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает. |
||||
[ ] **test_etcp_reconnect** — флаки под параллельной нагрузкой `make check -j4`: |
||||
phase 4 (reconnect после server restart) таймаутит (`sent=480 recv=0`), при одиночном |
||||
запуске стабильно `TEST PASSED`. Наш код (chat_join/chat_sync) его не трогает. |
||||
|
||||
@ -0,0 +1,55 @@
|
||||
bin |
||||
lib |
||||
|
||||
# Win build files |
||||
*.dll |
||||
*.exe |
||||
*.lib |
||||
|
||||
# GNU build files |
||||
*.o |
||||
*.so* |
||||
*.lo |
||||
*.P* |
||||
*.la* |
||||
*.a |
||||
*.pc |
||||
*config* |
||||
Makefile |
||||
Makefile.in |
||||
.libs |
||||
aclocal.m4 |
||||
autom4te.cache |
||||
stamp-* |
||||
libtool |
||||
soundstretch |
||||
|
||||
# Files generated by MSVC |
||||
*.bsc |
||||
*.suo |
||||
*.sdf |
||||
*.filters |
||||
*.user |
||||
source/SoundTouch/Win32/ |
||||
source/SoundTouch/x64/ |
||||
source/SoundStretch/Win32/ |
||||
source/SoundStretch/x64/ |
||||
source/SoundTouchDll/Win32/ |
||||
source/SoundTouchDll/x64/ |
||||
source/SoundTouchDll/DllTest/Win32/ |
||||
source/SoundTouchDll/DllTest/x64/ |
||||
.vs |
||||
|
||||
# Files generated by Android Studio |
||||
source/android-lib/.gradle |
||||
source/android-lib/.idea |
||||
**/*.iml |
||||
source/android-lib/local.properties |
||||
source/android-lib/build |
||||
source/android-lib/.externalNativeBuild |
||||
|
||||
# CMake build directory |
||||
build* |
||||
CMakeFiles |
||||
CMakeCache.txt |
||||
*.cmake |
||||
@ -0,0 +1,131 @@
|
||||
cmake_minimum_required(VERSION 3.16) |
||||
project(SoundTouch VERSION 2.3.3 LANGUAGES CXX) |
||||
set(CMAKE_CXX_STANDARD 17) |
||||
|
||||
include(GNUInstallDirs) |
||||
|
||||
set(COMPILE_OPTIONS) |
||||
|
||||
if(MSVC) |
||||
list(APPEND COMPILE_OPTIONS /O2 /fp:fast) |
||||
else() |
||||
list(APPEND COMPILE_OPTIONS -Wall -Wextra -Wzero-as-null-pointer-constant -Wno-unknown-pragmas) |
||||
if(EMSCRIPTEN) |
||||
list(APPEND COMPILE_OPTIONS -O3) |
||||
else() |
||||
list(APPEND COMPILE_OPTIONS -Ofast) |
||||
endif() |
||||
endif() |
||||
|
||||
set(CMAKE_C_VISIBILITY_PRESET hidden) |
||||
set(CMAKE_CXX_VISIBILITY_PRESET hidden) |
||||
|
||||
##################### |
||||
# SoundTouch library |
||||
|
||||
add_library(SoundTouch STATIC |
||||
source/SoundTouch/AAFilter.cpp |
||||
source/SoundTouch/BPMDetect.cpp |
||||
source/SoundTouch/cpu_detect_x86.cpp |
||||
source/SoundTouch/FIFOSampleBuffer.cpp |
||||
source/SoundTouch/FIRFilter.cpp |
||||
source/SoundTouch/InterpolateCubic.cpp |
||||
source/SoundTouch/InterpolateLinear.cpp |
||||
source/SoundTouch/InterpolateShannon.cpp |
||||
source/SoundTouch/mmx_optimized.cpp |
||||
source/SoundTouch/PeakFinder.cpp |
||||
source/SoundTouch/RateTransposer.cpp |
||||
source/SoundTouch/SoundTouch.cpp |
||||
source/SoundTouch/sse_optimized.cpp |
||||
source/SoundTouch/TDStretch.cpp |
||||
) |
||||
target_include_directories(SoundTouch PUBLIC |
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> |
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}> |
||||
) |
||||
target_include_directories(SoundTouch PRIVATE |
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/soundtouch |
||||
) |
||||
|
||||
target_compile_definitions(SoundTouch PRIVATE ${COMPILE_DEFINITIONS}) |
||||
target_compile_options(SoundTouch PRIVATE ${COMPILE_OPTIONS}) |
||||
set_target_properties(SoundTouch PROPERTIES OUTPUT_NAME "soundtouch-static") |
||||
|
||||
option(INTEGER_SAMPLES "Use integers instead of floats for samples" OFF) |
||||
if(INTEGER_SAMPLES) |
||||
target_compile_definitions(SoundTouch PRIVATE SOUNDTOUCH_INTEGER_SAMPLES) |
||||
set(SOUNDTOUCH_INTEGER_SAMPLES TRUE) |
||||
else() |
||||
target_compile_definitions(SoundTouch PRIVATE SOUNDTOUCH_FLOAT_SAMPLES) |
||||
set(SOUNDTOUCH_FLOAT_SAMPLES TRUE) |
||||
endif() |
||||
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv7.*)$") |
||||
target_compile_options(SoundTouch PRIVATE -mfpu=neon) |
||||
endif() |
||||
|
||||
install( |
||||
FILES |
||||
include/soundtouch/BPMDetect.h |
||||
include/soundtouch/FIFOSampleBuffer.h |
||||
include/soundtouch/FIFOSamplePipe.h |
||||
include/soundtouch/STTypes.h |
||||
include/soundtouch/SoundTouch.h |
||||
DESTINATION |
||||
"${CMAKE_INSTALL_INCLUDEDIR}/soundtouch" |
||||
) |
||||
|
||||
|
||||
######################## |
||||
# SoundTouchDll library |
||||
|
||||
option(SOUNDTOUCH_DLL "Build SoundTouchDLL C wrapper library" ON) |
||||
if(SOUNDTOUCH_DLL) |
||||
add_library(SoundTouchDLL SHARED |
||||
source/SoundTouchDLL/SoundTouchDLL.cpp |
||||
source/SoundTouchDLL/SoundTouchDLL.rc |
||||
) |
||||
set_target_properties(SoundTouch PROPERTIES POSITION_INDEPENDENT_CODE TRUE) |
||||
target_compile_options(SoundTouchDLL PRIVATE ${COMPILE_OPTIONS}) |
||||
set_target_properties(SoundTouchDLL PROPERTIES CXX_VISIBILITY_PRESET hidden) |
||||
target_compile_definitions(SoundTouchDLL PRIVATE DLL_EXPORTS) |
||||
target_include_directories(SoundTouchDLL INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>) |
||||
target_include_directories(SoundTouchDLL PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include/soundtouch") |
||||
target_link_libraries(SoundTouchDLL PRIVATE SoundTouch) |
||||
set_target_properties(SoundTouchDLL PROPERTIES OUTPUT_NAME "soundtouch") |
||||
if(NOT ANDROID) |
||||
set_target_properties(SoundTouchDLL PROPERTIES VERSION ${CMAKE_PROJECT_VERSION}) |
||||
if(NOT WIN32) |
||||
set_target_properties(SoundTouchDLL PROPERTIES SOVERSION ${PROJECT_VERSION_MAJOR}) |
||||
endif() |
||||
endif() |
||||
install(FILES include/soundtouch/SoundTouchDLL.h DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/soundtouch") |
||||
install(TARGETS SoundTouchDLL EXPORT SoundTouchTargets) |
||||
endif() |
||||
|
||||
######################## |
||||
|
||||
# CMake config |
||||
include(CMakePackageConfigHelpers) |
||||
set(SOUNDTOUCH_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/SoundTouch") |
||||
install( |
||||
EXPORT SoundTouchTargets |
||||
FILE SoundTouchTargets.cmake |
||||
NAMESPACE SoundTouch:: |
||||
DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}" |
||||
) |
||||
configure_package_config_file(SoundTouchConfig.cmake.in |
||||
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfig.cmake" |
||||
INSTALL_DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}" |
||||
) |
||||
write_basic_package_version_file( |
||||
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfigVersion.cmake" |
||||
VERSION "${CMAKE_PROJECT_VERSION}" |
||||
COMPATIBILITY SameMajorVersion |
||||
) |
||||
install( |
||||
FILES |
||||
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfig.cmake" |
||||
"${CMAKE_CURRENT_BINARY_DIR}/SoundTouchConfigVersion.cmake" |
||||
DESTINATION "${SOUNDTOUCH_INSTALL_CMAKEDIR}" |
||||
) |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,11 @@
|
||||
@PACKAGE_INIT@ |
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/SoundTouchTargets.cmake") |
||||
|
||||
check_required_components(SoundTouch) |
||||
|
||||
if(@SOUNDTOUCH_DLL@) |
||||
check_required_components(SoundTouchDLL) |
||||
get_target_property(SoundTouchDLL_LOCATION SoundTouch::SoundTouchDLL LOCATION) |
||||
message(STATUS "Found SoundTouchDLL: ${SoundTouchDLL_LOCATION}") |
||||
endif() |
||||
@ -0,0 +1,205 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Beats-per-minute (BPM) detection routine.
|
||||
///
|
||||
/// The beat detection algorithm works as follows:
|
||||
/// - Use function 'inputSamples' to input a chunks of samples to the class for
|
||||
/// analysis. It's a good idea to enter a large sound file or stream in smallish
|
||||
/// chunks of around few kilosamples in order not to extinguish too much RAM memory.
|
||||
/// - Input sound data is decimated to approx 500 Hz to reduce calculation burden,
|
||||
/// which is basically ok as low (bass) frequencies mostly determine the beat rate.
|
||||
/// Simple averaging is used for anti-alias filtering because the resulting signal
|
||||
/// quality isn't of that high importance.
|
||||
/// - Decimated sound data is enveloped, i.e. the amplitude shape is detected by
|
||||
/// taking absolute value that's smoothed by sliding average. Signal levels that
|
||||
/// are below a couple of times the general RMS amplitude level are cut away to
|
||||
/// leave only notable peaks there.
|
||||
/// - Repeating sound patterns (e.g. beats) are detected by calculating short-term
|
||||
/// autocorrelation function of the enveloped signal.
|
||||
/// - After whole sound data file has been analyzed as above, the bpm level is
|
||||
/// detected by function 'getBpm' that finds the highest peak of the autocorrelation
|
||||
/// function, calculates it's precise location and converts this reading to bpm's.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef _BPMDetect_H_ |
||||
#define _BPMDetect_H_ |
||||
|
||||
#include <vector> |
||||
#include "STTypes.h" |
||||
#include "FIFOSampleBuffer.h" |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
/// Minimum allowed BPM rate. Used to restrict accepted result above a reasonable limit.
|
||||
#define MIN_BPM 45 |
||||
|
||||
/// Maximum allowed BPM rate range. Used for calculating algorithm parametrs
|
||||
#define MAX_BPM_RANGE 200 |
||||
|
||||
/// Maximum allowed BPM rate range. Used to restrict accepted result below a reasonable limit.
|
||||
#define MAX_BPM_VALID 190 |
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
typedef struct |
||||
{ |
||||
float pos; |
||||
float strength; |
||||
} BEAT; |
||||
|
||||
|
||||
class IIR2_filter |
||||
{ |
||||
double coeffs[5]; |
||||
double prev[5]; |
||||
|
||||
public: |
||||
IIR2_filter(const double *lpf_coeffs); |
||||
float update(float x); |
||||
}; |
||||
|
||||
|
||||
/// Class for calculating BPM rate for audio data.
|
||||
class BPMDetect |
||||
{ |
||||
protected: |
||||
/// Auto-correlation accumulator bins.
|
||||
float *xcorr; |
||||
|
||||
/// Sample average counter.
|
||||
int decimateCount; |
||||
|
||||
/// Sample average accumulator for FIFO-like decimation.
|
||||
soundtouch::LONG_SAMPLETYPE decimateSum; |
||||
|
||||
/// Decimate sound by this coefficient to reach approx. 500 Hz.
|
||||
int decimateBy; |
||||
|
||||
/// Auto-correlation window length
|
||||
int windowLen; |
||||
|
||||
/// Number of channels (1 = mono, 2 = stereo)
|
||||
int channels; |
||||
|
||||
/// sample rate
|
||||
int sampleRate; |
||||
|
||||
/// Beginning of auto-correlation window: Autocorrelation isn't being updated for
|
||||
/// the first these many correlation bins.
|
||||
int windowStart; |
||||
|
||||
/// window functions for data preconditioning
|
||||
float *hamw; |
||||
float *hamw2; |
||||
|
||||
// beat detection variables
|
||||
int pos; |
||||
int peakPos; |
||||
int beatcorr_ringbuffpos; |
||||
int init_scaler; |
||||
float peakVal; |
||||
float *beatcorr_ringbuff; |
||||
|
||||
/// FIFO-buffer for decimated processing samples.
|
||||
soundtouch::FIFOSampleBuffer *buffer; |
||||
|
||||
/// Collection of detected beat positions
|
||||
//BeatCollection beats;
|
||||
std::vector<BEAT> beats; |
||||
|
||||
// 2nd order low-pass-filter
|
||||
IIR2_filter beat_lpf; |
||||
|
||||
/// Updates auto-correlation function for given number of decimated samples that
|
||||
/// are read from the internal 'buffer' pipe (samples aren't removed from the pipe
|
||||
/// though).
|
||||
void updateXCorr(int process_samples /// How many samples are processed.
|
||||
); |
||||
|
||||
/// Decimates samples to approx. 500 Hz.
|
||||
///
|
||||
/// \return Number of output samples.
|
||||
int decimate(soundtouch::SAMPLETYPE *dest, ///< Destination buffer
|
||||
const soundtouch::SAMPLETYPE *src, ///< Source sample buffer
|
||||
int numsamples ///< Number of source samples.
|
||||
); |
||||
|
||||
/// Calculates amplitude envelope for the buffer of samples.
|
||||
/// Result is output to 'samples'.
|
||||
void calcEnvelope(soundtouch::SAMPLETYPE *samples, ///< Pointer to input/output data buffer
|
||||
int numsamples ///< Number of samples in buffer
|
||||
); |
||||
|
||||
/// remove constant bias from xcorr data
|
||||
void removeBias(); |
||||
|
||||
// Detect individual beat positions
|
||||
void updateBeatPos(int process_samples); |
||||
|
||||
|
||||
public: |
||||
/// Constructor.
|
||||
BPMDetect(int numChannels, ///< Number of channels in sample data.
|
||||
int sampleRate ///< Sample rate in Hz.
|
||||
); |
||||
|
||||
/// Destructor.
|
||||
virtual ~BPMDetect(); |
||||
|
||||
/// Inputs a block of samples for analyzing: Envelopes the samples and then
|
||||
/// updates the autocorrelation estimation. When whole song data has been input
|
||||
/// in smaller blocks using this function, read the resulting bpm with 'getBpm'
|
||||
/// function.
|
||||
///
|
||||
/// Notice that data in 'samples' array can be disrupted in processing.
|
||||
void inputSamples(const soundtouch::SAMPLETYPE *samples, ///< Pointer to input/working data buffer
|
||||
int numSamples ///< Number of samples in buffer
|
||||
); |
||||
|
||||
/// Analyzes the results and returns the BPM rate. Use this function to read result
|
||||
/// after whole song data has been input to the class by consecutive calls of
|
||||
/// 'inputSamples' function.
|
||||
///
|
||||
/// \return Beats-per-minute rate, or zero if detection failed.
|
||||
float getBpm(); |
||||
|
||||
/// Get beat position arrays. Note: The array includes also really low beat detection values
|
||||
/// in absence of clear strong beats. Consumer may wish to filter low values away.
|
||||
/// - "pos" receive array of beat positions
|
||||
/// - "values" receive array of beat detection strengths
|
||||
/// - max_num indicates max.size of "pos" and "values" array.
|
||||
///
|
||||
/// You can query a suitable array sized by calling this with nullptr in "pos" & "values".
|
||||
///
|
||||
/// \return number of beats in the arrays.
|
||||
int getBeats(float *pos, float *strength, int max_num); |
||||
}; |
||||
} |
||||
#endif // _BPMDetect_H_
|
||||
@ -0,0 +1,180 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// A buffer class for temporarily storaging sound samples, operates as a
|
||||
/// first-in-first-out pipe.
|
||||
///
|
||||
/// Samples are added to the end of the sample buffer with the 'putSamples'
|
||||
/// function, and are received from the beginning of the buffer by calling
|
||||
/// the 'receiveSamples' function. The class automatically removes the
|
||||
/// output samples from the buffer as well as grows the storage size
|
||||
/// whenever necessary.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef FIFOSampleBuffer_H |
||||
#define FIFOSampleBuffer_H |
||||
|
||||
#include "FIFOSamplePipe.h" |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
/// Sample buffer working in FIFO (first-in-first-out) principle. The class takes
|
||||
/// care of storage size adjustment and data moving during input/output operations.
|
||||
///
|
||||
/// Notice that in case of stereo audio, one sample is considered to consist of
|
||||
/// both channel data.
|
||||
class FIFOSampleBuffer : public FIFOSamplePipe |
||||
{ |
||||
private: |
||||
/// Sample buffer.
|
||||
SAMPLETYPE *buffer; |
||||
|
||||
// Raw unaligned buffer memory. 'buffer' is made aligned by pointing it to first
|
||||
// 16-byte aligned location of this buffer
|
||||
SAMPLETYPE *bufferUnaligned; |
||||
|
||||
/// Sample buffer size in bytes
|
||||
uint sizeInBytes; |
||||
|
||||
/// How many samples are currently in buffer.
|
||||
uint samplesInBuffer; |
||||
|
||||
/// Channels, 1=mono, 2=stereo.
|
||||
uint channels; |
||||
|
||||
/// Current position pointer to the buffer. This pointer is increased when samples are
|
||||
/// removed from the pipe so that it's necessary to actually rewind buffer (move data)
|
||||
/// only new data when is put to the pipe.
|
||||
uint bufferPos; |
||||
|
||||
/// Rewind the buffer by moving data from position pointed by 'bufferPos' to real
|
||||
/// beginning of the buffer.
|
||||
void rewind(); |
||||
|
||||
/// Ensures that the buffer has capacity for at least this many samples.
|
||||
void ensureCapacity(uint capacityRequirement); |
||||
|
||||
/// Returns current capacity.
|
||||
uint getCapacity() const; |
||||
|
||||
public: |
||||
|
||||
/// Constructor
|
||||
FIFOSampleBuffer(int numChannels = 2 ///< Number of channels, 1=mono, 2=stereo.
|
||||
///< Default is stereo.
|
||||
); |
||||
|
||||
/// destructor
|
||||
~FIFOSampleBuffer() override; |
||||
|
||||
/// Returns a pointer to the beginning of the output samples.
|
||||
/// This function is provided for accessing the output samples directly.
|
||||
/// Please be careful for not to corrupt the book-keeping!
|
||||
///
|
||||
/// When using this function to output samples, also remember to 'remove' the
|
||||
/// output samples from the buffer by calling the
|
||||
/// 'receiveSamples(numSamples)' function
|
||||
virtual SAMPLETYPE *ptrBegin() override; |
||||
|
||||
/// Returns a pointer to the end of the used part of the sample buffer (i.e.
|
||||
/// where the new samples are to be inserted). This function may be used for
|
||||
/// inserting new samples into the sample buffer directly. Please be careful
|
||||
/// not corrupt the book-keeping!
|
||||
///
|
||||
/// When using this function as means for inserting new samples, also remember
|
||||
/// to increase the sample count afterwards, by calling the
|
||||
/// 'putSamples(numSamples)' function.
|
||||
SAMPLETYPE *ptrEnd( |
||||
uint slackCapacity ///< How much free capacity (in samples) there _at least_
|
||||
///< should be so that the caller can successfully insert the
|
||||
///< desired samples to the buffer. If necessary, the function
|
||||
///< grows the buffer size to comply with this requirement.
|
||||
); |
||||
|
||||
/// Adds 'numSamples' pcs of samples from the 'samples' memory position to
|
||||
/// the sample buffer.
|
||||
virtual void putSamples(const SAMPLETYPE *samples, ///< Pointer to samples.
|
||||
uint numSamples ///< Number of samples to insert.
|
||||
) override; |
||||
|
||||
/// Adjusts the book-keeping to increase number of samples in the buffer without
|
||||
/// copying any actual samples.
|
||||
///
|
||||
/// This function is used to update the number of samples in the sample buffer
|
||||
/// when accessing the buffer directly with 'ptrEnd' function. Please be
|
||||
/// careful though!
|
||||
virtual void putSamples(uint numSamples ///< Number of samples been inserted.
|
||||
); |
||||
|
||||
/// Output samples from beginning of the sample buffer. Copies requested samples to
|
||||
/// output buffer and removes them from the sample buffer. If there are less than
|
||||
/// 'numsample' samples in the buffer, returns all that available.
|
||||
///
|
||||
/// \return Number of samples returned.
|
||||
virtual uint receiveSamples(SAMPLETYPE *output, ///< Buffer where to copy output samples.
|
||||
uint maxSamples ///< How many samples to receive at max.
|
||||
) override; |
||||
|
||||
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
|
||||
/// sample buffer without copying them anywhere.
|
||||
///
|
||||
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
|
||||
/// with 'ptrBegin' function.
|
||||
virtual uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
|
||||
) override; |
||||
|
||||
/// Returns number of samples currently available.
|
||||
virtual uint numSamples() const override; |
||||
|
||||
/// Sets number of channels, 1 = mono, 2 = stereo.
|
||||
void setChannels(int numChannels); |
||||
|
||||
/// Get number of channels
|
||||
int getChannels() |
||||
{ |
||||
return channels; |
||||
} |
||||
|
||||
/// Returns nonzero if there aren't any samples available for outputting.
|
||||
virtual int isEmpty() const override; |
||||
|
||||
/// Clears all the samples.
|
||||
virtual void clear() override; |
||||
|
||||
/// allow trimming (downwards) amount of samples in pipeline.
|
||||
/// Returns adjusted amount of samples
|
||||
uint adjustAmountOfSamples(uint numSamples) override; |
||||
|
||||
/// Add silence to end of buffer
|
||||
void addSilent(uint nSamples); |
||||
}; |
||||
|
||||
} |
||||
|
||||
#endif |
||||
@ -0,0 +1,230 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// 'FIFOSamplePipe' : An abstract base class for classes that manipulate sound
|
||||
/// samples by operating like a first-in-first-out pipe: New samples are fed
|
||||
/// into one end of the pipe with the 'putSamples' function, and the processed
|
||||
/// samples are received from the other end with the 'receiveSamples' function.
|
||||
///
|
||||
/// 'FIFOProcessor' : A base class for classes the do signal processing with
|
||||
/// the samples while operating like a first-in-first-out pipe. When samples
|
||||
/// are input with the 'putSamples' function, the class processes them
|
||||
/// and moves the processed samples to the given 'output' pipe object, which
|
||||
/// may be either another processing stage, or a fifo sample buffer object.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef FIFOSamplePipe_H |
||||
#define FIFOSamplePipe_H |
||||
|
||||
#include <assert.h> |
||||
#include <stdlib.h> |
||||
#include "STTypes.h" |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
/// Abstract base class for FIFO (first-in-first-out) sample processing classes.
|
||||
class FIFOSamplePipe |
||||
{ |
||||
protected: |
||||
|
||||
bool verifyNumberOfChannels(int nChannels) const |
||||
{ |
||||
if ((nChannels > 0) && (nChannels <= SOUNDTOUCH_MAX_CHANNELS)) |
||||
{ |
||||
return true; |
||||
} |
||||
ST_THROW_RT_ERROR("Error: Illegal number of channels"); |
||||
return false; |
||||
} |
||||
|
||||
public: |
||||
// virtual default destructor
|
||||
virtual ~FIFOSamplePipe() {} |
||||
|
||||
|
||||
/// Returns a pointer to the beginning of the output samples.
|
||||
/// This function is provided for accessing the output samples directly.
|
||||
/// Please be careful for not to corrupt the book-keeping!
|
||||
///
|
||||
/// When using this function to output samples, also remember to 'remove' the
|
||||
/// output samples from the buffer by calling the
|
||||
/// 'receiveSamples(numSamples)' function
|
||||
virtual SAMPLETYPE *ptrBegin() = 0; |
||||
|
||||
/// Adds 'numSamples' pcs of samples from the 'samples' memory position to
|
||||
/// the sample buffer.
|
||||
virtual void putSamples(const SAMPLETYPE *samples, ///< Pointer to samples.
|
||||
uint numSamples ///< Number of samples to insert.
|
||||
) = 0; |
||||
|
||||
|
||||
// Moves samples from the 'other' pipe instance to this instance.
|
||||
void moveSamples(FIFOSamplePipe &other ///< Other pipe instance where from the receive the data.
|
||||
) |
||||
{ |
||||
const uint oNumSamples = other.numSamples(); |
||||
|
||||
putSamples(other.ptrBegin(), oNumSamples); |
||||
other.receiveSamples(oNumSamples); |
||||
} |
||||
|
||||
/// Output samples from beginning of the sample buffer. Copies requested samples to
|
||||
/// output buffer and removes them from the sample buffer. If there are less than
|
||||
/// 'numsample' samples in the buffer, returns all that available.
|
||||
///
|
||||
/// \return Number of samples returned.
|
||||
virtual uint receiveSamples(SAMPLETYPE *output, ///< Buffer where to copy output samples.
|
||||
uint maxSamples ///< How many samples to receive at max.
|
||||
) = 0; |
||||
|
||||
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
|
||||
/// sample buffer without copying them anywhere.
|
||||
///
|
||||
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
|
||||
/// with 'ptrBegin' function.
|
||||
virtual uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
|
||||
) = 0; |
||||
|
||||
/// Returns number of samples currently available.
|
||||
virtual uint numSamples() const = 0; |
||||
|
||||
// Returns nonzero if there aren't any samples available for outputting.
|
||||
virtual int isEmpty() const = 0; |
||||
|
||||
/// Clears all the samples.
|
||||
virtual void clear() = 0; |
||||
|
||||
/// allow trimming (downwards) amount of samples in pipeline.
|
||||
/// Returns adjusted amount of samples
|
||||
virtual uint adjustAmountOfSamples(uint numSamples) = 0; |
||||
|
||||
}; |
||||
|
||||
|
||||
/// Base-class for sound processing routines working in FIFO principle. With this base
|
||||
/// class it's easy to implement sound processing stages that can be chained together,
|
||||
/// so that samples that are fed into beginning of the pipe automatically go through
|
||||
/// all the processing stages.
|
||||
///
|
||||
/// When samples are input to this class, they're first processed and then put to
|
||||
/// the FIFO pipe that's defined as output of this class. This output pipe can be
|
||||
/// either other processing stage or a FIFO sample buffer.
|
||||
class FIFOProcessor :public FIFOSamplePipe |
||||
{ |
||||
protected: |
||||
/// Internal pipe where processed samples are put.
|
||||
FIFOSamplePipe *output; |
||||
|
||||
/// Sets output pipe.
|
||||
void setOutPipe(FIFOSamplePipe *pOutput) |
||||
{ |
||||
assert(output == nullptr); |
||||
assert(pOutput != nullptr); |
||||
output = pOutput; |
||||
} |
||||
|
||||
/// Constructor. Doesn't define output pipe; it has to be set be
|
||||
/// 'setOutPipe' function.
|
||||
FIFOProcessor() |
||||
{ |
||||
output = nullptr; |
||||
} |
||||
|
||||
/// Constructor. Configures output pipe.
|
||||
FIFOProcessor(FIFOSamplePipe *pOutput ///< Output pipe.
|
||||
) |
||||
{ |
||||
output = pOutput; |
||||
} |
||||
|
||||
/// Destructor.
|
||||
virtual ~FIFOProcessor() override |
||||
{ |
||||
} |
||||
|
||||
/// Returns a pointer to the beginning of the output samples.
|
||||
/// This function is provided for accessing the output samples directly.
|
||||
/// Please be careful for not to corrupt the book-keeping!
|
||||
///
|
||||
/// When using this function to output samples, also remember to 'remove' the
|
||||
/// output samples from the buffer by calling the
|
||||
/// 'receiveSamples(numSamples)' function
|
||||
virtual SAMPLETYPE *ptrBegin() override |
||||
{ |
||||
return output->ptrBegin(); |
||||
} |
||||
|
||||
public: |
||||
|
||||
/// Output samples from beginning of the sample buffer. Copies requested samples to
|
||||
/// output buffer and removes them from the sample buffer. If there are less than
|
||||
/// 'numsample' samples in the buffer, returns all that available.
|
||||
///
|
||||
/// \return Number of samples returned.
|
||||
virtual uint receiveSamples(SAMPLETYPE *outBuffer, ///< Buffer where to copy output samples.
|
||||
uint maxSamples ///< How many samples to receive at max.
|
||||
) override |
||||
{ |
||||
return output->receiveSamples(outBuffer, maxSamples); |
||||
} |
||||
|
||||
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
|
||||
/// sample buffer without copying them anywhere.
|
||||
///
|
||||
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
|
||||
/// with 'ptrBegin' function.
|
||||
virtual uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
|
||||
) override |
||||
{ |
||||
return output->receiveSamples(maxSamples); |
||||
} |
||||
|
||||
/// Returns number of samples currently available.
|
||||
virtual uint numSamples() const override |
||||
{ |
||||
return output->numSamples(); |
||||
} |
||||
|
||||
/// Returns nonzero if there aren't any samples available for outputting.
|
||||
virtual int isEmpty() const override |
||||
{ |
||||
return output->isEmpty(); |
||||
} |
||||
|
||||
/// allow trimming (downwards) amount of samples in pipeline.
|
||||
/// Returns adjusted amount of samples
|
||||
virtual uint adjustAmountOfSamples(uint numSamples) override |
||||
{ |
||||
return output->adjustAmountOfSamples(numSamples); |
||||
} |
||||
}; |
||||
|
||||
} |
||||
|
||||
#endif |
||||
@ -0,0 +1,176 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Common type definitions for SoundTouch audio processing library.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef STTypes_H |
||||
#define STTypes_H |
||||
|
||||
typedef unsigned int uint; |
||||
typedef unsigned long ulong; |
||||
|
||||
// Patch for MinGW: on Win64 long is 32-bit
|
||||
#ifdef _WIN64 |
||||
typedef unsigned long long ulongptr; |
||||
#else |
||||
typedef ulong ulongptr; |
||||
#endif |
||||
|
||||
|
||||
// Helper macro for aligning pointer up to next 16-byte boundary
|
||||
#define SOUNDTOUCH_ALIGN_POINTER_16(x) ( ( (ulongptr)(x) + 15 ) & ~(ulongptr)15 ) |
||||
|
||||
|
||||
namespace soundtouch |
||||
{ |
||||
/// Max allowed number of channels
|
||||
#define SOUNDTOUCH_MAX_CHANNELS 16 |
||||
|
||||
/// Activate these undef's to overrule the possible sampletype
|
||||
/// setting inherited from some other header file:
|
||||
//#undef SOUNDTOUCH_INTEGER_SAMPLES
|
||||
//#undef SOUNDTOUCH_FLOAT_SAMPLES
|
||||
|
||||
/// If following flag is defined, always uses multichannel processing
|
||||
/// routines also for mono and stero sound. This is for routine testing
|
||||
/// purposes; output should be same with either routines, yet disabling
|
||||
/// the dedicated mono/stereo processing routines will result in slower
|
||||
/// runtime performance so recommendation is to keep this off.
|
||||
// #define USE_MULTICH_ALWAYS
|
||||
|
||||
#if !(SOUNDTOUCH_INTEGER_SAMPLES || SOUNDTOUCH_FLOAT_SAMPLES) |
||||
|
||||
/// Choose either 32bit floating point or 16bit integer sampletype
|
||||
/// by choosing one of the following defines, unless this selection
|
||||
/// has already been done in some other file.
|
||||
////
|
||||
/// Notes:
|
||||
/// - In Windows environment, choose the sample format with the
|
||||
/// following defines.
|
||||
/// - In GNU environment, the floating point samples are used by
|
||||
/// default, but integer samples can be chosen by giving the
|
||||
/// following switch to the configure script:
|
||||
/// ./configure --enable-integer-samples
|
||||
/// However, if you still prefer to select the sample format here
|
||||
/// also in GNU environment, then please #undef the INTEGER_SAMPLE
|
||||
/// and FLOAT_SAMPLE defines first as in comments above.
|
||||
//#define SOUNDTOUCH_INTEGER_SAMPLES 1 //< 16bit integer samples
|
||||
#define SOUNDTOUCH_FLOAT_SAMPLES 1 //< 32bit float samples
|
||||
|
||||
#endif |
||||
|
||||
#if (_M_IX86 || __i386__ || __x86_64__ || _M_X64) |
||||
/// Define this to allow X86-specific assembler/intrinsic optimizations.
|
||||
/// Notice that library contains also usual C++ versions of each of these
|
||||
/// these routines, so if you're having difficulties getting the optimized
|
||||
/// routines compiled for whatever reason, you may disable these optimizations
|
||||
/// to make the library compile.
|
||||
|
||||
#define SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS 1 |
||||
#undef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS |
||||
#elif defined(__arm__) || defined(__aarch64__) |
||||
#undef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS |
||||
#define SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS 1 |
||||
#else |
||||
/// Always disable optimizations when not using a x86 systems.
|
||||
#undef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS |
||||
#undef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS |
||||
|
||||
#endif |
||||
|
||||
// If defined, allows the SIMD-optimized routines to skip unevenly aligned
|
||||
// memory offsets that can cause performance penalty in some SIMD implementations.
|
||||
// Causes slight compromise in sound quality.
|
||||
// #define SOUNDTOUCH_ALLOW_NONEXACT_SIMD_OPTIMIZATION 1
|
||||
|
||||
|
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES |
||||
// 16bit integer sample type
|
||||
typedef short SAMPLETYPE; |
||||
// data type for sample accumulation: Use 32bit integer to prevent overflows
|
||||
typedef long LONG_SAMPLETYPE; |
||||
|
||||
#ifdef SOUNDTOUCH_FLOAT_SAMPLES |
||||
// check that only one sample type is defined
|
||||
#error "conflicting sample types defined" |
||||
#endif // SOUNDTOUCH_FLOAT_SAMPLES
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS |
||||
// Allow MMX optimizations (not available in X64 mode)
|
||||
#if (!_M_X64) |
||||
#define SOUNDTOUCH_ALLOW_MMX 1 |
||||
#endif |
||||
#endif |
||||
|
||||
#else |
||||
|
||||
// floating point samples
|
||||
typedef float SAMPLETYPE; |
||||
// data type for sample accumulation: Use float also here to enable
|
||||
// efficient autovectorization
|
||||
typedef float LONG_SAMPLETYPE; |
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS |
||||
// Allow SSE optimizations
|
||||
#define SOUNDTOUCH_ALLOW_SSE 1 |
||||
#endif |
||||
#ifdef SOUNDTOUCH_ALLOW_NEON_OPTIMIZATIONS |
||||
#define SOUNDTOUCH_USE_NEON 1 |
||||
#endif |
||||
|
||||
#endif // SOUNDTOUCH_INTEGER_SAMPLES
|
||||
|
||||
#if ((SOUNDTOUCH_ALLOW_SSE) || (__SSE__) || (SOUNDTOUCH_USE_NEON)) |
||||
#if SOUNDTOUCH_ALLOW_NONEXACT_SIMD_OPTIMIZATION |
||||
#define ST_SIMD_AVOID_UNALIGNED |
||||
#endif |
||||
#endif |
||||
|
||||
} |
||||
|
||||
// define ST_NO_EXCEPTION_HANDLING switch to disable throwing std exceptions:
|
||||
#define ST_NO_EXCEPTION_HANDLING 1 |
||||
#ifdef ST_NO_EXCEPTION_HANDLING |
||||
// Exceptions disabled. Throw asserts instead if enabled.
|
||||
#include <assert.h> |
||||
#define ST_THROW_RT_ERROR(x) {assert((const char *)x);} |
||||
#else |
||||
// use c++ standard exceptions
|
||||
#include <stdexcept> |
||||
#include <string> |
||||
#define ST_THROW_RT_ERROR(x) {throw std::runtime_error(x);} |
||||
#endif |
||||
|
||||
// When this #define is active, eliminates a clicking sound when the "rate" or "pitch"
|
||||
// parameter setting crosses from value <1 to >=1 or vice versa during processing.
|
||||
// Default is off as such crossover is untypical case and involves a slight sound
|
||||
// quality compromise.
|
||||
//#define SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER 1
|
||||
|
||||
#endif |
||||
@ -0,0 +1,348 @@
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// SoundTouch - main class for tempo/pitch/rate adjusting routines.
|
||||
///
|
||||
/// Notes:
|
||||
/// - Initialize the SoundTouch object instance by setting up the sound stream
|
||||
/// parameters with functions 'setSampleRate' and 'setChannels', then set
|
||||
/// desired tempo/pitch/rate settings with the corresponding functions.
|
||||
///
|
||||
/// - The SoundTouch class behaves like a first-in-first-out pipeline: The
|
||||
/// samples that are to be processed are fed into one of the pipe by calling
|
||||
/// function 'putSamples', while the ready processed samples can be read
|
||||
/// from the other end of the pipeline with function 'receiveSamples'.
|
||||
///
|
||||
/// - The SoundTouch processing classes require certain sized 'batches' of
|
||||
/// samples in order to process the sound. For this reason the classes buffer
|
||||
/// incoming samples until there are enough of samples available for
|
||||
/// processing, then they carry out the processing step and consequently
|
||||
/// make the processed samples available for outputting.
|
||||
///
|
||||
/// - For the above reason, the processing routines introduce a certain
|
||||
/// 'latency' between the input and output, so that the samples input to
|
||||
/// SoundTouch may not be immediately available in the output, and neither
|
||||
/// the amount of outputtable samples may not immediately be in direct
|
||||
/// relationship with the amount of previously input samples.
|
||||
///
|
||||
/// - The tempo/pitch/rate control parameters can be altered during processing.
|
||||
/// Please notice though that they aren't currently protected by semaphores,
|
||||
/// so in multi-thread application external semaphore protection may be
|
||||
/// required.
|
||||
///
|
||||
/// - This class utilizes classes 'TDStretch' for tempo change (without modifying
|
||||
/// pitch) and 'RateTransposer' for changing the playback rate (that is, both
|
||||
/// tempo and pitch in the same ratio) of the sound. The third available control
|
||||
/// 'pitch' (change pitch but maintain tempo) is produced by a combination of
|
||||
/// combining the two other controls.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef SoundTouch_H |
||||
#define SoundTouch_H |
||||
|
||||
#include "FIFOSamplePipe.h" |
||||
#include "STTypes.h" |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
/// Soundtouch library version string
|
||||
#define SOUNDTOUCH_VERSION "2.3.3" |
||||
|
||||
/// SoundTouch library version id
|
||||
#define SOUNDTOUCH_VERSION_ID (20303) |
||||
|
||||
//
|
||||
// Available setting IDs for the 'setSetting' & 'get_setting' functions:
|
||||
|
||||
/// Enable/disable anti-alias filter in pitch transposer (0 = disable)
|
||||
#define SETTING_USE_AA_FILTER 0 |
||||
|
||||
/// Pitch transposer anti-alias filter length (8 .. 128 taps, default = 32)
|
||||
#define SETTING_AA_FILTER_LENGTH 1 |
||||
|
||||
/// Enable/disable quick seeking algorithm in tempo changer routine
|
||||
/// (enabling quick seeking lowers CPU utilization but causes a minor sound
|
||||
/// quality compromising)
|
||||
#define SETTING_USE_QUICKSEEK 2 |
||||
|
||||
/// Time-stretch algorithm single processing sequence length in milliseconds. This determines
|
||||
/// to how long sequences the original sound is chopped in the time-stretch algorithm.
|
||||
/// See "STTypes.h" or README for more information.
|
||||
#define SETTING_SEQUENCE_MS 3 |
||||
|
||||
/// Time-stretch algorithm seeking window length in milliseconds for algorithm that finds the
|
||||
/// best possible overlapping location. This determines from how wide window the algorithm
|
||||
/// may look for an optimal joining location when mixing the sound sequences back together.
|
||||
/// See "STTypes.h" or README for more information.
|
||||
#define SETTING_SEEKWINDOW_MS 4 |
||||
|
||||
/// Time-stretch algorithm overlap length in milliseconds. When the chopped sound sequences
|
||||
/// are mixed back together, to form a continuous sound stream, this parameter defines over
|
||||
/// how long period the two consecutive sequences are let to overlap each other.
|
||||
/// See "STTypes.h" or README for more information.
|
||||
#define SETTING_OVERLAP_MS 5 |
||||
|
||||
|
||||
/// Call "getSetting" with this ID to query processing sequence size in samples.
|
||||
/// This value gives approximate value of how many input samples you'll need to
|
||||
/// feed into SoundTouch after initial buffering to get out a new batch of
|
||||
/// output samples.
|
||||
///
|
||||
/// This value does not include initial buffering at beginning of a new processing
|
||||
/// stream, use SETTING_INITIAL_LATENCY to get the initial buffering size.
|
||||
///
|
||||
/// Notices:
|
||||
/// - This is read-only parameter, i.e. setSetting ignores this parameter
|
||||
/// - This parameter value is not constant but change depending on
|
||||
/// tempo/pitch/rate/samplerate settings.
|
||||
#define SETTING_NOMINAL_INPUT_SEQUENCE 6 |
||||
|
||||
|
||||
/// Call "getSetting" with this ID to query nominal average processing output
|
||||
/// size in samples. This value tells approcimate value how many output samples
|
||||
/// SoundTouch outputs once it does DSP processing run for a batch of input samples.
|
||||
///
|
||||
/// Notices:
|
||||
/// - This is read-only parameter, i.e. setSetting ignores this parameter
|
||||
/// - This parameter value is not constant but change depending on
|
||||
/// tempo/pitch/rate/samplerate settings.
|
||||
#define SETTING_NOMINAL_OUTPUT_SEQUENCE 7 |
||||
|
||||
|
||||
/// Call "getSetting" with this ID to query initial processing latency, i.e.
|
||||
/// approx. how many samples you'll need to enter to SoundTouch pipeline before
|
||||
/// you can expect to get first batch of ready output samples out.
|
||||
///
|
||||
/// After the first output batch, you can then expect to get approx.
|
||||
/// SETTING_NOMINAL_OUTPUT_SEQUENCE ready samples out for every
|
||||
/// SETTING_NOMINAL_INPUT_SEQUENCE samples that you enter into SoundTouch.
|
||||
///
|
||||
/// Example:
|
||||
/// processing with parameter -tempo=5
|
||||
/// => initial latency = 5509 samples
|
||||
/// input sequence = 4167 samples
|
||||
/// output sequence = 3969 samples
|
||||
///
|
||||
/// Accordingly, you can expect to feed in approx. 5509 samples at beginning of
|
||||
/// the stream, and then you'll get out the first 3969 samples. After that, for
|
||||
/// every approx. 4167 samples that you'll put in, you'll receive again approx.
|
||||
/// 3969 samples out.
|
||||
///
|
||||
/// This also means that average latency during stream processing is
|
||||
/// INITIAL_LATENCY-OUTPUT_SEQUENCE/2, in the above example case 5509-3969/2
|
||||
/// = 3524 samples
|
||||
///
|
||||
/// Notices:
|
||||
/// - This is read-only parameter, i.e. setSetting ignores this parameter
|
||||
/// - This parameter value is not constant but change depending on
|
||||
/// tempo/pitch/rate/samplerate settings.
|
||||
#define SETTING_INITIAL_LATENCY 8 |
||||
|
||||
|
||||
class SoundTouch final : public FIFOProcessor |
||||
{ |
||||
private: |
||||
/// Rate transposer class instance
|
||||
class RateTransposer *pRateTransposer; |
||||
|
||||
/// Time-stretch class instance
|
||||
class TDStretch *pTDStretch; |
||||
|
||||
/// Virtual pitch parameter. Effective rate & tempo are calculated from these parameters.
|
||||
double virtualRate; |
||||
|
||||
/// Virtual pitch parameter. Effective rate & tempo are calculated from these parameters.
|
||||
double virtualTempo; |
||||
|
||||
/// Virtual pitch parameter. Effective rate & tempo are calculated from these parameters.
|
||||
double virtualPitch; |
||||
|
||||
/// Flag: Has sample rate been set?
|
||||
bool bSrateSet; |
||||
|
||||
/// Accumulator for how many samples in total will be expected as output vs. samples put in,
|
||||
/// considering current processing settings.
|
||||
double samplesExpectedOut; |
||||
|
||||
/// Accumulator for how many samples in total have been read out from the processing so far
|
||||
long samplesOutput; |
||||
|
||||
/// Calculates effective rate & tempo valuescfrom 'virtualRate', 'virtualTempo' and
|
||||
/// 'virtualPitch' parameters.
|
||||
void calcEffectiveRateAndTempo(); |
||||
|
||||
protected : |
||||
/// Number of channels
|
||||
uint channels; |
||||
|
||||
/// Effective 'rate' value calculated from 'virtualRate', 'virtualTempo' and 'virtualPitch'
|
||||
double rate; |
||||
|
||||
/// Effective 'tempo' value calculated from 'virtualRate', 'virtualTempo' and 'virtualPitch'
|
||||
double tempo; |
||||
|
||||
public: |
||||
SoundTouch(); |
||||
~SoundTouch() override; |
||||
|
||||
/// Get SoundTouch library version string
|
||||
static const char *getVersionString(); |
||||
|
||||
/// Get SoundTouch library version Id
|
||||
static uint getVersionId(); |
||||
|
||||
/// Sets new rate control value. Normal rate = 1.0, smaller values
|
||||
/// represent slower rate, larger faster rates.
|
||||
void setRate(double newRate); |
||||
|
||||
/// Sets new tempo control value. Normal tempo = 1.0, smaller values
|
||||
/// represent slower tempo, larger faster tempo.
|
||||
void setTempo(double newTempo); |
||||
|
||||
/// Sets new rate control value as a difference in percents compared
|
||||
/// to the original rate (-50 .. +100 %)
|
||||
void setRateChange(double newRate); |
||||
|
||||
/// Sets new tempo control value as a difference in percents compared
|
||||
/// to the original tempo (-50 .. +100 %)
|
||||
void setTempoChange(double newTempo); |
||||
|
||||
/// Sets new pitch control value. Original pitch = 1.0, smaller values
|
||||
/// represent lower pitches, larger values higher pitch.
|
||||
void setPitch(double newPitch); |
||||
|
||||
/// Sets pitch change in octaves compared to the original pitch
|
||||
/// (-1.00 .. +1.00)
|
||||
void setPitchOctaves(double newPitch); |
||||
|
||||
/// Sets pitch change in semi-tones compared to the original pitch
|
||||
/// (-12 .. +12)
|
||||
void setPitchSemiTones(int newPitch); |
||||
void setPitchSemiTones(double newPitch); |
||||
|
||||
/// Sets the number of channels, 1 = mono, 2 = stereo
|
||||
void setChannels(uint numChannels); |
||||
|
||||
/// Sets sample rate.
|
||||
void setSampleRate(uint srate); |
||||
|
||||
/// Get ratio between input and output audio durations, useful for calculating
|
||||
/// processed output duration: if you'll process a stream of N samples, then
|
||||
/// you can expect to get out N * getInputOutputSampleRatio() samples.
|
||||
///
|
||||
/// This ratio will give accurate target duration ratio for a full audio track,
|
||||
/// given that the the whole track is processed with same processing parameters.
|
||||
///
|
||||
/// If this ratio is applied to calculate intermediate offsets inside a processing
|
||||
/// stream, then this ratio is approximate and can deviate +- some tens of milliseconds
|
||||
/// from ideal offset, yet by end of the audio stream the duration ratio will become
|
||||
/// exact.
|
||||
///
|
||||
/// Example: if processing with parameters "-tempo=15 -pitch=-3", the function
|
||||
/// will return value 0.8695652... Now, if processing an audio stream whose duration
|
||||
/// is exactly one million audio samples, then you can expect the processed
|
||||
/// output duration be 0.869565 * 1000000 = 869565 samples.
|
||||
double getInputOutputSampleRatio(); |
||||
|
||||
/// Flushes the last samples from the processing pipeline to the output.
|
||||
/// Clears also the internal processing buffers.
|
||||
//
|
||||
/// Note: This function is meant for extracting the last samples of a sound
|
||||
/// stream. This function may introduce additional blank samples in the end
|
||||
/// of the sound stream, and thus it's not recommended to call this function
|
||||
/// in the middle of a sound stream.
|
||||
void flush(); |
||||
|
||||
/// Adds 'numSamples' pcs of samples from the 'samples' memory position into
|
||||
/// the input of the object. Notice that sample rate _has_to_ be set before
|
||||
/// calling this function, otherwise throws a runtime_error exception.
|
||||
void putSamples( |
||||
const SAMPLETYPE *samples, ///< Pointer to sample buffer.
|
||||
uint numSamples ///< Number of samples in buffer. Notice
|
||||
///< that in case of stereo-sound a single sample
|
||||
///< contains data for both channels.
|
||||
) override; |
||||
|
||||
/// Output samples from beginning of the sample buffer. Copies requested samples to
|
||||
/// output buffer and removes them from the sample buffer. If there are less than
|
||||
/// 'numsample' samples in the buffer, returns all that available.
|
||||
///
|
||||
/// \return Number of samples returned.
|
||||
uint receiveSamples(SAMPLETYPE *output, ///< Buffer where to copy output samples.
|
||||
uint maxSamples ///< How many samples to receive at max.
|
||||
) override; |
||||
|
||||
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
|
||||
/// sample buffer without copying them anywhere.
|
||||
///
|
||||
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
|
||||
/// with 'ptrBegin' function.
|
||||
uint receiveSamples(uint maxSamples ///< Remove this many samples from the beginning of pipe.
|
||||
) override; |
||||
|
||||
/// Clears all the samples in the object's output and internal processing
|
||||
/// buffers.
|
||||
void clear() override; |
||||
|
||||
/// Changes a setting controlling the processing system behaviour. See the
|
||||
/// 'SETTING_...' defines for available setting ID's.
|
||||
///
|
||||
/// \return 'true' if the setting was successfully changed
|
||||
bool setSetting(int settingId, ///< Setting ID number. see SETTING_... defines.
|
||||
int value ///< New setting value.
|
||||
); |
||||
|
||||
/// Reads a setting controlling the processing system behaviour. See the
|
||||
/// 'SETTING_...' defines for available setting ID's.
|
||||
///
|
||||
/// \return the setting value.
|
||||
int getSetting(int settingId ///< Setting ID number, see SETTING_... defines.
|
||||
) const; |
||||
|
||||
/// Returns number of samples currently unprocessed.
|
||||
uint numUnprocessedSamples() const; |
||||
|
||||
/// Return number of channels
|
||||
uint numChannels() const |
||||
{ |
||||
return channels; |
||||
} |
||||
|
||||
/// Other handy functions that are implemented in the ancestor classes (see
|
||||
/// classes 'FIFOProcessor' and 'FIFOSamplePipe')
|
||||
///
|
||||
/// - receiveSamples() : Use this function to receive 'ready' processed samples from SoundTouch.
|
||||
/// - numSamples() : Get number of 'ready' samples that can be received with
|
||||
/// function 'receiveSamples()'
|
||||
/// - isEmpty() : Returns nonzero if there aren't any 'ready' samples.
|
||||
/// - clear() : Clears all samples from ready/processing buffers.
|
||||
}; |
||||
|
||||
} |
||||
#endif |
||||
@ -0,0 +1,177 @@
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// SoundTouch DLL wrapper - wraps SoundTouch routines into a Dynamic Load
|
||||
/// Library interface.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef _SoundTouchDLL_h_ |
||||
#define _SoundTouchDLL_h_ |
||||
|
||||
#if defined(_WIN32) || defined(WIN32) |
||||
// Windows
|
||||
#ifndef __cplusplus |
||||
#error "Expected g++" |
||||
#endif |
||||
|
||||
#ifdef DLL_EXPORTS |
||||
#define SOUNDTOUCHDLL_API extern "C" __declspec(dllexport) |
||||
#else |
||||
#define SOUNDTOUCHDLL_API extern "C" __declspec(dllimport) |
||||
#endif |
||||
|
||||
#else |
||||
// GNU version
|
||||
|
||||
#if defined(DLL_EXPORTS) || defined(SoundTouchDLL_EXPORTS) |
||||
// GCC declaration for exporting functions
|
||||
#define SOUNDTOUCHDLL_API extern "C" __attribute__((__visibility__("default"))) |
||||
#else |
||||
// import function
|
||||
#define SOUNDTOUCHDLL_API extern "C" |
||||
#endif |
||||
|
||||
// Linux-replacements for Windows declarations:
|
||||
#define __cdecl |
||||
typedef unsigned int DWORD; |
||||
#define FALSE 0 |
||||
#define TRUE 1 |
||||
|
||||
#endif |
||||
|
||||
typedef void * ST_HANDLE; |
||||
|
||||
/// Create a new instance of SoundTouch processor.
|
||||
SOUNDTOUCHDLL_API ST_HANDLE __cdecl soundtouch_createInstance(); |
||||
|
||||
/// Destroys a SoundTouch processor instance.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_destroyInstance(ST_HANDLE h); |
||||
|
||||
/// Get SoundTouch library version string
|
||||
SOUNDTOUCHDLL_API const char *__cdecl soundtouch_getVersionString(); |
||||
|
||||
/// Get SoundTouch library version Id
|
||||
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_getVersionId(); |
||||
|
||||
/// Sets new rate control value. Normal rate = 1.0, smaller values
|
||||
/// represent slower rate, larger faster rates.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRate(ST_HANDLE h, float newRate); |
||||
|
||||
/// Sets new tempo control value. Normal tempo = 1.0, smaller values
|
||||
/// represent slower tempo, larger faster tempo.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setTempo(ST_HANDLE h, float newTempo); |
||||
|
||||
/// Sets new rate control value as a difference in percents compared
|
||||
/// to the original rate (-50 .. +100 %);
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRateChange(ST_HANDLE h, float newRate); |
||||
|
||||
/// Sets new tempo control value as a difference in percents compared
|
||||
/// to the original tempo (-50 .. +100 %);
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setTempoChange(ST_HANDLE h, float newTempo); |
||||
|
||||
/// Sets new pitch control value. Original pitch = 1.0, smaller values
|
||||
/// represent lower pitches, larger values higher pitch.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitch(ST_HANDLE h, float newPitch); |
||||
|
||||
/// Sets pitch change in octaves compared to the original pitch
|
||||
/// (-1.00 .. +1.00);
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitchOctaves(ST_HANDLE h, float newPitch); |
||||
|
||||
/// Sets pitch change in semi-tones compared to the original pitch
|
||||
/// (-12 .. +12);
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitchSemiTones(ST_HANDLE h, float newPitch); |
||||
|
||||
|
||||
/// Sets the number of channels, 1 = mono, 2 = stereo, n = multichannel
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setChannels(ST_HANDLE h, unsigned int numChannels); |
||||
|
||||
/// Sets sample rate.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setSampleRate(ST_HANDLE h, unsigned int srate); |
||||
|
||||
/// Flushes the last samples from the processing pipeline to the output.
|
||||
/// Clears also the internal processing buffers.
|
||||
//
|
||||
/// Note: This function is meant for extracting the last samples of a sound
|
||||
/// stream. This function may introduce additional blank samples in the end
|
||||
/// of the sound stream, and thus it's not recommended to call this function
|
||||
/// in the middle of a sound stream.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_flush(ST_HANDLE h); |
||||
|
||||
/// Adds 'numSamples' pcs of samples from the 'samples' memory position into
|
||||
/// the input of the object. Notice that sample rate _has_to_ be set before
|
||||
/// calling this function, otherwise throws a runtime_error exception.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_putSamples(ST_HANDLE h, |
||||
const float *samples, ///< Pointer to sample buffer.
|
||||
unsigned int numSamples ///< Number of sample frames in buffer. Notice
|
||||
///< that in case of multi-channel sound a single
|
||||
///< sample frame contains data for all channels.
|
||||
); |
||||
|
||||
/// Clears all the samples in the object's output and internal processing
|
||||
/// buffers.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_clear(ST_HANDLE h); |
||||
|
||||
/// Changes a setting controlling the processing system behaviour. See the
|
||||
/// 'SETTING_...' defines for available setting ID's.
|
||||
///
|
||||
/// \return 'nonzero' if the setting was successfully changed, otherwise zero
|
||||
SOUNDTOUCHDLL_API int __cdecl soundtouch_setSetting(ST_HANDLE h, |
||||
int settingId, ///< Setting ID number. see SETTING_... defines.
|
||||
int value ///< New setting value.
|
||||
); |
||||
|
||||
/// Reads a setting controlling the processing system behaviour. See the
|
||||
/// 'SETTING_...' defines for available setting ID's.
|
||||
///
|
||||
/// \return the setting value.
|
||||
SOUNDTOUCHDLL_API int __cdecl soundtouch_getSetting(ST_HANDLE h, |
||||
int settingId ///< Setting ID number, see SETTING_... defines.
|
||||
); |
||||
|
||||
|
||||
/// Returns number of samples currently unprocessed.
|
||||
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_numUnprocessedSamples(ST_HANDLE h); |
||||
|
||||
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
|
||||
/// sample buffer without copying them anywhere.
|
||||
///
|
||||
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
|
||||
/// with 'ptrBegin' function.
|
||||
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_receiveSamples(ST_HANDLE h, |
||||
float *outBuffer, ///< Buffer where to copy output samples.
|
||||
unsigned int maxSamples ///< How many samples to receive at max.
|
||||
); |
||||
|
||||
/// Returns number of samples currently available.
|
||||
SOUNDTOUCHDLL_API unsigned int __cdecl soundtouch_numSamples(ST_HANDLE h); |
||||
|
||||
/// Returns nonzero if there aren't any samples available for outputting.
|
||||
SOUNDTOUCHDLL_API int __cdecl soundtouch_isEmpty(ST_HANDLE h); |
||||
|
||||
#endif // _SoundTouchDLL_h_
|
||||
|
||||
@ -0,0 +1,66 @@
|
||||
# SoundTouch library |
||||
|
||||
## About |
||||
|
||||
SoundTouch is an open-source audio processing library that allows changing the sound tempo, pitch and playback rate parameters independently from each other: |
||||
* Change **tempo** while maintaining the original pitch |
||||
* Change **pitch** while maintaining the original tempo |
||||
* Change **playback rate** that affects both tempo and pitch at the |
||||
same time |
||||
* Change any combination of tempo/pitch/rate |
||||
|
||||
Visit [SoundTouch website](https://www.surina.net/soundtouch) and see the [README file](https://www.surina.net/soundtouch/readme.html) for more information and audio examples. |
||||
|
||||
### The latest stable release is 2.3.3 |
||||
|
||||
## Example |
||||
|
||||
Use SoundStretch example app for modifying wav audio files, for example as follows: |
||||
|
||||
``` |
||||
soundstretch my_original_file.wav output_file.wav -tempo=+15 -pitch=-3 |
||||
``` |
||||
|
||||
See the [README file](http://soundtouch.surina.net/README.html) for more usage examples and instructions how to build SoundTouch + SoundStretch. |
||||
|
||||
Ready [SoundStretch application executables](https://www.surina.net/soundtouch/download.html) are available for download for Windows and Mac OS. |
||||
|
||||
## Language & Platforms |
||||
|
||||
SoundTouch is written in C++ and compiles in virtually any platform: |
||||
* Windows |
||||
* Mac OS |
||||
* Linux & Unices (including also Raspberry, Beaglebone, Yocto etc embedded Linux flavors) |
||||
* Android |
||||
* iOS |
||||
* embedded systems |
||||
|
||||
The source code package includes dynamic library import modules for C#, Java and Pascal/Delphi languages. |
||||
|
||||
## Tarballs |
||||
|
||||
Source code release tarballs: |
||||
* https://www.surina.net/soundtouch/soundtouch-2.3.3.tar.gz |
||||
* https://www.surina.net/soundtouch/soundtouch-2.3.2.tar.gz |
||||
* https://www.surina.net/soundtouch/soundtouch-2.3.1.tar.gz |
||||
* https://www.surina.net/soundtouch/soundtouch-2.3.0.tar.gz |
||||
* https://www.surina.net/soundtouch/soundtouch-2.2.0.tar.gz |
||||
* https://www.surina.net/soundtouch/soundtouch-2.1.2.tar.gz |
||||
* https://www.surina.net/soundtouch/soundtouch-2.1.1.tar.gz |
||||
* https://www.surina.net/soundtouch/soundtouch-2.0.0.tar.gz |
||||
|
||||
## License |
||||
|
||||
SoundTouch is released under LGPL v2.1: |
||||
|
||||
This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation. |
||||
|
||||
This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. |
||||
|
||||
You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
||||
|
||||
See [LGPL v2.1 full license text ](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) for details. |
||||
|
||||
-- |
||||
|
||||
Also commercial license free of GPL limitations available upon request |
||||
@ -0,0 +1,222 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// FIR low-pass (anti-alias) filter with filter coefficient design routine and
|
||||
/// MMX optimization.
|
||||
///
|
||||
/// Anti-alias filter is used to prevent folding of high frequencies when
|
||||
/// transposing the sample rate with interpolation.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <memory.h> |
||||
#include <assert.h> |
||||
#include <math.h> |
||||
#include <stdlib.h> |
||||
#include "AAFilter.h" |
||||
#include "FIRFilter.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
#define PI 3.14159265358979323846 |
||||
#define TWOPI (2 * PI) |
||||
|
||||
// define this to save AA filter coefficients to a file
|
||||
// #define _DEBUG_SAVE_AAFILTER_COEFFICIENTS 1
|
||||
|
||||
#ifdef _DEBUG_SAVE_AAFILTER_COEFFICIENTS |
||||
#include <stdio.h> |
||||
|
||||
static void _DEBUG_SAVE_AAFIR_COEFFS(SAMPLETYPE *coeffs, int len) |
||||
{ |
||||
FILE *fptr = fopen("aa_filter_coeffs.txt", "wt"); |
||||
if (fptr == nullptr) return; |
||||
|
||||
for (int i = 0; i < len; i ++) |
||||
{ |
||||
double temp = coeffs[i]; |
||||
fprintf(fptr, "%lf\n", temp); |
||||
} |
||||
fclose(fptr); |
||||
} |
||||
|
||||
#else |
||||
#define _DEBUG_SAVE_AAFIR_COEFFS(x, y) |
||||
#endif |
||||
|
||||
/*****************************************************************************
|
||||
* |
||||
* Implementation of the class 'AAFilter' |
||||
* |
||||
*****************************************************************************/ |
||||
|
||||
AAFilter::AAFilter(uint len) |
||||
{ |
||||
pFIR = FIRFilter::newInstance(); |
||||
cutoffFreq = 0.5; |
||||
setLength(len); |
||||
} |
||||
|
||||
|
||||
AAFilter::~AAFilter() |
||||
{ |
||||
delete pFIR; |
||||
} |
||||
|
||||
|
||||
// Sets new anti-alias filter cut-off edge frequency, scaled to
|
||||
// sampling frequency (nyquist frequency = 0.5).
|
||||
// The filter will cut frequencies higher than the given frequency.
|
||||
void AAFilter::setCutoffFreq(double newCutoffFreq) |
||||
{ |
||||
cutoffFreq = newCutoffFreq; |
||||
calculateCoeffs(); |
||||
} |
||||
|
||||
|
||||
// Sets number of FIR filter taps
|
||||
void AAFilter::setLength(uint newLength) |
||||
{ |
||||
length = newLength; |
||||
calculateCoeffs(); |
||||
} |
||||
|
||||
|
||||
// Calculates coefficients for a low-pass FIR filter using Hamming window
|
||||
void AAFilter::calculateCoeffs() |
||||
{ |
||||
uint i; |
||||
double cntTemp, temp, tempCoeff,h, w; |
||||
double wc; |
||||
double scaleCoeff, sum; |
||||
double *work; |
||||
SAMPLETYPE *coeffs; |
||||
|
||||
assert(length >= 2); |
||||
assert(length % 4 == 0); |
||||
assert(cutoffFreq >= 0); |
||||
assert(cutoffFreq <= 0.5); |
||||
|
||||
work = new double[length]; |
||||
coeffs = new SAMPLETYPE[length]; |
||||
|
||||
wc = 2.0 * PI * cutoffFreq; |
||||
tempCoeff = TWOPI / (double)length; |
||||
|
||||
sum = 0; |
||||
for (i = 0; i < length; i ++) |
||||
{ |
||||
cntTemp = (double)i - (double)(length / 2); |
||||
|
||||
temp = cntTemp * wc; |
||||
if (temp != 0) |
||||
{ |
||||
h = sin(temp) / temp; // sinc function
|
||||
} |
||||
else |
||||
{ |
||||
h = 1.0; |
||||
} |
||||
w = 0.54 + 0.46 * cos(tempCoeff * cntTemp); // hamming window
|
||||
|
||||
temp = w * h; |
||||
work[i] = temp; |
||||
|
||||
// calc net sum of coefficients
|
||||
sum += temp; |
||||
} |
||||
|
||||
// ensure the sum of coefficients is larger than zero
|
||||
assert(sum > 0); |
||||
|
||||
// ensure we've really designed a lowpass filter...
|
||||
assert(work[length/2] > 0); |
||||
assert(work[length/2 + 1] > -1e-6); |
||||
assert(work[length/2 - 1] > -1e-6); |
||||
|
||||
// Calculate a scaling coefficient in such a way that the result can be
|
||||
// divided by 16384
|
||||
scaleCoeff = 16384.0f / sum; |
||||
|
||||
for (i = 0; i < length; i ++) |
||||
{ |
||||
temp = work[i] * scaleCoeff; |
||||
// scale & round to nearest integer
|
||||
temp += (temp >= 0) ? 0.5 : -0.5; |
||||
// ensure no overfloods
|
||||
assert(temp >= -32768 && temp <= 32767); |
||||
coeffs[i] = (SAMPLETYPE)temp; |
||||
} |
||||
|
||||
// Set coefficients. Use divide factor 14 => divide result by 2^14 = 16384
|
||||
pFIR->setCoefficients(coeffs, length, 14); |
||||
|
||||
_DEBUG_SAVE_AAFIR_COEFFS(coeffs, length); |
||||
|
||||
delete[] work; |
||||
delete[] coeffs; |
||||
} |
||||
|
||||
|
||||
// Applies the filter to the given sequence of samples.
|
||||
// Note : The amount of outputted samples is by value of 'filter length'
|
||||
// smaller than the amount of input samples.
|
||||
uint AAFilter::evaluate(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels) const |
||||
{ |
||||
return pFIR->evaluate(dest, src, numSamples, numChannels); |
||||
} |
||||
|
||||
|
||||
/// Applies the filter to the given src & dest pipes, so that processed amount of
|
||||
/// samples get removed from src, and produced amount added to dest
|
||||
/// Note : The amount of outputted samples is by value of 'filter length'
|
||||
/// smaller than the amount of input samples.
|
||||
uint AAFilter::evaluate(FIFOSampleBuffer &dest, FIFOSampleBuffer &src) const |
||||
{ |
||||
SAMPLETYPE *pdest; |
||||
const SAMPLETYPE *psrc; |
||||
uint numSrcSamples; |
||||
uint result; |
||||
int numChannels = src.getChannels(); |
||||
|
||||
assert(numChannels == dest.getChannels()); |
||||
|
||||
numSrcSamples = src.numSamples(); |
||||
psrc = src.ptrBegin(); |
||||
pdest = dest.ptrEnd(numSrcSamples); |
||||
result = pFIR->evaluate(pdest, psrc, numSrcSamples, numChannels); |
||||
src.receiveSamples(result); |
||||
dest.putSamples(result); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
|
||||
uint AAFilter::getLength() const |
||||
{ |
||||
return pFIR->getLength(); |
||||
} |
||||
@ -0,0 +1,93 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Sampled sound tempo changer/time stretch algorithm. Changes the sound tempo
|
||||
/// while maintaining the original pitch by using a time domain WSOLA-like method
|
||||
/// with several performance-increasing tweaks.
|
||||
///
|
||||
/// Anti-alias filter is used to prevent folding of high frequencies when
|
||||
/// transposing the sample rate with interpolation.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef AAFilter_H |
||||
#define AAFilter_H |
||||
|
||||
#include "STTypes.h" |
||||
#include "FIFOSampleBuffer.h" |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
class AAFilter |
||||
{ |
||||
protected: |
||||
class FIRFilter *pFIR; |
||||
|
||||
/// Low-pass filter cut-off frequency, negative = invalid
|
||||
double cutoffFreq; |
||||
|
||||
/// num of filter taps
|
||||
uint length; |
||||
|
||||
/// Calculate the FIR coefficients realizing the given cutoff-frequency
|
||||
void calculateCoeffs(); |
||||
public: |
||||
AAFilter(uint length); |
||||
|
||||
~AAFilter(); |
||||
|
||||
/// Sets new anti-alias filter cut-off edge frequency, scaled to sampling
|
||||
/// frequency (nyquist frequency = 0.5). The filter will cut off the
|
||||
/// frequencies than that.
|
||||
void setCutoffFreq(double newCutoffFreq); |
||||
|
||||
/// Sets number of FIR filter taps, i.e. ~filter complexity
|
||||
void setLength(uint newLength); |
||||
|
||||
uint getLength() const; |
||||
|
||||
/// Applies the filter to the given sequence of samples.
|
||||
/// Note : The amount of outputted samples is by value of 'filter length'
|
||||
/// smaller than the amount of input samples.
|
||||
uint evaluate(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
uint numSamples, |
||||
uint numChannels) const; |
||||
|
||||
/// Applies the filter to the given src & dest pipes, so that processed amount of
|
||||
/// samples get removed from src, and produced amount added to dest
|
||||
/// Note : The amount of outputted samples is by value of 'filter length'
|
||||
/// smaller than the amount of input samples.
|
||||
uint evaluate(FIFOSampleBuffer &dest, |
||||
FIFOSampleBuffer &src) const; |
||||
|
||||
}; |
||||
|
||||
} |
||||
|
||||
#endif |
||||
@ -0,0 +1,569 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Beats-per-minute (BPM) detection routine.
|
||||
///
|
||||
/// The beat detection algorithm works as follows:
|
||||
/// - Use function 'inputSamples' to input a chunks of samples to the class for
|
||||
/// analysis. It's a good idea to enter a large sound file or stream in smallish
|
||||
/// chunks of around few kilosamples in order not to extinguish too much RAM memory.
|
||||
/// - Inputted sound data is decimated to approx 500 Hz to reduce calculation burden,
|
||||
/// which is basically ok as low (bass) frequencies mostly determine the beat rate.
|
||||
/// Simple averaging is used for anti-alias filtering because the resulting signal
|
||||
/// quality isn't of that high importance.
|
||||
/// - Decimated sound data is enveloped, i.e. the amplitude shape is detected by
|
||||
/// taking absolute value that's smoothed by sliding average. Signal levels that
|
||||
/// are below a couple of times the general RMS amplitude level are cut away to
|
||||
/// leave only notable peaks there.
|
||||
/// - Repeating sound patterns (e.g. beats) are detected by calculating short-term
|
||||
/// autocorrelation function of the enveloped signal.
|
||||
/// - After whole sound data file has been analyzed as above, the bpm level is
|
||||
/// detected by function 'getBpm' that finds the highest peak of the autocorrelation
|
||||
/// function, calculates it's precise location and converts this reading to bpm's.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#define _USE_MATH_DEFINES |
||||
|
||||
#include <math.h> |
||||
#include <assert.h> |
||||
#include <string.h> |
||||
#include <stdio.h> |
||||
#include <cfloat> |
||||
#include "FIFOSampleBuffer.h" |
||||
#include "PeakFinder.h" |
||||
#include "BPMDetect.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
// algorithm input sample block size
|
||||
static const int INPUT_BLOCK_SIZE = 2048; |
||||
|
||||
// decimated sample block size
|
||||
static const int DECIMATED_BLOCK_SIZE = 256; |
||||
|
||||
/// Target sample rate after decimation
|
||||
static const int TARGET_SRATE = 1000; |
||||
|
||||
/// XCorr update sequence size, update in about 200msec chunks
|
||||
static const int XCORR_UPDATE_SEQUENCE = (int)(TARGET_SRATE / 5); |
||||
|
||||
/// Moving average N size
|
||||
static const int MOVING_AVERAGE_N = 15; |
||||
|
||||
/// XCorr decay time constant, decay to half in 30 seconds
|
||||
/// If it's desired to have the system adapt quicker to beat rate
|
||||
/// changes within a continuing music stream, then the
|
||||
/// 'xcorr_decay_time_constant' value can be reduced, yet that
|
||||
/// can increase possibility of glitches in bpm detection.
|
||||
static const double XCORR_DECAY_TIME_CONSTANT = 30.0; |
||||
|
||||
/// Data overlap factor for beat detection algorithm
|
||||
static const int OVERLAP_FACTOR = 4; |
||||
|
||||
static const double TWOPI = (2 * M_PI); |
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Enable following define to create bpm analysis file:
|
||||
|
||||
//#define _CREATE_BPM_DEBUG_FILE
|
||||
|
||||
#ifdef _CREATE_BPM_DEBUG_FILE |
||||
|
||||
static void _SaveDebugData(const char *name, const float *data, int minpos, int maxpos, double coeff) |
||||
{ |
||||
FILE *fptr = fopen(name, "wt"); |
||||
int i; |
||||
|
||||
if (fptr) |
||||
{ |
||||
printf("\nWriting BPM debug data into file %s\n", name); |
||||
for (i = minpos; i < maxpos; i ++) |
||||
{ |
||||
fprintf(fptr, "%d\t%.1lf\t%f\n", i, coeff / (double)i, data[i]); |
||||
} |
||||
fclose(fptr); |
||||
} |
||||
} |
||||
|
||||
void _SaveDebugBeatPos(const char *name, const std::vector<BEAT> &beats) |
||||
{ |
||||
printf("\nWriting beat detections data into file %s\n", name); |
||||
|
||||
FILE *fptr = fopen(name, "wt"); |
||||
if (fptr) |
||||
{ |
||||
for (uint i = 0; i < beats.size(); i++) |
||||
{ |
||||
BEAT b = beats[i]; |
||||
fprintf(fptr, "%lf\t%lf\n", b.pos, b.strength); |
||||
} |
||||
fclose(fptr); |
||||
} |
||||
} |
||||
#else |
||||
#define _SaveDebugData(name, a,b,c,d) |
||||
#define _SaveDebugBeatPos(name, b) |
||||
#endif |
||||
|
||||
// Hamming window
|
||||
void hamming(float *w, int N) |
||||
{ |
||||
for (int i = 0; i < N; i++) |
||||
{ |
||||
w[i] = (float)(0.54 - 0.46 * cos(TWOPI * i / (N - 1))); |
||||
} |
||||
|
||||
} |
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// IIR2_filter - 2nd order IIR filter
|
||||
|
||||
IIR2_filter::IIR2_filter(const double *lpf_coeffs) |
||||
{ |
||||
memcpy(coeffs, lpf_coeffs, 5 * sizeof(double)); |
||||
memset(prev, 0, sizeof(prev)); |
||||
} |
||||
|
||||
|
||||
float IIR2_filter::update(float x) |
||||
{ |
||||
prev[0] = x; |
||||
double y = x * coeffs[0]; |
||||
|
||||
for (int i = 4; i >= 1; i--) |
||||
{ |
||||
y += coeffs[i] * prev[i]; |
||||
prev[i] = prev[i - 1]; |
||||
} |
||||
|
||||
prev[3] = y; |
||||
return (float)y; |
||||
} |
||||
|
||||
|
||||
// IIR low-pass filter coefficients, calculated with matlab/octave cheby2(2,40,0.05)
|
||||
const double _LPF_coeffs[5] = { 0.00996655391939, -0.01944529148401, 0.00996655391939, 1.96867605796247, -0.96916387431724 }; |
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
BPMDetect::BPMDetect(int numChannels, int aSampleRate) : |
||||
beat_lpf(_LPF_coeffs) |
||||
{ |
||||
beats.reserve(250); // initial reservation to prevent frequent reallocation
|
||||
|
||||
this->sampleRate = aSampleRate; |
||||
this->channels = numChannels; |
||||
|
||||
decimateSum = 0; |
||||
decimateCount = 0; |
||||
|
||||
// choose decimation factor so that result is approx. 1000 Hz
|
||||
decimateBy = sampleRate / TARGET_SRATE; |
||||
if ((decimateBy <= 0) || (decimateBy * DECIMATED_BLOCK_SIZE < INPUT_BLOCK_SIZE)) |
||||
{ |
||||
ST_THROW_RT_ERROR("Too small samplerate"); |
||||
} |
||||
|
||||
// Calculate window length & starting item according to desired min & max bpms
|
||||
windowLen = (60 * sampleRate) / (decimateBy * MIN_BPM); |
||||
windowStart = (60 * sampleRate) / (decimateBy * MAX_BPM_RANGE); |
||||
|
||||
assert(windowLen > windowStart); |
||||
|
||||
// allocate new working objects
|
||||
xcorr = new float[windowLen]; |
||||
memset(xcorr, 0, windowLen * sizeof(float)); |
||||
|
||||
pos = 0; |
||||
peakPos = 0; |
||||
peakVal = 0; |
||||
init_scaler = 1; |
||||
beatcorr_ringbuffpos = 0; |
||||
beatcorr_ringbuff = new float[windowLen]; |
||||
memset(beatcorr_ringbuff, 0, windowLen * sizeof(float)); |
||||
|
||||
// allocate processing buffer
|
||||
buffer = new FIFOSampleBuffer(); |
||||
// we do processing in mono mode
|
||||
buffer->setChannels(1); |
||||
buffer->clear(); |
||||
|
||||
// calculate hamming windows
|
||||
hamw = new float[XCORR_UPDATE_SEQUENCE]; |
||||
hamming(hamw, XCORR_UPDATE_SEQUENCE); |
||||
hamw2 = new float[XCORR_UPDATE_SEQUENCE / 2]; |
||||
hamming(hamw2, XCORR_UPDATE_SEQUENCE / 2); |
||||
} |
||||
|
||||
|
||||
BPMDetect::~BPMDetect() |
||||
{ |
||||
delete[] xcorr; |
||||
delete[] beatcorr_ringbuff; |
||||
delete[] hamw; |
||||
delete[] hamw2; |
||||
delete buffer; |
||||
} |
||||
|
||||
|
||||
/// convert to mono, low-pass filter & decimate to about 500 Hz.
|
||||
/// return number of outputted samples.
|
||||
///
|
||||
/// Decimation is used to remove the unnecessary frequencies and thus to reduce
|
||||
/// the amount of data needed to be processed as calculating autocorrelation
|
||||
/// function is a very-very heavy operation.
|
||||
///
|
||||
/// Anti-alias filtering is done simply by averaging the samples. This is really a
|
||||
/// poor-man's anti-alias filtering, but it's not so critical in this kind of application
|
||||
/// (it'd also be difficult to design a high-quality filter with steep cut-off at very
|
||||
/// narrow band)
|
||||
int BPMDetect::decimate(SAMPLETYPE *dest, const SAMPLETYPE *src, int numsamples) |
||||
{ |
||||
int count, outcount; |
||||
LONG_SAMPLETYPE out; |
||||
|
||||
assert(channels > 0); |
||||
assert(decimateBy > 0); |
||||
outcount = 0; |
||||
for (count = 0; count < numsamples; count ++) |
||||
{ |
||||
int j; |
||||
|
||||
// convert to mono and accumulate
|
||||
for (j = 0; j < channels; j ++) |
||||
{ |
||||
decimateSum += src[j]; |
||||
} |
||||
src += j; |
||||
|
||||
decimateCount ++; |
||||
if (decimateCount >= decimateBy) |
||||
{ |
||||
// Store every Nth sample only
|
||||
out = (LONG_SAMPLETYPE)(decimateSum / (decimateBy * channels)); |
||||
decimateSum = 0; |
||||
decimateCount = 0; |
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES |
||||
// check ranges for sure (shouldn't actually be necessary)
|
||||
if (out > 32767) |
||||
{ |
||||
out = 32767; |
||||
} |
||||
else if (out < -32768) |
||||
{ |
||||
out = -32768; |
||||
} |
||||
#endif // SOUNDTOUCH_INTEGER_SAMPLES
|
||||
dest[outcount] = (SAMPLETYPE)out; |
||||
outcount ++; |
||||
} |
||||
} |
||||
return outcount; |
||||
} |
||||
|
||||
|
||||
// Calculates autocorrelation function of the sample history buffer
|
||||
void BPMDetect::updateXCorr(int process_samples) |
||||
{ |
||||
int offs; |
||||
SAMPLETYPE *pBuffer; |
||||
|
||||
assert(buffer->numSamples() >= (uint)(process_samples + windowLen)); |
||||
assert(process_samples == XCORR_UPDATE_SEQUENCE); |
||||
|
||||
pBuffer = buffer->ptrBegin(); |
||||
|
||||
// calculate decay factor for xcorr filtering
|
||||
float xcorr_decay = (float)pow(0.5, 1.0 / (XCORR_DECAY_TIME_CONSTANT * TARGET_SRATE / process_samples)); |
||||
|
||||
// prescale pbuffer
|
||||
float tmp[XCORR_UPDATE_SEQUENCE]; |
||||
for (int i = 0; i < process_samples; i++) |
||||
{ |
||||
tmp[i] = hamw[i] * hamw[i] * pBuffer[i]; |
||||
} |
||||
|
||||
for (offs = windowStart; offs < windowLen; offs ++) |
||||
{ |
||||
float sum; |
||||
int i; |
||||
|
||||
sum = 0; |
||||
for (i = 0; i < process_samples; i ++) |
||||
{ |
||||
sum += tmp[i] * pBuffer[i + offs]; // scaling the sub-result shouldn't be necessary
|
||||
} |
||||
xcorr[offs] *= xcorr_decay; // decay 'xcorr' here with suitable time constant.
|
||||
|
||||
xcorr[offs] += (float)fabs(sum); |
||||
} |
||||
} |
||||
|
||||
|
||||
// Detect individual beat positions
|
||||
void BPMDetect::updateBeatPos(int process_samples) |
||||
{ |
||||
SAMPLETYPE *pBuffer; |
||||
|
||||
assert(buffer->numSamples() >= (uint)(process_samples + windowLen)); |
||||
|
||||
pBuffer = buffer->ptrBegin(); |
||||
assert(process_samples == XCORR_UPDATE_SEQUENCE / 2); |
||||
|
||||
// static double thr = 0.0003;
|
||||
double posScale = (double)this->decimateBy / (double)this->sampleRate; |
||||
int resetDur = (int)(0.12 / posScale + 0.5); |
||||
|
||||
// prescale pbuffer
|
||||
float tmp[XCORR_UPDATE_SEQUENCE / 2]; |
||||
for (int i = 0; i < process_samples; i++) |
||||
{ |
||||
tmp[i] = hamw2[i] * hamw2[i] * pBuffer[i]; |
||||
} |
||||
|
||||
for (int offs = windowStart; offs < windowLen; offs++) |
||||
{ |
||||
float sum = 0; |
||||
for (int i = 0; i < process_samples; i++) |
||||
{ |
||||
sum += tmp[i] * pBuffer[offs + i]; |
||||
} |
||||
beatcorr_ringbuff[(beatcorr_ringbuffpos + offs) % windowLen] += (float)((sum > 0) ? sum : 0); // accumulate only positive correlations
|
||||
} |
||||
|
||||
int skipstep = XCORR_UPDATE_SEQUENCE / OVERLAP_FACTOR; |
||||
|
||||
// compensate empty buffer at beginning by scaling coefficient
|
||||
float scale = (float)windowLen / (float)(skipstep * init_scaler); |
||||
if (scale > 1.0f) |
||||
{ |
||||
init_scaler++; |
||||
} |
||||
else |
||||
{ |
||||
scale = 1.0f; |
||||
} |
||||
|
||||
// detect beats
|
||||
for (int i = 0; i < skipstep; i++) |
||||
{ |
||||
float sum = beatcorr_ringbuff[beatcorr_ringbuffpos]; |
||||
sum -= beat_lpf.update(sum); |
||||
|
||||
if (sum > peakVal) |
||||
{ |
||||
// found new local largest value
|
||||
peakVal = sum; |
||||
peakPos = pos; |
||||
} |
||||
if (pos > peakPos + resetDur) |
||||
{ |
||||
// largest value not updated for 200msec => accept as beat
|
||||
peakPos += skipstep; |
||||
if (peakVal > 0) |
||||
{ |
||||
// add detected beat to end of "beats" vector
|
||||
BEAT temp = { (float)(peakPos * posScale), (float)(peakVal * scale) }; |
||||
beats.push_back(temp); |
||||
} |
||||
|
||||
peakVal = 0; |
||||
peakPos = pos; |
||||
} |
||||
|
||||
beatcorr_ringbuff[beatcorr_ringbuffpos] = 0; |
||||
pos++; |
||||
beatcorr_ringbuffpos = (beatcorr_ringbuffpos + 1) % windowLen; |
||||
} |
||||
} |
||||
|
||||
|
||||
#define max(x,y) ((x) > (y) ? (x) : (y)) |
||||
|
||||
void BPMDetect::inputSamples(const SAMPLETYPE *samples, int numSamples) |
||||
{ |
||||
SAMPLETYPE decimated[DECIMATED_BLOCK_SIZE]; |
||||
|
||||
// iterate so that max INPUT_BLOCK_SAMPLES processed per iteration
|
||||
while (numSamples > 0) |
||||
{ |
||||
int block; |
||||
int decSamples; |
||||
|
||||
block = (numSamples > INPUT_BLOCK_SIZE) ? INPUT_BLOCK_SIZE : numSamples; |
||||
|
||||
// decimate. note that converts to mono at the same time
|
||||
decSamples = decimate(decimated, samples, block); |
||||
samples += block * channels; |
||||
numSamples -= block; |
||||
|
||||
buffer->putSamples(decimated, decSamples); |
||||
} |
||||
|
||||
// when the buffer has enough samples for processing...
|
||||
int req = max(windowLen + XCORR_UPDATE_SEQUENCE, 2 * XCORR_UPDATE_SEQUENCE); |
||||
while ((int)buffer->numSamples() >= req) |
||||
{ |
||||
// ... update autocorrelations...
|
||||
updateXCorr(XCORR_UPDATE_SEQUENCE); |
||||
// ...update beat position calculation...
|
||||
updateBeatPos(XCORR_UPDATE_SEQUENCE / 2); |
||||
// ... and remove proceessed samples from the buffer
|
||||
int n = XCORR_UPDATE_SEQUENCE / OVERLAP_FACTOR; |
||||
buffer->receiveSamples(n); |
||||
} |
||||
} |
||||
|
||||
|
||||
void BPMDetect::removeBias() |
||||
{ |
||||
int i; |
||||
|
||||
// Remove linear bias: calculate linear regression coefficient
|
||||
// 1. calc mean of 'xcorr' and 'i'
|
||||
double mean_i = 0; |
||||
double mean_x = 0; |
||||
for (i = windowStart; i < windowLen; i++) |
||||
{ |
||||
mean_x += xcorr[i]; |
||||
} |
||||
mean_x /= (windowLen - windowStart); |
||||
mean_i = 0.5 * (windowLen - 1 + windowStart); |
||||
|
||||
// 2. calculate linear regression coefficient
|
||||
double b = 0; |
||||
double div = 0; |
||||
for (i = windowStart; i < windowLen; i++) |
||||
{ |
||||
double xt = xcorr[i] - mean_x; |
||||
double xi = i - mean_i; |
||||
b += xt * xi; |
||||
div += xi * xi; |
||||
} |
||||
b /= div; |
||||
|
||||
// subtract linear regression and resolve min. value bias
|
||||
float minval = FLT_MAX; // arbitrary large number
|
||||
for (i = windowStart; i < windowLen; i ++) |
||||
{ |
||||
xcorr[i] -= (float)(b * i); |
||||
if (xcorr[i] < minval) |
||||
{ |
||||
minval = xcorr[i]; |
||||
} |
||||
} |
||||
|
||||
// subtract min.value
|
||||
for (i = windowStart; i < windowLen; i ++) |
||||
{ |
||||
xcorr[i] -= minval; |
||||
} |
||||
} |
||||
|
||||
|
||||
// Calculate N-point moving average for "source" values
|
||||
void MAFilter(float *dest, const float *source, int start, int end, int N) |
||||
{ |
||||
for (int i = start; i < end; i++) |
||||
{ |
||||
int i1 = i - N / 2; |
||||
int i2 = i + N / 2 + 1; |
||||
if (i1 < start) i1 = start; |
||||
if (i2 > end) i2 = end; |
||||
|
||||
double sum = 0; |
||||
for (int j = i1; j < i2; j ++) |
||||
{ |
||||
sum += source[j]; |
||||
} |
||||
dest[i] = (float)(sum / (i2 - i1)); |
||||
} |
||||
} |
||||
|
||||
|
||||
float BPMDetect::getBpm() |
||||
{ |
||||
double peakPos; |
||||
double coeff; |
||||
PeakFinder peakFinder; |
||||
|
||||
// remove bias from xcorr data
|
||||
removeBias(); |
||||
|
||||
coeff = 60.0 * ((double)sampleRate / (double)decimateBy); |
||||
|
||||
// save bpm debug data if debug data writing enabled
|
||||
_SaveDebugData("soundtouch-bpm-xcorr.txt", xcorr, windowStart, windowLen, coeff); |
||||
|
||||
// Smoothen by N-point moving-average
|
||||
float *data = new float[windowLen]; |
||||
memset(data, 0, sizeof(float) * windowLen); |
||||
MAFilter(data, xcorr, windowStart, windowLen, MOVING_AVERAGE_N); |
||||
|
||||
// find peak position
|
||||
peakPos = peakFinder.detectPeak(data, windowStart, windowLen); |
||||
|
||||
// save bpm debug data if debug data writing enabled
|
||||
_SaveDebugData("soundtouch-bpm-smoothed.txt", data, windowStart, windowLen, coeff); |
||||
|
||||
delete[] data; |
||||
|
||||
assert(decimateBy != 0); |
||||
if (peakPos < 1e-9) return 0.0; // detection failed.
|
||||
|
||||
_SaveDebugBeatPos("soundtouch-detected-beats.txt", beats); |
||||
|
||||
// calculate BPM
|
||||
float bpm = (float)(coeff / peakPos); |
||||
return (bpm >= MIN_BPM && bpm <= MAX_BPM_VALID) ? bpm : 0; |
||||
} |
||||
|
||||
|
||||
/// Get beat position arrays. Note: The array includes also really low beat detection values
|
||||
/// in absence of clear strong beats. Consumer may wish to filter low values away.
|
||||
/// - "pos" receive array of beat positions
|
||||
/// - "values" receive array of beat detection strengths
|
||||
/// - max_num indicates max.size of "pos" and "values" array.
|
||||
///
|
||||
/// You can query a suitable array sized by calling this with nullptr in "pos" & "values".
|
||||
///
|
||||
/// \return number of beats in the arrays.
|
||||
int BPMDetect::getBeats(float *pos, float *values, int max_num) |
||||
{ |
||||
int num = (int)beats.size(); |
||||
if ((!pos) || (!values)) return num; // pos or values nullptr, return just size
|
||||
|
||||
for (int i = 0; (i < num) && (i < max_num); i++) |
||||
{ |
||||
pos[i] = beats[i].pos; |
||||
values[i] = beats[i].strength; |
||||
} |
||||
return num; |
||||
} |
||||
@ -0,0 +1,275 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// A buffer class for temporarily storaging sound samples, operates as a
|
||||
/// first-in-first-out pipe.
|
||||
///
|
||||
/// Samples are added to the end of the sample buffer with the 'putSamples'
|
||||
/// function, and are received from the beginning of the buffer by calling
|
||||
/// the 'receiveSamples' function. The class automatically removes the
|
||||
/// outputted samples from the buffer, as well as grows the buffer size
|
||||
/// whenever necessary.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <stdlib.h> |
||||
#include <memory.h> |
||||
#include <string.h> |
||||
#include <assert.h> |
||||
|
||||
#include "FIFOSampleBuffer.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
// Constructor
|
||||
FIFOSampleBuffer::FIFOSampleBuffer(int numChannels) |
||||
{ |
||||
assert(numChannels > 0); |
||||
sizeInBytes = 0; // reasonable initial value
|
||||
buffer = nullptr; |
||||
bufferUnaligned = nullptr; |
||||
samplesInBuffer = 0; |
||||
bufferPos = 0; |
||||
channels = (uint)numChannels; |
||||
ensureCapacity(32); // allocate initial capacity
|
||||
} |
||||
|
||||
|
||||
// destructor
|
||||
FIFOSampleBuffer::~FIFOSampleBuffer() |
||||
{ |
||||
delete[] bufferUnaligned; |
||||
bufferUnaligned = nullptr; |
||||
buffer = nullptr; |
||||
} |
||||
|
||||
|
||||
// Sets number of channels, 1 = mono, 2 = stereo
|
||||
void FIFOSampleBuffer::setChannels(int numChannels) |
||||
{ |
||||
uint usedBytes; |
||||
|
||||
if (!verifyNumberOfChannels(numChannels)) return; |
||||
|
||||
usedBytes = channels * samplesInBuffer; |
||||
channels = (uint)numChannels; |
||||
samplesInBuffer = usedBytes / channels; |
||||
} |
||||
|
||||
|
||||
// if output location pointer 'bufferPos' isn't zero, 'rewinds' the buffer and
|
||||
// zeroes this pointer by copying samples from the 'bufferPos' pointer
|
||||
// location on to the beginning of the buffer.
|
||||
void FIFOSampleBuffer::rewind() |
||||
{ |
||||
if (buffer && bufferPos) |
||||
{ |
||||
memmove(buffer, ptrBegin(), sizeof(SAMPLETYPE) * channels * samplesInBuffer); |
||||
bufferPos = 0; |
||||
} |
||||
} |
||||
|
||||
|
||||
// Adds 'numSamples' pcs of samples from the 'samples' memory position to
|
||||
// the sample buffer.
|
||||
void FIFOSampleBuffer::putSamples(const SAMPLETYPE *samples, uint nSamples) |
||||
{ |
||||
memcpy(ptrEnd(nSamples), samples, sizeof(SAMPLETYPE) * nSamples * channels); |
||||
samplesInBuffer += nSamples; |
||||
} |
||||
|
||||
|
||||
// Increases the number of samples in the buffer without copying any actual
|
||||
// samples.
|
||||
//
|
||||
// This function is used to update the number of samples in the sample buffer
|
||||
// when accessing the buffer directly with 'ptrEnd' function. Please be
|
||||
// careful though!
|
||||
void FIFOSampleBuffer::putSamples(uint nSamples) |
||||
{ |
||||
uint req; |
||||
|
||||
req = samplesInBuffer + nSamples; |
||||
ensureCapacity(req); |
||||
samplesInBuffer += nSamples; |
||||
} |
||||
|
||||
|
||||
// Returns a pointer to the end of the used part of the sample buffer (i.e.
|
||||
// where the new samples are to be inserted). This function may be used for
|
||||
// inserting new samples into the sample buffer directly. Please be careful!
|
||||
//
|
||||
// Parameter 'slackCapacity' tells the function how much free capacity (in
|
||||
// terms of samples) there _at least_ should be, in order to the caller to
|
||||
// successfully insert all the required samples to the buffer. When necessary,
|
||||
// the function grows the buffer size to comply with this requirement.
|
||||
//
|
||||
// When using this function as means for inserting new samples, also remember
|
||||
// to increase the sample count afterwards, by calling the
|
||||
// 'putSamples(numSamples)' function.
|
||||
SAMPLETYPE *FIFOSampleBuffer::ptrEnd(uint slackCapacity) |
||||
{ |
||||
ensureCapacity(samplesInBuffer + slackCapacity); |
||||
return buffer + samplesInBuffer * channels; |
||||
} |
||||
|
||||
|
||||
// Returns a pointer to the beginning of the currently non-outputted samples.
|
||||
// This function is provided for accessing the output samples directly.
|
||||
// Please be careful!
|
||||
//
|
||||
// When using this function to output samples, also remember to 'remove' the
|
||||
// outputted samples from the buffer by calling the
|
||||
// 'receiveSamples(numSamples)' function
|
||||
SAMPLETYPE *FIFOSampleBuffer::ptrBegin() |
||||
{ |
||||
assert(buffer); |
||||
return buffer + bufferPos * channels; |
||||
} |
||||
|
||||
|
||||
// Ensures that the buffer has enough capacity, i.e. space for _at least_
|
||||
// 'capacityRequirement' number of samples. The buffer is grown in steps of
|
||||
// 4 kilobytes to eliminate the need for frequently growing up the buffer,
|
||||
// as well as to round the buffer size up to the virtual memory page size.
|
||||
void FIFOSampleBuffer::ensureCapacity(uint capacityRequirement) |
||||
{ |
||||
SAMPLETYPE *tempUnaligned, *temp; |
||||
|
||||
if (capacityRequirement > getCapacity()) |
||||
{ |
||||
// enlarge the buffer in 4kbyte steps (round up to next 4k boundary)
|
||||
sizeInBytes = (capacityRequirement * channels * sizeof(SAMPLETYPE) + 4095) & (uint)-4096; |
||||
assert(sizeInBytes % 2 == 0); |
||||
tempUnaligned = new SAMPLETYPE[sizeInBytes / sizeof(SAMPLETYPE) + 16 / sizeof(SAMPLETYPE)]; |
||||
if (tempUnaligned == nullptr) |
||||
{ |
||||
ST_THROW_RT_ERROR("Couldn't allocate memory!\n"); |
||||
} |
||||
// Align the buffer to begin at 16byte cache line boundary for optimal performance
|
||||
temp = (SAMPLETYPE *)SOUNDTOUCH_ALIGN_POINTER_16(tempUnaligned); |
||||
if (samplesInBuffer) |
||||
{ |
||||
memcpy(temp, ptrBegin(), samplesInBuffer * channels * sizeof(SAMPLETYPE)); |
||||
} |
||||
delete[] bufferUnaligned; |
||||
buffer = temp; |
||||
bufferUnaligned = tempUnaligned; |
||||
bufferPos = 0; |
||||
} |
||||
else |
||||
{ |
||||
// simply rewind the buffer (if necessary)
|
||||
rewind(); |
||||
} |
||||
} |
||||
|
||||
|
||||
// Returns the current buffer capacity in terms of samples
|
||||
uint FIFOSampleBuffer::getCapacity() const |
||||
{ |
||||
return sizeInBytes / (channels * sizeof(SAMPLETYPE)); |
||||
} |
||||
|
||||
|
||||
// Returns the number of samples currently in the buffer
|
||||
uint FIFOSampleBuffer::numSamples() const |
||||
{ |
||||
return samplesInBuffer; |
||||
} |
||||
|
||||
|
||||
// Output samples from beginning of the sample buffer. Copies demanded number
|
||||
// of samples to output and removes them from the sample buffer. If there
|
||||
// are less than 'numsample' samples in the buffer, returns all available.
|
||||
//
|
||||
// Returns number of samples copied.
|
||||
uint FIFOSampleBuffer::receiveSamples(SAMPLETYPE *output, uint maxSamples) |
||||
{ |
||||
uint num; |
||||
|
||||
num = (maxSamples > samplesInBuffer) ? samplesInBuffer : maxSamples; |
||||
|
||||
memcpy(output, ptrBegin(), channels * sizeof(SAMPLETYPE) * num); |
||||
return receiveSamples(num); |
||||
} |
||||
|
||||
|
||||
// Removes samples from the beginning of the sample buffer without copying them
|
||||
// anywhere. Used to reduce the number of samples in the buffer, when accessing
|
||||
// the sample buffer with the 'ptrBegin' function.
|
||||
uint FIFOSampleBuffer::receiveSamples(uint maxSamples) |
||||
{ |
||||
if (maxSamples >= samplesInBuffer) |
||||
{ |
||||
uint temp; |
||||
|
||||
temp = samplesInBuffer; |
||||
samplesInBuffer = 0; |
||||
return temp; |
||||
} |
||||
|
||||
samplesInBuffer -= maxSamples; |
||||
bufferPos += maxSamples; |
||||
|
||||
return maxSamples; |
||||
} |
||||
|
||||
|
||||
// Returns nonzero if the sample buffer is empty
|
||||
int FIFOSampleBuffer::isEmpty() const |
||||
{ |
||||
return (samplesInBuffer == 0) ? 1 : 0; |
||||
} |
||||
|
||||
|
||||
// Clears the sample buffer
|
||||
void FIFOSampleBuffer::clear() |
||||
{ |
||||
samplesInBuffer = 0; |
||||
bufferPos = 0; |
||||
} |
||||
|
||||
|
||||
/// allow trimming (downwards) amount of samples in pipeline.
|
||||
/// Returns adjusted amount of samples
|
||||
uint FIFOSampleBuffer::adjustAmountOfSamples(uint numSamples) |
||||
{ |
||||
if (numSamples < samplesInBuffer) |
||||
{ |
||||
samplesInBuffer = numSamples; |
||||
} |
||||
return samplesInBuffer; |
||||
} |
||||
|
||||
|
||||
/// Add silence to end of buffer
|
||||
void FIFOSampleBuffer::addSilent(uint nSamples) |
||||
{ |
||||
memset(ptrEnd(nSamples), 0, sizeof(SAMPLETYPE) * nSamples * channels); |
||||
samplesInBuffer += nSamples; |
||||
} |
||||
@ -0,0 +1,312 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// General FIR digital filter routines with MMX optimization.
|
||||
///
|
||||
/// Notes : MMX optimized functions reside in a separate, platform-specific file,
|
||||
/// e.g. 'mmx_win.cpp' or 'mmx_gcc.cpp'
|
||||
///
|
||||
/// This source file contains OpenMP optimizations that allow speeding up the
|
||||
/// corss-correlation algorithm by executing it in several threads / CPU cores
|
||||
/// in parallel. See the following article link for more detailed discussion
|
||||
/// about SoundTouch OpenMP optimizations:
|
||||
/// http://www.softwarecoven.com/parallel-computing-in-embedded-mobile-devices
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <memory.h> |
||||
#include <assert.h> |
||||
#include <math.h> |
||||
#include <stdlib.h> |
||||
#include "FIRFilter.h" |
||||
#include "cpu_detect.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
/*****************************************************************************
|
||||
* |
||||
* Implementation of the class 'FIRFilter' |
||||
* |
||||
*****************************************************************************/ |
||||
|
||||
FIRFilter::FIRFilter() |
||||
{ |
||||
resultDivFactor = 0; |
||||
resultDivider = 0; |
||||
length = 0; |
||||
lengthDiv8 = 0; |
||||
filterCoeffs = nullptr; |
||||
filterCoeffsStereo = nullptr; |
||||
} |
||||
|
||||
|
||||
FIRFilter::~FIRFilter() |
||||
{ |
||||
delete[] filterCoeffs; |
||||
delete[] filterCoeffsStereo; |
||||
} |
||||
|
||||
|
||||
// Usual C-version of the filter routine for stereo sound
|
||||
uint FIRFilter::evaluateFilterStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples) const |
||||
{ |
||||
int j, end; |
||||
// hint compiler autovectorization that loop length is divisible by 8
|
||||
uint ilength = length & -8; |
||||
|
||||
assert((length != 0) && (length == ilength) && (src != nullptr) && (dest != nullptr) && (filterCoeffs != nullptr)); |
||||
assert(numSamples > ilength); |
||||
|
||||
end = 2 * (numSamples - ilength); |
||||
|
||||
for (j = 0; j < end; j += 2) |
||||
{ |
||||
const SAMPLETYPE *ptr; |
||||
LONG_SAMPLETYPE suml, sumr; |
||||
|
||||
suml = sumr = 0; |
||||
ptr = src + j; |
||||
|
||||
for (uint i = 0; i < ilength; i ++) |
||||
{ |
||||
suml += ptr[2 * i] * filterCoeffsStereo[2 * i]; |
||||
sumr += ptr[2 * i + 1] * filterCoeffsStereo[2 * i + 1]; |
||||
} |
||||
|
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES |
||||
suml >>= resultDivFactor; |
||||
sumr >>= resultDivFactor; |
||||
// saturate to 16 bit integer limits
|
||||
suml = (suml < -32768) ? -32768 : (suml > 32767) ? 32767 : suml; |
||||
// saturate to 16 bit integer limits
|
||||
sumr = (sumr < -32768) ? -32768 : (sumr > 32767) ? 32767 : sumr; |
||||
#endif // SOUNDTOUCH_INTEGER_SAMPLES
|
||||
dest[j] = (SAMPLETYPE)suml; |
||||
dest[j + 1] = (SAMPLETYPE)sumr; |
||||
} |
||||
return numSamples - ilength; |
||||
} |
||||
|
||||
|
||||
// Usual C-version of the filter routine for mono sound
|
||||
uint FIRFilter::evaluateFilterMono(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples) const |
||||
{ |
||||
int j, end; |
||||
|
||||
// hint compiler autovectorization that loop length is divisible by 8
|
||||
int ilength = length & -8; |
||||
|
||||
assert(ilength != 0); |
||||
|
||||
end = numSamples - ilength; |
||||
for (j = 0; j < end; j ++) |
||||
{ |
||||
const SAMPLETYPE *pSrc = src + j; |
||||
LONG_SAMPLETYPE sum; |
||||
int i; |
||||
|
||||
sum = 0; |
||||
for (i = 0; i < ilength; i ++) |
||||
{ |
||||
sum += pSrc[i] * filterCoeffs[i]; |
||||
} |
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES |
||||
sum >>= resultDivFactor; |
||||
// saturate to 16 bit integer limits
|
||||
sum = (sum < -32768) ? -32768 : (sum > 32767) ? 32767 : sum; |
||||
#endif // SOUNDTOUCH_INTEGER_SAMPLES
|
||||
dest[j] = (SAMPLETYPE)sum; |
||||
} |
||||
return end; |
||||
} |
||||
|
||||
|
||||
uint FIRFilter::evaluateFilterMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels) |
||||
{ |
||||
int j, end; |
||||
|
||||
assert(length != 0); |
||||
assert(src != nullptr); |
||||
assert(dest != nullptr); |
||||
assert(filterCoeffs != nullptr); |
||||
assert(numChannels < 16); |
||||
|
||||
// hint compiler autovectorization that loop length is divisible by 8
|
||||
int ilength = length & -8; |
||||
|
||||
end = numChannels * (numSamples - ilength); |
||||
|
||||
for (j = 0; j < end; j += numChannels) |
||||
{ |
||||
const SAMPLETYPE *ptr; |
||||
LONG_SAMPLETYPE sums[16]; |
||||
uint c; |
||||
int i; |
||||
|
||||
for (c = 0; c < numChannels; c ++) |
||||
{ |
||||
sums[c] = 0; |
||||
} |
||||
|
||||
ptr = src + j; |
||||
|
||||
for (i = 0; i < ilength; i ++) |
||||
{ |
||||
SAMPLETYPE coef=filterCoeffs[i]; |
||||
for (c = 0; c < numChannels; c ++) |
||||
{ |
||||
sums[c] += ptr[0] * coef; |
||||
ptr ++; |
||||
} |
||||
} |
||||
|
||||
for (c = 0; c < numChannels; c ++) |
||||
{ |
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES |
||||
sums[c] >>= resultDivFactor; |
||||
#endif // SOUNDTOUCH_INTEGER_SAMPLES
|
||||
dest[j+c] = (SAMPLETYPE)sums[c]; |
||||
} |
||||
} |
||||
return numSamples - ilength; |
||||
} |
||||
|
||||
|
||||
// Set filter coeffiecients and length.
|
||||
//
|
||||
// Throws an exception if filter length isn't divisible by 8
|
||||
void FIRFilter::setCoefficients(const SAMPLETYPE *coeffs, uint newLength, uint uResultDivFactor) |
||||
{ |
||||
assert(newLength > 0); |
||||
if (newLength % 8) ST_THROW_RT_ERROR("FIR filter length not divisible by 8"); |
||||
|
||||
#ifdef SOUNDTOUCH_FLOAT_SAMPLES |
||||
// scale coefficients already here if using floating samples
|
||||
double scale = 1.0 / resultDivider; |
||||
#else |
||||
short scale = 1; |
||||
#endif |
||||
|
||||
lengthDiv8 = newLength / 8; |
||||
length = lengthDiv8 * 8; |
||||
assert(length == newLength); |
||||
|
||||
resultDivFactor = uResultDivFactor; |
||||
resultDivider = (SAMPLETYPE)::pow(2.0, (int)resultDivFactor); |
||||
|
||||
delete[] filterCoeffs; |
||||
filterCoeffs = new SAMPLETYPE[length]; |
||||
delete[] filterCoeffsStereo; |
||||
filterCoeffsStereo = new SAMPLETYPE[length*2]; |
||||
for (uint i = 0; i < length; i ++) |
||||
{ |
||||
filterCoeffs[i] = (SAMPLETYPE)(coeffs[i] * scale); |
||||
// create also stereo set of filter coefficients: this allows compiler
|
||||
// to autovectorize filter evaluation much more efficiently
|
||||
filterCoeffsStereo[2 * i] = (SAMPLETYPE)(coeffs[i] * scale); |
||||
filterCoeffsStereo[2 * i + 1] = (SAMPLETYPE)(coeffs[i] * scale); |
||||
} |
||||
} |
||||
|
||||
|
||||
uint FIRFilter::getLength() const |
||||
{ |
||||
return length; |
||||
} |
||||
|
||||
|
||||
// Applies the filter to the given sequence of samples.
|
||||
//
|
||||
// Note : The amount of outputted samples is by value of 'filter_length'
|
||||
// smaller than the amount of input samples.
|
||||
uint FIRFilter::evaluate(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels) |
||||
{ |
||||
assert(length > 0); |
||||
assert(lengthDiv8 * 8 == length); |
||||
|
||||
if (numSamples < length) return 0; |
||||
|
||||
#ifndef USE_MULTICH_ALWAYS |
||||
if (numChannels == 1) |
||||
{ |
||||
return evaluateFilterMono(dest, src, numSamples); |
||||
} |
||||
else if (numChannels == 2) |
||||
{ |
||||
return evaluateFilterStereo(dest, src, numSamples); |
||||
} |
||||
else |
||||
#endif // USE_MULTICH_ALWAYS
|
||||
{ |
||||
assert(numChannels > 0); |
||||
return evaluateFilterMulti(dest, src, numSamples, numChannels); |
||||
} |
||||
} |
||||
|
||||
|
||||
// Operator 'new' is overloaded so that it automatically creates a suitable instance
|
||||
// depending on if we've a MMX-capable CPU available or not.
|
||||
void * FIRFilter::operator new(size_t) |
||||
{ |
||||
// Notice! don't use "new FIRFilter" directly, use "newInstance" to create a new instance instead!
|
||||
ST_THROW_RT_ERROR("Error in FIRFilter::new: Don't use 'new FIRFilter', use 'newInstance' member instead!"); |
||||
return newInstance(); |
||||
} |
||||
|
||||
|
||||
FIRFilter * FIRFilter::newInstance() |
||||
{ |
||||
uint uExtensions; |
||||
|
||||
uExtensions = detectCPUextensions(); |
||||
(void)uExtensions; |
||||
|
||||
// Check if MMX/SSE instruction set extensions supported by CPU
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_MMX |
||||
// MMX routines available only with integer sample types
|
||||
if (uExtensions & SUPPORT_MMX) |
||||
{ |
||||
return ::new FIRFilterMMX; |
||||
} |
||||
else |
||||
#endif // SOUNDTOUCH_ALLOW_MMX
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_SSE |
||||
if (uExtensions & SUPPORT_SSE) |
||||
{ |
||||
// SSE support
|
||||
return ::new FIRFilterSSE; |
||||
} |
||||
else |
||||
#endif // SOUNDTOUCH_ALLOW_SSE
|
||||
|
||||
{ |
||||
// ISA optimizations not supported, use plain C version
|
||||
return ::new FIRFilter; |
||||
} |
||||
} |
||||
@ -0,0 +1,140 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// General FIR digital filter routines with MMX optimization.
|
||||
///
|
||||
/// Note : MMX optimized functions reside in a separate, platform-specific file,
|
||||
/// e.g. 'mmx_win.cpp' or 'mmx_gcc.cpp'
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef FIRFilter_H |
||||
#define FIRFilter_H |
||||
|
||||
#include <stddef.h> |
||||
#include "STTypes.h" |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
class FIRFilter |
||||
{ |
||||
protected: |
||||
// Number of FIR filter taps
|
||||
uint length; |
||||
// Number of FIR filter taps divided by 8
|
||||
uint lengthDiv8; |
||||
|
||||
// Result divider factor in 2^k format
|
||||
uint resultDivFactor; |
||||
|
||||
// Result divider value.
|
||||
SAMPLETYPE resultDivider; |
||||
|
||||
// Memory for filter coefficients
|
||||
SAMPLETYPE *filterCoeffs; |
||||
SAMPLETYPE *filterCoeffsStereo; |
||||
|
||||
virtual uint evaluateFilterStereo(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
uint numSamples) const; |
||||
virtual uint evaluateFilterMono(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
uint numSamples) const; |
||||
virtual uint evaluateFilterMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, uint numSamples, uint numChannels); |
||||
|
||||
public: |
||||
FIRFilter(); |
||||
virtual ~FIRFilter(); |
||||
|
||||
/// Operator 'new' is overloaded so that it automatically creates a suitable instance
|
||||
/// depending on if we've a MMX-capable CPU available or not.
|
||||
static void * operator new(size_t s); |
||||
|
||||
static FIRFilter *newInstance(); |
||||
|
||||
/// Applies the filter to the given sequence of samples.
|
||||
/// Note : The amount of outputted samples is by value of 'filter_length'
|
||||
/// smaller than the amount of input samples.
|
||||
///
|
||||
/// \return Number of samples copied to 'dest'.
|
||||
uint evaluate(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
uint numSamples, |
||||
uint numChannels); |
||||
|
||||
uint getLength() const; |
||||
|
||||
virtual void setCoefficients(const SAMPLETYPE *coeffs, |
||||
uint newLength, |
||||
uint uResultDivFactor); |
||||
}; |
||||
|
||||
|
||||
// Optional subclasses that implement CPU-specific optimizations:
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_MMX |
||||
|
||||
/// Class that implements MMX optimized functions exclusive for 16bit integer samples type.
|
||||
class FIRFilterMMX : public FIRFilter |
||||
{ |
||||
protected: |
||||
short *filterCoeffsUnalign; |
||||
short *filterCoeffsAlign; |
||||
|
||||
virtual uint evaluateFilterStereo(short *dest, const short *src, uint numSamples) const override; |
||||
public: |
||||
FIRFilterMMX(); |
||||
~FIRFilterMMX(); |
||||
|
||||
virtual void setCoefficients(const short *coeffs, uint newLength, uint uResultDivFactor) override; |
||||
}; |
||||
|
||||
#endif // SOUNDTOUCH_ALLOW_MMX
|
||||
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_SSE |
||||
/// Class that implements SSE optimized functions exclusive for floating point samples type.
|
||||
class FIRFilterSSE : public FIRFilter |
||||
{ |
||||
protected: |
||||
float *filterCoeffsUnalign; |
||||
float *filterCoeffsAlign; |
||||
|
||||
virtual uint evaluateFilterStereo(float *dest, const float *src, uint numSamples) const override; |
||||
public: |
||||
FIRFilterSSE(); |
||||
~FIRFilterSSE(); |
||||
|
||||
virtual void setCoefficients(const float *coeffs, uint newLength, uint uResultDivFactor) override; |
||||
}; |
||||
|
||||
#endif // SOUNDTOUCH_ALLOW_SSE
|
||||
|
||||
} |
||||
|
||||
#endif // FIRFilter_H
|
||||
@ -0,0 +1,196 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Cubic interpolation routine.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <stddef.h> |
||||
#include <math.h> |
||||
#include "InterpolateCubic.h" |
||||
#include "STTypes.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
// cubic interpolation coefficients
|
||||
static const float _coeffs[]= |
||||
{ -0.5f, 1.0f, -0.5f, 0.0f, |
||||
1.5f, -2.5f, 0.0f, 1.0f, |
||||
-1.5f, 2.0f, 0.5f, 0.0f, |
||||
0.5f, -0.5f, 0.0f, 0.0f}; |
||||
|
||||
|
||||
InterpolateCubic::InterpolateCubic() |
||||
{ |
||||
fract = 0; |
||||
} |
||||
|
||||
|
||||
void InterpolateCubic::resetRegisters() |
||||
{ |
||||
fract = 0; |
||||
} |
||||
|
||||
|
||||
/// Transpose mono audio. Returns number of produced output samples, and
|
||||
/// updates "srcSamples" to amount of consumed source samples
|
||||
int InterpolateCubic::transposeMono(SAMPLETYPE *pdest, |
||||
const SAMPLETYPE *psrc, |
||||
int &srcSamples) |
||||
{ |
||||
int i; |
||||
int srcSampleEnd = srcSamples - 4; |
||||
int srcCount = 0; |
||||
|
||||
i = 0; |
||||
while (srcCount < srcSampleEnd) |
||||
{ |
||||
float out; |
||||
const float x3 = 1.0f; |
||||
const float x2 = (float)fract; // x
|
||||
const float x1 = x2*x2; // x^2
|
||||
const float x0 = x1*x2; // x^3
|
||||
float y0, y1, y2, y3; |
||||
|
||||
assert(fract < 1.0); |
||||
|
||||
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3; |
||||
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3; |
||||
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3; |
||||
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3; |
||||
|
||||
out = y0 * psrc[0] + y1 * psrc[1] + y2 * psrc[2] + y3 * psrc[3]; |
||||
|
||||
pdest[i] = (SAMPLETYPE)out; |
||||
i ++; |
||||
|
||||
// update position fraction
|
||||
fract += rate; |
||||
// update whole positions
|
||||
int whole = (int)fract; |
||||
fract -= whole; |
||||
psrc += whole; |
||||
srcCount += whole; |
||||
} |
||||
srcSamples = srcCount; |
||||
return i; |
||||
} |
||||
|
||||
|
||||
/// Transpose stereo audio. Returns number of produced output samples, and
|
||||
/// updates "srcSamples" to amount of consumed source samples
|
||||
int InterpolateCubic::transposeStereo(SAMPLETYPE *pdest, |
||||
const SAMPLETYPE *psrc, |
||||
int &srcSamples) |
||||
{ |
||||
int i; |
||||
int srcSampleEnd = srcSamples - 4; |
||||
int srcCount = 0; |
||||
|
||||
i = 0; |
||||
while (srcCount < srcSampleEnd) |
||||
{ |
||||
const float x3 = 1.0f; |
||||
const float x2 = (float)fract; // x
|
||||
const float x1 = x2*x2; // x^2
|
||||
const float x0 = x1*x2; // x^3
|
||||
float y0, y1, y2, y3; |
||||
float out0, out1; |
||||
|
||||
assert(fract < 1.0); |
||||
|
||||
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3; |
||||
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3; |
||||
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3; |
||||
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3; |
||||
|
||||
out0 = y0 * psrc[0] + y1 * psrc[2] + y2 * psrc[4] + y3 * psrc[6]; |
||||
out1 = y0 * psrc[1] + y1 * psrc[3] + y2 * psrc[5] + y3 * psrc[7]; |
||||
|
||||
pdest[2*i] = (SAMPLETYPE)out0; |
||||
pdest[2*i+1] = (SAMPLETYPE)out1; |
||||
i ++; |
||||
|
||||
// update position fraction
|
||||
fract += rate; |
||||
// update whole positions
|
||||
int whole = (int)fract; |
||||
fract -= whole; |
||||
psrc += 2*whole; |
||||
srcCount += whole; |
||||
} |
||||
srcSamples = srcCount; |
||||
return i; |
||||
} |
||||
|
||||
|
||||
/// Transpose multi-channel audio. Returns number of produced output samples, and
|
||||
/// updates "srcSamples" to amount of consumed source samples
|
||||
int InterpolateCubic::transposeMulti(SAMPLETYPE *pdest, |
||||
const SAMPLETYPE *psrc, |
||||
int &srcSamples) |
||||
{ |
||||
int i; |
||||
int srcSampleEnd = srcSamples - 4; |
||||
int srcCount = 0; |
||||
|
||||
i = 0; |
||||
while (srcCount < srcSampleEnd) |
||||
{ |
||||
const float x3 = 1.0f; |
||||
const float x2 = (float)fract; // x
|
||||
const float x1 = x2*x2; // x^2
|
||||
const float x0 = x1*x2; // x^3
|
||||
float y0, y1, y2, y3; |
||||
|
||||
assert(fract < 1.0); |
||||
|
||||
y0 = _coeffs[0] * x0 + _coeffs[1] * x1 + _coeffs[2] * x2 + _coeffs[3] * x3; |
||||
y1 = _coeffs[4] * x0 + _coeffs[5] * x1 + _coeffs[6] * x2 + _coeffs[7] * x3; |
||||
y2 = _coeffs[8] * x0 + _coeffs[9] * x1 + _coeffs[10] * x2 + _coeffs[11] * x3; |
||||
y3 = _coeffs[12] * x0 + _coeffs[13] * x1 + _coeffs[14] * x2 + _coeffs[15] * x3; |
||||
|
||||
for (int c = 0; c < numChannels; c ++) |
||||
{ |
||||
float out; |
||||
out = y0 * psrc[c] + y1 * psrc[c + numChannels] + y2 * psrc[c + 2 * numChannels] + y3 * psrc[c + 3 * numChannels]; |
||||
pdest[0] = (SAMPLETYPE)out; |
||||
pdest ++; |
||||
} |
||||
i ++; |
||||
|
||||
// update position fraction
|
||||
fract += rate; |
||||
// update whole positions
|
||||
int whole = (int)fract; |
||||
fract -= whole; |
||||
psrc += numChannels*whole; |
||||
srcCount += whole; |
||||
} |
||||
srcSamples = srcCount; |
||||
return i; |
||||
} |
||||
@ -0,0 +1,69 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Cubic interpolation routine.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef _InterpolateCubic_H_ |
||||
#define _InterpolateCubic_H_ |
||||
|
||||
#include "RateTransposer.h" |
||||
#include "STTypes.h" |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
class InterpolateCubic : public TransposerBase |
||||
{ |
||||
protected: |
||||
virtual int transposeMono(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples) override; |
||||
virtual int transposeStereo(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples) override; |
||||
virtual int transposeMulti(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples) override; |
||||
|
||||
double fract; |
||||
|
||||
public: |
||||
InterpolateCubic(); |
||||
|
||||
virtual void resetRegisters() override; |
||||
|
||||
virtual int getLatency() const override |
||||
{ |
||||
return 1; |
||||
} |
||||
}; |
||||
|
||||
} |
||||
|
||||
#endif |
||||
@ -0,0 +1,296 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Linear interpolation algorithm.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <assert.h> |
||||
#include <stdlib.h> |
||||
#include "InterpolateLinear.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// InterpolateLinearInteger - integer arithmetic implementation
|
||||
//
|
||||
|
||||
/// fixed-point interpolation routine precision
|
||||
#define SCALE 65536 |
||||
|
||||
|
||||
// Constructor
|
||||
InterpolateLinearInteger::InterpolateLinearInteger() : TransposerBase() |
||||
{ |
||||
// Notice: use local function calling syntax for sake of clarity,
|
||||
// to indicate the fact that C++ constructor can't call virtual functions.
|
||||
resetRegisters(); |
||||
setRate(1.0f); |
||||
} |
||||
|
||||
|
||||
void InterpolateLinearInteger::resetRegisters() |
||||
{ |
||||
iFract = 0; |
||||
} |
||||
|
||||
|
||||
// Transposes the sample rate of the given samples using linear interpolation.
|
||||
// 'Mono' version of the routine. Returns the number of samples returned in
|
||||
// the "dest" buffer
|
||||
int InterpolateLinearInteger::transposeMono(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples) |
||||
{ |
||||
int i; |
||||
int srcSampleEnd = srcSamples - 1; |
||||
int srcCount = 0; |
||||
|
||||
i = 0; |
||||
while (srcCount < srcSampleEnd) |
||||
{ |
||||
LONG_SAMPLETYPE temp; |
||||
|
||||
assert(iFract < SCALE); |
||||
|
||||
temp = (SCALE - iFract) * src[0] + iFract * src[1]; |
||||
dest[i] = (SAMPLETYPE)(temp / SCALE); |
||||
i++; |
||||
|
||||
iFract += iRate; |
||||
|
||||
int iWhole = iFract / SCALE; |
||||
iFract -= iWhole * SCALE; |
||||
srcCount += iWhole; |
||||
src += iWhole; |
||||
} |
||||
srcSamples = srcCount; |
||||
|
||||
return i; |
||||
} |
||||
|
||||
|
||||
// Transposes the sample rate of the given samples using linear interpolation.
|
||||
// 'Stereo' version of the routine. Returns the number of samples returned in
|
||||
// the "dest" buffer
|
||||
int InterpolateLinearInteger::transposeStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples) |
||||
{ |
||||
int i; |
||||
int srcSampleEnd = srcSamples - 1; |
||||
int srcCount = 0; |
||||
|
||||
i = 0; |
||||
while (srcCount < srcSampleEnd) |
||||
{ |
||||
LONG_SAMPLETYPE temp0; |
||||
LONG_SAMPLETYPE temp1; |
||||
|
||||
assert(iFract < SCALE); |
||||
|
||||
temp0 = (SCALE - iFract) * src[0] + iFract * src[2]; |
||||
temp1 = (SCALE - iFract) * src[1] + iFract * src[3]; |
||||
dest[0] = (SAMPLETYPE)(temp0 / SCALE); |
||||
dest[1] = (SAMPLETYPE)(temp1 / SCALE); |
||||
dest += 2; |
||||
i++; |
||||
|
||||
iFract += iRate; |
||||
|
||||
int iWhole = iFract / SCALE; |
||||
iFract -= iWhole * SCALE; |
||||
srcCount += iWhole; |
||||
src += 2*iWhole; |
||||
} |
||||
srcSamples = srcCount; |
||||
|
||||
return i; |
||||
} |
||||
|
||||
|
||||
int InterpolateLinearInteger::transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples) |
||||
{ |
||||
int i; |
||||
int srcSampleEnd = srcSamples - 1; |
||||
int srcCount = 0; |
||||
|
||||
i = 0; |
||||
while (srcCount < srcSampleEnd) |
||||
{ |
||||
LONG_SAMPLETYPE temp, vol1; |
||||
|
||||
assert(iFract < SCALE); |
||||
vol1 = (LONG_SAMPLETYPE)(SCALE - iFract); |
||||
for (int c = 0; c < numChannels; c ++) |
||||
{ |
||||
temp = vol1 * src[c] + iFract * src[c + numChannels]; |
||||
dest[0] = (SAMPLETYPE)(temp / SCALE); |
||||
dest ++; |
||||
} |
||||
i++; |
||||
|
||||
iFract += iRate; |
||||
|
||||
int iWhole = iFract / SCALE; |
||||
iFract -= iWhole * SCALE; |
||||
srcCount += iWhole; |
||||
src += iWhole * numChannels; |
||||
} |
||||
srcSamples = srcCount; |
||||
|
||||
return i; |
||||
} |
||||
|
||||
|
||||
// Sets new target iRate. Normal iRate = 1.0, smaller values represent slower
|
||||
// iRate, larger faster iRates.
|
||||
void InterpolateLinearInteger::setRate(double newRate) |
||||
{ |
||||
iRate = (int)(newRate * SCALE + 0.5); |
||||
TransposerBase::setRate(newRate); |
||||
} |
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// InterpolateLinearFloat - floating point arithmetic implementation
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
// Constructor
|
||||
InterpolateLinearFloat::InterpolateLinearFloat() : TransposerBase() |
||||
{ |
||||
// Notice: use local function calling syntax for sake of clarity,
|
||||
// to indicate the fact that C++ constructor can't call virtual functions.
|
||||
resetRegisters(); |
||||
setRate(1.0); |
||||
} |
||||
|
||||
|
||||
void InterpolateLinearFloat::resetRegisters() |
||||
{ |
||||
fract = 0; |
||||
} |
||||
|
||||
|
||||
// Transposes the sample rate of the given samples using linear interpolation.
|
||||
// 'Mono' version of the routine. Returns the number of samples returned in
|
||||
// the "dest" buffer
|
||||
int InterpolateLinearFloat::transposeMono(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples) |
||||
{ |
||||
int i; |
||||
int srcSampleEnd = srcSamples - 1; |
||||
int srcCount = 0; |
||||
|
||||
i = 0; |
||||
while (srcCount < srcSampleEnd) |
||||
{ |
||||
double out; |
||||
assert(fract < 1.0); |
||||
|
||||
out = (1.0 - fract) * src[0] + fract * src[1]; |
||||
dest[i] = (SAMPLETYPE)out; |
||||
i ++; |
||||
|
||||
// update position fraction
|
||||
fract += rate; |
||||
// update whole positions
|
||||
int whole = (int)fract; |
||||
fract -= whole; |
||||
src += whole; |
||||
srcCount += whole; |
||||
} |
||||
srcSamples = srcCount; |
||||
return i; |
||||
} |
||||
|
||||
|
||||
// Transposes the sample rate of the given samples using linear interpolation.
|
||||
// 'Mono' version of the routine. Returns the number of samples returned in
|
||||
// the "dest" buffer
|
||||
int InterpolateLinearFloat::transposeStereo(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples) |
||||
{ |
||||
int i; |
||||
int srcSampleEnd = srcSamples - 1; |
||||
int srcCount = 0; |
||||
|
||||
i = 0; |
||||
while (srcCount < srcSampleEnd) |
||||
{ |
||||
double out0, out1; |
||||
assert(fract < 1.0); |
||||
|
||||
out0 = (1.0 - fract) * src[0] + fract * src[2]; |
||||
out1 = (1.0 - fract) * src[1] + fract * src[3]; |
||||
dest[2*i] = (SAMPLETYPE)out0; |
||||
dest[2*i+1] = (SAMPLETYPE)out1; |
||||
i ++; |
||||
|
||||
// update position fraction
|
||||
fract += rate; |
||||
// update whole positions
|
||||
int whole = (int)fract; |
||||
fract -= whole; |
||||
src += 2*whole; |
||||
srcCount += whole; |
||||
} |
||||
srcSamples = srcCount; |
||||
return i; |
||||
} |
||||
|
||||
|
||||
int InterpolateLinearFloat::transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples) |
||||
{ |
||||
int i; |
||||
int srcSampleEnd = srcSamples - 1; |
||||
int srcCount = 0; |
||||
|
||||
i = 0; |
||||
while (srcCount < srcSampleEnd) |
||||
{ |
||||
float temp, vol1, fract_float; |
||||
|
||||
vol1 = (float)(1.0 - fract); |
||||
fract_float = (float)fract; |
||||
for (int c = 0; c < numChannels; c ++) |
||||
{ |
||||
temp = vol1 * src[c] + fract_float * src[c + numChannels]; |
||||
*dest = (SAMPLETYPE)temp; |
||||
dest ++; |
||||
} |
||||
i++; |
||||
|
||||
fract += rate; |
||||
|
||||
int iWhole = (int)fract; |
||||
fract -= iWhole; |
||||
srcCount += iWhole; |
||||
src += iWhole * numChannels; |
||||
} |
||||
srcSamples = srcCount; |
||||
|
||||
return i; |
||||
} |
||||
@ -0,0 +1,98 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Linear interpolation routine.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef _InterpolateLinear_H_ |
||||
#define _InterpolateLinear_H_ |
||||
|
||||
#include "RateTransposer.h" |
||||
#include "STTypes.h" |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
/// Linear transposer class that uses integer arithmetic
|
||||
class InterpolateLinearInteger final : public TransposerBase |
||||
{ |
||||
protected: |
||||
int iFract; |
||||
int iRate; |
||||
|
||||
virtual int transposeMono(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples) override; |
||||
virtual int transposeStereo(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples) override; |
||||
virtual int transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples) override; |
||||
public: |
||||
InterpolateLinearInteger(); |
||||
|
||||
/// Sets new target rate. Normal rate = 1.0, smaller values represent slower
|
||||
/// rate, larger faster rates.
|
||||
virtual void setRate(double newRate) override; |
||||
|
||||
virtual void resetRegisters() override; |
||||
|
||||
virtual int getLatency() const override |
||||
{ |
||||
return 0; |
||||
} |
||||
}; |
||||
|
||||
|
||||
/// Linear transposer class that uses floating point arithmetic
|
||||
class InterpolateLinearFloat final : public TransposerBase |
||||
{ |
||||
protected: |
||||
double fract; |
||||
|
||||
virtual int transposeMono(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples); |
||||
virtual int transposeStereo(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples); |
||||
virtual int transposeMulti(SAMPLETYPE *dest, const SAMPLETYPE *src, int &srcSamples); |
||||
|
||||
public: |
||||
InterpolateLinearFloat(); |
||||
|
||||
virtual void resetRegisters(); |
||||
|
||||
int getLatency() const |
||||
{ |
||||
return 0; |
||||
} |
||||
}; |
||||
|
||||
} |
||||
|
||||
#endif |
||||
@ -0,0 +1,181 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Sample interpolation routine using 8-tap band-limited Shannon interpolation
|
||||
/// with kaiser window.
|
||||
///
|
||||
/// Notice. This algorithm is remarkably much heavier than linear or cubic
|
||||
/// interpolation, and not remarkably better than cubic algorithm. Thus mostly
|
||||
/// for experimental purposes
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <math.h> |
||||
#include "InterpolateShannon.h" |
||||
#include "STTypes.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
|
||||
/// Kaiser window with beta = 2.0
|
||||
/// Values scaled down by 5% to avoid overflows
|
||||
static const double _kaiser8[8] = |
||||
{ |
||||
0.41778693317814, |
||||
0.64888025049173, |
||||
0.83508562409944, |
||||
0.93887857733412, |
||||
0.93887857733412, |
||||
0.83508562409944, |
||||
0.64888025049173, |
||||
0.41778693317814 |
||||
}; |
||||
|
||||
|
||||
InterpolateShannon::InterpolateShannon() |
||||
{ |
||||
fract = 0; |
||||
} |
||||
|
||||
|
||||
void InterpolateShannon::resetRegisters() |
||||
{ |
||||
fract = 0; |
||||
} |
||||
|
||||
|
||||
#define PI 3.1415926536 |
||||
#define sinc(x) (sin(PI * (x)) / (PI * (x))) |
||||
|
||||
/// Transpose mono audio. Returns number of produced output samples, and
|
||||
/// updates "srcSamples" to amount of consumed source samples
|
||||
int InterpolateShannon::transposeMono(SAMPLETYPE *pdest, |
||||
const SAMPLETYPE *psrc, |
||||
int &srcSamples) |
||||
{ |
||||
int i; |
||||
int srcSampleEnd = srcSamples - 8; |
||||
int srcCount = 0; |
||||
|
||||
i = 0; |
||||
while (srcCount < srcSampleEnd) |
||||
{ |
||||
double out; |
||||
assert(fract < 1.0); |
||||
|
||||
out = psrc[0] * sinc(-3.0 - fract) * _kaiser8[0]; |
||||
out += psrc[1] * sinc(-2.0 - fract) * _kaiser8[1]; |
||||
out += psrc[2] * sinc(-1.0 - fract) * _kaiser8[2]; |
||||
if (fract < 1e-6) |
||||
{ |
||||
out += psrc[3] * _kaiser8[3]; // sinc(0) = 1
|
||||
} |
||||
else |
||||
{ |
||||
out += psrc[3] * sinc(- fract) * _kaiser8[3]; |
||||
} |
||||
out += psrc[4] * sinc( 1.0 - fract) * _kaiser8[4]; |
||||
out += psrc[5] * sinc( 2.0 - fract) * _kaiser8[5]; |
||||
out += psrc[6] * sinc( 3.0 - fract) * _kaiser8[6]; |
||||
out += psrc[7] * sinc( 4.0 - fract) * _kaiser8[7]; |
||||
|
||||
pdest[i] = (SAMPLETYPE)out; |
||||
i ++; |
||||
|
||||
// update position fraction
|
||||
fract += rate; |
||||
// update whole positions
|
||||
int whole = (int)fract; |
||||
fract -= whole; |
||||
psrc += whole; |
||||
srcCount += whole; |
||||
} |
||||
srcSamples = srcCount; |
||||
return i; |
||||
} |
||||
|
||||
|
||||
/// Transpose stereo audio. Returns number of produced output samples, and
|
||||
/// updates "srcSamples" to amount of consumed source samples
|
||||
int InterpolateShannon::transposeStereo(SAMPLETYPE *pdest, |
||||
const SAMPLETYPE *psrc, |
||||
int &srcSamples) |
||||
{ |
||||
int i; |
||||
int srcSampleEnd = srcSamples - 8; |
||||
int srcCount = 0; |
||||
|
||||
i = 0; |
||||
while (srcCount < srcSampleEnd) |
||||
{ |
||||
double out0, out1, w; |
||||
assert(fract < 1.0); |
||||
|
||||
w = sinc(-3.0 - fract) * _kaiser8[0]; |
||||
out0 = psrc[0] * w; out1 = psrc[1] * w; |
||||
w = sinc(-2.0 - fract) * _kaiser8[1]; |
||||
out0 += psrc[2] * w; out1 += psrc[3] * w; |
||||
w = sinc(-1.0 - fract) * _kaiser8[2]; |
||||
out0 += psrc[4] * w; out1 += psrc[5] * w; |
||||
w = _kaiser8[3] * ((fract < 1e-5) ? 1.0 : sinc(- fract)); // sinc(0) = 1
|
||||
out0 += psrc[6] * w; out1 += psrc[7] * w; |
||||
w = sinc( 1.0 - fract) * _kaiser8[4]; |
||||
out0 += psrc[8] * w; out1 += psrc[9] * w; |
||||
w = sinc( 2.0 - fract) * _kaiser8[5]; |
||||
out0 += psrc[10] * w; out1 += psrc[11] * w; |
||||
w = sinc( 3.0 - fract) * _kaiser8[6]; |
||||
out0 += psrc[12] * w; out1 += psrc[13] * w; |
||||
w = sinc( 4.0 - fract) * _kaiser8[7]; |
||||
out0 += psrc[14] * w; out1 += psrc[15] * w; |
||||
|
||||
pdest[2*i] = (SAMPLETYPE)out0; |
||||
pdest[2*i+1] = (SAMPLETYPE)out1; |
||||
i ++; |
||||
|
||||
// update position fraction
|
||||
fract += rate; |
||||
// update whole positions
|
||||
int whole = (int)fract; |
||||
fract -= whole; |
||||
psrc += 2*whole; |
||||
srcCount += whole; |
||||
} |
||||
srcSamples = srcCount; |
||||
return i; |
||||
} |
||||
|
||||
|
||||
/// Transpose stereo audio. Returns number of produced output samples, and
|
||||
/// updates "srcSamples" to amount of consumed source samples
|
||||
int InterpolateShannon::transposeMulti(SAMPLETYPE *, |
||||
const SAMPLETYPE *, |
||||
int &) |
||||
{ |
||||
// not implemented
|
||||
assert(false); |
||||
return 0; |
||||
} |
||||
@ -0,0 +1,74 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Sample interpolation routine using 8-tap band-limited Shannon interpolation
|
||||
/// with kaiser window.
|
||||
///
|
||||
/// Notice. This algorithm is remarkably much heavier than linear or cubic
|
||||
/// interpolation, and not remarkably better than cubic algorithm. Thus mostly
|
||||
/// for experimental purposes
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef _InterpolateShannon_H_ |
||||
#define _InterpolateShannon_H_ |
||||
|
||||
#include "RateTransposer.h" |
||||
#include "STTypes.h" |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
class InterpolateShannon final : public TransposerBase |
||||
{ |
||||
protected: |
||||
int transposeMono(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples) override; |
||||
int transposeStereo(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples) override; |
||||
int transposeMulti(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples) override; |
||||
|
||||
double fract; |
||||
|
||||
public: |
||||
InterpolateShannon(); |
||||
|
||||
void resetRegisters() override; |
||||
|
||||
virtual int getLatency() const override |
||||
{ |
||||
return 3; |
||||
} |
||||
}; |
||||
|
||||
} |
||||
|
||||
#endif |
||||
@ -0,0 +1,277 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Peak detection routine.
|
||||
///
|
||||
/// The routine detects highest value on an array of values and calculates the
|
||||
/// precise peak location as a mass-center of the 'hump' around the peak value.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <math.h> |
||||
#include <assert.h> |
||||
|
||||
#include "PeakFinder.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
#define max(x, y) (((x) > (y)) ? (x) : (y)) |
||||
|
||||
|
||||
PeakFinder::PeakFinder() |
||||
{ |
||||
minPos = maxPos = 0; |
||||
} |
||||
|
||||
|
||||
// Finds real 'top' of a peak hump from neighnourhood of the given 'peakpos'.
|
||||
int PeakFinder::findTop(const float *data, int peakpos) const |
||||
{ |
||||
int i; |
||||
int start, end; |
||||
float refvalue; |
||||
|
||||
refvalue = data[peakpos]; |
||||
|
||||
// seek within ±10 points
|
||||
start = peakpos - 10; |
||||
if (start < minPos) start = minPos; |
||||
end = peakpos + 10; |
||||
if (end > maxPos) end = maxPos; |
||||
|
||||
for (i = start; i <= end; i ++) |
||||
{ |
||||
if (data[i] > refvalue) |
||||
{ |
||||
peakpos = i; |
||||
refvalue = data[i]; |
||||
} |
||||
} |
||||
|
||||
// failure if max value is at edges of seek range => it's not peak, it's at slope.
|
||||
if ((peakpos == start) || (peakpos == end)) return 0; |
||||
|
||||
return peakpos; |
||||
} |
||||
|
||||
|
||||
// Finds 'ground level' of a peak hump by starting from 'peakpos' and proceeding
|
||||
// to direction defined by 'direction' until next 'hump' after minimum value will
|
||||
// begin
|
||||
int PeakFinder::findGround(const float *data, int peakpos, int direction) const |
||||
{ |
||||
int lowpos; |
||||
int pos; |
||||
int climb_count; |
||||
float refvalue; |
||||
float delta; |
||||
|
||||
climb_count = 0; |
||||
refvalue = data[peakpos]; |
||||
lowpos = peakpos; |
||||
|
||||
pos = peakpos; |
||||
|
||||
while ((pos > minPos+1) && (pos < maxPos-1)) |
||||
{ |
||||
int prevpos; |
||||
|
||||
prevpos = pos; |
||||
pos += direction; |
||||
|
||||
// calculate derivate
|
||||
delta = data[pos] - data[prevpos]; |
||||
if (delta <= 0) |
||||
{ |
||||
// going downhill, ok
|
||||
if (climb_count) |
||||
{ |
||||
climb_count --; // decrease climb count
|
||||
} |
||||
|
||||
// check if new minimum found
|
||||
if (data[pos] < refvalue) |
||||
{ |
||||
// new minimum found
|
||||
lowpos = pos; |
||||
refvalue = data[pos]; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
// going uphill, increase climbing counter
|
||||
climb_count ++; |
||||
if (climb_count > 5) break; // we've been climbing too long => it's next uphill => quit
|
||||
} |
||||
} |
||||
return lowpos; |
||||
} |
||||
|
||||
|
||||
// Find offset where the value crosses the given level, when starting from 'peakpos' and
|
||||
// proceeds to direction defined in 'direction'
|
||||
int PeakFinder::findCrossingLevel(const float *data, float level, int peakpos, int direction) const |
||||
{ |
||||
float peaklevel; |
||||
int pos; |
||||
|
||||
peaklevel = data[peakpos]; |
||||
assert(peaklevel >= level); |
||||
pos = peakpos; |
||||
while ((pos >= minPos) && (pos + direction < maxPos)) |
||||
{ |
||||
if (data[pos + direction] < level) return pos; // crossing found
|
||||
pos += direction; |
||||
} |
||||
return -1; // not found
|
||||
} |
||||
|
||||
|
||||
// Calculates the center of mass location of 'data' array items between 'firstPos' and 'lastPos'
|
||||
double PeakFinder::calcMassCenter(const float *data, int firstPos, int lastPos) const |
||||
{ |
||||
int i; |
||||
float sum; |
||||
float wsum; |
||||
|
||||
sum = 0; |
||||
wsum = 0; |
||||
for (i = firstPos; i <= lastPos; i ++) |
||||
{ |
||||
sum += (float)i * data[i]; |
||||
wsum += data[i]; |
||||
} |
||||
|
||||
if (wsum < 1e-6) return 0; |
||||
return sum / wsum; |
||||
} |
||||
|
||||
|
||||
/// get exact center of peak near given position by calculating local mass of center
|
||||
double PeakFinder::getPeakCenter(const float *data, int peakpos) const |
||||
{ |
||||
float peakLevel; // peak level
|
||||
int crosspos1, crosspos2; // position where the peak 'hump' crosses cutting level
|
||||
float cutLevel; // cutting value
|
||||
float groundLevel; // ground level of the peak
|
||||
int gp1, gp2; // bottom positions of the peak 'hump'
|
||||
|
||||
// find ground positions.
|
||||
gp1 = findGround(data, peakpos, -1); |
||||
gp2 = findGround(data, peakpos, 1); |
||||
|
||||
peakLevel = data[peakpos]; |
||||
|
||||
if (gp1 == gp2) |
||||
{ |
||||
// avoid rounding errors when all are equal
|
||||
assert(gp1 == peakpos); |
||||
cutLevel = groundLevel = peakLevel; |
||||
} else { |
||||
// get average of the ground levels
|
||||
groundLevel = 0.5f * (data[gp1] + data[gp2]); |
||||
|
||||
// calculate 70%-level of the peak
|
||||
cutLevel = 0.70f * peakLevel + 0.30f * groundLevel; |
||||
} |
||||
|
||||
// find mid-level crossings
|
||||
crosspos1 = findCrossingLevel(data, cutLevel, peakpos, -1); |
||||
crosspos2 = findCrossingLevel(data, cutLevel, peakpos, 1); |
||||
|
||||
if ((crosspos1 < 0) || (crosspos2 < 0)) return 0; // no crossing, no peak..
|
||||
|
||||
// calculate mass center of the peak surroundings
|
||||
return calcMassCenter(data, crosspos1, crosspos2); |
||||
} |
||||
|
||||
|
||||
double PeakFinder::detectPeak(const float *data, int aminPos, int amaxPos) |
||||
{ |
||||
|
||||
int i; |
||||
int peakpos; // position of peak level
|
||||
double highPeak, peak; |
||||
|
||||
this->minPos = aminPos; |
||||
this->maxPos = amaxPos; |
||||
|
||||
// find absolute peak
|
||||
peakpos = minPos; |
||||
peak = data[minPos]; |
||||
for (i = minPos + 1; i < maxPos; i ++) |
||||
{ |
||||
if (data[i] > peak) |
||||
{ |
||||
peak = data[i]; |
||||
peakpos = i; |
||||
} |
||||
} |
||||
|
||||
// Calculate exact location of the highest peak mass center
|
||||
highPeak = getPeakCenter(data, peakpos); |
||||
peak = highPeak; |
||||
|
||||
// Now check if the highest peak were in fact harmonic of the true base beat peak
|
||||
// - sometimes the highest peak can be Nth harmonic of the true base peak yet
|
||||
// just a slightly higher than the true base
|
||||
|
||||
for (i = 1; i < 3; i ++) |
||||
{ |
||||
double peaktmp, harmonic; |
||||
int i1,i2; |
||||
|
||||
harmonic = (double)pow(2.0, i); |
||||
peakpos = (int)(highPeak / harmonic + 0.5f); |
||||
if (peakpos < minPos) break; |
||||
peakpos = findTop(data, peakpos); // seek true local maximum index
|
||||
if (peakpos == 0) continue; // no local max here
|
||||
|
||||
// calculate mass-center of possible harmonic peak
|
||||
peaktmp = getPeakCenter(data, peakpos); |
||||
|
||||
// accept harmonic peak if
|
||||
// (a) it is found
|
||||
// (b) is within ±4% of the expected harmonic interval
|
||||
// (c) has at least half x-corr value of the max. peak
|
||||
|
||||
double diff = harmonic * peaktmp / highPeak; |
||||
if ((diff < 0.96) || (diff > 1.04)) continue; // peak too afar from expected
|
||||
|
||||
// now compare to highest detected peak
|
||||
i1 = (int)(highPeak + 0.5); |
||||
i2 = (int)(peaktmp + 0.5); |
||||
if (data[i2] >= 0.4*data[i1]) |
||||
{ |
||||
// The harmonic is at least half as high primary peak,
|
||||
// thus use the harmonic peak instead
|
||||
peak = peaktmp; |
||||
} |
||||
} |
||||
|
||||
return peak; |
||||
} |
||||
@ -0,0 +1,90 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// The routine detects highest value on an array of values and calculates the
|
||||
/// precise peak location as a mass-center of the 'hump' around the peak value.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef _PeakFinder_H_ |
||||
#define _PeakFinder_H_ |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
class PeakFinder |
||||
{ |
||||
protected: |
||||
/// Min, max allowed peak positions within the data vector
|
||||
int minPos, maxPos; |
||||
|
||||
/// Calculates the mass center between given vector items.
|
||||
double calcMassCenter(const float *data, ///< Data vector.
|
||||
int firstPos, ///< Index of first vector item belonging to the peak.
|
||||
int lastPos ///< Index of last vector item belonging to the peak.
|
||||
) const; |
||||
|
||||
/// Finds the data vector index where the monotoniously decreasing signal crosses the
|
||||
/// given level.
|
||||
int findCrossingLevel(const float *data, ///< Data vector.
|
||||
float level, ///< Goal crossing level.
|
||||
int peakpos, ///< Peak position index within the data vector.
|
||||
int direction /// Direction where to proceed from the peak: 1 = right, -1 = left.
|
||||
) const; |
||||
|
||||
// Finds real 'top' of a peak hump from neighnourhood of the given 'peakpos'.
|
||||
int findTop(const float *data, int peakpos) const; |
||||
|
||||
|
||||
/// Finds the 'ground' level, i.e. smallest level between two neighbouring peaks, to right-
|
||||
/// or left-hand side of the given peak position.
|
||||
int findGround(const float *data, /// Data vector.
|
||||
int peakpos, /// Peak position index within the data vector.
|
||||
int direction /// Direction where to proceed from the peak: 1 = right, -1 = left.
|
||||
) const; |
||||
|
||||
/// get exact center of peak near given position by calculating local mass of center
|
||||
double getPeakCenter(const float *data, int peakpos) const; |
||||
|
||||
public: |
||||
/// Constructor.
|
||||
PeakFinder(); |
||||
|
||||
/// Detect exact peak position of the data vector by finding the largest peak 'hump'
|
||||
/// and calculating the mass-center location of the peak hump.
|
||||
///
|
||||
/// \return The location of the largest base harmonic peak hump.
|
||||
double detectPeak(const float *data, /// Data vector to be analyzed. The data vector has
|
||||
/// to be at least 'maxPos' items long.
|
||||
int minPos, ///< Min allowed peak location within the vector data.
|
||||
int maxPos ///< Max allowed peak location within the vector data.
|
||||
); |
||||
}; |
||||
|
||||
} |
||||
|
||||
#endif // _PeakFinder_H_
|
||||
@ -0,0 +1,313 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Sample rate transposer. Changes sample rate by using linear interpolation
|
||||
/// together with anti-alias filtering (first order interpolation with anti-
|
||||
/// alias filtering should be quite adequate for this application)
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <memory.h> |
||||
#include <assert.h> |
||||
#include <stdlib.h> |
||||
#include <stdio.h> |
||||
#include "RateTransposer.h" |
||||
#include "InterpolateLinear.h" |
||||
#include "InterpolateCubic.h" |
||||
#include "InterpolateShannon.h" |
||||
#include "AAFilter.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
// Define default interpolation algorithm here
|
||||
TransposerBase::ALGORITHM TransposerBase::algorithm = TransposerBase::CUBIC; |
||||
|
||||
|
||||
// Constructor
|
||||
RateTransposer::RateTransposer() : FIFOProcessor(&outputBuffer) |
||||
{ |
||||
bUseAAFilter = |
||||
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER |
||||
true; |
||||
#else |
||||
// Disable Anti-alias filter if desirable to avoid click at rate change zero value crossover
|
||||
false; |
||||
#endif |
||||
|
||||
// Instantiates the anti-alias filter
|
||||
pAAFilter = new AAFilter(64); |
||||
pTransposer = TransposerBase::newInstance(); |
||||
clear(); |
||||
} |
||||
|
||||
|
||||
RateTransposer::~RateTransposer() |
||||
{ |
||||
delete pAAFilter; |
||||
delete pTransposer; |
||||
} |
||||
|
||||
|
||||
/// Enables/disables the anti-alias filter. Zero to disable, nonzero to enable
|
||||
void RateTransposer::enableAAFilter(bool newMode) |
||||
{ |
||||
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER |
||||
// Disable Anti-alias filter if desirable to avoid click at rate change zero value crossover
|
||||
bUseAAFilter = newMode; |
||||
clear(); |
||||
#endif |
||||
} |
||||
|
||||
|
||||
/// Returns nonzero if anti-alias filter is enabled.
|
||||
bool RateTransposer::isAAFilterEnabled() const |
||||
{ |
||||
return bUseAAFilter; |
||||
} |
||||
|
||||
|
||||
AAFilter *RateTransposer::getAAFilter() |
||||
{ |
||||
return pAAFilter; |
||||
} |
||||
|
||||
|
||||
// Sets new target iRate. Normal iRate = 1.0, smaller values represent slower
|
||||
// iRate, larger faster iRates.
|
||||
void RateTransposer::setRate(double newRate) |
||||
{ |
||||
double fCutoff; |
||||
|
||||
pTransposer->setRate(newRate); |
||||
|
||||
// design a new anti-alias filter
|
||||
if (newRate > 1.0) |
||||
{ |
||||
fCutoff = 0.5 / newRate; |
||||
} |
||||
else |
||||
{ |
||||
fCutoff = 0.5 * newRate; |
||||
} |
||||
pAAFilter->setCutoffFreq(fCutoff); |
||||
} |
||||
|
||||
|
||||
// Adds 'nSamples' pcs of samples from the 'samples' memory position into
|
||||
// the input of the object.
|
||||
void RateTransposer::putSamples(const SAMPLETYPE *samples, uint nSamples) |
||||
{ |
||||
processSamples(samples, nSamples); |
||||
} |
||||
|
||||
|
||||
// Transposes sample rate by applying anti-alias filter to prevent folding.
|
||||
// Returns amount of samples returned in the "dest" buffer.
|
||||
// The maximum amount of samples that can be returned at a time is set by
|
||||
// the 'set_returnBuffer_size' function.
|
||||
void RateTransposer::processSamples(const SAMPLETYPE *src, uint nSamples) |
||||
{ |
||||
if (nSamples == 0) return; |
||||
|
||||
// Store samples to input buffer
|
||||
inputBuffer.putSamples(src, nSamples); |
||||
|
||||
// If anti-alias filter is turned off, simply transpose without applying
|
||||
// the filter
|
||||
if (bUseAAFilter == false) |
||||
{ |
||||
(void)pTransposer->transpose(outputBuffer, inputBuffer); |
||||
return; |
||||
} |
||||
|
||||
assert(pAAFilter); |
||||
|
||||
// Transpose with anti-alias filter
|
||||
if (pTransposer->rate < 1.0f) |
||||
{ |
||||
// If the parameter 'Rate' value is smaller than 1, first transpose
|
||||
// the samples and then apply the anti-alias filter to remove aliasing.
|
||||
|
||||
// Transpose the samples, store the result to end of "midBuffer"
|
||||
pTransposer->transpose(midBuffer, inputBuffer); |
||||
|
||||
// Apply the anti-alias filter for transposed samples in midBuffer
|
||||
pAAFilter->evaluate(outputBuffer, midBuffer); |
||||
} |
||||
else |
||||
{ |
||||
// If the parameter 'Rate' value is larger than 1, first apply the
|
||||
// anti-alias filter to remove high frequencies (prevent them from folding
|
||||
// over the lover frequencies), then transpose.
|
||||
|
||||
// Apply the anti-alias filter for samples in inputBuffer
|
||||
pAAFilter->evaluate(midBuffer, inputBuffer); |
||||
|
||||
// Transpose the AA-filtered samples in "midBuffer"
|
||||
pTransposer->transpose(outputBuffer, midBuffer); |
||||
} |
||||
} |
||||
|
||||
|
||||
// Sets the number of channels, 1 = mono, 2 = stereo
|
||||
void RateTransposer::setChannels(int nChannels) |
||||
{ |
||||
if (!verifyNumberOfChannels(nChannels) || |
||||
(pTransposer->numChannels == nChannels)) return; |
||||
|
||||
pTransposer->setChannels(nChannels); |
||||
inputBuffer.setChannels(nChannels); |
||||
midBuffer.setChannels(nChannels); |
||||
outputBuffer.setChannels(nChannels); |
||||
} |
||||
|
||||
|
||||
// Clears all the samples in the object
|
||||
void RateTransposer::clear() |
||||
{ |
||||
outputBuffer.clear(); |
||||
midBuffer.clear(); |
||||
inputBuffer.clear(); |
||||
pTransposer->resetRegisters(); |
||||
|
||||
// prefill buffer to avoid losing first samples at beginning of stream
|
||||
int prefill = getLatency(); |
||||
inputBuffer.addSilent(prefill); |
||||
} |
||||
|
||||
|
||||
// Returns nonzero if there aren't any samples available for outputting.
|
||||
int RateTransposer::isEmpty() const |
||||
{ |
||||
int res; |
||||
|
||||
res = FIFOProcessor::isEmpty(); |
||||
if (res == 0) return 0; |
||||
return inputBuffer.isEmpty(); |
||||
} |
||||
|
||||
|
||||
/// Return approximate initial input-output latency
|
||||
int RateTransposer::getLatency() const |
||||
{ |
||||
return pTransposer->getLatency() + |
||||
((bUseAAFilter) ? (pAAFilter->getLength() / 2) : 0); |
||||
} |
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TransposerBase - Base class for interpolation
|
||||
//
|
||||
|
||||
// static function to set interpolation algorithm
|
||||
void TransposerBase::setAlgorithm(TransposerBase::ALGORITHM a) |
||||
{ |
||||
TransposerBase::algorithm = a; |
||||
} |
||||
|
||||
|
||||
// Transposes the sample rate of the given samples using linear interpolation.
|
||||
// Returns the number of samples returned in the "dest" buffer
|
||||
int TransposerBase::transpose(FIFOSampleBuffer &dest, FIFOSampleBuffer &src) |
||||
{ |
||||
int numSrcSamples = src.numSamples(); |
||||
int sizeDemand = (int)((double)numSrcSamples / rate) + 8; |
||||
int numOutput; |
||||
SAMPLETYPE *psrc = src.ptrBegin(); |
||||
SAMPLETYPE *pdest = dest.ptrEnd(sizeDemand); |
||||
|
||||
#ifndef USE_MULTICH_ALWAYS |
||||
if (numChannels == 1) |
||||
{ |
||||
numOutput = transposeMono(pdest, psrc, numSrcSamples); |
||||
} |
||||
else if (numChannels == 2) |
||||
{ |
||||
numOutput = transposeStereo(pdest, psrc, numSrcSamples); |
||||
} |
||||
else |
||||
#endif // USE_MULTICH_ALWAYS
|
||||
{ |
||||
assert(numChannels > 0); |
||||
numOutput = transposeMulti(pdest, psrc, numSrcSamples); |
||||
} |
||||
dest.putSamples(numOutput); |
||||
src.receiveSamples(numSrcSamples); |
||||
return numOutput; |
||||
} |
||||
|
||||
|
||||
TransposerBase::TransposerBase() |
||||
{ |
||||
numChannels = 0; |
||||
rate = 1.0f; |
||||
} |
||||
|
||||
|
||||
TransposerBase::~TransposerBase() |
||||
{ |
||||
} |
||||
|
||||
|
||||
void TransposerBase::setChannels(int channels) |
||||
{ |
||||
numChannels = channels; |
||||
resetRegisters(); |
||||
} |
||||
|
||||
|
||||
void TransposerBase::setRate(double newRate) |
||||
{ |
||||
rate = newRate; |
||||
} |
||||
|
||||
|
||||
// static factory function
|
||||
TransposerBase *TransposerBase::newInstance() |
||||
{ |
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES |
||||
// Notice: For integer arithmetic support only linear algorithm (due to simplest calculus)
|
||||
return ::new InterpolateLinearInteger; |
||||
#else |
||||
switch (algorithm) |
||||
{ |
||||
case LINEAR: |
||||
return new InterpolateLinearFloat; |
||||
|
||||
case CUBIC: |
||||
return new InterpolateCubic; |
||||
|
||||
case SHANNON: |
||||
return new InterpolateShannon; |
||||
|
||||
default: |
||||
assert(false); |
||||
return nullptr; |
||||
} |
||||
#endif |
||||
} |
||||
@ -0,0 +1,164 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Sample rate transposer. Changes sample rate by using linear interpolation
|
||||
/// together with anti-alias filtering (first order interpolation with anti-
|
||||
/// alias filtering should be quite adequate for this application).
|
||||
///
|
||||
/// Use either of the derived classes of 'RateTransposerInteger' or
|
||||
/// 'RateTransposerFloat' for corresponding integer/floating point tranposing
|
||||
/// algorithm implementation.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef RateTransposer_H |
||||
#define RateTransposer_H |
||||
|
||||
#include <stddef.h> |
||||
#include "AAFilter.h" |
||||
#include "FIFOSamplePipe.h" |
||||
#include "FIFOSampleBuffer.h" |
||||
|
||||
#include "STTypes.h" |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
/// Abstract base class for transposer implementations (linear, advanced vs integer, float etc)
|
||||
class TransposerBase |
||||
{ |
||||
public: |
||||
enum ALGORITHM { |
||||
LINEAR = 0, |
||||
CUBIC, |
||||
SHANNON |
||||
}; |
||||
|
||||
protected: |
||||
virtual int transposeMono(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples) = 0; |
||||
virtual int transposeStereo(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples) = 0; |
||||
virtual int transposeMulti(SAMPLETYPE *dest, |
||||
const SAMPLETYPE *src, |
||||
int &srcSamples) = 0; |
||||
|
||||
static ALGORITHM algorithm; |
||||
|
||||
public: |
||||
double rate; |
||||
int numChannels; |
||||
|
||||
TransposerBase(); |
||||
virtual ~TransposerBase(); |
||||
|
||||
virtual int transpose(FIFOSampleBuffer &dest, FIFOSampleBuffer &src); |
||||
virtual void setRate(double newRate); |
||||
virtual void setChannels(int channels); |
||||
virtual int getLatency() const = 0; |
||||
|
||||
virtual void resetRegisters() = 0; |
||||
|
||||
// static factory function
|
||||
static TransposerBase *newInstance(); |
||||
|
||||
// static function to set interpolation algorithm
|
||||
static void setAlgorithm(ALGORITHM a); |
||||
}; |
||||
|
||||
|
||||
/// A common linear samplerate transposer class.
|
||||
///
|
||||
class RateTransposer final : public FIFOProcessor |
||||
{ |
||||
protected: |
||||
/// Anti-alias filter object
|
||||
AAFilter *pAAFilter; |
||||
TransposerBase *pTransposer; |
||||
|
||||
/// Buffer for collecting samples to feed the anti-alias filter between
|
||||
/// two batches
|
||||
FIFOSampleBuffer inputBuffer; |
||||
|
||||
/// Buffer for keeping samples between transposing & anti-alias filter
|
||||
FIFOSampleBuffer midBuffer; |
||||
|
||||
/// Output sample buffer
|
||||
FIFOSampleBuffer outputBuffer; |
||||
|
||||
bool bUseAAFilter; |
||||
|
||||
|
||||
/// Transposes sample rate by applying anti-alias filter to prevent folding.
|
||||
/// Returns amount of samples returned in the "dest" buffer.
|
||||
/// The maximum amount of samples that can be returned at a time is set by
|
||||
/// the 'set_returnBuffer_size' function.
|
||||
void processSamples(const SAMPLETYPE *src, |
||||
uint numSamples); |
||||
|
||||
public: |
||||
RateTransposer(); |
||||
virtual ~RateTransposer() override; |
||||
|
||||
/// Returns the output buffer object
|
||||
FIFOSamplePipe *getOutput() { return &outputBuffer; }; |
||||
|
||||
/// Return anti-alias filter object
|
||||
AAFilter *getAAFilter(); |
||||
|
||||
/// Enables/disables the anti-alias filter. Zero to disable, nonzero to enable
|
||||
void enableAAFilter(bool newMode); |
||||
|
||||
/// Returns nonzero if anti-alias filter is enabled.
|
||||
bool isAAFilterEnabled() const; |
||||
|
||||
/// Sets new target rate. Normal rate = 1.0, smaller values represent slower
|
||||
/// rate, larger faster rates.
|
||||
virtual void setRate(double newRate); |
||||
|
||||
/// Sets the number of channels, 1 = mono, 2 = stereo
|
||||
void setChannels(int channels); |
||||
|
||||
/// Adds 'numSamples' pcs of samples from the 'samples' memory position into
|
||||
/// the input of the object.
|
||||
void putSamples(const SAMPLETYPE *samples, uint numSamples) override; |
||||
|
||||
/// Clears all the samples in the object
|
||||
void clear() override; |
||||
|
||||
/// Returns nonzero if there aren't any samples available for outputting.
|
||||
int isEmpty() const override; |
||||
|
||||
/// Return approximate initial input-output latency
|
||||
int getLatency() const; |
||||
}; |
||||
|
||||
} |
||||
|
||||
#endif |
||||
@ -0,0 +1,538 @@
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// SoundTouch - main class for tempo/pitch/rate adjusting routines.
|
||||
///
|
||||
/// Notes:
|
||||
/// - Initialize the SoundTouch object instance by setting up the sound stream
|
||||
/// parameters with functions 'setSampleRate' and 'setChannels', then set
|
||||
/// desired tempo/pitch/rate settings with the corresponding functions.
|
||||
///
|
||||
/// - The SoundTouch class behaves like a first-in-first-out pipeline: The
|
||||
/// samples that are to be processed are fed into one of the pipe by calling
|
||||
/// function 'putSamples', while the ready processed samples can be read
|
||||
/// from the other end of the pipeline with function 'receiveSamples'.
|
||||
///
|
||||
/// - The SoundTouch processing classes require certain sized 'batches' of
|
||||
/// samples in order to process the sound. For this reason the classes buffer
|
||||
/// incoming samples until there are enough of samples available for
|
||||
/// processing, then they carry out the processing step and consequently
|
||||
/// make the processed samples available for outputting.
|
||||
///
|
||||
/// - For the above reason, the processing routines introduce a certain
|
||||
/// 'latency' between the input and output, so that the samples input to
|
||||
/// SoundTouch may not be immediately available in the output, and neither
|
||||
/// the amount of outputtable samples may not immediately be in direct
|
||||
/// relationship with the amount of previously input samples.
|
||||
///
|
||||
/// - The tempo/pitch/rate control parameters can be altered during processing.
|
||||
/// Please notice though that they aren't currently protected by semaphores,
|
||||
/// so in multi-thread application external semaphore protection may be
|
||||
/// required.
|
||||
///
|
||||
/// - This class utilizes classes 'TDStretch' for tempo change (without modifying
|
||||
/// pitch) and 'RateTransposer' for changing the playback rate (that is, both
|
||||
/// tempo and pitch in the same ratio) of the sound. The third available control
|
||||
/// 'pitch' (change pitch but maintain tempo) is produced by a combination of
|
||||
/// combining the two other controls.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <assert.h> |
||||
#include <stdlib.h> |
||||
#include <memory.h> |
||||
#include <math.h> |
||||
#include <stdio.h> |
||||
|
||||
#include "SoundTouch.h" |
||||
#include "TDStretch.h" |
||||
#include "RateTransposer.h" |
||||
#include "cpu_detect.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
/// test if two floating point numbers are equal
|
||||
#define TEST_FLOAT_EQUAL(a, b) (fabs(a - b) < 1e-10) |
||||
|
||||
|
||||
/// Print library version string for autoconf
|
||||
extern "C" void soundtouch_ac_test() |
||||
{ |
||||
printf("SoundTouch Version: %s\n",SOUNDTOUCH_VERSION); |
||||
} |
||||
|
||||
|
||||
SoundTouch::SoundTouch() |
||||
{ |
||||
// Initialize rate transposer and tempo changer instances
|
||||
|
||||
pRateTransposer = new RateTransposer(); |
||||
pTDStretch = TDStretch::newInstance(); |
||||
|
||||
setOutPipe(pTDStretch); |
||||
|
||||
rate = tempo = 0; |
||||
|
||||
virtualPitch = |
||||
virtualRate = |
||||
virtualTempo = 1.0; |
||||
|
||||
calcEffectiveRateAndTempo(); |
||||
|
||||
samplesExpectedOut = 0; |
||||
samplesOutput = 0; |
||||
|
||||
channels = 0; |
||||
bSrateSet = false; |
||||
} |
||||
|
||||
|
||||
SoundTouch::~SoundTouch() |
||||
{ |
||||
delete pRateTransposer; |
||||
delete pTDStretch; |
||||
} |
||||
|
||||
|
||||
/// Get SoundTouch library version string
|
||||
const char *SoundTouch::getVersionString() |
||||
{ |
||||
static const char *_version = SOUNDTOUCH_VERSION; |
||||
|
||||
return _version; |
||||
} |
||||
|
||||
|
||||
/// Get SoundTouch library version Id
|
||||
uint SoundTouch::getVersionId() |
||||
{ |
||||
return SOUNDTOUCH_VERSION_ID; |
||||
} |
||||
|
||||
|
||||
// Sets the number of channels, 1 = mono, 2 = stereo
|
||||
void SoundTouch::setChannels(uint numChannels) |
||||
{ |
||||
if (!verifyNumberOfChannels(numChannels)) return; |
||||
|
||||
channels = numChannels; |
||||
pRateTransposer->setChannels((int)numChannels); |
||||
pTDStretch->setChannels((int)numChannels); |
||||
} |
||||
|
||||
|
||||
// Sets new rate control value. Normal rate = 1.0, smaller values
|
||||
// represent slower rate, larger faster rates.
|
||||
void SoundTouch::setRate(double newRate) |
||||
{ |
||||
virtualRate = newRate; |
||||
calcEffectiveRateAndTempo(); |
||||
} |
||||
|
||||
|
||||
// Sets new rate control value as a difference in percents compared
|
||||
// to the original rate (-50 .. +100 %)
|
||||
void SoundTouch::setRateChange(double newRate) |
||||
{ |
||||
virtualRate = 1.0 + 0.01 * newRate; |
||||
calcEffectiveRateAndTempo(); |
||||
} |
||||
|
||||
|
||||
// Sets new tempo control value. Normal tempo = 1.0, smaller values
|
||||
// represent slower tempo, larger faster tempo.
|
||||
void SoundTouch::setTempo(double newTempo) |
||||
{ |
||||
virtualTempo = newTempo; |
||||
calcEffectiveRateAndTempo(); |
||||
} |
||||
|
||||
|
||||
// Sets new tempo control value as a difference in percents compared
|
||||
// to the original tempo (-50 .. +100 %)
|
||||
void SoundTouch::setTempoChange(double newTempo) |
||||
{ |
||||
virtualTempo = 1.0 + 0.01 * newTempo; |
||||
calcEffectiveRateAndTempo(); |
||||
} |
||||
|
||||
|
||||
// Sets new pitch control value. Original pitch = 1.0, smaller values
|
||||
// represent lower pitches, larger values higher pitch.
|
||||
void SoundTouch::setPitch(double newPitch) |
||||
{ |
||||
virtualPitch = newPitch; |
||||
calcEffectiveRateAndTempo(); |
||||
} |
||||
|
||||
|
||||
// Sets pitch change in octaves compared to the original pitch
|
||||
// (-1.00 .. +1.00)
|
||||
void SoundTouch::setPitchOctaves(double newPitch) |
||||
{ |
||||
virtualPitch = exp(0.69314718056 * newPitch); |
||||
calcEffectiveRateAndTempo(); |
||||
} |
||||
|
||||
|
||||
// Sets pitch change in semi-tones compared to the original pitch
|
||||
// (-12 .. +12)
|
||||
void SoundTouch::setPitchSemiTones(int newPitch) |
||||
{ |
||||
setPitchOctaves((double)newPitch / 12.0); |
||||
} |
||||
|
||||
|
||||
void SoundTouch::setPitchSemiTones(double newPitch) |
||||
{ |
||||
setPitchOctaves(newPitch / 12.0); |
||||
} |
||||
|
||||
|
||||
// Calculates 'effective' rate and tempo values from the
|
||||
// nominal control values.
|
||||
void SoundTouch::calcEffectiveRateAndTempo() |
||||
{ |
||||
double oldTempo = tempo; |
||||
double oldRate = rate; |
||||
|
||||
tempo = virtualTempo / virtualPitch; |
||||
rate = virtualPitch * virtualRate; |
||||
|
||||
if (!TEST_FLOAT_EQUAL(rate,oldRate)) pRateTransposer->setRate(rate); |
||||
if (!TEST_FLOAT_EQUAL(tempo, oldTempo)) pTDStretch->setTempo(tempo); |
||||
|
||||
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER |
||||
if (rate <= 1.0f) |
||||
{ |
||||
if (output != pTDStretch) |
||||
{ |
||||
FIFOSamplePipe *tempoOut; |
||||
|
||||
assert(output == pRateTransposer); |
||||
// move samples in the current output buffer to the output of pTDStretch
|
||||
tempoOut = pTDStretch->getOutput(); |
||||
tempoOut->moveSamples(*output); |
||||
// move samples in pitch transposer's store buffer to tempo changer's input
|
||||
// deprecated : pTDStretch->moveSamples(*pRateTransposer->getStore());
|
||||
|
||||
output = pTDStretch; |
||||
} |
||||
} |
||||
else |
||||
#endif |
||||
{ |
||||
if (output != pRateTransposer) |
||||
{ |
||||
FIFOSamplePipe *transOut; |
||||
|
||||
assert(output == pTDStretch); |
||||
// move samples in the current output buffer to the output of pRateTransposer
|
||||
transOut = pRateTransposer->getOutput(); |
||||
transOut->moveSamples(*output); |
||||
// move samples in tempo changer's input to pitch transposer's input
|
||||
pRateTransposer->moveSamples(*pTDStretch->getInput()); |
||||
|
||||
output = pRateTransposer; |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
// Sets sample rate.
|
||||
void SoundTouch::setSampleRate(uint srate) |
||||
{ |
||||
// set sample rate, leave other tempo changer parameters as they are.
|
||||
pTDStretch->setParameters((int)srate); |
||||
bSrateSet = true; |
||||
} |
||||
|
||||
|
||||
// Adds 'numSamples' pcs of samples from the 'samples' memory position into
|
||||
// the input of the object.
|
||||
void SoundTouch::putSamples(const SAMPLETYPE *samples, uint nSamples) |
||||
{ |
||||
if (bSrateSet == false) |
||||
{ |
||||
ST_THROW_RT_ERROR("SoundTouch : Sample rate not defined"); |
||||
} |
||||
else if (channels == 0) |
||||
{ |
||||
ST_THROW_RT_ERROR("SoundTouch : Number of channels not defined"); |
||||
} |
||||
|
||||
// accumulate how many samples are expected out from processing, given the current
|
||||
// processing setting
|
||||
samplesExpectedOut += (double)nSamples / ((double)rate * (double)tempo); |
||||
|
||||
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER |
||||
if (rate <= 1.0f) |
||||
{ |
||||
// transpose the rate down, output the transposed sound to tempo changer buffer
|
||||
assert(output == pTDStretch); |
||||
pRateTransposer->putSamples(samples, nSamples); |
||||
pTDStretch->moveSamples(*pRateTransposer); |
||||
} |
||||
else |
||||
#endif |
||||
{ |
||||
// evaluate the tempo changer, then transpose the rate up,
|
||||
assert(output == pRateTransposer); |
||||
pTDStretch->putSamples(samples, nSamples); |
||||
pRateTransposer->moveSamples(*pTDStretch); |
||||
} |
||||
} |
||||
|
||||
|
||||
// Flushes the last samples from the processing pipeline to the output.
|
||||
// Clears also the internal processing buffers.
|
||||
//
|
||||
// Note: This function is meant for extracting the last samples of a sound
|
||||
// stream. This function may introduce additional blank samples in the end
|
||||
// of the sound stream, and thus it's not recommended to call this function
|
||||
// in the middle of a sound stream.
|
||||
void SoundTouch::flush() |
||||
{ |
||||
int i; |
||||
int numStillExpected; |
||||
SAMPLETYPE *buff = new SAMPLETYPE[128 * channels]; |
||||
|
||||
// how many samples are still expected to output
|
||||
numStillExpected = (int)((long)(samplesExpectedOut + 0.5) - samplesOutput); |
||||
if (numStillExpected < 0) numStillExpected = 0; |
||||
|
||||
memset(buff, 0, 128 * channels * sizeof(SAMPLETYPE)); |
||||
// "Push" the last active samples out from the processing pipeline by
|
||||
// feeding blank samples into the processing pipeline until new,
|
||||
// processed samples appear in the output (not however, more than
|
||||
// 24ksamples in any case)
|
||||
for (i = 0; (numStillExpected > (int)numSamples()) && (i < 200); i ++) |
||||
{ |
||||
putSamples(buff, 128); |
||||
} |
||||
|
||||
adjustAmountOfSamples(numStillExpected); |
||||
|
||||
delete[] buff; |
||||
|
||||
// Clear input buffers
|
||||
pTDStretch->clearInput(); |
||||
// yet leave the output intouched as that's where the
|
||||
// flushed samples are!
|
||||
} |
||||
|
||||
|
||||
// Changes a setting controlling the processing system behaviour. See the
|
||||
// 'SETTING_...' defines for available setting ID's.
|
||||
bool SoundTouch::setSetting(int settingId, int value) |
||||
{ |
||||
int sampleRate, sequenceMs, seekWindowMs, overlapMs; |
||||
|
||||
// read current tdstretch routine parameters
|
||||
pTDStretch->getParameters(&sampleRate, &sequenceMs, &seekWindowMs, &overlapMs); |
||||
|
||||
switch (settingId) |
||||
{ |
||||
case SETTING_USE_AA_FILTER : |
||||
// enables / disabless anti-alias filter
|
||||
pRateTransposer->enableAAFilter((value != 0) ? true : false); |
||||
return true; |
||||
|
||||
case SETTING_AA_FILTER_LENGTH : |
||||
// sets anti-alias filter length
|
||||
pRateTransposer->getAAFilter()->setLength(value); |
||||
return true; |
||||
|
||||
case SETTING_USE_QUICKSEEK : |
||||
// enables / disables tempo routine quick seeking algorithm
|
||||
pTDStretch->enableQuickSeek((value != 0) ? true : false); |
||||
return true; |
||||
|
||||
case SETTING_SEQUENCE_MS: |
||||
// change time-stretch sequence duration parameter
|
||||
pTDStretch->setParameters(sampleRate, value, seekWindowMs, overlapMs); |
||||
return true; |
||||
|
||||
case SETTING_SEEKWINDOW_MS: |
||||
// change time-stretch seek window length parameter
|
||||
pTDStretch->setParameters(sampleRate, sequenceMs, value, overlapMs); |
||||
return true; |
||||
|
||||
case SETTING_OVERLAP_MS: |
||||
// change time-stretch overlap length parameter
|
||||
pTDStretch->setParameters(sampleRate, sequenceMs, seekWindowMs, value); |
||||
return true; |
||||
|
||||
default : |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
|
||||
// Reads a setting controlling the processing system behaviour. See the
|
||||
// 'SETTING_...' defines for available setting ID's.
|
||||
//
|
||||
// Returns the setting value.
|
||||
int SoundTouch::getSetting(int settingId) const |
||||
{ |
||||
int temp; |
||||
|
||||
switch (settingId) |
||||
{ |
||||
case SETTING_USE_AA_FILTER : |
||||
return (uint)pRateTransposer->isAAFilterEnabled(); |
||||
|
||||
case SETTING_AA_FILTER_LENGTH : |
||||
return pRateTransposer->getAAFilter()->getLength(); |
||||
|
||||
case SETTING_USE_QUICKSEEK : |
||||
return (uint)pTDStretch->isQuickSeekEnabled(); |
||||
|
||||
case SETTING_SEQUENCE_MS: |
||||
pTDStretch->getParameters(nullptr, &temp, nullptr, nullptr); |
||||
return temp; |
||||
|
||||
case SETTING_SEEKWINDOW_MS: |
||||
pTDStretch->getParameters(nullptr, nullptr, &temp, nullptr); |
||||
return temp; |
||||
|
||||
case SETTING_OVERLAP_MS: |
||||
pTDStretch->getParameters(nullptr, nullptr, nullptr, &temp); |
||||
return temp; |
||||
|
||||
case SETTING_NOMINAL_INPUT_SEQUENCE : |
||||
{ |
||||
int size = pTDStretch->getInputSampleReq(); |
||||
|
||||
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER |
||||
if (rate <= 1.0) |
||||
{ |
||||
// transposing done before timestretch, which impacts latency
|
||||
return (int)(size * rate + 0.5); |
||||
} |
||||
#endif |
||||
return size; |
||||
} |
||||
|
||||
case SETTING_NOMINAL_OUTPUT_SEQUENCE : |
||||
{ |
||||
int size = pTDStretch->getOutputBatchSize(); |
||||
|
||||
if (rate > 1.0) |
||||
{ |
||||
// transposing done after timestretch, which impacts latency
|
||||
return (int)(size / rate + 0.5); |
||||
} |
||||
return size; |
||||
} |
||||
|
||||
case SETTING_INITIAL_LATENCY: |
||||
{ |
||||
double latency = pTDStretch->getLatency(); |
||||
int latency_tr = pRateTransposer->getLatency(); |
||||
|
||||
#ifndef SOUNDTOUCH_PREVENT_CLICK_AT_RATE_CROSSOVER |
||||
if (rate <= 1.0) |
||||
{ |
||||
// transposing done before timestretch, which impacts latency
|
||||
latency = (latency + latency_tr) * rate; |
||||
} |
||||
else |
||||
#endif |
||||
{ |
||||
latency += (double)latency_tr / rate; |
||||
} |
||||
|
||||
return (int)(latency + 0.5); |
||||
} |
||||
|
||||
default : |
||||
return 0; |
||||
} |
||||
} |
||||
|
||||
|
||||
// Clears all the samples in the object's output and internal processing
|
||||
// buffers.
|
||||
void SoundTouch::clear() |
||||
{ |
||||
samplesExpectedOut = 0; |
||||
samplesOutput = 0; |
||||
pRateTransposer->clear(); |
||||
pTDStretch->clear(); |
||||
} |
||||
|
||||
|
||||
/// Returns number of samples currently unprocessed.
|
||||
uint SoundTouch::numUnprocessedSamples() const |
||||
{ |
||||
FIFOSamplePipe * psp; |
||||
if (pTDStretch) |
||||
{ |
||||
psp = pTDStretch->getInput(); |
||||
if (psp) |
||||
{ |
||||
return psp->numSamples(); |
||||
} |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
|
||||
/// Output samples from beginning of the sample buffer. Copies requested samples to
|
||||
/// output buffer and removes them from the sample buffer. If there are less than
|
||||
/// 'numsample' samples in the buffer, returns all that available.
|
||||
///
|
||||
/// \return Number of samples returned.
|
||||
uint SoundTouch::receiveSamples(SAMPLETYPE *output, uint maxSamples) |
||||
{ |
||||
uint ret = FIFOProcessor::receiveSamples(output, maxSamples); |
||||
samplesOutput += (long)ret; |
||||
return ret; |
||||
} |
||||
|
||||
|
||||
/// Adjusts book-keeping so that given number of samples are removed from beginning of the
|
||||
/// sample buffer without copying them anywhere.
|
||||
///
|
||||
/// Used to reduce the number of samples in the buffer when accessing the sample buffer directly
|
||||
/// with 'ptrBegin' function.
|
||||
uint SoundTouch::receiveSamples(uint maxSamples) |
||||
{ |
||||
uint ret = FIFOProcessor::receiveSamples(maxSamples); |
||||
samplesOutput += (long)ret; |
||||
return ret; |
||||
} |
||||
|
||||
|
||||
/// Get ratio between input and output audio durations, useful for calculating
|
||||
/// processed output duration: if you'll process a stream of N samples, then
|
||||
/// you can expect to get out N * getInputOutputSampleRatio() samples.
|
||||
double SoundTouch::getInputOutputSampleRatio() |
||||
{ |
||||
return 1.0 / (tempo * rate); |
||||
} |
||||
@ -0,0 +1,29 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00 |
||||
# Visual Studio 14 |
||||
VisualStudioVersion = 14.0.23107.0 |
||||
MinimumVisualStudioVersion = 10.0.40219.1 |
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SoundTouch", "SoundTouch.vcxproj", "{68A5DD20-7057-448B-8FE0-B6AC8D205509}" |
||||
EndProject |
||||
Global |
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution |
||||
Debug|Win32 = Debug|Win32 |
||||
Debug|x64 = Debug|x64 |
||||
Release|Win32 = Release|Win32 |
||||
Release|x64 = Release|x64 |
||||
ReleaseX64|Win32 = ReleaseX64|Win32 |
||||
ReleaseX64|x64 = ReleaseX64|x64 |
||||
EndGlobalSection |
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution |
||||
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.Debug|Win32.ActiveCfg = Debug|Win32 |
||||
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.Debug|x64.ActiveCfg = Debug|x64 |
||||
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.Release|Win32.ActiveCfg = Release|Win32 |
||||
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.Release|x64.ActiveCfg = Release|x64 |
||||
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.ReleaseX64|Win32.ActiveCfg = ReleaseX64|Win32 |
||||
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.ReleaseX64|Win32.Build.0 = ReleaseX64|Win32 |
||||
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.ReleaseX64|x64.ActiveCfg = ReleaseX64|x64 |
||||
{68A5DD20-7057-448B-8FE0-B6AC8D205509}.ReleaseX64|x64.Build.0 = ReleaseX64|x64 |
||||
EndGlobalSection |
||||
GlobalSection(SolutionProperties) = preSolution |
||||
HideSolutionNode = FALSE |
||||
EndGlobalSection |
||||
EndGlobal |
||||
@ -0,0 +1,342 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<Project DefaultTargets="Build" ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup Label="ProjectConfigurations"> |
||||
<ProjectConfiguration Include="Debug|Win32"> |
||||
<Configuration>Debug</Configuration> |
||||
<Platform>Win32</Platform> |
||||
</ProjectConfiguration> |
||||
<ProjectConfiguration Include="Debug|x64"> |
||||
<Configuration>Debug</Configuration> |
||||
<Platform>x64</Platform> |
||||
</ProjectConfiguration> |
||||
<ProjectConfiguration Include="Release|Win32"> |
||||
<Configuration>Release</Configuration> |
||||
<Platform>Win32</Platform> |
||||
</ProjectConfiguration> |
||||
<ProjectConfiguration Include="Release|x64"> |
||||
<Configuration>Release</Configuration> |
||||
<Platform>x64</Platform> |
||||
</ProjectConfiguration> |
||||
</ItemGroup> |
||||
<PropertyGroup Label="Globals"> |
||||
<ProjectGuid>{68A5DD20-7057-448B-8FE0-B6AC8D205509}</ProjectGuid> |
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion> |
||||
</PropertyGroup> |
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> |
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration"> |
||||
<ConfigurationType>StaticLibrary</ConfigurationType> |
||||
<PlatformToolset>v142</PlatformToolset> |
||||
<UseOfMfc>false</UseOfMfc> |
||||
<CharacterSet>Unicode</CharacterSet> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration"> |
||||
<ConfigurationType>StaticLibrary</ConfigurationType> |
||||
<PlatformToolset>v142</PlatformToolset> |
||||
<UseOfMfc>false</UseOfMfc> |
||||
<CharacterSet>Unicode</CharacterSet> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration"> |
||||
<ConfigurationType>StaticLibrary</ConfigurationType> |
||||
<PlatformToolset>v142</PlatformToolset> |
||||
<UseOfMfc>false</UseOfMfc> |
||||
<CharacterSet>Unicode</CharacterSet> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration"> |
||||
<ConfigurationType>StaticLibrary</ConfigurationType> |
||||
<PlatformToolset>v142</PlatformToolset> |
||||
<UseOfMfc>false</UseOfMfc> |
||||
<CharacterSet>Unicode</CharacterSet> |
||||
</PropertyGroup> |
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> |
||||
<ImportGroup Label="ExtensionSettings"> |
||||
</ImportGroup> |
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets"> |
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> |
||||
<Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC71.props" /> |
||||
</ImportGroup> |
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets"> |
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> |
||||
<Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC71.props" /> |
||||
</ImportGroup> |
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets"> |
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> |
||||
<Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC71.props" /> |
||||
</ImportGroup> |
||||
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets"> |
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> |
||||
<Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC71.props" /> |
||||
</ImportGroup> |
||||
<PropertyGroup Label="UserMacros" /> |
||||
<PropertyGroup> |
||||
<_ProjectFileVersion>14.0.23107.0</_ProjectFileVersion> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> |
||||
<OutDir>$(Platform)\$(Configuration)\</OutDir> |
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> |
||||
<OutDir>$(Platform)\$(Configuration)\</OutDir> |
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir> |
||||
<TargetName>$(ProjectName)_x64</TargetName> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> |
||||
<OutDir>$(Platform)\$(Configuration)\</OutDir> |
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir> |
||||
<TargetName>$(ProjectName)D</TargetName> |
||||
</PropertyGroup> |
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> |
||||
<OutDir>$(Platform)\$(Configuration)\</OutDir> |
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir> |
||||
<TargetName>$(ProjectName)D_x64</TargetName> |
||||
</PropertyGroup> |
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> |
||||
<ClCompile> |
||||
<Optimization>Full</Optimization> |
||||
<InlineFunctionExpansion>AnySuitable</InlineFunctionExpansion> |
||||
<IntrinsicFunctions>true</IntrinsicFunctions> |
||||
<AdditionalIncludeDirectories>..\..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> |
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||
<StringPooling>true</StringPooling> |
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary> |
||||
<FunctionLevelLinking>true</FunctionLevelLinking> |
||||
<FloatingPointModel>Fast</FloatingPointModel> |
||||
<PrecompiledHeader /> |
||||
<PrecompiledHeaderOutputFile>$(OutDir)$(TargetName).pch</PrecompiledHeaderOutputFile> |
||||
<AssemblerListingLocation>$(OutDir)</AssemblerListingLocation> |
||||
<ObjectFileName>$(OutDir)</ObjectFileName> |
||||
<ProgramDataBaseFileName>$(OutDir)</ProgramDataBaseFileName> |
||||
<WarningLevel>Level3</WarningLevel> |
||||
<SuppressStartupBanner>true</SuppressStartupBanner> |
||||
<DebugInformationFormat /> |
||||
<CompileAs>Default</CompileAs> |
||||
<EnableEnhancedInstructionSet>StreamingSIMDExtensions2</EnableEnhancedInstructionSet> |
||||
<XMLDocumentationFileName>$(IntDir)</XMLDocumentationFileName> |
||||
<BrowseInformationFile>$(IntDir)</BrowseInformationFile> |
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation> |
||||
</ClCompile> |
||||
<ResourceCompile> |
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||
<Culture>0x040b</Culture> |
||||
</ResourceCompile> |
||||
<Lib> |
||||
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile> |
||||
<SuppressStartupBanner>true</SuppressStartupBanner> |
||||
</Lib> |
||||
<PostBuildEvent> |
||||
<Command>if not exist ..\..\lib mkdir ..\..\lib |
||||
copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib</Command> |
||||
</PostBuildEvent> |
||||
</ItemDefinitionGroup> |
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> |
||||
<Midl> |
||||
<TargetEnvironment>X64</TargetEnvironment> |
||||
</Midl> |
||||
<ClCompile> |
||||
<Optimization>Full</Optimization> |
||||
<InlineFunctionExpansion>AnySuitable</InlineFunctionExpansion> |
||||
<IntrinsicFunctions>true</IntrinsicFunctions> |
||||
<AdditionalIncludeDirectories>..\..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> |
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||
<StringPooling>true</StringPooling> |
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary> |
||||
<FunctionLevelLinking>true</FunctionLevelLinking> |
||||
<FloatingPointModel>Fast</FloatingPointModel> |
||||
<PrecompiledHeader /> |
||||
<PrecompiledHeaderOutputFile>$(OutDir)$(TargetName).pch</PrecompiledHeaderOutputFile> |
||||
<AssemblerListingLocation>$(OutDir)</AssemblerListingLocation> |
||||
<ObjectFileName>$(OutDir)</ObjectFileName> |
||||
<ProgramDataBaseFileName>$(OutDir)</ProgramDataBaseFileName> |
||||
<WarningLevel>Level3</WarningLevel> |
||||
<SuppressStartupBanner>true</SuppressStartupBanner> |
||||
<DebugInformationFormat /> |
||||
<CompileAs>Default</CompileAs> |
||||
<EnableEnhancedInstructionSet> |
||||
</EnableEnhancedInstructionSet> |
||||
<XMLDocumentationFileName>$(IntDir)</XMLDocumentationFileName> |
||||
<BrowseInformationFile>$(IntDir)</BrowseInformationFile> |
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation> |
||||
</ClCompile> |
||||
<ResourceCompile> |
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||
<Culture>0x040b</Culture> |
||||
</ResourceCompile> |
||||
<Lib> |
||||
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile> |
||||
<SuppressStartupBanner>true</SuppressStartupBanner> |
||||
</Lib> |
||||
<PostBuildEvent> |
||||
<Command>if not exist ..\..\lib mkdir ..\..\lib |
||||
copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib</Command> |
||||
</PostBuildEvent> |
||||
</ItemDefinitionGroup> |
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> |
||||
<ClCompile> |
||||
<Optimization>Disabled</Optimization> |
||||
<AdditionalIncludeDirectories>..\..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> |
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks> |
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary> |
||||
<FloatingPointModel>Fast</FloatingPointModel> |
||||
<PrecompiledHeader /> |
||||
<PrecompiledHeaderOutputFile>$(OutDir)$(TargetName).pch</PrecompiledHeaderOutputFile> |
||||
<AssemblerListingLocation>$(OutDir)</AssemblerListingLocation> |
||||
<ObjectFileName>$(OutDir)</ObjectFileName> |
||||
<ProgramDataBaseFileName>$(OutDir)</ProgramDataBaseFileName> |
||||
<BrowseInformation>true</BrowseInformation> |
||||
<WarningLevel>Level3</WarningLevel> |
||||
<SuppressStartupBanner>true</SuppressStartupBanner> |
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat> |
||||
<CompileAs>Default</CompileAs> |
||||
<EnableEnhancedInstructionSet>StreamingSIMDExtensions2</EnableEnhancedInstructionSet> |
||||
<XMLDocumentationFileName>$(IntDir)</XMLDocumentationFileName> |
||||
<BrowseInformationFile>$(IntDir)</BrowseInformationFile> |
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation> |
||||
</ClCompile> |
||||
<ResourceCompile> |
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||
<Culture>0x040b</Culture> |
||||
</ResourceCompile> |
||||
<Lib> |
||||
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile> |
||||
<SuppressStartupBanner>true</SuppressStartupBanner> |
||||
</Lib> |
||||
<PostBuildEvent> |
||||
<Command>if not exist ..\..\lib mkdir ..\..\lib |
||||
copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib</Command> |
||||
</PostBuildEvent> |
||||
</ItemDefinitionGroup> |
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> |
||||
<Midl> |
||||
<TargetEnvironment>X64</TargetEnvironment> |
||||
</Midl> |
||||
<ClCompile> |
||||
<Optimization>Disabled</Optimization> |
||||
<AdditionalIncludeDirectories>..\..\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> |
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks> |
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary> |
||||
<FloatingPointModel>Fast</FloatingPointModel> |
||||
<PrecompiledHeader /> |
||||
<PrecompiledHeaderOutputFile>$(OutDir)$(TargetName).pch</PrecompiledHeaderOutputFile> |
||||
<AssemblerListingLocation>$(OutDir)</AssemblerListingLocation> |
||||
<ObjectFileName>$(OutDir)</ObjectFileName> |
||||
<ProgramDataBaseFileName>$(OutDir)</ProgramDataBaseFileName> |
||||
<BrowseInformation>true</BrowseInformation> |
||||
<WarningLevel>Level3</WarningLevel> |
||||
<SuppressStartupBanner>true</SuppressStartupBanner> |
||||
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat> |
||||
<CompileAs>Default</CompileAs> |
||||
<EnableEnhancedInstructionSet> |
||||
</EnableEnhancedInstructionSet> |
||||
<XMLDocumentationFileName>$(IntDir)</XMLDocumentationFileName> |
||||
<BrowseInformationFile>$(IntDir)</BrowseInformationFile> |
||||
<MultiProcessorCompilation>true</MultiProcessorCompilation> |
||||
</ClCompile> |
||||
<ResourceCompile> |
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions> |
||||
<Culture>0x040b</Culture> |
||||
</ResourceCompile> |
||||
<Lib> |
||||
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile> |
||||
<SuppressStartupBanner>true</SuppressStartupBanner> |
||||
</Lib> |
||||
<PostBuildEvent> |
||||
<Command>if not exist ..\..\lib mkdir ..\..\lib |
||||
copy $(OutDir)$(TargetName)$(TargetExt) ..\..\lib</Command> |
||||
</PostBuildEvent> |
||||
</ItemDefinitionGroup> |
||||
<ItemGroup> |
||||
<ClCompile Include="AAFilter.cpp"> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization> |
||||
</ClCompile> |
||||
<ClCompile Include="BPMDetect.cpp"> |
||||
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">4996</DisableSpecificWarnings> |
||||
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">4996</DisableSpecificWarnings> |
||||
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">4996</DisableSpecificWarnings> |
||||
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Release|x64'">4996</DisableSpecificWarnings> |
||||
</ClCompile> |
||||
<ClCompile Include="cpu_detect_x86.cpp" /> |
||||
<ClCompile Include="FIFOSampleBuffer.cpp"> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization> |
||||
</ClCompile> |
||||
<ClCompile Include="FIRFilter.cpp"> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization> |
||||
</ClCompile> |
||||
<ClCompile Include="InterpolateCubic.cpp" /> |
||||
<ClCompile Include="InterpolateLinear.cpp" /> |
||||
<ClCompile Include="InterpolateShannon.cpp" /> |
||||
<ClCompile Include="mmx_optimized.cpp" /> |
||||
<ClCompile Include="PeakFinder.cpp" /> |
||||
<ClCompile Include="RateTransposer.cpp"> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization> |
||||
</ClCompile> |
||||
<ClCompile Include="SoundTouch.cpp"> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization> |
||||
</ClCompile> |
||||
<ClCompile Include="sse_optimized.cpp" /> |
||||
<ClCompile Include="TDStretch.cpp"> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Disabled</Optimization> |
||||
<BasicRuntimeChecks Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">EnableFastChecks</BasicRuntimeChecks> |
||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">true</BrowseInformation> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">MaxSpeed</Optimization> |
||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Release|x64'">MaxSpeed</Optimization> |
||||
</ClCompile> |
||||
</ItemGroup> |
||||
<ItemGroup> |
||||
<ClInclude Include="..\..\include\BPMDetect.h" /> |
||||
<ClInclude Include="..\..\include\FIFOSampleBuffer.h" /> |
||||
<ClInclude Include="..\..\include\FIFOSamplePipe.h" /> |
||||
<ClInclude Include="..\..\include\SoundTouch.h" /> |
||||
<ClInclude Include="..\..\include\STTypes.h" /> |
||||
<ClInclude Include="AAFilter.h" /> |
||||
<ClInclude Include="cpu_detect.h" /> |
||||
<ClInclude Include="FIRFilter.h" /> |
||||
<ClInclude Include="InterpolateCubic.h" /> |
||||
<ClInclude Include="InterpolateLinear.h" /> |
||||
<ClInclude Include="InterpolateShannon.h" /> |
||||
<ClInclude Include="PeakFinder.h" /> |
||||
<ClInclude Include="RateTransposer.h" /> |
||||
<ClInclude Include="TDStretch.h" /> |
||||
</ItemGroup> |
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> |
||||
<ImportGroup Label="ExtensionTargets"> |
||||
</ImportGroup> |
||||
</Project> |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,279 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Sampled sound tempo changer/time stretch algorithm. Changes the sound tempo
|
||||
/// while maintaining the original pitch by using a time domain WSOLA-like method
|
||||
/// with several performance-increasing tweaks.
|
||||
///
|
||||
/// Note : MMX/SSE optimized functions reside in separate, platform-specific files
|
||||
/// 'mmx_optimized.cpp' and 'sse_optimized.cpp'
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef TDStretch_H |
||||
#define TDStretch_H |
||||
|
||||
#include <stddef.h> |
||||
#include "STTypes.h" |
||||
#include "RateTransposer.h" |
||||
#include "FIFOSamplePipe.h" |
||||
|
||||
namespace soundtouch |
||||
{ |
||||
|
||||
/// Default values for sound processing parameters:
|
||||
/// Notice that the default parameters are tuned for contemporary popular music
|
||||
/// processing. For speech processing applications these parameters suit better:
|
||||
/// #define DEFAULT_SEQUENCE_MS 40
|
||||
/// #define DEFAULT_SEEKWINDOW_MS 15
|
||||
/// #define DEFAULT_OVERLAP_MS 8
|
||||
///
|
||||
|
||||
/// Default length of a single processing sequence, in milliseconds. This determines to how
|
||||
/// long sequences the original sound is chopped in the time-stretch algorithm.
|
||||
///
|
||||
/// The larger this value is, the lesser sequences are used in processing. In principle
|
||||
/// a bigger value sounds better when slowing down tempo, but worse when increasing tempo
|
||||
/// and vice versa.
|
||||
///
|
||||
/// Increasing this value reduces computational burden & vice versa.
|
||||
//#define DEFAULT_SEQUENCE_MS 40
|
||||
#define DEFAULT_SEQUENCE_MS USE_AUTO_SEQUENCE_LEN |
||||
|
||||
/// Giving this value for the sequence length sets automatic parameter value
|
||||
/// according to tempo setting (recommended)
|
||||
#define USE_AUTO_SEQUENCE_LEN 0 |
||||
|
||||
/// Seeking window default length in milliseconds for algorithm that finds the best possible
|
||||
/// overlapping location. This determines from how wide window the algorithm may look for an
|
||||
/// optimal joining location when mixing the sound sequences back together.
|
||||
///
|
||||
/// The bigger this window setting is, the higher the possibility to find a better mixing
|
||||
/// position will become, but at the same time large values may cause a "drifting" artifact
|
||||
/// because consequent sequences will be taken at more uneven intervals.
|
||||
///
|
||||
/// If there's a disturbing artifact that sounds as if a constant frequency was drifting
|
||||
/// around, try reducing this setting.
|
||||
///
|
||||
/// Increasing this value increases computational burden & vice versa.
|
||||
//#define DEFAULT_SEEKWINDOW_MS 15
|
||||
#define DEFAULT_SEEKWINDOW_MS USE_AUTO_SEEKWINDOW_LEN |
||||
|
||||
/// Giving this value for the seek window length sets automatic parameter value
|
||||
/// according to tempo setting (recommended)
|
||||
#define USE_AUTO_SEEKWINDOW_LEN 0 |
||||
|
||||
/// Overlap length in milliseconds. When the chopped sound sequences are mixed back together,
|
||||
/// to form a continuous sound stream, this parameter defines over how long period the two
|
||||
/// consecutive sequences are let to overlap each other.
|
||||
///
|
||||
/// This shouldn't be that critical parameter. If you reduce the DEFAULT_SEQUENCE_MS setting
|
||||
/// by a large amount, you might wish to try a smaller value on this.
|
||||
///
|
||||
/// Increasing this value increases computational burden & vice versa.
|
||||
#define DEFAULT_OVERLAP_MS 8 |
||||
|
||||
|
||||
/// Class that does the time-stretch (tempo change) effect for the processed
|
||||
/// sound.
|
||||
class TDStretch : public FIFOProcessor |
||||
{ |
||||
protected: |
||||
int channels; |
||||
int sampleReq; |
||||
|
||||
int overlapLength; |
||||
int seekLength; |
||||
int seekWindowLength; |
||||
int overlapDividerBitsNorm; |
||||
int overlapDividerBitsPure; |
||||
int slopingDivider; |
||||
int sampleRate; |
||||
int sequenceMs; |
||||
int seekWindowMs; |
||||
int overlapMs; |
||||
|
||||
unsigned long maxnorm; |
||||
float maxnormf; |
||||
|
||||
double tempo; |
||||
double nominalSkip; |
||||
double skipFract; |
||||
|
||||
bool bQuickSeek; |
||||
bool bAutoSeqSetting; |
||||
bool bAutoSeekSetting; |
||||
bool isBeginning; |
||||
|
||||
SAMPLETYPE *pMidBuffer; |
||||
SAMPLETYPE *pMidBufferUnaligned; |
||||
|
||||
FIFOSampleBuffer outputBuffer; |
||||
FIFOSampleBuffer inputBuffer; |
||||
|
||||
void acceptNewOverlapLength(int newOverlapLength); |
||||
|
||||
virtual void clearCrossCorrState(); |
||||
void calculateOverlapLength(int overlapMs); |
||||
|
||||
virtual double calcCrossCorr(const SAMPLETYPE *mixingPos, const SAMPLETYPE *compare, double &norm); |
||||
virtual double calcCrossCorrAccumulate(const SAMPLETYPE *mixingPos, const SAMPLETYPE *compare, double &norm); |
||||
|
||||
virtual int seekBestOverlapPositionFull(const SAMPLETYPE *refPos); |
||||
virtual int seekBestOverlapPositionQuick(const SAMPLETYPE *refPos); |
||||
virtual int seekBestOverlapPosition(const SAMPLETYPE *refPos); |
||||
|
||||
virtual void overlapStereo(SAMPLETYPE *output, const SAMPLETYPE *input) const; |
||||
virtual void overlapMono(SAMPLETYPE *output, const SAMPLETYPE *input) const; |
||||
virtual void overlapMulti(SAMPLETYPE *output, const SAMPLETYPE *input) const; |
||||
|
||||
void clearMidBuffer(); |
||||
void overlap(SAMPLETYPE *output, const SAMPLETYPE *input, uint ovlPos) const; |
||||
|
||||
void calcSeqParameters(); |
||||
void adaptNormalizer(); |
||||
|
||||
/// Changes the tempo of the given sound samples.
|
||||
/// Returns amount of samples returned in the "output" buffer.
|
||||
/// The maximum amount of samples that can be returned at a time is set by
|
||||
/// the 'set_returnBuffer_size' function.
|
||||
void processSamples(); |
||||
|
||||
public: |
||||
TDStretch(); |
||||
virtual ~TDStretch() override; |
||||
|
||||
/// Operator 'new' is overloaded so that it automatically creates a suitable instance
|
||||
/// depending on if we've a MMX/SSE/etc-capable CPU available or not.
|
||||
static void *operator new(size_t s); |
||||
|
||||
/// Use this function instead of "new" operator to create a new instance of this class.
|
||||
/// This function automatically chooses a correct feature set depending on if the CPU
|
||||
/// supports MMX/SSE/etc extensions.
|
||||
static TDStretch *newInstance(); |
||||
|
||||
/// Returns the output buffer object
|
||||
FIFOSamplePipe *getOutput() { return &outputBuffer; }; |
||||
|
||||
/// Returns the input buffer object
|
||||
FIFOSamplePipe *getInput() { return &inputBuffer; }; |
||||
|
||||
/// Sets new target tempo. Normal tempo = 'SCALE', smaller values represent slower
|
||||
/// tempo, larger faster tempo.
|
||||
void setTempo(double newTempo); |
||||
|
||||
/// Returns nonzero if there aren't any samples available for outputting.
|
||||
virtual void clear() override; |
||||
|
||||
/// Clears the input buffer
|
||||
void clearInput(); |
||||
|
||||
/// Sets the number of channels, 1 = mono, 2 = stereo
|
||||
void setChannels(int numChannels); |
||||
|
||||
/// Enables/disables the quick position seeking algorithm. Zero to disable,
|
||||
/// nonzero to enable
|
||||
void enableQuickSeek(bool enable); |
||||
|
||||
/// Returns nonzero if the quick seeking algorithm is enabled.
|
||||
bool isQuickSeekEnabled() const; |
||||
|
||||
/// Sets routine control parameters. These control are certain time constants
|
||||
/// defining how the sound is stretched to the desired duration.
|
||||
//
|
||||
/// 'sampleRate' = sample rate of the sound
|
||||
/// 'sequenceMS' = one processing sequence length in milliseconds
|
||||
/// 'seekwindowMS' = seeking window length for scanning the best overlapping
|
||||
/// position
|
||||
/// 'overlapMS' = overlapping length
|
||||
void setParameters(int sampleRate, ///< Samplerate of sound being processed (Hz)
|
||||
int sequenceMS = -1, ///< Single processing sequence length (ms)
|
||||
int seekwindowMS = -1, ///< Offset seeking window length (ms)
|
||||
int overlapMS = -1 ///< Sequence overlapping length (ms)
|
||||
); |
||||
|
||||
/// Get routine control parameters, see setParameters() function.
|
||||
/// Any of the parameters to this function can be nullptr, in such case corresponding parameter
|
||||
/// value isn't returned.
|
||||
void getParameters(int *pSampleRate, int *pSequenceMs, int *pSeekWindowMs, int *pOverlapMs) const; |
||||
|
||||
/// Adds 'numsamples' pcs of samples from the 'samples' memory position into
|
||||
/// the input of the object.
|
||||
virtual void putSamples( |
||||
const SAMPLETYPE *samples, ///< Input sample data
|
||||
uint numSamples ///< Number of samples in 'samples' so that one sample
|
||||
///< contains both channels if stereo
|
||||
) override; |
||||
|
||||
/// return nominal input sample requirement for triggering a processing batch
|
||||
int getInputSampleReq() const |
||||
{ |
||||
return (int)(nominalSkip + 0.5); |
||||
} |
||||
|
||||
/// return nominal output sample amount when running a processing batch
|
||||
int getOutputBatchSize() const |
||||
{ |
||||
return seekWindowLength - overlapLength; |
||||
} |
||||
|
||||
/// return approximate initial input-output latency
|
||||
int getLatency() const |
||||
{ |
||||
return sampleReq; |
||||
} |
||||
}; |
||||
|
||||
|
||||
// Implementation-specific class declarations:
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_MMX |
||||
/// Class that implements MMX optimized routines for 16bit integer samples type.
|
||||
class TDStretchMMX : public TDStretch |
||||
{ |
||||
protected: |
||||
double calcCrossCorr(const short *mixingPos, const short *compare, double &norm) override; |
||||
double calcCrossCorrAccumulate(const short *mixingPos, const short *compare, double &norm) override; |
||||
virtual void overlapStereo(short *output, const short *input) const override; |
||||
virtual void clearCrossCorrState() override; |
||||
}; |
||||
#endif /// SOUNDTOUCH_ALLOW_MMX
|
||||
|
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_SSE |
||||
/// Class that implements SSE optimized routines for floating point samples type.
|
||||
class TDStretchSSE : public TDStretch |
||||
{ |
||||
protected: |
||||
double calcCrossCorr(const float *mixingPos, const float *compare, double &norm) override; |
||||
double calcCrossCorrAccumulate(const float *mixingPos, const float *compare, double &norm) override; |
||||
}; |
||||
|
||||
#endif /// SOUNDTOUCH_ALLOW_SSE
|
||||
|
||||
} |
||||
#endif /// TDStretch_H
|
||||
@ -0,0 +1,55 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// A header file for detecting the Intel MMX instructions set extension.
|
||||
///
|
||||
/// Please see 'mmx_win.cpp', 'mmx_cpp.cpp' and 'mmx_non_x86.cpp' for the
|
||||
/// routine implementations for x86 Windows, x86 gnu version and non-x86
|
||||
/// platforms, respectively.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef _CPU_DETECT_H_ |
||||
#define _CPU_DETECT_H_ |
||||
|
||||
#include "STTypes.h" |
||||
|
||||
#define SUPPORT_MMX 0x0001 |
||||
#define SUPPORT_3DNOW 0x0002 |
||||
#define SUPPORT_ALTIVEC 0x0004 |
||||
#define SUPPORT_SSE 0x0008 |
||||
#define SUPPORT_SSE2 0x0010 |
||||
|
||||
/// Checks which instruction set extensions are supported by the CPU.
|
||||
///
|
||||
/// \return A bitmask of supported extensions, see SUPPORT_... defines.
|
||||
uint detectCPUextensions(void); |
||||
|
||||
/// Disables given set of instruction extensions. See SUPPORT_... defines.
|
||||
void disableExtensions(uint wDisableMask); |
||||
|
||||
#endif // _CPU_DETECT_H_
|
||||
@ -0,0 +1,130 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// Generic version of the x86 CPU extension detection routine.
|
||||
///
|
||||
/// This file is for GNU & other non-Windows compilers, see 'cpu_detect_x86_win.cpp'
|
||||
/// for the Microsoft compiler version.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "cpu_detect.h" |
||||
#include "STTypes.h" |
||||
|
||||
|
||||
#if defined(SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS) |
||||
|
||||
#if defined(__GNUC__) && defined(__i386__) |
||||
// gcc
|
||||
#include "cpuid.h" |
||||
#elif defined(_M_IX86) |
||||
// windows non-gcc
|
||||
#include <intrin.h> |
||||
#endif |
||||
|
||||
#define bit_MMX (1 << 23) |
||||
#define bit_SSE (1 << 25) |
||||
#define bit_SSE2 (1 << 26) |
||||
#endif |
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// processor instructions extension detection routines
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Flag variable indicating whick ISA extensions are disabled (for debugging)
|
||||
static uint _dwDisabledISA = 0x00; // 0xffffffff; //<- use this to disable all extensions
|
||||
|
||||
// Disables given set of instruction extensions. See SUPPORT_... defines.
|
||||
void disableExtensions(uint dwDisableMask) |
||||
{ |
||||
_dwDisabledISA = dwDisableMask; |
||||
} |
||||
|
||||
|
||||
/// Checks which instruction set extensions are supported by the CPU.
|
||||
uint detectCPUextensions(void) |
||||
{ |
||||
/// If building for a 64bit system (no Itanium) and the user wants optimizations.
|
||||
/// Return the OR of SUPPORT_{MMX,SSE,SSE2}. 11001 or 0x19.
|
||||
/// Keep the _dwDisabledISA test (2 more operations, could be eliminated).
|
||||
#if ((defined(__GNUC__) && defined(__x86_64__)) \ |
||||
|| defined(_M_X64)) \
|
||||
&& defined(SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS) |
||||
return 0x19 & ~_dwDisabledISA; |
||||
|
||||
/// If building for a 32bit system and the user wants optimizations.
|
||||
/// Keep the _dwDisabledISA test (2 more operations, could be eliminated).
|
||||
#elif ((defined(__GNUC__) && defined(__i386__)) \ |
||||
|| defined(_M_IX86)) \
|
||||
&& defined(SOUNDTOUCH_ALLOW_X86_OPTIMIZATIONS) |
||||
|
||||
if (_dwDisabledISA == 0xffffffff) return 0; |
||||
|
||||
uint res = 0; |
||||
|
||||
#if defined(__GNUC__) |
||||
// GCC version of cpuid. Requires GCC 4.3.0 or later for __cpuid intrinsic support.
|
||||
uint eax, ebx, ecx, edx; // unsigned int is the standard type. uint is defined by the compiler and not guaranteed to be portable.
|
||||
|
||||
// Check if no cpuid support.
|
||||
if (!__get_cpuid (1, &eax, &ebx, &ecx, &edx)) return 0; // always disable extensions.
|
||||
|
||||
if (edx & bit_MMX) res = res | SUPPORT_MMX; |
||||
if (edx & bit_SSE) res = res | SUPPORT_SSE; |
||||
if (edx & bit_SSE2) res = res | SUPPORT_SSE2; |
||||
|
||||
#else |
||||
// Window / VS version of cpuid. Notice that Visual Studio 2005 or later required
|
||||
// for __cpuid intrinsic support.
|
||||
int reg[4] = {-1}; |
||||
|
||||
// Check if no cpuid support.
|
||||
__cpuid(reg,0); |
||||
if ((unsigned int)reg[0] == 0) return 0; // always disable extensions.
|
||||
|
||||
__cpuid(reg,1); |
||||
if ((unsigned int)reg[3] & bit_MMX) res = res | SUPPORT_MMX; |
||||
if ((unsigned int)reg[3] & bit_SSE) res = res | SUPPORT_SSE; |
||||
if ((unsigned int)reg[3] & bit_SSE2) res = res | SUPPORT_SSE2; |
||||
|
||||
#endif |
||||
|
||||
return res & ~_dwDisabledISA; |
||||
|
||||
#else |
||||
|
||||
/// One of these is true:
|
||||
/// 1) We don't want optimizations.
|
||||
/// 2) Using an unsupported compiler.
|
||||
/// 3) Running on a non-x86 platform.
|
||||
return 0; |
||||
|
||||
#endif |
||||
} |
||||
@ -0,0 +1,392 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// MMX optimized routines. All MMX optimized functions have been gathered into
|
||||
/// this single source code file, regardless to their class or original source
|
||||
/// code file, in order to ease porting the library to other compiler and
|
||||
/// processor platforms.
|
||||
///
|
||||
/// The MMX-optimizations are programmed using MMX compiler intrinsics that
|
||||
/// are supported both by Microsoft Visual C++ and GCC compilers, so this file
|
||||
/// should compile with both toolsets.
|
||||
///
|
||||
/// NOTICE: If using Visual Studio 6.0, you'll need to install the "Visual C++
|
||||
/// 6.0 processor pack" update to support compiler intrinsic syntax. The update
|
||||
/// is available for download at Microsoft Developers Network, see here:
|
||||
/// http://msdn.microsoft.com/en-us/vstudio/aa718349.aspx
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "STTypes.h" |
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_MMX |
||||
// MMX routines available only with integer sample type
|
||||
|
||||
using namespace soundtouch; |
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// implementation of MMX optimized functions of class 'TDStretchMMX'
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "TDStretch.h" |
||||
#include <mmintrin.h> |
||||
#include <limits.h> |
||||
#include <math.h> |
||||
|
||||
|
||||
// Calculates cross correlation of two buffers
|
||||
double TDStretchMMX::calcCrossCorr(const short *pV1, const short *pV2, double &dnorm) |
||||
{ |
||||
const __m64 *pVec1, *pVec2; |
||||
__m64 shifter; |
||||
__m64 accu, normaccu; |
||||
long corr, norm; |
||||
int i; |
||||
|
||||
pVec1 = (__m64*)pV1; |
||||
pVec2 = (__m64*)pV2; |
||||
|
||||
shifter = _m_from_int(overlapDividerBitsNorm); |
||||
normaccu = accu = _mm_setzero_si64(); |
||||
|
||||
// Process 4 parallel sets of 2 * stereo samples or 4 * mono samples
|
||||
// during each round for improved CPU-level parallellization.
|
||||
for (i = 0; i < channels * overlapLength / 16; i ++) |
||||
{ |
||||
__m64 temp, temp2; |
||||
|
||||
// dictionary of instructions:
|
||||
// _m_pmaddwd : 4*16bit multiply-add, resulting two 32bits = [a0*b0+a1*b1 ; a2*b2+a3*b3]
|
||||
// _mm_add_pi32 : 2*32bit add
|
||||
// _m_psrad : 32bit right-shift
|
||||
|
||||
temp = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[0], pVec2[0]), shifter), |
||||
_mm_sra_pi32(_mm_madd_pi16(pVec1[1], pVec2[1]), shifter)); |
||||
temp2 = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[0], pVec1[0]), shifter), |
||||
_mm_sra_pi32(_mm_madd_pi16(pVec1[1], pVec1[1]), shifter)); |
||||
accu = _mm_add_pi32(accu, temp); |
||||
normaccu = _mm_add_pi32(normaccu, temp2); |
||||
|
||||
temp = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[2], pVec2[2]), shifter), |
||||
_mm_sra_pi32(_mm_madd_pi16(pVec1[3], pVec2[3]), shifter)); |
||||
temp2 = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[2], pVec1[2]), shifter), |
||||
_mm_sra_pi32(_mm_madd_pi16(pVec1[3], pVec1[3]), shifter)); |
||||
accu = _mm_add_pi32(accu, temp); |
||||
normaccu = _mm_add_pi32(normaccu, temp2); |
||||
|
||||
pVec1 += 4; |
||||
pVec2 += 4; |
||||
} |
||||
|
||||
// copy hi-dword of mm0 to lo-dword of mm1, then sum mmo+mm1
|
||||
// and finally store the result into the variable "corr"
|
||||
|
||||
accu = _mm_add_pi32(accu, _mm_srli_si64(accu, 32)); |
||||
corr = _m_to_int(accu); |
||||
|
||||
normaccu = _mm_add_pi32(normaccu, _mm_srli_si64(normaccu, 32)); |
||||
norm = _m_to_int(normaccu); |
||||
|
||||
// Clear MMS state
|
||||
_m_empty(); |
||||
|
||||
if (norm > (long)maxnorm) |
||||
{ |
||||
// modify 'maxnorm' inside critical section to avoid multi-access conflict if in OpenMP mode
|
||||
maxnorm = norm; |
||||
} |
||||
|
||||
// Normalize result by dividing by sqrt(norm) - this step is easiest
|
||||
// done using floating point operation
|
||||
dnorm = (double)norm; |
||||
|
||||
return (double)corr / sqrt(dnorm < 1e-9 ? 1.0 : dnorm); |
||||
// Note: Warning about the missing EMMS instruction is harmless
|
||||
// as it'll be called elsewhere.
|
||||
} |
||||
|
||||
|
||||
/// Update cross-correlation by accumulating "norm" coefficient by previously calculated value
|
||||
double TDStretchMMX::calcCrossCorrAccumulate(const short *pV1, const short *pV2, double &dnorm) |
||||
{ |
||||
const __m64 *pVec1, *pVec2; |
||||
__m64 shifter; |
||||
__m64 accu; |
||||
long corr, lnorm; |
||||
int i; |
||||
|
||||
// cancel first normalizer tap from previous round
|
||||
lnorm = 0; |
||||
for (i = 1; i <= channels; i ++) |
||||
{ |
||||
lnorm -= (pV1[-i] * pV1[-i]) >> overlapDividerBitsNorm; |
||||
} |
||||
|
||||
pVec1 = (__m64*)pV1; |
||||
pVec2 = (__m64*)pV2; |
||||
|
||||
shifter = _m_from_int(overlapDividerBitsNorm); |
||||
accu = _mm_setzero_si64(); |
||||
|
||||
// Process 4 parallel sets of 2 * stereo samples or 4 * mono samples
|
||||
// during each round for improved CPU-level parallellization.
|
||||
for (i = 0; i < channels * overlapLength / 16; i ++) |
||||
{ |
||||
__m64 temp; |
||||
|
||||
// dictionary of instructions:
|
||||
// _m_pmaddwd : 4*16bit multiply-add, resulting two 32bits = [a0*b0+a1*b1 ; a2*b2+a3*b3]
|
||||
// _mm_add_pi32 : 2*32bit add
|
||||
// _m_psrad : 32bit right-shift
|
||||
|
||||
temp = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[0], pVec2[0]), shifter), |
||||
_mm_sra_pi32(_mm_madd_pi16(pVec1[1], pVec2[1]), shifter)); |
||||
accu = _mm_add_pi32(accu, temp); |
||||
|
||||
temp = _mm_add_pi32(_mm_sra_pi32(_mm_madd_pi16(pVec1[2], pVec2[2]), shifter), |
||||
_mm_sra_pi32(_mm_madd_pi16(pVec1[3], pVec2[3]), shifter)); |
||||
accu = _mm_add_pi32(accu, temp); |
||||
|
||||
pVec1 += 4; |
||||
pVec2 += 4; |
||||
} |
||||
|
||||
// copy hi-dword of mm0 to lo-dword of mm1, then sum mmo+mm1
|
||||
// and finally store the result into the variable "corr"
|
||||
|
||||
accu = _mm_add_pi32(accu, _mm_srli_si64(accu, 32)); |
||||
corr = _m_to_int(accu); |
||||
|
||||
// Clear MMS state
|
||||
_m_empty(); |
||||
|
||||
// update normalizer with last samples of this round
|
||||
pV1 = (short *)pVec1; |
||||
for (int j = 1; j <= channels; j ++) |
||||
{ |
||||
lnorm += (pV1[-j] * pV1[-j]) >> overlapDividerBitsNorm; |
||||
} |
||||
dnorm += (double)lnorm; |
||||
|
||||
if (lnorm > (long)maxnorm) |
||||
{ |
||||
maxnorm = lnorm; |
||||
} |
||||
|
||||
// Normalize result by dividing by sqrt(norm) - this step is easiest
|
||||
// done using floating point operation
|
||||
return (double)corr / sqrt((dnorm < 1e-9) ? 1.0 : dnorm); |
||||
} |
||||
|
||||
|
||||
void TDStretchMMX::clearCrossCorrState() |
||||
{ |
||||
// Clear MMS state
|
||||
_m_empty(); |
||||
//_asm EMMS;
|
||||
} |
||||
|
||||
|
||||
// MMX-optimized version of the function overlapStereo
|
||||
void TDStretchMMX::overlapStereo(short *output, const short *input) const |
||||
{ |
||||
const __m64 *pVinput, *pVMidBuf; |
||||
__m64 *pVdest; |
||||
__m64 mix1, mix2, adder, shifter; |
||||
int i; |
||||
|
||||
pVinput = (const __m64*)input; |
||||
pVMidBuf = (const __m64*)pMidBuffer; |
||||
pVdest = (__m64*)output; |
||||
|
||||
// mix1 = mixer values for 1st stereo sample
|
||||
// mix1 = mixer values for 2nd stereo sample
|
||||
// adder = adder for updating mixer values after each round
|
||||
|
||||
mix1 = _mm_set_pi16(0, overlapLength, 0, overlapLength); |
||||
adder = _mm_set_pi16(1, -1, 1, -1); |
||||
mix2 = _mm_add_pi16(mix1, adder); |
||||
adder = _mm_add_pi16(adder, adder); |
||||
|
||||
// Overlaplength-division by shifter. "+1" is to account for "-1" deduced in
|
||||
// overlapDividerBits calculation earlier.
|
||||
shifter = _m_from_int(overlapDividerBitsPure + 1); |
||||
|
||||
for (i = 0; i < overlapLength / 4; i ++) |
||||
{ |
||||
__m64 temp1, temp2; |
||||
|
||||
// load & shuffle data so that input & mixbuffer data samples are paired
|
||||
temp1 = _mm_unpacklo_pi16(pVMidBuf[0], pVinput[0]); // = i0l m0l i0r m0r
|
||||
temp2 = _mm_unpackhi_pi16(pVMidBuf[0], pVinput[0]); // = i1l m1l i1r m1r
|
||||
|
||||
// temp = (temp .* mix) >> shifter
|
||||
temp1 = _mm_sra_pi32(_mm_madd_pi16(temp1, mix1), shifter); |
||||
temp2 = _mm_sra_pi32(_mm_madd_pi16(temp2, mix2), shifter); |
||||
pVdest[0] = _mm_packs_pi32(temp1, temp2); // pack 2*2*32bit => 4*16bit
|
||||
|
||||
// update mix += adder
|
||||
mix1 = _mm_add_pi16(mix1, adder); |
||||
mix2 = _mm_add_pi16(mix2, adder); |
||||
|
||||
// --- second round begins here ---
|
||||
|
||||
// load & shuffle data so that input & mixbuffer data samples are paired
|
||||
temp1 = _mm_unpacklo_pi16(pVMidBuf[1], pVinput[1]); // = i2l m2l i2r m2r
|
||||
temp2 = _mm_unpackhi_pi16(pVMidBuf[1], pVinput[1]); // = i3l m3l i3r m3r
|
||||
|
||||
// temp = (temp .* mix) >> shifter
|
||||
temp1 = _mm_sra_pi32(_mm_madd_pi16(temp1, mix1), shifter); |
||||
temp2 = _mm_sra_pi32(_mm_madd_pi16(temp2, mix2), shifter); |
||||
pVdest[1] = _mm_packs_pi32(temp1, temp2); // pack 2*2*32bit => 4*16bit
|
||||
|
||||
// update mix += adder
|
||||
mix1 = _mm_add_pi16(mix1, adder); |
||||
mix2 = _mm_add_pi16(mix2, adder); |
||||
|
||||
pVinput += 2; |
||||
pVMidBuf += 2; |
||||
pVdest += 2; |
||||
} |
||||
|
||||
_m_empty(); // clear MMS state
|
||||
} |
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// implementation of MMX optimized functions of class 'FIRFilter'
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "FIRFilter.h" |
||||
|
||||
|
||||
FIRFilterMMX::FIRFilterMMX() : FIRFilter() |
||||
{ |
||||
filterCoeffsAlign = nullptr; |
||||
filterCoeffsUnalign = nullptr; |
||||
} |
||||
|
||||
|
||||
FIRFilterMMX::~FIRFilterMMX() |
||||
{ |
||||
delete[] filterCoeffsUnalign; |
||||
} |
||||
|
||||
|
||||
// (overloaded) Calculates filter coefficients for MMX routine
|
||||
void FIRFilterMMX::setCoefficients(const short *coeffs, uint newLength, uint uResultDivFactor) |
||||
{ |
||||
uint i; |
||||
FIRFilter::setCoefficients(coeffs, newLength, uResultDivFactor); |
||||
|
||||
// Ensure that filter coeffs array is aligned to 16-byte boundary
|
||||
delete[] filterCoeffsUnalign; |
||||
filterCoeffsUnalign = new short[2 * newLength + 8]; |
||||
filterCoeffsAlign = (short *)SOUNDTOUCH_ALIGN_POINTER_16(filterCoeffsUnalign); |
||||
|
||||
// rearrange the filter coefficients for mmx routines
|
||||
for (i = 0;i < length; i += 4) |
||||
{ |
||||
filterCoeffsAlign[2 * i + 0] = coeffs[i + 0]; |
||||
filterCoeffsAlign[2 * i + 1] = coeffs[i + 2]; |
||||
filterCoeffsAlign[2 * i + 2] = coeffs[i + 0]; |
||||
filterCoeffsAlign[2 * i + 3] = coeffs[i + 2]; |
||||
|
||||
filterCoeffsAlign[2 * i + 4] = coeffs[i + 1]; |
||||
filterCoeffsAlign[2 * i + 5] = coeffs[i + 3]; |
||||
filterCoeffsAlign[2 * i + 6] = coeffs[i + 1]; |
||||
filterCoeffsAlign[2 * i + 7] = coeffs[i + 3]; |
||||
} |
||||
} |
||||
|
||||
|
||||
// mmx-optimized version of the filter routine for stereo sound
|
||||
uint FIRFilterMMX::evaluateFilterStereo(short *dest, const short *src, uint numSamples) const |
||||
{ |
||||
// Create stack copies of the needed member variables for asm routines :
|
||||
uint i, j; |
||||
__m64 *pVdest = (__m64*)dest; |
||||
|
||||
if (length < 2) return 0; |
||||
|
||||
for (i = 0; i < (numSamples - length) / 2; i ++) |
||||
{ |
||||
__m64 accu1; |
||||
__m64 accu2; |
||||
const __m64 *pVsrc = (const __m64*)src; |
||||
const __m64 *pVfilter = (const __m64*)filterCoeffsAlign; |
||||
|
||||
accu1 = accu2 = _mm_setzero_si64(); |
||||
for (j = 0; j < lengthDiv8 * 2; j ++) |
||||
{ |
||||
__m64 temp1, temp2; |
||||
|
||||
temp1 = _mm_unpacklo_pi16(pVsrc[0], pVsrc[1]); // = l2 l0 r2 r0
|
||||
temp2 = _mm_unpackhi_pi16(pVsrc[0], pVsrc[1]); // = l3 l1 r3 r1
|
||||
|
||||
accu1 = _mm_add_pi32(accu1, _mm_madd_pi16(temp1, pVfilter[0])); // += l2*f2+l0*f0 r2*f2+r0*f0
|
||||
accu1 = _mm_add_pi32(accu1, _mm_madd_pi16(temp2, pVfilter[1])); // += l3*f3+l1*f1 r3*f3+r1*f1
|
||||
|
||||
temp1 = _mm_unpacklo_pi16(pVsrc[1], pVsrc[2]); // = l4 l2 r4 r2
|
||||
|
||||
accu2 = _mm_add_pi32(accu2, _mm_madd_pi16(temp2, pVfilter[0])); // += l3*f2+l1*f0 r3*f2+r1*f0
|
||||
accu2 = _mm_add_pi32(accu2, _mm_madd_pi16(temp1, pVfilter[1])); // += l4*f3+l2*f1 r4*f3+r2*f1
|
||||
|
||||
// accu1 += l2*f2+l0*f0 r2*f2+r0*f0
|
||||
// += l3*f3+l1*f1 r3*f3+r1*f1
|
||||
|
||||
// accu2 += l3*f2+l1*f0 r3*f2+r1*f0
|
||||
// l4*f3+l2*f1 r4*f3+r2*f1
|
||||
|
||||
pVfilter += 2; |
||||
pVsrc += 2; |
||||
} |
||||
// accu >>= resultDivFactor
|
||||
accu1 = _mm_srai_pi32(accu1, resultDivFactor); |
||||
accu2 = _mm_srai_pi32(accu2, resultDivFactor); |
||||
|
||||
// pack 2*2*32bits => 4*16 bits
|
||||
pVdest[0] = _mm_packs_pi32(accu1, accu2); |
||||
src += 4; |
||||
pVdest ++; |
||||
} |
||||
|
||||
_m_empty(); // clear emms state
|
||||
|
||||
return (numSamples & 0xfffffffe) - length; |
||||
} |
||||
|
||||
#else |
||||
|
||||
// workaround to not complain about empty module
|
||||
bool _dontcomplain_mmx_empty; |
||||
|
||||
#endif // SOUNDTOUCH_ALLOW_MMX
|
||||
@ -0,0 +1,364 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// SSE optimized routines for Pentium-III, Athlon-XP and later CPUs. All SSE
|
||||
/// optimized functions have been gathered into this single source
|
||||
/// code file, regardless to their class or original source code file, in order
|
||||
/// to ease porting the library to other compiler and processor platforms.
|
||||
///
|
||||
/// The SSE-optimizations are programmed using SSE compiler intrinsics that
|
||||
/// are supported both by Microsoft Visual C++ and GCC compilers, so this file
|
||||
/// should compile with both toolsets.
|
||||
///
|
||||
/// NOTICE: If using Visual Studio 6.0, you'll need to install the "Visual C++
|
||||
/// 6.0 processor pack" update to support SSE instruction set. The update is
|
||||
/// available for download at Microsoft Developers Network, see here:
|
||||
/// http://msdn.microsoft.com/en-us/vstudio/aa718349.aspx
|
||||
///
|
||||
/// If the above URL is expired or removed, go to "http://msdn.microsoft.com" and
|
||||
/// perform a search with keywords "processor pack".
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "cpu_detect.h" |
||||
#include "STTypes.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
#ifdef SOUNDTOUCH_ALLOW_SSE |
||||
|
||||
// SSE routines available only with float sample type
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// implementation of SSE optimized functions of class 'TDStretchSSE'
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "TDStretch.h" |
||||
#include <xmmintrin.h> |
||||
#include <math.h> |
||||
|
||||
// Calculates cross correlation of two buffers
|
||||
double TDStretchSSE::calcCrossCorr(const float *pV1, const float *pV2, double &anorm) |
||||
{ |
||||
int i; |
||||
const float *pVec1; |
||||
const __m128 *pVec2; |
||||
__m128 vSum, vNorm; |
||||
|
||||
// Note. It means a major slow-down if the routine needs to tolerate
|
||||
// unaligned __m128 memory accesses. It's way faster if we can skip
|
||||
// unaligned slots and use _mm_load_ps instruction instead of _mm_loadu_ps.
|
||||
// This can mean up to ~ 10-fold difference (incl. part of which is
|
||||
// due to skipping every second round for stereo sound though).
|
||||
//
|
||||
// Compile-time define SOUNDTOUCH_ALLOW_NONEXACT_SIMD_OPTIMIZATION is provided
|
||||
// for choosing if this little cheating is allowed.
|
||||
|
||||
#ifdef ST_SIMD_AVOID_UNALIGNED |
||||
// Little cheating allowed, return valid correlation only for
|
||||
// aligned locations, meaning every second round for stereo sound.
|
||||
|
||||
#define _MM_LOAD _mm_load_ps |
||||
|
||||
if (((ulongptr)pV1) & 15) return -1e50; // skip unaligned locations
|
||||
|
||||
#else |
||||
// No cheating allowed, use unaligned load & take the resulting
|
||||
// performance hit.
|
||||
#define _MM_LOAD _mm_loadu_ps |
||||
#endif |
||||
|
||||
// ensure overlapLength is divisible by 8
|
||||
assert((overlapLength % 8) == 0); |
||||
|
||||
// Calculates the cross-correlation value between 'pV1' and 'pV2' vectors
|
||||
// Note: pV2 _must_ be aligned to 16-bit boundary, pV1 need not.
|
||||
pVec1 = (const float*)pV1; |
||||
pVec2 = (const __m128*)pV2; |
||||
vSum = vNorm = _mm_setzero_ps(); |
||||
|
||||
// Unroll the loop by factor of 4 * 4 operations. Use same routine for
|
||||
// stereo & mono, for mono it just means twice the amount of unrolling.
|
||||
for (i = 0; i < channels * overlapLength / 16; i ++) |
||||
{ |
||||
__m128 vTemp; |
||||
// vSum += pV1[0..3] * pV2[0..3]
|
||||
vTemp = _MM_LOAD(pVec1); |
||||
vSum = _mm_add_ps(vSum, _mm_mul_ps(vTemp ,pVec2[0])); |
||||
vNorm = _mm_add_ps(vNorm, _mm_mul_ps(vTemp ,vTemp)); |
||||
|
||||
// vSum += pV1[4..7] * pV2[4..7]
|
||||
vTemp = _MM_LOAD(pVec1 + 4); |
||||
vSum = _mm_add_ps(vSum, _mm_mul_ps(vTemp, pVec2[1])); |
||||
vNorm = _mm_add_ps(vNorm, _mm_mul_ps(vTemp ,vTemp)); |
||||
|
||||
// vSum += pV1[8..11] * pV2[8..11]
|
||||
vTemp = _MM_LOAD(pVec1 + 8); |
||||
vSum = _mm_add_ps(vSum, _mm_mul_ps(vTemp, pVec2[2])); |
||||
vNorm = _mm_add_ps(vNorm, _mm_mul_ps(vTemp ,vTemp)); |
||||
|
||||
// vSum += pV1[12..15] * pV2[12..15]
|
||||
vTemp = _MM_LOAD(pVec1 + 12); |
||||
vSum = _mm_add_ps(vSum, _mm_mul_ps(vTemp, pVec2[3])); |
||||
vNorm = _mm_add_ps(vNorm, _mm_mul_ps(vTemp ,vTemp)); |
||||
|
||||
pVec1 += 16; |
||||
pVec2 += 4; |
||||
} |
||||
|
||||
// return value = vSum[0] + vSum[1] + vSum[2] + vSum[3]
|
||||
float *pvNorm = (float*)&vNorm; |
||||
float norm = (pvNorm[0] + pvNorm[1] + pvNorm[2] + pvNorm[3]); |
||||
anorm = norm; |
||||
|
||||
float *pvSum = (float*)&vSum; |
||||
return (double)(pvSum[0] + pvSum[1] + pvSum[2] + pvSum[3]) / sqrt(norm < 1e-9 ? 1.0 : norm); |
||||
|
||||
/* This is approximately corresponding routine in C-language yet without normalization:
|
||||
double corr, norm; |
||||
uint i; |
||||
|
||||
// Calculates the cross-correlation value between 'pV1' and 'pV2' vectors
|
||||
corr = norm = 0.0; |
||||
for (i = 0; i < channels * overlapLength / 16; i ++) |
||||
{ |
||||
corr += pV1[0] * pV2[0] + |
||||
pV1[1] * pV2[1] + |
||||
pV1[2] * pV2[2] + |
||||
pV1[3] * pV2[3] + |
||||
pV1[4] * pV2[4] + |
||||
pV1[5] * pV2[5] + |
||||
pV1[6] * pV2[6] + |
||||
pV1[7] * pV2[7] + |
||||
pV1[8] * pV2[8] + |
||||
pV1[9] * pV2[9] + |
||||
pV1[10] * pV2[10] + |
||||
pV1[11] * pV2[11] + |
||||
pV1[12] * pV2[12] + |
||||
pV1[13] * pV2[13] + |
||||
pV1[14] * pV2[14] + |
||||
pV1[15] * pV2[15]; |
||||
|
||||
for (j = 0; j < 15; j ++) norm += pV1[j] * pV1[j]; |
||||
|
||||
pV1 += 16; |
||||
pV2 += 16; |
||||
} |
||||
return corr / sqrt(norm); |
||||
*/ |
||||
} |
||||
|
||||
|
||||
|
||||
double TDStretchSSE::calcCrossCorrAccumulate(const float *pV1, const float *pV2, double &norm) |
||||
{ |
||||
// call usual calcCrossCorr function because SSE does not show big benefit of
|
||||
// accumulating "norm" value, and also the "norm" rolling algorithm would get
|
||||
// complicated due to SSE-specific alignment-vs-nonexact correlation rules.
|
||||
return calcCrossCorr(pV1, pV2, norm); |
||||
} |
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// implementation of SSE optimized functions of class 'FIRFilter'
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "FIRFilter.h" |
||||
|
||||
FIRFilterSSE::FIRFilterSSE() : FIRFilter() |
||||
{ |
||||
filterCoeffsAlign = nullptr; |
||||
filterCoeffsUnalign = nullptr; |
||||
} |
||||
|
||||
|
||||
FIRFilterSSE::~FIRFilterSSE() |
||||
{ |
||||
delete[] filterCoeffsUnalign; |
||||
filterCoeffsAlign = nullptr; |
||||
filterCoeffsUnalign = nullptr; |
||||
} |
||||
|
||||
|
||||
// (overloaded) Calculates filter coefficients for SSE routine
|
||||
void FIRFilterSSE::setCoefficients(const float *coeffs, uint newLength, uint uResultDivFactor) |
||||
{ |
||||
uint i; |
||||
float fDivider; |
||||
|
||||
FIRFilter::setCoefficients(coeffs, newLength, uResultDivFactor); |
||||
|
||||
// Scale the filter coefficients so that it won't be necessary to scale the filtering result
|
||||
// also rearrange coefficients suitably for SSE
|
||||
// Ensure that filter coeffs array is aligned to 16-byte boundary
|
||||
delete[] filterCoeffsUnalign; |
||||
filterCoeffsUnalign = new float[2 * newLength + 4]; |
||||
filterCoeffsAlign = (float *)SOUNDTOUCH_ALIGN_POINTER_16(filterCoeffsUnalign); |
||||
|
||||
fDivider = (float)resultDivider; |
||||
|
||||
// rearrange the filter coefficients for mmx routines
|
||||
for (i = 0; i < newLength; i ++) |
||||
{ |
||||
filterCoeffsAlign[2 * i + 0] = |
||||
filterCoeffsAlign[2 * i + 1] = coeffs[i + 0] / fDivider; |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
// SSE-optimized version of the filter routine for stereo sound
|
||||
uint FIRFilterSSE::evaluateFilterStereo(float *dest, const float *source, uint numSamples) const |
||||
{ |
||||
int count = (int)((numSamples - length) & (uint)-2); |
||||
int j; |
||||
|
||||
assert(count % 2 == 0); |
||||
|
||||
if (count < 2) return 0; |
||||
|
||||
assert(source != nullptr); |
||||
assert(dest != nullptr); |
||||
assert((length % 8) == 0); |
||||
assert(filterCoeffsAlign != nullptr); |
||||
assert(((ulongptr)filterCoeffsAlign) % 16 == 0); |
||||
|
||||
// filter is evaluated for two stereo samples with each iteration, thus use of 'j += 2'
|
||||
for (j = 0; j < count; j += 2) |
||||
{ |
||||
const float *pSrc; |
||||
float *pDest; |
||||
const __m128 *pFil; |
||||
__m128 sum1, sum2; |
||||
uint i; |
||||
|
||||
pSrc = (const float*)source + j * 2; // source audio data
|
||||
pDest = dest + j * 2; // destination audio data
|
||||
pFil = (const __m128*)filterCoeffsAlign; // filter coefficients. NOTE: Assumes coefficients
|
||||
// are aligned to 16-byte boundary
|
||||
sum1 = sum2 = _mm_setzero_ps(); |
||||
|
||||
for (i = 0; i < length / 8; i ++) |
||||
{ |
||||
// Unroll loop for efficiency & calculate filter for 2*2 stereo samples
|
||||
// at each pass
|
||||
|
||||
// sum1 is accu for 2*2 filtered stereo sound data at the primary sound data offset
|
||||
// sum2 is accu for 2*2 filtered stereo sound data for the next sound sample offset.
|
||||
|
||||
sum1 = _mm_add_ps(sum1, _mm_mul_ps(_mm_loadu_ps(pSrc) , pFil[0])); |
||||
sum2 = _mm_add_ps(sum2, _mm_mul_ps(_mm_loadu_ps(pSrc + 2), pFil[0])); |
||||
|
||||
sum1 = _mm_add_ps(sum1, _mm_mul_ps(_mm_loadu_ps(pSrc + 4), pFil[1])); |
||||
sum2 = _mm_add_ps(sum2, _mm_mul_ps(_mm_loadu_ps(pSrc + 6), pFil[1])); |
||||
|
||||
sum1 = _mm_add_ps(sum1, _mm_mul_ps(_mm_loadu_ps(pSrc + 8) , pFil[2])); |
||||
sum2 = _mm_add_ps(sum2, _mm_mul_ps(_mm_loadu_ps(pSrc + 10), pFil[2])); |
||||
|
||||
sum1 = _mm_add_ps(sum1, _mm_mul_ps(_mm_loadu_ps(pSrc + 12), pFil[3])); |
||||
sum2 = _mm_add_ps(sum2, _mm_mul_ps(_mm_loadu_ps(pSrc + 14), pFil[3])); |
||||
|
||||
pSrc += 16; |
||||
pFil += 4; |
||||
} |
||||
|
||||
// Now sum1 and sum2 both have a filtered 2-channel sample each, but we still need
|
||||
// to sum the two hi- and lo-floats of these registers together.
|
||||
|
||||
// post-shuffle & add the filtered values and store to dest.
|
||||
_mm_storeu_ps(pDest, _mm_add_ps( |
||||
_mm_shuffle_ps(sum1, sum2, _MM_SHUFFLE(1,0,3,2)), // s2_1 s2_0 s1_3 s1_2
|
||||
_mm_shuffle_ps(sum1, sum2, _MM_SHUFFLE(3,2,1,0)) // s2_3 s2_2 s1_1 s1_0
|
||||
)); |
||||
} |
||||
|
||||
// Ideas for further improvement:
|
||||
// 1. If it could be guaranteed that 'source' were always aligned to 16-byte
|
||||
// boundary, a faster aligned '_mm_load_ps' instruction could be used.
|
||||
// 2. If it could be guaranteed that 'dest' were always aligned to 16-byte
|
||||
// boundary, a faster '_mm_store_ps' instruction could be used.
|
||||
|
||||
return (uint)count; |
||||
|
||||
/* original routine in C-language. please notice the C-version has differently
|
||||
organized coefficients though. |
||||
double suml1, suml2; |
||||
double sumr1, sumr2; |
||||
uint i, j; |
||||
|
||||
for (j = 0; j < count; j += 2) |
||||
{ |
||||
const float *ptr; |
||||
const float *pFil; |
||||
|
||||
suml1 = sumr1 = 0.0; |
||||
suml2 = sumr2 = 0.0; |
||||
ptr = src; |
||||
pFil = filterCoeffs; |
||||
for (i = 0; i < lengthLocal; i ++) |
||||
{ |
||||
// unroll loop for efficiency.
|
||||
|
||||
suml1 += ptr[0] * pFil[0] + |
||||
ptr[2] * pFil[2] + |
||||
ptr[4] * pFil[4] + |
||||
ptr[6] * pFil[6]; |
||||
|
||||
sumr1 += ptr[1] * pFil[1] + |
||||
ptr[3] * pFil[3] + |
||||
ptr[5] * pFil[5] + |
||||
ptr[7] * pFil[7]; |
||||
|
||||
suml2 += ptr[8] * pFil[0] + |
||||
ptr[10] * pFil[2] + |
||||
ptr[12] * pFil[4] + |
||||
ptr[14] * pFil[6]; |
||||
|
||||
sumr2 += ptr[9] * pFil[1] + |
||||
ptr[11] * pFil[3] + |
||||
ptr[13] * pFil[5] + |
||||
ptr[15] * pFil[7]; |
||||
|
||||
ptr += 16; |
||||
pFil += 8; |
||||
} |
||||
dest[0] = (float)suml1; |
||||
dest[1] = (float)sumr1; |
||||
dest[2] = (float)suml2; |
||||
dest[3] = (float)sumr2; |
||||
|
||||
src += 4; |
||||
dest += 4; |
||||
} |
||||
*/ |
||||
} |
||||
|
||||
#endif // SOUNDTOUCH_ALLOW_SSE
|
||||
@ -0,0 +1,250 @@
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
///
|
||||
/// SoundTouch DLL wrapper - wraps SoundTouch routines into a Dynamic Load
|
||||
/// Library interface.
|
||||
///
|
||||
/// Author : Copyright (c) Olli Parviainen
|
||||
/// Author e-mail : oparviai 'at' iki.fi
|
||||
/// SoundTouch WWW: http://www.surina.net/soundtouch
|
||||
///
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// License :
|
||||
//
|
||||
// SoundTouch audio processing library
|
||||
// Copyright (c) Olli Parviainen
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU Lesser General Public
|
||||
// License as published by the Free Software Foundation; either
|
||||
// version 2.1 of the License, or (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
#if defined(_WIN32) || defined(WIN32) |
||||
#include <windows.h> |
||||
|
||||
// DLL main in Windows compilation
|
||||
BOOL APIENTRY DllMain( HANDLE hModule, |
||||
DWORD ul_reason_for_call, |
||||
LPVOID lpReserved |
||||
) |
||||
{ |
||||
switch (ul_reason_for_call) |
||||
{ |
||||
case DLL_PROCESS_ATTACH: |
||||
case DLL_THREAD_ATTACH: |
||||
case DLL_THREAD_DETACH: |
||||
case DLL_PROCESS_DETACH: |
||||
break; |
||||
} |
||||
return TRUE; |
||||
} |
||||
#endif |
||||
|
||||
#include <limits.h> |
||||
#include <string.h> |
||||
#include "SoundTouchDLL.h" |
||||
#include "SoundTouch.h" |
||||
#include "BPMDetect.h" |
||||
|
||||
using namespace soundtouch; |
||||
|
||||
#ifdef SOUNDTOUCH_INTEGER_SAMPLES |
||||
#error "error - compile the dll version with float samples" |
||||
#endif // SOUNDTOUCH_INTEGER_SAMPLES
|
||||
|
||||
//////////////
|
||||
|
||||
SOUNDTOUCHDLL_API ST_HANDLE __cdecl soundtouch_createInstance() |
||||
{ |
||||
return new SoundTouch(); |
||||
} |
||||
|
||||
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_destroyInstance(ST_HANDLE h) |
||||
{ |
||||
delete static_cast<SoundTouch*>(h); |
||||
} |
||||
|
||||
|
||||
/// Get SoundTouch library version string
|
||||
SOUNDTOUCHDLL_API const char *__cdecl soundtouch_getVersionString() |
||||
{ |
||||
return SoundTouch::getVersionString(); |
||||
} |
||||
|
||||
|
||||
/// Get SoundTouch library version Id
|
||||
SOUNDTOUCHDLL_API uint __cdecl soundtouch_getVersionId() |
||||
{ |
||||
return SoundTouch::getVersionId(); |
||||
} |
||||
|
||||
/// Sets new rate control value. Normal rate = 1.0, smaller values
|
||||
/// represent slower rate, larger faster rates.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRate(ST_HANDLE h, float newRate) |
||||
{ |
||||
static_cast<SoundTouch*>(h)->setRate(newRate); |
||||
} |
||||
|
||||
|
||||
/// Sets new tempo control value. Normal tempo = 1.0, smaller values
|
||||
/// represent slower tempo, larger faster tempo.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setTempo(ST_HANDLE h, float newTempo) |
||||
{ |
||||
static_cast<SoundTouch*>(h)->setTempo(newTempo); |
||||
} |
||||
|
||||
/// Sets new rate control value as a difference in percents compared
|
||||
/// to the original rate (-50 .. +100 %)
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRateChange(ST_HANDLE h, float newRate) |
||||
{ |
||||
static_cast<SoundTouch*>(h)->setRateChange(newRate); |
||||
} |
||||
|
||||
/// Sets new tempo control value as a difference in percents compared
|
||||
/// to the original tempo (-50 .. +100 %)
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setTempoChange(ST_HANDLE h, float newTempo) |
||||
{ |
||||
static_cast<SoundTouch*>(h)->setTempoChange(newTempo); |
||||
} |
||||
|
||||
/// Sets new pitch control value. Original pitch = 1.0, smaller values
|
||||
/// represent lower pitches, larger values higher pitch.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitch(ST_HANDLE h, float newPitch) |
||||
{ |
||||
static_cast<SoundTouch*>(h)->setPitch(newPitch); |
||||
} |
||||
|
||||
/// Sets pitch change in octaves compared to the original pitch
|
||||
/// (-1.00 .. +1.00)
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitchOctaves(ST_HANDLE h, float newPitch) |
||||
{ |
||||
static_cast<SoundTouch*>(h)->setPitchOctaves(newPitch); |
||||
} |
||||
|
||||
/// Sets pitch change in semi-tones compared to the original pitch
|
||||
/// (-12 .. +12)
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setPitchSemiTones(ST_HANDLE h, float newPitch) |
||||
{ |
||||
static_cast<SoundTouch*>(h)->setPitchSemiTones(newPitch); |
||||
} |
||||
|
||||
|
||||
/// Sets the number of channels, 1 = mono, 2 = stereo
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setChannels(ST_HANDLE h, uint numChannels) |
||||
{ |
||||
static_cast<SoundTouch*>(h)->setChannels(numChannels); |
||||
} |
||||
|
||||
/// Sets sample rate.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_setSampleRate(ST_HANDLE h, uint srate) |
||||
{ |
||||
static_cast<SoundTouch*>(h)->setSampleRate(srate); |
||||
} |
||||
|
||||
/// Flushes the last samples from the processing pipeline to the output.
|
||||
/// Clears also the internal processing buffers.
|
||||
//
|
||||
/// Note: This function is meant for extracting the last samples of a sound
|
||||
/// stream. This function may introduce additional blank samples in the end
|
||||
/// of the sound stream, and thus it's not recommended to call this function
|
||||
/// in the middle of a sound stream.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_flush(ST_HANDLE h) |
||||
{ |
||||
static_cast<SoundTouch*>(h)->flush(); |
||||
} |
||||
|
||||
/// Adds 'numSamples' pcs of samples from the 'samples' memory position into
|
||||
/// the input of the object. Notice that sample rate _has_to_ be set before
|
||||
/// calling this function, otherwise throws a runtime_error exception.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_putSamples(ST_HANDLE h, |
||||
const SAMPLETYPE *samples, ///< Pointer to sample buffer.
|
||||
unsigned int numSamples ///< Number of samples in buffer. Notice
|
||||
///< that in case of stereo-sound a single sample
|
||||
///< contains data for both channels.
|
||||
) |
||||
{ |
||||
static_cast<SoundTouch*>(h)->putSamples(samples, numSamples); |
||||
} |
||||
|
||||
/// Clears all the samples in the object's output and internal processing
|
||||
/// buffers.
|
||||
SOUNDTOUCHDLL_API void __cdecl soundtouch_clear(ST_HANDLE h) |
||||
{ |
||||
return static_cast<SoundTouch*>(h)->clear(); |
||||
} |
||||
|
||||
/// Changes a setting controlling the processing system behaviour. See the
|
||||
/// 'SETTING_...' defines for available setting ID's.
|
||||
///
|
||||
/// \return 'nonzero' if the setting was successfully changed
|
||||
SOUNDTOUCHDLL_API int __cdecl soundtouch_setSetting(ST_HANDLE h, |
||||
int settingId, ///< Setting ID number. see SETTING_... defines.
|
||||
int value ///< New setting value.
|
||||
) |
||||
{ |
||||
return static_cast<SoundTouch*>(h)->setSetting(settingId, value); |
||||
} |
||||
|
||||
/// Reads a setting controlling the processing system behaviour. See the
|
||||
/// 'SETTING_...' defines for available setting ID's.
|
||||
///
|
||||
/// \return the setting value.
|
||||
SOUNDTOUCHDLL_API int __cdecl soundtouch_getSetting(ST_HANDLE h, |
||||
int settingId ///< Setting ID number, see SETTING_... defines.
|
||||
) |
||||
{ |
||||
return static_cast<SoundTouch*>(h)->getSetting(settingId); |
||||
} |
||||
|
||||
|
||||
/// Returns number of samples currently unprocessed.
|
||||
SOUNDTOUCHDLL_API uint __cdecl soundtouch_numUnprocessedSamples(ST_HANDLE h) |
||||
{ |
||||
return static_cast<SoundTouch*>(h)->numUnprocessedSamples(); |
||||
} |
||||
|
||||
|
||||
/// Receive ready samples from the processing pipeline.
|
||||
///
|
||||
/// if called with outBuffer=nullptr, just reduces amount of ready samples within the pipeline.
|
||||
SOUNDTOUCHDLL_API uint __cdecl soundtouch_receiveSamples(ST_HANDLE h, |
||||
SAMPLETYPE *outBuffer, ///< Buffer where to copy output samples.
|
||||
unsigned int maxSamples ///< How many samples to receive at max.
|
||||
) |
||||
{ |
||||
if (outBuffer) |
||||
{ |
||||
return static_cast<SoundTouch*>(h)->receiveSamples(outBuffer, maxSamples); |
||||
} |
||||
else |
||||
{ |
||||
return static_cast<SoundTouch*>(h)->receiveSamples(maxSamples); |
||||
} |
||||
} |
||||
|
||||
|
||||
/// Returns number of samples currently available.
|
||||
SOUNDTOUCHDLL_API uint __cdecl soundtouch_numSamples(ST_HANDLE h) |
||||
{ |
||||
return static_cast<SoundTouch*>(h)->numSamples(); |
||||
} |
||||
|
||||
|
||||
/// Returns nonzero if there aren't any samples available for outputting.
|
||||
SOUNDTOUCHDLL_API int __cdecl soundtouch_isEmpty(ST_HANDLE h) |
||||
{ |
||||
return static_cast<SoundTouch*>(h)->isEmpty(); |
||||
} |
||||
@ -0,0 +1,100 @@
|
||||
// Microsoft Visual C++ generated resource script. |
||||
// |
||||
#include "resource.h" |
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS |
||||
///////////////////////////////////////////////////////////////////////////// |
||||
// |
||||
// Generated from the TEXTINCLUDE 2 resource. |
||||
// |
||||
#include "winres.h" |
||||
|
||||
///////////////////////////////////////////////////////////////////////////// |
||||
#undef APSTUDIO_READONLY_SYMBOLS |
||||
|
||||
///////////////////////////////////////////////////////////////////////////// |
||||
// English (United States) resources |
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) |
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US |
||||
#pragma code_page(1252) |
||||
|
||||
#ifdef APSTUDIO_INVOKED |
||||
///////////////////////////////////////////////////////////////////////////// |
||||
// |
||||
// TEXTINCLUDE |
||||
// |
||||
|
||||
1 TEXTINCLUDE |
||||
BEGIN |
||||
"resource.h\0" |
||||
END |
||||
|
||||
2 TEXTINCLUDE |
||||
BEGIN |
||||
"#include ""afxres.h""\r\n" |
||||
"\0" |
||||
END |
||||
|
||||
3 TEXTINCLUDE |
||||
BEGIN |
||||
"\r\n" |
||||
"\0" |
||||
END |
||||
|
||||
#endif // APSTUDIO_INVOKED |
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////// |
||||
// |
||||
// Version |
||||
// |
||||
|
||||
VS_VERSION_INFO VERSIONINFO |
||||
FILEVERSION 2,3,2,0 |
||||
PRODUCTVERSION 2,3,2,0 |
||||
FILEFLAGSMASK 0x17L |
||||
#ifdef _DEBUG |
||||
FILEFLAGS 0x1L |
||||
#else |
||||
FILEFLAGS 0x0L |
||||
#endif |
||||
FILEOS 0x4L |
||||
FILETYPE 0x2L |
||||
FILESUBTYPE 0x0L |
||||
BEGIN |
||||
BLOCK "StringFileInfo" |
||||
BEGIN |
||||
BLOCK "000004b0" |
||||
BEGIN |
||||
VALUE "Comments", "SoundTouch Library licensed for 3rd party applications subject to LGPL license v2.1. Visit http://www.surina.net/soundtouch for more information about the SoundTouch library." |
||||
VALUE "FileDescription", "SoundTouch Dynamic Link Library" |
||||
VALUE "FileVersion", "2.3.3.0" |
||||
VALUE "InternalName", "SoundTouch" |
||||
VALUE "LegalCopyright", "Copyright (C) Olli Parviainen 2024" |
||||
VALUE "OriginalFilename", "SoundTouch.dll" |
||||
VALUE "ProductName", " SoundTouch Dynamic Link Library" |
||||
VALUE "ProductVersion", "2.3.3.0" |
||||
END |
||||
END |
||||
BLOCK "VarFileInfo" |
||||
BEGIN |
||||
VALUE "Translation", 0x0, 1200 |
||||
END |
||||
END |
||||
|
||||
#endif // English (United States) resources |
||||
///////////////////////////////////////////////////////////////////////////// |
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED |
||||
///////////////////////////////////////////////////////////////////////////// |
||||
// |
||||
// Generated from the TEXTINCLUDE 3 resource. |
||||
// |
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////// |
||||
#endif // not APSTUDIO_INVOKED |
||||
|
||||
@ -0,0 +1,15 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by SoundTouchDLL.rc
|
||||
//
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED |
||||
#ifndef APSTUDIO_READONLY_SYMBOLS |
||||
#define _APS_NEXT_RESOURCE_VALUE 101 |
||||
#define _APS_NEXT_COMMAND_VALUE 40001 |
||||
#define _APS_NEXT_CONTROL_VALUE 1000 |
||||
#define _APS_NEXT_SYMED_VALUE 101 |
||||
#endif |
||||
#endif |
||||
@ -0,0 +1,785 @@
|
||||
/*
|
||||
* dm_core.c — прямой p2p чат между двумя пользователями (DM) |
||||
* |
||||
* Отдельная подсистема (не канал): беседы хранятся в dm_conversations, |
||||
* сообщения в dm_messages, синхронизация идёт напрямую через etcp_router |
||||
* (сервис 0x34), при недоступности пира — через dm_mailbox (0x35). |
||||
* |
||||
* Два независимых направленных потока (я→пир, пир→я) с монотонным seq. |
||||
* Каноническое тело сообщения — см. dm_core.h (общее с dm_mailbox). |
||||
*/ |
||||
|
||||
#include "dm_core.h" |
||||
#include "dm_crypto.h" |
||||
#include "dm_mailbox.h" |
||||
#include "../chat/chat_core.h" |
||||
#include "../chat/chat_event.h" |
||||
#include "../utun_instance.h" |
||||
#include "../ntp_time.h" |
||||
#include "../routing_layer/etcp_router.h" |
||||
#include "../routing_layer/topo_node_sqlite.h" |
||||
#include "../routing_layer/topo_group.h" |
||||
#include "../transport_layer/etcp_api.h" |
||||
#include "../transport_layer/etcp.h" |
||||
#include "../transport_layer/secure_channel.h" |
||||
#include "../../lib/mem.h" |
||||
#include "../../lib/ll_queue.h" |
||||
#include "../../lib/debug_config.h" |
||||
#include "../../lib/platform_compat.h" |
||||
|
||||
#include <string.h> |
||||
#include <stdlib.h> |
||||
#include <sqlite3.h> |
||||
|
||||
#define DM_ID "dm_core" |
||||
#define DM_DATA_MAX 1024 /* макс размер открытого текста сообщения (v1 — текст) */ |
||||
|
||||
/* Подкоманды протокола DM (первый байт payload после ROUTER_SVC-заголовка). */ |
||||
#define DM_SUBCMD_MSG 0x01 /* [msg] — сообщение */ |
||||
#define DM_SUBCMD_ACK 0x02 /* [conv_id:8][seq:8] — подтверждение приёма */ |
||||
#define DM_SUBCMD_HELLO 0x03 /* [conv_id:8][last_out_seq:8][last_in_seq:8] — catch-up при connect */ |
||||
|
||||
/* Per-instance состояние модуля. */ |
||||
struct dm_state { |
||||
struct UTUN_INSTANCE* inst; |
||||
sqlite3* db; |
||||
int initialized; |
||||
}; |
||||
|
||||
/* ── вспомогательные ── */ |
||||
|
||||
static struct dm_state* dm_of(struct UTUN_INSTANCE* inst) { |
||||
return inst ? inst->dm : NULL; |
||||
} |
||||
|
||||
/* Выполнить SQL без результата. Возвращает 0/ошибку. */ |
||||
static int dm_exec(struct dm_state* dm, const char* sql) { |
||||
char* err = NULL; |
||||
if (sqlite3_exec(dm->db, sql, NULL, NULL, &err) != SQLITE_OK) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: sql failed: %s (%s)", DM_ID, sql, err ? err : "?"); |
||||
if (err) sqlite3_free(err); |
||||
return -1; |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
/* Создать таблицы бесед и сообщений (общие для всех узлов). */ |
||||
static void dm_create_tables(struct dm_state* dm) { |
||||
dm_exec(dm, "CREATE TABLE IF NOT EXISTS dm_conversations (" |
||||
" conv_id TEXT PRIMARY KEY," |
||||
" peer_node_id INTEGER NOT NULL," |
||||
" peer_x25519 BLOB NOT NULL," |
||||
" peer_ed25519 BLOB NOT NULL," |
||||
" peer_name TEXT DEFAULT ''," |
||||
" group_id INTEGER DEFAULT 0," |
||||
" last_out_seq INTEGER DEFAULT 0," |
||||
" last_in_seq INTEGER DEFAULT 0," |
||||
" created_at INTEGER DEFAULT 0," |
||||
" last_ts INTEGER DEFAULT 0)"); |
||||
dm_exec(dm, "CREATE TABLE IF NOT EXISTS dm_messages (" |
||||
" conv_id TEXT NOT NULL," |
||||
" dir INTEGER NOT NULL," /* 0=входящее, 1=исходящее */ |
||||
" seq INTEGER NOT NULL," |
||||
" ts INTEGER NOT NULL," |
||||
" author INTEGER NOT NULL," |
||||
" ct TEXT DEFAULT ''," |
||||
" data BLOB," /* зашифрованное тело (ciphertext+tag) */ |
||||
" sig BLOB," /* Ed25519 автора */ |
||||
" PRIMARY KEY (conv_id, dir, seq))"); |
||||
} |
||||
|
||||
/* Прочитать оба pubkey узла (x25519 + ed25519) из таблицы nodes. 0=ок, <0=нет. */ |
||||
static int dm_node_pubkeys(struct dm_state* dm, uint64_t node_id, uint8_t x25519[32], uint8_t ed25519[32]) { |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(dm->db, |
||||
"SELECT x25519_pubkey, ed25519_pubkey FROM nodes WHERE node_id=?", |
||||
-1, &st, NULL) != SQLITE_OK) return -1; |
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); |
||||
int rc = -1; |
||||
if (sqlite3_step(st) == SQLITE_ROW) { |
||||
const uint8_t* x = (const uint8_t*)sqlite3_column_blob(st, 0); |
||||
const uint8_t* e = (const uint8_t*)sqlite3_column_blob(st, 1); |
||||
if (x && e && sqlite3_column_bytes(st, 0) >= 32 && sqlite3_column_bytes(st, 1) >= 32) { |
||||
memcpy(x25519, x, 32); |
||||
memcpy(ed25519, e, 32); |
||||
rc = 0; |
||||
} |
||||
} |
||||
sqlite3_finalize(st); |
||||
return rc; |
||||
} |
||||
|
||||
/* Есть ли общая группа с пиром (anti-spam: принимаем DM только от знакомых из общих каналов). */ |
||||
static int dm_share_group(struct dm_state* dm, uint64_t peer_id) { |
||||
uint64_t* ch_ids = NULL; |
||||
int ch_count = 0; |
||||
if (topo_node_sqlite_get_member_channels(dm->db, dm->inst->node_id, &ch_ids, &ch_count) != 0) |
||||
return 0; |
||||
int shared = 0; |
||||
for (int i = 0; i < ch_count; i++) { |
||||
char ch[64]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)ch_ids[i]); |
||||
if (topo_node_sqlite_member_in_channel(dm->db, ch, peer_id)) { shared = 1; break; } |
||||
} |
||||
if (ch_ids) u_free(ch_ids); |
||||
return shared; |
||||
} |
||||
|
||||
/* Доступен ли пир для прямой доставки: живой ETCP-линк или присутствие в BGP-группе. */ |
||||
static int dm_peer_reachable(struct dm_state* dm, uint64_t peer_id) { |
||||
struct ETCP_CONN* conn = instance_find_conn(dm->inst, peer_id); |
||||
if (conn && conn->links_up && conn->initialized) return 1; |
||||
if (dm->inst->topo_groups && dm->inst->topo_groups->group_list) { |
||||
struct ll_entry* e = dm->inst->topo_groups->group_list->head; |
||||
while (e) { |
||||
struct TOPO_GROUP* g = (struct TOPO_GROUP*)e; |
||||
if (g->group_type == TOPO_GROUP_TYPE_CHAT && topo_node_find_by_id(g, peer_id)) return 1; |
||||
e = e->next; |
||||
} |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
/* Отправить payload подкомандой subcmd в dst через etcp_router (mode=0, своё E2E). */ |
||||
static int dm_route_send(struct dm_state* dm, uint64_t group_id, uint64_t dst, uint8_t subcmd, |
||||
const uint8_t* body, size_t body_len) { |
||||
struct ll_entry* entry = queue_entry_new(0); |
||||
if (!entry) return -1; |
||||
entry->dgram = u_malloc(1 + 1 + body_len); |
||||
if (!entry->dgram) { queue_entry_free(entry); return -1; } |
||||
entry->dgram[0] = ETCP_RT_ID_DM; |
||||
entry->dgram[1] = subcmd; |
||||
if (body_len) memcpy(entry->dgram + 2, body, body_len); |
||||
entry->len = (uint16_t)(2 + body_len); |
||||
int rc = etcp_route_send(dm->inst, group_id, dst, entry, 1, 0); |
||||
if (rc != 0) { queue_dgram_free(entry); queue_entry_free(entry); return -1; } |
||||
return 0; |
||||
} |
||||
|
||||
/* ── работа с беседой ── */ |
||||
|
||||
/* Загрузить беседу из БД в структуру. Возвращает 0/ошибку. */ |
||||
struct dm_conv { |
||||
char conv_id[64]; |
||||
uint64_t peer_node_id; |
||||
uint8_t peer_x25519[32]; |
||||
uint8_t peer_ed25519[32]; |
||||
char peer_name[64]; |
||||
uint64_t group_id; |
||||
uint8_t content_key[DM_CONTENT_KEY_SIZE]; |
||||
uint64_t last_out_seq; |
||||
uint64_t last_in_seq; |
||||
}; |
||||
|
||||
static int dm_conv_load(struct dm_state* dm, const char* conv_id, struct dm_conv* c) { |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(dm->db, |
||||
"SELECT peer_node_id, peer_x25519, peer_ed25519, COALESCE(peer_name,'')," |
||||
" COALESCE(group_id,0), last_out_seq, last_in_seq FROM dm_conversations WHERE conv_id=?", |
||||
-1, &st, NULL) != SQLITE_OK) return -1; |
||||
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC); |
||||
int rc = -1; |
||||
if (sqlite3_step(st) == SQLITE_ROW) { |
||||
memset(c, 0, sizeof(*c)); |
||||
snprintf(c->conv_id, sizeof(c->conv_id), "%s", conv_id); |
||||
c->peer_node_id = (uint64_t)sqlite3_column_int64(st, 0); |
||||
const uint8_t* x = (const uint8_t*)sqlite3_column_blob(st, 1); |
||||
const uint8_t* e = (const uint8_t*)sqlite3_column_blob(st, 2); |
||||
if (x && sqlite3_column_bytes(st, 1) >= 32) memcpy(c->peer_x25519, x, 32); |
||||
if (e && sqlite3_column_bytes(st, 2) >= 32) memcpy(c->peer_ed25519, e, 32); |
||||
const char* name = (const char*)sqlite3_column_text(st, 3); |
||||
if (name) snprintf(c->peer_name, sizeof(c->peer_name), "%s", name); |
||||
c->group_id = (uint64_t)sqlite3_column_int64(st, 4); |
||||
c->last_out_seq = (uint64_t)sqlite3_column_int64(st, 5); |
||||
c->last_in_seq = (uint64_t)sqlite3_column_int64(st, 6); |
||||
dm_derive_content_key(dm->inst->my_keys.private_key, c->peer_x25519, c->content_key); |
||||
rc = 0; |
||||
} |
||||
sqlite3_finalize(st); |
||||
return rc; |
||||
} |
||||
|
||||
/* Вставить/обновить беседу в БД. Возвращает 0/ошибку. */ |
||||
static int dm_conv_save(struct dm_state* dm, const struct dm_conv* c) { |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(dm->db, |
||||
"INSERT INTO dm_conversations(conv_id,peer_node_id,peer_x25519,peer_ed25519," |
||||
" peer_name,group_id,last_out_seq,last_in_seq,created_at,last_ts)" |
||||
" VALUES(?,?,?,?,?,?,?,?,?,0)" |
||||
" ON CONFLICT(conv_id) DO UPDATE SET" |
||||
" peer_x25519=excluded.peer_x25519, peer_ed25519=excluded.peer_ed25519," |
||||
" peer_name=excluded.peer_name, group_id=excluded.group_id," |
||||
" last_out_seq=excluded.last_out_seq, last_in_seq=excluded.last_in_seq", |
||||
-1, &st, NULL) != SQLITE_OK) return -1; |
||||
sqlite3_bind_text(st, 1, c->conv_id, -1, SQLITE_STATIC); |
||||
sqlite3_bind_int64(st, 2, (sqlite3_int64)c->peer_node_id); |
||||
sqlite3_bind_blob(st, 3, c->peer_x25519, 32, SQLITE_STATIC); |
||||
sqlite3_bind_blob(st, 4, c->peer_ed25519, 32, SQLITE_STATIC); |
||||
sqlite3_bind_text(st, 5, c->peer_name, -1, SQLITE_STATIC); |
||||
sqlite3_bind_int64(st, 6, (sqlite3_int64)c->group_id); |
||||
sqlite3_bind_int64(st, 7, (sqlite3_int64)c->last_out_seq); |
||||
sqlite3_bind_int64(st, 8, (sqlite3_int64)c->last_in_seq); |
||||
sqlite3_bind_int64(st, 9, (sqlite3_int64)ntp_time_get_seconds(dm->inst)); |
||||
int rc = sqlite3_step(st) == SQLITE_DONE ? 0 : -1; |
||||
sqlite3_finalize(st); |
||||
return rc; |
||||
} |
||||
|
||||
/* ── сборка/разбор канонического тела ── */ |
||||
|
||||
/* Собрать каноническое тело сообщения (зашифровать + подписать).
|
||||
* out — буфер (u_malloc), *out_len — длина. dir=1 для исходящего. */ |
||||
static int dm_build_body(struct dm_state* dm, const struct dm_conv* c, uint64_t seq, uint64_t ts, |
||||
const char* ct, const uint8_t* data, uint32_t data_len, |
||||
uint8_t** out, size_t* out_len) { |
||||
if (!c || !ct || (!data && data_len) || data_len > DM_DATA_MAX) return -1; |
||||
|
||||
uint8_t ct_len = (uint8_t)strnlen(ct, 255); |
||||
uint64_t conv_num = (uint64_t)strtoull(c->conv_id, NULL, 10); |
||||
uint8_t nonce[DM_NONCE_SIZE]; |
||||
dm_build_nonce(conv_num, dm->inst->node_id, seq, nonce); |
||||
|
||||
/* шифротекст = data_len + tag(16) */ |
||||
uint8_t* enc = u_malloc(data_len + DM_TAG_SIZE); |
||||
if (!enc) return -1; |
||||
size_t enc_len = 0; |
||||
if (dm_encrypt(c->content_key, nonce, data, data_len, enc, &enc_len) != 0) { |
||||
u_free(enc); |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: encrypt failed conv=%s", DM_ID, c->conv_id); |
||||
return -1; |
||||
} |
||||
|
||||
uint64_t author = dm->inst->node_id; |
||||
size_t body_len = DM_MSG_FIXED_HDR + ct_len + 2 + enc_len + DM_MSG_SIG_SIZE; |
||||
uint8_t* body = u_malloc(body_len); |
||||
if (!body) { u_free(enc); return -1; } |
||||
size_t off = 0; |
||||
memcpy(body + off, &conv_num, 8); off += 8; |
||||
memcpy(body + off, &seq, 8); off += 8; |
||||
memcpy(body + off, &ts, 8); off += 8; |
||||
memcpy(body + off, &author, 8); off += 8; |
||||
body[off++] = ct_len; |
||||
memcpy(body + off, ct, ct_len); off += ct_len; |
||||
uint16_t el = (uint16_t)enc_len; |
||||
memcpy(body + off, &el, 2); off += 2; |
||||
memcpy(body + off, enc, enc_len); off += enc_len; |
||||
/* подпись по всем полям до sig */ |
||||
sc_ed25519_sign(dm->inst->my_ed25519_privkey, body, off, body + off); |
||||
u_free(enc); |
||||
|
||||
*out = body; |
||||
*out_len = body_len; |
||||
return 0; |
||||
} |
||||
|
||||
/* ── обработка входящего сообщения ── */ |
||||
|
||||
/* Обработать каноническое тело входящего сообщения (из live-доставки или mailbox):
|
||||
* проверить подпись, расшифровать, dedup по seq, вставить, отослать ACK, событие GUI. */ |
||||
static void dm_on_msg(struct dm_state* dm, const uint8_t* msg, size_t mlen) { |
||||
if (!msg || mlen < DM_MSG_FIXED_HDR + DM_MSG_SIG_SIZE) return; |
||||
uint64_t conv_num; memcpy(&conv_num, msg, 8); |
||||
uint64_t seq; memcpy(&seq, msg + 8, 8); |
||||
uint64_t ts; memcpy(&ts, msg + 16, 8); |
||||
uint64_t author; memcpy(&author, msg + 24, 8); |
||||
uint8_t ct_len = msg[32]; |
||||
const uint8_t* ct = msg + 33; |
||||
uint16_t enc_len; memcpy(&enc_len, msg + 33 + ct_len, 2); |
||||
const uint8_t* enc = msg + 35 + ct_len; |
||||
const uint8_t* sig = msg + mlen - DM_MSG_SIG_SIZE; |
||||
|
||||
if (author == dm->inst->node_id) return; /* своё (эхо) — игнор */ |
||||
|
||||
char conv_id[64]; |
||||
snprintf(conv_id, sizeof(conv_id), "%llu", (unsigned long long)conv_num); |
||||
|
||||
struct dm_conv c; |
||||
if (dm_conv_load(dm, conv_id, &c) != 0) { |
||||
/* первое сообщение: автоприём при общей группе + совпадение conv_id */ |
||||
if (dm_derive_conv_id(dm->inst->node_id, author) != conv_num) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: conv_id mismatch from 0x%016llx", DM_ID, (unsigned long long)author); |
||||
return; |
||||
} |
||||
if (!dm_share_group(dm, author)) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: no shared group with 0x%016llx — reject DM", DM_ID, (unsigned long long)author); |
||||
return; |
||||
} |
||||
memset(&c, 0, sizeof(c)); |
||||
snprintf(c.conv_id, sizeof(c.conv_id), "%s", conv_id); |
||||
c.peer_node_id = author; |
||||
if (dm_node_pubkeys(dm, author, c.peer_x25519, c.peer_ed25519) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no pubkeys for 0x%016llx", DM_ID, (unsigned long long)author); |
||||
return; |
||||
} |
||||
dm_derive_content_key(dm->inst->my_keys.private_key, c.peer_x25519, c.content_key); |
||||
c.group_id = 0; |
||||
dm_conv_save(dm, &c); |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: auto-created conversation conv=%s peer=0x%016llx", |
||||
DM_ID, conv_id, (unsigned long long)author); |
||||
} |
||||
|
||||
/* проверка подписи автора по всем полям до sig */ |
||||
if (sc_ed25519_verify(c.peer_ed25519, msg, mlen - DM_MSG_SIG_SIZE, sig) != SC_OK) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: invalid signature conv=%s seq=%llu", DM_ID, conv_id, (unsigned long long)seq); |
||||
return; |
||||
} |
||||
|
||||
/* dedup: вставляем только новое */ |
||||
if (seq <= c.last_in_seq) { |
||||
/* уже есть — только подтвердить */ |
||||
uint8_t ack[16]; memcpy(ack, &conv_num, 8); memcpy(ack + 8, &seq, 8); |
||||
dm_route_send(dm, c.group_id, author, DM_SUBCMD_ACK, ack, sizeof(ack)); |
||||
return; |
||||
} |
||||
|
||||
/* расшифровать (nonce детерминирован по conv_id+author+seq) */ |
||||
uint8_t nonce[DM_NONCE_SIZE]; |
||||
dm_build_nonce(conv_num, author, seq, nonce); |
||||
uint8_t* plain = u_malloc(enc_len > DM_TAG_SIZE ? enc_len - DM_TAG_SIZE : 0); |
||||
if (!plain) return; |
||||
size_t plain_len = 0; |
||||
if (dm_decrypt(c.content_key, nonce, enc, enc_len, plain, &plain_len) != 0) { |
||||
u_free(plain); |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: decrypt failed conv=%s seq=%llu", DM_ID, conv_id, (unsigned long long)seq); |
||||
return; |
||||
} |
||||
|
||||
/* вставить в dm_messages (dir=0, входящее) */ |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(dm->db, |
||||
"INSERT OR IGNORE INTO dm_messages(conv_id,dir,seq,ts,author,ct,data,sig)" |
||||
" VALUES(?,0,?,?,?,?,?,?)", |
||||
-1, &st, NULL) == SQLITE_OK) { |
||||
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC); |
||||
sqlite3_bind_int64(st, 2, (sqlite3_int64)seq); |
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)ts); |
||||
sqlite3_bind_int64(st, 4, (sqlite3_int64)author); |
||||
sqlite3_bind_text(st, 5, (const char*)ct, ct_len, SQLITE_STATIC); |
||||
sqlite3_bind_blob(st, 6, enc, enc_len, SQLITE_STATIC); |
||||
sqlite3_bind_blob(st, 7, sig, DM_MSG_SIG_SIZE, SQLITE_STATIC); |
||||
sqlite3_step(st); |
||||
} |
||||
if (st) sqlite3_finalize(st); |
||||
|
||||
c.last_in_seq = seq; |
||||
dm_conv_save(dm, &c); |
||||
|
||||
/* подтвердить приём (нужно и для mailbox-доставки → storage удалит по ACK) */ |
||||
uint8_t ack[16]; memcpy(ack, &conv_num, 8); memcpy(ack + 8, &seq, 8); |
||||
dm_route_send(dm, c.group_id, author, DM_SUBCMD_ACK, ack, sizeof(ack)); |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: recv conv=%s seq=%llu author=0x%016llx ct=%.*s len=%zu", |
||||
DM_ID, conv_id, (unsigned long long)seq, (unsigned long long)author, ct_len, ct, plain_len); |
||||
|
||||
/* событие GUI: [conv_id_len:1][conv_id][author_node_id:8] */ |
||||
{ |
||||
size_t cl = strlen(conv_id); |
||||
uint8_t evt[1 + 64 + 8]; |
||||
evt[0] = (uint8_t)cl; |
||||
memcpy(evt + 1, conv_id, cl); |
||||
memcpy(evt + 1 + cl, &author, 8); |
||||
chat_event_post(dm->inst, CHAT_EVT_DM_MSG_RECEIVED, evt, 1 + (int)cl + 8); |
||||
} |
||||
u_free(plain); |
||||
} |
||||
|
||||
/* ── обработка HELLO (catch-up) ── */ |
||||
|
||||
/* Пересобрать каноническое тело из сохранённых полей (для catch-up resend). */ |
||||
static int dm_rebuild_body(uint64_t conv_num, uint64_t seq, uint64_t ts, uint64_t author, |
||||
const char* ct, const uint8_t* enc, int enc_len, const uint8_t sig[64], |
||||
uint8_t** out, size_t* out_len) { |
||||
uint8_t ctl = (uint8_t)strnlen(ct, 255); |
||||
size_t body_len = DM_MSG_FIXED_HDR + ctl + 2 + (size_t)enc_len + DM_MSG_SIG_SIZE; |
||||
uint8_t* body = u_malloc(body_len); |
||||
if (!body) return -1; |
||||
size_t off = 0; |
||||
memcpy(body + off, &conv_num, 8); off += 8; |
||||
memcpy(body + off, &seq, 8); off += 8; |
||||
memcpy(body + off, &ts, 8); off += 8; |
||||
memcpy(body + off, &author, 8); off += 8; |
||||
body[off++] = ctl; |
||||
memcpy(body + off, ct, ctl); off += ctl; |
||||
uint16_t el = (uint16_t)enc_len; |
||||
memcpy(body + off, &el, 2); off += 2; |
||||
memcpy(body + off, enc, enc_len); off += enc_len; |
||||
memcpy(body + off, sig, DM_MSG_SIG_SIZE); |
||||
*out = body; |
||||
*out_len = body_len; |
||||
return 0; |
||||
} |
||||
|
||||
/* На connect обе стороны шлют HELLO со своими счётчиками; получив HELLO, досылаем
|
||||
* пиру недостающие ему наши сообщения (его last_in_seq+1 .. наш last_out_seq). */ |
||||
static void dm_on_hello(struct dm_state* dm, uint64_t conv_num, uint64_t from_peer, uint64_t peer_out_seq, uint64_t peer_in_seq) { |
||||
char conv_id[64]; |
||||
snprintf(conv_id, sizeof(conv_id), "%llu", (unsigned long long)conv_num); |
||||
|
||||
struct dm_conv c; |
||||
if (dm_conv_load(dm, conv_id, &c) != 0) return; |
||||
|
||||
if (peer_in_seq < c.last_out_seq) { |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(dm->db, |
||||
"SELECT seq, ts, author, ct, data, sig FROM dm_messages" |
||||
" WHERE conv_id=? AND dir=1 AND seq>? ORDER BY seq", |
||||
-1, &st, NULL) == SQLITE_OK) { |
||||
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC); |
||||
sqlite3_bind_int64(st, 2, (sqlite3_int64)peer_in_seq); |
||||
while (sqlite3_step(st) == SQLITE_ROW) { |
||||
uint64_t seq = (uint64_t)sqlite3_column_int64(st, 0); |
||||
uint64_t ts = (uint64_t)sqlite3_column_int64(st, 1); |
||||
uint64_t author = (uint64_t)sqlite3_column_int64(st, 2); |
||||
const char* ct = (const char*)sqlite3_column_text(st, 3); |
||||
const uint8_t* enc = (const uint8_t*)sqlite3_column_blob(st, 4); |
||||
int enc_len = sqlite3_column_bytes(st, 4); |
||||
const uint8_t* sig = (const uint8_t*)sqlite3_column_blob(st, 5); |
||||
uint8_t* body = NULL; size_t body_len = 0; |
||||
if (dm_rebuild_body(conv_num, seq, ts, author, ct, enc, enc_len, sig, &body, &body_len) == 0) { |
||||
dm_route_send(dm, c.group_id, from_peer, DM_SUBCMD_MSG, body, body_len); |
||||
u_free(body); |
||||
} |
||||
} |
||||
} |
||||
if (st) sqlite3_finalize(st); |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: hello from 0x%016llx conv=%s out=%llu in=%llu", |
||||
DM_ID, (unsigned long long)from_peer, conv_id, |
||||
(unsigned long long)peer_out_seq, (unsigned long long)peer_in_seq); |
||||
} |
||||
|
||||
/* ── обработка ACK ── */ |
||||
|
||||
/* Получено подтверждение доставки от пира — логируем (надёжность гарантирует etcp_router). */ |
||||
static void dm_on_ack(uint64_t conv_num, uint64_t seq) { |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: ack conv=%llu seq=%llu", DM_ID, |
||||
(unsigned long long)conv_num, (unsigned long long)seq); |
||||
} |
||||
|
||||
/* ── etcp_router recv (0x34) ── */ |
||||
|
||||
static void dm_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { |
||||
if (!entry || !conn || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 1) { |
||||
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } |
||||
return; |
||||
} |
||||
struct dm_state* dm = dm_of(conn->instance); |
||||
if (!dm || !dm->initialized) { queue_dgram_free(entry); queue_entry_free(entry); return; } |
||||
|
||||
const uint8_t* d = entry->dgram; |
||||
uint64_t from_node; memcpy(&from_node, d + ROUTER_SVC_SRC_OFF, 8); |
||||
const uint8_t* p = d + ROUTER_SVC_PAYLOAD_OFF; |
||||
size_t plen = entry->len - ROUTER_SVC_PAYLOAD_OFF; |
||||
uint8_t subcmd = p[0]; |
||||
|
||||
if (subcmd == DM_SUBCMD_MSG) { |
||||
size_t mlen = 0; |
||||
if (dm_msg_len(p + 1, plen - 1, &mlen)) |
||||
dm_on_msg(dm, p + 1, mlen); |
||||
} else if (subcmd == DM_SUBCMD_ACK) { |
||||
if (plen >= 1 + 16) { |
||||
uint64_t conv_num; memcpy(&conv_num, p + 1, 8); |
||||
uint64_t seq; memcpy(&seq, p + 9, 8); |
||||
dm_on_ack(conv_num, seq); |
||||
} |
||||
} else if (subcmd == DM_SUBCMD_HELLO) { |
||||
if (plen >= 1 + 24) { |
||||
uint64_t conv_num; memcpy(&conv_num, p + 1, 8); |
||||
uint64_t out_seq; memcpy(&out_seq, p + 9, 8); |
||||
uint64_t in_seq; memcpy(&in_seq, p + 17, 8); |
||||
dm_on_hello(dm, conv_num, from_node, out_seq, in_seq); |
||||
} |
||||
} else { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: unknown subcmd=%02x", DM_ID, subcmd); |
||||
} |
||||
queue_dgram_free(entry); queue_entry_free(entry); |
||||
} |
||||
|
||||
/* ── connection status ── */ |
||||
|
||||
/* При поднятии соединения с пиром: HELLO (catch-up) + вытянуть mailbox. */ |
||||
static void dm_on_conn_status(struct ETCP_CONN* conn, int status, void* arg) { |
||||
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg; |
||||
struct dm_state* dm = dm_of(inst); |
||||
if (!conn || status != ETCP_CONN_STATUS_UP || !dm) return; |
||||
uint64_t peer = conn->peer_node_id; |
||||
if (!peer) return; |
||||
|
||||
/* На подъём ЛЮБОГО соединения: HELLO для беседы с подключившимся пиром +
|
||||
* PULL mailbox для бесед, чей пир не подключён напрямую (offline-доставка). */ |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(dm->db, |
||||
"SELECT conv_id, peer_node_id, last_out_seq, last_in_seq FROM dm_conversations", |
||||
-1, &st, NULL) != SQLITE_OK) return; |
||||
while (sqlite3_step(st) == SQLITE_ROW) { |
||||
const char* conv_id = (const char*)sqlite3_column_text(st, 0); |
||||
uint64_t conv_peer = (uint64_t)sqlite3_column_int64(st, 1); |
||||
uint64_t out_seq = (uint64_t)sqlite3_column_int64(st, 2); |
||||
uint64_t in_seq = (uint64_t)sqlite3_column_int64(st, 3); |
||||
uint64_t conv_num = strtoull(conv_id, NULL, 10); |
||||
|
||||
if (conv_peer == peer) { |
||||
uint8_t hello[24]; |
||||
memcpy(hello, &conv_num, 8); |
||||
memcpy(hello + 8, &out_seq, 8); |
||||
memcpy(hello + 16, &in_seq, 8); |
||||
/* group_id берём из строки беседы — для простоты HELLO по 0, маршрутизация глобальная */ |
||||
dm_route_send(dm, 0, peer, DM_SUBCMD_HELLO, hello, sizeof(hello)); |
||||
} |
||||
|
||||
/* пир не подключён напрямую — тянем mailbox со storage-узлов */ |
||||
struct ETCP_CONN* pc = instance_find_conn(inst, conv_peer); |
||||
if (!pc || !pc->links_up || !pc->initialized) { |
||||
dm_mailbox_pull(inst, conv_peer, in_seq); |
||||
} |
||||
} |
||||
sqlite3_finalize(st); |
||||
} |
||||
|
||||
/* ── публичный API ── */ |
||||
|
||||
/* Начать беседу с пользователем. Создаёт запись если её нет, выводит ключи,
|
||||
* обеспечивает прямое соединение (если есть общая группа). */ |
||||
int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, |
||||
const uint8_t peer_x25519[32], const uint8_t peer_ed25519[32], |
||||
const char* peer_name, const char* source_ch_id) { |
||||
if (!inst || !peer_node_id || !peer_x25519 || !peer_ed25519) return -1; |
||||
if (peer_node_id == inst->node_id) return -1; |
||||
struct dm_state* dm = dm_of(inst); |
||||
if (!dm || !dm->initialized) return -1; |
||||
|
||||
uint64_t conv_num = dm_derive_conv_id(inst->node_id, peer_node_id); |
||||
char conv_id[64]; |
||||
snprintf(conv_id, sizeof(conv_id), "%llu", (unsigned long long)conv_num); |
||||
|
||||
struct dm_conv c; |
||||
if (dm_conv_load(dm, conv_id, &c) == 0) { |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: conversation already exists conv=%s", DM_ID, conv_id); |
||||
return 0; |
||||
} |
||||
|
||||
memset(&c, 0, sizeof(c)); |
||||
snprintf(c.conv_id, sizeof(c.conv_id), "%s", conv_id); |
||||
c.peer_node_id = peer_node_id; |
||||
memcpy(c.peer_x25519, peer_x25519, 32); |
||||
memcpy(c.peer_ed25519, peer_ed25519, 32); |
||||
if (peer_name) snprintf(c.peer_name, sizeof(c.peer_name), "%s", peer_name); |
||||
c.group_id = source_ch_id ? strtoull(source_ch_id, NULL, 10) : 0; |
||||
dm_derive_content_key(inst->my_keys.private_key, c.peer_x25519, c.content_key); |
||||
|
||||
if (dm_conv_save(dm, &c) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: conv_save failed conv=%s", DM_ID, conv_id); |
||||
return -1; |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: started conv=%s peer=0x%016llx name=%s group=%llu", |
||||
DM_ID, conv_id, (unsigned long long)peer_node_id, c.peer_name, |
||||
(unsigned long long)c.group_id); |
||||
|
||||
/* событие GUI */ |
||||
{ |
||||
size_t cl = strlen(conv_id); |
||||
uint8_t evt[1 + 64]; |
||||
evt[0] = (uint8_t)cl; |
||||
memcpy(evt + 1, conv_id, cl); |
||||
chat_event_post(inst, CHAT_EVT_DM_CONV_UPDATED, evt, 1 + (int)cl); |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
/* Отправить сообщение в беседу: зашифровать, подписать, сохранить, доставить. */ |
||||
int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type, |
||||
const uint8_t* data, uint32_t data_len) { |
||||
if (!inst || !conv_id || !content_type || (!data && data_len)) return -1; |
||||
struct dm_state* dm = dm_of(inst); |
||||
if (!dm || !dm->initialized) return -1; |
||||
|
||||
struct dm_conv c; |
||||
if (dm_conv_load(dm, conv_id, &c) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no conversation %s", DM_ID, conv_id); |
||||
return -1; |
||||
} |
||||
|
||||
uint64_t seq = c.last_out_seq + 1; |
||||
uint64_t ts = (uint64_t)(ntp_time_get_us(inst) / 1000); |
||||
|
||||
uint8_t* body = NULL; |
||||
size_t body_len = 0; |
||||
if (dm_build_body(dm, &c, seq, ts, content_type, data, data_len, &body, &body_len) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: build body failed", DM_ID); |
||||
return -1; |
||||
} |
||||
|
||||
/* сохранить локально (dir=1, исходящее) */ |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(dm->db, |
||||
"INSERT INTO dm_messages(conv_id,dir,seq,ts,author,ct,data,sig)" |
||||
" VALUES(?,1,?,?,?,?,?,?)", |
||||
-1, &st, NULL) == SQLITE_OK) { |
||||
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC); |
||||
sqlite3_bind_int64(st, 2, (sqlite3_int64)seq); |
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)ts); |
||||
sqlite3_bind_int64(st, 4, (sqlite3_int64)inst->node_id); |
||||
sqlite3_bind_text(st, 5, content_type, -1, SQLITE_STATIC); |
||||
/* data/sig уже внутри body: data_enc по смещению 35+ct_len, sig в конце */ |
||||
uint8_t ctl = (uint8_t)strnlen(content_type, 255); |
||||
const uint8_t* enc = body + 35 + ctl; |
||||
uint16_t enc_len; memcpy(&enc_len, body + 33 + ctl, 2); |
||||
sqlite3_bind_blob(st, 6, enc, enc_len, SQLITE_STATIC); |
||||
sqlite3_bind_blob(st, 7, body + body_len - DM_MSG_SIG_SIZE, DM_MSG_SIG_SIZE, SQLITE_STATIC); |
||||
sqlite3_step(st); |
||||
} |
||||
if (st) sqlite3_finalize(st); |
||||
|
||||
c.last_out_seq = seq; |
||||
dm_conv_save(dm, &c); |
||||
|
||||
/* доставка: доступен → прямой send, иначе → mailbox */ |
||||
if (dm_peer_reachable(dm, c.peer_node_id)) { |
||||
dm_route_send(dm, c.group_id, c.peer_node_id, DM_SUBCMD_MSG, body, body_len); |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: sent direct conv=%s seq=%llu → 0x%016llx", |
||||
DM_ID, conv_id, (unsigned long long)seq, (unsigned long long)c.peer_node_id); |
||||
} else { |
||||
dm_mailbox_put(inst, c.peer_node_id, inst->node_id, body, body_len); |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: peer offline conv=%s seq=%llu → mailbox", |
||||
DM_ID, conv_id, (unsigned long long)seq); |
||||
} |
||||
u_free(body); |
||||
return 0; |
||||
} |
||||
|
||||
/* Вставка сообщения, полученного через mailbox (вызывается dm_mailbox по PULL_RESP).
|
||||
* Обрабатывается тем же путём, что и live-сообщение (verify/decrypt/dedup/insert). */ |
||||
void dm_core_inject_message(const uint8_t* msg, size_t len, void* arg) { |
||||
struct UTUN_INSTANCE* inst = (struct UTUN_INSTANCE*)arg; |
||||
struct dm_state* dm = dm_of(inst); |
||||
if (!dm) return; |
||||
dm_on_msg(dm, msg, len); |
||||
} |
||||
|
||||
/* ── жизненный цикл ── */ |
||||
|
||||
int dm_core_init(struct UTUN_INSTANCE* inst) { |
||||
if (!inst) return -1; |
||||
if (inst->dm && inst->dm->initialized) return 0; |
||||
struct dm_state* dm = u_calloc(1, sizeof(*dm)); |
||||
if (!dm) return -1; |
||||
inst->dm = dm; |
||||
dm->inst = inst; |
||||
dm->db = chat_core_get_db(inst); |
||||
if (!dm->db) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no chat db", DM_ID); u_free(dm); inst->dm = NULL; return -1; } |
||||
dm_create_tables(dm); |
||||
if (etcp_router_bind(inst, ETCP_RT_ID_DM, dm_recv_cb) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: etcp_router_bind failed", DM_ID); |
||||
u_free(dm); inst->dm = NULL; return -1; |
||||
} |
||||
etcp_add_conn_status_cbk(inst, dm_on_conn_status, inst); |
||||
dm_mailbox_set_deliver_cb(inst, dm_core_inject_message, inst); |
||||
dm->initialized = 1; |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: initialized", DM_ID); |
||||
return 0; |
||||
} |
||||
|
||||
void dm_core_destroy(struct UTUN_INSTANCE* inst) { |
||||
if (!inst || !inst->dm || !inst->dm->initialized) return; |
||||
etcp_remove_conn_status_cbk(inst, dm_on_conn_status, inst); |
||||
etcp_router_unbind(inst, ETCP_RT_ID_DM); |
||||
u_free(inst->dm); |
||||
inst->dm = NULL; |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: destroyed", DM_ID); |
||||
} |
||||
|
||||
/* ── JSON для GUI/headless ── */ |
||||
|
||||
/* Список бесед в JSON. */ |
||||
int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len) { |
||||
struct dm_state* dm = dm_of(inst); |
||||
if (!buf || !out_len || !dm || !dm->db) return -1; |
||||
int off = 0; |
||||
off += snprintf(buf + off, buf_size - (size_t)off, "["); |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(dm->db, |
||||
"SELECT conv_id, peer_node_id, COALESCE(peer_name,''), last_ts, last_out_seq, last_in_seq" |
||||
" FROM dm_conversations ORDER BY last_ts DESC", |
||||
-1, &st, NULL) == SQLITE_OK) { |
||||
int first = 1; |
||||
while (sqlite3_step(st) == SQLITE_ROW) { |
||||
const char* conv_id = (const char*)sqlite3_column_text(st, 0); |
||||
uint64_t peer = (uint64_t)sqlite3_column_int64(st, 1); |
||||
const char* name = (const char*)sqlite3_column_text(st, 2); |
||||
uint64_t last_ts = (uint64_t)sqlite3_column_int64(st, 3); |
||||
uint64_t out_seq = (uint64_t)sqlite3_column_int64(st, 4); |
||||
uint64_t in_seq = (uint64_t)sqlite3_column_int64(st, 5); |
||||
off += snprintf(buf + off, buf_size - (size_t)off, |
||||
"%s{\"conv_id\":\"%s\",\"peer_node_id\":%llu,\"peer_name\":\"%s\"," |
||||
"\"last_ts\":%llu,\"out_seq\":%llu,\"in_seq\":%llu}", |
||||
first ? "" : ",", conv_id, (unsigned long long)peer, |
||||
name ? name : "", (unsigned long long)last_ts, |
||||
(unsigned long long)out_seq, (unsigned long long)in_seq); |
||||
first = 0; |
||||
} |
||||
} |
||||
if (st) sqlite3_finalize(st); |
||||
off += snprintf(buf + off, buf_size - (size_t)off, "]"); |
||||
*out_len = (size_t)off; |
||||
return 0; |
||||
} |
||||
|
||||
/* Сообщения беседы (расшифрованные) в JSON. */ |
||||
int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset, |
||||
char* buf, size_t buf_size, size_t* out_len) { |
||||
struct dm_state* dm = dm_of(inst); |
||||
if (!conv_id || !buf || !out_len || !dm || !dm->db) return -1; |
||||
|
||||
struct dm_conv c; |
||||
if (dm_conv_load(dm, conv_id, &c) != 0) return -1; |
||||
|
||||
char sql[256]; |
||||
if (count > 0) |
||||
snprintf(sql, sizeof(sql), |
||||
"SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY seq LIMIT %d OFFSET %d", |
||||
count, offset); |
||||
else |
||||
snprintf(sql, sizeof(sql), |
||||
"SELECT dir,seq,ts,author,ct,data FROM dm_messages WHERE conv_id=? ORDER BY seq"); |
||||
|
||||
int off = 0; |
||||
off += snprintf(buf + off, buf_size - (size_t)off, "["); |
||||
uint64_t conv_num = (uint64_t)strtoull(conv_id, NULL, 10); |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(dm->db, sql, -1, &st, NULL) == SQLITE_OK) { |
||||
sqlite3_bind_text(st, 1, conv_id, -1, SQLITE_STATIC); |
||||
int first = 1; |
||||
while (sqlite3_step(st) == SQLITE_ROW) { |
||||
int dir = sqlite3_column_int(st, 0); |
||||
uint64_t seq = (uint64_t)sqlite3_column_int64(st, 1); |
||||
uint64_t ts = (uint64_t)sqlite3_column_int64(st, 2); |
||||
uint64_t author = (uint64_t)sqlite3_column_int64(st, 3); |
||||
const char* ct = (const char*)sqlite3_column_text(st, 4); |
||||
const uint8_t* enc = (const uint8_t*)sqlite3_column_blob(st, 5); |
||||
int enc_len = sqlite3_column_bytes(st, 5); |
||||
/* расшифровать (nonce детерминирован по conv_id+author+seq) */ |
||||
char text[DM_DATA_MAX + 1]; |
||||
text[0] = '\0'; |
||||
if (enc && enc_len >= (int)DM_TAG_SIZE) { |
||||
uint8_t nonce[DM_NONCE_SIZE]; |
||||
dm_build_nonce(conv_num, author, seq, nonce); |
||||
uint8_t* plain = u_malloc((size_t)enc_len); |
||||
size_t plen = 0; |
||||
if (dm_decrypt(c.content_key, nonce, enc, (size_t)enc_len, plain, &plen) == 0) { |
||||
if (plen >= sizeof(text)) plen = sizeof(text) - 1; |
||||
memcpy(text, plain, plen); |
||||
text[plen] = '\0'; |
||||
} |
||||
u_free(plain); |
||||
} |
||||
off += snprintf(buf + off, buf_size - (size_t)off, |
||||
"%s{\"dir\":%d,\"seq\":%llu,\"ts\":%llu,\"author\":%llu,\"ct\":\"%s\",\"data\":\"%s\"}", |
||||
first ? "" : ",", dir, (unsigned long long)seq, (unsigned long long)ts, |
||||
(unsigned long long)author, ct ? ct : "", text); |
||||
first = 0; |
||||
} |
||||
} |
||||
if (st) sqlite3_finalize(st); |
||||
off += snprintf(buf + off, buf_size - (size_t)off, "]"); |
||||
*out_len = (size_t)off; |
||||
return 0; |
||||
} |
||||
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* dm_core.h — прямой p2p чат между двумя пользователями (DM) |
||||
* |
||||
* Отдельная подсистема (НЕ канал): без TOPO_GROUP/member_sync/merkle. |
||||
* - conv_id и content_key детерминированно выводятся обеими сторонами |
||||
* из своих ключей и pubkey пира (см. dm_crypto.h) — без переговоров. |
||||
* - сообщения идут через etcp_router (прямое соединение или релей), |
||||
* при недоступности пира — через dm_mailbox (storage-узлы). |
||||
* - содержимое шифруется E2E (AES-256-CCM), запись подписывается Ed25519. |
||||
* |
||||
* Два независимых направленных потока (я→пир и пир→я) с монотонным seq. |
||||
*/ |
||||
|
||||
#ifndef DM_CORE_H |
||||
#define DM_CORE_H |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
#include <string.h> |
||||
|
||||
struct UTUN_INSTANCE; |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
/* ── Каноническое тело DM-сообщения (общий wire-формат dm_core и dm_mailbox) ──
|
||||
* [conv_id:8][seq:8][ts:8][author:8][ct_len:1][ct:ct_len][data_len:2][data_enc:data_len][sig:64] |
||||
* data_enc — AES-256-CCM (шифротекст + tag), sig — Ed25519 автора по всем полям до sig. */ |
||||
#define DM_MSG_FIXED_HDR 33 /* conv_id+seq+ts+author(32) + ct_len(1) — до ct */ |
||||
#define DM_MSG_SIG_SIZE 64 |
||||
|
||||
/* Вычислить полную длину канонического тела из его заголовка.
|
||||
* Возвращает 1 и *out_len, либо 0 если буфер мал/повреждён. */ |
||||
static inline int dm_msg_len(const uint8_t* msg, size_t avail, size_t* out_len) { |
||||
if (!msg || avail < DM_MSG_FIXED_HDR) return 0; |
||||
uint8_t ct_len = msg[32]; |
||||
if (avail < (size_t)DM_MSG_FIXED_HDR + ct_len + 2) return 0; |
||||
uint16_t data_len; |
||||
memcpy(&data_len, msg + DM_MSG_FIXED_HDR + ct_len, 2); |
||||
size_t total = (size_t)DM_MSG_FIXED_HDR + ct_len + 2 + data_len + DM_MSG_SIG_SIZE; |
||||
if (avail < total) return 0; |
||||
*out_len = total; |
||||
return 1; |
||||
} |
||||
|
||||
/* Жизненный цикл: инициализация после chat_core_init, destroy перед chat_core_destroy. */ |
||||
int dm_core_init(struct UTUN_INSTANCE* inst); |
||||
void dm_core_destroy(struct UTUN_INSTANCE* inst); |
||||
|
||||
/* Начать беседу с пользователем (peer). source_ch_id — канал, где нашли target
|
||||
* (общая группа; нужен для маршрутизации и подключения). Создаёт беседу если её |
||||
* ещё нет, выводит conv_id/ключи, пытается установить прямое соединение. |
||||
* Возвращает 0 при успехе, <0 при ошибке. */ |
||||
int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, |
||||
const uint8_t peer_x25519[32], const uint8_t peer_ed25519[32], |
||||
const char* peer_name, const char* source_ch_id); |
||||
|
||||
/* Отправить сообщение в беседу. Шифрует, подписывает, сохраняет локально и
|
||||
* доставляет (прямое соединение при доступности пира, иначе mailbox). |
||||
* conv_id — строковое представление (как в dm_conversations.conv_id). */ |
||||
int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type, |
||||
const uint8_t* data, uint32_t data_len); |
||||
|
||||
/* Список бесед в JSON: [{conv_id,peer_node_id,peer_name,last_ts,out_seq,in_seq}] */ |
||||
int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len); |
||||
|
||||
/* Сообщения беседы в JSON (расшифрованные): [{dir,seq,ts,author,ct,data}]
|
||||
* count=0 → все, offset=0 → с начала. */ |
||||
int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset, |
||||
char* buf, size_t buf_size, size_t* out_len); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif /* DM_CORE_H */ |
||||
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* dm_crypto.c — криптография прямого чата (DM) |
||||
*/ |
||||
|
||||
#include "dm_crypto.h" |
||||
#include "../../lib/debug_config.h" |
||||
|
||||
#define OPENSSL_API_COMPAT 0x10100000L |
||||
#include <openssl/evp.h> |
||||
#include <openssl/sha.h> |
||||
|
||||
#include <string.h> |
||||
#include <stdio.h> |
||||
|
||||
#define DM_ID "dm_crypto" |
||||
|
||||
uint64_t dm_derive_conv_id(uint64_t a, uint64_t b) { |
||||
uint64_t lo = a < b ? a : b; |
||||
uint64_t hi = a < b ? b : a; |
||||
|
||||
SHA256_CTX ctx; |
||||
uint8_t hash[32]; |
||||
SHA256_Init(&ctx); |
||||
SHA256_Update(&ctx, "utun_dm_v1", 9); |
||||
SHA256_Update(&ctx, &lo, 8); |
||||
SHA256_Update(&ctx, &hi, 8); |
||||
SHA256_Final(hash, &ctx); |
||||
|
||||
uint64_t id; |
||||
memcpy(&id, hash, 8); |
||||
id &= 0x7FFFFFFFFFFFFFFFULL; |
||||
if (id == 0) id = 1; |
||||
return id; |
||||
} |
||||
|
||||
int dm_derive_content_key(const uint8_t my_priv[32], const uint8_t peer_pub[32], |
||||
uint8_t out[DM_CONTENT_KEY_SIZE]) { |
||||
if (!my_priv || !peer_pub || !out) return -1; |
||||
|
||||
EVP_PKEY* my_pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, my_priv, 32); |
||||
if (!my_pkey) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: EVP_PKEY_new_raw_private_key failed", DM_ID); return -1; } |
||||
EVP_PKEY* peer_pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL, peer_pub, 32); |
||||
if (!peer_pkey) { EVP_PKEY_free(my_pkey); DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: EVP_PKEY_new_raw_public_key failed", DM_ID); return -1; } |
||||
|
||||
EVP_PKEY_CTX* dctx = EVP_PKEY_CTX_new(my_pkey, NULL); |
||||
uint8_t shared[32]; |
||||
size_t shared_len = sizeof(shared); |
||||
int rc = -1; |
||||
if (!dctx) goto out; |
||||
if (EVP_PKEY_derive_init(dctx) <= 0) goto out; |
||||
if (EVP_PKEY_derive_set_peer(dctx, peer_pkey) <= 0) goto out; |
||||
if (EVP_PKEY_derive(dctx, shared, &shared_len) <= 0 || shared_len != sizeof(shared)) goto out; |
||||
|
||||
SHA256_CTX sctx; |
||||
SHA256_Init(&sctx); |
||||
SHA256_Update(&sctx, shared, sizeof(shared)); |
||||
SHA256_Update(&sctx, "utun_dm_content", 15); |
||||
SHA256_Final(out, &sctx); |
||||
rc = 0; |
||||
|
||||
out: |
||||
if (dctx) EVP_PKEY_CTX_free(dctx); |
||||
EVP_PKEY_free(my_pkey); |
||||
EVP_PKEY_free(peer_pkey); |
||||
if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: X25519 derive failed", DM_ID); |
||||
return rc; |
||||
} |
||||
|
||||
void dm_build_nonce(uint64_t conv_id, uint64_t author, uint64_t seq, uint8_t nonce[DM_NONCE_SIZE]) { |
||||
SHA256_CTX ctx; |
||||
uint8_t hash[32]; |
||||
SHA256_Init(&ctx); |
||||
SHA256_Update(&ctx, &conv_id, 8); |
||||
SHA256_Update(&ctx, &author, 8); |
||||
SHA256_Update(&ctx, &seq, 8); |
||||
SHA256_Final(hash, &ctx); |
||||
memcpy(nonce, hash, DM_NONCE_SIZE); |
||||
} |
||||
|
||||
static int dm_ccm_crypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE], |
||||
const uint8_t* in, size_t in_len, uint8_t* out, size_t* out_len, |
||||
int encrypt, const uint8_t* tag_in) { |
||||
if (!key || !nonce || !in || !out || !out_len) return -1; |
||||
|
||||
EVP_CIPHER_CTX* ectx = EVP_CIPHER_CTX_new(); |
||||
if (!ectx) return -1; |
||||
const EVP_CIPHER* cipher = EVP_aes_256_ccm(); |
||||
|
||||
int rc = -1; |
||||
if (encrypt) { |
||||
if (EVP_EncryptInit_ex(ectx, cipher, NULL, NULL, NULL) != 1 |
||||
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_IVLEN, DM_NONCE_SIZE, NULL) != 1 |
||||
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_TAG, DM_TAG_SIZE, NULL) != 1 |
||||
|| EVP_EncryptInit_ex(ectx, NULL, NULL, key, nonce) != 1) goto done; |
||||
int outl; |
||||
if (EVP_EncryptUpdate(ectx, out, &outl, in, (int)in_len) != 1 || outl != (int)in_len) goto done; |
||||
int tmpl; |
||||
if (EVP_EncryptFinal_ex(ectx, out + outl, &tmpl) != 1) goto done; |
||||
if (EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_GET_TAG, DM_TAG_SIZE, out + in_len) != 1) goto done; |
||||
*out_len = in_len + DM_TAG_SIZE; |
||||
} else { |
||||
if (in_len < DM_TAG_SIZE) goto done; |
||||
size_t body = in_len - DM_TAG_SIZE; |
||||
if (EVP_DecryptInit_ex(ectx, cipher, NULL, NULL, NULL) != 1 |
||||
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_IVLEN, DM_NONCE_SIZE, NULL) != 1 |
||||
|| EVP_CIPHER_CTX_ctrl(ectx, EVP_CTRL_AEAD_SET_TAG, DM_TAG_SIZE, (void*)(tag_in ? tag_in : in + body)) != 1 |
||||
|| EVP_DecryptInit_ex(ectx, NULL, NULL, key, nonce) != 1) goto done; |
||||
int outl; |
||||
if (EVP_DecryptUpdate(ectx, out, &outl, in, (int)body) != 1 || outl != (int)body) goto done; |
||||
int tmpl; |
||||
if (EVP_DecryptFinal_ex(ectx, out + outl, &tmpl) != 1) goto done; |
||||
*out_len = body; |
||||
} |
||||
rc = 0; |
||||
done: |
||||
EVP_CIPHER_CTX_free(ectx); |
||||
return rc; |
||||
} |
||||
|
||||
int dm_encrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE], |
||||
const uint8_t* plain, size_t plain_len, uint8_t* out, size_t* out_len) { |
||||
return dm_ccm_crypt(key, nonce, plain, plain_len, out, out_len, 1, NULL); |
||||
} |
||||
|
||||
int dm_decrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE], |
||||
const uint8_t* ct, size_t ct_len, uint8_t* out, size_t* out_len) { |
||||
return dm_ccm_crypt(key, nonce, ct, ct_len, out, out_len, 0, NULL); |
||||
} |
||||
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* dm_crypto.h — криптография прямого чата (DM) |
||||
* |
||||
* Детерминированная идентичность и E2E-шифрование: |
||||
* - conv_id / content_key выводятся обеими сторонами из своих ключей и |
||||
* pubkey пира (известен из общей группы) — без переговоров. |
||||
* - шифрование AES-256-CCM с детерминированным nonce из (dir, seq). |
||||
*/ |
||||
|
||||
#ifndef DM_CRYPTO_H |
||||
#define DM_CRYPTO_H |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
#define DM_CONTENT_KEY_SIZE 32 |
||||
#define DM_NONCE_SIZE 13 /* AES-CCM требует ровно 13 байт */ |
||||
#define DM_TAG_SIZE 16 |
||||
|
||||
/* conv_id = SHA256("utun_dm_v1" || min(a,b) || max(a,b))[..8] & 0x7FFF...,
|
||||
* 63-битное число, одинаковое на обеих сторонах (одна ветка на пару). */ |
||||
uint64_t dm_derive_conv_id(uint64_t a, uint64_t b); |
||||
|
||||
/* content_key = SHA256(X25519(my_priv, peer_pub) || "utun_dm_content").
|
||||
* Возвращает 0 при успехе, <0 при ошибке. */ |
||||
int dm_derive_content_key(const uint8_t my_priv[32], const uint8_t peer_pub[32], |
||||
uint8_t out[DM_CONTENT_KEY_SIZE]); |
||||
|
||||
/* Детерминированный nonce, уникальный на каждое сообщение:
|
||||
* SHA256(conv_id || author || seq)[..13]. Одинаков на обеих сторонах, |
||||
* не пересекается между направлениями (author различается). */ |
||||
void dm_build_nonce(uint64_t conv_id, uint64_t author, uint64_t seq, uint8_t nonce[DM_NONCE_SIZE]); |
||||
|
||||
/* AES-256-CCM. ciphertext = ciphertext(plain_len) || tag(DM_TAG_SIZE).
|
||||
* out должен вмещать plain_len + DM_TAG_SIZE. 0=ок, <0=ошибка. */ |
||||
int dm_encrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE], |
||||
const uint8_t* plain, size_t plain_len, uint8_t* out, size_t* out_len); |
||||
|
||||
/* Обратная операция. ct_len = plain_len + DM_TAG_SIZE. 0=ок, <0=ошибка. */ |
||||
int dm_decrypt(const uint8_t key[DM_CONTENT_KEY_SIZE], const uint8_t nonce[DM_NONCE_SIZE], |
||||
const uint8_t* ct, size_t ct_len, uint8_t* out, size_t* out_len); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif /* DM_CRYPTO_H */ |
||||
@ -0,0 +1,353 @@
|
||||
/*
|
||||
* dm_mailbox.c — offline-хранение DM-сообщений на storage-узлах |
||||
* |
||||
* Storage-узел хранит зашифрованные блобы в таблице dm_mail и отдаёт их |
||||
* получателю по PULL, удаляя после ACK. Отправитель кладёт блобы по PUT. |
||||
* Содержимое для storage-узла непрозрачно (E2E-шифрование + подпись автора). |
||||
*/ |
||||
|
||||
#include "dm_mailbox.h" |
||||
#include "dm_core.h" |
||||
#include "../chat/chat_core.h" |
||||
#include "../utun_instance.h" |
||||
#include "../ntp_time.h" |
||||
#include "../routing_layer/etcp_router.h" |
||||
#include "../routing_layer/topo_node_sqlite.h" |
||||
#include "../transport_layer/etcp_api.h" |
||||
#include "../transport_layer/etcp.h" |
||||
#include "../../lib/mem.h" |
||||
#include "../../lib/ll_queue.h" |
||||
#include "../../lib/debug_config.h" |
||||
#include "../../lib/json_flat.h" |
||||
#include "../../lib/platform_compat.h" |
||||
|
||||
#include <string.h> |
||||
#include <stdlib.h> |
||||
#include <sqlite3.h> |
||||
|
||||
#define MB_ID "dm_mailbox" |
||||
|
||||
/* Подкоманды протокола mailbox (первый байт payload после ROUTER_SVC-заголовка). */ |
||||
#define MB_SUBCMD_PUT 0x01 /* отправитель → storage: [recipient:8][sender:8][msg] */ |
||||
#define MB_SUBCMD_PULL 0x02 /* получатель → storage: [recipient:8][sender:8][since_seq:8] */ |
||||
#define MB_SUBCMD_PULL_RESP 0x03 /* storage → получатель: [recipient:8][count:2][msg]* */ |
||||
#define MB_SUBCMD_ACK 0x04 /* получатель → storage: [recipient:8][sender:8][up_to_seq:8] */ |
||||
|
||||
#define MB_TTL_DAYS 7 |
||||
#define MB_MAX_BATCH 32 |
||||
|
||||
/* Per-instance состояние модуля. */ |
||||
struct dm_mb_state { |
||||
struct UTUN_INSTANCE* inst; |
||||
sqlite3* db; |
||||
int initialized; |
||||
dm_mail_deliver_fn deliver_fn; |
||||
void* deliver_arg; |
||||
}; |
||||
|
||||
static struct dm_mb_state* mb_of(struct UTUN_INSTANCE* inst) { |
||||
return inst ? inst->dm_mailbox : NULL; |
||||
} |
||||
|
||||
void dm_mailbox_set_deliver_cb(struct UTUN_INSTANCE* inst, dm_mail_deliver_fn fn, void* arg) { |
||||
struct dm_mb_state* mb = mb_of(inst); |
||||
if (!mb) return; |
||||
mb->deliver_fn = fn; |
||||
mb->deliver_arg = arg; |
||||
} |
||||
|
||||
/* Выполнить SQL без результата. Возвращает 0/ошибку. */ |
||||
static int mb_exec(struct dm_mb_state* mb, const char* sql) { |
||||
char* err = NULL; |
||||
if (sqlite3_exec(mb->db, sql, NULL, NULL, &err) != SQLITE_OK) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: sql failed: %s (%s)", MB_ID, sql, err ? err : "?"); |
||||
if (err) sqlite3_free(err); |
||||
return -1; |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
/* Прочитать seq из канонического тела сообщения (смещение 8). */ |
||||
static uint64_t mb_msg_seq(const uint8_t* msg) { |
||||
uint64_t seq; memcpy(&seq, msg + 8, 8); return seq; |
||||
} |
||||
|
||||
/* Создать таблицу dm_mail (на любом узле; фактически используется storage-узлами). */ |
||||
static void mb_create_tables(struct dm_mb_state* mb) { |
||||
mb_exec(mb, "CREATE TABLE IF NOT EXISTS dm_mail (" |
||||
" recipient INTEGER NOT NULL," |
||||
" sender INTEGER NOT NULL," |
||||
" seq INTEGER NOT NULL," |
||||
" msg BLOB NOT NULL," |
||||
" ttl INTEGER NOT NULL," |
||||
" PRIMARY KEY (recipient, sender, seq))"); |
||||
} |
||||
|
||||
/* Отправить payload подкомандой subcmd в dst через etcp_router (mode=0, своё E2E). */ |
||||
static int mb_route_send(struct dm_mb_state* mb, uint64_t group_id, uint64_t dst, uint8_t subcmd, |
||||
const uint8_t* body, size_t body_len) { |
||||
struct ll_entry* entry = queue_entry_new(0); |
||||
if (!entry) return -1; |
||||
entry->dgram = u_malloc(1 + 1 + body_len); |
||||
if (!entry->dgram) { queue_entry_free(entry); return -1; } |
||||
entry->dgram[0] = ETCP_RT_ID_DM_MAILBOX; |
||||
memcpy(entry->dgram + 1, &subcmd, 1); |
||||
if (body_len) memcpy(entry->dgram + 1 + 1, body, body_len); |
||||
entry->len = (uint16_t)(1 + 1 + body_len); |
||||
int rc = etcp_route_send(mb->inst, group_id, dst, entry, 1, 0); |
||||
if (rc != 0) { queue_dgram_free(entry); queue_entry_free(entry); return -1; } |
||||
return 0; |
||||
} |
||||
|
||||
/* ── обработка входящих mailbox-пакетов (etcp_router 0x35) ── */ |
||||
|
||||
static void mb_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { |
||||
if (!entry || !conn || !entry->dgram || entry->len < ROUTER_SVC_PAYLOAD_OFF + 1) { |
||||
if (entry) { queue_dgram_free(entry); queue_entry_free(entry); } |
||||
return; |
||||
} |
||||
struct UTUN_INSTANCE* inst = conn->instance; |
||||
struct dm_mb_state* mb = mb_of(inst); |
||||
if (!inst || !mb || !mb->db) { queue_dgram_free(entry); queue_entry_free(entry); return; } |
||||
|
||||
const uint8_t* d = entry->dgram; |
||||
uint64_t group_id; memcpy(&group_id, d + ROUTER_SVC_GROUP_OFF, 8); |
||||
const uint8_t* p = d + ROUTER_SVC_PAYLOAD_OFF; |
||||
size_t plen = entry->len - ROUTER_SVC_PAYLOAD_OFF; |
||||
uint8_t subcmd = p[0]; |
||||
|
||||
if (subcmd == MB_SUBCMD_PUT) { |
||||
/* [subcmd][recipient:8][sender:8][msg] — storage: сохранить блоб */ |
||||
if (plen < 1 + 8 + 8 + 16) { queue_dgram_free(entry); queue_entry_free(entry); return; } |
||||
uint64_t recipient; memcpy(&recipient, p + 1, 8); |
||||
uint64_t sender; memcpy(&sender, p + 9, 8); |
||||
const uint8_t* msg = p + 17; |
||||
size_t mlen = plen - 17; |
||||
uint64_t seq = mb_msg_seq(msg); |
||||
int64_t ttl = (int64_t)ntp_time_get_seconds(inst) + MB_TTL_DAYS * 86400; |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(mb->db, |
||||
"INSERT OR IGNORE INTO dm_mail(recipient,sender,seq,msg,ttl) VALUES(?,?,?,?,?)", |
||||
-1, &st, NULL) == SQLITE_OK) { |
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient); |
||||
sqlite3_bind_int64(st, 2, (sqlite3_int64)sender); |
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)seq); |
||||
sqlite3_bind_blob(st, 4, msg, (int)mlen, SQLITE_STATIC); |
||||
sqlite3_bind_int64(st, 5, ttl); |
||||
int rc = sqlite3_step(st); |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: PUT recipient=0x%016llx sender=0x%016llx seq=%llu rc=%d", |
||||
MB_ID, (unsigned long long)recipient, (unsigned long long)sender, |
||||
(unsigned long long)seq, rc); |
||||
} |
||||
if (st) sqlite3_finalize(st); |
||||
} else if (subcmd == MB_SUBCMD_PULL) { |
||||
/* [subcmd][recipient:8][sender:8][since_seq:8] — storage: вернуть очередь */ |
||||
if (plen < 1 + 8 + 8 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; } |
||||
uint64_t recipient; memcpy(&recipient, p + 1, 8); |
||||
uint64_t sender; memcpy(&sender, p + 9, 8); |
||||
uint64_t since_seq; memcpy(&since_seq, p + 17, 8); |
||||
|
||||
/* прочитать блобы (до MB_MAX_BATCH) в буфер: [recipient:8][sender:8][count:2][msg]* */ |
||||
size_t buf_cap = 18 + (size_t)MB_MAX_BATCH * 4096; |
||||
uint8_t* body = u_malloc(buf_cap); |
||||
if (!body) { queue_dgram_free(entry); queue_entry_free(entry); return; } |
||||
memcpy(body, &recipient, 8); |
||||
memcpy(body + 8, &sender, 8); |
||||
uint16_t count = 0; |
||||
size_t boff = 18; |
||||
|
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(mb->db, |
||||
"SELECT msg, length(msg) FROM dm_mail WHERE recipient=? AND sender=? AND seq>? ORDER BY seq LIMIT ?", |
||||
-1, &st, NULL) == SQLITE_OK) { |
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient); |
||||
sqlite3_bind_int64(st, 2, (sqlite3_int64)sender); |
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)since_seq); |
||||
sqlite3_bind_int(st, 4, MB_MAX_BATCH); |
||||
while (sqlite3_step(st) == SQLITE_ROW) { |
||||
const uint8_t* msg = (const uint8_t*)sqlite3_column_blob(st, 0); |
||||
int mlen = sqlite3_column_int(st, 1); |
||||
if (!msg || mlen <= 0) continue; |
||||
if (boff + (size_t)mlen > buf_cap) break; |
||||
memcpy(body + boff, msg, (size_t)mlen); |
||||
boff += (size_t)mlen; |
||||
count++; |
||||
} |
||||
} |
||||
if (st) sqlite3_finalize(st); |
||||
memcpy(body + 16, &count, 2); |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: PULL recipient=0x%016llx sender=0x%016llx since=%llu → %u msgs", |
||||
MB_ID, (unsigned long long)recipient, (unsigned long long)sender, |
||||
(unsigned long long)since_seq, count); |
||||
if (count > 0) |
||||
mb_route_send(mb, group_id, recipient, MB_SUBCMD_PULL_RESP, body, boff); |
||||
u_free(body); |
||||
} else if (subcmd == MB_SUBCMD_PULL_RESP) { |
||||
/* [subcmd][recipient:8][sender:8][count:2][msg]* — получатель: передать в dm_core + ACK storage */ |
||||
if (plen < 1 + 8 + 8 + 2) { queue_dgram_free(entry); queue_entry_free(entry); return; } |
||||
uint64_t storage; memcpy(&storage, d + ROUTER_SVC_SRC_OFF, 8); |
||||
uint64_t recipient; memcpy(&recipient, p + 1, 8); |
||||
uint64_t sender; memcpy(&sender, p + 9, 8); |
||||
uint16_t count; memcpy(&count, p + 17, 2); |
||||
const uint8_t* m = p + 19; |
||||
size_t remaining = plen - 19; |
||||
uint64_t max_seq = 0; |
||||
for (int i = 0; i < count; i++) { |
||||
size_t mlen = 0; |
||||
if (!dm_msg_len(m, remaining, &mlen)) break; |
||||
uint64_t seq = mb_msg_seq(m); |
||||
if (seq > max_seq) max_seq = seq; |
||||
if (mb->deliver_fn) mb->deliver_fn(m, mlen, mb->deliver_arg); |
||||
m += mlen; remaining -= mlen; |
||||
} |
||||
/* подтвердить storage-узлу приём — удалить доставленное (recipient/sender/up_to_seq) */ |
||||
if (count > 0 && max_seq > 0) { |
||||
uint8_t ack[24]; |
||||
memcpy(ack, &recipient, 8); |
||||
memcpy(ack + 8, &sender, 8); |
||||
memcpy(ack + 16, &max_seq, 8); |
||||
mb_route_send(mb, group_id, storage, MB_SUBCMD_ACK, ack, sizeof(ack)); |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: PULL_RESP recipient=0x%016llx sender=0x%016llx count=%u → ACK up_to=%llu", |
||||
MB_ID, (unsigned long long)recipient, (unsigned long long)sender, count, |
||||
(unsigned long long)max_seq); |
||||
} |
||||
} else if (subcmd == MB_SUBCMD_ACK) { |
||||
/* [subcmd][recipient:8][sender:8][up_to_seq:8] — storage: удалить доставленное */ |
||||
if (plen < 1 + 8 + 8 + 8) { queue_dgram_free(entry); queue_entry_free(entry); return; } |
||||
uint64_t recipient; memcpy(&recipient, p + 1, 8); |
||||
uint64_t sender; memcpy(&sender, p + 9, 8); |
||||
uint64_t up_to_seq; memcpy(&up_to_seq, p + 17, 8); |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(mb->db, |
||||
"DELETE FROM dm_mail WHERE recipient=? AND sender=? AND seq<=?", |
||||
-1, &st, NULL) == SQLITE_OK) { |
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)recipient); |
||||
sqlite3_bind_int64(st, 2, (sqlite3_int64)sender); |
||||
sqlite3_bind_int64(st, 3, (sqlite3_int64)up_to_seq); |
||||
sqlite3_step(st); |
||||
} |
||||
if (st) sqlite3_finalize(st); |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: ACK recipient=0x%016llx sender=0x%016llx up_to=%llu", |
||||
MB_ID, (unsigned long long)recipient, (unsigned long long)sender, |
||||
(unsigned long long)up_to_seq); |
||||
} else { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: unknown subcmd=%02x", MB_ID, subcmd); |
||||
} |
||||
|
||||
queue_dgram_free(entry); queue_entry_free(entry); |
||||
} |
||||
|
||||
/* ── Жизненный цикл ── */ |
||||
|
||||
int dm_mailbox_init(struct UTUN_INSTANCE* inst) { |
||||
if (!inst) return -1; |
||||
if (inst->dm_mailbox && inst->dm_mailbox->initialized) return 0; |
||||
struct dm_mb_state* mb = u_calloc(1, sizeof(*mb)); |
||||
if (!mb) return -1; |
||||
inst->dm_mailbox = mb; |
||||
mb->inst = inst; |
||||
mb->db = chat_core_get_db(inst); |
||||
if (!mb->db) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: no chat db", MB_ID); u_free(mb); inst->dm_mailbox = NULL; return -1; } |
||||
mb_create_tables(mb); |
||||
if (etcp_router_bind(inst, ETCP_RT_ID_DM_MAILBOX, mb_recv_cb) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "%s: etcp_router_bind failed", MB_ID); |
||||
u_free(mb); inst->dm_mailbox = NULL; return -1; |
||||
} |
||||
mb->initialized = 1; |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: initialized", MB_ID); |
||||
return 0; |
||||
} |
||||
|
||||
void dm_mailbox_destroy(struct UTUN_INSTANCE* inst) { |
||||
struct dm_mb_state* mb = mb_of(inst); |
||||
if (!inst || !mb || !mb->initialized) return; |
||||
etcp_router_unbind(inst, ETCP_RT_ID_DM_MAILBOX); |
||||
u_free(mb); |
||||
inst->dm_mailbox = NULL; |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: destroyed", MB_ID); |
||||
} |
||||
|
||||
/* ── Публичные операции ── */ |
||||
|
||||
/* Собрать storage-узлы (dm_storage=yes) из общих групп без дублей. */ |
||||
int dm_mailbox_get_storage_nodes(struct UTUN_INSTANCE* inst, uint64_t* out, int max) { |
||||
struct dm_mb_state* mb = mb_of(inst); |
||||
if (!inst || !out || max <= 0 || !mb || !mb->db) return 0; |
||||
uint64_t my_id = inst->node_id; |
||||
uint64_t* ch_ids = NULL; |
||||
int ch_count = 0; |
||||
if (topo_node_sqlite_get_member_channels(mb->db, my_id, &ch_ids, &ch_count) != 0) return 0; |
||||
|
||||
int found = 0; |
||||
char buf[256]; |
||||
for (int c = 0; c < ch_count && found < max; c++) { |
||||
char ch[64]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)ch_ids[c]); |
||||
char tbl[80]; snprintf(tbl, sizeof(tbl), "peers_%s", ch); |
||||
char sql[256]; snprintf(sql, sizeof(sql), |
||||
"SELECT node_id, COALESCE(adm_tags,'') FROM \"%s\"", tbl); |
||||
sqlite3_stmt* st = NULL; |
||||
if (sqlite3_prepare_v2(mb->db, sql, -1, &st, NULL) != SQLITE_OK) continue; |
||||
while (sqlite3_step(st) == SQLITE_ROW && found < max) { |
||||
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0); |
||||
if (nid == my_id) continue; |
||||
const char* tags = (const char*)sqlite3_column_text(st, 1); |
||||
if (tags && json_flat_get(tags, "dm_storage", buf, sizeof(buf)) == 0 |
||||
&& strcmp(buf, "yes") == 0) { |
||||
int dup = 0; |
||||
for (int i = 0; i < found; i++) if (out[i] == nid) { dup = 1; break; } |
||||
if (!dup) out[found++] = nid; |
||||
} |
||||
} |
||||
sqlite3_finalize(st); |
||||
} |
||||
if (ch_ids) u_free(ch_ids); |
||||
return found; |
||||
} |
||||
|
||||
/* Отправитель: разослать зашифрованное сообщение на все storage-узлы. */ |
||||
int dm_mailbox_put(struct UTUN_INSTANCE* inst, uint64_t recipient, uint64_t sender, |
||||
const uint8_t* msg, size_t msg_len) { |
||||
struct dm_mb_state* mb = mb_of(inst); |
||||
if (!inst || !mb || !msg || msg_len == 0) return -1; |
||||
|
||||
uint64_t nodes[8]; |
||||
int n = dm_mailbox_get_storage_nodes(inst, nodes, 8); |
||||
if (n == 0) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_DM, "%s: no storage nodes for offline delivery", MB_ID); |
||||
return -1; |
||||
} |
||||
|
||||
/* body = [recipient:8][sender:8][msg] */ |
||||
size_t body_len = 8 + 8 + msg_len; |
||||
uint8_t* body = u_malloc(body_len); |
||||
if (!body) return -1; |
||||
memcpy(body, &recipient, 8); |
||||
memcpy(body + 8, &sender, 8); |
||||
memcpy(body + 16, msg, msg_len); |
||||
|
||||
int sent = 0; |
||||
for (int i = 0; i < n; i++) { |
||||
if (mb_route_send(mb, 0, nodes[i], MB_SUBCMD_PUT, body, body_len) == 0) sent++; |
||||
} |
||||
u_free(body); |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: put → %d/%d storage nodes", MB_ID, sent, n); |
||||
return sent > 0 ? 0 : -1; |
||||
} |
||||
|
||||
/* Получатель: запросить почту от отправителя со всех storage-узлов. */ |
||||
void dm_mailbox_pull(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, uint64_t since_seq) { |
||||
struct dm_mb_state* mb = mb_of(inst); |
||||
if (!inst || !mb) return; |
||||
uint64_t nodes[8]; |
||||
int n = dm_mailbox_get_storage_nodes(inst, nodes, 8); |
||||
if (n == 0) return; |
||||
|
||||
uint8_t body[24]; |
||||
memcpy(body, &inst->node_id, 8); |
||||
memcpy(body + 8, &peer_node_id, 8); |
||||
memcpy(body + 16, &since_seq, 8); |
||||
for (int i = 0; i < n; i++) |
||||
mb_route_send(mb, 0, nodes[i], MB_SUBCMD_PULL, body, sizeof(body)); |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "%s: pull from %d storage nodes (peer=0x%016llx since=%llu)", |
||||
MB_ID, n, (unsigned long long)peer_node_id, (unsigned long long)since_seq); |
||||
} |
||||
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* dm_mailbox.h — offline-хранение DM-сообщений на storage-узлах |
||||
* |
||||
* Когда пир недоступен (нет прямой связи), отправитель кладёт зашифрованное |
||||
* сообщение на storage-узлы (мемберы общих групп с флагом dm_storage=yes). |
||||
* Получатель при выходе в online вытягивает накопившуюся почту, проверяет |
||||
* подпись, расшифровывает и вставляет в dm_messages (dedup по seq). |
||||
* |
||||
* Сервис ETCP_RT_ID_DM_MAILBOX (0x35), подкоманды PUT/PULL/PULL_RESP/ACK. |
||||
*/ |
||||
|
||||
#ifndef DM_MAILBOX_H |
||||
#define DM_MAILBOX_H |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
struct UTUN_INSTANCE; |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
/* Жизненный цикл: init после chat_core_init, destroy перед chat_core_destroy. */ |
||||
int dm_mailbox_init(struct UTUN_INSTANCE* inst); |
||||
void dm_mailbox_destroy(struct UTUN_INSTANCE* inst); |
||||
|
||||
/* Коллбэк доставки вытянутых сообщений в dm_core (msg = каноническое тело
|
||||
* DM-сообщения, включая подпись). Вызывается из uasync-потока. */ |
||||
typedef void (*dm_mail_deliver_fn)(const uint8_t* msg, size_t len, void* arg); |
||||
void dm_mailbox_set_deliver_cb(struct UTUN_INSTANCE* inst, dm_mail_deliver_fn fn, void* arg); |
||||
|
||||
/* Отправитель: положить зашифрованное сообщение на storage-узлы. msg — уже
|
||||
* собранное каноническое тело (зашифровано + подписано). 0=ок, <0=ошибка. */ |
||||
int dm_mailbox_put(struct UTUN_INSTANCE* inst, uint64_t recipient, uint64_t sender, |
||||
const uint8_t* msg, size_t msg_len); |
||||
|
||||
/* Получатель: вытянуть почту от конкретного отправителя (seq > since_seq) со
|
||||
* всех известных storage-узлов. Вызывается из uasync-потока. */ |
||||
void dm_mailbox_pull(struct UTUN_INSTANCE* inst, uint64_t peer_node_id, uint64_t since_seq); |
||||
|
||||
/* Собрать node_id storage-узлов (dm_storage=yes) из общих групп. До max штук.
|
||||
* Возвращает количество найденных. out — буфер на max*8 байт. */ |
||||
int dm_mailbox_get_storage_nodes(struct UTUN_INSTANCE* inst, uint64_t* out, int max); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif /* DM_MAILBOX_H */ |
||||
@ -1,79 +0,0 @@
|
||||
## Текущая задача: отладка merkle sync в chatgui |
||||
|
||||
### Контекст |
||||
|
||||
Две инстанции chatgui обмениваются мемберами (участниками) каналов через merkle sync. |
||||
Одна на Linux (node `41eab6811e3ff67b`), вторая на Windows (node `32a3a1ee5625ceb9`). |
||||
|
||||
### Что уже исправлено (vetka topo_upd) |
||||
|
||||
1. **`conn_mgr.c:276`** — убрано требование `links_up` при поиске существующего коннекта. |
||||
Раньше второй `conn_mgr_connect_node` (для второй группы/канала) не находил первый коннект |
||||
потому что тот был в handshake (links_up=0) и создавал дублирующий линк → два init-таймера |
||||
→ два потока INIT-пакетов → бесконечный цикл реинициализации ETCP. |
||||
|
||||
2. **`etcp_connections.c:insert_link_queue`** — детект коллизии адреса: если адрес уже занят |
||||
ДРУГИМ коннектом → жирная ошибка + return -1 (вместо молчаливой замены). |
||||
|
||||
3. **`etcp.c:etcp_conn_set_peer_node_id`** — детект коллизии peer_id: если key уже занят |
||||
другим коннектом → жирная ошибка + return (не переиндексировать). |
||||
|
||||
4. **`merkle_sync.c`** — исправлен формат сообщений: |
||||
- Отправитель клал type ПЕРЕД ch_len+ns: `[svc][TYPE][ch_len][ns]...` |
||||
- Получатель (`_recv_cb`) ожидал `[svc][ch_len][ns][TYPE]...` (как у chat_sync) |
||||
- Переставлены байты в `_send_hashes`, `_send_batch`, MSG_REQUEST builder |
||||
- Результат: handle_hashes/handle_request/handle_batch начали реально получать сообщения |
||||
|
||||
5. **`merkle_sync.c`** — `_handle_batch` теперь вызывает `_session_done(s, MT_OK)` после |
||||
обработки всех терминальных бакетов. До этого сессия никогда не завершалась, |
||||
таймер истекал → retry → к тому моменту соединение уже отваливалось. |
||||
|
||||
### Текущее состояние |
||||
|
||||
Merkle sync обменивается сообщениями и завершает сессии (`session SYNCED` в логах). |
||||
Но данные мемберов не совпадают: Linux имеет 3 мембера в канале, Windows — 2. |
||||
|
||||
### Найденная проблема (исследуется) |
||||
|
||||
В логах `handle_batch` показывает `len=7` для обоих каналов с обеих сторон. |
||||
Это означает что в BATCH-сообщении данные мемберов = всего 2 байта (count=0). |
||||
|
||||
При этом `merkle_tree_hash` показывает НЕнулевой bitmap (00008000) → `update_bucket_hash` |
||||
находит мемберов в бакете. Но `get_items` для того же бакета возвращает 0 записей. |
||||
|
||||
**Гипотеза:** `update_bucket_hash` и `get_items` используют разную логику фильтрации |
||||
для одного и того же level/prefix → SQL запрос в `get_items` не находит мемберов. |
||||
|
||||
Файлы: `tools/chatgui/transport/member_sync.c` функции `_member_update_bucket_hash` и `_member_get_items`. |
||||
|
||||
### Данные из БД (Linux) |
||||
|
||||
Канал 17611191138590969914: 3 мембера |
||||
- 4749809422690154107 (0x41eab6811e3ff67b — Linux self) |
||||
- 3648938168119840441 (0x32a3a1ee5625ceb9 — Windows peer) |
||||
- 5749221071098079620 (0x4fc9561a7dfde984 — третий узел) |
||||
|
||||
Канал 10175004475814804132: 2 мембера |
||||
- 3648938168119840441 (0x32a3a1ee5625ceb9) |
||||
- 4749809422690154107 (0x41eab6811e3ff67b) |
||||
|
||||
Merkle tree для канала 176111...: |
||||
``` |
||||
level=1 prefix=0x4000000000000000 member_count=1 hash=F612B771... |
||||
level=2 prefix=0x41E0000000000000 member_count=1 hash=F612B771... |
||||
level=3 prefix=0x41EA000000000000 member_count=1 hash=F612B771... |
||||
level=4 prefix=0x41EAB00000000000 member_count=1 hash=F612B771... |
||||
level=5 prefix=0x41EAB68000000000 member_count=1 hash=F612B771... |
||||
``` |
||||
|
||||
### Логи |
||||
|
||||
Linux: `/home/vnc1/proj/utun3/tools/chatgui/build/chatgui.log` |
||||
Windows: `/home/vnc1/proj/utun3/tools/chatgui/build/chatgui2.log` |
||||
|
||||
Конфиг: `db_sync=debug` включён. |
||||
|
||||
### БД |
||||
|
||||
Linux: `/home/vnc1/proj/utun3/tools/chatgui/build/chat_data/chats.db` |
||||
Windows: `C:/ARM/_uTun/utun2/tools/chatgui/build/chat_data/chats.db` (доступа нет) |
||||
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* test_dm.c — тест криптографии прямого чата (dm_crypto) |
||||
* |
||||
* Проверяет: |
||||
* - детерминированность и симметричность conv_id (одинаков на обеих сторонах); |
||||
* - симметричность content_key (ECDH: обе стороны выводят одинаковый ключ); |
||||
* - round-trip шифрования AES-256-CCM и детекцию неверного ключа. |
||||
*/ |
||||
|
||||
#include "../src/dm/dm_crypto.h" |
||||
#include "../src/transport_layer/secure_channel.h" |
||||
#include "../lib/debug_config.h" |
||||
|
||||
#include <string.h> |
||||
#include <stdio.h> |
||||
|
||||
int main(void) { |
||||
debug_config_init(); |
||||
debug_set_level(DEBUG_LEVEL_ERROR); |
||||
|
||||
int failures = 0; |
||||
|
||||
/* ── 1. conv_id: симметричность и уникальность ── */ |
||||
uint64_t a = 0x1122334455667788ULL; |
||||
uint64_t b = 0x8877665544332211ULL; |
||||
uint64_t id_ab = dm_derive_conv_id(a, b); |
||||
uint64_t id_ba = dm_derive_conv_id(b, a); |
||||
if (id_ab == 0 || id_ab != id_ba) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "conv_id not symmetric: ab=%llu ba=%llu", |
||||
(unsigned long long)id_ab, (unsigned long long)id_ba); |
||||
failures++; |
||||
} else { |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "conv_id symmetric: %llu == %llu", |
||||
(unsigned long long)id_ab, (unsigned long long)id_ba); |
||||
} |
||||
|
||||
uint64_t c = 0xDEADBEEF00000001ULL; |
||||
if (dm_derive_conv_id(a, c) == id_ab) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "conv_id collision for different pairs"); |
||||
failures++; |
||||
} |
||||
|
||||
/* ── 2. content_key: симметричность ECDH ── */ |
||||
struct SC_MYKEYS k1, k2; |
||||
if (sc_generate_keypair(&k1) != SC_OK || sc_generate_keypair(&k2) != SC_OK) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "keypair generation failed"); |
||||
return 1; |
||||
} |
||||
uint8_t key1[DM_CONTENT_KEY_SIZE], key2[DM_CONTENT_KEY_SIZE]; |
||||
if (dm_derive_content_key(k1.private_key, k2.public_key, key1) != 0 |
||||
|| dm_derive_content_key(k2.private_key, k1.public_key, key2) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "content_key derive failed"); |
||||
failures++; |
||||
} else if (memcmp(key1, key2, DM_CONTENT_KEY_SIZE) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "content_key not symmetric (ECDH mismatch)"); |
||||
failures++; |
||||
} else { |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "content_key symmetric (ECDH ok)"); |
||||
} |
||||
|
||||
/* ── 3. AES-256-CCM round-trip ── */ |
||||
const char* text = "hello, direct chat!"; |
||||
size_t text_len = strlen(text); |
||||
uint8_t nonce[DM_NONCE_SIZE]; |
||||
dm_build_nonce(id_ab, a, 42, nonce); |
||||
|
||||
uint8_t enc[256]; |
||||
size_t enc_len = 0; |
||||
if (dm_encrypt(key1, nonce, (const uint8_t*)text, text_len, enc, &enc_len) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "encrypt failed"); |
||||
failures++; |
||||
} else if (enc_len != text_len + DM_TAG_SIZE) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "encrypt length mismatch: %zu", enc_len); |
||||
failures++; |
||||
} else { |
||||
uint8_t dec[256]; |
||||
size_t dec_len = 0; |
||||
if (dm_decrypt(key2, nonce, enc, enc_len, dec, &dec_len) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "decrypt failed"); |
||||
failures++; |
||||
} else if (dec_len != text_len || memcmp(dec, text, text_len) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "decrypt content mismatch"); |
||||
failures++; |
||||
} else { |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "AES-256-CCM round-trip ok"); |
||||
} |
||||
} |
||||
|
||||
/* ── 4. детекция неверного ключа ── */ |
||||
{ |
||||
uint8_t wrong[DM_CONTENT_KEY_SIZE]; |
||||
memset(wrong, 0x5A, sizeof(wrong)); |
||||
uint8_t dec[256]; |
||||
size_t dec_len = 0; |
||||
if (enc_len > 0 && dm_decrypt(wrong, nonce, enc, enc_len, dec, &dec_len) == 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DM, "decrypt with wrong key should fail"); |
||||
failures++; |
||||
} else { |
||||
DEBUG_INFO(DEBUG_CATEGORY_DM, "wrong-key tamper detection ok"); |
||||
} |
||||
} |
||||
|
||||
if (failures == 0) { |
||||
printf("TEST PASSED\n"); |
||||
return 0; |
||||
} |
||||
printf("TEST FAILED (%d failures)\n", failures); |
||||
return 1; |
||||
} |
||||
@ -0,0 +1,452 @@
|
||||
// test_dm_e2e.c — интеграционный тест прямого чата (DM): прямая отправка + offline-хранение.
|
||||
//
|
||||
// Однопоточный: 3 узла (A, B, C) в одном процессе, один UASYNC, без fork.
|
||||
// Топология — цепочка B→C→A (A владелец канала, C storage, B target).
|
||||
// Сценарий управляется одной master state-machine (таймер dm_tick).
|
||||
//
|
||||
// Сценарий:
|
||||
// фаза 1 (прямая): A,B,C online. A dm_start(B) → dm_send("hello1") → B получает напрямую.
|
||||
// фаза 2 (offline): B1 завершается (A теряет B). A dm_send("hello2") → недоступен →
|
||||
// кладёт в mailbox на C (dm_storage=yes). C видит блоб в dm_mail.
|
||||
// фаза 3 (reconnect): B2 переподключается к C → dm_on_conn_status → PULL mailbox → получает "hello2".
|
||||
|
||||
#include "chat_core.h" |
||||
#include "chat_sync.h" |
||||
#include "member_sync.h" |
||||
#include "../dm/dm_core.h" |
||||
#include "../dm/dm_crypto.h" |
||||
#include "../routing_layer/topo_node_sqlite.h" |
||||
#include "../routing_layer/topo_group.h" |
||||
#include "../routing_layer/topo_node.h" |
||||
#include "../utun_instance.h" |
||||
#include "../transport_layer/etcp.h" |
||||
#include "../transport_layer/secure_channel.h" |
||||
#include "../ntp_time.h" |
||||
#include "../lib/u_async.h" |
||||
#include "../lib/debug_config.h" |
||||
#include "../lib/mem.h" |
||||
#include "../lib/platform_compat.h" |
||||
|
||||
#include <sqlite3.h> |
||||
#define OPENSSL_API_COMPAT 0x10100000L |
||||
#include <openssl/evp.h> |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <stdarg.h> |
||||
#include <time.h> |
||||
#include <unistd.h> |
||||
|
||||
#define CH_ID "4242424242424242" |
||||
|
||||
#define IDX_A 0 |
||||
#define IDX_B 1 |
||||
#define IDX_C 2 |
||||
|
||||
#define TICK_TB 100 /* 10 ms на такт state machine */ |
||||
#define MAX_TICKS 3000 /* глобальный таймаут ≈ 30 c */ |
||||
|
||||
/* фазы master state-machine */ |
||||
enum dm_phase { |
||||
P_WAIT_CONN, /* ждём подъёма всех соединений */ |
||||
P_SETUP, /* однократная настройка канала/мемберов на A,B,C */ |
||||
P_WAIT_BGP, /* ждём сходимости BGP CHAT-группы */ |
||||
P_SEND1, /* A: dm_start + hello1 */ |
||||
P_WAIT_MSG1, /* ждём hello1 у B */ |
||||
P_B_OFFLINE, /* destroy B1 */ |
||||
P_WAIT_B_OFFLINE, /* ждём ухода B из BGP у A */ |
||||
P_SEND2, /* A: hello2 → mailbox */ |
||||
P_WAIT_MAIL, /* ждём блоб в dm_mail у C */ |
||||
P_RECONNECT, /* create B2 */ |
||||
P_WAIT_B2, /* ждём hello2 у B2 и опустошение dm_mail у C */ |
||||
P_DONE, |
||||
}; |
||||
|
||||
struct dm_ctx { |
||||
struct UASYNC* ua; |
||||
struct UTUN_INSTANCE* inst[3]; |
||||
enum dm_phase phase; |
||||
int result; /* 0=running, 1=ok, 2=fail */ |
||||
int ticks; |
||||
void* timer; |
||||
}; |
||||
|
||||
/* общие данные (ключи узлов и канала) */ |
||||
struct dm_shared { |
||||
uint8_t ch_x_pub[32], ch_ed_pub[32], ch_ed_priv[32], ch_sig[64]; |
||||
uint64_t nid[3]; |
||||
uint8_t x_pub[3][32], x_priv[3][32], ed_pub[3][32]; |
||||
int port[3]; |
||||
char conv_id[64]; |
||||
}; |
||||
|
||||
static struct dm_shared g_sh; |
||||
|
||||
static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0; |
||||
#define TEST(n) do { G_TOTAL++; printf(" %-58s", n); fflush(stdout); } while(0) |
||||
#define OK() do { G_PASSED++; printf("OK\n"); } while(0) |
||||
#define FAIL(f,...) do { G_FAILED++; printf("FAIL: " f "\n", ##__VA_ARGS__); } while(0) |
||||
|
||||
/* ── крипто-хелперы ── */ |
||||
|
||||
static void gen_x25519(uint8_t pub[32], uint8_t priv[32]) { |
||||
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_X25519, NULL); |
||||
EVP_PKEY* pkey = NULL; |
||||
EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx); |
||||
size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l); |
||||
l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l); |
||||
EVP_PKEY_free(pkey); |
||||
} |
||||
|
||||
static void gen_ed25519(uint8_t pub[32], uint8_t priv[32]) { |
||||
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); |
||||
EVP_PKEY* pkey = NULL; |
||||
EVP_PKEY_keygen_init(ctx); EVP_PKEY_keygen(ctx, &pkey); EVP_PKEY_CTX_free(ctx); |
||||
size_t l = 32; EVP_PKEY_get_raw_public_key(pkey, pub, &l); |
||||
l = 32; EVP_PKEY_get_raw_private_key(pkey, priv, &l); |
||||
EVP_PKEY_free(pkey); |
||||
} |
||||
|
||||
static void to_hex(const uint8_t* bin, size_t n, char* out) { |
||||
for (size_t i = 0; i < n; i++) sprintf(out + i * 2, "%02x", bin[i]); |
||||
out[n * 2] = '\0'; |
||||
} |
||||
|
||||
static int wf(const char* p, const char* f, ...) { |
||||
va_list ap; FILE* fp = fopen(p, "w"); if (!fp) return -1; |
||||
va_start(ap, f); vfprintf(fp, f, ap); va_end(ap); fclose(fp); return 0; |
||||
} |
||||
|
||||
/* ── генерация данных сценария + конфигов ── */ |
||||
|
||||
static void fill_shared(int base_port) { |
||||
memset(&g_sh, 0, sizeof(g_sh)); |
||||
|
||||
for (int i = 0; i < 3; i++) { |
||||
gen_x25519(g_sh.x_pub[i], g_sh.x_priv[i]); |
||||
sc_derive_ed25519_pubkey(g_sh.x_priv[i], g_sh.ed_pub[i]); |
||||
g_sh.nid[i] = sc_derive_node_id_from_pubkey(g_sh.x_pub[i]); |
||||
g_sh.port[i] = base_port + i * 1000; |
||||
} |
||||
|
||||
{ uint8_t ch_x_priv[32]; gen_x25519(g_sh.ch_x_pub, ch_x_priv); } |
||||
gen_ed25519(g_sh.ch_ed_pub, g_sh.ch_ed_priv); |
||||
|
||||
/* подпись канала: Ed25519(ch_ed_priv, ch_id||'\0'||name||'\0'||owner(8)||ch_x_pub(32)||ch_ed_pub(32)) */ |
||||
{ |
||||
uint8_t msg[256]; size_t off = 0; |
||||
off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", CH_ID) + 1; |
||||
off += (size_t)snprintf((char*)msg + off, sizeof(msg) - off, "%s", "dm-test") + 1; |
||||
memcpy(msg + off, &g_sh.nid[IDX_A], 8); off += 8; |
||||
memcpy(msg + off, g_sh.ch_x_pub, 32); off += 32; |
||||
memcpy(msg + off, g_sh.ch_ed_pub, 32); off += 32; |
||||
sc_ed25519_sign(g_sh.ch_ed_priv, msg, off, g_sh.ch_sig); |
||||
} |
||||
|
||||
uint64_t conv = dm_derive_conv_id(g_sh.nid[IDX_A], g_sh.nid[IDX_B]); |
||||
snprintf(g_sh.conv_id, sizeof(g_sh.conv_id), "%llu", (unsigned long long)conv); |
||||
} |
||||
|
||||
static void write_configs(const char* dir) { |
||||
for (int i = 0; i < 3; i++) { |
||||
char priv[65], pub[65], path[512], dbd[512]; |
||||
to_hex(g_sh.x_priv[i], 32, priv); |
||||
to_hex(g_sh.x_pub[i], 32, pub); |
||||
snprintf(path, sizeof(path), "%s/%s.conf", dir, i == IDX_A ? "a" : i == IDX_B ? "b" : "c"); |
||||
snprintf(dbd, sizeof(dbd), "%s/db%s", dir, i == IDX_A ? "a" : i == IDX_B ? "b" : "c"); |
||||
utun_mkdir(dbd, 0755); |
||||
|
||||
char client[512] = ""; |
||||
if (i == IDX_C) { |
||||
/* C → A (клиент к A) */ |
||||
char apub[65]; to_hex(g_sh.x_pub[IDX_A], 32, apub); |
||||
snprintf(client, sizeof(client), |
||||
"[client: to_a]\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n", |
||||
apub, g_sh.port[IDX_A]); |
||||
} else if (i == IDX_B) { |
||||
/* B → C (клиент к C) */ |
||||
char cpub[65]; to_hex(g_sh.x_pub[IDX_C], 32, cpub); |
||||
snprintf(client, sizeof(client), |
||||
"[client: to_c]\npeer_public_key=%s\nlink=s1:127.0.0.1:%d\n", |
||||
cpub, g_sh.port[IDX_C]); |
||||
} |
||||
|
||||
wf(path, |
||||
"[global]\ntun_ip=10.97.%d.1/24\ntun_ifname=tun%d0\ndb_path=%s/db%s\n" |
||||
"my_private_key=%s\nmy_public_key=%s\n" |
||||
"[server: s1]\naddr=127.0.0.1:%d\ntype=public\n" |
||||
"%s" |
||||
"[chatserver]\nstorage_autoload=0\n[allowed_keys]\nallow_all=1\n", |
||||
i, i, dir, i == IDX_A ? "a" : i == IDX_B ? "b" : "c", |
||||
priv, pub, g_sh.port[i], client); |
||||
} |
||||
} |
||||
|
||||
/* ── чистые проверки (без опроса) ── */ |
||||
|
||||
static int conn_up(struct UTUN_INSTANCE* inst, uint64_t nid) { |
||||
struct ETCP_CONN* c = instance_find_conn(inst, nid); |
||||
return c && c->initialized && c->links_up; |
||||
} |
||||
|
||||
static int bgp_has(struct UTUN_INSTANCE* inst, uint64_t nid) { |
||||
uint64_t gid = strtoull(CH_ID, NULL, 10); |
||||
struct TOPO_GROUP* g = topo_groups_find(inst->topo_groups, gid); |
||||
if (!g) return 0; |
||||
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(g, nid); |
||||
return nq && nq->paths && nq->paths->head; |
||||
} |
||||
|
||||
/* есть ли расшифрованный текст needle в беседе (одна проверка) */ |
||||
static int dm_msg_has(struct UTUN_INSTANCE* inst, const char* needle) { |
||||
char buf[8192]; size_t len = 0; |
||||
if (dm_list_messages_json(inst, g_sh.conv_id, 100, 0, buf, sizeof(buf), &len) != 0) return 0; |
||||
return strstr(buf, needle) != NULL; |
||||
} |
||||
|
||||
/* количество блобов в dm_mail для recipient=B от sender=A (одна проверка) */ |
||||
static int dm_mail_count(struct UTUN_INSTANCE* inst) { |
||||
sqlite3_stmt* s = NULL; |
||||
int n = 0; |
||||
if (sqlite3_prepare_v2(inst->topo_sqlite_db, |
||||
"SELECT COUNT(*) FROM dm_mail WHERE recipient=? AND sender=?", |
||||
-1, &s, NULL) == SQLITE_OK) { |
||||
sqlite3_bind_int64(s, 1, (sqlite3_int64)g_sh.nid[IDX_B]); |
||||
sqlite3_bind_int64(s, 2, (sqlite3_int64)g_sh.nid[IDX_A]); |
||||
if (sqlite3_step(s) == SQLITE_ROW) n = sqlite3_column_int(s, 0); |
||||
} |
||||
if (s) sqlite3_finalize(s); |
||||
return n; |
||||
} |
||||
|
||||
/* ── настройка канала и мемберов (однократно) ── */ |
||||
|
||||
static void channel_put_shared(struct UTUN_INSTANCE* inst, int has_priv) { |
||||
topo_node_sqlite_channel_put(inst->topo_sqlite_db, CH_ID, "dm-test", g_sh.nid[IDX_A], |
||||
g_sh.ch_x_pub, NULL, g_sh.ch_ed_pub, has_priv ? g_sh.ch_ed_priv : NULL, g_sh.ch_sig); |
||||
chat_core_ensure_channel_ready(inst, CH_ID); |
||||
} |
||||
|
||||
static void member_put_self(struct UTUN_INSTANCE* inst, const char* name) { |
||||
uint64_t join_ts = (uint64_t)ntp_time_get_seconds(inst); |
||||
uint8_t jm[128]; |
||||
int jl = member_sync_build_join_msg(g_sh.ch_x_pub, g_sh.ch_ed_pub, inst->node_id, |
||||
inst->my_keys.public_key, join_ts, jm, (int)sizeof(jm)); |
||||
uint8_t join_sig[64]; sc_ed25519_sign(inst->my_ed25519_privkey, jm, (size_t)jl, join_sig); |
||||
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name); |
||||
member_sync_put(inst, CH_ID, inst->node_id, inst->my_keys.public_key, inst->my_ed25519_pubkey, |
||||
join_sig, join_ts, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL); |
||||
} |
||||
|
||||
static void member_put_placeholder(struct UTUN_INSTANCE* inst, int idx, const char* name) { |
||||
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name); |
||||
member_sync_put(inst, CH_ID, g_sh.nid[idx], g_sh.x_pub[idx], g_sh.ed_pub[idx], |
||||
NULL, 0, NULL, 0, userinfo, NULL, NULL, 0, 0, NULL); |
||||
} |
||||
|
||||
/* мембер C со флагом dm_storage=yes (adm_tags подписан канальным ключом) */ |
||||
static void member_put_storage(struct UTUN_INSTANCE* inst, int idx, const char* name) { |
||||
char userinfo[256]; snprintf(userinfo, sizeof(userinfo), "{\"name\":\"%s\"}", name); |
||||
char adm_tags[64]; snprintf(adm_tags, sizeof(adm_tags), "{\"dm_storage\":\"yes\",\"ver\":\"1\"}"); |
||||
uint8_t ats[64]; |
||||
uint8_t amsg[256]; size_t off = strlen(adm_tags); |
||||
memcpy(amsg, adm_tags, off); |
||||
memcpy(amsg + off, &g_sh.nid[idx], 8); off += 8; |
||||
sc_ed25519_sign(g_sh.ch_ed_priv, amsg, off, ats); |
||||
member_sync_put(inst, CH_ID, g_sh.nid[idx], g_sh.x_pub[idx], g_sh.ed_pub[idx], |
||||
NULL, 0, NULL, 0, userinfo, adm_tags, ats, 0, 0, NULL); |
||||
} |
||||
|
||||
/* однократная настройка роли */ |
||||
static void do_setup(struct UTUN_INSTANCE* inst, int role) { |
||||
uint64_t nc = g_sh.nid[IDX_C]; |
||||
switch (role) { |
||||
case IDX_A: |
||||
channel_put_shared(inst, 1); |
||||
member_put_self(inst, "A"); |
||||
member_put_placeholder(inst, IDX_B, "B"); |
||||
member_put_storage(inst, IDX_C, "C"); |
||||
member_sync_start(inst, nc, CH_ID, NULL, NULL); |
||||
break; |
||||
case IDX_B: |
||||
channel_put_shared(inst, 0); |
||||
member_put_self(inst, "B"); |
||||
member_put_placeholder(inst, IDX_A, "A"); |
||||
member_put_storage(inst, IDX_C, "C"); |
||||
member_sync_start(inst, nc, CH_ID, NULL, NULL); |
||||
break; |
||||
case IDX_C: |
||||
channel_put_shared(inst, 0); |
||||
member_put_self(inst, "C"); |
||||
member_put_placeholder(inst, IDX_A, "A"); |
||||
member_put_placeholder(inst, IDX_B, "B"); |
||||
member_sync_start(inst, g_sh.nid[IDX_A], CH_ID, NULL, NULL); |
||||
member_sync_start(inst, g_sh.nid[IDX_B], CH_ID, NULL, NULL); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
/* ── master state-machine (движется таймером) ── */ |
||||
|
||||
static void dm_tick(void* arg) { |
||||
struct dm_ctx* t = arg; |
||||
t->timer = NULL; |
||||
if (t->result) return; |
||||
|
||||
struct UTUN_INSTANCE* A = t->inst[IDX_A]; |
||||
struct UTUN_INSTANCE* B = t->inst[IDX_B]; |
||||
struct UTUN_INSTANCE* C = t->inst[IDX_C]; |
||||
uint64_t na = g_sh.nid[IDX_A], nb = g_sh.nid[IDX_B], nc = g_sh.nid[IDX_C]; |
||||
|
||||
switch (t->phase) { |
||||
case P_WAIT_CONN: |
||||
/* C→A и B→C должны быть up */ |
||||
if (conn_up(A, nc) && conn_up(C, na) && conn_up(B, nc) && conn_up(C, nb)) |
||||
t->phase = P_SETUP; |
||||
break; |
||||
case P_SETUP: |
||||
do_setup(A, IDX_A); |
||||
do_setup(B, IDX_B); |
||||
do_setup(C, IDX_C); |
||||
t->phase = P_WAIT_BGP; |
||||
break; |
||||
case P_WAIT_BGP: |
||||
/* A видит C и B; C видит A и B; B видит A (и C) */ |
||||
if (bgp_has(A, nc) && bgp_has(A, nb) && bgp_has(C, na) && bgp_has(C, nb) && bgp_has(B, na)) |
||||
t->phase = P_SEND1; |
||||
break; |
||||
case P_SEND1: |
||||
if (dm_start(A, nb, g_sh.x_pub[IDX_B], g_sh.ed_pub[IDX_B], "B", CH_ID) != 0 |
||||
|| dm_send(A, g_sh.conv_id, "text", (const uint8_t*)"hello1", 6) != 0) { |
||||
fprintf(stderr, "A: dm_start/send hello1 failed\n"); |
||||
t->result = 2; |
||||
} else { |
||||
t->phase = P_WAIT_MSG1; |
||||
} |
||||
break; |
||||
case P_WAIT_MSG1: |
||||
if (dm_msg_has(B, "hello1")) t->phase = P_B_OFFLINE; |
||||
break; |
||||
case P_B_OFFLINE: |
||||
utun_instance_destroy(B); |
||||
t->inst[IDX_B] = NULL; |
||||
t->phase = P_WAIT_B_OFFLINE; |
||||
break; |
||||
case P_WAIT_B_OFFLINE: |
||||
if (!bgp_has(A, nb)) t->phase = P_SEND2; |
||||
break; |
||||
case P_SEND2: |
||||
if (dm_send(A, g_sh.conv_id, "text", (const uint8_t*)"hello2", 6) != 0) { |
||||
fprintf(stderr, "A: dm_send hello2 failed\n"); |
||||
t->result = 2; |
||||
} else { |
||||
t->phase = P_WAIT_MAIL; |
||||
} |
||||
break; |
||||
case P_WAIT_MAIL: |
||||
if (dm_mail_count(C) > 0) t->phase = P_RECONNECT; |
||||
break; |
||||
case P_RECONNECT: { |
||||
char cfg[512]; |
||||
snprintf(cfg, sizeof(cfg), "%s/b.conf", getenv("UTUN_TEST_DIR") ? getenv("UTUN_TEST_DIR") : "."); |
||||
struct UTUN_INSTANCE* b2 = utun_instance_create(t->ua, cfg); |
||||
if (!b2) { fprintf(stderr, "B2 create failed\n"); t->result = 2; break; } |
||||
utun_instance_init(b2); |
||||
t->inst[IDX_B] = b2; |
||||
t->phase = P_WAIT_B2; |
||||
break; |
||||
} |
||||
case P_WAIT_B2: |
||||
if (dm_msg_has(B, "hello2") && dm_mail_count(C) == 0) t->phase = P_DONE; |
||||
break; |
||||
case P_DONE: |
||||
t->result = 1; |
||||
uasync_stop(t->ua); |
||||
return; |
||||
} |
||||
|
||||
if (t->phase == P_DONE) { t->result = 1; uasync_stop(t->ua); return; } |
||||
|
||||
if (++t->ticks > MAX_TICKS) { |
||||
fprintf(stderr, "test: timeout in phase %d (A_conn=%d C_conn=%d B_conn=%d)\n", |
||||
(int)t->phase, |
||||
conn_up(A, nc), conn_up(C, na), B ? conn_up(B, nc) : -1); |
||||
t->result = 2; |
||||
uasync_stop(t->ua); |
||||
return; |
||||
} |
||||
t->timer = uasync_set_timeout(t->ua, TICK_TB, t, dm_tick, "dm_tick"); |
||||
} |
||||
|
||||
/* ── main ── */ |
||||
|
||||
int main(int argc, char** argv) { |
||||
(void)argc; (void)argv; |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); |
||||
if (getenv("UTUN_TEST_DEBUG")) { |
||||
debug_set_category_level_by_name("dm", "trace"); |
||||
debug_set_category_level_by_name("chat_sync", "info"); |
||||
debug_set_category_level_by_name("member_sync", "info"); |
||||
debug_set_category_level_by_name("bgp", "info"); |
||||
debug_set_category_level_by_name("etcp_route", "info"); |
||||
debug_set_category_level_by_name("crypto", "trace"); |
||||
} |
||||
utun_instance_set_tun_init_enabled(0); |
||||
srand((unsigned)time(NULL)); |
||||
|
||||
char dir[512]; snprintf(dir, sizeof(dir), "/tmp/utun_dm_e2e_XXXXXX"); |
||||
if (!mkdtemp(dir)) { fprintf(stderr, "mkdtemp failed\n"); return 1; } |
||||
setenv("UTUN_TEST_DIR", dir, 1); |
||||
|
||||
fill_shared(58000 + (getpid() % 2000)); |
||||
write_configs(dir); |
||||
|
||||
printf("=== test_dm_e2e ===\n"); fflush(stdout); |
||||
|
||||
TEST("single-thread: direct send + offline mailbox + reconnect pull"); { |
||||
struct UASYNC* ua = uasync_create(); |
||||
if (!ua) { FAIL("uasync_create failed"); return 1; } |
||||
|
||||
struct dm_ctx t; |
||||
memset(&t, 0, sizeof(t)); |
||||
t.ua = ua; |
||||
t.phase = P_WAIT_CONN; |
||||
|
||||
char cfgA[512], cfgB[512], cfgC[512]; |
||||
snprintf(cfgA, sizeof(cfgA), "%s/a.conf", dir); |
||||
snprintf(cfgB, sizeof(cfgB), "%s/b.conf", dir); |
||||
snprintf(cfgC, sizeof(cfgC), "%s/c.conf", dir); |
||||
|
||||
t.inst[IDX_A] = utun_instance_create(ua, cfgA); |
||||
t.inst[IDX_B] = utun_instance_create(ua, cfgB); |
||||
t.inst[IDX_C] = utun_instance_create(ua, cfgC); |
||||
|
||||
if (!t.inst[IDX_A] || !t.inst[IDX_B] || !t.inst[IDX_C]) { |
||||
FAIL("instance create failed A=%p B=%p C=%p", |
||||
(void*)t.inst[IDX_A], (void*)t.inst[IDX_B], (void*)t.inst[IDX_C]); |
||||
uasync_destroy(ua, 0); |
||||
return 1; |
||||
} |
||||
utun_instance_init(t.inst[IDX_A]); |
||||
utun_instance_init(t.inst[IDX_C]); |
||||
/* B стартует после C, чтобы B→C пришлось на уже поднятый C (как в сценарии) */ |
||||
utun_instance_init(t.inst[IDX_B]); |
||||
|
||||
t.timer = uasync_set_timeout(ua, TICK_TB, &t, dm_tick, "dm_tick"); |
||||
uasync_mainloop(ua); |
||||
|
||||
int ok = (t.result == 1); |
||||
|
||||
if (t.inst[IDX_A]) utun_instance_destroy(t.inst[IDX_A]); |
||||
if (t.inst[IDX_B]) utun_instance_destroy(t.inst[IDX_B]); |
||||
if (t.inst[IDX_C]) utun_instance_destroy(t.inst[IDX_C]); |
||||
uasync_destroy(ua, 0); |
||||
|
||||
if (ok) OK(); else FAIL("result=%d phase=%d", t.result, (int)t.phase); |
||||
} |
||||
|
||||
char cmd[512]; snprintf(cmd, sizeof(cmd), "rm -rf %s", dir); (void)!system(cmd); |
||||
|
||||
printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED); |
||||
return G_FAILED > 0 ? 1 : 0; |
||||
} |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue