Browse Source
- voice_jitter (кодированные Opus-кадры + decode-on-pull + SoundTouch time-stretch) вместо PCM-буфера audio_jitter; излишек держится до декодирования - call_audio.c/.h — единая реализация для desktop/Android/headless (C-интерфейс) - libutun_voice (voice_sources.cmake): SoundTouch + voice_jitter + call_audio + call_tones - Android: pull-модель (nativeCallAudioPull), убран Kotlin FIFO, ANDROID_STL=c++_static - headless audio-сокет: PCM-протокол (decode-only, без тактирования), daemon остаётся C-only - CALL_STATS расширен dropped/underruns; удалены audio_jitter.cpp и libutun_lite/call_audio.c - тест test_voice_jitter (ёмкость/догон/стационар), test_call_headless переведён на PCMrouter-recv-conn-uaf
29 changed files with 659 additions and 756 deletions
@ -1,180 +0,0 @@ |
|||||||
// audio_jitter.cpp — адаптивный джиттер-буфер с time-stretch (SoundTouch).
|
|
||||||
//
|
|
||||||
// PCM-кадры (int16, 960 сэмплов @20мс) кладутся из uasync-потока в кольцо и
|
|
||||||
// вытаскиваются аудио-потоком через SoundTouch с плавно меняющимся темпом:
|
|
||||||
// глубина < target → 1.0x, глубина растёт → ускорение до 1.6x (догон live).
|
|
||||||
// Кольцо защищено мьютексом (SPSC); SoundTouch трогает только аудио-поток.
|
|
||||||
|
|
||||||
#include "audio_jitter.h" |
|
||||||
|
|
||||||
#include "soundtouch/SoundTouch.h" |
|
||||||
|
|
||||||
#include <atomic> |
|
||||||
#include <cmath> |
|
||||||
#include <cstring> |
|
||||||
#include <mutex> |
|
||||||
#include <vector> |
|
||||||
|
|
||||||
static const double kTargetMs = 60.0; // целевая глубина буфера
|
|
||||||
static const double kMaxTempo = 1.6; |
|
||||||
static const double kMinTempo = 1.0; |
|
||||||
static const double kMaxDepthMs = 1000.0; // при этой глубине tempo == kMaxTempo
|
|
||||||
static const double kEmaAlpha = 0.2; // сглаживание глубины
|
|
||||||
static const double kTempoStep = 0.01; // макс. изменение темпа за один pull (без кликов)
|
|
||||||
|
|
||||||
struct ajb { |
|
||||||
int sr = 0; |
|
||||||
int ch = 0; |
|
||||||
int frame_samples = 0; |
|
||||||
int frame_ms = 0; |
|
||||||
int cap = 0; |
|
||||||
|
|
||||||
std::mutex mtx; |
|
||||||
std::vector<int16_t> ring; // cap * frame_samples
|
|
||||||
int head = 0; |
|
||||||
int count = 0; |
|
||||||
uint32_t dropped = 0; |
|
||||||
|
|
||||||
soundtouch::SoundTouch st; // только аудио-поток
|
|
||||||
std::vector<int16_t> frame_buf; // scratch: вытащенный кадр
|
|
||||||
std::vector<float> fin; // scratch: кадр в float
|
|
||||||
std::vector<float> fout; // scratch: выход float
|
|
||||||
|
|
||||||
double ema_depth_ms = 0.0; |
|
||||||
double tempo = 1.0; |
|
||||||
std::atomic<int> tempo_x100{100}; |
|
||||||
}; |
|
||||||
|
|
||||||
static double ajb_compute_tempo(double depth_ms) { |
|
||||||
if (depth_ms <= kTargetMs) return kMinTempo; |
|
||||||
double x = (depth_ms - kTargetMs) / (kMaxDepthMs - kTargetMs); |
|
||||||
if (x > 1.0) x = 1.0; |
|
||||||
return kMinTempo + (kMaxTempo - kMinTempo) * x; |
|
||||||
} |
|
||||||
|
|
||||||
/* Вытащить один кадр из кольца (под мьютексом). Возвращает 0 если пусто. */ |
|
||||||
static int ajb_pop_frame(struct ajb* j, int16_t* out) { |
|
||||||
std::lock_guard<std::mutex> lk(j->mtx); |
|
||||||
if (j->count == 0) return 0; |
|
||||||
std::memcpy(out, &j->ring[(size_t)j->head * j->frame_samples], |
|
||||||
(size_t)j->frame_samples * sizeof(int16_t)); |
|
||||||
j->head = (j->head + 1) % j->cap; |
|
||||||
j->count--; |
|
||||||
return 1; |
|
||||||
} |
|
||||||
|
|
||||||
extern "C" { |
|
||||||
|
|
||||||
struct ajb* ajb_create(int sample_rate, int channels, int frame_samples, int frame_ms, int max_frames) { |
|
||||||
if (sample_rate <= 0 || channels <= 0 || frame_samples <= 0 || frame_ms <= 0 || max_frames <= 0) |
|
||||||
return nullptr; |
|
||||||
|
|
||||||
ajb* j = new ajb(); |
|
||||||
j->sr = sample_rate; |
|
||||||
j->ch = channels; |
|
||||||
j->frame_samples = frame_samples; |
|
||||||
j->frame_ms = frame_ms; |
|
||||||
j->cap = max_frames; |
|
||||||
j->ring.assign((size_t)max_frames * frame_samples, 0); |
|
||||||
j->frame_buf.assign(frame_samples, 0); |
|
||||||
j->fin.assign(frame_samples, 0.0f); |
|
||||||
|
|
||||||
j->st.setSampleRate((uint)sample_rate); |
|
||||||
j->st.setChannels((uint)channels); |
|
||||||
j->st.setTempo(kMinTempo); |
|
||||||
j->st.setSetting(SETTING_USE_QUICKSEEK, 1); |
|
||||||
j->st.setSetting(SETTING_SEQUENCE_MS, 20); |
|
||||||
j->st.setSetting(SETTING_SEEKWINDOW_MS, 10); |
|
||||||
j->st.setSetting(SETTING_OVERLAP_MS, 4); |
|
||||||
j->st.setSetting(SETTING_USE_AA_FILTER, 0); |
|
||||||
return j; |
|
||||||
} |
|
||||||
|
|
||||||
void ajb_destroy(struct ajb* j) { |
|
||||||
if (!j) return; |
|
||||||
delete j; |
|
||||||
} |
|
||||||
|
|
||||||
void ajb_reset(struct ajb* j) { |
|
||||||
if (!j) return; |
|
||||||
{ |
|
||||||
std::lock_guard<std::mutex> lk(j->mtx); |
|
||||||
j->head = 0; |
|
||||||
j->count = 0; |
|
||||||
} |
|
||||||
j->st.clear(); |
|
||||||
j->st.setTempo(kMinTempo); |
|
||||||
j->tempo = kMinTempo; |
|
||||||
j->ema_depth_ms = 0.0; |
|
||||||
j->tempo_x100.store(100); |
|
||||||
} |
|
||||||
|
|
||||||
void ajb_push(struct ajb* j, const int16_t* pcm, int samples) { |
|
||||||
if (!j || !pcm || samples != j->frame_samples) return; |
|
||||||
std::lock_guard<std::mutex> lk(j->mtx); |
|
||||||
if (j->count == j->cap) { |
|
||||||
j->head = (j->head + 1) % j->cap; // дроп старейшего
|
|
||||||
j->count--; |
|
||||||
j->dropped++; |
|
||||||
} |
|
||||||
int idx = (j->head + j->count) % j->cap; |
|
||||||
std::memcpy(&j->ring[(size_t)idx * j->frame_samples], pcm, (size_t)samples * sizeof(int16_t)); |
|
||||||
j->count++; |
|
||||||
} |
|
||||||
|
|
||||||
int ajb_pull(struct ajb* j, int16_t* out, int max_samples) { |
|
||||||
if (!j || !out || max_samples <= 0) return 0; |
|
||||||
|
|
||||||
/* 1) глубина (сглаженная) → целевой темп (плавно) */ |
|
||||||
int cnt; |
|
||||||
{ |
|
||||||
std::lock_guard<std::mutex> lk(j->mtx); |
|
||||||
cnt = j->count; |
|
||||||
} |
|
||||||
double depth_ms = (double)cnt * j->frame_ms; |
|
||||||
j->ema_depth_ms = kEmaAlpha * depth_ms + (1.0 - kEmaAlpha) * j->ema_depth_ms; |
|
||||||
double target = ajb_compute_tempo(j->ema_depth_ms); |
|
||||||
double d = target - j->tempo; |
|
||||||
if (d > kTempoStep) d = kTempoStep; |
|
||||||
else if (d < -kTempoStep) d = -kTempoStep; |
|
||||||
j->tempo += d; |
|
||||||
j->st.setTempo(j->tempo); |
|
||||||
j->tempo_x100.store((int)std::lround(j->tempo * 100.0)); |
|
||||||
|
|
||||||
/* 2) кормим SoundTouch, пока он не сможет выдать max_samples (или кольцо пусто) */ |
|
||||||
int guard = 0; |
|
||||||
while (j->st.numSamples() < (uint)max_samples && guard++ < j->cap + 4) { |
|
||||||
if (!ajb_pop_frame(j, j->frame_buf.data())) break; |
|
||||||
for (int i = 0; i < j->frame_samples; i++) |
|
||||||
j->fin[(size_t)i] = (float)j->frame_buf[(size_t)i] / 32768.0f; |
|
||||||
j->st.putSamples(j->fin.data(), (uint)j->frame_samples); |
|
||||||
} |
|
||||||
|
|
||||||
/* 3) забираем готовое */ |
|
||||||
j->fout.resize((size_t)max_samples); |
|
||||||
uint n = j->st.receiveSamples(j->fout.data(), (uint)max_samples); |
|
||||||
for (uint i = 0; i < n; i++) { |
|
||||||
float v = j->fout[i] * 32767.0f; |
|
||||||
if (v > 32767.0f) v = 32767.0f; |
|
||||||
else if (v < -32768.0f) v = -32768.0f; |
|
||||||
out[i] = (int16_t)std::lrintf(v); |
|
||||||
} |
|
||||||
return (int)n; |
|
||||||
} |
|
||||||
|
|
||||||
void ajb_get_stats(struct ajb* j, int* buffer_ms, int* tempo_x100) { |
|
||||||
if (!j) return; |
|
||||||
if (buffer_ms) { |
|
||||||
std::lock_guard<std::mutex> lk(j->mtx); |
|
||||||
*buffer_ms = j->count * j->frame_ms; |
|
||||||
} |
|
||||||
if (tempo_x100) *tempo_x100 = j->tempo_x100.load(); |
|
||||||
} |
|
||||||
|
|
||||||
uint32_t ajb_dropped(struct ajb* j) { |
|
||||||
if (!j) return 0; |
|
||||||
std::lock_guard<std::mutex> lk(j->mtx); |
|
||||||
return j->dropped; |
|
||||||
} |
|
||||||
|
|
||||||
} // extern "C"
|
|
||||||
@ -1,51 +0,0 @@ |
|||||||
#ifndef AUDIO_JITTER_H |
|
||||||
#define AUDIO_JITTER_H |
|
||||||
|
|
||||||
// audio_jitter.h — адаптивный джиттер-буфер с time-stretch (SoundTouch), C API.
|
|
||||||
//
|
|
||||||
// Хранит декодированные PCM-кадры (по frame_samples сэмплов). На pull применяет
|
|
||||||
// tempo: при росте глубины буфера (сетевая пауза → burst) воспроизведение плавно
|
|
||||||
// ускоряется (до 1.6x) и догоняет live, не теряя данные (до max_frames кадров).
|
|
||||||
//
|
|
||||||
// Потоки:
|
|
||||||
// - ajb_push — uasync-поток (producer);
|
|
||||||
// - ajb_pull — аудио-поток (consumer, единственный владелец SoundTouch);
|
|
||||||
// - ajb_get_stats — любой поток.
|
|
||||||
// Кольцо кадров защищено мьютексом (SPSC, дёшево: ~50 кадров/с).
|
|
||||||
|
|
||||||
#include <stdint.h> |
|
||||||
|
|
||||||
#ifdef __cplusplus |
|
||||||
extern "C" { |
|
||||||
#endif |
|
||||||
|
|
||||||
struct ajb; |
|
||||||
|
|
||||||
/* Создать буфер. frame_samples — сэмплов в кадре (960 @20мс/48k), frame_ms —
|
|
||||||
* длительность кадра в мс, max_frames — ёмкость (50 = 1000мс). */ |
|
||||||
struct ajb* ajb_create(int sample_rate, int channels, int frame_samples, int frame_ms, int max_frames); |
|
||||||
|
|
||||||
void ajb_destroy(struct ajb* j); |
|
||||||
|
|
||||||
/* Сброс: очистить кольцо и внутреннее состояние SoundTouch (темп → 1.0). */ |
|
||||||
void ajb_reset(struct ajb* j); |
|
||||||
|
|
||||||
/* producer: положить декодированный PCM-кадр (ровно frame_samples сэмплов).
|
|
||||||
* При переполнении дропает старейший кадр (счётчик внутри). */ |
|
||||||
void ajb_push(struct ajb* j, const int16_t* pcm, int samples); |
|
||||||
|
|
||||||
/* consumer: выдать до max_samples PCM с учётом текущего темпа. Возвращает число
|
|
||||||
* выданных сэмплов; может быть < max_samples при нехватке данных (андерфлоу). */ |
|
||||||
int ajb_pull(struct ajb* j, int16_t* out, int max_samples); |
|
||||||
|
|
||||||
/* Диагностика: текущая глубина буфера (мс) и темп (×100, 100 = 1.0x). */ |
|
||||||
void ajb_get_stats(struct ajb* j, int* buffer_ms, int* tempo_x100); |
|
||||||
|
|
||||||
/* Число дропнутых кадров при переполнении (для диагностики/тестов). */ |
|
||||||
uint32_t ajb_dropped(struct ajb* j); |
|
||||||
|
|
||||||
#ifdef __cplusplus |
|
||||||
} |
|
||||||
#endif |
|
||||||
|
|
||||||
#endif /* AUDIO_JITTER_H */ |
|
||||||
@ -0,0 +1,187 @@ |
|||||||
|
// voice_jitter.cpp — адаптивный джиттер-буфер голоса с time-stretch (SoundTouch).
|
||||||
|
//
|
||||||
|
// Кодированные кадры (Opus, до VJ_MAX_ENC байт) кладутся из uasync-потока в кольцо
|
||||||
|
// и вытаскиваются аудио-потоком: кадр декодируется (decode-коллбэк) и подаётся в
|
||||||
|
// SoundTouch с плавно меняющимся темпом: глубина < target → 1.0x, глубина растёт →
|
||||||
|
// ускорение до 1.6x (догон live). Декодируется только то, что готово к воспроизведению.
|
||||||
|
// Кольцо защищено мьютексом (SPSC); decode и SoundTouch трогает только аудио-поток.
|
||||||
|
|
||||||
|
#include "voice_jitter.h" |
||||||
|
|
||||||
|
#include "soundtouch/SoundTouch.h" |
||||||
|
|
||||||
|
#include <atomic> |
||||||
|
#include <cmath> |
||||||
|
#include <cstring> |
||||||
|
#include <mutex> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
static const int VJ_MAX_ENC = 256; /* макс. размер кодированного кадра (Opus 20мс) */ |
||||||
|
static const double kTargetMs = 60.0; /* целевая глубина буфера */ |
||||||
|
static const double kMaxTempo = 1.6; |
||||||
|
static const double kMinTempo = 1.0; |
||||||
|
static const double kMaxDepthMs = 1000.0; /* при этой глубине tempo == kMaxTempo */ |
||||||
|
static const double kEmaAlpha = 0.2; /* сглаживание глубины */ |
||||||
|
static const double kTempoStep = 0.01; /* макс. изменение темпа за один pull (без кликов) */ |
||||||
|
|
||||||
|
struct vj { |
||||||
|
int sr = 0; |
||||||
|
int ch = 0; |
||||||
|
int frame_samples = 0; |
||||||
|
int frame_ms = 0; |
||||||
|
int cap = 0; |
||||||
|
|
||||||
|
vj_decode_fn decode = nullptr; |
||||||
|
void* decode_arg = nullptr; |
||||||
|
|
||||||
|
std::mutex mtx; |
||||||
|
std::vector<uint8_t> ring; /* cap * VJ_MAX_ENC — кодированные кадры */ |
||||||
|
std::vector<int> lens; /* cap — длины кадров */ |
||||||
|
int head = 0; |
||||||
|
int count = 0; |
||||||
|
uint32_t dropped = 0; |
||||||
|
uint32_t underruns = 0; |
||||||
|
|
||||||
|
soundtouch::SoundTouch st; /* только аудио-поток */ |
||||||
|
uint8_t enc_buf[VJ_MAX_ENC]; /* scratch: вытащенный кодированный кадр */ |
||||||
|
std::vector<int16_t> pcm_buf; /* scratch: декодированный кадр */ |
||||||
|
std::vector<float> fin; /* scratch: кадр в float */ |
||||||
|
std::vector<float> fout; /* scratch: выход float */ |
||||||
|
|
||||||
|
double ema_depth_ms = 0.0; |
||||||
|
double tempo = 1.0; |
||||||
|
std::atomic<int> tempo_x100{100}; |
||||||
|
}; |
||||||
|
|
||||||
|
static double vj_compute_tempo(double depth_ms) { |
||||||
|
if (depth_ms <= kTargetMs) return kMinTempo; |
||||||
|
double x = (depth_ms - kTargetMs) / (kMaxDepthMs - kTargetMs); |
||||||
|
if (x > 1.0) x = 1.0; |
||||||
|
return kMinTempo + (kMaxTempo - kMinTempo) * x; |
||||||
|
} |
||||||
|
|
||||||
|
/* Вытащить один кодированный кадр из кольца (под мьютексом). Возвращает 0 если пусто. */ |
||||||
|
static int vj_pop_enc(struct vj* j, uint8_t* out, int* len_out) { |
||||||
|
std::lock_guard<std::mutex> lk(j->mtx); |
||||||
|
if (j->count == 0) return 0; |
||||||
|
*len_out = j->lens[(size_t)j->head]; |
||||||
|
std::memcpy(out, &j->ring[(size_t)j->head * VJ_MAX_ENC], (size_t)*len_out); |
||||||
|
j->head = (j->head + 1) % j->cap; |
||||||
|
j->count--; |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
extern "C" { |
||||||
|
|
||||||
|
struct vj* vj_create(int sample_rate, int channels, int frame_samples, int frame_ms, |
||||||
|
int max_frames, vj_decode_fn decode, void* decode_arg) { |
||||||
|
if (sample_rate <= 0 || channels <= 0 || frame_samples <= 0 || frame_ms <= 0 || |
||||||
|
max_frames <= 0 || !decode) |
||||||
|
return nullptr; |
||||||
|
|
||||||
|
vj* j = new vj(); |
||||||
|
j->sr = sample_rate; |
||||||
|
j->ch = channels; |
||||||
|
j->frame_samples = frame_samples; |
||||||
|
j->frame_ms = frame_ms; |
||||||
|
j->cap = max_frames; |
||||||
|
j->decode = decode; |
||||||
|
j->decode_arg = decode_arg; |
||||||
|
j->ring.assign((size_t)max_frames * VJ_MAX_ENC, 0); |
||||||
|
j->lens.assign(max_frames, 0); |
||||||
|
j->pcm_buf.assign(frame_samples, 0); |
||||||
|
j->fin.assign(frame_samples, 0.0f); |
||||||
|
|
||||||
|
j->st.setSampleRate((uint)sample_rate); |
||||||
|
j->st.setChannels((uint)channels); |
||||||
|
j->st.setTempo(kMinTempo); |
||||||
|
j->st.setSetting(SETTING_USE_QUICKSEEK, 1); |
||||||
|
j->st.setSetting(SETTING_SEQUENCE_MS, 20); |
||||||
|
j->st.setSetting(SETTING_SEEKWINDOW_MS, 10); |
||||||
|
j->st.setSetting(SETTING_OVERLAP_MS, 4); |
||||||
|
j->st.setSetting(SETTING_USE_AA_FILTER, 0); |
||||||
|
return j; |
||||||
|
} |
||||||
|
|
||||||
|
void vj_destroy(struct vj* j) { |
||||||
|
if (!j) return; |
||||||
|
delete j; |
||||||
|
} |
||||||
|
|
||||||
|
void vj_push(struct vj* j, const uint8_t* enc, int len) { |
||||||
|
if (!j || !enc || len <= 0 || len > VJ_MAX_ENC) return; |
||||||
|
std::lock_guard<std::mutex> lk(j->mtx); |
||||||
|
if (j->count == j->cap) { |
||||||
|
j->head = (j->head + 1) % j->cap; /* дроп старейшего */ |
||||||
|
j->count--; |
||||||
|
j->dropped++; |
||||||
|
} |
||||||
|
int idx = (j->head + j->count) % j->cap; |
||||||
|
std::memcpy(&j->ring[(size_t)idx * VJ_MAX_ENC], enc, (size_t)len); |
||||||
|
j->lens[(size_t)idx] = len; |
||||||
|
j->count++; |
||||||
|
} |
||||||
|
|
||||||
|
int vj_pull(struct vj* j, int16_t* out, int max_samples) { |
||||||
|
if (!j || !out || max_samples <= 0) return 0; |
||||||
|
|
||||||
|
/* 1) глубина (сглаженная) → целевой темп (плавно) */ |
||||||
|
int cnt; |
||||||
|
{ |
||||||
|
std::lock_guard<std::mutex> lk(j->mtx); |
||||||
|
cnt = j->count; |
||||||
|
} |
||||||
|
double depth_ms = (double)cnt * j->frame_ms; |
||||||
|
j->ema_depth_ms = kEmaAlpha * depth_ms + (1.0 - kEmaAlpha) * j->ema_depth_ms; |
||||||
|
double target = vj_compute_tempo(j->ema_depth_ms); |
||||||
|
double d = target - j->tempo; |
||||||
|
if (d > kTempoStep) d = kTempoStep; |
||||||
|
else if (d < -kTempoStep) d = -kTempoStep; |
||||||
|
j->tempo += d; |
||||||
|
j->st.setTempo(j->tempo); |
||||||
|
j->tempo_x100.store((int)std::lround(j->tempo * 100.0)); |
||||||
|
|
||||||
|
/* 2) декодируем и кормим SoundTouch, пока не сможет выдать max_samples (или кольцо пусто) */ |
||||||
|
int guard = 0; |
||||||
|
while (j->st.numSamples() < (uint)max_samples && guard++ < j->cap + 4) { |
||||||
|
int elen = 0; |
||||||
|
if (!vj_pop_enc(j, j->enc_buf, &elen)) break; |
||||||
|
int n = j->decode(j->decode_arg, j->enc_buf, elen, j->pcm_buf.data(), j->frame_samples); |
||||||
|
if (n <= 0) continue; /* битый/пустой кадр — пропускаем */ |
||||||
|
if (n > j->frame_samples) n = j->frame_samples; |
||||||
|
for (int i = 0; i < n; i++) |
||||||
|
j->fin[(size_t)i] = (float)j->pcm_buf[(size_t)i] / 32768.0f; |
||||||
|
j->st.putSamples(j->fin.data(), (uint)n); |
||||||
|
} |
||||||
|
|
||||||
|
/* 3) забираем готовое */ |
||||||
|
j->fout.resize((size_t)max_samples); |
||||||
|
uint n = j->st.receiveSamples(j->fout.data(), (uint)max_samples); |
||||||
|
for (uint i = 0; i < n; i++) { |
||||||
|
float v = j->fout[i] * 32767.0f; |
||||||
|
if (v > 32767.0f) v = 32767.0f; |
||||||
|
else if (v < -32768.0f) v = -32768.0f; |
||||||
|
out[i] = (int16_t)std::lrintf(v); |
||||||
|
} |
||||||
|
if ((int)n < max_samples) j->underruns++; |
||||||
|
return (int)n; |
||||||
|
} |
||||||
|
|
||||||
|
void vj_get_stats(struct vj* j, int* depth_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns) { |
||||||
|
if (!j) return; |
||||||
|
if (depth_ms) { |
||||||
|
std::lock_guard<std::mutex> lk(j->mtx); |
||||||
|
*depth_ms = j->count * j->frame_ms; |
||||||
|
} |
||||||
|
if (tempo_x100) *tempo_x100 = j->tempo_x100.load(); |
||||||
|
if (dropped) *dropped = j->dropped; |
||||||
|
if (underruns) *underruns = j->underruns; |
||||||
|
} |
||||||
|
|
||||||
|
uint32_t vj_dropped(struct vj* j) { |
||||||
|
if (!j) return 0; |
||||||
|
std::lock_guard<std::mutex> lk(j->mtx); |
||||||
|
return j->dropped; |
||||||
|
} |
||||||
|
|
||||||
|
} // extern "C"
|
||||||
@ -0,0 +1,54 @@ |
|||||||
|
#ifndef VOICE_JITTER_H |
||||||
|
#define VOICE_JITTER_H |
||||||
|
|
||||||
|
// voice_jitter.h — адаптивный джиттер-буфер голоса с time-stretch (SoundTouch), C API.
|
||||||
|
//
|
||||||
|
// Хранит КОДИРОВАННЫЕ (Opus) кадры: uasync-поток кладёт кодированные кадры,
|
||||||
|
// аудио-поток на pull декодирует их (через коллбэк decode) и пропускает через
|
||||||
|
// SoundTouch с плавно меняющимся темпом. Излишек голоса держится до декодирования —
|
||||||
|
// декодируется только то, что готово к воспроизведению (экономия памяти ~48x и
|
||||||
|
// разгрузка uasync-потока).
|
||||||
|
//
|
||||||
|
// Потоки:
|
||||||
|
// - vj_push — uasync-поток (producer);
|
||||||
|
// - vj_pull — аудио-поток (consumer; единственный владелец decode/SoundTouch);
|
||||||
|
// - vj_get_stats — любой поток.
|
||||||
|
// Кольцо кодированных кадров защищено мьютексом (SPSC, дёшево: ~50 кадров/с).
|
||||||
|
|
||||||
|
#include <stdint.h> |
||||||
|
|
||||||
|
#ifdef __cplusplus |
||||||
|
extern "C" { |
||||||
|
#endif |
||||||
|
|
||||||
|
struct vj; |
||||||
|
|
||||||
|
/* decode: закодированный кадр → PCM. Возвращает число декодированных сэмплов. */ |
||||||
|
typedef int (*vj_decode_fn)(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples); |
||||||
|
|
||||||
|
/* Создать буфер. frame_samples — сэмплов в кадре (960 @20мс/48k), frame_ms —
|
||||||
|
* длительность кадра в мс, max_frames — ёмкость кодированных кадров (50 = 1000мс). */ |
||||||
|
struct vj* vj_create(int sample_rate, int channels, int frame_samples, int frame_ms, |
||||||
|
int max_frames, vj_decode_fn decode, void* decode_arg); |
||||||
|
|
||||||
|
void vj_destroy(struct vj* j); |
||||||
|
|
||||||
|
/* producer: положить кодированный кадр (копируется). При переполнении дропает старейший. */ |
||||||
|
void vj_push(struct vj* j, const uint8_t* enc, int len); |
||||||
|
|
||||||
|
/* consumer: выдать до max_samples декодированного+растянутого PCM. Может вернуть
|
||||||
|
* < max_samples при нехватке данных (андерфлоу). */ |
||||||
|
int vj_pull(struct vj* j, int16_t* out, int max_samples); |
||||||
|
|
||||||
|
/* Диагностика: глубина буфера (мс), темп (×100, 100 = 1.0x), дропнуто кадров
|
||||||
|
* при переполнении, число underrun-ов (pull вернул меньше запрошенного). */ |
||||||
|
void vj_get_stats(struct vj* j, int* depth_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns); |
||||||
|
|
||||||
|
/* Число дропнутых кадров при переполнении (для диагностики/тестов). */ |
||||||
|
uint32_t vj_dropped(struct vj* j); |
||||||
|
|
||||||
|
#ifdef __cplusplus |
||||||
|
} |
||||||
|
#endif |
||||||
|
|
||||||
|
#endif /* VOICE_JITTER_H */ |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
# voice_sources.cmake — голосовой стек звонка (единый для desktop/Android/headless). |
||||||
|
# |
||||||
|
# SoundTouch (time-stretch) + voice_jitter (кодированные кадры + decode-on-pull) |
||||||
|
# + call_audio (Opus/тоны) + call_tones. C++ внутри, наружу — C-интерфейс |
||||||
|
# (call_audio.h). Экспортирует UTUN_VOICE_SOURCES и UTUN_VOICE_INCLUDE_DIRS. |
||||||
|
# |
||||||
|
# Использование: |
||||||
|
# set(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../lib) |
||||||
|
# set(CALL_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../src/call) |
||||||
|
# include(${CALL_DIR}/voice_sources.cmake) |
||||||
|
# utun_setup_voice("${LIB_DIR}" "${CALL_DIR}") |
||||||
|
# add_library(utun_voice STATIC ${UTUN_VOICE_SOURCES}) |
||||||
|
# target_include_directories(utun_voice PUBLIC ${UTUN_VOICE_INCLUDE_DIRS}) |
||||||
|
# target_link_libraries(utun_voice PUBLIC opus) # opus_codec |
||||||
|
|
||||||
|
function(utun_setup_voice LIB_DIR CALL_DIR) |
||||||
|
set(_st_dir "${LIB_DIR}/soundtouch-master") |
||||||
|
set(_src_dir "${CALL_DIR}/..") |
||||||
|
set(UTUN_VOICE_SOURCES |
||||||
|
${CALL_DIR}/call_audio.c |
||||||
|
${CALL_DIR}/call_tones.c |
||||||
|
${CALL_DIR}/voice_jitter.cpp |
||||||
|
${_st_dir}/source/SoundTouch/SoundTouch.cpp |
||||||
|
${_st_dir}/source/SoundTouch/TDStretch.cpp |
||||||
|
${_st_dir}/source/SoundTouch/RateTransposer.cpp |
||||||
|
${_st_dir}/source/SoundTouch/FIFOSampleBuffer.cpp |
||||||
|
${_st_dir}/source/SoundTouch/AAFilter.cpp |
||||||
|
${_st_dir}/source/SoundTouch/FIRFilter.cpp |
||||||
|
${_st_dir}/source/SoundTouch/InterpolateLinear.cpp |
||||||
|
${_st_dir}/source/SoundTouch/InterpolateCubic.cpp |
||||||
|
${_st_dir}/source/SoundTouch/InterpolateShannon.cpp |
||||||
|
${_st_dir}/source/SoundTouch/cpu_detect_x86.cpp |
||||||
|
${_st_dir}/source/SoundTouch/mmx_optimized.cpp |
||||||
|
${_st_dir}/source/SoundTouch/sse_optimized.cpp |
||||||
|
PARENT_SCOPE |
||||||
|
) |
||||||
|
set(UTUN_VOICE_INCLUDE_DIRS |
||||||
|
${CALL_DIR} |
||||||
|
${_src_dir} |
||||||
|
${_src_dir}/transport_layer |
||||||
|
${_src_dir}/routing_layer |
||||||
|
${_src_dir}/chat |
||||||
|
${_src_dir}/proxy |
||||||
|
${_src_dir}/media_delivery |
||||||
|
${_src_dir}/media_async |
||||||
|
${LIB_DIR} |
||||||
|
${LIB_DIR}/libopus/include |
||||||
|
${_st_dir}/include |
||||||
|
${_st_dir}/include/soundtouch |
||||||
|
PARENT_SCOPE |
||||||
|
) |
||||||
|
endfunction() |
||||||
@ -1,156 +0,0 @@ |
|||||||
#include "call_audio.h" |
|
||||||
#include "instance_lite.h" |
|
||||||
#include "../../../lib/opus_codec.h" |
|
||||||
#include "../../../lib/debug_config.h" |
|
||||||
#include "../../../lib/mem.h" |
|
||||||
#include "../../../lib/u_async.h" |
|
||||||
#include "../../../src/call/call.h" |
|
||||||
#include <string.h> |
|
||||||
#include <pthread.h> |
|
||||||
|
|
||||||
#define CALL_AUDIO_BITRATE 32000 |
|
||||||
|
|
||||||
static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER; |
|
||||||
static int g_active = 0; |
|
||||||
static uint64_t g_call_id = 0; |
|
||||||
static opus_codec_encoder_t* g_encoder = NULL; |
|
||||||
static opus_codec_decoder_t* g_decoder = NULL; |
|
||||||
static call_audio_pcm_recv_fn g_pcm_fn = NULL; |
|
||||||
static void* g_pcm_arg = NULL; |
|
||||||
|
|
||||||
void call_audio_set_pcm_recv_cb(call_audio_pcm_recv_fn fn, void* arg) { |
|
||||||
pthread_mutex_lock(&g_mtx); |
|
||||||
g_pcm_fn = fn; |
|
||||||
g_pcm_arg = arg; |
|
||||||
pthread_mutex_unlock(&g_mtx); |
|
||||||
} |
|
||||||
|
|
||||||
/* uasync-поток: принятый Opus-кадр пира (из call.c media_recv). */ |
|
||||||
static void call_audio_media_recv(struct UTUN_INSTANCE* inst, uint64_t call_id, |
|
||||||
const uint8_t* opus, int len, void* arg) { |
|
||||||
(void)inst; (void)arg; |
|
||||||
int16_t pcm[CALL_AUDIO_FRAME_SAMPLES]; |
|
||||||
call_audio_pcm_recv_fn fn = NULL; |
|
||||||
void* fn_arg = NULL; |
|
||||||
int n = 0; |
|
||||||
|
|
||||||
pthread_mutex_lock(&g_mtx); |
|
||||||
if (g_active && call_id == g_call_id && g_decoder && opus && len > 0) { |
|
||||||
n = opus_codec_decode(g_decoder, opus, len, pcm, CALL_AUDIO_FRAME_SAMPLES); |
|
||||||
fn = g_pcm_fn; |
|
||||||
fn_arg = g_pcm_arg; |
|
||||||
} |
|
||||||
pthread_mutex_unlock(&g_mtx); |
|
||||||
|
|
||||||
if (n > 0 && fn) |
|
||||||
fn(call_id, pcm, n, fn_arg); |
|
||||||
} |
|
||||||
|
|
||||||
int call_audio_start(uint64_t call_id) { |
|
||||||
if (call_id == 0) { |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_audio_start: bad call_id=0"); |
|
||||||
return -1; |
|
||||||
} |
|
||||||
|
|
||||||
pthread_mutex_lock(&g_mtx); |
|
||||||
if (g_active && g_call_id == call_id) { |
|
||||||
pthread_mutex_unlock(&g_mtx); |
|
||||||
return 0; /* уже запущен для этого звонка */ |
|
||||||
} |
|
||||||
if (g_active) { |
|
||||||
/* другой звонок активен — сбрасываем его */ |
|
||||||
pthread_mutex_unlock(&g_mtx); |
|
||||||
call_audio_stop(); |
|
||||||
pthread_mutex_lock(&g_mtx); |
|
||||||
} |
|
||||||
|
|
||||||
opus_codec_encoder_t* enc = opus_codec_encoder_create(CALL_AUDIO_SAMPLE_RATE, 1); |
|
||||||
if (!enc) { |
|
||||||
pthread_mutex_unlock(&g_mtx); |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_audio_start: encoder create failed"); |
|
||||||
return -1; |
|
||||||
} |
|
||||||
opus_codec_encoder_bitrate_set(enc, CALL_AUDIO_BITRATE); |
|
||||||
|
|
||||||
opus_codec_decoder_t* dec = opus_codec_decoder_create(CALL_AUDIO_SAMPLE_RATE, 1); |
|
||||||
if (!dec) { |
|
||||||
opus_codec_encoder_destroy(enc); |
|
||||||
pthread_mutex_unlock(&g_mtx); |
|
||||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_audio_start: decoder create failed"); |
|
||||||
return -1; |
|
||||||
} |
|
||||||
|
|
||||||
g_encoder = enc; |
|
||||||
g_decoder = dec; |
|
||||||
g_call_id = call_id; |
|
||||||
g_active = 1; |
|
||||||
pthread_mutex_unlock(&g_mtx); |
|
||||||
|
|
||||||
call_set_media_recv_cb(instance_lite_get_instance(), call_audio_media_recv, NULL); |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_audio_start: ok call=0x%016llx", |
|
||||||
(unsigned long long)call_id); |
|
||||||
return 0; |
|
||||||
} |
|
||||||
|
|
||||||
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) { |
|
||||||
if (!pcm || count != CALL_AUDIO_FRAME_SAMPLES) |
|
||||||
return -1; |
|
||||||
|
|
||||||
uint8_t opus[256]; |
|
||||||
struct UASYNC* ua = NULL; |
|
||||||
struct UTUN_INSTANCE* inst = NULL; |
|
||||||
int len = -1; |
|
||||||
|
|
||||||
pthread_mutex_lock(&g_mtx); |
|
||||||
if (!g_active || call_id != g_call_id || !g_encoder) { |
|
||||||
pthread_mutex_unlock(&g_mtx); |
|
||||||
return -1; |
|
||||||
} |
|
||||||
len = opus_codec_encode(g_encoder, pcm, count, opus, (int)sizeof(opus)); |
|
||||||
ua = instance_lite_get_uasync(); |
|
||||||
inst = instance_lite_get_instance(); |
|
||||||
pthread_mutex_unlock(&g_mtx); |
|
||||||
|
|
||||||
if (len <= 0 || !ua || !uasync_is_running(ua) || !inst) |
|
||||||
return -1; |
|
||||||
|
|
||||||
struct call_media_arg* a = (struct call_media_arg*)u_malloc(sizeof(*a)); |
|
||||||
if (!a) return -1; |
|
||||||
a->inst = inst; |
|
||||||
a->call_id = call_id; |
|
||||||
a->len = (uint16_t)len; |
|
||||||
memcpy(a->opus, opus, (size_t)len); |
|
||||||
uasync_post(ua, call_send_media_trampoline, a); |
|
||||||
return 0; |
|
||||||
} |
|
||||||
|
|
||||||
void call_audio_stop(void) { |
|
||||||
opus_codec_encoder_t* enc = NULL; |
|
||||||
opus_codec_decoder_t* dec = NULL; |
|
||||||
int was_active = 0; |
|
||||||
|
|
||||||
pthread_mutex_lock(&g_mtx); |
|
||||||
if (g_active) { |
|
||||||
was_active = 1; |
|
||||||
g_active = 0; |
|
||||||
g_call_id = 0; |
|
||||||
enc = g_encoder; g_encoder = NULL; |
|
||||||
dec = g_decoder; g_decoder = NULL; |
|
||||||
} |
|
||||||
pthread_mutex_unlock(&g_mtx); |
|
||||||
|
|
||||||
if (!was_active) |
|
||||||
return; |
|
||||||
|
|
||||||
call_set_media_recv_cb(instance_lite_get_instance(), NULL, NULL); |
|
||||||
if (enc) opus_codec_encoder_destroy(enc); |
|
||||||
if (dec) opus_codec_decoder_destroy(dec); |
|
||||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "call_audio_stop"); |
|
||||||
} |
|
||||||
|
|
||||||
int call_audio_is_active(uint64_t call_id) { |
|
||||||
pthread_mutex_lock(&g_mtx); |
|
||||||
int active = g_active && call_id == g_call_id; |
|
||||||
pthread_mutex_unlock(&g_mtx); |
|
||||||
return active; |
|
||||||
} |
|
||||||
@ -1,52 +0,0 @@ |
|||||||
#ifndef CALL_AUDIO_H |
|
||||||
#define CALL_AUDIO_H |
|
||||||
|
|
||||||
/* call_audio.h — аудио-движок P2P-звонка для Android (аналог десктопного CallAudioEngine)
|
|
||||||
* |
|
||||||
* Держит один активный звонок: Opus encoder (PCM→Opus, отправка пиру) и |
|
||||||
* decoder (Opus→PCM, приём от пира). Аудио I/O живёт в Kotlin (AudioRecord/ |
|
||||||
* AudioTrack), кодеки — здесь (Opus есть только в C). |
|
||||||
* |
|
||||||
* Потоки: |
|
||||||
* - feed_pcm: из AudioRecord-потока Kotlin (кодирование + uasync_post) |
|
||||||
* - media_recv: из uasync-потока (call.c) — декодирование + коллбэк PCM |
|
||||||
* - start/stop: из GUI-потока Kotlin (по ACCEPTED/ENDED) |
|
||||||
* Все общие поля защищены мьютексом; encoder/decoder используются строго |
|
||||||
* одним потоком каждый, но освобождаются в stop под тем же мьютексом. */ |
|
||||||
#include <stdint.h> |
|
||||||
|
|
||||||
#ifdef __cplusplus |
|
||||||
extern "C" { |
|
||||||
#endif |
|
||||||
|
|
||||||
/* Параметры аудио: Opus 48kHz mono, кадр 20мс = 960 сэмплов. */ |
|
||||||
#define CALL_AUDIO_SAMPLE_RATE 48000 |
|
||||||
#define CALL_AUDIO_FRAME_MS 20 |
|
||||||
#define CALL_AUDIO_FRAME_SAMPLES (CALL_AUDIO_SAMPLE_RATE * CALL_AUDIO_FRAME_MS / 1000) |
|
||||||
|
|
||||||
/* Запустить полнодуплексное аудио для активного звонка (после ACCEPTED).
|
|
||||||
* Создаёт encoder+decoder, регистрирует приём медиа в call.c. |
|
||||||
* Повторный start с тем же call_id — no-op; с другим — сперва stop. |
|
||||||
* Возвращает 0 при успехе, <0 при ошибке. */ |
|
||||||
int call_audio_start(uint64_t call_id); |
|
||||||
|
|
||||||
/* Отправить один кадр PCM (ровно CALL_AUDIO_FRAME_SAMPLES) пиру.
|
|
||||||
* Кодируется в Opus и уходит через call_send_media (uasync-пост). |
|
||||||
* Можно звать из любого потока. 0 — успех, <0 — звонок не активен/ошибка. */ |
|
||||||
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count); |
|
||||||
|
|
||||||
/* Остановить аудио: снять recv-cb, освободить encoder/decoder. */ |
|
||||||
void call_audio_stop(void); |
|
||||||
|
|
||||||
/* Активен ли звонок с данным call_id (для логов/диагностики). */ |
|
||||||
int call_audio_is_active(uint64_t call_id); |
|
||||||
|
|
||||||
/* Коллбэк приёма декодированного PCM пира (960 сэмплов/кадр).
|
|
||||||
* Вызывается в uasync-потоке на каждый принятый Opus-кадр. */ |
|
||||||
typedef void (*call_audio_pcm_recv_fn)(uint64_t call_id, const int16_t* pcm, int count, void* arg); |
|
||||||
void call_audio_set_pcm_recv_cb(call_audio_pcm_recv_fn fn, void* arg); |
|
||||||
|
|
||||||
#ifdef __cplusplus |
|
||||||
} |
|
||||||
#endif |
|
||||||
#endif /* CALL_AUDIO_H */ |
|
||||||
@ -1,101 +0,0 @@ |
|||||||
// test_audio_jitter.cpp — проверка адаптивного джиттер-буфера (SoundTouch).
|
|
||||||
//
|
|
||||||
// 1. Нет потерь до ёмкости (50 кадров = 1000мс), дроп после переполнения.
|
|
||||||
// 2. Догон: глубокий буфер → темп растёт выше 1.0x (ускоренное воспроизведение).
|
|
||||||
// 3. Стационар: при неглубоком буфере темп остаётся ~1.0x (без runaway).
|
|
||||||
|
|
||||||
#include "call/audio_jitter.h" |
|
||||||
|
|
||||||
#include <cmath> |
|
||||||
#include <cstdio> |
|
||||||
#include <cstring> |
|
||||||
#include <vector> |
|
||||||
|
|
||||||
static int g_fail = 0; |
|
||||||
|
|
||||||
#define CHECK(cond, ...) do { \ |
|
||||||
if (!(cond)) { g_fail++; printf("FAIL: " __VA_ARGS__); printf("\n"); } \
|
|
||||||
} while (0) |
|
||||||
|
|
||||||
static void make_sine(std::vector<int16_t>& frame, double freq, double phase0) { |
|
||||||
for (size_t i = 0; i < frame.size(); i++) |
|
||||||
frame[i] = (int16_t)(sin(phase0 + 2.0 * M_PI * freq * (double)i / 48000.0) * 8000.0); |
|
||||||
} |
|
||||||
|
|
||||||
int main(void) { |
|
||||||
const int FRAME = 960; |
|
||||||
const int CAP = 50; |
|
||||||
std::vector<int16_t> frame(FRAME, 0); |
|
||||||
std::vector<int16_t> out(480, 0); |
|
||||||
int buffer_ms = 0, tempo_x100 = 0; |
|
||||||
|
|
||||||
/* ── 1. ёмкость и дроп ── */ |
|
||||||
{ |
|
||||||
ajb* j = ajb_create(48000, 1, FRAME, 20, CAP); |
|
||||||
CHECK(j != nullptr, "create failed"); |
|
||||||
|
|
||||||
make_sine(frame, 440.0, 0.0); |
|
||||||
for (int i = 0; i < CAP; i++) ajb_push(j, frame.data(), FRAME); |
|
||||||
ajb_get_stats(j, &buffer_ms, &tempo_x100); |
|
||||||
printf(" capacity: pushed %d frames, buffer_ms=%d dropped=%u\n", CAP, buffer_ms, (unsigned)ajb_dropped(j)); |
|
||||||
CHECK(ajb_dropped(j) == 0, "expected no drop at capacity, dropped=%u", (unsigned)ajb_dropped(j)); |
|
||||||
CHECK(buffer_ms == CAP * 20, "expected buffer_ms=%d, got %d", CAP * 20, buffer_ms); |
|
||||||
|
|
||||||
for (int i = 0; i < 5; i++) ajb_push(j, frame.data(), FRAME); |
|
||||||
printf(" overflow: dropped=%u (expect 5)\n", (unsigned)ajb_dropped(j)); |
|
||||||
CHECK(ajb_dropped(j) == 5, "expected 5 drops, got %u", (unsigned)ajb_dropped(j)); |
|
||||||
ajb_destroy(j); |
|
||||||
} |
|
||||||
|
|
||||||
/* ── 2. догон: глубокий буфер → tempo > 1.0x ── */ |
|
||||||
{ |
|
||||||
ajb* j = ajb_create(48000, 1, FRAME, 20, CAP); |
|
||||||
make_sine(frame, 440.0, 0.0); |
|
||||||
for (int i = 0; i < 40; i++) ajb_push(j, frame.data(), FRAME); /* 800мс burst */ |
|
||||||
|
|
||||||
int max_tempo = 100; |
|
||||||
int total_out = 0; |
|
||||||
for (int it = 0; it < 200; it++) { |
|
||||||
ajb_get_stats(j, &buffer_ms, &tempo_x100); |
|
||||||
if (buffer_ms == 0) break; |
|
||||||
if (tempo_x100 > max_tempo) max_tempo = tempo_x100; |
|
||||||
int n = ajb_pull(j, out.data(), (int)out.size()); |
|
||||||
total_out += n; |
|
||||||
} |
|
||||||
ajb_get_stats(j, &buffer_ms, &tempo_x100); |
|
||||||
printf(" catch-up: max_tempo_x100=%d total_out=%d final_buffer_ms=%d\n", |
|
||||||
max_tempo, total_out, buffer_ms); |
|
||||||
CHECK(max_tempo > 110, "expected tempo to exceed 1.10x during deep buffer, got %d", max_tempo); |
|
||||||
CHECK(total_out < 40 * FRAME, "expected output < input (stretch), out=%d in=%d", total_out, 40 * FRAME); |
|
||||||
CHECK(buffer_ms == 0, "expected drained buffer, buffer_ms=%d", buffer_ms); |
|
||||||
ajb_destroy(j); |
|
||||||
} |
|
||||||
|
|
||||||
/* ── 3. стационар: feed 1 frame → pull 1 frame, tempo ~1.0x ── */ |
|
||||||
{ |
|
||||||
ajb* j = ajb_create(48000, 1, FRAME, 20, CAP); |
|
||||||
make_sine(frame, 440.0, 0.0); |
|
||||||
int max_tempo = 100; |
|
||||||
for (int i = 0; i < 60; i++) { |
|
||||||
ajb_push(j, frame.data(), FRAME); |
|
||||||
int got = 0; |
|
||||||
while (got < FRAME) { |
|
||||||
int n = ajb_pull(j, out.data(), (int)out.size()); |
|
||||||
if (n <= 0) break; |
|
||||||
got += n; |
|
||||||
} |
|
||||||
ajb_get_stats(j, &buffer_ms, &tempo_x100); |
|
||||||
if (tempo_x100 > max_tempo) max_tempo = tempo_x100; |
|
||||||
} |
|
||||||
printf(" steady-state: max_tempo_x100=%d (expect ~100)\n", max_tempo); |
|
||||||
CHECK(max_tempo <= 110, "tempo should stay ~1.0x in steady state, got %d", max_tempo); |
|
||||||
ajb_destroy(j); |
|
||||||
} |
|
||||||
|
|
||||||
if (g_fail == 0) { |
|
||||||
printf("TEST PASSED\n"); |
|
||||||
return 0; |
|
||||||
} |
|
||||||
printf("TEST FAILED (%d checks)\n", g_fail); |
|
||||||
return 1; |
|
||||||
} |
|
||||||
@ -0,0 +1,103 @@ |
|||||||
|
// test_voice_jitter.cpp — проверка адаптивного джиттер-буфера голоса (SoundTouch).
|
||||||
|
//
|
||||||
|
// Декодер подменяется генератором синуса (кодированный кадр — маленький маркер),
|
||||||
|
// чтобы проверить буфер без Opus: 1) ёмкость/дроп, 2) догон (глубокий буфер →
|
||||||
|
// темп > 1.0x), 3) стационар (темп ~1.0x без runaway).
|
||||||
|
|
||||||
|
#include "call/voice_jitter.h" |
||||||
|
|
||||||
|
#include <cmath> |
||||||
|
#include <cstdio> |
||||||
|
#include <cstring> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
static int g_fail = 0; |
||||||
|
|
||||||
|
#define CHECK(cond, ...) do { \ |
||||||
|
if (!(cond)) { g_fail++; printf("FAIL: " __VA_ARGS__); printf("\n"); } \
|
||||||
|
} while (0) |
||||||
|
|
||||||
|
/* фейковый декодер: генерирует кадр синуса (960 сэмплов) из любого «кодированного» маркера. */ |
||||||
|
static int sine_decode(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples) { |
||||||
|
(void)arg; (void)enc; (void)len; |
||||||
|
for (int i = 0; i < max_samples; i++) |
||||||
|
pcm[i] = (int16_t)(sin(2.0 * M_PI * 440.0 * (double)i / 48000.0) * 8000.0); |
||||||
|
return max_samples; |
||||||
|
} |
||||||
|
|
||||||
|
int main(void) { |
||||||
|
const int FRAME = 960; |
||||||
|
const int CAP = 50; |
||||||
|
uint8_t marker[8] = {0}; |
||||||
|
std::vector<int16_t> out(480, 0); |
||||||
|
int buffer_ms = 0, tempo_x100 = 0; |
||||||
|
uint32_t dropped = 0, underruns = 0; |
||||||
|
|
||||||
|
/* ── 1. ёмкость и дроп ── */ |
||||||
|
{ |
||||||
|
vj* j = vj_create(48000, 1, FRAME, 20, CAP, sine_decode, nullptr); |
||||||
|
CHECK(j != nullptr, "create failed"); |
||||||
|
|
||||||
|
for (int i = 0; i < CAP; i++) vj_push(j, marker, (int)sizeof(marker)); |
||||||
|
vj_get_stats(j, &buffer_ms, &tempo_x100, &dropped, &underruns); |
||||||
|
printf(" capacity: pushed %d frames, buffer_ms=%d dropped=%u\n", CAP, buffer_ms, (unsigned)dropped); |
||||||
|
CHECK(dropped == 0, "expected no drop at capacity, dropped=%u", (unsigned)dropped); |
||||||
|
CHECK(buffer_ms == CAP * 20, "expected buffer_ms=%d, got %d", CAP * 20, buffer_ms); |
||||||
|
|
||||||
|
for (int i = 0; i < 5; i++) vj_push(j, marker, (int)sizeof(marker)); |
||||||
|
vj_get_stats(j, &buffer_ms, &tempo_x100, &dropped, &underruns); |
||||||
|
printf(" overflow: dropped=%u (expect 5)\n", (unsigned)dropped); |
||||||
|
CHECK(dropped == 5, "expected 5 drops, got %u", (unsigned)dropped); |
||||||
|
vj_destroy(j); |
||||||
|
} |
||||||
|
|
||||||
|
/* ── 2. догон: глубокий буфер → tempo > 1.0x ── */ |
||||||
|
{ |
||||||
|
vj* j = vj_create(48000, 1, FRAME, 20, CAP, sine_decode, nullptr); |
||||||
|
for (int i = 0; i < 40; i++) vj_push(j, marker, (int)sizeof(marker)); /* 800мс burst */ |
||||||
|
|
||||||
|
int max_tempo = 100; |
||||||
|
int total_out = 0; |
||||||
|
for (int it = 0; it < 200; it++) { |
||||||
|
vj_get_stats(j, &buffer_ms, &tempo_x100, &dropped, &underruns); |
||||||
|
if (buffer_ms == 0) break; |
||||||
|
if (tempo_x100 > max_tempo) max_tempo = tempo_x100; |
||||||
|
int n = vj_pull(j, out.data(), (int)out.size()); |
||||||
|
total_out += n; |
||||||
|
} |
||||||
|
vj_get_stats(j, &buffer_ms, &tempo_x100, &dropped, &underruns); |
||||||
|
printf(" catch-up: max_tempo_x100=%d total_out=%d final_buffer_ms=%d\n", |
||||||
|
max_tempo, total_out, buffer_ms); |
||||||
|
CHECK(max_tempo > 110, "expected tempo to exceed 1.10x during deep buffer, got %d", max_tempo); |
||||||
|
CHECK(total_out < 40 * FRAME, "expected output < input (stretch), out=%d in=%d", total_out, 40 * FRAME); |
||||||
|
CHECK(buffer_ms == 0, "expected drained buffer, buffer_ms=%d", buffer_ms); |
||||||
|
vj_destroy(j); |
||||||
|
} |
||||||
|
|
||||||
|
/* ── 3. стационар: feed 1 frame → pull 1 frame, tempo ~1.0x ── */ |
||||||
|
{ |
||||||
|
vj* j = vj_create(48000, 1, FRAME, 20, CAP, sine_decode, nullptr); |
||||||
|
int max_tempo = 100; |
||||||
|
for (int i = 0; i < 60; i++) { |
||||||
|
vj_push(j, marker, (int)sizeof(marker)); |
||||||
|
int got = 0; |
||||||
|
while (got < FRAME) { |
||||||
|
int n = vj_pull(j, out.data(), (int)out.size()); |
||||||
|
if (n <= 0) break; |
||||||
|
got += n; |
||||||
|
} |
||||||
|
vj_get_stats(j, &buffer_ms, &tempo_x100, &dropped, &underruns); |
||||||
|
if (tempo_x100 > max_tempo) max_tempo = tempo_x100; |
||||||
|
} |
||||||
|
printf(" steady-state: max_tempo_x100=%d (expect ~100)\n", max_tempo); |
||||||
|
CHECK(max_tempo <= 110, "tempo should stay ~1.0x in steady state, got %d", max_tempo); |
||||||
|
vj_destroy(j); |
||||||
|
} |
||||||
|
|
||||||
|
if (g_fail == 0) { |
||||||
|
printf("TEST PASSED\n"); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
printf("TEST FAILED (%d checks)\n", g_fail); |
||||||
|
return 1; |
||||||
|
} |
||||||
Loading…
Reference in new issue