Test Android bridge mute after real AEC and AGC across unmute
This commit is contained in:
1 parent
000b28027c
commit
79b06846c6
2 files changed
+176
No files matched your search
@@ -677,6 +677,22 @@ if(UNIX AND NOT APPLE)
|
||||
add_test(NAME test_call_audio_tx_${scenario} COMMAND test_call_audio_tx ${scenario})
|
||||
set_tests_properties(test_call_audio_tx_${scenario} PROPERTIES TIMEOUT 15)
|
||||
endforeach()
|
||||
# Настоящий host bridge; неиспользуемые Android-сервисы исключаются, как в test_android_replies.
|
||||
add_executable(test_android_call_mute tests/test_android_call_mute.cpp ../chatgui-android/jni_bridge/android_jni_bridge.c)
|
||||
target_include_directories(test_android_call_mute PRIVATE ${UTUN_INCLUDE_DIRS})
|
||||
target_compile_options(test_android_call_mute PRIVATE -ffunction-sections -fdata-sections)
|
||||
target_link_libraries(test_android_call_mute PRIVATE utun_voice utun pthread)
|
||||
target_link_options(test_android_call_mute PRIVATE "-Wl,--gc-sections" "-Wl,--wrap=get_time_tb"
|
||||
"-Wl,--wrap=chat_setting_get_int" "-Wl,--wrap=call_send_media" "-Wl,--wrap=opus_codec_encode"
|
||||
"-Wl,--wrap=speex_aec_process_capture" "-Wl,--wrap=audio_compressor_process_frame")
|
||||
foreach(aec 0 1)
|
||||
foreach(agc 0 1)
|
||||
add_test(NAME test_android_call_mute_aec${aec}_agc${agc} COMMAND test_android_call_mute ${aec} ${agc})
|
||||
set_tests_properties(test_android_call_mute_aec${aec}_agc${agc} PROPERTIES TIMEOUT 15)
|
||||
endforeach()
|
||||
endforeach()
|
||||
add_test(NAME test_android_call_mute_legacy COMMAND test_android_call_mute legacy)
|
||||
set_tests_properties(test_android_call_mute_legacy PROPERTIES TIMEOUT 15)
|
||||
endif()
|
||||
|
||||
# smoke-тест видео-декодера (FFmpeg). Ручной запуск: test_video_engine <mp4>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
/* Настоящие C bridge/Opus/AEC/AGC/uasync; Android runtime и сетевой звонок не создаются.
|
||||
* Wrapper читает входы/выходы DSP, не меняя их. Проверяем mute и следующий unmute по точному PCM до Opus. */
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
extern "C" {
|
||||
#include "../../chatgui-android/jni_bridge/android_jni_bridge.h"
|
||||
#include "utun_instance.h"
|
||||
#include "call/call_audio.h"
|
||||
#include "audio_compressor.h"
|
||||
#include "opus_codec.h"
|
||||
#include "speex_aec.h"
|
||||
#include "debug_config.h"
|
||||
#include "u_async.h"
|
||||
#include "mem.h"
|
||||
}
|
||||
|
||||
#define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Android mute line=%d check=%s", __LINE__, #x); abort(); } } while (0)
|
||||
static constexpr unsigned Samples = 960, Warmup = 300, PhaseFrames = 20, Total = Warmup + 3 * PhaseFrames;
|
||||
using Pcm = std::array<int16_t, Samples>;
|
||||
struct pcm_stats { unsigned frames = 0, nonzero = 0; int peak = 0; double energy = 0; };
|
||||
struct phase_stats { pcm_stats raw, aec, agc, opus; unsigned mismatches = 0; };
|
||||
static std::array<phase_stats, 3> phases;
|
||||
static uint64_t time_tb = 100000000;
|
||||
static int aec_enabled, agc_enabled, phase = -1, codec_delay;
|
||||
static unsigned sent;
|
||||
static bool feeding, muted_frame, saw_aec, saw_agc, saw_encode;
|
||||
static Pcm expected_raw, processed;
|
||||
static int64_t expected_pts;
|
||||
static opus_codec_encoder_t* remote_encoder;
|
||||
|
||||
/* Только числовой снимок, без логов/выделений памяти на каждом DSP-вызове. */
|
||||
static void observe(pcm_stats& stats, const int16_t* pcm) {
|
||||
++stats.frames;
|
||||
for (unsigned i = 0; i < Samples; ++i) {
|
||||
stats.nonzero += pcm[i] != 0;
|
||||
stats.peak = std::max(stats.peak, std::abs(int(pcm[i])));
|
||||
stats.energy += double(pcm[i]) * pcm[i];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" uint64_t __wrap_get_time_tb() { return time_tb; }
|
||||
extern "C" int __wrap_chat_setting_get_int(UTUN_INSTANCE*, const char* name, int fallback) {
|
||||
if (!strcmp(name, "aec_enabled")) return aec_enabled;
|
||||
if (!strcmp(name, "call_compressor_enabled")) return agc_enabled;
|
||||
return fallback;
|
||||
}
|
||||
extern "C" int __wrap_call_send_media(UTUN_INSTANCE*, uint64_t id, const uint8_t* data, int len, const call_capture_time* timing) {
|
||||
CHECK(id == 42 && data && len > 0 && timing);
|
||||
CHECK(timing->pts_us == expected_pts && timing->epoch == 1 && timing->delay_samples == unsigned(codec_delay));
|
||||
++sent; return 0;
|
||||
}
|
||||
|
||||
/* AEC получает оригинальный raw-вход даже во время mute; настоящий фильтр продолжает работать. */
|
||||
extern "C" int __real_speex_aec_process_capture(speex_aec_t*, const int16_t*, int, int16_t*);
|
||||
extern "C" int __wrap_speex_aec_process_capture(speex_aec_t* aec, const int16_t* pcm, int count, int16_t* out) {
|
||||
CHECK(feeding && !saw_aec && count == int(Samples) && !memcmp(pcm, expected_raw.data(), sizeof(expected_raw)));
|
||||
saw_aec = true;
|
||||
int result = __real_speex_aec_process_capture(aec, pcm, count, out); CHECK(result == count);
|
||||
std::copy_n(out, Samples, processed.begin());
|
||||
if (phase >= 0) observe(phases[phase].aec, out);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* AGC обрабатывает выход AEC до mute; проверяем также реально выбранное состояние compressor. */
|
||||
extern "C" int __real_audio_compressor_process_frame(audio_compressor*, const int16_t*, int16_t*, size_t);
|
||||
extern "C" int __wrap_audio_compressor_process_frame(audio_compressor* ac, const int16_t* pcm, int16_t* out, size_t count) {
|
||||
CHECK(feeding && !saw_agc && count == Samples && audio_compressor_is_enabled(ac) == agc_enabled);
|
||||
CHECK(!memcmp(pcm, processed.data(), sizeof(processed)));
|
||||
saw_agc = true;
|
||||
int result = __real_audio_compressor_process_frame(ac, pcm, out, count); CHECK(result == 0);
|
||||
std::copy_n(out, Samples, processed.begin());
|
||||
if (phase >= 0) observe(phases[phase].agc, out);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Несовпадения копим до cleanup: прежний bridge даёт диагностируемый FAIL с освобождением ресурсов. */
|
||||
extern "C" int __real_opus_codec_encode(opus_codec_encoder_t*, const int16_t*, int, uint8_t*, int);
|
||||
extern "C" int __wrap_opus_codec_encode(opus_codec_encoder_t* encoder, const int16_t* pcm, int count, uint8_t* out, int capacity) {
|
||||
if (encoder != remote_encoder) {
|
||||
CHECK(feeding && !saw_encode && saw_agc && count == int(Samples)); saw_encode = true;
|
||||
CHECK(opus_codec_encoder_delay(encoder, &codec_delay) == 0);
|
||||
if (phase >= 0) {
|
||||
observe(phases[phase].opus, pcm);
|
||||
for (unsigned i = 0; i < Samples; ++i)
|
||||
phases[phase].mismatches += pcm[i] != (muted_frame ? 0 : processed[i]);
|
||||
}
|
||||
}
|
||||
return __real_opus_codec_encode(encoder, pcm, count, out, capacity);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR);
|
||||
debug_set_category_level(DEBUG_CATEGORY_CALL, DEBUG_LEVEL_INFO);
|
||||
bool legacy = argc == 2 && !strcmp(argv[1], "legacy");
|
||||
if (legacy) { aec_enabled = 1; agc_enabled = 0; }
|
||||
else {
|
||||
CHECK(argc == 3 && (!strcmp(argv[1], "0") || !strcmp(argv[1], "1")) &&
|
||||
(!strcmp(argv[2], "0") || !strcmp(argv[2], "1")));
|
||||
aec_enabled = argv[1][0] == '1'; agc_enabled = argv[2][0] == '1';
|
||||
}
|
||||
size_t allocated = u_get_allocated_count();
|
||||
UTUN_INSTANCE inst{}; inst.ua = uasync_create(); CHECK(inst.ua);
|
||||
CHECK(call_audio_init(&inst) == 0);
|
||||
call_audio_set_aec_delay_frames(1); CHECK(call_audio_start(42) == 0);
|
||||
remote_encoder = opus_codec_encoder_create(48000, 1); CHECK(remote_encoder);
|
||||
Pcm remote{}, rendered{}, microphone{}, silence{};
|
||||
uint32_t rng = 1234567;
|
||||
uint16_t seq = 0;
|
||||
auto incoming = [&](uint32_t timestamp) {
|
||||
for (auto& sample : remote) { rng = rng * 1664525u + 1013904223u; sample = int(rng >> 18) - 8192; }
|
||||
uint8_t opus[256]; int len = opus_codec_encode(remote_encoder, remote.data(), Samples, opus, sizeof(opus)); CHECK(len > 0);
|
||||
call_audio_on_media(&inst, 42, seq++, timestamp, opus, len, nullptr);
|
||||
};
|
||||
incoming(0); incoming(0); // Настоящий стартовый запас FIFO, приватное состояние не подменяется.
|
||||
for (unsigned frame = 0; frame < Total; ++frame) {
|
||||
time_tb = 100000000 + frame * 200;
|
||||
incoming(frame * 20); CHECK(call_audio_pull_pcm(42, rendered.data(), Samples) == int(Samples));
|
||||
for (unsigned i = 0; i < Samples; ++i) microphone[i] = rendered[i] / 2;
|
||||
phase = frame < Warmup ? -1 : int((frame - Warmup) / PhaseFrames);
|
||||
bool mute = phase == 1;
|
||||
expected_raw = legacy && mute ? silence : microphone;
|
||||
processed = expected_raw; muted_frame = mute && !legacy;
|
||||
expected_pts = 1000000 + int64_t(frame) * 20000;
|
||||
saw_aec = saw_agc = saw_encode = false;
|
||||
if (phase >= 0) observe(phases[phase].raw, expected_raw.data());
|
||||
feeding = true;
|
||||
CHECK(utun_bridge_call_audio_feed(42, expected_raw.data(), Samples, muted_frame, expected_pts) == 0);
|
||||
feeding = false;
|
||||
CHECK(saw_aec == bool(aec_enabled) && saw_agc && saw_encode);
|
||||
CHECK(expected_raw == (legacy && mute ? silence : microphone)); // Mute не изменяет массив вызывающего.
|
||||
uasync_poll(inst.ua, 0);
|
||||
}
|
||||
CHECK(sent == Total && !inst.ua->posted_tasks_head);
|
||||
call_audio_stop(); call_audio_destroy(&inst); opus_codec_encoder_destroy(remote_encoder);
|
||||
CHECK(inst.ua->timer_alloc_count == inst.ua->timer_free_count && inst.ua->socket_alloc_count == inst.ua->socket_free_count);
|
||||
uasync_destroy(inst.ua, 0);
|
||||
CHECK(u_get_allocated_count() == allocated);
|
||||
bool ok = true;
|
||||
const char* names[] = {"before", "muted", "after"};
|
||||
for (unsigned i = 0; i < phases.size(); ++i) {
|
||||
const auto& stats = phases[i];
|
||||
CHECK(stats.raw.frames == PhaseFrames && stats.agc.frames == PhaseFrames && stats.opus.frames == PhaseFrames);
|
||||
CHECK(stats.aec.frames == (aec_enabled ? PhaseFrames : 0));
|
||||
bool valid = !stats.mismatches && (i == 1 && !legacy ? !stats.opus.nonzero : stats.opus.nonzero > 0);
|
||||
valid = valid && (legacy && i == 1 ? !stats.raw.nonzero : stats.raw.nonzero > 0);
|
||||
ok = ok && valid;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL,
|
||||
"Android mute aec=%d agc=%d legacy=%d phase=%s frames=%u raw_nonzero=%u aec_nonzero=%u agc_nonzero=%u "
|
||||
"opus_nonzero=%u peak=%d rms=%.3f mismatches=%u result=%s", aec_enabled, agc_enabled, legacy, names[i], PhaseFrames,
|
||||
stats.raw.nonzero, stats.aec.nonzero, stats.agc.nonzero, stats.opus.nonzero, stats.opus.peak,
|
||||
std::sqrt(stats.opus.energy / (PhaseFrames * Samples)), stats.mismatches, valid ? "PASS" : "FAIL");
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CALL, "Android mute cleanup sent=%u PTS_epoch=1 allocations=%zu->%zu result=%s",
|
||||
sent, allocated, u_get_allocated_count(), ok ? "PASS" : "FAIL");
|
||||
if (!ok) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "Android mute PCM ordering regression failed aec=%d agc=%d", aec_enabled, agc_enabled);
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
Reference in new issue
Block a user