// 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 #include #include #include #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, 1); 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, 1); push_const(SRC_A, 1, 10000, 8); int mx = pull_max_abs(16); radio_audio_stop(); if (mx > 1000) OK(); else FAIL("max|pcm|=%d (ожидали > 1000)", mx); } TEST("mixing: два источника складываются (soft-clip)"); { radio_audio_start(GID, 1); push_const(SRC_A, 1, 20000, 8); push_const(SRC_B, 1, 20000, 8); int mx = pull_max_abs(16); radio_audio_stop(); if (mx > 30000) OK(); else FAIL("max|pcm|=%d (ожидали > 30000 — сумма двух источников)", mx); } TEST("reorder: seq 0,2,1,3,5,4 воспроизводятся в порядке 0,1,2,3,4,5"); { const int npulls = 16; int freqs[6] = {300, 1000, 2000, 3000, 4000, 5000}; uint8_t pkt[6][256]; int lens[6]; int ok = 0; if (encode_frames(freqs, 6, pkt, lens)) { int seqs_in[6] = {0, 1, 2, 3, 4, 5}, idx_in[6] = {0, 1, 2, 3, 4, 5}; int seqs_out[6] = {0, 2, 1, 3, 5, 4}, idx_out[6] = {0, 2, 1, 3, 5, 4}; int16_t buf_in[npulls * 960], buf_out[npulls * 960]; run_burst(pkt, lens, seqs_in, idx_in, 6, buf_in, npulls); run_burst(pkt, lens, seqs_out, idx_out, 6, 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 = 16; int freqs[6] = {300, 1000, 2000, 3000, 4000, 5000}; uint8_t pkt[6][256]; int lens[6]; int ok = 0; if (encode_frames(freqs, 6, pkt, lens)) { int seqs_ref[6] = {0, 1, 2, 3, 4, 5}, idx_ref[6] = {0, 1, 2, 3, 4, 5}; int seqs_late[7] = {0, 1, 2, 3, 4, 5, 0}, idx_late[7] = {0, 1, 2, 3, 4, 5, 0}; /* последний — поздний дубль seq=0 */ int16_t buf_ref[npulls * 960], buf_late[npulls * 960]; run_burst(pkt, lens, seqs_ref, idx_ref, 6, buf_ref, npulls); run_burst(pkt, lens, seqs_late, idx_late, 7, 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; }