call: единый голосовой стек (libutun_voice) + SoundTouch на Android
- 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 переведён на PCM
This commit is contained in:
1 parent
de930b21a2
commit
2de554f8b9
29 files changed
+659
-756
No files matched your search
@@ -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 */
|
||||
+7
-2
@@ -339,15 +339,16 @@ static void call_stats_timer_cb(void* arg) {
|
||||
if (s->state != CALL_ST_ACTIVE) return;
|
||||
|
||||
int buffer_ms = 0, tempo_x100 = 100;
|
||||
uint32_t dropped = 0, underruns = 0;
|
||||
if (ctx->audio_ops && ctx->audio_ops->get_stats)
|
||||
ctx->audio_ops->get_stats(s->call_id, &buffer_ms, &tempo_x100);
|
||||
ctx->audio_ops->get_stats(s->call_id, &buffer_ms, &tempo_x100, &dropped, &underruns);
|
||||
|
||||
uint16_t rtt = 0;
|
||||
struct ETCP_ROUTER_CONN* rconn =
|
||||
etcp_router_conn_get(ctx->inst, s->group_id, s->peer_node_id, ETCP_RT_ID_CALL);
|
||||
if (rconn && rconn->recv_conn) rtt = rconn->recv_conn->rtt_last;
|
||||
|
||||
uint8_t buf[14];
|
||||
uint8_t buf[18];
|
||||
size_t off = 0;
|
||||
memcpy(buf + off, &s->call_id, 8); off += 8;
|
||||
buf[off++] = (uint8_t)(rtt >> 8); buf[off++] = (uint8_t)(rtt & 0xFF);
|
||||
@@ -355,6 +356,10 @@ static void call_stats_timer_cb(void* arg) {
|
||||
buf[off++] = (uint8_t)(bms >> 8); buf[off++] = (uint8_t)(bms & 0xFF);
|
||||
uint16_t t = (uint16_t)(tempo_x100 < 0 ? 0 : (tempo_x100 > 0xFFFF ? 0xFFFF : tempo_x100));
|
||||
buf[off++] = (uint8_t)(t >> 8); buf[off++] = (uint8_t)(t & 0xFF);
|
||||
uint16_t dr = (uint16_t)(dropped > 0xFFFF ? 0xFFFF : dropped);
|
||||
buf[off++] = (uint8_t)(dr >> 8); buf[off++] = (uint8_t)(dr & 0xFF);
|
||||
uint16_t un = (uint16_t)(underruns > 0xFFFF ? 0xFFFF : underruns);
|
||||
buf[off++] = (uint8_t)(un >> 8); buf[off++] = (uint8_t)(un & 0xFF);
|
||||
chat_event_post(ctx->inst, CHAT_EVT_CALL_STATS, buf, (int)off);
|
||||
|
||||
s->stats_timer = uasync_set_timeout(ctx->inst->ua, 10000, s, call_stats_timer_cb, "call_stats");
|
||||
|
||||
+4
-2
@@ -57,8 +57,10 @@ struct call_audio_ops {
|
||||
/* uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер). */
|
||||
void (*on_media)(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len);
|
||||
/* uasync-поток: текущие метрики джиттер-буфера (buffer_ms, tempo_x100). */
|
||||
int (*get_stats)(uint64_t call_id, int* buffer_ms, int* tempo_x100);
|
||||
/* uasync-поток: текущие метрики джиттер-буфера (buffer_ms, tempo_x100,
|
||||
* dropped/underruns — опционально, NULL допустим). */
|
||||
int (*get_stats)(uint64_t call_id, int* buffer_ms, int* tempo_x100,
|
||||
uint32_t* dropped, uint32_t* underruns);
|
||||
};
|
||||
void call_set_audio_ops(struct UTUN_INSTANCE* inst, const struct call_audio_ops* ops);
|
||||
|
||||
|
||||
+39
-28
@@ -1,15 +1,15 @@
|
||||
// call_audio.c — сервисный аудио-движок P2P-звонка (Opus + джиттер + тоны).
|
||||
//
|
||||
// Один активный звонок: encoder (аудио-поток), decoder (uasync-поток),
|
||||
// адаптивный джиттер-буфер ajb (SoundTouch) и генератор тонов.
|
||||
// Один активный звонок: encoder (аудио-поток), decoder + адаптивный джиттер-буфер
|
||||
// vj (кодированные кадры + SoundTouch stretch на pull) и генератор тонов.
|
||||
//
|
||||
// RX: call.c → call_audio_on_media (uasync) → decode → ajb_push;
|
||||
// аудио-поток call_audio_pull_pcm → ajb_pull (+ глитч/завершение).
|
||||
// RX: call.c → call_audio_on_media (uasync) → vj_push (кодированный кадр);
|
||||
// аудио-поток call_audio_pull_pcm → vj_pull (декод+stretch) (+ глитч/завершение).
|
||||
// TX: аудио-поток call_audio_feed_pcm → encode → uasync_post(call_send_media).
|
||||
|
||||
#include "call_audio.h"
|
||||
#include "call.h"
|
||||
#include "audio_jitter.h"
|
||||
#include "voice_jitter.h"
|
||||
#include "call_tones.h"
|
||||
#include "../utun_instance.h"
|
||||
#include "../../lib/opus_codec.h"
|
||||
@@ -34,7 +34,7 @@ static uint64_t g_call_id = 0;
|
||||
static uint64_t g_last_media_tb = 0;
|
||||
static opus_codec_encoder_t* g_encoder = NULL;
|
||||
static opus_codec_decoder_t* g_decoder = NULL;
|
||||
static struct ajb* g_ajb = NULL;
|
||||
static struct vj* g_vj = NULL;
|
||||
static struct call_tones* g_tones = NULL;
|
||||
|
||||
static const struct call_audio_ops g_ops = {
|
||||
@@ -42,6 +42,12 @@ static const struct call_audio_ops g_ops = {
|
||||
.get_stats = call_audio_get_stats,
|
||||
};
|
||||
|
||||
/* vj decode-коллбэк: дёргается только из аудио-потока (vj_pull). */
|
||||
static int call_audio_decode_cb(void* arg, const uint8_t* enc, int len, int16_t* pcm, int max_samples) {
|
||||
opus_codec_decoder_t* dec = (opus_codec_decoder_t*)arg;
|
||||
return opus_codec_decode(dec, enc, len, pcm, max_samples);
|
||||
}
|
||||
|
||||
int call_audio_init(struct UTUN_INSTANCE* inst) {
|
||||
if (!inst) return -1;
|
||||
if (g_inst == inst) return 0;
|
||||
@@ -58,21 +64,16 @@ void call_audio_destroy(struct UTUN_INSTANCE* inst) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: destroyed", CALL_AUDIO_ID);
|
||||
}
|
||||
|
||||
/* ── uasync-поток: приём медиа (декод + ajb_push) ── */
|
||||
/* ── uasync-поток: приём медиа (кладём кодированный кадр в jitter-буфер) ── */
|
||||
|
||||
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len) {
|
||||
(void)inst; (void)seq; (void)ts_ms;
|
||||
int16_t pcm[CALL_AUDIO_FRAME_SAMPLES];
|
||||
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);
|
||||
if (n > 0 && g_ajb) {
|
||||
ajb_push(g_ajb, pcm, n);
|
||||
g_last_media_tb = get_time_tb();
|
||||
}
|
||||
if (g_active && call_id == g_call_id && g_vj && opus && len > 0) {
|
||||
vj_push(g_vj, opus, len);
|
||||
g_last_media_tb = get_time_tb();
|
||||
}
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
}
|
||||
@@ -112,18 +113,19 @@ int call_audio_start(uint64_t call_id) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct ajb* j = ajb_create(CALL_AUDIO_SAMPLE_RATE, 1, CALL_AUDIO_FRAME_SAMPLES, CALL_AUDIO_FRAME_MS, CALL_AJ_MAX_FRAMES);
|
||||
struct vj* j = vj_create(CALL_AUDIO_SAMPLE_RATE, 1, CALL_AUDIO_FRAME_SAMPLES, CALL_AUDIO_FRAME_MS,
|
||||
CALL_AJ_MAX_FRAMES, call_audio_decode_cb, dec);
|
||||
if (!j) {
|
||||
opus_codec_decoder_destroy(dec);
|
||||
opus_codec_encoder_destroy(enc);
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: ajb create failed", CALL_AUDIO_ID);
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: vj create failed", CALL_AUDIO_ID);
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct call_tones* tones = call_tones_create();
|
||||
if (!tones) {
|
||||
ajb_destroy(j);
|
||||
vj_destroy(j);
|
||||
opus_codec_decoder_destroy(dec);
|
||||
opus_codec_encoder_destroy(enc);
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
@@ -133,7 +135,7 @@ int call_audio_start(uint64_t call_id) {
|
||||
|
||||
g_encoder = enc;
|
||||
g_decoder = dec;
|
||||
g_ajb = j;
|
||||
g_vj = j;
|
||||
g_tones = tones;
|
||||
g_call_id = call_id;
|
||||
g_active = 1;
|
||||
@@ -158,7 +160,7 @@ void call_audio_begin_end(uint64_t call_id) {
|
||||
void call_audio_release(uint64_t call_id) {
|
||||
opus_codec_encoder_t* enc = NULL;
|
||||
opus_codec_decoder_t* dec = NULL;
|
||||
struct ajb* j = NULL;
|
||||
struct vj* j = NULL;
|
||||
struct call_tones* tones = NULL;
|
||||
int was_active = 0;
|
||||
|
||||
@@ -172,7 +174,7 @@ void call_audio_release(uint64_t call_id) {
|
||||
g_last_media_tb = 0;
|
||||
enc = g_encoder; g_encoder = NULL;
|
||||
dec = g_decoder; g_decoder = NULL;
|
||||
j = g_ajb; g_ajb = NULL;
|
||||
j = g_vj; g_vj = NULL;
|
||||
tones = g_tones; g_tones = NULL;
|
||||
}
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
@@ -182,11 +184,19 @@ void call_audio_release(uint64_t call_id) {
|
||||
/* дальше on_media не тронет объекты (active=0, указатели NULL) */
|
||||
if (enc) opus_codec_encoder_destroy(enc);
|
||||
if (dec) opus_codec_decoder_destroy(dec);
|
||||
if (j) ajb_destroy(j);
|
||||
if (j) vj_destroy(j);
|
||||
if (tones) call_tones_destroy(tones);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: released call=0x%016llx", CALL_AUDIO_ID, (unsigned long long)call_id);
|
||||
}
|
||||
|
||||
void call_audio_stop(void) {
|
||||
uint64_t cid = 0;
|
||||
pthread_mutex_lock(&g_mtx);
|
||||
if (g_active) cid = g_call_id;
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
if (cid) call_audio_release(cid);
|
||||
}
|
||||
|
||||
/* ── аудио-поток: TX / RX ── */
|
||||
|
||||
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count) {
|
||||
@@ -223,7 +233,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
||||
if (!out || max_samples <= 0) return 0;
|
||||
|
||||
int active = 0, ending = 0;
|
||||
struct ajb* j = NULL;
|
||||
struct vj* j = NULL;
|
||||
struct call_tones* tones = NULL;
|
||||
uint64_t last_media = 0;
|
||||
|
||||
@@ -231,7 +241,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
||||
if (g_active && call_id == g_call_id) {
|
||||
active = 1;
|
||||
ending = g_ending;
|
||||
j = g_ajb;
|
||||
j = g_vj;
|
||||
tones = g_tones;
|
||||
last_media = g_last_media_tb;
|
||||
}
|
||||
@@ -252,7 +262,7 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int n = ajb_pull(j, out, max_samples);
|
||||
int n = vj_pull(j, out, max_samples);
|
||||
if (n >= max_samples) return n;
|
||||
|
||||
/* недобор: либо сталь (глитч-тон), либо тишина */
|
||||
@@ -271,11 +281,12 @@ int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples) {
|
||||
|
||||
/* ── uasync-поток: метрики ── */
|
||||
|
||||
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100) {
|
||||
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,
|
||||
uint32_t* dropped, uint32_t* underruns) {
|
||||
int ok = 0;
|
||||
pthread_mutex_lock(&g_mtx);
|
||||
if (g_active && call_id == g_call_id && g_ajb) {
|
||||
ajb_get_stats(g_ajb, buffer_ms, tempo_x100);
|
||||
if (g_active && call_id == g_call_id && g_vj) {
|
||||
vj_get_stats(g_vj, buffer_ms, tempo_x100, dropped, underruns);
|
||||
ok = 1;
|
||||
}
|
||||
pthread_mutex_unlock(&g_mtx);
|
||||
|
||||
+18
-10
@@ -1,19 +1,22 @@
|
||||
#ifndef CALL_AUDIO_H
|
||||
#define CALL_AUDIO_H
|
||||
|
||||
// call_audio.h — сервисный аудио-движок P2P-звонка (GUI-сборки: desktop/Android).
|
||||
// call_audio.h — сервисный аудио-движок P2P-звонка (единый для desktop/Android/headless).
|
||||
//
|
||||
// Владеет Opus codec, адаптивным джиттер-буфером (SoundTouch time-stretch) и
|
||||
// тонами (глитч при сталле / завершение). Потребитель (desktop miniaudio или
|
||||
// Android AudioTrack) получает финальный PCM через call_audio_pull_pcm и отдаёт
|
||||
// захваченный PCM через call_audio_feed_pcm.
|
||||
// Владеет Opus codec, адаптивным джиттер-буфером (voice_jitter: кодированные
|
||||
// кадры + SoundTouch time-stretch на pull) и тонами (глитч при сталле / завершение).
|
||||
// Излишек голоса держится до декодирования — декодируется только то, что готово
|
||||
// к воспроизведению. Потребитель (desktop miniaudio / Android AudioTrack) получает
|
||||
// финальный PCM через call_audio_pull_pcm и отдаёт захваченный PCM через
|
||||
// call_audio_feed_pcm.
|
||||
//
|
||||
// Потоки:
|
||||
// - call_audio_start / begin_end / release — GUI-поток (по ACCEPTED/ENDED);
|
||||
// - call_audio_feed_pcm / pull_pcm — аудио-поток (single-writer);
|
||||
// - call_audio_on_media / get_stats — uasync-поток (из call.c).
|
||||
// Общие указатели (encoder/decoder/ajb/tones) защищены мьютексом; encoder —
|
||||
// только аудио-поток, decoder+ajb_push — только uasync-поток.
|
||||
// Общие указатели (encoder/decoder/vj/tones) защищены мьютексом; encoder и
|
||||
// decoder — только аудио-поток (decoder дёргается из vj_pull через коллбэк),
|
||||
// vj_push — только uasync-поток.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
@@ -40,6 +43,9 @@ void call_audio_begin_end(uint64_t call_id);
|
||||
/* GUI-поток (после остановки аудиоустройства): освободить все объекты. */
|
||||
void call_audio_release(uint64_t call_id);
|
||||
|
||||
/* GUI-поток: остановить текущий активный звонок (удобная обёртка release). */
|
||||
void call_audio_stop(void);
|
||||
|
||||
/* аудио-поток: закодировать PCM-кадр (ровно CALL_AUDIO_FRAME_SAMPLES) и отправить пиру. */
|
||||
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count);
|
||||
|
||||
@@ -47,12 +53,14 @@ int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count);
|
||||
* Возвращает число сэмплов (может быть < max — андерфлоу/конец тона). */
|
||||
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
|
||||
|
||||
/* uasync-поток (из call.c): принятый Opus-кадр — декод → ajb_push. */
|
||||
/* uasync-поток (из call.c): принятый Opus-кадр — кладём в джиттер-буфер (vj_push,
|
||||
* декодирование произойдёт на pull в аудио-потоке). */
|
||||
void call_audio_on_media(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len);
|
||||
|
||||
/* uasync-поток: метрики джиттер-буфера для CALL_STATS. */
|
||||
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100);
|
||||
/* uasync-поток: метрики джиттер-буфера для CALL_STATS (dropped/underruns — необязательные). */
|
||||
int call_audio_get_stats(uint64_t call_id, int* buffer_ms, int* tempo_x100,
|
||||
uint32_t* dropped, uint32_t* underruns);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
// call_headless.c — headless-аудио-сокет для P2P-звонков
|
||||
// call_headless.c — headless-аудио-сокет для P2P-звонков (PCM, без тактирования)
|
||||
//
|
||||
// TCP-сокет с бинарным фреймингом [type:1][call_id:8][len:2][data:len].
|
||||
// Клиент привязывается к звонку (HELLO) и шлёт/принимает Opus-кадры (FRAME).
|
||||
// Клиент привязывается к звонку (HELLO) и обменивается ГОТОВЫМ PCM (int16 mono 48k):
|
||||
// клиент → ядро: PCM-кадр (960 сэмплов) — кодируется в Opus и уходит пиру;
|
||||
// ядро → клиент: Opus пира декодируется в PCM и отдаётся клиенту.
|
||||
// Тактирование/джиттер — на стороне клиента (ядро лишь кодирует/декодирует).
|
||||
// Сигналинг — на control-сокете (chat_headless_control), медиа — здесь.
|
||||
#include "call_headless.h"
|
||||
#include "call.h"
|
||||
#include "../utun_instance.h"
|
||||
#include "../../lib/opus_codec.h"
|
||||
#include "../../lib/u_async.h"
|
||||
#include "../../lib/mem.h"
|
||||
#include "../../lib/socket_compat.h"
|
||||
@@ -24,6 +28,11 @@
|
||||
#define AUDIO_MAX_CLIENTS 16
|
||||
#define AUDIO_MAX_FRAME 2048
|
||||
|
||||
#define CA_SAMPLE_RATE 48000
|
||||
#define CA_FRAME_SAMPLES 960 /* 20мс @48k */
|
||||
#define CA_PCM_FRAME_BYTES (CA_FRAME_SAMPLES * (int)sizeof(int16_t)) /* 1920 */
|
||||
#define CA_BITRATE 32000
|
||||
|
||||
struct call_audio_client {
|
||||
socket_t fd;
|
||||
void* socket_id;
|
||||
@@ -46,6 +55,16 @@ struct call_audio {
|
||||
socket_t listen_fd;
|
||||
void* listen_sock_id;
|
||||
int running;
|
||||
/* Opus-кодеки (одна активная звонка за раз — busy в call.c) */
|
||||
uint64_t enc_call_id;
|
||||
opus_codec_encoder_t* enc;
|
||||
uint64_t dec_call_id;
|
||||
opus_codec_decoder_t* dec;
|
||||
/* диагностика */
|
||||
uint32_t c_enc_frames;
|
||||
uint32_t c_dec_frames;
|
||||
uint32_t c_enc_dropped;
|
||||
uint32_t c_dec_dropped;
|
||||
};
|
||||
|
||||
static struct call_audio* ca_of(struct UTUN_INSTANCE* inst) {
|
||||
@@ -88,16 +107,57 @@ static void ca_write_frame(struct call_audio_client* cli, uint8_t type, uint64_t
|
||||
if (len) ca_send_raw(cli, data, len);
|
||||
}
|
||||
|
||||
/* ── приём медиа от пира → клиенту ── */
|
||||
/* ── Opus-кодеки (одна активная звонка за раз) ── */
|
||||
|
||||
static opus_codec_encoder_t* ca_encoder(struct call_audio* ca, uint64_t call_id) {
|
||||
if (ca->enc && ca->enc_call_id == call_id) return ca->enc;
|
||||
if (ca->enc) { opus_codec_encoder_destroy(ca->enc); ca->enc = NULL; }
|
||||
ca->enc = opus_codec_encoder_create(CA_SAMPLE_RATE, 1);
|
||||
if (ca->enc) {
|
||||
opus_codec_encoder_bitrate_set(ca->enc, CA_BITRATE);
|
||||
ca->enc_call_id = call_id;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: encoder created for call_id=%016llx", CH_ID, (unsigned long long)call_id);
|
||||
} else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: encoder create failed", CH_ID);
|
||||
}
|
||||
return ca->enc;
|
||||
}
|
||||
|
||||
static opus_codec_decoder_t* ca_decoder(struct call_audio* ca, uint64_t call_id) {
|
||||
if (ca->dec && ca->dec_call_id == call_id) return ca->dec;
|
||||
if (ca->dec) { opus_codec_decoder_destroy(ca->dec); ca->dec = NULL; }
|
||||
ca->dec = opus_codec_decoder_create(CA_SAMPLE_RATE, 1);
|
||||
if (ca->dec) {
|
||||
ca->dec_call_id = call_id;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: decoder created for call_id=%016llx", CH_ID, (unsigned long long)call_id);
|
||||
} else {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: decoder create failed", CH_ID);
|
||||
}
|
||||
return ca->dec;
|
||||
}
|
||||
|
||||
/* ── приём медиа от пира → PCM клиенту ── */
|
||||
|
||||
static void ca_media_recv(struct UTUN_INSTANCE* inst, uint64_t call_id,
|
||||
const uint8_t* opus, int len, void* arg) {
|
||||
struct call_audio* ca = (struct call_audio*)arg;
|
||||
(void)inst;
|
||||
if (!ca) return;
|
||||
if (!ca || !opus || len <= 0) return;
|
||||
|
||||
opus_codec_decoder_t* dec = ca_decoder(ca, call_id);
|
||||
if (!dec) return;
|
||||
|
||||
int16_t pcm[CA_FRAME_SAMPLES];
|
||||
int n = opus_codec_decode(dec, opus, len, pcm, CA_FRAME_SAMPLES);
|
||||
if (n <= 0) {
|
||||
ca->c_dec_dropped++;
|
||||
return;
|
||||
}
|
||||
ca->c_dec_frames++;
|
||||
|
||||
for (struct call_audio_client* c = ca->clients; c; c = c->next) {
|
||||
if (c->call_id == call_id && !c->closing) {
|
||||
ca_write_frame(c, CALL_AUDIO_FRAME, call_id, opus, (uint16_t)len);
|
||||
ca_write_frame(c, CALL_AUDIO_FRAME, call_id, (const uint8_t*)pcm, (uint16_t)(n * (int)sizeof(int16_t)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -127,8 +187,25 @@ static void ca_parse_frames(struct call_audio_client* cli) {
|
||||
cli->call_id = call_id;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio client bound to call_id=%016llx", CH_ID, (unsigned long long)call_id);
|
||||
} else if (type == CALL_AUDIO_FRAME) {
|
||||
if (call_send_media(ca->inst, call_id, data, flen) != 0)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: send_media failed call_id=%016llx len=%u", CH_ID, (unsigned long long)call_id, flen);
|
||||
if (flen != CA_PCM_FRAME_BYTES) {
|
||||
ca->c_enc_dropped++;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: bad PCM frame size=%u (expect %d) — dropped",
|
||||
CH_ID, flen, CA_PCM_FRAME_BYTES);
|
||||
} else {
|
||||
opus_codec_encoder_t* enc = ca_encoder(ca, call_id);
|
||||
uint8_t opus[256];
|
||||
int olen = enc ? opus_codec_encode(enc, (const int16_t*)data, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
|
||||
if (olen <= 0) {
|
||||
ca->c_enc_dropped++;
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: encode failed (olen=%d) call_id=%016llx",
|
||||
CH_ID, olen, (unsigned long long)call_id);
|
||||
} else {
|
||||
ca->c_enc_frames++;
|
||||
if (call_send_media(ca->inst, call_id, opus, olen) != 0)
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: send_media failed call_id=%016llx len=%d",
|
||||
CH_ID, (unsigned long long)call_id, olen);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: unknown frame type=0x%02x — closing", CH_ID, type);
|
||||
ca_close_client(cli);
|
||||
@@ -266,6 +343,8 @@ void call_headless_destroy(struct UTUN_INSTANCE* inst) {
|
||||
call_set_media_recv_cb(inst, NULL, NULL);
|
||||
if (ca->listen_sock_id && ca->ua) uasync_remove_socket_t(ca->ua, ca->listen_fd);
|
||||
if (ca->listen_fd != SOCKET_INVALID) { socket_close_wrapper(ca->listen_fd); ca->listen_fd = SOCKET_INVALID; }
|
||||
if (ca->enc) { opus_codec_encoder_destroy(ca->enc); ca->enc = NULL; }
|
||||
if (ca->dec) { opus_codec_decoder_destroy(ca->dec); ca->dec = NULL; }
|
||||
struct call_audio_client* c = ca->clients;
|
||||
while (c) {
|
||||
struct call_audio_client* n = c->next;
|
||||
@@ -274,5 +353,6 @@ void call_headless_destroy(struct UTUN_INSTANCE* inst) {
|
||||
c = n;
|
||||
}
|
||||
ca->clients = NULL; ca->client_count = 0;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed", CH_ID);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u)",
|
||||
CH_ID, ca->c_enc_frames, ca->c_dec_frames, ca->c_enc_dropped, ca->c_dec_dropped);
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
// call_headless.h — headless-аудио-сокет для P2P-звонков
|
||||
// call_headless.h — headless-аудио-сокет для P2P-звонков (PCM, без тактирования)
|
||||
//
|
||||
// Отдельный TCP-сокет, через который headless-клиент (CLI/скрипт) обменивается
|
||||
// Opus-кадрами с ядром звонка. Сигналинг идёт по control-сокету (chat_headless_control),
|
||||
// аудио — по этому сокету.
|
||||
// ГОТОВЫМ PCM с ядром звонка. Ядро кодирует PCM→Opus (TX) и декодирует Opus→PCM
|
||||
// (RX); тактирование и джиттер — на стороне клиента. Сигналинг идёт по
|
||||
// control-сокету (chat_headless_control), аудио — по этому сокету.
|
||||
//
|
||||
// Бинарный фрейм (обе стороны):
|
||||
// [type:1][call_id:8][len:2][data:len]
|
||||
// type = 0x01 HELLO (клиент → ядро: привязать соединение к call_id, data пусто)
|
||||
// type = 0x02 FRAME (Opus-кадр: клиент → ядро = отправить пиру; ядро → клиент = кадр от пира)
|
||||
// type = 0x02 FRAME (PCM int16 mono 48k, 960 сэмплов = 1920 байт)
|
||||
// клиент → ядро = PCM захвата (кодируется и шлётся пиру);
|
||||
// ядро → клиент = PCM воспроизведения (декодированный Opus пира)
|
||||
#ifndef CALL_HEADLESS_H
|
||||
#define CALL_HEADLESS_H
|
||||
|
||||
@@ -19,6 +22,7 @@ struct UTUN_INSTANCE;
|
||||
#define CALL_AUDIO_HELLO 0x01
|
||||
#define CALL_AUDIO_FRAME 0x02
|
||||
#define CALL_AUDIO_HDR_SIZE 11 /* type(1) + call_id(8) + len(2) */
|
||||
#define CALL_AUDIO_PCM_FRAME_BYTES (960 * (int)sizeof(int16_t)) /* 1920 — PCM int16 mono 48k 20мс */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user