- voice_jitter: vj_create_cfg (target_ms/min_tempo/max_tempo/pre_roll), pre-roll с таймаутом, vj_end для дренажа остатка, глубина учитывает внутренний буфер SoundTouch - radio_audio: reorder по seq (mesh, out-of-order через соседей), force-flush pending по таймауту и FIN, компрессор микрофона (AGC, конфиг [chatserver]), статистика буфера - audio_compressor: clear_output для потокового слива без потери gain_smoothed - chatgui messagelist: fromUtf8 для emoji-строк - тесты: pre-roll/симметричный темп, reorder, late-drop
158 lines
6.6 KiB
C++
158 lines
6.6 KiB
C++
// test_radio_audio.cpp — тест аудио-движка рации (jitter + микшер).
|
||
//
|
||
// Проверяет сервисный слой src/radio/radio_audio.c без полного UTUN_INSTANCE:
|
||
// 1. single-source: подаём кодированные Opus-кадры через radio_audio_on_frame,
|
||
// radio_audio_pull_pcm выдаёт декодированный ненулевой PCM (jitter+decode).
|
||
// 2. mixing: два источника с большими амплитудами — сумма насыщается (soft-clip),
|
||
// что доказывает микширование нескольких источников.
|
||
//
|
||
// Сборка/запуск: через CMake (tools/chatgui), линкуется utun_voice + utun.
|
||
|
||
#include "radio/radio_audio.h"
|
||
|
||
extern "C" {
|
||
#include "../../lib/opus_codec.h"
|
||
}
|
||
|
||
#include <cstdio>
|
||
#include <cstring>
|
||
#include <cstdlib>
|
||
#include <cmath>
|
||
|
||
#define GID 0x1234567890abcdefULL
|
||
#define SRC_A 0xAAAAULL
|
||
#define SRC_B 0xBBBBULL
|
||
|
||
static int G_PASSED = 0, G_FAILED = 0, G_TOTAL = 0;
|
||
#define TEST(n) do { G_TOTAL++; printf(" %-58s", n); fflush(stdout); } while(0)
|
||
#define OK() do { G_PASSED++; printf("OK\n"); } while(0)
|
||
#define FAIL(f,...) do { G_FAILED++; printf("FAIL: " f "\n", ##__VA_ARGS__); } while(0)
|
||
|
||
/* закодировать N кадров постоянного PCM и подать через on_frame */
|
||
static void push_const(uint64_t src, uint16_t stream, int value, int nframes) {
|
||
opus_codec_encoder_t* enc = opus_codec_encoder_create(48000, 1);
|
||
if (!enc) { FAIL("encoder create failed"); return; }
|
||
opus_codec_encoder_bitrate_set(enc, 32000);
|
||
|
||
int16_t pcm[960];
|
||
for (int i = 0; i < 960; i++) pcm[i] = (int16_t)value;
|
||
|
||
for (int f = 0; f < nframes; f++) {
|
||
uint8_t opus[256];
|
||
int len = opus_codec_encode(enc, pcm, 960, opus, (int)sizeof(opus));
|
||
if (len <= 0) { FAIL("encode failed"); break; }
|
||
radio_audio_on_frame(NULL, GID, src, stream, (uint16_t)f, 0, opus, len, NULL);
|
||
}
|
||
opus_codec_encoder_destroy(enc);
|
||
}
|
||
|
||
/* закодировать n синусоидальных кадров (seq 0..n-1, freq[i]) одним encoder'ом. */
|
||
static int encode_frames(const int* freqs, int n, uint8_t pkt[][256], int* lens) {
|
||
opus_codec_encoder_t* enc = opus_codec_encoder_create(48000, 1);
|
||
if (!enc) { FAIL("encoder create failed"); return 0; }
|
||
opus_codec_encoder_bitrate_set(enc, 32000);
|
||
int ok = 1;
|
||
for (int f = 0; f < n; f++) {
|
||
int16_t pcm[960];
|
||
for (int i = 0; i < 960; i++)
|
||
pcm[i] = (int16_t)(sin(2.0 * M_PI * freqs[f] * (double)i / 48000.0) * 10000.0);
|
||
lens[f] = opus_codec_encode(enc, pcm, 960, pkt[f], 256);
|
||
if (lens[f] <= 0) { ok = 0; break; }
|
||
}
|
||
opus_codec_encoder_destroy(enc);
|
||
return ok;
|
||
}
|
||
|
||
/* подать кадры pkt[idx[i]] с seq[i] через on_frame */
|
||
static void feed_frames(const uint8_t pkt[][256], const int* lens, const int* seqs, const int* idx, int n) {
|
||
for (int i = 0; i < n; i++)
|
||
radio_audio_on_frame(NULL, GID, SRC_A, 1, (uint16_t)seqs[i], 0, pkt[idx[i]], lens[idx[i]], NULL);
|
||
}
|
||
|
||
/* прогнать burst и собрать npulls*960 сэмплов выхода */
|
||
static void run_burst(const uint8_t pkt[][256], const int* lens, const int* seqs, const int* idx,
|
||
int n, int16_t* result, int npulls) {
|
||
radio_audio_start(GID);
|
||
feed_frames(pkt, lens, seqs, idx, n);
|
||
int16_t out[960];
|
||
int pos = 0;
|
||
for (int p = 0; p < npulls; p++) {
|
||
radio_audio_pull_pcm(GID, out, 960);
|
||
for (int i = 0; i < 960; i++) result[pos++] = out[i];
|
||
}
|
||
radio_audio_stop();
|
||
}
|
||
|
||
/* вытянуть pull-цикл, вернуть max |sample| */
|
||
static int pull_max_abs(int npulls) {
|
||
int mx = 0;
|
||
int16_t out[960];
|
||
for (int p = 0; p < npulls; p++) {
|
||
int n = radio_audio_pull_pcm(GID, out, 960);
|
||
(void)n;
|
||
for (int i = 0; i < 960; i++) {
|
||
int a = out[i] < 0 ? -out[i] : out[i];
|
||
if (a > mx) mx = a;
|
||
}
|
||
}
|
||
return mx;
|
||
}
|
||
|
||
int main() {
|
||
printf("=== test_radio_audio ===\n"); fflush(stdout);
|
||
|
||
TEST("single-source: decode через jitter-буфер выдаёт ненулевой PCM"); {
|
||
radio_audio_start(GID);
|
||
push_const(SRC_A, 1, 10000, 5);
|
||
int mx = pull_max_abs(10);
|
||
radio_audio_stop();
|
||
if (mx > 1000) OK(); else FAIL("max|pcm|=%d (ожидали > 1000)", mx);
|
||
}
|
||
|
||
TEST("mixing: два источника складываются (soft-clip)"); {
|
||
radio_audio_start(GID);
|
||
push_const(SRC_A, 1, 20000, 5);
|
||
push_const(SRC_B, 1, 20000, 5);
|
||
int mx = pull_max_abs(10);
|
||
radio_audio_stop();
|
||
if (mx > 30000) OK(); else FAIL("max|pcm|=%d (ожидали > 30000 — сумма двух источников)", mx);
|
||
}
|
||
|
||
TEST("reorder: seq 0,2,1,3 воспроизводятся в порядке 0,1,2,3"); {
|
||
const int npulls = 12;
|
||
int freqs[4] = {300, 1000, 2000, 3000};
|
||
uint8_t pkt[4][256];
|
||
int lens[4];
|
||
int ok = 0;
|
||
if (encode_frames(freqs, 4, pkt, lens)) {
|
||
int seqs_in[4] = {0, 1, 2, 3}, idx_in[4] = {0, 1, 2, 3};
|
||
int seqs_out[4] = {0, 2, 1, 3}, idx_out[4] = {0, 2, 1, 3};
|
||
int16_t buf_in[npulls * 960], buf_out[npulls * 960];
|
||
run_burst(pkt, lens, seqs_in, idx_in, 4, buf_in, npulls);
|
||
run_burst(pkt, lens, seqs_out, idx_out, 4, buf_out, npulls);
|
||
ok = (memcmp(buf_in, buf_out, sizeof(buf_in)) == 0);
|
||
}
|
||
if (ok) OK(); else FAIL("out-of-order должен дать тот же PCM, что и in-order (reorder не сработал)");
|
||
}
|
||
|
||
TEST("late seq: кадр с seq < next_seq дропается"); {
|
||
const int npulls = 12;
|
||
int freqs[4] = {300, 1000, 2000, 3000};
|
||
uint8_t pkt[4][256];
|
||
int lens[4];
|
||
int ok = 0;
|
||
if (encode_frames(freqs, 4, pkt, lens)) {
|
||
int seqs_ref[4] = {0, 1, 2, 3}, idx_ref[4] = {0, 1, 2, 3};
|
||
int seqs_late[5] = {0, 1, 2, 3, 0}, idx_late[5] = {0, 1, 2, 3, 0}; /* последний — поздний дубль seq=0 */
|
||
int16_t buf_ref[npulls * 960], buf_late[npulls * 960];
|
||
run_burst(pkt, lens, seqs_ref, idx_ref, 4, buf_ref, npulls);
|
||
run_burst(pkt, lens, seqs_late, idx_late, 5, buf_late, npulls);
|
||
ok = (memcmp(buf_ref, buf_late, sizeof(buf_ref)) == 0);
|
||
}
|
||
if (ok) OK(); else FAIL("поздний дубль seq=0 должен быть дропнут (PCM совпадает с эталоном)");
|
||
}
|
||
|
||
printf("\n%d/%d passed, %d failed\n", G_PASSED, G_TOTAL, G_FAILED);
|
||
return G_FAILED > 0 ? 1 : 0;
|
||
}
|