136 lines
7.2 KiB
C++
136 lines
7.2 KiB
C++
/* Настоящие 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 <vector>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#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<int16_t> capture, transmitted;
|
|
static std::vector<uint8_t> masks;
|
|
static bool talking;
|
|
static std::atomic<int> pulls{0}, blocks{0};
|
|
static std::atomic<bool> stall{false}, stalled{false};
|
|
static std::atomic<bool> idleReported{false};
|
|
static void capture_diagnostic(int, const char*, const char* message) {
|
|
if (std::strstr(message, "voice-io: capture stalled")) idleReported = true;
|
|
}
|
|
static std::atomic<uint64_t> output_position{0}, output_delay{0};
|
|
extern "C" void radio_audio_playback_progress(uint64_t, uint64_t position, uint64_t delay) {
|
|
output_delay = delay; output_position = position;
|
|
}
|
|
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::microseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
}
|
|
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; }
|
|
int main() {
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN);
|
|
VoiceAudioIo voice;
|
|
std::array<int16_t, 960> pcm{}; pcm.fill(1234);
|
|
// Отсутствие PCM должно попадать в лог без необходимости ждать следующего кадра.
|
|
debug_set_log_hook(capture_diagnostic);
|
|
REQUIRE(voice.start(VoiceAudioIo::Kind::Radio, 7) && voice.startCapture(1, false));
|
|
await([] { return idleReported.load(); });
|
|
voice.capture(pcm.data(), 960, 0);
|
|
await([] { return blocks.load() == 1; });
|
|
voice.stop(); debug_set_log_hook(nullptr);
|
|
REQUIRE(capture.size() == 960 && capture.front() == 1234 && capture.back() == 1234);
|
|
reset(); pulls = 0; output_position = 0;
|
|
// Выдача RX worker не подтверждает вывод; частичный callback сохраняет позицию и backend delay.
|
|
REQUIRE(voice.start(VoiceAudioIo::Kind::Radio, 7));
|
|
await([] { return pulls.load() >= 2; });
|
|
REQUIRE(output_position == 0);
|
|
float partial[1920]{};
|
|
voice.mix(partial, 240, now_us() + 500000);
|
|
await([] { return output_position.load() == 240; });
|
|
REQUIRE(output_delay > 4500);
|
|
voice.mix(partial, 720, now_us());
|
|
await([] { return output_position.load() == 960; });
|
|
voice.stop();
|
|
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");
|
|
}
|