You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

314 lines
14 KiB

// radio_jitter.cpp — адаптивный джиттер-буфер голоса с time-stretch (SoundTouch).
//
// Кодированные кадры (Opus, до RJ_MAX_ENC байт) кладутся из uasync-потока в кольцо
// и вытаскиваются аудио-потоком: кадр декодируется (decode-коллбэк) и подаётся в
// SoundTouch с плавно меняющимся темпом вокруг целевой глубины: мельче target — темп
// падает к min_tempo (накопление), глубже — растёт к max_tempo (догон live). При
// pre_roll=1 звук не отдаётся, пока глубина не достигнет target_ms. Декодируется
// только то, что готово к воспроизведению.
// Кольцо защищено мьютексом (SPSC); decode и SoundTouch трогает только аудио-поток.
#include "radio_jitter.h"
#include "soundtouch/SoundTouch.h"
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstring>
#include <mutex>
#include <vector>
static const int RJ_MAX_ENC = 512; /* макс. размер кодированного кадра (Opus 20мс, стерео) */
static const double kDefaultTargetMs = 60.0; /* целевая глубина буфера по умолчанию */
static const double kDefaultMinTempo = 1.0;
static const double kDefaultMaxTempo = 1.6;
static const double kMaxDepthMs = 1000.0; /* при этой глубине tempo == max_tempo */
static const double kEmaAlpha = 0.2; /* сглаживание глубины */
static const double kTempoStep = 0.01; /* макс. изменение темпа за один pull (без кликов) */
static const double kPrimeTimeoutMs = 200.0; /* pre-roll: принудительный prime через это время */
static const double kStallGapMs = 80.0; /* gap между pull > это → сталь аудио-девайса */
static const double kResyncDepthMs = 400.0; /* бэклог > это после сталла → хард-ресинк к live */
struct rj {
int sr = 0;
int ch = 0;
int frame_samples = 0;
int frame_ms = 0;
int cap = 0;
rj_decode_fn decode = nullptr;
void* decode_arg = nullptr;
std::mutex mtx;
std::vector<uint8_t> ring; /* cap * RJ_MAX_ENC — кодированные кадры */
std::vector<int> lens; /* cap — длины кадров */
int head = 0;
int count = 0;
uint32_t dropped = 0;
std::atomic<uint32_t> underruns{0};
std::atomic<uint32_t> resyncs{0};
std::atomic<int> processing_ms{0};
/* детект сталла аудио-девайса (gap между pull) */
std::chrono::steady_clock::time_point last_pull_tp; /* только аудио-поток */
int first_pull = 1; /* только аудио-поток */
int stalled = 0; /* только аудио-поток */
soundtouch::SoundTouch st; /* только аудио-поток */
uint8_t enc_buf[RJ_MAX_ENC]; /* scratch: вытащенный кодированный кадр */
std::vector<int16_t> pcm_buf; /* scratch: декодированный кадр */
std::vector<float> fin; /* scratch: кадр в float */
std::vector<float> fout; /* scratch: выход float */
double target_ms = kDefaultTargetMs;
double min_tempo = kDefaultMinTempo;
double max_tempo = kDefaultMaxTempo;
int pre_roll = 0;
int primed = 0; /* только аудио-поток */
int prime_started = 0; /* только аудио-поток: первый непустой pull */
std::chrono::steady_clock::time_point prime_t0; /* только аудио-поток: старт pre-roll */
double ema_depth_ms = 0.0;
double tempo = 1.0;
std::atomic<int> tempo_x100{100};
};
/* Симметричный целевой темп с мёртвой зоной ±25% вокруг target: внутри зоны темп=1.0
* (без time-stretch, меньше артефактов), за её пределами — плавно к min/max. */
static double rj_compute_tempo(double depth_ms, double target_ms, double min_tempo, double max_tempo) {
double dead = target_ms * 0.25;
double lo = target_ms - dead;
double hi = target_ms + dead;
if (depth_ms >= lo && depth_ms <= hi) return 1.0;
if (depth_ms < lo) {
double x = depth_ms / lo; /* 0 → min, lo → 1.0 */
if (x < 0.0) x = 0.0;
return min_tempo + (1.0 - min_tempo) * x;
}
double x = (depth_ms - hi) / (kMaxDepthMs - hi); /* hi → 1.0, kMaxDepth → max */
if (x > 1.0) x = 1.0;
return 1.0 + (max_tempo - 1.0) * x;
}
/* Вытащить один кодированный кадр из кольца (под мьютексом). Возвращает 0 если пусто. */
static int rj_pop_enc(struct rj* 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 * RJ_MAX_ENC], (size_t)*len_out);
j->head = (j->head + 1) % j->cap;
j->count--;
return 1;
}
extern "C" {
struct rj* rj_create_cfg(const struct rj_cfg* cfg) {
if (!cfg || cfg->sample_rate <= 0 || cfg->channels <= 0 || cfg->frame_samples <= 0 ||
cfg->frame_ms <= 0 || cfg->max_frames <= 0 || !cfg->decode)
return nullptr;
if (cfg->min_tempo <= 0.0 || cfg->max_tempo < cfg->min_tempo || cfg->target_ms <= 0.0)
return nullptr;
rj* j = new rj();
j->sr = cfg->sample_rate;
j->ch = cfg->channels;
j->frame_samples = cfg->frame_samples;
j->frame_ms = cfg->frame_ms;
j->cap = cfg->max_frames;
j->decode = cfg->decode;
j->decode_arg = cfg->decode_arg;
j->target_ms = cfg->target_ms;
j->min_tempo = cfg->min_tempo;
j->max_tempo = cfg->max_tempo;
j->pre_roll = cfg->pre_roll;
j->ring.assign((size_t)cfg->max_frames * RJ_MAX_ENC, 0);
j->lens.assign(cfg->max_frames, 0);
j->pcm_buf.assign((size_t)cfg->frame_samples * cfg->channels, 0);
j->fin.assign((size_t)cfg->frame_samples * cfg->channels, 0.0f);
j->st.setSampleRate((uint)cfg->sample_rate);
j->st.setChannels((uint)cfg->channels);
j->st.setTempo(cfg->min_tempo);
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;
}
struct rj* rj_create(int sample_rate, int channels, int frame_samples, int frame_ms,
int max_frames, rj_decode_fn decode, void* decode_arg) {
struct rj_cfg cfg;
cfg.sample_rate = sample_rate;
cfg.channels = channels;
cfg.frame_samples = frame_samples;
cfg.frame_ms = frame_ms;
cfg.max_frames = max_frames;
cfg.target_ms = kDefaultTargetMs;
cfg.min_tempo = kDefaultMinTempo;
cfg.max_tempo = kDefaultMaxTempo;
cfg.pre_roll = 0;
cfg.decode = decode;
cfg.decode_arg = decode_arg;
return rj_create_cfg(&cfg);
}
void rj_destroy(struct rj* j) {
if (!j) return;
delete j;
}
void rj_push(struct rj* j, const uint8_t* enc, int len) {
if (!j || !enc || len <= 0 || len > RJ_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 * RJ_MAX_ENC], enc, (size_t)len);
j->lens[(size_t)idx] = len;
j->count++;
}
/* Хард-ресинк к live: сбросить бэклог (кольцо + SoundTouch) и темп к 1.0.
* Применяется после сталла аудио-девайса, когда бэклог > kResyncDepthMs — один
* чистый срез вместо долгого догона темпом (chipmunk) + дропов по одному. */
static void rj_hard_resync(struct rj* j) {
{ std::lock_guard<std::mutex> lk(j->mtx); j->count = 0; j->head = 0; }
j->st.clear();
j->processing_ms.store(0);
j->tempo = 1.0;
j->tempo_x100.store(100);
j->ema_depth_ms = j->target_ms;
j->resyncs++;
}
int rj_pull(struct rj* j, int16_t* out, int max_samples) {
if (!j || !out || max_samples <= 0) return 0;
/* ── детект сталла аудио-девайса (gap между pull) + ресинк по его окончании ── */
{
auto now = std::chrono::steady_clock::now();
if (j->first_pull) {
j->first_pull = 0;
j->last_pull_tp = now;
} else {
double gap_ms = std::chrono::duration<double, std::milli>(now - j->last_pull_tp).count();
j->last_pull_tp = now;
if (gap_ms > kStallGapMs) {
j->stalled = 1; /* сталь начался — просто флаг */
} else if (j->stalled) {
j->stalled = 0; /* сталь кончился */
int c;
{ std::lock_guard<std::mutex> lk(j->mtx); c = j->count; }
double depth_ms = (double)c * j->frame_ms
+ (double)j->st.numSamples() * 1000.0 / (double)j->sr;
if (depth_ms > kResyncDepthMs) rj_hard_resync(j);
}
}
}
/* 0) pre-roll: не отдаём звук, пока глубина кольца < target_ms (или не истёк таймаут).
* Состояние — только аудио-поток; кадры копятся в кольце продюсером (uasync). */
int cnt;
{
std::lock_guard<std::mutex> lk(j->mtx);
cnt = j->count;
}
if (j->pre_roll && !j->primed) {
if (cnt > 0 && !j->prime_started) {
j->prime_started = 1;
j->prime_t0 = std::chrono::steady_clock::now();
}
double ring_ms = (double)cnt * j->frame_ms;
double waited_ms = 0.0;
if (j->prime_started) {
auto now = std::chrono::steady_clock::now();
waited_ms = std::chrono::duration<double, std::milli>(now - j->prime_t0).count();
}
if (ring_ms >= j->target_ms || waited_ms >= kPrimeTimeoutMs) {
j->primed = 1;
} else {
return 0; /* тишина, ждём накопления */
}
}
/* 1) глубина (кольцо + внутренний буфер SoundTouch, сглаженная) → целевой темп (плавно) */
double depth_ms = (double)cnt * j->frame_ms + (double)j->st.numSamples() * 1000.0 / (double)j->sr;
j->ema_depth_ms = kEmaAlpha * depth_ms + (1.0 - kEmaAlpha) * j->ema_depth_ms;
double target = rj_compute_tempo(j->ema_depth_ms, j->target_ms, j->min_tempo, j->max_tempo);
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 (или кольцо пусто).
* max_samples — суммарное число значений (интерлив); SoundTouch работает в слотах
* (слот = все каналы), поэтому max_slots = max_samples / channels. */
int ch = j->ch;
uint max_slots = (uint)max_samples / (uint)ch;
int guard = 0;
while (j->st.numSamples() < max_slots && guard++ < j->cap + 4) {
int elen = 0;
if (!rj_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 * ch; 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(), max_slots);
for (uint i = 0; i < n * (uint)ch; 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);
}
j->processing_ms.store((int)std::lround((double)j->st.numSamples() * 1000.0 / j->sr));
if ((int)(n * (uint)ch) < max_samples) j->underruns++;
return (int)(n * (uint)ch);
}
void rj_end(struct rj* j) {
if (!j) return;
j->primed = 1; /* только аудио-поток: снять pre-roll и дренировать остаток */
}
void rj_get_stats(struct rj* j, int* depth_ms, int* tempo_x100, uint32_t* dropped, uint32_t* underruns) { if (!j) return;
std::lock_guard<std::mutex> lk(j->mtx);
if (depth_ms) {
/* полная глубина = кодированные кадры в кольце + невыданный PCM внутри SoundTouch */
double st_ms = j->processing_ms.load();
*depth_ms = (int)((double)j->count * j->frame_ms + st_ms + 0.5);
}
if (tempo_x100) *tempo_x100 = j->tempo_x100.load();
if (dropped) *dropped = j->dropped;
if (underruns) *underruns = j->underruns.load();
}
uint32_t rj_dropped(struct rj* j) {
if (!j) return 0;
std::lock_guard<std::mutex> lk(j->mtx);
return j->dropped;
}
uint32_t rj_resyncs(struct rj* j) {
if (!j) return 0;
std::lock_guard<std::mutex> lk(j->mtx);
return j->resyncs.load();
}
} // extern "C"