/* Настоящие workers/AEC; граница сервисного PCM заменена наблюдателями без сети/устройства. */ #include "../src/voice_audio_io.h" extern "C" { #include "call/call_audio.h" #include "radio/radio_audio.h" #include "debug_config.h" } #include #include #include #include #include #include #include #include #include #define REQUIRE(x) do { if (!(x)) { fprintf(stderr, "voice-io:%d %s\n", __LINE__, #x); abort(); } } while (0) static std::mutex mutex; static std::vector capture, transmitted; static std::vector masks; static bool talking; static std::atomic pulls{0}, blocks{0}; static std::atomic stall{false}, stalled{false}; extern "C" int call_audio_pull_pcm(uint64_t, int16_t* out, int count) { ++pulls; std::fill_n(out, count, 1000); return count; } 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 radio_audio_reset_playback(uint64_t) {} extern "C" void radio_audio_capture_discontinuity(uint64_t) {} extern "C" void radio_audio_talk_begin(uint64_t) { std::lock_guard lock(mutex); talking = true; } extern "C" void radio_audio_talk_end(uint64_t) { std::lock_guard lock(mutex); talking = false; } extern "C" int radio_audio_feed_pcm(uint64_t, const int16_t* pcm, int count) { if (stall) { stalled = true; while (stall) std::this_thread::sleep_for(std::chrono::milliseconds(1)); } std::lock_guard lock(mutex); capture.insert(capture.end(), pcm, pcm + count); if (talking) transmitted.insert(transmitted.end(), pcm, pcm + count); ++blocks; return 0; } extern "C" int call_audio_feed_prepared_pcm(uint64_t, const int16_t* pcm, const uint8_t* mask) { std::lock_guard lock(mutex); capture.insert(capture.end(), pcm, pcm + 960); masks.insert(masks.end(), mask, mask + 960); ++blocks; return 0; } static int64_t now_us() { return std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); } template 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; } int main() { debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); VoiceAudioIo voice; std::array pcm{}; pcm.fill(1234); voice.start(VoiceAudioIo::Kind::Radio, 7); REQUIRE(voice.startCapture(1, false)); voice.capture(pcm.data(), 240, 0); voice.setPtt(true); voice.capture(pcm.data(), 360, 0); voice.setPtt(false); voice.capture(pcm.data(), 360, 0); await([] { return blocks.load() >= 3; }); voice.stopCapture(); REQUIRE(capture.size() == 960 && transmitted.size() == 360 && !talking); reset(); REQUIRE(voice.startCapture(1, false)); voice.setPtt(true); voice.capture(pcm.data(), 120, 0); voice.setPtt(false); voice.stopCapture(); REQUIRE(capture.size() == 120 && transmitted.size() == 120 && !talking); reset(); REQUIRE(voice.startCapture(1, false)); stall = true; voice.capture(pcm.data(), 960, 0); await([] { return stalled.load(); }); int before = pulls; for (int i = 0; i < 10; ++i) { float output[1920]{}; voice.mix(output, 960); std::this_thread::sleep_for(std::chrono::milliseconds(3)); } REQUIRE(pulls > before + 3); stall = false; voice.stopCapture(); voice.stop(); reset(); voice.start(VoiceAudioIo::Kind::Call, 42); REQUIRE(voice.startCapture(1, false)); voice.capture(pcm.data(), 480, 0); voice.setMuted(true); voice.capture(pcm.data(), 480, 0); await([] { return blocks.load() == 1; }); voice.stopCapture(); REQUIRE(masks.size() == 960); for (int i = 0; i < 960; ++i) REQUIRE(masks[i] == (i >= 480) && capture[i] == 1234); voice.stop(); reset(); voice.start(VoiceAudioIo::Kind::Call, 42); REQUIRE(voice.startCapture(1, false)); voice.capture(pcm.data(), 960, now_us() - 300000); // Backend latency не является возрастом worker-очереди. await([] { return blocks.load() == 1; }); voice.stopCapture(); voice.stop(); REQUIRE(capture.size() == 960); reset(); // Референс противофазный: mono downmix дал бы ноль и не смог подавить эхо. voice.start(VoiceAudioIo::Kind::Call, 43); REQUIRE(voice.startCapture(1, true)); uint32_t rng = 1234567; double echoPower = 0, outputPower = 0; int64_t time = now_us(); for (int frame = 0; frame < 240; ++frame) { float reference[1920]; for (int i = 0; i < 960; ++i) { rng = rng * 1664525u + 1013904223u; int16_t sample = (int16_t)((rng >> 18) - 8192); reference[2*i] = sample / 32768.0f; reference[2*i+1] = -reference[2*i]; pcm[i] = sample / 2; if (frame >= 180) echoPower += (double)pcm[i] * pcm[i]; } voice.reference(reference, 960, time + frame * 20000); voice.capture(pcm.data(), 960, time + frame * 20000); await([&] { return blocks.load() == frame + 1; }); } voice.stopCapture(); voice.stop(); for (size_t i = 180 * 960; i < capture.size(); ++i) outputPower += (double)capture[i] * capture[i]; printf("stereo reference residual ratio=%.6f\n", outputPower / echoPower); REQUIRE(outputPower < echoPower * 0.1); puts("voice workers: exact PTT/mute boundaries, short tail, independent RX, stereo AEC PASS"); }