|
|
|
|
@ -1,5 +1,6 @@
|
|
|
|
|
/* Настоящие workers/AEC; граница сервисного PCM заменена наблюдателями без сети/устройства. */ |
|
|
|
|
#include "../src/voice_audio_io.h" |
|
|
|
|
#include "../src/voice_capture_clock.h" |
|
|
|
|
extern "C" { |
|
|
|
|
#include "call/call_audio.h" |
|
|
|
|
#include "radio/radio_audio.h" |
|
|
|
|
@ -19,6 +20,8 @@ extern "C" {
|
|
|
|
|
static std::mutex mutex; |
|
|
|
|
static std::vector<int16_t> capture, transmitted; |
|
|
|
|
static std::vector<uint8_t> masks; |
|
|
|
|
static std::vector<int64_t> captureTimes; |
|
|
|
|
static std::atomic<unsigned> captureResets{0}; |
|
|
|
|
static bool talking; |
|
|
|
|
static std::atomic<int> pulls{0}, blocks{0}; |
|
|
|
|
static std::atomic<bool> stall{false}, stalled{false}; |
|
|
|
|
@ -34,7 +37,7 @@ extern "C" int call_audio_pull_pcm(uint64_t, int16_t* out, int count) { ++pulls;
|
|
|
|
|
extern "C" int radio_audio_pull_pcm(uint64_t, int16_t* out, int count) { |
|
|
|
|
++pulls; for (int i = 0; i < count; ++i) out[i] = i % 2 ? 2000 : 1000; return count; |
|
|
|
|
} |
|
|
|
|
extern "C" void call_audio_reset_io(uint64_t, int) {} |
|
|
|
|
extern "C" void call_audio_reset_io(uint64_t, int playback) { if (!playback) ++captureResets; } |
|
|
|
|
extern "C" int call_audio_get_clock(uint64_t, int64_t, call_clock_snapshot* out) { *out = {}; out->epoch = 1; return 0; } |
|
|
|
|
extern "C" void call_audio_playback_progress(uint64_t, uint64_t, uint64_t position, int64_t at, int64_t now, int) { |
|
|
|
|
output_position = position; output_delay = at > now ? (at - now) / 100 : 0; |
|
|
|
|
@ -54,6 +57,7 @@ static int64_t capture_time;
|
|
|
|
|
extern "C" int call_audio_feed_prepared_pcm(uint64_t, const int16_t* pcm, const uint8_t* mask, int64_t pts) { |
|
|
|
|
std::lock_guard lock(mutex); |
|
|
|
|
capture_time = pts; |
|
|
|
|
captureTimes.push_back(pts); |
|
|
|
|
capture.insert(capture.end(), pcm, pcm + 960); masks.insert(masks.end(), mask, mask + 960); |
|
|
|
|
++blocks; return 0; |
|
|
|
|
} |
|
|
|
|
@ -64,9 +68,25 @@ template<class F> static void await(F ready) {
|
|
|
|
|
for (int i = 0; i < 2000 && !ready(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
|
|
|
|
REQUIRE(ready()); |
|
|
|
|
} |
|
|
|
|
static void reset() { std::lock_guard lock(mutex); capture.clear(); transmitted.clear(); masks.clear(); talking = false; blocks = 0; } |
|
|
|
|
static void reset() { |
|
|
|
|
std::lock_guard lock(mutex); |
|
|
|
|
capture.clear(); transmitted.clear(); masks.clear(); captureTimes.clear(); captureResets = 0; talking = false; blocks = 0; |
|
|
|
|
} |
|
|
|
|
int main() { |
|
|
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); |
|
|
|
|
// Десять минут +/-300ppm дрейфа: связь с физическим временем сохраняется, шага >2мс нет.
|
|
|
|
|
for (double drift : {-0.0003, 0.0003}) { |
|
|
|
|
VoiceCaptureClock clock; |
|
|
|
|
int64_t previous = 0; |
|
|
|
|
for (int frame = 0; frame < 30000; ++frame) { |
|
|
|
|
int64_t actual = 1000000 + std::llround(frame * 20000.0 * (1 + drift)); |
|
|
|
|
int64_t mapped = clock.map(uint64_t(frame) * 960, actual + (frame % 75 == 0 ? -14000 : 0)); |
|
|
|
|
if (frame) REQUIRE(std::abs(mapped - previous - 20000) <= 101); |
|
|
|
|
if (frame > 2000) REQUIRE(std::abs(mapped - actual) < 1500); |
|
|
|
|
previous = mapped; |
|
|
|
|
} |
|
|
|
|
clock.reset(); REQUIRE(clock.map(123, 900000) == 900000); |
|
|
|
|
} |
|
|
|
|
VoiceAudioIo voice; |
|
|
|
|
std::array<int16_t, 960> pcm{}; pcm.fill(1234); |
|
|
|
|
// Отсутствие PCM должно попадать в лог без необходимости ждать следующего кадра.
|
|
|
|
|
@ -126,6 +146,29 @@ int main() {
|
|
|
|
|
voice.capture(pcm.data(), 960, capturePts); // Backend latency не является возрастом worker-очереди.
|
|
|
|
|
await([] { return blocks.load() == 1; }); voice.stopCapture(); voice.stop(); |
|
|
|
|
REQUIRE(capture.size() == 960 && capture_time == capturePts); reset(); |
|
|
|
|
// Чанки backend 25мс пересекают границы Opus 20мс. Джиттер меток не меняет число/порядок PCM.
|
|
|
|
|
voice.start(VoiceAudioIo::Kind::Call, 44); REQUIRE(voice.startCapture(1, false)); |
|
|
|
|
std::array<int16_t, 1200> chunk{}; chunk.fill(2345); |
|
|
|
|
const int64_t base = now_us() - 50000; |
|
|
|
|
for (int i = 0; i < 120; ++i) { |
|
|
|
|
voice.capture(chunk.data(), 1200, base + i * 25000 + (i % 2 ? 7000 : -7000), 1); |
|
|
|
|
await([&] { return blocks.load() == (i + 1) * 1200 / 960; }); |
|
|
|
|
} |
|
|
|
|
REQUIRE(captureResets == 0); |
|
|
|
|
voice.capture(chunk.data(), 1200, base + 120 * 25000, 1, true); |
|
|
|
|
voice.capture(chunk.data(), 1200, base + 121 * 25000, 1); |
|
|
|
|
await([] { return blocks.load() == 151; }); |
|
|
|
|
REQUIRE(captureResets == 1); // Явная потеря PCM по-прежнему разрывает эпоху.
|
|
|
|
|
voice.capture(chunk.data(), 1200, base + 122 * 25000, 2); |
|
|
|
|
await([] { return blocks.load() == 152; }); |
|
|
|
|
REQUIRE(captureResets == 2); // Смена устройства не скрывается фильтром.
|
|
|
|
|
voice.capture(chunk.data(), 1200, base + 123 * 25000 + 100000, 2); |
|
|
|
|
await([] { return blocks.load() == 153; }); |
|
|
|
|
REQUIRE(captureResets == 3); // Большой разрыв исходных меток также остаётся разрывом.
|
|
|
|
|
voice.stop(); |
|
|
|
|
REQUIRE(capture.size() == 153 * 960 && std::all_of(capture.begin(), capture.end(), [](int16_t v) { return v == 2345; })); |
|
|
|
|
for (size_t i = 1; i < 150; ++i) REQUIRE(std::abs(captureTimes[i] - captureTimes[i-1] - 20000) <= 201); |
|
|
|
|
reset(); |
|
|
|
|
// Референс противофазный: mono downmix дал бы ноль и не смог подавить эхо.
|
|
|
|
|
voice.start(VoiceAudioIo::Kind::Call, 43); REQUIRE(voice.startCapture(1, true)); |
|
|
|
|
uint32_t rng = 1234567; |
|
|
|
|
|