get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/patches/2196800/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 2196800,
    "url": "http://patchwork.ozlabs.org/api/patches/2196800/?format=api",
    "web_url": "http://patchwork.ozlabs.org/project/qemu-devel/patch/20260216-audio-v1-18-e676662e4514@redhat.com/",
    "project": {
        "id": 14,
        "url": "http://patchwork.ozlabs.org/api/projects/14/?format=api",
        "name": "QEMU Development",
        "link_name": "qemu-devel",
        "list_id": "qemu-devel.nongnu.org",
        "list_email": "qemu-devel@nongnu.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": "",
        "list_archive_url": "",
        "list_archive_url_format": "",
        "commit_url_format": ""
    },
    "msgid": "<20260216-audio-v1-18-e676662e4514@redhat.com>",
    "list_archive_url": null,
    "date": "2026-02-16T11:15:07",
    "name": "[18/85] tests: start manual audio backend test",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "062b690dfe8c2ad8ca2c38595e04a1e91a9627f8",
    "submitter": {
        "id": 66774,
        "url": "http://patchwork.ozlabs.org/api/people/66774/?format=api",
        "name": "Marc-André Lureau",
        "email": "marcandre.lureau@redhat.com"
    },
    "delegate": null,
    "mbox": "http://patchwork.ozlabs.org/project/qemu-devel/patch/20260216-audio-v1-18-e676662e4514@redhat.com/mbox/",
    "series": [
        {
            "id": 492294,
            "url": "http://patchwork.ozlabs.org/api/series/492294/?format=api",
            "web_url": "http://patchwork.ozlabs.org/project/qemu-devel/list/?series=492294",
            "date": "2026-02-16T11:14:52",
            "name": "audio: cleanups & add a manual test",
            "version": 1,
            "mbox": "http://patchwork.ozlabs.org/series/492294/mbox/"
        }
    ],
    "comments": "http://patchwork.ozlabs.org/api/patches/2196800/comments/",
    "check": "pending",
    "checks": "http://patchwork.ozlabs.org/api/patches/2196800/checks/",
    "tags": {},
    "related": [],
    "headers": {
        "Return-Path": "<qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org>",
        "X-Original-To": "incoming@patchwork.ozlabs.org",
        "Delivered-To": "patchwork-incoming@legolas.ozlabs.org",
        "Authentication-Results": [
            "legolas.ozlabs.org;\n\tdkim=pass (1024-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=d3p8cYqS;\n\tdkim-atps=neutral",
            "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=nongnu.org\n (client-ip=209.51.188.17; helo=lists.gnu.org;\n envelope-from=qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org;\n receiver=patchwork.ozlabs.org)"
        ],
        "Received": [
            "from lists.gnu.org (lists.gnu.org [209.51.188.17])\n\t(using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits))\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4fF0fR3fByz1xtN\n\tfor <incoming@patchwork.ozlabs.org>; Mon, 16 Feb 2026 22:19:27 +1100 (AEDT)",
            "from localhost ([::1] helo=lists1p.gnu.org)\n\tby lists.gnu.org with esmtp (Exim 4.90_1)\n\t(envelope-from <qemu-devel-bounces@nongnu.org>)\n\tid 1vrwdN-0007vy-1P; Mon, 16 Feb 2026 06:19:22 -0500",
            "from eggs.gnu.org ([2001:470:142:3::10])\n by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <marcandre.lureau@redhat.com>)\n id 1vrwar-0005JS-PG\n for qemu-devel@nongnu.org; Mon, 16 Feb 2026 06:17:04 -0500",
            "from us-smtp-delivery-124.mimecast.com ([170.10.133.124])\n by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <marcandre.lureau@redhat.com>)\n id 1vrwaj-0007SO-Qo\n for qemu-devel@nongnu.org; Mon, 16 Feb 2026 06:16:41 -0500",
            "from mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com\n (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by\n relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3,\n cipher=TLS_AES_256_GCM_SHA384) id us-mta-198-MpXdyA5NMDqOWILfKIiQ1g-1; Mon,\n 16 Feb 2026 06:16:30 -0500",
            "from mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com\n (mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.4])\n (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest\n SHA256)\n (No client certificate requested)\n by mx-prod-mc-03.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS\n id 736FD19560B2; Mon, 16 Feb 2026 11:16:28 +0000 (UTC)",
            "from localhost (unknown [10.45.242.26])\n by mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP\n id BDD2430001A5; Mon, 16 Feb 2026 11:16:27 +0000 (UTC)"
        ],
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n s=mimecast20190719; t=1771240594;\n h=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n to:to:cc:cc:mime-version:mime-version:content-type:content-type:\n content-transfer-encoding:content-transfer-encoding:\n in-reply-to:in-reply-to:references:references;\n bh=U3xIBvN95qHXHsq0xoLiJzxgBouxPOGkc64oTCZAQok=;\n b=d3p8cYqSWfU9hLFluTMOmAp6Z2o0PR5FZlFnKePiBBJqmwwNclP0kJD/9TTFK5rHkzb+CM\n daeR2ljOn4tcnhpKBlfiE/uu65HRM7IyXAru64iG+bxGMYwEZvZlAhcB/AdI78SOD1rntK\n q25hSob6fc2KgGK/gPKw3nDvWejefEY=",
        "X-MC-Unique": "MpXdyA5NMDqOWILfKIiQ1g-1",
        "X-Mimecast-MFC-AGG-ID": "MpXdyA5NMDqOWILfKIiQ1g_1771240588",
        "From": "=?utf-8?q?Marc-Andr=C3=A9_Lureau?= <marcandre.lureau@redhat.com>",
        "Date": "Mon, 16 Feb 2026 12:15:07 +0100",
        "Subject": "[PATCH 18/85] tests: start manual audio backend test",
        "MIME-Version": "1.0",
        "Content-Type": "text/plain; charset=\"utf-8\"",
        "Content-Transfer-Encoding": "8bit",
        "Message-Id": "<20260216-audio-v1-18-e676662e4514@redhat.com>",
        "References": "<20260216-audio-v1-0-e676662e4514@redhat.com>",
        "In-Reply-To": "<20260216-audio-v1-0-e676662e4514@redhat.com>",
        "To": "qemu-devel@nongnu.org",
        "Cc": "Gerd Hoffmann <kraxel@redhat.com>,  Eduardo Habkost <eduardo@habkost.net>,\n Paolo Bonzini <pbonzini@redhat.com>,\n =?utf-8?q?Daniel_P=2E_Berrang=C3=A9?= <berrange@redhat.com>, =?utf-8?q?Phil?=\n\t=?utf-8?q?ippe_Mathieu-Daud=C3=A9?= <philmd@linaro.org>,\n  John Snow <jsnow@redhat.com>, Cleber Rosa <crosa@redhat.com>,\n  Christian Schoenebeck <qemu_oss@crudebyte.com>,\n  Akihiko Odaki <odaki@rsg.ci.i.u-tokyo.ac.jp>,\n  Thomas Huth <huth@tuxfamily.org>, Alexandre Ratchov <alex@caoua.org>,\n\t=?utf-8?q?Alex_Benn=C3=A9e?= <alex.bennee@linaro.org>,\n  Laurent Vivier <laurent@vivier.eu>, \"Michael S. Tsirkin\" <mst@redhat.com>,\n  Manos Pitsidianakis <manos.pitsidianakis@linaro.org>,\n  Alistair Francis <alistair@alistair23.me>,\n  \"Edgar E. Iglesias\" <edgar.iglesias@gmail.com>,\n  Peter Maydell <peter.maydell@linaro.org>, qemu-arm@nongnu.org, =?utf-8?q?M?=\n\t=?utf-8?q?arc-Andr=C3=A9_Lureau?= <marcandre.lureau@redhat.com>",
        "X-Developer-Signature": "v=1; a=openpgp-sha256; l=25709;\n i=marcandre.lureau@redhat.com; h=from:subject:message-id;\n bh=LFmgbskSCK71ElN/GubZyRqCsfdOjnPkUhXawuYD0Ak=;\n b=owEBbQKS/ZANAwAKAdro4Ql1lpzlAcsmYgBpkvxO/b5zE2ibxPlbbkleqhPOT8gBlYZreZYp8\n 4eJzVn2ClWJAjMEAAEKAB0WIQSHqb2TP4fGBtJ29i3a6OEJdZac5QUCaZL8TgAKCRDa6OEJdZac\n 5WIOEAC6eB/U/i/AGMthWjhAy9jMHDTjTqYTAF6VdcvvLoJmVi83iQokrRifJUdZvq8ptj3B756\n oJy3HUi4I7oWegEzXzoJojxX7U7/YMTa94O0o6TMKoOdCSWNpuhUUPZBbZk7I0/AtV/MkMraadf\n 9EtOo1idJ8JaJ0W0z6kLHTQ+Mep4GzsjniY5gOoO9d0+CWR4BJkKapzuZdnf7lob6thbVfMqCaP\n cUYFyQDgu0bvtxG+WddR0psAQDWHEmkYNQ8q7WjtOmXZv5CbTpR3ls5v5dXCXsTvX7dNuE91URv\n FGWrWsnMSYlVDSAtgr8hluFOWunwVU15nQXXmnvvRuQCgn3ZNviZ7bEvX7yR/ULinx/oQf1NN4b\n SvPHvx0ni6XldCjirm7wOPZyKU6gHSN6jltTBORtUOqOZEm/NRQzUozcy3iKm7Jy2OVMnlTkaW2\n vLQ9YaGBuhDkc/32aIADjykfCz2/r1hSLleG9/qGyV7JFeT7Vofk2IHr/Qk8pxmx46ghXR+0OBQ\n 2aFKEaTeJ8JgdvPejt4j7ci//IkiHOQnwp0o5+LmmvMN2cxjuVc89BaPf3lsCFVvMQNVTCHXpEc\n HZwLaKrkp43/pMDMF8QbZQ3u64yKsI+r2Hy5OWcJg1AWTkG15XlUA5kHZL6q0L95BAY+aJZKUjb\n 7GxFaTCySz3l9gQ==",
        "X-Developer-Key": "i=marcandre.lureau@redhat.com; a=openpgp;\n fpr=87A9BD933F87C606D276F62DDAE8E10975969CE5",
        "X-Scanned-By": "MIMEDefang 3.4.1 on 10.30.177.4",
        "Received-SPF": "pass client-ip=170.10.133.124;\n envelope-from=marcandre.lureau@redhat.com;\n helo=us-smtp-delivery-124.mimecast.com",
        "X-Spam_score_int": "-20",
        "X-Spam_score": "-2.1",
        "X-Spam_bar": "--",
        "X-Spam_report": "(-2.1 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.001,\n DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1,\n RCVD_IN_DNSWL_NONE=-0.0001, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001,\n RCVD_IN_VALIDITY_SAFE_BLOCKED=0.001, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001,\n 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 <qemu-devel.nongnu.org>",
        "List-Unsubscribe": "<https://lists.nongnu.org/mailman/options/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.nongnu.org/archive/html/qemu-devel>",
        "List-Post": "<mailto:qemu-devel@nongnu.org>",
        "List-Help": "<mailto:qemu-devel-request@nongnu.org?subject=help>",
        "List-Subscribe": "<https://lists.nongnu.org/mailman/listinfo/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=subscribe>",
        "Errors-To": "qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org",
        "Sender": "qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org"
    },
    "content": "Start a simple test program that will exercise the QEMU audio APIs.\n\nIt is meant to run manually for now, as it accesses the sound system and\nproduces sound by default, and also runs for a few seconds. We may want\nto make it silent or use the \"none\" (noaudio) backend by default though,\nso it can run as part of the automated test suite.\n\nSigned-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>\n---\n MAINTAINERS               |   1 +\n meson.build               |  17 +-\n tests/audio/audio-stubs.c |  62 +++++\n tests/audio/test-audio.c  | 599 ++++++++++++++++++++++++++++++++++++++++++++++\n audio/meson.build         |   2 +-\n tests/audio/meson.build   |  23 ++\n tests/meson.build         |   1 +\n ui/meson.build            |   3 +\n 8 files changed, 702 insertions(+), 6 deletions(-)",
    "diff": "diff --git a/MAINTAINERS b/MAINTAINERS\nindex d3aa6d6732d..f52ead996b4 100644\n--- a/MAINTAINERS\n+++ b/MAINTAINERS\n@@ -3034,6 +3034,7 @@ X: audio/sndioaudio.c\n X: audio/spiceaudio.c\n F: include/qemu/audio*.h\n F: qapi/audio.json\n+F: tests/audio/\n \n ALSA Audio backend\n M: Gerd Hoffmann <kraxel@redhat.com>\ndiff --git a/meson.build b/meson.build\nindex 904605b844f..659f4739b10 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -3879,6 +3879,7 @@ target_modules += { 'accel' : { 'qtest': qtest_module_ss }}\n modinfo_collect = find_program('scripts/modinfo-collect.py')\n modinfo_generate = find_program('scripts/modinfo-generate.py')\n modinfo_files = []\n+audio_modinfo_files = []\n \n block_mods = []\n system_mods = []\n@@ -3906,15 +3907,21 @@ foreach d, list : modules\n                     install: true,\n                     install_dir: qemu_moddir)\n       if module_ss.sources() != []\n-        modinfo_files += custom_target(d + '-' + m + '.modinfo',\n-                                       output: d + '-' + m + '.modinfo',\n-                                       input: sl.extract_all_objects(recursive: true),\n-                                       capture: true,\n-                                       command: [modinfo_collect, '@INPUT@'])\n+        modinfo = custom_target(d + '-' + m + '.modinfo',\n+                                output: d + '-' + m + '.modinfo',\n+                                input: sl.extract_all_objects(recursive: true),\n+                                capture: true,\n+                                command: [modinfo_collect, '@INPUT@'])\n+        modinfo_files += modinfo\n+        if d == 'audio'\n+          audio_modinfo_files += modinfo\n+        endif\n       endif\n     else\n       if d == 'block'\n         block_ss.add_all(module_ss)\n+      elif d == 'audio'\n+        audio_ss.add_all(module_ss)\n       else\n         system_ss.add_all(module_ss)\n       endif\ndiff --git a/tests/audio/audio-stubs.c b/tests/audio/audio-stubs.c\nnew file mode 100644\nindex 00000000000..dd7f635d460\n--- /dev/null\n+++ b/tests/audio/audio-stubs.c\n@@ -0,0 +1,62 @@\n+/* SPDX-License-Identifier: GPL-2.0-or-later */\n+\n+/*\n+ * Stubs for audio test - provides missing functions for standalone audio test\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/dbus.h\"\n+#include \"ui/qemu-spice-module.h\"\n+#include \"ui/dbus-module.h\"\n+#include \"system/replay.h\"\n+#include \"system/runstate.h\"\n+\n+int using_spice;\n+int using_dbus_display;\n+\n+struct QemuSpiceOps qemu_spice;\n+\n+GQuark dbus_display_error_quark(void)\n+{\n+    return g_quark_from_static_string(\"dbus-display-error-quark\");\n+}\n+\n+#ifdef WIN32\n+/* from ui/dbus.h */\n+bool\n+dbus_win32_import_socket(GDBusMethodInvocation *invocation,\n+                         GVariant *arg_listener, int *socket);\n+\n+bool\n+dbus_win32_import_socket(GDBusMethodInvocation *invocation,\n+                         GVariant *arg_listener, int *socket)\n+{\n+    return true;\n+}\n+#endif\n+\n+void replay_audio_in(size_t *recorded, st_sample *samples,\n+                     size_t *wpos, size_t size)\n+{\n+}\n+\n+void replay_audio_out(size_t *played)\n+{\n+}\n+\n+static int dummy_vmse;\n+\n+VMChangeStateEntry *qemu_add_vm_change_state_handler(VMChangeStateHandler *cb,\n+                                                     void *opaque)\n+{\n+    return (VMChangeStateEntry *)&dummy_vmse;\n+}\n+\n+void qemu_del_vm_change_state_handler(VMChangeStateEntry *e)\n+{\n+}\n+\n+bool runstate_is_running(void)\n+{\n+    return true;\n+}\ndiff --git a/tests/audio/test-audio.c b/tests/audio/test-audio.c\nnew file mode 100644\nindex 00000000000..edb4971972c\n--- /dev/null\n+++ b/tests/audio/test-audio.c\n@@ -0,0 +1,599 @@\n+/* SPDX-License-Identifier: GPL-2.0-or-later */\n+\n+#include \"qemu/osdep.h\"\n+#include \"qemu/config-file.h\"\n+#include \"qemu/cutils.h\"\n+#include \"qemu/help_option.h\"\n+#include \"qemu/module.h\"\n+#include \"qemu/main-loop.h\"\n+#include \"qemu/audio.h\"\n+#include \"qemu/log.h\"\n+#include \"qapi/error.h\"\n+#include \"trace/control.h\"\n+#include \"glib.h\"\n+\n+#include \"audio/audio_int.h\"\n+\n+#include <math.h>\n+#ifdef CONFIG_SDL\n+/*\n+ * SDL insists on wrapping the main() function with its own implementation on\n+ * some platforms; it does so via a macro that renames our main function, so\n+ * <SDL.h> must be #included here even with no SDL code called from this file.\n+ */\n+#include <SDL.h>\n+#endif\n+\n+#define SAMPLE_RATE     44100\n+#define CHANNELS        2\n+#define DURATION_SECS   2\n+#define FREQUENCY       440.0\n+#define BUFFER_FRAMES   1024\n+#define TIMEOUT_SECS    (DURATION_SECS + 1)\n+\n+/* Command-line options */\n+static gchar *opt_audiodev;\n+static gchar *opt_trace;\n+\n+static GOptionEntry test_options[] = {\n+    { \"audiodev\", 'a', 0, G_OPTION_ARG_STRING, &opt_audiodev,\n+      \"Audio device spec (e.g., none or pa,out.buffer-length=50000)\", \"DEV\" },\n+    { \"trace\", 'T', 0, G_OPTION_ARG_STRING, &opt_trace,\n+      \"Trace options (e.g., 'pw_*')\", \"TRACE\" },\n+    { NULL }\n+};\n+\n+#define TEST_AUDIODEV_ID \"test\"\n+\n+typedef struct TestSineState {\n+    AudioBackend *be;\n+    SWVoiceOut *voice;\n+    int64_t total_frames;\n+    int64_t frames_written;\n+} TestSineState;\n+\n+/* Default audio settings for tests */\n+static const struct audsettings default_test_settings = {\n+    .freq = SAMPLE_RATE,\n+    .nchannels = CHANNELS,\n+    .fmt = AUDIO_FORMAT_S16,\n+    .endianness = 0,\n+};\n+\n+static void dummy_audio_callback(void *opaque, int avail)\n+{\n+}\n+\n+static AudioBackend *get_test_audio_backend(void)\n+{\n+    AudioBackend *be;\n+    Error *err = NULL;\n+\n+    if (opt_audiodev) {\n+        be = audio_be_by_name(TEST_AUDIODEV_ID, &err);\n+    } else {\n+        be = audio_get_default_audio_be(&err);\n+    }\n+\n+    if (err) {\n+        g_error(\"%s\", error_get_pretty(err));\n+        error_free(err);\n+        exit(1);\n+    }\n+    g_assert_nonnull(be);\n+    return be;\n+}\n+\n+/*\n+ * Helper functions for opening test voices with default settings.\n+ * These reduce boilerplate in test functions.\n+ */\n+static SWVoiceOut *open_test_voice_out(AudioBackend *be, const char *name,\n+                                       void *opaque, audio_callback_fn cb)\n+{\n+    struct audsettings as = default_test_settings;\n+    SWVoiceOut *voice;\n+\n+    voice = AUD_open_out(be, NULL, name, opaque, cb, &as);\n+    g_assert_nonnull(voice);\n+    return voice;\n+}\n+\n+static SWVoiceIn *open_test_voice_in(AudioBackend *be, const char *name,\n+                                     void *opaque, audio_callback_fn cb)\n+{\n+    struct audsettings as = default_test_settings;\n+\n+    return AUD_open_in(be, NULL, name, opaque, cb, &as);\n+}\n+\n+/*\n+ * Generate 440Hz sine wave samples into buffer.\n+ */\n+static void generate_sine_samples(int16_t *buffer, int frames,\n+                                  int64_t start_frame)\n+{\n+    for (int i = 0; i < frames; i++) {\n+        double t = (double)(start_frame + i) / SAMPLE_RATE;\n+        double sample = sin(2.0 * M_PI * FREQUENCY * t);\n+        int16_t s = (int16_t)(sample * 32767.0);\n+\n+        buffer[i * 2] = s;       /* left channel */\n+        buffer[i * 2 + 1] = s;   /* right channel */\n+    }\n+}\n+\n+static void test_sine_callback(void *opaque, int avail)\n+{\n+    TestSineState *s = opaque;\n+    int16_t buffer[BUFFER_FRAMES * CHANNELS];\n+    int frames_remaining;\n+    int frames_to_write;\n+    size_t bytes_written;\n+\n+    frames_remaining = s->total_frames - s->frames_written;\n+    if (frames_remaining <= 0) {\n+        return;\n+    }\n+\n+    frames_to_write = avail / (sizeof(int16_t) * CHANNELS);\n+    frames_to_write = MIN(frames_to_write, BUFFER_FRAMES);\n+    frames_to_write = MIN(frames_to_write, frames_remaining);\n+\n+    generate_sine_samples(buffer, frames_to_write, s->frames_written);\n+\n+    bytes_written = AUD_write(s->voice, buffer,\n+                              frames_to_write * sizeof(int16_t) * CHANNELS);\n+    s->frames_written += bytes_written / (sizeof(int16_t) * CHANNELS);\n+}\n+\n+\n+static void test_audio_out_sine_wave(void)\n+{\n+    TestSineState state = {0};\n+    int64_t start_time;\n+    int64_t elapsed_ms;\n+\n+    state.be = get_test_audio_backend();\n+    state.total_frames = SAMPLE_RATE * DURATION_SECS;\n+    state.frames_written = 0;\n+\n+    g_test_message(\"Opening audio output...\");\n+    state.voice = open_test_voice_out(state.be, \"test-sine\",\n+                                      &state, test_sine_callback);\n+\n+    g_test_message(\"Playing 440Hz sine wave for %d seconds...\", DURATION_SECS);\n+    AUD_set_active_out(state.voice, true);\n+\n+    /*\n+     * Run the audio subsystem until all frames are written or timeout.\n+     */\n+    start_time = g_get_monotonic_time();\n+    while (state.frames_written < state.total_frames) {\n+        audio_run(state.be, \"test\");\n+        main_loop_wait(true);\n+\n+        elapsed_ms = (g_get_monotonic_time() - start_time) / 1000;\n+        if (elapsed_ms > TIMEOUT_SECS * 1000) {\n+            g_test_message(\"Timeout waiting for audio to complete\");\n+            break;\n+        }\n+\n+        g_usleep(G_USEC_PER_SEC / 100);  /* 10ms */\n+    }\n+\n+    g_test_message(\"Wrote %\" PRId64 \" frames (%.2f seconds)\",\n+                   state.frames_written,\n+                   (double)state.frames_written / SAMPLE_RATE);\n+\n+    g_assert_cmpint(state.frames_written, ==, state.total_frames);\n+\n+    AUD_set_active_out(state.voice, false);\n+    AUD_close_out(state.be, state.voice);\n+}\n+\n+static void test_audio_prio_list(void)\n+{\n+    g_autofree gchar *backends = NULL;\n+    GString *str = g_string_new(NULL);\n+    bool has_none = false;\n+\n+    for (int i = 0; audio_prio_list[i]; i++) {\n+        if (i > 0) {\n+            g_string_append_c(str, ' ');\n+        }\n+        g_string_append(str, audio_prio_list[i]);\n+\n+        if (g_strcmp0(audio_prio_list[i], \"none\") == 0) {\n+            has_none = true;\n+        }\n+    }\n+\n+    backends = g_string_free(str, FALSE);\n+    g_test_message(\"Available backends: %s\", backends);\n+\n+    /* The 'none' backend should always be available */\n+    g_assert_true(has_none);\n+}\n+\n+static void test_audio_out_active_state(void)\n+{\n+    AudioBackend *be;\n+    SWVoiceOut *voice;\n+\n+    be = get_test_audio_backend();\n+    voice = open_test_voice_out(be, \"test-active\", NULL, dummy_audio_callback);\n+\n+    g_assert_false(AUD_is_active_out(voice));\n+\n+    AUD_set_active_out(voice, true);\n+    g_assert_true(AUD_is_active_out(voice));\n+\n+    AUD_set_active_out(voice, false);\n+    g_assert_false(AUD_is_active_out(voice));\n+\n+    AUD_close_out(be, voice);\n+}\n+\n+static void test_audio_out_buffer_size(void)\n+{\n+    AudioBackend *be;\n+    SWVoiceOut *voice;\n+    int buffer_size;\n+\n+    be = get_test_audio_backend();\n+    voice = open_test_voice_out(be, \"test-buffer\", NULL, dummy_audio_callback);\n+\n+    buffer_size = AUD_get_buffer_size_out(voice);\n+    g_test_message(\"Buffer size: %d bytes\", buffer_size);\n+    g_assert_cmpint(buffer_size, >, 0);\n+\n+    AUD_close_out(be, voice);\n+\n+    g_assert_cmpint(AUD_get_buffer_size_out(NULL), ==, 0);\n+}\n+\n+static void test_audio_out_volume(void)\n+{\n+    AudioBackend *be;\n+    SWVoiceOut *voice;\n+    Volume vol;\n+\n+    be = get_test_audio_backend();\n+    voice = open_test_voice_out(be, \"test-volume\", NULL, dummy_audio_callback);\n+\n+    vol = (Volume){ .mute = false, .channels = 2, .vol = {255, 255} };\n+    AUD_set_volume_out(voice, &vol);\n+\n+    vol = (Volume){ .mute = true, .channels = 2, .vol = {255, 255} };\n+    AUD_set_volume_out(voice, &vol);\n+\n+    vol = (Volume){ .mute = false, .channels = 2, .vol = {128, 128} };\n+    AUD_set_volume_out(voice, &vol);\n+\n+    AUD_close_out(be, voice);\n+}\n+\n+static void test_audio_in_active_state(void)\n+{\n+    AudioBackend *be;\n+    SWVoiceIn *voice;\n+\n+    be = get_test_audio_backend();\n+    voice = open_test_voice_in(be, \"test-in-active\", NULL, dummy_audio_callback);\n+    if (!voice) {\n+        g_test_skip(\"The backend may not support input\");\n+        return;\n+    }\n+\n+    g_assert_false(AUD_is_active_in(voice));\n+\n+    AUD_set_active_in(voice, true);\n+    g_assert_true(AUD_is_active_in(voice));\n+\n+    AUD_set_active_in(voice, false);\n+    g_assert_false(AUD_is_active_in(voice));\n+\n+    AUD_close_in(be, voice);\n+}\n+\n+static void test_audio_in_volume(void)\n+{\n+    AudioBackend *be;\n+    SWVoiceIn *voice;\n+    Volume vol;\n+\n+    be = get_test_audio_backend();\n+    voice = open_test_voice_in(be, \"test-in-volume\", NULL, dummy_audio_callback);\n+    if (!voice) {\n+        g_test_skip(\"The backend may not support input\");\n+        return;\n+    }\n+\n+    vol = (Volume){ .mute = false, .channels = 2, .vol = {255, 255} };\n+    AUD_set_volume_in(voice, &vol);\n+\n+    vol = (Volume){ .mute = true, .channels = 2, .vol = {255, 255} };\n+    AUD_set_volume_in(voice, &vol);\n+\n+    AUD_close_in(be, voice);\n+}\n+\n+\n+/* Capture test state */\n+#define CAPTURE_BUFFER_FRAMES (SAMPLE_RATE / 10)  /* 100ms of audio */\n+#define CAPTURE_BUFFER_SIZE   (CAPTURE_BUFFER_FRAMES * CHANNELS * sizeof(int16_t))\n+\n+typedef struct TestCaptureState {\n+    bool notify_called;\n+    bool capture_called;\n+    bool destroy_called;\n+    audcnotification_e last_notify;\n+    int16_t *captured_samples;\n+    size_t captured_bytes;\n+    size_t capture_buffer_size;\n+} TestCaptureState;\n+\n+static void test_capture_notify(void *opaque, audcnotification_e cmd)\n+{\n+    TestCaptureState *s = opaque;\n+    s->notify_called = true;\n+    s->last_notify = cmd;\n+}\n+\n+static void test_capture_capture(void *opaque, const void *buf, int size)\n+{\n+    TestCaptureState *s = opaque;\n+    size_t bytes_to_copy;\n+\n+    s->capture_called = true;\n+\n+    if (!s->captured_samples || s->captured_bytes >= s->capture_buffer_size) {\n+        return;\n+    }\n+\n+    bytes_to_copy = MIN(size, s->capture_buffer_size - s->captured_bytes);\n+    memcpy((uint8_t *)s->captured_samples + s->captured_bytes, buf, bytes_to_copy);\n+    s->captured_bytes += bytes_to_copy;\n+}\n+\n+static void test_capture_destroy(void *opaque)\n+{\n+    TestCaptureState *s = opaque;\n+    s->destroy_called = true;\n+}\n+\n+/*\n+ * Compare captured audio with expected sine wave.\n+ * Returns the number of matching samples (within tolerance).\n+ */\n+static int compare_sine_samples(const int16_t *captured, int frames,\n+                                int64_t start_frame, int tolerance)\n+{\n+    int matching = 0;\n+\n+    for (int i = 0; i < frames; i++) {\n+        double t = (double)(start_frame + i) / SAMPLE_RATE;\n+        double sample = sin(2.0 * M_PI * FREQUENCY * t);\n+        int16_t expected = (int16_t)(sample * 32767.0);\n+\n+        /* Check left channel */\n+        if (abs(captured[i * 2] - expected) <= tolerance) {\n+            matching++;\n+        }\n+        /* Check right channel */\n+        if (abs(captured[i * 2 + 1] - expected) <= tolerance) {\n+            matching++;\n+        }\n+    }\n+\n+    return matching;\n+}\n+\n+static void test_audio_capture(void)\n+{\n+    AudioBackend *be;\n+    CaptureVoiceOut *cap;\n+    SWVoiceOut *voice;\n+    TestCaptureState state = {0};\n+    TestSineState sine_state = {0};\n+    struct audsettings as = default_test_settings;\n+    struct audio_capture_ops ops = {\n+        .notify = test_capture_notify,\n+        .capture = test_capture_capture,\n+        .destroy = test_capture_destroy,\n+    };\n+    int64_t start_time;\n+    int64_t elapsed_ms;\n+    int captured_frames;\n+    int matching_samples;\n+    int total_samples;\n+    double match_ratio;\n+\n+    be = get_test_audio_backend();\n+\n+    state.captured_samples = g_malloc0(CAPTURE_BUFFER_SIZE);\n+    state.captured_bytes = 0;\n+    state.capture_buffer_size = CAPTURE_BUFFER_SIZE;\n+\n+    cap = AUD_add_capture(be, &as, &ops, &state);\n+    g_assert_nonnull(cap);\n+\n+    sine_state.be = be;\n+    sine_state.total_frames = CAPTURE_BUFFER_FRAMES;\n+    sine_state.frames_written = 0;\n+\n+    voice = open_test_voice_out(be, \"test-capture-sine\",\n+                                &sine_state, test_sine_callback);\n+    sine_state.voice = voice;\n+\n+    AUD_set_active_out(voice, true);\n+\n+    start_time = g_get_monotonic_time();\n+    while (sine_state.frames_written < sine_state.total_frames ||\n+           state.captured_bytes < CAPTURE_BUFFER_SIZE) {\n+        audio_run(be, \"test-capture\");\n+        main_loop_wait(true);\n+\n+        elapsed_ms = (g_get_monotonic_time() - start_time) / 1000;\n+        if (elapsed_ms > 1000) {  /* 1 second timeout */\n+            break;\n+        }\n+\n+        g_usleep(G_USEC_PER_SEC / 1000);  /* 1ms */\n+    }\n+\n+    g_test_message(\"Wrote %\" PRId64 \" frames, captured %zu bytes\",\n+                   sine_state.frames_written, state.captured_bytes);\n+\n+    g_assert_true(state.capture_called);\n+    g_assert_cmpuint(state.captured_bytes, >, 0);\n+\n+    /* Compare captured data with expected sine wave */\n+    captured_frames = state.captured_bytes / (CHANNELS * sizeof(int16_t));\n+    if (captured_frames > 0) {\n+        /*\n+         * Allow some tolerance due to mixing/conversion.\n+         * The tolerance accounts for potential rounding differences.\n+         */\n+        matching_samples = compare_sine_samples(state.captured_samples,\n+                                                captured_frames, 0, 100);\n+        total_samples = captured_frames * CHANNELS;\n+        match_ratio = (double)matching_samples / total_samples;\n+\n+        g_test_message(\"Captured %d frames, %d/%d samples match (%.1f%%)\",\n+                       captured_frames, matching_samples, total_samples,\n+                       match_ratio * 100.0);\n+\n+        /*\n+         * Expect at least 90% of samples to match within tolerance.\n+         * Some variation is expected due to mixing engine processing.\n+         */\n+        g_assert_cmpfloat(match_ratio, >=, 0.9);\n+    }\n+\n+    AUD_set_active_out(voice, false);\n+    AUD_close_out(be, voice);\n+\n+    AUD_del_capture(cap, &state);\n+    g_assert_true(state.destroy_called);\n+\n+    g_free(state.captured_samples);\n+}\n+\n+static void test_audio_null_handling(void)\n+{\n+    uint8_t buffer[64];\n+\n+    /* AUD_is_active_out/in(NULL) should return false */\n+    g_assert_false(AUD_is_active_out(NULL));\n+    g_assert_false(AUD_is_active_in(NULL));\n+\n+    /* AUD_get_buffer_size_out(NULL) should return 0 */\n+    g_assert_cmpint(AUD_get_buffer_size_out(NULL), ==, 0);\n+\n+    /* AUD_write/read(NULL, ...) should return size (no-op) */\n+    g_assert_cmpuint(AUD_write(NULL, buffer, sizeof(buffer)), ==,\n+                     sizeof(buffer));\n+    g_assert_cmpuint(AUD_read(NULL, buffer, sizeof(buffer)), ==,\n+                     sizeof(buffer));\n+\n+    /* These should not crash */\n+    AUD_set_active_out(NULL, true);\n+    AUD_set_active_out(NULL, false);\n+    AUD_set_active_in(NULL, true);\n+    AUD_set_active_in(NULL, false);\n+}\n+\n+static void test_audio_multiple_voices(void)\n+{\n+    AudioBackend *be;\n+    SWVoiceOut *out1, *out2;\n+    SWVoiceIn *in1;\n+\n+    be = get_test_audio_backend();\n+    out1 = open_test_voice_out(be, \"test-multi-out1\", NULL, dummy_audio_callback);\n+    out2 = open_test_voice_out(be, \"test-multi-out2\", NULL, dummy_audio_callback);\n+    in1 = open_test_voice_in(be, \"test-multi-in1\", NULL, dummy_audio_callback);\n+\n+    AUD_set_active_out(out1, true);\n+    AUD_set_active_out(out2, true);\n+    AUD_set_active_in(in1, true);\n+\n+    g_assert_true(AUD_is_active_out(out1));\n+    g_assert_true(AUD_is_active_out(out2));\n+    if (in1) {\n+        g_assert_true(AUD_is_active_in(in1));\n+    }\n+\n+    AUD_set_active_out(out1, false);\n+    AUD_set_active_out(out2, false);\n+    AUD_set_active_in(in1, false);\n+\n+    AUD_close_in(be, in1);\n+    AUD_close_out(be, out2);\n+    AUD_close_out(be, out1);\n+}\n+\n+int main(int argc, char **argv)\n+{\n+    GOptionContext *context;\n+    g_autoptr(GError) error = NULL;\n+    g_autofree gchar *dir = NULL;\n+    int ret;\n+\n+    context = g_option_context_new(\"- QEMU audio test\");\n+    g_option_context_add_main_entries(context, test_options, NULL);\n+\n+    if (!g_option_context_parse(context, &argc, &argv, &error)) {\n+        g_printerr(\"Option parsing failed: %s\\n\", error->message);\n+        return 1;\n+    }\n+    g_option_context_free(context);\n+\n+    g_test_init(&argc, &argv, NULL);\n+\n+    module_call_init(MODULE_INIT_TRACE);\n+    qemu_add_opts(&qemu_trace_opts);\n+    if (opt_trace) {\n+        trace_opt_parse(opt_trace);\n+        qemu_set_log(LOG_TRACE, &error_fatal);\n+    }\n+    trace_init_backends();\n+    trace_init_file();\n+\n+    dir = g_test_build_filename(G_TEST_BUILT, \"..\", \"..\", NULL);\n+    g_setenv(\"QEMU_MODULE_DIR\", dir, true);\n+    qemu_init_exec_dir(argv[0]);\n+    module_call_init(MODULE_INIT_QOM);\n+    module_init_info(qemu_modinfo);\n+\n+    qemu_init_main_loop(&error_abort);\n+    if (opt_audiodev) {\n+        g_autofree gchar *spec = is_help_option(opt_audiodev) ?\n+            opt_audiodev : g_strdup_printf(\"%s,id=%s\", opt_audiodev, TEST_AUDIODEV_ID);\n+        audio_parse_option(spec);\n+    }\n+    audio_create_default_audiodevs();\n+    audio_init_audiodevs();\n+\n+    g_test_add_func(\"/audio/prio-list\", test_audio_prio_list);\n+\n+    g_test_add_func(\"/audio/out/active-state\", test_audio_out_active_state);\n+    g_test_add_func(\"/audio/out/sine-wave\", test_audio_out_sine_wave);\n+    g_test_add_func(\"/audio/out/buffer-size\", test_audio_out_buffer_size);\n+    g_test_add_func(\"/audio/out/volume\", test_audio_out_volume);\n+    g_test_add_func(\"/audio/out/capture\", test_audio_capture);\n+\n+    g_test_add_func(\"/audio/in/active-state\", test_audio_in_active_state);\n+    g_test_add_func(\"/audio/in/volume\", test_audio_in_volume);\n+\n+    g_test_add_func(\"/audio/null-handling\", test_audio_null_handling);\n+    g_test_add_func(\"/audio/multiple-voices\", test_audio_multiple_voices);\n+\n+    ret = g_test_run();\n+\n+    audio_cleanup();\n+\n+    return ret;\n+}\ndiff --git a/audio/meson.build b/audio/meson.build\nindex a5fec14fb3a..55ee0bfc865 100644\n--- a/audio/meson.build\n+++ b/audio/meson.build\n@@ -32,7 +32,7 @@ endforeach\n if dbus_display\n     module_ss = ss.source_set()\n     module_ss.add(when: [gio, pixman],\n-                  if_true: [dbus_display1, files('dbusaudio.c')])\n+                  if_true: [dbus_display1_h, files('dbusaudio.c')])\n     audio_modules += {'dbus': module_ss}\n endif\n \ndiff --git a/tests/audio/meson.build b/tests/audio/meson.build\nnew file mode 100644\nindex 00000000000..84754bde221\n--- /dev/null\n+++ b/tests/audio/meson.build\n@@ -0,0 +1,23 @@\n+# SPDX-License-Identifier: GPL-2.0-or-later\n+\n+if not have_system\n+  subdir_done()\n+endif\n+\n+modinfo_dep = not_found\n+if enable_modules\n+    modinfo_src = custom_target('modinfo.c',\n+                                output: 'modinfo.c',\n+                                input: audio_modinfo_files,\n+                                command: [modinfo_generate, '--skip-missing-deps', '@INPUT@'],\n+                                capture: true)\n+\n+    modinfo_lib = static_library('modinfo.c', modinfo_src)\n+    modinfo_dep = declare_dependency(link_with: modinfo_lib)\n+endif\n+\n+# manual audio test - not part of automated test suite\n+# as it relies on audio system\n+executable('test-audio',\n+    sources: [files('test-audio.c', 'audio-stubs.c'), dbus_display1],\n+    dependencies: [audio, qemuutil, modinfo_dep, gio, spice, sdl])\ndiff --git a/tests/meson.build b/tests/meson.build\nindex cbe79162411..cb766e49ca4 100644\n--- a/tests/meson.build\n+++ b/tests/meson.build\n@@ -83,6 +83,7 @@ if 'CONFIG_TCG' in config_all_accel\n   subdir('tcg/plugins')\n endif\n \n+subdir('audio')\n subdir('unit')\n subdir('qapi-schema')\n subdir('qtest')\ndiff --git a/ui/meson.build b/ui/meson.build\nindex 6371422c460..69404bca71a 100644\n--- a/ui/meson.build\n+++ b/ui/meson.build\n@@ -70,6 +70,7 @@ if opengl.found()\n   ui_modules += {'egl-headless' : egl_headless_ss}\n endif\n \n+dbus_display1 = files()\n if dbus_display\n   dbus_ss = ss.source_set()\n   env = environment()\n@@ -88,6 +89,8 @@ if dbus_display\n                                           '--interface-prefix', 'org.qemu.',\n                                           '--c-namespace', 'QemuDBus',\n                                           '--generate-c-code', '@BASENAME@'])\n+  dbus_display1_h = declare_dependency(dependencies: [gio],\n+                                       sources: dbus_display1[0])\n   dbus_ss.add(when: gio,\n               if_true: [files(\n                 'dbus-chardev.c',\n",
    "prefixes": [
        "18/85"
    ]
}