QEMU main repository: Please see https://www.qemu.org/docs/master/devel/submitting-a-patch.html for how to submit changes to QEMU. Pull Requests are ignored. Please only use release tarballs from the QEMU website. http://www.qemu.org
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.
599 lines
17 KiB
599 lines
17 KiB
/* 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,
|
|
.endianness = 0,
|
|
};
|
|
|
|
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 = AUD_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 AUD_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 = AUD_write(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);
|
|
AUD_set_active_out(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(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);
|
|
|
|
AUD_set_active_out(state.voice, false);
|
|
AUD_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(AUD_is_active_out(voice));
|
|
|
|
AUD_set_active_out(voice, true);
|
|
g_assert_true(AUD_is_active_out(voice));
|
|
|
|
AUD_set_active_out(voice, false);
|
|
g_assert_false(AUD_is_active_out(voice));
|
|
|
|
AUD_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 = AUD_get_buffer_size_out(voice);
|
|
g_test_message("Buffer size: %d bytes", buffer_size);
|
|
g_assert_cmpint(buffer_size, >, 0);
|
|
|
|
AUD_close_out(be, voice);
|
|
|
|
g_assert_cmpint(AUD_get_buffer_size_out(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} };
|
|
AUD_set_volume_out(voice, &vol);
|
|
|
|
vol = (Volume){ .mute = true, .channels = 2, .vol = {255, 255} };
|
|
AUD_set_volume_out(voice, &vol);
|
|
|
|
vol = (Volume){ .mute = false, .channels = 2, .vol = {128, 128} };
|
|
AUD_set_volume_out(voice, &vol);
|
|
|
|
AUD_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(AUD_is_active_in(voice));
|
|
|
|
AUD_set_active_in(voice, true);
|
|
g_assert_true(AUD_is_active_in(voice));
|
|
|
|
AUD_set_active_in(voice, false);
|
|
g_assert_false(AUD_is_active_in(voice));
|
|
|
|
AUD_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} };
|
|
AUD_set_volume_in(voice, &vol);
|
|
|
|
vol = (Volume){ .mute = true, .channels = 2, .vol = {255, 255} };
|
|
AUD_set_volume_in(voice, &vol);
|
|
|
|
AUD_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 = AUD_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;
|
|
|
|
AUD_set_active_out(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(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);
|
|
}
|
|
|
|
AUD_set_active_out(voice, false);
|
|
AUD_close_out(be, voice);
|
|
|
|
AUD_del_capture(cap, &state);
|
|
g_assert_true(state.destroy_called);
|
|
|
|
g_free(state.captured_samples);
|
|
}
|
|
|
|
static void test_audio_null_handling(void)
|
|
{
|
|
uint8_t buffer[64];
|
|
|
|
/* AUD_is_active_out/in(NULL) should return false */
|
|
g_assert_false(AUD_is_active_out(NULL));
|
|
g_assert_false(AUD_is_active_in(NULL));
|
|
|
|
/* AUD_get_buffer_size_out(NULL) should return 0 */
|
|
g_assert_cmpint(AUD_get_buffer_size_out(NULL), ==, 0);
|
|
|
|
/* AUD_write/read(NULL, ...) should return size (no-op) */
|
|
g_assert_cmpuint(AUD_write(NULL, buffer, sizeof(buffer)), ==,
|
|
sizeof(buffer));
|
|
g_assert_cmpuint(AUD_read(NULL, buffer, sizeof(buffer)), ==,
|
|
sizeof(buffer));
|
|
|
|
/* These should not crash */
|
|
AUD_set_active_out(NULL, true);
|
|
AUD_set_active_out(NULL, false);
|
|
AUD_set_active_in(NULL, true);
|
|
AUD_set_active_in(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);
|
|
|
|
AUD_set_active_out(out1, true);
|
|
AUD_set_active_out(out2, true);
|
|
AUD_set_active_in(in1, true);
|
|
|
|
g_assert_true(AUD_is_active_out(out1));
|
|
g_assert_true(AUD_is_active_out(out2));
|
|
if (in1) {
|
|
g_assert_true(AUD_is_active_in(in1));
|
|
}
|
|
|
|
AUD_set_active_out(out1, false);
|
|
AUD_set_active_out(out2, false);
|
|
AUD_set_active_in(in1, false);
|
|
|
|
AUD_close_in(be, in1);
|
|
AUD_close_out(be, out2);
|
|
AUD_close_out(be, out1);
|
|
}
|
|
|
|
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);
|
|
|
|
ret = g_test_run();
|
|
|
|
audio_cleanup();
|
|
|
|
return ret;
|
|
}
|
|
|