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