blob: af8cf03d10ef3f309d3b0221967d2cc238c340e6 [file] [log] [blame]
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include "qemu/osdep.h"
#include "qemu/config-file.h"
#include "qemu/cutils.h"
#include "qemu/help_option.h"
#include "qemu/module.h"
#include "qemu/main-loop.h"
#include "qemu/audio.h"
#include "qemu/log.h"
#include "qapi/error.h"
#include "trace/control.h"
#include "glib.h"
#include "audio/audio_int.h"
#include <math.h>
#ifdef CONFIG_SDL
/*
* SDL insists on wrapping the main() function with its own implementation on
* some platforms; it does so via a macro that renames our main function, so
* <SDL.h> must be #included here even with no SDL code called from this file.
*/
#include <SDL.h>
#endif
#define SAMPLE_RATE 44100
#define CHANNELS 2
#define DURATION_SECS 2
#define FREQUENCY 440.0
#define BUFFER_FRAMES 1024
#define TIMEOUT_SECS (DURATION_SECS + 1)
/* Command-line options */
static gchar *opt_audiodev;
static gchar *opt_trace;
static GOptionEntry test_options[] = {
{ "audiodev", 'a', 0, G_OPTION_ARG_STRING, &opt_audiodev,
"Audio device spec (e.g., none or pa,out.buffer-length=50000)", "DEV" },
{ "trace", 'T', 0, G_OPTION_ARG_STRING, &opt_trace,
"Trace options (e.g., 'pw_*')", "TRACE" },
{ NULL }
};
#define TEST_AUDIODEV_ID "test"
typedef struct TestSineState {
AudioBackend *be;
SWVoiceOut *voice;
int64_t total_frames;
int64_t frames_written;
} TestSineState;
/* Default audio settings for tests */
static const struct audsettings default_test_settings = {
.freq = SAMPLE_RATE,
.nchannels = CHANNELS,
.fmt = AUDIO_FORMAT_S16,
.big_endian = false,
};
static void dummy_audio_callback(void *opaque, int avail)
{
}
static AudioBackend *get_test_audio_backend(void)
{
AudioBackend *be;
Error *err = NULL;
if (opt_audiodev) {
be = audio_be_by_name(TEST_AUDIODEV_ID, &err);
} else {
be = audio_get_default_audio_be(&err);
}
if (err) {
g_error("%s", error_get_pretty(err));
error_free(err);
exit(1);
}
g_assert_nonnull(be);
return be;
}
/*
* Helper functions for opening test voices with default settings.
* These reduce boilerplate in test functions.
*/
static SWVoiceOut *open_test_voice_out(AudioBackend *be, const char *name,
void *opaque, audio_callback_fn cb)
{
struct audsettings as = default_test_settings;
SWVoiceOut *voice;
voice = audio_be_open_out(be, NULL, name, opaque, cb, &as);
g_assert_nonnull(voice);
return voice;
}
static SWVoiceIn *open_test_voice_in(AudioBackend *be, const char *name,
void *opaque, audio_callback_fn cb)
{
struct audsettings as = default_test_settings;
return audio_be_open_in(be, NULL, name, opaque, cb, &as);
}
/*
* Generate 440Hz sine wave samples into buffer.
*/
static void generate_sine_samples(int16_t *buffer, int frames,
int64_t start_frame)
{
for (int i = 0; i < frames; i++) {
double t = (double)(start_frame + i) / SAMPLE_RATE;
double sample = sin(2.0 * M_PI * FREQUENCY * t);
int16_t s = (int16_t)(sample * 32767.0);
buffer[i * 2] = s; /* left channel */
buffer[i * 2 + 1] = s; /* right channel */
}
}
static void test_sine_callback(void *opaque, int avail)
{
TestSineState *s = opaque;
int16_t buffer[BUFFER_FRAMES * CHANNELS];
int frames_remaining;
int frames_to_write;
size_t bytes_written;
frames_remaining = s->total_frames - s->frames_written;
if (frames_remaining <= 0) {
return;
}
frames_to_write = avail / (sizeof(int16_t) * CHANNELS);
frames_to_write = MIN(frames_to_write, BUFFER_FRAMES);
frames_to_write = MIN(frames_to_write, frames_remaining);
generate_sine_samples(buffer, frames_to_write, s->frames_written);
bytes_written = audio_be_write(s->be, s->voice, buffer,
frames_to_write * sizeof(int16_t) * CHANNELS);
s->frames_written += bytes_written / (sizeof(int16_t) * CHANNELS);
}
static void test_audio_out_sine_wave(void)
{
TestSineState state = {0};
int64_t start_time;
int64_t elapsed_ms;
state.be = get_test_audio_backend();
state.total_frames = SAMPLE_RATE * DURATION_SECS;
state.frames_written = 0;
g_test_message("Opening audio output...");
state.voice = open_test_voice_out(state.be, "test-sine",
&state, test_sine_callback);
g_test_message("Playing 440Hz sine wave for %d seconds...", DURATION_SECS);
audio_be_set_active_out(state.be, state.voice, true);
/*
* Run the audio subsystem until all frames are written or timeout.
*/
start_time = g_get_monotonic_time();
while (state.frames_written < state.total_frames) {
audio_run(AUDIO_MIXENG_BACKEND(state.be), "test");
main_loop_wait(true);
elapsed_ms = (g_get_monotonic_time() - start_time) / 1000;
if (elapsed_ms > TIMEOUT_SECS * 1000) {
g_test_message("Timeout waiting for audio to complete");
break;
}
g_usleep(G_USEC_PER_SEC / 100); /* 10ms */
}
g_test_message("Wrote %" PRId64 " frames (%.2f seconds)",
state.frames_written,
(double)state.frames_written / SAMPLE_RATE);
g_assert_cmpint(state.frames_written, ==, state.total_frames);
audio_be_set_active_out(state.be, state.voice, false);
audio_be_close_out(state.be, state.voice);
}
static void test_audio_prio_list(void)
{
g_autofree gchar *backends = NULL;
GString *str = g_string_new(NULL);
bool has_none = false;
for (int i = 0; audio_prio_list[i]; i++) {
if (i > 0) {
g_string_append_c(str, ' ');
}
g_string_append(str, audio_prio_list[i]);
if (g_strcmp0(audio_prio_list[i], "none") == 0) {
has_none = true;
}
}
backends = g_string_free(str, FALSE);
g_test_message("Available backends: %s", backends);
/* The 'none' backend should always be available */
g_assert_true(has_none);
}
static void test_audio_out_active_state(void)
{
AudioBackend *be;
SWVoiceOut *voice;
be = get_test_audio_backend();
voice = open_test_voice_out(be, "test-active", NULL, dummy_audio_callback);
g_assert_false(audio_be_is_active_out(be, voice));
audio_be_set_active_out(be, voice, true);
g_assert_true(audio_be_is_active_out(be, voice));
audio_be_set_active_out(be, voice, false);
g_assert_false(audio_be_is_active_out(be, voice));
audio_be_close_out(be, voice);
}
static void test_audio_out_buffer_size(void)
{
AudioBackend *be;
SWVoiceOut *voice;
int buffer_size;
be = get_test_audio_backend();
voice = open_test_voice_out(be, "test-buffer", NULL, dummy_audio_callback);
buffer_size = audio_be_get_buffer_size_out(be, voice);
g_test_message("Buffer size: %d bytes", buffer_size);
g_assert_cmpint(buffer_size, >, 0);
audio_be_close_out(be, voice);
g_assert_cmpint(audio_be_get_buffer_size_out(be, NULL), ==, 0);
}
static void test_audio_out_volume(void)
{
AudioBackend *be;
SWVoiceOut *voice;
Volume vol;
be = get_test_audio_backend();
voice = open_test_voice_out(be, "test-volume", NULL, dummy_audio_callback);
vol = (Volume){ .mute = false, .channels = 2, .vol = {255, 255} };
audio_be_set_volume_out(be, voice, &vol);
vol = (Volume){ .mute = true, .channels = 2, .vol = {255, 255} };
audio_be_set_volume_out(be, voice, &vol);
vol = (Volume){ .mute = false, .channels = 2, .vol = {128, 128} };
audio_be_set_volume_out(be, voice, &vol);
audio_be_close_out(be, voice);
}
static void test_audio_in_active_state(void)
{
AudioBackend *be;
SWVoiceIn *voice;
be = get_test_audio_backend();
voice = open_test_voice_in(be, "test-in-active", NULL, dummy_audio_callback);
if (!voice) {
g_test_skip("The backend may not support input");
return;
}
g_assert_false(audio_be_is_active_in(be, voice));
audio_be_set_active_in(be, voice, true);
g_assert_true(audio_be_is_active_in(be, voice));
audio_be_set_active_in(be, voice, false);
g_assert_false(audio_be_is_active_in(be, voice));
audio_be_close_in(be, voice);
}
static void test_audio_in_volume(void)
{
AudioBackend *be;
SWVoiceIn *voice;
Volume vol;
be = get_test_audio_backend();
voice = open_test_voice_in(be, "test-in-volume", NULL, dummy_audio_callback);
if (!voice) {
g_test_skip("The backend may not support input");
return;
}
vol = (Volume){ .mute = false, .channels = 2, .vol = {255, 255} };
audio_be_set_volume_in(be, voice, &vol);
vol = (Volume){ .mute = true, .channels = 2, .vol = {255, 255} };
audio_be_set_volume_in(be, voice, &vol);
audio_be_close_in(be, voice);
}
/* Capture test state */
#define CAPTURE_BUFFER_FRAMES (SAMPLE_RATE / 10) /* 100ms of audio */
#define CAPTURE_BUFFER_SIZE (CAPTURE_BUFFER_FRAMES * CHANNELS * sizeof(int16_t))
typedef struct TestCaptureState {
bool notify_called;
bool capture_called;
bool destroy_called;
audcnotification_e last_notify;
int16_t *captured_samples;
size_t captured_bytes;
size_t capture_buffer_size;
} TestCaptureState;
static void test_capture_notify(void *opaque, audcnotification_e cmd)
{
TestCaptureState *s = opaque;
s->notify_called = true;
s->last_notify = cmd;
}
static void test_capture_capture(void *opaque, const void *buf, int size)
{
TestCaptureState *s = opaque;
size_t bytes_to_copy;
s->capture_called = true;
if (!s->captured_samples || s->captured_bytes >= s->capture_buffer_size) {
return;
}
bytes_to_copy = MIN(size, s->capture_buffer_size - s->captured_bytes);
memcpy((uint8_t *)s->captured_samples + s->captured_bytes, buf, bytes_to_copy);
s->captured_bytes += bytes_to_copy;
}
static void test_capture_destroy(void *opaque)
{
TestCaptureState *s = opaque;
s->destroy_called = true;
}
/*
* Compare captured audio with expected sine wave.
* Returns the number of matching samples (within tolerance).
*/
static int compare_sine_samples(const int16_t *captured, int frames,
int64_t start_frame, int tolerance)
{
int matching = 0;
for (int i = 0; i < frames; i++) {
double t = (double)(start_frame + i) / SAMPLE_RATE;
double sample = sin(2.0 * M_PI * FREQUENCY * t);
int16_t expected = (int16_t)(sample * 32767.0);
/* Check left channel */
if (abs(captured[i * 2] - expected) <= tolerance) {
matching++;
}
/* Check right channel */
if (abs(captured[i * 2 + 1] - expected) <= tolerance) {
matching++;
}
}
return matching;
}
static void test_audio_capture(void)
{
AudioBackend *be;
CaptureVoiceOut *cap;
SWVoiceOut *voice;
TestCaptureState state = {0};
TestSineState sine_state = {0};
struct audsettings as = default_test_settings;
struct audio_capture_ops ops = {
.notify = test_capture_notify,
.capture = test_capture_capture,
.destroy = test_capture_destroy,
};
int64_t start_time;
int64_t elapsed_ms;
int captured_frames;
int matching_samples;
int total_samples;
double match_ratio;
be = get_test_audio_backend();
state.captured_samples = g_malloc0(CAPTURE_BUFFER_SIZE);
state.captured_bytes = 0;
state.capture_buffer_size = CAPTURE_BUFFER_SIZE;
cap = audio_be_add_capture(be, &as, &ops, &state);
g_assert_nonnull(cap);
sine_state.be = be;
sine_state.total_frames = CAPTURE_BUFFER_FRAMES;
sine_state.frames_written = 0;
voice = open_test_voice_out(be, "test-capture-sine",
&sine_state, test_sine_callback);
sine_state.voice = voice;
audio_be_set_active_out(be, voice, true);
start_time = g_get_monotonic_time();
while (sine_state.frames_written < sine_state.total_frames ||
state.captured_bytes < CAPTURE_BUFFER_SIZE) {
audio_run(AUDIO_MIXENG_BACKEND(be), "test-capture");
main_loop_wait(true);
elapsed_ms = (g_get_monotonic_time() - start_time) / 1000;
if (elapsed_ms > 1000) { /* 1 second timeout */
break;
}
g_usleep(G_USEC_PER_SEC / 1000); /* 1ms */
}
g_test_message("Wrote %" PRId64 " frames, captured %zu bytes",
sine_state.frames_written, state.captured_bytes);
g_assert_true(state.capture_called);
g_assert_cmpuint(state.captured_bytes, >, 0);
/* Compare captured data with expected sine wave */
captured_frames = state.captured_bytes / (CHANNELS * sizeof(int16_t));
if (captured_frames > 0) {
/*
* Allow some tolerance due to mixing/conversion.
* The tolerance accounts for potential rounding differences.
*/
matching_samples = compare_sine_samples(state.captured_samples,
captured_frames, 0, 100);
total_samples = captured_frames * CHANNELS;
match_ratio = (double)matching_samples / total_samples;
g_test_message("Captured %d frames, %d/%d samples match (%.1f%%)",
captured_frames, matching_samples, total_samples,
match_ratio * 100.0);
/*
* Expect at least 90% of samples to match within tolerance.
* Some variation is expected due to mixing engine processing.
*/
g_assert_cmpfloat(match_ratio, >=, 0.9);
}
audio_be_set_active_out(be, voice, false);
audio_be_close_out(be, voice);
audio_be_del_capture(be, cap, &state);
g_assert_true(state.destroy_called);
g_free(state.captured_samples);
}
static void test_audio_null_handling(void)
{
AudioBackend *be = get_test_audio_backend();
uint8_t buffer[64];
/* audio_be_is_active_out/in(NULL) should return false */
g_assert_false(audio_be_is_active_out(be, NULL));
g_assert_false(audio_be_is_active_in(be, NULL));
/* audio_be_get_buffer_size_out(NULL) should return 0 */
g_assert_cmpint(audio_be_get_buffer_size_out(be, NULL), ==, 0);
/* audio_be_write/read(NULL, ...) should return 0 */
g_assert_cmpuint(audio_be_write(be, NULL, buffer, sizeof(buffer)), ==, 0);
g_assert_cmpuint(audio_be_read(be, NULL, buffer, sizeof(buffer)), ==, 0);
/* These should not crash */
audio_be_set_active_out(be, NULL, true);
audio_be_set_active_out(be, NULL, false);
audio_be_set_active_in(be, NULL, true);
audio_be_set_active_in(be, NULL, false);
}
static void test_audio_multiple_voices(void)
{
AudioBackend *be;
SWVoiceOut *out1, *out2;
SWVoiceIn *in1;
be = get_test_audio_backend();
out1 = open_test_voice_out(be, "test-multi-out1", NULL, dummy_audio_callback);
out2 = open_test_voice_out(be, "test-multi-out2", NULL, dummy_audio_callback);
in1 = open_test_voice_in(be, "test-multi-in1", NULL, dummy_audio_callback);
audio_be_set_active_out(be, out1, true);
audio_be_set_active_out(be, out2, true);
audio_be_set_active_in(be, in1, true);
g_assert_true(audio_be_is_active_out(be, out1));
g_assert_true(audio_be_is_active_out(be, out2));
if (in1) {
g_assert_true(audio_be_is_active_in(be, in1));
}
audio_be_set_active_out(be, out1, false);
audio_be_set_active_out(be, out2, false);
audio_be_set_active_in(be, in1, false);
audio_be_close_in(be, in1);
audio_be_close_out(be, out2);
audio_be_close_out(be, out1);
}
static const struct audsettings invalid_test_settings = {
.nchannels = 0,
.freq = SAMPLE_RATE,
.fmt = AUDIO_FORMAT_S16,
.big_endian = false,
};
static void test_audio_invalid_settings(void)
{
AudioBackend *be = get_test_audio_backend();
void *voice;
voice = audio_be_open_out(be, NULL, "invalid", NULL,
dummy_audio_callback, &invalid_test_settings);
g_assert_null(voice);
voice = audio_be_open_in(be, NULL, "invalid", NULL,
dummy_audio_callback, &invalid_test_settings);
g_assert_null(voice);
}
int main(int argc, char **argv)
{
GOptionContext *context;
g_autoptr(GError) error = NULL;
g_autofree gchar *dir = NULL;
int ret;
context = g_option_context_new("- QEMU audio test");
g_option_context_add_main_entries(context, test_options, NULL);
if (!g_option_context_parse(context, &argc, &argv, &error)) {
g_printerr("Option parsing failed: %s\n", error->message);
return 1;
}
g_option_context_free(context);
g_test_init(&argc, &argv, NULL);
module_call_init(MODULE_INIT_TRACE);
qemu_add_opts(&qemu_trace_opts);
if (opt_trace) {
trace_opt_parse(opt_trace);
qemu_set_log(LOG_TRACE, &error_fatal);
}
trace_init_backends();
trace_init_file();
dir = g_test_build_filename(G_TEST_BUILT, "..", "..", NULL);
g_setenv("QEMU_MODULE_DIR", dir, true);
qemu_init_exec_dir(argv[0]);
module_call_init(MODULE_INIT_QOM);
module_init_info(qemu_modinfo);
qemu_init_main_loop(&error_abort);
if (opt_audiodev) {
g_autofree gchar *spec = is_help_option(opt_audiodev) ?
opt_audiodev : g_strdup_printf("%s,id=%s", opt_audiodev, TEST_AUDIODEV_ID);
audio_parse_option(spec);
}
audio_create_default_audiodevs();
audio_init_audiodevs();
g_test_add_func("/audio/prio-list", test_audio_prio_list);
g_test_add_func("/audio/out/active-state", test_audio_out_active_state);
g_test_add_func("/audio/out/sine-wave", test_audio_out_sine_wave);
g_test_add_func("/audio/out/buffer-size", test_audio_out_buffer_size);
g_test_add_func("/audio/out/volume", test_audio_out_volume);
g_test_add_func("/audio/out/capture", test_audio_capture);
g_test_add_func("/audio/in/active-state", test_audio_in_active_state);
g_test_add_func("/audio/in/volume", test_audio_in_volume);
g_test_add_func("/audio/null-handling", test_audio_null_handling);
g_test_add_func("/audio/multiple-voices", test_audio_multiple_voices);
g_test_add_func("/audio/invalid-settings", test_audio_invalid_settings);
ret = g_test_run();
audio_cleanup();
return ret;
}