From nobody Sun Jan 25 12:01:04 2026 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=quarantine dis=none) header.from=redhat.com ARC-Seal: i=1; a=rsa-sha256; t=1769154739; cv=none; d=zohomail.com; s=zohoarc; b=YeAAWukSpvc5bILhpqLNDJpfzgvqaVD+B7h0sD2JSzNO5TyQidiPOIt61trAh2dw9Ndldt01Aq2X3PF7fmRSkVjYTqWyq+tbO1RPmSb6fmPmTV13mT202wT7IGKYWeRxcF3XHAyR1HTcqbVW0WOAcIIIbj21Mwy+U4JkShxNaBc= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1769154739; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=bfgH24tvSj7B4y7zWvUOy2L+dIbpQArN9TNLYLlSc04=; b=HMZEkbJnLnUkqZ0gaaPb0OBxw0KXZbBXb6OheB4y2E72u5+aHZi8CLDqXglalm7rvh035v13KKq3tVsv4Yx7H5LLjtNIA10R7XC1IMkNsvkr//iJXj24BQ+zHsFpq+rltmJPHZVJHz5cDSbskSzOlraWWO6B4MH8XvoXCKbIm28= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=quarantine dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1769154739833884.7048621106741; Thu, 22 Jan 2026 23:52:19 -0800 (PST) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1vjBxg-0004AS-4P; Fri, 23 Jan 2026 02:52:10 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1vjBxA-0003la-3p for qemu-devel@nongnu.org; Fri, 23 Jan 2026 02:51:36 -0500 Received: from us-smtp-delivery-124.mimecast.com ([170.10.133.124]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1vjBx7-0002AB-Gp for qemu-devel@nongnu.org; Fri, 23 Jan 2026 02:51:35 -0500 Received: from mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (ec2-35-165-154-97.us-west-2.compute.amazonaws.com [35.165.154.97]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-634-Jc9Wq8wBOsan_urlAzADoA-1; Fri, 23 Jan 2026 02:51:29 -0500 Received: from mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.12]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mx-prod-mc-06.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id 1D0DB1800451; Fri, 23 Jan 2026 07:51:28 +0000 (UTC) Received: from localhost (unknown [10.45.242.5]) by mx-prod-int-03.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id 3DD251958DC1; Fri, 23 Jan 2026 07:51:25 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1769154692; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=bfgH24tvSj7B4y7zWvUOy2L+dIbpQArN9TNLYLlSc04=; b=RWRnrjcMgfgip46mphvewbA7eBaUi2GKm/Zw5HlYGmi3JUBtcu4yxQysYYJ0KoZloUjEJc cbJj9qOYHg7s54lssj4Yr0ueDlwd0YtJ8txmxRMuJoyUqDnubfU/mYlyZ7Yq/W6wtwQF1t 5sQysXH4RoB22iKIsQo4Lgn66TOxrts= X-MC-Unique: Jc9Wq8wBOsan_urlAzADoA-1 X-Mimecast-MFC-AGG-ID: Jc9Wq8wBOsan_urlAzADoA_1769154688 From: marcandre.lureau@redhat.com To: qemu-devel@nongnu.org Cc: =?UTF-8?q?Philippe=20Mathieu-Daud=C3=A9?= , Gerd Hoffmann , =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= , Paolo Bonzini , =?UTF-8?q?Daniel=20P=2E=20Berrang=C3=A9?= Subject: [PATCH 16/37] tests: start manual audio backend test Date: Fri, 23 Jan 2026 11:49:19 +0400 Message-ID: <20260123074945.2563196-17-marcandre.lureau@redhat.com> In-Reply-To: <20260123074945.2563196-1-marcandre.lureau@redhat.com> References: <20260123074945.2563196-1-marcandre.lureau@redhat.com> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-Scanned-By: MIMEDefang 3.0 on 10.30.177.12 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=170.10.133.124; envelope-from=marcandre.lureau@redhat.com; helo=us-smtp-delivery-124.mimecast.com X-Spam_score_int: -21 X-Spam_score: -2.2 X-Spam_bar: -- X-Spam_report: (-2.2 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.07, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_NONE=-0.0001, RCVD_IN_MSPIKE_H3=0.001, RCVD_IN_MSPIKE_WL=0.001, RCVD_IN_VALIDITY_CERTIFIED_BLOCKED=0.001, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001, T_FILL_THIS_FORM_SHORT=0.01 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: qemu development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @redhat.com) X-ZM-MESSAGEID: 1769154740750158500 From: Marc-Andr=C3=A9 Lureau Start a simple test program that will exercise the QEMU audio APIs. It is meant to run manually for now, as it accesses the sound system and produces sound by default, and also runs for a few seconds. We may want to make it silent or use the "none" (noaudio) backend by default though, so it can run as part of the automated test suite. Signed-off-by: Marc-Andr=C3=A9 Lureau --- meson.build | 15 +- tests/audio/audio-stubs.c | 46 +++ tests/audio/test-audio.c | 583 ++++++++++++++++++++++++++++++++++++++ tests/audio/meson.build | 18 ++ tests/meson.build | 1 + 5 files changed, 658 insertions(+), 5 deletions(-) create mode 100644 tests/audio/audio-stubs.c create mode 100644 tests/audio/test-audio.c create mode 100644 tests/audio/meson.build diff --git a/meson.build b/meson.build index d9e2834f6e8..240a2a0473a 100644 --- a/meson.build +++ b/meson.build @@ -3869,6 +3869,7 @@ target_modules +=3D { 'accel' : { 'qtest': qtest_modu= le_ss }} modinfo_collect =3D find_program('scripts/modinfo-collect.py') modinfo_generate =3D find_program('scripts/modinfo-generate.py') modinfo_files =3D [] +audio_modinfo_files =3D [] =20 block_mods =3D [] system_mods =3D [] @@ -3896,11 +3897,15 @@ foreach d, list : modules install: true, install_dir: qemu_moddir) if module_ss.sources() !=3D [] - modinfo_files +=3D custom_target(d + '-' + m + '.modinfo', - output: d + '-' + m + '.modinfo', - input: sl.extract_all_objects(recur= sive: true), - capture: true, - command: [modinfo_collect, '@INPUT@= ']) + modinfo =3D custom_target(d + '-' + m + '.modinfo', + output: d + '-' + m + '.modinfo', + input: sl.extract_all_objects(recursive: t= rue), + capture: true, + command: [modinfo_collect, '@INPUT@']) + modinfo_files +=3D modinfo + if d =3D=3D 'audio' + audio_modinfo_files +=3D modinfo + endif endif else if d =3D=3D 'block' diff --git a/tests/audio/audio-stubs.c b/tests/audio/audio-stubs.c new file mode 100644 index 00000000000..0bcdb6ce7a3 --- /dev/null +++ b/tests/audio/audio-stubs.c @@ -0,0 +1,46 @@ +/* + * Stubs for audio test - provides missing functions for standalone audio = test + */ + +#include "qemu/osdep.h" +#include "qemu/dbus.h" +#include "ui/qemu-spice-module.h" +#include "ui/dbus-module.h" +#include "system/replay.h" +#include "system/runstate.h" + +int using_spice; +int using_dbus_display; + +struct QemuSpiceOps qemu_spice; + +GQuark dbus_display_error_quark(void) +{ + return g_quark_from_static_string("dbus-display-error-quark"); +} + +void replay_audio_in(size_t *recorded, st_sample *samples, + size_t *wpos, size_t size) +{ +} + +void replay_audio_out(size_t *played) +{ +} + +static int dummy_vmse; + +VMChangeStateEntry *qemu_add_vm_change_state_handler(VMChangeStateHandler = *cb, + void *opaque) +{ + return (VMChangeStateEntry *)&dummy_vmse; +} + +void qemu_del_vm_change_state_handler(VMChangeStateEntry *e) +{ +} + +bool runstate_is_running(void) +{ + return true; +} diff --git a/tests/audio/test-audio.c b/tests/audio/test-audio.c new file mode 100644 index 00000000000..38e30f47f89 --- /dev/null +++ b/tests/audio/test-audio.c @@ -0,0 +1,583 @@ +/* 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 + +#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[] =3D { + { "audiodev", 'a', 0, G_OPTION_ARG_STRING, &opt_audiodev, + "Audio device spec (e.g., none or pa,out.buffer-length=3D50000)", "D= EV" }, + { "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 =3D { + .freq =3D SAMPLE_RATE, + .nchannels =3D CHANNELS, + .fmt =3D AUDIO_FORMAT_S16, + .endianness =3D 0, +}; + +static void dummy_audio_callback(void *opaque, int avail) +{ +} + +static AudioBackend *get_test_audio_backend(void) +{ + AudioBackend *be; + Error *err =3D NULL; + + if (opt_audiodev) { + be =3D audio_be_by_name(TEST_AUDIODEV_ID, &err); + } else { + be =3D 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 =3D default_test_settings; + SWVoiceOut *voice; + + voice =3D 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 =3D default_test_settings; + SWVoiceIn *voice; + + voice =3D AUD_open_in(be, NULL, name, opaque, cb, &as); + g_assert_nonnull(voice); + return voice; +} + +/* + * Generate 440Hz sine wave samples into buffer. + */ +static void generate_sine_samples(int16_t *buffer, int frames, + int64_t start_frame) +{ + for (int i =3D 0; i < frames; i++) { + double t =3D (double)(start_frame + i) / SAMPLE_RATE; + double sample =3D sin(2.0 * M_PI * FREQUENCY * t); + int16_t s =3D (int16_t)(sample * 32767.0); + + buffer[i * 2] =3D s; /* left channel */ + buffer[i * 2 + 1] =3D s; /* right channel */ + } +} + +static void test_sine_callback(void *opaque, int avail) +{ + TestSineState *s =3D opaque; + int16_t buffer[BUFFER_FRAMES * CHANNELS]; + int frames_remaining; + int frames_to_write; + size_t bytes_written; + + frames_remaining =3D s->total_frames - s->frames_written; + if (frames_remaining <=3D 0) { + return; + } + + frames_to_write =3D avail / (sizeof(int16_t) * CHANNELS); + frames_to_write =3D MIN(frames_to_write, BUFFER_FRAMES); + frames_to_write =3D MIN(frames_to_write, frames_remaining); + + generate_sine_samples(buffer, frames_to_write, s->frames_written); + + bytes_written =3D AUD_write(s->voice, buffer, + frames_to_write * sizeof(int16_t) * CHANNELS= ); + s->frames_written +=3D bytes_written / (sizeof(int16_t) * CHANNELS); +} + + +static void test_audio_out_sine_wave(void) +{ + TestSineState state =3D {0}; + int64_t start_time; + int64_t elapsed_ms; + + state.be =3D get_test_audio_backend(); + state.total_frames =3D SAMPLE_RATE * DURATION_SECS; + state.frames_written =3D 0; + + g_test_message("Opening audio output..."); + state.voice =3D open_test_voice_out(state.be, "test-sine", + &state, test_sine_callback); + + g_test_message("Playing 440Hz sine wave for %d seconds...", DURATION_S= ECS); + AUD_set_active_out(state.voice, true); + + /* + * Run the audio subsystem until all frames are written or timeout. + */ + start_time =3D g_get_monotonic_time(); + while (state.frames_written < state.total_frames) { + audio_run(state.be, "test"); + main_loop_wait(true); + + elapsed_ms =3D (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, =3D=3D, 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 =3D NULL; + GString *str =3D g_string_new(NULL); + bool has_none =3D false; + + for (int i =3D 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") =3D=3D 0) { + has_none =3D true; + } + } + + backends =3D 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 =3D get_test_audio_backend(); + voice =3D open_test_voice_out(be, "test-active", NULL, dummy_audio_cal= lback); + + 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 =3D get_test_audio_backend(); + voice =3D open_test_voice_out(be, "test-buffer", NULL, dummy_audio_cal= lback); + + buffer_size =3D 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), =3D=3D, 0); +} + +static void test_audio_out_volume(void) +{ + AudioBackend *be; + SWVoiceOut *voice; + Volume vol; + + be =3D get_test_audio_backend(); + voice =3D open_test_voice_out(be, "test-volume", NULL, dummy_audio_cal= lback); + + vol =3D (Volume){.mute =3D false, .channels =3D 2, .vol =3D {255, 255}= }; + AUD_set_volume_out(voice, &vol); + + vol =3D (Volume){.mute =3D true, .channels =3D 2, .vol =3D {255, 255}}; + AUD_set_volume_out(voice, &vol); + + vol =3D (Volume){.mute =3D false, .channels =3D 2, .vol =3D {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 =3D get_test_audio_backend(); + voice =3D open_test_voice_in(be, "test-in-active", NULL, dummy_audio_c= allback); + + 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 =3D get_test_audio_backend(); + voice =3D open_test_voice_in(be, "test-in-volume", NULL, dummy_audio_c= allback); + + vol =3D (Volume){.mute =3D false, .channels =3D 2, .vol =3D {255, 255}= }; + AUD_set_volume_in(voice, &vol); + + vol =3D (Volume){.mute =3D true, .channels =3D 2, .vol =3D {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(i= nt16_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 =3D opaque; + s->notify_called =3D true; + s->last_notify =3D cmd; +} + +static void test_capture_capture(void *opaque, const void *buf, int size) +{ + TestCaptureState *s =3D opaque; + size_t bytes_to_copy; + + s->capture_called =3D true; + + if (!s->captured_samples || s->captured_bytes >=3D s->capture_buffer_s= ize) { + return; + } + + bytes_to_copy =3D 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 +=3D bytes_to_copy; +} + +static void test_capture_destroy(void *opaque) +{ + TestCaptureState *s =3D opaque; + s->destroy_called =3D 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 =3D 0; + + for (int i =3D 0; i < frames; i++) { + double t =3D (double)(start_frame + i) / SAMPLE_RATE; + double sample =3D sin(2.0 * M_PI * FREQUENCY * t); + int16_t expected =3D (int16_t)(sample * 32767.0); + + /* Check left channel */ + if (abs(captured[i * 2] - expected) <=3D tolerance) { + matching++; + } + /* Check right channel */ + if (abs(captured[i * 2 + 1] - expected) <=3D tolerance) { + matching++; + } + } + + return matching; +} + +static void test_audio_capture(void) +{ + AudioBackend *be; + CaptureVoiceOut *cap; + SWVoiceOut *voice; + TestCaptureState state =3D {0}; + TestSineState sine_state =3D {0}; + struct audsettings as =3D default_test_settings; + struct audio_capture_ops ops =3D { + .notify =3D test_capture_notify, + .capture =3D test_capture_capture, + .destroy =3D test_capture_destroy, + }; + int64_t start_time; + int64_t elapsed_ms; + int captured_frames; + int matching_samples; + int total_samples; + double match_ratio; + + be =3D get_test_audio_backend(); + + state.captured_samples =3D g_malloc0(CAPTURE_BUFFER_SIZE); + state.captured_bytes =3D 0; + state.capture_buffer_size =3D CAPTURE_BUFFER_SIZE; + + cap =3D AUD_add_capture(be, &as, &ops, &state); + g_assert_nonnull(cap); + + sine_state.be =3D be; + sine_state.total_frames =3D CAPTURE_BUFFER_FRAMES; + sine_state.frames_written =3D 0; + + voice =3D open_test_voice_out(be, "test-capture-sine", + &sine_state, test_sine_callback); + sine_state.voice =3D voice; + + AUD_set_active_out(voice, true); + + start_time =3D 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 =3D (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 =3D 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 =3D compare_sine_samples(state.captured_samples, + captured_frames, 0, 100); + total_samples =3D captured_frames * CHANNELS; + match_ratio =3D (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, >=3D, 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), =3D=3D, 0); + + /* AUD_write/read(NULL, ...) should return size (no-op) */ + g_assert_cmpuint(AUD_write(NULL, buffer, sizeof(buffer)), =3D=3D, + sizeof(buffer)); + g_assert_cmpuint(AUD_read(NULL, buffer, sizeof(buffer)), =3D=3D, + 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 =3D get_test_audio_backend(); + out1 =3D open_test_voice_out(be, "test-multi-out1", NULL, dummy_audio_= callback); + out2 =3D open_test_voice_out(be, "test-multi-out2", NULL, dummy_audio_= callback); + in1 =3D open_test_voice_in(be, "test-multi-in1", NULL, dummy_audio_cal= lback); + + 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)); + 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 =3D NULL; + g_autofree gchar *dir =3D NULL; + int ret; + + context =3D 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); + } + trace_init_file(); + qemu_set_log(LOG_TRACE, &error_fatal); + + dir =3D 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 =3D is_help_option(opt_audiodev) ? + opt_audiodev : g_strdup_printf("%s,id=3D%s", opt_audiodev, TES= T_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 =3D g_test_run(); + + audio_cleanup(); + + return ret; +} diff --git a/tests/audio/meson.build b/tests/audio/meson.build new file mode 100644 index 00000000000..611b5749a5b --- /dev/null +++ b/tests/audio/meson.build @@ -0,0 +1,18 @@ +modinfo_dep =3D not_found +if enable_modules + modinfo_src =3D custom_target('modinfo.c', + output: 'modinfo.c', + input: audio_modinfo_files, + command: [modinfo_generate, '--skip-missin= g-deps', '@INPUT@'], + capture: true) + + modinfo_lib =3D static_library('modinfo.c', modinfo_src) + modinfo_dep =3D declare_dependency(link_with: modinfo_lib) +endif + +# manual audio test - not part of automated test suite +# as it relies on audio system +executable('test-audio', + sources: files('test-audio.c', 'audio-stubs.c'), + dependencies: [audio, qemuutil, spice, modinfo_dep], + build_by_default: false) diff --git a/tests/meson.build b/tests/meson.build index cbe79162411..cb766e49ca4 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -83,6 +83,7 @@ if 'CONFIG_TCG' in config_all_accel subdir('tcg/plugins') endif =20 +subdir('audio') subdir('unit') subdir('qapi-schema') subdir('qtest') --=20 2.52.0