You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
163 lines
7.6 KiB
163 lines
7.6 KiB
/* Реальные ветки miniaudio на null backend: аппаратный звук и сеть не нужны. */ |
|
#include "../transport/audio_diagnostics.h" |
|
#include "../../../lib/speex_aec.h" |
|
#include <assert.h> |
|
#include <stdint.h> |
|
#include <stdio.h> |
|
#include <string.h> |
|
|
|
static uint64_t observed_unfilled, observed_no_progress, observed_holes; |
|
static uint32_t callback_count; |
|
static void test_event(const void* device, int kind, int stage, int64_t value, int64_t detail) { |
|
if (kind == AUDIO_DIAG_UNFILLED) observed_unfilled += (uint64_t)value; |
|
if (kind == AUDIO_DIAG_NO_PROGRESS) observed_no_progress += (uint64_t)value; |
|
if (kind == AUDIO_DIAG_HOLE) observed_holes += (uint64_t)value; |
|
audio_diag_event(device, kind, stage, value, detail); |
|
} |
|
#define MA_DIAGNOSTIC_EVENT(device, kind, stage, value, detail) test_event(device, kind, stage, value, detail) |
|
#define MA_DIAGNOSTIC_LOG(log, level, message) audio_diag_log(log, level, message) |
|
#define MA_PULSE_WAIT_TIMEOUT_MS 20 |
|
#define MINIAUDIO_IMPLEMENTATION |
|
#include "../../../lib/miniaudio.h" |
|
|
|
static void callback(ma_device* device, void* out, const void* in, ma_uint32 count) { |
|
(void)device; (void)in; |
|
callback_count++; |
|
if (out) for (ma_uint32 i = 0; i < count; i++) ((int16_t*)out)[i] = 12000; |
|
} |
|
|
|
static void starvation(int duplex) { |
|
ma_backend backend = ma_backend_null; |
|
ma_context context; |
|
ma_device device; |
|
ma_device_config config = ma_device_config_init(duplex ? ma_device_type_duplex : ma_device_type_playback); |
|
config.capture.format = config.playback.format = ma_format_s16; |
|
config.capture.channels = config.playback.channels = 1; |
|
config.sampleRate = 48000; config.periodSizeInFrames = 480; config.dataCallback = callback; |
|
assert(ma_context_init(&backend, 1, NULL, &context) == MA_SUCCESS); |
|
assert(ma_device_init(&context, &config, &device) == MA_SUCCESS); |
|
if (duplex) assert(ma_duplex_rb_init(ma_format_s16, 1, 48000, 48000, 480, NULL, &device.duplexRB) == MA_SUCCESS); |
|
/* Worker не запускаем: стенд синхронно вызывает backend-обработчик. */ |
|
ma_device__set_state(&device, ma_device_state_started); |
|
audio_diag_label(&device, duplex ? 1 : 2, duplex ? "test-duplex" : "test-playback"); |
|
int16_t input[480] = {0}, output[480]; |
|
observed_unfilled = 0; callback_count = 0; |
|
uint64_t untouched = 0; |
|
for (int tick = 0; tick < 20; tick++) { |
|
if (duplex && tick < 3) assert(ma_device_handle_backend_data_callback(&device, NULL, input, 480) == MA_SUCCESS); |
|
for (int i = 0; i < 480; i++) output[i] = 2345; |
|
assert(ma_device_handle_backend_data_callback(&device, output, NULL, 480) == MA_SUCCESS); |
|
for (int i = 0; i < 480; i++) untouched += output[i] == 2345; |
|
} |
|
assert(untouched == observed_unfilled); |
|
assert(untouched == 0 && callback_count >= 20); |
|
printf("starvation duplex=%d unfilled=%llu callback_count=%u PASS\n", duplex, |
|
(unsigned long long)untouched, callback_count); |
|
ma_device__set_state(&device, ma_device_state_stopped); |
|
if (duplex) ma_duplex_rb_uninit(&device.duplexRB); |
|
ma_device_uninit(&device); ma_context_uninit(&context); |
|
} |
|
|
|
#ifdef MA_SUPPORT_PULSEAUDIO |
|
static size_t zero_writable(const ma_pa_stream* stream) { |
|
(void)stream; |
|
/* Без внешнего stop callback обязан вернуть управление сам. */ |
|
return 0; |
|
} |
|
static void no_progress(void) { |
|
ma_context context; |
|
ma_device device; |
|
memset(&context, 0, sizeof(context)); memset(&device, 0, sizeof(device)); |
|
device.pContext = &context; |
|
device.playback.internalFormat = ma_format_s16; device.playback.internalChannels = 1; |
|
context.pulse.pa_stream_writable_size = (ma_proc)zero_writable; |
|
ma_device__set_state(&device, ma_device_state_started); |
|
observed_no_progress = 0; |
|
ma_device_on_write__pulse((ma_pa_stream*)&device, 960, &device); |
|
assert(observed_no_progress == 1); |
|
audio_diag_event(&device, AUDIO_DIAG_CLOSE, -1, 0, 0); |
|
puts("PulseAudio zero-progress detection PASS"); |
|
} |
|
static unsigned int wait_cancelled, wait_unrefed; |
|
static ma_pa_operation_state_t wait_state(const ma_pa_operation* operation) { (void)operation; return MA_PA_OPERATION_RUNNING; } |
|
static void wait_cancel(ma_pa_operation* operation) { (void)operation; ++wait_cancelled; } |
|
static void wait_unref(ma_pa_operation* operation) { (void)operation; ++wait_unrefed; } |
|
static int wait_iterate(ma_pa_mainloop* loop, int block, int* result) { |
|
(void)loop; (void)result; |
|
assert(block == 0); |
|
return 0; |
|
} |
|
static void bounded_wait(void) { |
|
ma_context context; |
|
ma_timer timer; |
|
memset(&context, 0, sizeof(context)); |
|
context.pulse.pa_operation_get_state = (ma_proc)wait_state; |
|
context.pulse.pa_operation_cancel = (ma_proc)wait_cancel; |
|
context.pulse.pa_operation_unref = (ma_proc)wait_unref; |
|
context.pulse.pa_mainloop_iterate = (ma_proc)wait_iterate; |
|
wait_cancelled = wait_unrefed = 0; |
|
ma_timer_init(&timer); |
|
assert(ma_wait_for_operation_and_unref__pulse(&context, NULL, (ma_pa_operation*)&context) == MA_TIMEOUT); |
|
assert(wait_cancelled == 1 && wait_unrefed == 1); |
|
assert(ma_timer_get_time_in_seconds(&timer) < 0.5); |
|
puts("PulseAudio deadline and callback cancellation PASS"); |
|
} |
|
|
|
static unsigned int hole_frames, hole_drops; |
|
static int hole_peek(ma_pa_stream* stream, const void** data, size_t* bytes) { |
|
(void)stream; *data = NULL; *bytes = 9600; return 0; |
|
} |
|
static int hole_drop(ma_pa_stream* stream) { (void)stream; ++hole_drops; return 0; } |
|
static void hole_capture(ma_device* device, void* out, const void* in, ma_uint32 frames) { |
|
(void)device; (void)out; |
|
assert(in); |
|
for (ma_uint32 i = 0; i < frames; ++i) assert(((const int16_t*)in)[i] == 0); |
|
hole_frames += frames; |
|
} |
|
static void capture_hole(void) { |
|
ma_backend backend = ma_backend_null; |
|
ma_context context; |
|
ma_device device; |
|
ma_device_config config = ma_device_config_init(ma_device_type_capture); |
|
config.capture.format = ma_format_s16; config.capture.channels = 1; config.sampleRate = 48000; |
|
config.noFixedSizedCallback = MA_TRUE; config.dataCallback = hole_capture; |
|
assert(ma_context_init(&backend, 1, NULL, &context) == MA_SUCCESS); |
|
assert(ma_device_init(&context, &config, &device) == MA_SUCCESS); |
|
context.pulse.pa_stream_peek = (ma_proc)hole_peek; |
|
context.pulse.pa_stream_drop = (ma_proc)hole_drop; |
|
observed_holes = hole_frames = hole_drops = 0; |
|
ma_device__set_state(&device, ma_device_state_started); |
|
ma_device_on_read__pulse((ma_pa_stream*)&device, 9600, &device); |
|
assert(observed_holes == 4800 && hole_frames == 4800 && hole_drops == 1); |
|
ma_device__set_state(&device, ma_device_state_stopped); |
|
ma_device_uninit(&device); ma_context_uninit(&context); |
|
puts("PulseAudio capture hole preserves duration and silence PASS"); |
|
} |
|
|
|
#endif |
|
|
|
/* Характеризация найденной потери хвоста; после исправления ожидаем два кадра. */ |
|
static void aec_variable_chunks(void) { |
|
speex_aec_t* aec = speex_aec_create(48000, 960, 14400, 8); |
|
int16_t pcm[1920] = {0}; |
|
assert(aec != NULL); |
|
speex_aec_feed_playback(aec, pcm, 1200); |
|
speex_aec_feed_playback(aec, pcm + 1200, 720); |
|
const int frames = speex_aec_delay_fill(aec); |
|
assert(frames == 2); |
|
printf("AEC characterization: input=1920 queued=%d*960; all samples preserved\n", frames); |
|
speex_aec_destroy(aec); |
|
} |
|
|
|
int main(void) { |
|
audio_diag_start(); |
|
starvation(1); starvation(0); |
|
#ifdef MA_SUPPORT_PULSEAUDIO |
|
no_progress(); |
|
bounded_wait(); |
|
capture_hole(); |
|
#endif |
|
aec_variable_chunks(); |
|
audio_diag_stop(); |
|
return 0; |
|
}
|
|
|