From: Marc-André Lureau <[email protected]>

This patch introduce a rust/audio crate that replaces QEMU audio/
mixing/resampling code with GStreamer and Rust. It could potentially
remove the need for all the system-specific audio API implementation,
since GStreamer has audio elements for
ALSA/Pipewire/PulseAudio/jack/OSX/WASAPI etc (removing ~10k loc).

TODO:
- test on various system, with various configuration to see if this
  backend can replace the other QEMU audio backends
- add a spicesink/spicesrc to handle spice, or rewrite spice to use
  the capture approach used by VNC code. Or drop capture support, and
  use custom qemusrc/qemusink for both Spice and VNC, lowering the feature
  and behaviour disparity.
- build-sys: make gstreamer optional
- build-sys: loadable module support
- investigate dropping get_buffer_size_out()
- investigate improving emulated devices to not require regular
  timers (appsrc need-data is called once)
- add generic audio backend tests
- more tests for the mixing/liveadder behaviour (synchronization)
- other: replace audio/dbus with a rust implementation (not using gstreamer)

Signed-off-by: Marc-André Lureau <[email protected]>
---
 qapi/audio.json               |   29 +
 audio/audio-driver_template.h |    2 +
 rust/audio/wrapper.h          |   27 +
 audio/audio.c                 |    5 +
 Cargo.lock                    |  572 ++++++++++++++++--
 Cargo.toml                    |    6 +
 audio/trace-events            |    5 +
 rust/audio/Cargo.toml         |   29 +
 rust/audio/build.rs           |   49 ++
 rust/audio/meson.build        |   75 +++
 rust/audio/src/audio.rs       |  516 ++++++++++++++++
 rust/audio/src/bindings.rs    |   32 +
 rust/audio/src/gstreamer.rs   | 1070 +++++++++++++++++++++++++++++++++
 rust/audio/src/lib.rs         |   99 +++
 rust/meson.build              |    6 +
 15 files changed, 2467 insertions(+), 55 deletions(-)
 create mode 100644 rust/audio/wrapper.h
 create mode 100644 rust/audio/Cargo.toml
 create mode 100644 rust/audio/build.rs
 create mode 100644 rust/audio/meson.build
 create mode 100644 rust/audio/src/audio.rs
 create mode 100644 rust/audio/src/bindings.rs
 create mode 100644 rust/audio/src/gstreamer.rs
 create mode 100644 rust/audio/src/lib.rs

diff --git a/qapi/audio.json b/qapi/audio.json
index 2df87b9710..76dc7cbfa6 100644
--- a/qapi/audio.json
+++ b/qapi/audio.json
@@ -128,6 +128,33 @@
     '*out':       'AudiodevAlsaPerDirectionOptions',
     '*threshold': 'uint32' } }
 
+    ##
+    # @AudiodevGStreamerOptions:
+    #
+    # Options of the GStreamer audio backend.
+    #
+    # @in: options of the capture stream
+    #
+    # @out: options of the playback stream
+    #
+    # @sink: the name of the GStreamer sink element to use
+    #        (default 'autoaudiosink')
+    #
+    # @source: the name of the GStreamer source element to use
+    #        (default 'autoaudiosrc')
+    #
+    # Since: 11.0
+    ##
+    { 'struct': 'AudiodevGStreamerOptions',
+      'data': {
+        '*in':        'AudiodevPerDirectionOptions',
+        '*out':       'AudiodevPerDirectionOptions',
+        '*sink':      'str',
+        '*source':    'str'
+      }
+    }
+
+
 ##
 # @AudiodevSndioOptions:
 #
@@ -484,6 +511,7 @@
             { 'name': 'sdl', 'if': 'CONFIG_AUDIO_SDL' },
             { 'name': 'sndio', 'if': 'CONFIG_AUDIO_SNDIO' },
             { 'name': 'spice', 'if': 'CONFIG_SPICE' },
+            { 'name': 'gstreamer' },
             'wav' ] }
 
 ##
@@ -530,6 +558,7 @@
                    'if': 'CONFIG_AUDIO_SNDIO' },
     'spice':     { 'type': 'AudiodevGenericOptions',
                    'if': 'CONFIG_SPICE' },
+    'gstreamer': { 'type': 'AudiodevGStreamerOptions' },
     'wav':       'AudiodevWavOptions' } }
 
 ##
diff --git a/audio/audio-driver_template.h b/audio/audio-driver_template.h
index 40d1ad9dea..aa2451ac7f 100644
--- a/audio/audio-driver_template.h
+++ b/audio/audio-driver_template.h
@@ -391,6 +391,8 @@ AudiodevPerDirectionOptions *glue(audio_get_pdo_, 
TYPE)(Audiodev *dev)
     case AUDIODEV_DRIVER_SPICE:
         return dev->u.spice.TYPE;
 #endif
+    case AUDIODEV_DRIVER_GSTREAMER:
+        abort();
     case AUDIODEV_DRIVER_WAV:
         return dev->u.wav.TYPE;
 
diff --git a/rust/audio/wrapper.h b/rust/audio/wrapper.h
new file mode 100644
index 0000000000..a7960d0acc
--- /dev/null
+++ b/rust/audio/wrapper.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/*
+ * This header file is meant to be used as input to the `bindgen` application
+ * in order to generate C FFI compatible Rust bindings.
+ */
+
+#ifndef __CLANG_STDATOMIC_H
+#define __CLANG_STDATOMIC_H
+/*
+ * Fix potential missing stdatomic.h error in case bindgen does not insert the
+ * correct libclang header paths on its own. We do not use stdatomic.h symbols
+ * in QEMU code, so it's fine to declare dummy types instead.
+ */
+typedef enum memory_order {
+  memory_order_relaxed,
+  memory_order_consume,
+  memory_order_acquire,
+  memory_order_release,
+  memory_order_acq_rel,
+  memory_order_seq_cst,
+} memory_order;
+#endif /* __CLANG_STDATOMIC_H */
+
+#include "qemu/osdep.h"
+
+#include "qemu/audio.h"
diff --git a/audio/audio.c b/audio/audio.c
index ccb16ae3b2..5a11fe60db 100644
--- a/audio/audio.c
+++ b/audio/audio.c
@@ -219,6 +219,7 @@ static void audio_create_pdos(Audiodev *dev)
 #ifdef CONFIG_SPICE
         CASE(SPICE, spice, );
 #endif
+        CASE(GSTREAMER, gstreamer, );
         CASE(WAV, wav, );
 
     case AUDIODEV_DRIVER__MAX:
@@ -316,6 +317,8 @@ static AudiodevPerDirectionOptions 
*audio_get_pdo_out(Audiodev *dev)
     case AUDIODEV_DRIVER_SPICE:
         return dev->u.spice.out;
 #endif
+    case AUDIODEV_DRIVER_GSTREAMER:
+        return dev->u.gstreamer.out;
     case AUDIODEV_DRIVER_WAV:
         return dev->u.wav.out;
 
@@ -375,6 +378,8 @@ static AudiodevPerDirectionOptions 
*audio_get_pdo_in(Audiodev *dev)
     case AUDIODEV_DRIVER_SPICE:
         return dev->u.spice.in;
 #endif
+    case AUDIODEV_DRIVER_GSTREAMER:
+        return dev->u.gstreamer.in;
     case AUDIODEV_DRIVER_WAV:
         return dev->u.wav.in;
 
diff --git a/Cargo.lock b/Cargo.lock
index e6102b258b..274dd1be4a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,15 +4,21 @@ version = 4
 
 [[package]]
 name = "anyhow"
-version = "1.0.98"
+version = "1.0.100"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
 
 [[package]]
 name = "arbitrary-int"
-version = "1.2.7"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "c84fc003e338a6f69fbd4f7fe9f92b535ff13e9af8997f3b14b6ddff8b1df46d"
+checksum = "825297538d77367557b912770ca3083f778a196054b3ee63b22673c4a3cae0a5"
+
+[[package]]
+name = "atomic_refcell"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c"
 
 [[package]]
 name = "attrs"
@@ -24,6 +30,29 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "audio"
+version = "0.1.0"
+dependencies = [
+ "bql",
+ "common",
+ "futures",
+ "gio-sys",
+ "glib-sys",
+ "gstreamer-app",
+ "gstreamer-audio",
+ "qom",
+ "system",
+ "trace",
+ "util",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
 [[package]]
 name = "bilge"
 version = "0.2.0"
@@ -40,13 +69,19 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "feb11e002038ad243af39c2068c8a72bcf147acf05025dcdb916fcc000adb2d8"
 dependencies = [
- "itertools",
+ "itertools 0.11.0",
  "proc-macro-error",
  "proc-macro2",
  "quote",
  "syn",
 ]
 
+[[package]]
+name = "bitflags"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
+
 [[package]]
 name = "bits"
 version = "0.1.0"
@@ -63,14 +98,20 @@ dependencies = [
 
 [[package]]
 name = "cfg-expr"
-version = "0.20.3"
+version = "0.20.4"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "1a2c5f3bf25ec225351aa1c8e230d04d880d3bd89dea133537dafad4ae291e5c"
+checksum = "9acd0bdbbf4b2612d09f52ba61da432140cb10930354079d0d53fafc12968726"
 dependencies = [
  "smallvec",
  "target-lexicon",
 ]
 
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
 [[package]]
 name = "chardev"
 version = "0.1.0"
@@ -93,9 +134,9 @@ dependencies = [
 
 [[package]]
 name = "either"
-version = "1.12.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
 
 [[package]]
 name = "equivalent"
@@ -105,13 +146,149 @@ checksum = 
"877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
 
 [[package]]
 name = "foreign"
-version = "0.3.1"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "17ca1b5be8c9d320daf386f1809c7acc0cb09accbae795c2001953fa50585846"
+checksum = "bec05eb9c07a3f66653535e5e50eb5eb935eb00d3bb7e06ea26a9d3a1d016182"
 dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "171ed2f6dd927abbe108cfd9eebff2052c335013f5879d55bab0dc1dee19b706"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "windows-sys",
+]
+
+[[package]]
+name = "glib"
+version = "0.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "5b9dbecb1c33e483a98be4acfea2ab369e1c28f517c6eadb674537409c25c4b2"
+dependencies = [
+ "bitflags",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "smallvec",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "880e524e0085f3546cfb38532b2c202c0d64741d9977a6e4aa24704bfc9f19fb"
+dependencies = [
+ "heck",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "glib-sys"
 version = "0.21.2"
@@ -122,11 +299,144 @@ dependencies = [
  "system-deps",
 ]
 
+[[package]]
+name = "gobject-sys"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "538e41d8776173ec107e7b0f2aceced60abc368d7e1d81c1f0e2ecd35f59080d"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer"
+version = "0.24.3"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "69ac2f12970a2f85a681d2ceaa40c32fe86cc202ead315e0dfa2223a1217cd24"
+dependencies = [
+ "cfg-if",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "glib",
+ "gstreamer-sys",
+ "itertools 0.14.0",
+ "kstring",
+ "libc",
+ "muldiv",
+ "num-integer",
+ "num-rational",
+ "option-operations",
+ "pastey",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gstreamer-app"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "0af5d403738faf03494dfd502d223444b4b44feb997ba28ab3f118ee6d40a0b2"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "glib",
+ "gstreamer",
+ "gstreamer-app-sys",
+ "gstreamer-base",
+ "libc",
+]
+
+[[package]]
+name = "gstreamer-app-sys"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "aaf1a3af017f9493c34ccc8439cbce5c48f6ddff6ec0514c23996b374ff25f9a"
+dependencies = [
+ "glib-sys",
+ "gstreamer-base-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-audio"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "68e540174d060cd0d7ee2c2356f152f05d8262bf102b40a5869ff799377269d8"
+dependencies = [
+ "cfg-if",
+ "glib",
+ "gstreamer",
+ "gstreamer-audio-sys",
+ "gstreamer-base",
+ "libc",
+ "smallvec",
+]
+
+[[package]]
+name = "gstreamer-audio-sys"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "626cd3130bc155a8b6d4ac48cfddc15774b5a6cc76fcb191aab09a2655bad8f5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "gstreamer-base-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-base"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "71ff9b0bbc8041f0c6c8a53b206a6542f86c7d9fa8a7dff3f27d9c374d9f39b4"
+dependencies = [
+ "atomic_refcell",
+ "cfg-if",
+ "glib",
+ "gstreamer",
+ "gstreamer-base-sys",
+ "libc",
+]
+
+[[package]]
+name = "gstreamer-base-sys"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "fed78852b92db1459b8f4288f86e6530274073c20be2f94ba642cddaca08b00e"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "gstreamer-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gstreamer-sys"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "a24ae2930e683665832a19ef02466094b09d1f2da5673f001515ed5486aa9377"
+dependencies = [
+ "cfg-if",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
 [[package]]
 name = "hashbrown"
-version = "0.16.0"
+version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
 
 [[package]]
 name = "heck"
@@ -165,9 +475,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.11.4"
+version = "2.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
+checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
 dependencies = [
  "equivalent",
  "hashbrown",
@@ -182,11 +492,29 @@ dependencies = [
  "either",
 ]
 
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "kstring"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1"
+dependencies = [
+ "static_assertions",
+]
+
 [[package]]
 name = "libc"
-version = "0.2.162"
+version = "0.2.177"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
+checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
 
 [[package]]
 name = "memchr"
@@ -205,6 +533,67 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "muldiv"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "option-operations"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "b31ce827892359f23d3cd1cc4c75a6c241772bbd2db17a92dcf27cbefdf52689"
+dependencies = [
+ "pastey",
+]
+
+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
 [[package]]
 name = "pkg-config"
 version = "0.3.32"
@@ -236,6 +625,15 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "136558b6e1ebaecc92755d0ffaf9421f519531bed30cc2ad23b22cb00965cc5e"
 
+[[package]]
+name = "proc-macro-crate"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
+dependencies = [
+ "toml_edit",
+]
+
 [[package]]
 name = "proc-macro-error"
 version = "1.0.4"
@@ -261,9 +659,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.95"
+version = "1.0.103"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
 dependencies = [
  "unicode-ident",
 ]
@@ -292,18 +690,18 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.36"
+version = "1.0.42"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
 dependencies = [
  "proc-macro2",
 ]
 
 [[package]]
 name = "serde"
-version = "1.0.226"
+version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
 dependencies = [
  "serde_core",
  "serde_derive",
@@ -311,18 +709,18 @@ dependencies = [
 
 [[package]]
 name = "serde_core"
-version = "1.0.226"
+version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.226"
+version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -331,24 +729,36 @@ dependencies = [
 
 [[package]]
 name = "serde_spanned"
-version = "0.6.9"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
 dependencies = [
- "serde",
+ "serde_core",
 ]
 
+[[package]]
+name = "slab"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+
 [[package]]
 name = "smallvec"
 version = "1.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
 
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
 [[package]]
 name = "syn"
-version = "2.0.104"
+version = "2.0.111"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
+checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -367,9 +777,9 @@ dependencies = [
 
 [[package]]
 name = "system-deps"
-version = "7.0.5"
+version = "7.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb"
+checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f"
 dependencies = [
  "cfg-expr",
  "heck",
@@ -380,9 +790,9 @@ dependencies = [
 
 [[package]]
 name = "target-lexicon"
-version = "0.13.2"
+version = "0.13.3"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
+checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
 
 [[package]]
 name = "tests"
@@ -398,40 +808,77 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "thiserror"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "toml"
-version = "0.8.23"
+version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
 dependencies = [
- "serde",
+ "indexmap",
+ "serde_core",
  "serde_spanned",
  "toml_datetime",
- "toml_edit",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
 ]
 
 [[package]]
 name = "toml_datetime"
-version = "0.6.11"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
 dependencies = [
- "serde",
+ "serde_core",
 ]
 
 [[package]]
 name = "toml_edit"
-version = "0.22.27"
+version = "0.23.7"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
 dependencies = [
  "indexmap",
- "serde",
- "serde_spanned",
  "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
+dependencies = [
  "winnow",
 ]
 
+[[package]]
+name = "toml_writer"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
+
 [[package]]
 name = "trace"
 version = "0.1.0"
@@ -442,9 +889,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.12"
+version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
 
 [[package]]
 name = "util"
@@ -461,21 +908,36 @@ dependencies = [
 
 [[package]]
 name = "version-compare"
-version = "0.2.0"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
+checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
 
 [[package]]
 name = "version_check"
-version = "0.9.4"
+version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
 
 [[package]]
 name = "winnow"
-version = "0.7.13"
+version = "0.7.14"
 source = "registry+https://github.com/rust-lang/crates.io-index";
-checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
+checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
 dependencies = [
  "memchr",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 8e0fbab290..0ced3297e1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
 [workspace]
 resolver = "2"
 members = [
+    "rust/audio",
     "rust/hw/char/pl011",
     "rust/hw/timer/hpet",
     "rust/tests",
@@ -18,8 +19,13 @@ authors = ["The QEMU Project Developers 
<[email protected]>"]
 [workspace.dependencies]
 anyhow = "~1.0"
 foreign = "~0.3.1"
+futures = "0.3"
 libc = "0.2.162"
 glib-sys = { version = "0.21.2", features = ["v2_66"] }
+gio-sys = { version = "0.21.2", features = ["v2_66"] }
+gobect-sys = { version = "0.21.2", features = ["v2_66"] }
+gst_app = { package = "gstreamer-app", version = "0.24.2", features = 
["v1_24"] }
+gst_audio = { package = "gstreamer-audio", version = "0.24.2" }
 serde = { version = "1.0.226", features = ["derive"] }
 serde_derive = "1.0.226"
 
diff --git a/audio/trace-events b/audio/trace-events
index f7f639d960..d71d85fe01 100644
--- a/audio/trace-events
+++ b/audio/trace-events
@@ -34,3 +34,8 @@ audio_be_set_active_out(void *sw, bool on) "sw=%p, on=%d"
 audio_timer_start(int interval) "interval %d ms"
 audio_timer_stop(void) ""
 audio_timer_delayed(int interval) "interval %d ms"
+
+# gstreamer.rs
+gst_write(size_t bytes_written, uint64_t elapsed_ms, size_t bytes_per_second) 
"bytes_written=%zu, elapsed_ms=%" PRIu64 ", bytes_per_second=%zu"
+gst_need_data(const char *name, size_t len) "name=%s, len=%zu"
+gst_new_sample(const char *name, size_t len) "name=%s, len=%zu"
diff --git a/rust/audio/Cargo.toml b/rust/audio/Cargo.toml
new file mode 100644
index 0000000000..ceeb1a5b14
--- /dev/null
+++ b/rust/audio/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "audio"
+version = "0.1.0"
+description = "Rust bindings for QEMU/audio"
+resolver = "2"
+publish = false
+
+authors.workspace = true
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+bql = { path = "../bql" }
+futures = { workspace = true }
+glib-sys = { workspace = true }
+gio-sys = { workspace = true }
+gst_app = { workspace = true }
+gst_audio = { workspace = true }
+common = { path = "../common" }
+qom = { path = "../qom" }
+util = { path = "../util" }
+system = { path = "../system" }
+trace = { path = "../trace" }
+
+[lints]
+workspace = true
diff --git a/rust/audio/build.rs b/rust/audio/build.rs
new file mode 100644
index 0000000000..5654d1d562
--- /dev/null
+++ b/rust/audio/build.rs
@@ -0,0 +1,49 @@
+// Copyright 2024, Linaro Limited
+// Author(s): Manos Pitsidianakis <[email protected]>
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#[cfg(unix)]
+use std::os::unix::fs::symlink as symlink_file;
+#[cfg(windows)]
+use std::os::windows::fs::symlink_file;
+use std::{env, fs::remove_file, io::Result, path::Path};
+
+fn main() -> Result<()> {
+    let manifest_dir = env!("CARGO_MANIFEST_DIR");
+    let file = if let Ok(root) = env::var("MESON_BUILD_ROOT") {
+        let sub = get_rust_subdir(manifest_dir).unwrap();
+        format!("{root}/{sub}/bindings.inc.rs")
+    } else {
+        // Placing bindings.inc.rs in the source directory is supported
+        // but not documented or encouraged.
+        format!("{manifest_dir}/src/bindings.inc.rs")
+    };
+
+    let file = Path::new(&file);
+    if !Path::new(&file).exists() {
+        panic!(concat!(
+            "\n",
+            "    No generated C bindings found! Maybe you wanted one of\n",
+            "    `make clippy`, `make rustfmt`, `make rustdoc`?\n",
+            "\n",
+            "    For other uses of `cargo`, start a subshell with\n",
+            "    `pyvenv/bin/meson devenv`, or point MESON_BUILD_ROOT to\n",
+            "    the top of the build tree."
+        ));
+    }
+
+    let out_dir = env::var("OUT_DIR").unwrap();
+    let dest_path = format!("{out_dir}/bindings.inc.rs");
+    let dest_path = Path::new(&dest_path);
+    if dest_path.symlink_metadata().is_ok() {
+        remove_file(dest_path)?;
+    }
+    symlink_file(file, dest_path)?;
+
+    println!("cargo:rerun-if-changed=build.rs");
+    Ok(())
+}
+
+fn get_rust_subdir(path: &str) -> Option<&str> {
+    path.find("/rust").map(|index| &path[index + 1..])
+}
diff --git a/rust/audio/meson.build b/rust/audio/meson.build
new file mode 100644
index 0000000000..27519a3e3f
--- /dev/null
+++ b/rust/audio/meson.build
@@ -0,0 +1,75 @@
+c_enums = [
+  'audcnotification_e',
+  'AudioFormat',
+]
+_audio_bindgen_args = []
+foreach enum : c_enums
+  _audio_bindgen_args += ['--rustified-enum', enum]
+endforeach
+
+blocked_type = [
+  'Error',
+  'ObjectClass',
+]
+foreach type: blocked_type
+  _audio_bindgen_args += ['--blocklist-type', type]
+endforeach
+
+
+# TODO: Remove this comment when the clang/libclang mismatch issue is solved.
+#
+# Rust bindings generation with `bindgen` might fail in some cases where the
+# detected `libclang` does not match the expected `clang` version/target. In
+# this case you must pass the path to `clang` and `libclang` to your build
+# command invocation using the environment variables CLANG_PATH and
+# LIBCLANG_PATH
+_audio_bindings_inc_rs = rust.bindgen(
+  input: 'wrapper.h',
+  dependencies: common_ss.all_dependencies(),
+  output: 'bindings.inc.rs',
+  include_directories: bindings_incdir,
+  bindgen_version: ['>=0.60.0'],
+  args: bindgen_args_common + _audio_bindgen_args,
+  c_args: bindgen_c_args,
+)
+
+_audio_rs = static_library(
+  'audio',
+  structured_sources(
+    [
+      'src/lib.rs',
+      'src/bindings.rs',
+      'src/audio.rs',
+      'src/gstreamer.rs',
+    ],
+    {'.': _audio_bindings_inc_rs}
+  ),
+  override_options: ['rust_std=2021', 'build.rust_std=2021'],
+  rust_abi: 'rust',
+  link_with: [
+    _util_rs,
+    _bql_rs,
+    _qom_rs,
+    _trace_rs,
+    _system_rs,
+  ],
+  dependencies: [
+    gio_sys_rs,
+    glib_sys_rs,
+    gst_app_rs,
+    gst_audio_rs,
+    common_rs,
+    futures_rs,
+  ],
+  rust_dependency_map: {
+    'gstreamer_app': 'gst_app',
+    'gstreamer_audio': 'gst_audio',
+  }
+)
+
+audio_rs = declare_dependency(link_with: [_audio_rs])
+
+rust_devices_ss.add(declare_dependency(
+  link_whole: [_audio_rs],
+  variables: {'crate': 'audio'},
+))
diff --git a/rust/audio/src/audio.rs b/rust/audio/src/audio.rs
new file mode 100644
index 0000000000..436298ea17
--- /dev/null
+++ b/rust/audio/src/audio.rs
@@ -0,0 +1,516 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+use std::{
+    ffi::{CStr, CString},
+    ptr::NonNull,
+};
+
+use common::Opaque;
+use qom::{prelude::*, ObjectImpl};
+use util::{Error, Result};
+
+use crate::bindings;
+
+/// Called when audio data needs to be processed. The callback receives:
+/// - `available`: Number of bytes/frames available for processing
+pub type AudioCallback = Box<dyn Fn(i32) + Send>;
+
+pub type CaptureNotification = bindings::audcnotification_e;
+
+pub trait AudioCaptureOps {
+    fn notify(&mut self, cmd: CaptureNotification);
+
+    fn capture(&mut self, buf: &[u8]);
+
+    fn destroy(&mut self);
+}
+
+/// A safe wrapper around [`bindings::AudioBackend`].
+#[repr(transparent)]
+#[derive(common::Wrapper)]
+pub struct AudioBackend(Opaque<bindings::AudioBackend>);
+
+pub type AudioBackendClass = bindings::AudioBackendClass;
+
+pub trait AudioBackendImpl: ObjectImpl {
+    type VoiceOut;
+    type VoiceIn;
+    type Capture;
+
+    fn realize(&self, dev: &bindings::Audiodev) -> Result<()>;
+
+    fn id(&self) -> &CString;
+
+    fn open_out(
+        &self,
+        sw: Option<Self::VoiceOut>,
+        name: &CStr,
+        callback_fn: AudioCallback,
+        settings: &bindings::audsettings,
+    ) -> Option<Self::VoiceOut>;
+
+    fn open_in(
+        &self,
+        sw: Option<Self::VoiceIn>,
+        name: &CStr,
+        callback_fn: AudioCallback,
+        settings: &bindings::audsettings,
+    ) -> Option<Self::VoiceIn>;
+
+    fn close_out(&self, sw: Self::VoiceOut);
+
+    fn close_in(&self, sw: Self::VoiceIn);
+
+    fn is_active_out(&self, sw: &Self::VoiceOut) -> bool;
+
+    fn is_active_in(&self, sw: &Self::VoiceIn) -> bool;
+
+    fn set_active_out(&self, sw: &mut Self::VoiceOut, on: bool);
+
+    fn set_active_in(&self, sw: &mut Self::VoiceIn, on: bool);
+
+    fn set_volume_out(&self, sw: &mut Self::VoiceOut, vol: &bindings::Volume);
+
+    fn set_volume_in(&self, sw: &mut Self::VoiceIn, vol: &bindings::Volume);
+
+    fn write(&self, sw: &mut Self::VoiceOut, buf: &[u8]) -> usize;
+
+    fn read(&self, sw: &mut Self::VoiceIn, buf: &mut [u8]) -> usize;
+
+    fn get_buffer_size_out(&self, sw: &mut Self::VoiceOut) -> i32;
+
+    fn add_capture(
+        &self,
+        settings: &bindings::audsettings,
+        ops: Box<dyn AudioCaptureOps + Send>,
+    ) -> Option<Self::Capture>;
+
+    fn del_capture(&self, cap: Self::Capture);
+
+    fn set_dbus_server(
+        &self,
+        manager: *mut gio_sys::GDBusObjectManagerServer,
+        p2p: bool,
+    ) -> Result<()>;
+
+    /// Convert from opaque `SWVoiceOut` pointer to `VoiceOut` using Box
+    ///
+    /// # Safety
+    /// The pointer must be a valid pointer that was previously created by
+    /// `voice_out_to_ptr`
+    unsafe fn ptr_to_voice_out(&self, ptr: NonNull<bindings::SWVoiceOut>) -> 
Box<Self::VoiceOut> {
+        unsafe { Box::from_raw(ptr.as_ptr().cast::<Self::VoiceOut>()) }
+    }
+
+    /// Convert from `VoiceOut` to opaque `SWVoiceOut` pointer using Box
+    fn voice_out_to_ptr(&self, voice: Box<Self::VoiceOut>) -> 
NonNull<bindings::SWVoiceOut> {
+        
NonNull::new(Box::into_raw(voice).cast::<bindings::SWVoiceOut>()).unwrap()
+    }
+
+    /// Convert from opaque `SWVoiceIn` pointer to `VoiceIn` using Box
+    ///
+    /// # Safety
+    /// The pointer must be a valid pointer that was previously created by
+    /// `voice_in_to_ptr`
+    unsafe fn ptr_to_voice_in(&self, ptr: NonNull<bindings::SWVoiceIn>) -> 
Box<Self::VoiceIn> {
+        unsafe { Box::from_raw(ptr.as_ptr().cast::<Self::VoiceIn>()) }
+    }
+
+    /// Convert from `VoiceIn` to opaque `SWVoiceIn` pointer using Box
+    fn voice_in_to_ptr(&self, voice: Box<Self::VoiceIn>) -> 
NonNull<bindings::SWVoiceIn> {
+        
NonNull::new(Box::into_raw(voice).cast::<bindings::SWVoiceIn>()).unwrap()
+    }
+
+    /// Convert from opaque `CaptureVoiceOut` pointer to Capture using Box
+    ///
+    /// # Safety
+    /// The pointer must be a valid pointer that was previously created by
+    /// `capture_to_ptr`
+    unsafe fn ptr_to_capture(&self, ptr: NonNull<bindings::CaptureVoiceOut>) 
-> Box<Self::Capture> {
+        unsafe { Box::from_raw(ptr.as_ptr().cast::<Self::Capture>()) }
+    }
+
+    /// Convert from Capture to opaque `CaptureVoiceOut` pointer using Box
+    fn capture_to_ptr(&self, capture: Self::Capture) -> 
NonNull<bindings::CaptureVoiceOut> {
+        
NonNull::new(Box::into_raw(Box::new(capture)).cast::<bindings::CaptureVoiceOut>()).unwrap()
+    }
+}
+
+unsafe extern "C" fn rust_realize_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    dev: *mut bindings::Audiodev,
+    errp: *mut *mut util::bindings::Error,
+) -> bool {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let dev = NonNull::new(dev).unwrap().cast();
+    let result = unsafe { be.as_ref() }.realize(unsafe { dev.as_ref() });
+    unsafe { Error::bool_or_propagate(result, errp) }
+}
+
+unsafe extern "C" fn rust_get_id_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+) -> *const std::os::raw::c_char {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let id = unsafe { be.as_ref() }.id();
+    id.as_ptr()
+}
+
+unsafe extern "C" fn rust_open_out_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceOut,
+    name: *const std::os::raw::c_char,
+    callback_opaque: *mut std::os::raw::c_void,
+    callback_fn: Option<unsafe extern "C" fn(*mut std::os::raw::c_void, 
std::os::raw::c_int)>,
+    settings: *mut bindings::audsettings,
+) -> *mut bindings::SWVoiceOut {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let name = unsafe { CStr::from_ptr(name) };
+    let settings = unsafe { settings.as_ref().unwrap() };
+
+    let sw_voice = NonNull::new(sw).map(|ptr| unsafe { 
be_ref.ptr_to_voice_out(ptr) });
+
+    let rust_callback = c_callback_to_rust(callback_fn, callback_opaque);
+
+    let result = be_ref.open_out(
+        sw_voice.map(|boxed| *boxed),
+        name,
+        rust_callback.unwrap(),
+        settings,
+    );
+
+    result.map_or(std::ptr::null_mut(), |voice| {
+        be_ref.voice_out_to_ptr(Box::new(voice)).as_ptr()
+    })
+}
+
+unsafe extern "C" fn rust_open_in_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceIn,
+    name: *const std::os::raw::c_char,
+    callback_opaque: *mut std::os::raw::c_void,
+    callback_fn: Option<unsafe extern "C" fn(*mut std::os::raw::c_void, 
std::os::raw::c_int)>,
+    settings: *mut bindings::audsettings,
+) -> *mut bindings::SWVoiceIn {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let name = unsafe { CStr::from_ptr(name) };
+    let settings = unsafe { settings.as_ref().unwrap() };
+
+    let sw_voice = NonNull::new(sw).map(|ptr| unsafe { 
be_ref.ptr_to_voice_in(ptr) });
+
+    let rust_callback = c_callback_to_rust(callback_fn, callback_opaque);
+
+    let result = be_ref.open_in(
+        sw_voice.map(|boxed| *boxed),
+        name,
+        rust_callback.unwrap(),
+        settings,
+    );
+
+    result.map_or(std::ptr::null_mut(), |voice| {
+        be_ref.voice_in_to_ptr(Box::new(voice)).as_ptr()
+    })
+}
+
+unsafe extern "C" fn rust_close_out_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceOut,
+) {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let sw = NonNull::new(sw).unwrap();
+    let voice = unsafe { be_ref.ptr_to_voice_out(sw) };
+    be_ref.close_out(*voice);
+}
+
+unsafe extern "C" fn rust_close_in_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceIn,
+) {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let sw = NonNull::new(sw).unwrap();
+    let voice = unsafe { be_ref.ptr_to_voice_in(sw) };
+    be_ref.close_in(*voice);
+}
+
+unsafe extern "C" fn rust_is_active_out_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceOut,
+) -> bool {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let sw = NonNull::new(sw).unwrap();
+    let voice = unsafe { be_ref.ptr_to_voice_out(sw) };
+    let result = be_ref.is_active_out(&voice);
+    std::mem::forget(voice);
+    result
+}
+
+unsafe extern "C" fn rust_is_active_in_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceIn,
+) -> bool {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let sw = NonNull::new(sw).unwrap();
+    let voice = unsafe { be_ref.ptr_to_voice_in(sw) };
+    let result = be_ref.is_active_in(&voice);
+    std::mem::forget(voice);
+    result
+}
+
+unsafe extern "C" fn rust_set_active_out_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceOut,
+    on: bool,
+) {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let sw = NonNull::new(sw).unwrap();
+    let mut voice = unsafe { be_ref.ptr_to_voice_out(sw) };
+    be_ref.set_active_out(&mut voice, on);
+    std::mem::forget(voice);
+}
+
+unsafe extern "C" fn rust_set_active_in_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceIn,
+    on: bool,
+) {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let sw = NonNull::new(sw).unwrap();
+    let mut voice = unsafe { be_ref.ptr_to_voice_in(sw) };
+    be_ref.set_active_in(&mut voice, on);
+    std::mem::forget(voice);
+}
+
+unsafe extern "C" fn rust_set_volume_out_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceOut,
+    vol: *mut bindings::Volume,
+) {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let sw = NonNull::new(sw).unwrap();
+    let vol = unsafe { vol.as_ref().unwrap() };
+    let mut voice = unsafe { be_ref.ptr_to_voice_out(sw) };
+    be_ref.set_volume_out(&mut voice, vol);
+    std::mem::forget(voice);
+}
+
+unsafe extern "C" fn rust_set_volume_in_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceIn,
+    vol: *mut bindings::Volume,
+) {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let sw = NonNull::new(sw).unwrap();
+    let vol = unsafe { vol.as_ref().unwrap() };
+    let mut voice = unsafe { be_ref.ptr_to_voice_in(sw) };
+    be_ref.set_volume_in(&mut voice, vol);
+    std::mem::forget(voice);
+}
+
+unsafe extern "C" fn rust_write_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceOut,
+    buf: *mut std::os::raw::c_void,
+    size: usize,
+) -> usize {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let sw = NonNull::new(sw).unwrap();
+    let mut voice = unsafe { be_ref.ptr_to_voice_out(sw) };
+    let buf_slice = unsafe { std::slice::from_raw_parts(buf as *const u8, 
size) };
+    let result = be_ref.write(&mut voice, buf_slice);
+    // Put the voice back - we need to leak it since we only borrowed it
+    std::mem::forget(voice);
+    result
+}
+
+unsafe extern "C" fn rust_read_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceIn,
+    buf: *mut std::os::raw::c_void,
+    size: usize,
+) -> usize {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let sw = NonNull::new(sw).unwrap();
+    let mut voice = unsafe { be_ref.ptr_to_voice_in(sw) };
+    let buf_slice = unsafe { std::slice::from_raw_parts_mut(buf.cast::<u8>(), 
size) };
+    let result = be_ref.read(&mut voice, buf_slice);
+    std::mem::forget(voice);
+    result
+}
+
+unsafe extern "C" fn rust_get_buffer_size_out_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    sw: *mut bindings::SWVoiceOut,
+) -> std::os::raw::c_int {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let sw = NonNull::new(sw).unwrap();
+    let mut voice = unsafe { be_ref.ptr_to_voice_out(sw) };
+    let result = be_ref.get_buffer_size_out(&mut voice);
+    std::mem::forget(voice);
+    result
+}
+
+struct CToRustCaptureOpsBridge {
+    notify_fn:
+        Option<unsafe extern "C" fn(*mut std::os::raw::c_void, 
bindings::audcnotification_e)>,
+    capture_fn: Option<
+        unsafe extern "C" fn(
+            *mut std::os::raw::c_void,
+            *const std::os::raw::c_void,
+            std::os::raw::c_int,
+        ),
+    >,
+    destroy_fn: Option<unsafe extern "C" fn(*mut std::os::raw::c_void)>,
+    opaque: *mut std::os::raw::c_void,
+}
+
+// SAFETY: The bridge is Send because it only contains function pointers and 
raw
+// pointers. The caller must ensure thread safety when using the opaque 
pointer.
+unsafe impl Send for CToRustCaptureOpsBridge {}
+
+impl CToRustCaptureOpsBridge {
+    fn new(c_ops: &bindings::audio_capture_ops, opaque: *mut 
std::os::raw::c_void) -> Self {
+        Self {
+            notify_fn: c_ops.notify,
+            capture_fn: c_ops.capture,
+            destroy_fn: c_ops.destroy,
+            opaque,
+        }
+    }
+}
+
+impl AudioCaptureOps for CToRustCaptureOpsBridge {
+    fn notify(&mut self, cmd: CaptureNotification) {
+        if let Some(notify_fn) = self.notify_fn {
+            unsafe {
+                notify_fn(self.opaque, cmd);
+            }
+        }
+    }
+
+    fn capture(&mut self, buf: &[u8]) {
+        if let Some(capture_fn) = self.capture_fn {
+            unsafe {
+                capture_fn(
+                    self.opaque,
+                    buf.as_ptr().cast::<std::os::raw::c_void>(),
+                    buf.len() as std::os::raw::c_int,
+                );
+            }
+        }
+    }
+
+    fn destroy(&mut self) {
+        if let Some(destroy_fn) = self.destroy_fn {
+            unsafe {
+                destroy_fn(self.opaque);
+            }
+        }
+    }
+}
+
+unsafe extern "C" fn rust_add_capture_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    settings: *mut bindings::audsettings,
+    ops: *mut bindings::audio_capture_ops,
+    cb_opaque: *mut std::os::raw::c_void,
+) -> *mut bindings::CaptureVoiceOut {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let settings = unsafe { settings.as_ref().unwrap() };
+    let ops = unsafe { ops.as_ref().unwrap() };
+
+    let bridge = CToRustCaptureOpsBridge::new(ops, cb_opaque);
+    let rust_ops = Box::new(bridge) as Box<dyn AudioCaptureOps + Send>;
+
+    let result = unsafe { be.as_ref() }.add_capture(settings, rust_ops);
+    result.map_or(std::ptr::null_mut(), |capture| {
+        unsafe { be.as_ref() }.capture_to_ptr(capture).as_ptr()
+    })
+}
+
+unsafe extern "C" fn rust_del_capture_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    cap: *mut bindings::CaptureVoiceOut,
+    _cb_opaque: *mut std::os::raw::c_void,
+) {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let be_ref = unsafe { be.as_ref() };
+    let cap_ptr = NonNull::new(cap).unwrap();
+    let capture = *unsafe { be_ref.ptr_to_capture(cap_ptr) };
+    be_ref.del_capture(capture);
+}
+
+unsafe extern "C" fn rust_set_dbus_server_fn<T: AudioBackendImpl>(
+    be: *mut bindings::AudioBackend,
+    manager: *mut gio_sys::GDBusObjectManagerServer,
+    p2p: bool,
+    errp: *mut *mut util::bindings::Error,
+) -> bool {
+    let be = NonNull::new(be).unwrap().cast::<T>();
+    let result = unsafe { be.as_ref() }.set_dbus_server(manager, p2p);
+    unsafe { Error::bool_or_propagate(result, errp) }
+}
+
+// SAFETY: The C audio callback is expected to handle its own thread safety.
+struct SendCb(
+    unsafe extern "C" fn(*mut std::os::raw::c_void, std::os::raw::c_int),
+    *mut std::os::raw::c_void,
+);
+unsafe impl Send for SendCb {}
+
+impl SendCb {
+    fn call(&self, available: i32) {
+        unsafe { (self.0)(self.1, available) }
+    }
+}
+
+fn c_callback_to_rust(
+    c_fn: Option<unsafe extern "C" fn(*mut std::os::raw::c_void, 
std::os::raw::c_int)>,
+    opaque: *mut std::os::raw::c_void,
+) -> Option<AudioCallback> {
+    c_fn.map(|f| -> AudioCallback {
+        let cb = SendCb(f, opaque);
+        Box::new(move |available| cb.call(available))
+    })
+}
+
+impl AudioBackendClass {
+    pub(crate) fn class_init<T: AudioBackendImpl>(&mut self) {
+        self.realize = Some(rust_realize_fn::<T>);
+        self.get_id = Some(rust_get_id_fn::<T>);
+        self.open_out = Some(rust_open_out_fn::<T>);
+        self.open_in = Some(rust_open_in_fn::<T>);
+        self.close_out = Some(rust_close_out_fn::<T>);
+        self.close_in = Some(rust_close_in_fn::<T>);
+        self.is_active_out = Some(rust_is_active_out_fn::<T>);
+        self.is_active_in = Some(rust_is_active_in_fn::<T>);
+        self.set_active_out = Some(rust_set_active_out_fn::<T>);
+        self.set_active_in = Some(rust_set_active_in_fn::<T>);
+        self.set_volume_out = Some(rust_set_volume_out_fn::<T>);
+        self.set_volume_in = Some(rust_set_volume_in_fn::<T>);
+        self.write = Some(rust_write_fn::<T>);
+        self.read = Some(rust_read_fn::<T>);
+        self.get_buffer_size_out = Some(rust_get_buffer_size_out_fn::<T>);
+        self.add_capture = Some(rust_add_capture_fn::<T>);
+        self.del_capture = Some(rust_del_capture_fn::<T>);
+        self.set_dbus_server = Some(rust_set_dbus_server_fn::<T>);
+    }
+}
+
+unsafe impl ObjectType for AudioBackend {
+    type Class = AudioBackendClass;
+    const TYPE_NAME: &'static CStr =
+        unsafe { 
CStr::from_bytes_with_nul_unchecked(bindings::TYPE_AUDIO_BACKEND) };
+}
+qom_isa!(AudioBackend: Object);
diff --git a/rust/audio/src/bindings.rs b/rust/audio/src/bindings.rs
new file mode 100644
index 0000000000..aee53473a8
--- /dev/null
+++ b/rust/audio/src/bindings.rs
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#![allow(
+    dead_code,
+    improper_ctypes_definitions,
+    improper_ctypes,
+    non_camel_case_types,
+    non_snake_case,
+    non_upper_case_globals,
+    unnecessary_transmutes,
+    unsafe_op_in_unsafe_fn,
+    clippy::pedantic,
+    clippy::restriction,
+    clippy::style,
+    clippy::missing_const_for_fn,
+    clippy::ptr_offset_with_cast,
+    clippy::useless_transmute,
+    clippy::missing_safety_doc,
+    clippy::too_many_arguments
+)]
+
+use gio_sys::GDBusObjectManagerServer;
+use glib_sys::{GHashTable, GHashTableIter, GPtrArray, GSList};
+use qom::bindings::ObjectClass;
+use util::bindings::Error;
+
+#[cfg(MESON)]
+include!("bindings.inc.rs");
+
+#[cfg(not(MESON))]
+include!(concat!(env!("OUT_DIR"), "/bindings.inc.rs"));
+
+unsafe impl Send for audio_capture_ops {}
diff --git a/rust/audio/src/gstreamer.rs b/rust/audio/src/gstreamer.rs
new file mode 100644
index 0000000000..8b99944c9a
--- /dev/null
+++ b/rust/audio/src/gstreamer.rs
@@ -0,0 +1,1070 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+use std::{
+    ffi::{CStr, CString},
+    sync::{
+        atomic::{AtomicBool, Ordering},
+        Arc, Mutex,
+    },
+    time::Instant,
+};
+
+use bql::{BqlCell, BqlRefCell};
+use common::uninit_field_mut;
+use futures::StreamExt;
+use gst::prelude::*;
+use gst_app::{glib, gst};
+use gst_audio::{self, prelude::StreamVolumeExt, StreamVolume};
+use qom::{prelude::*, ObjectImpl, ParentField, ParentInit};
+// Import qom traits explicitly to avoid ambiguity with gst traits
+use qom::{IsA as QomIsA, ObjectType as QomObjectType};
+use util::{log::Log, log_mask_ln, Error, Result};
+
+use crate::{
+    bindings::{self},
+    AudioBackend, AudioBackendImpl, AudioCallback, AudiodevGStreamerOptions,
+    AudiodevPerDirectionOptions, CaptureNotification,
+};
+
+::trace::include_trace!("audio");
+
+#[repr(C)]
+#[derive(qom::Object)]
+pub struct AudioGStreamer {
+    parent_obj: ParentField<AudioBackend>,
+
+    id: BqlCell<CString>,
+    opt: BqlRefCell<Option<AudiodevGStreamerOptions>>,
+    mixer_pipeline: BqlRefCell<Option<MixerPipeline>>,
+}
+
+qom_isa!(AudioGStreamer : AudioBackend, Object);
+
+#[repr(C)]
+pub struct AudioGStreamerClass {
+    parent_class: <AudioBackend as QomObjectType>::Class,
+}
+
+// Shared mixer pipeline for mixing multiple audio voices
+struct MixerPipeline {
+    pipeline: gst::Pipeline,
+    mixer: gst::Element,
+    tee: gst::Element,
+    #[allow(dead_code)] // For RAII reasons
+    bus_watch: gst::bus::BusWatchGuard,
+}
+
+trait AudioGStreamerImpl: AudioBackendImpl + QomIsA<AudioGStreamer> {}
+
+impl AudioGStreamerClass {
+    fn class_init<T: AudioGStreamerImpl>(&mut self) {
+        self.parent_class.class_init::<T>();
+    }
+}
+
+impl AudioGStreamerImpl for AudioGStreamer {}
+
+unsafe impl QomObjectType for AudioGStreamer {
+    type Class = AudioGStreamerClass;
+
+    const TYPE_NAME: &'static CStr = crate::TYPE_AUDIO_GSTREAMER;
+}
+
+impl ObjectImpl for AudioGStreamer {
+    type ParentType = AudioBackend;
+
+    const INSTANCE_INIT: Option<unsafe fn(ParentInit<Self>)> = 
Some(Self::init);
+    const CLASS_INIT: fn(&mut Self::Class) = Self::Class::class_init::<Self>;
+}
+
+enum VoiceOutKind {
+    Mixed,
+    Standalone(gst::bus::BusWatchGuard),
+}
+
+pub struct VoiceOut {
+    kind: VoiceOutKind,
+    pipeline: gst::Pipeline,
+    stream_volume: Option<StreamVolume>,
+    appsrc: gst_app::AppSrc,
+    active: Arc<AtomicBool>,
+    active_time: Option<Instant>,
+    bytes_written: usize,
+    bytes_per_second: usize,
+    offset_time: Option<gst::ClockTime>,
+}
+
+pub struct VoiceIn {
+    pipeline: gst::Pipeline,
+    appsink: gst_app::AppSink,
+    active: bool,
+    pending_data: Arc<Mutex<Vec<u8>>>,
+}
+
+pub struct Capture {
+    bin: gst::Bin,
+    tee_pad: gst::Pad,
+    ops: Arc<Mutex<Box<dyn crate::audio::AudioCaptureOps + Send>>>,
+}
+
+impl AudioBackendImpl for AudioGStreamer {
+    type VoiceOut = VoiceOut;
+    type VoiceIn = VoiceIn;
+    type Capture = Capture;
+
+    fn realize(&self, dev: &bindings::Audiodev) -> Result<()> {
+        if dev.id.is_null() {
+            return Err(Error::msg("dev.id is null"));
+        }
+
+        gst::init()?;
+        let id_cstr = unsafe { CStr::from_ptr(dev.id) };
+        let id = CString::new(id_cstr.to_bytes())?;
+        self.id.set(id);
+
+        // ideally using qapi-rs instead
+        let gst = unsafe { &dev.u.gstreamer };
+        let opt: AudiodevGStreamerOptions = gst.into();
+
+        if opt
+            .out
+            .as_ref()
+            .and_then(|o| o.mixing_engine)
+            .unwrap_or(false)
+        {
+            let mixer_pipeline = create_mixer_pipeline(&opt)?;
+            *self.mixer_pipeline.borrow_mut() = Some(mixer_pipeline);
+        } else {
+            if opt
+                .out
+                .as_ref()
+                .and_then(|o| o.fixed_settings)
+                .unwrap_or(false)
+            {
+                log_mask_ln!(
+                    Log::Unimp,
+                    "Fixed settings aren't supported in non-mixer mode"
+                );
+            }
+            if opt.out.as_ref().and_then(|o| o.buffer_length).is_some() {
+                log_mask_ln!(
+                    Log::Unimp,
+                    "Buffer length isn't supported in non-mixer mode"
+                );
+            }
+        }
+
+        if opt.in_ != Default::default() {
+            log_mask_ln!(Log::Unimp, "Input configuration isn't supported 
yet");
+        }
+
+        *self.opt.borrow_mut() = Some(opt);
+
+        Ok(())
+    }
+
+    fn id(&self) -> &CString {
+        unsafe { &*self.id.as_ptr() }
+    }
+
+    fn open_out(
+        &self,
+        sw: Option<Self::VoiceOut>,
+        name: &CStr,
+        callback_fn: AudioCallback,
+        settings: &bindings::audsettings,
+    ) -> Option<Self::VoiceOut> {
+        let mut active = false;
+        if let Some(voice) = sw {
+            active = voice.active.load(Ordering::Relaxed);
+            self.close_out(voice);
+        }
+
+        let mixer_pipeline_ref = self.mixer_pipeline.borrow();
+        let mut result = if let Some(mixer_pipeline) = 
mixer_pipeline_ref.as_ref() {
+            create_mixed_voice(mixer_pipeline, settings, name)
+        } else {
+            create_standalone_voice(settings, name, 
self.opt.borrow().as_ref().unwrap())
+        };
+
+        if let Ok(voice) = &mut result {
+            let (mut tx, mut rx) = futures::channel::mpsc::channel(1);
+
+            let appsrc_clone = voice.appsrc.clone();
+            let active_clone = voice.active.clone();
+            glib::MainContext::default().spawn(async move {
+                loop {
+                    // TODO: some emulated devices don't call write() after 
callback (ex: hda)
+                    // we need to call them regularly. Ideally, they should 
call write() when data is available.
+                    let timeout = 
glib::timeout_future(std::time::Duration::from_millis(10));
+
+                    match futures::future::select(rx.next(), timeout).await {
+                        futures::future::Either::Left((Some(size), _)) => {
+                            let size = i32::try_from(size).unwrap_or(i32::MAX);
+                            callback_fn(size);
+                        }
+                        futures::future::Either::Left((None, _)) => break,
+                        futures::future::Either::Right(_) => {
+                            if active_clone.load(Ordering::Relaxed)
+                                && appsrc_clone.current_level_bytes() == 0
+                            {
+                                callback_fn(4096);
+                            }
+                        }
+                    }
+                }
+            });
+
+            let name = CString::new(name.to_bytes()).unwrap();
+            #[allow(clippy::shadow_unrelated)]
+            let active_clone = voice.active.clone();
+            voice.appsrc.set_callbacks(
+                gst_app::AppSrcCallbacks::builder()
+                    .need_data(move |_src, size| {
+                        let size = size as usize;
+                        trace::trace_gst_need_data(name.as_c_str(), size);
+
+                        if active_clone.load(Ordering::Relaxed) {
+                            if let Err(err) = tx.try_send(size) {
+                                eprintln!("Failed to send need-data to main 
loop: {err}");
+                            }
+                        }
+                    })
+                    .build(),
+            );
+
+            self.set_active_out(voice, active);
+        };
+
+        result
+            .inspect_err(|e| eprintln!("Failed to create voice output: {e}"))
+            .ok()
+    }
+
+    fn open_in(
+        &self,
+        sw: Option<Self::VoiceIn>,
+        name: &CStr,
+        callback_fn: AudioCallback,
+        settings: &bindings::audsettings,
+    ) -> Option<Self::VoiceIn> {
+        let mut active = false;
+        if let Some(voice) = sw {
+            active = voice.active;
+            self.close_in(voice);
+        }
+
+        let mut result = 
create_input_voice(self.opt.borrow().as_ref().unwrap(), settings);
+
+        if let Ok(voice) = &mut result {
+            let (mut tx, mut rx) = 
futures::channel::mpsc::channel::<gst::Sample>(1);
+            let pending_data = voice.pending_data.clone();
+
+            let name = CString::new(name.to_bytes()).unwrap();
+            glib::MainContext::default().spawn(async move {
+                while let Some(sample) = rx.next().await {
+                    let Some(buffer) = sample.buffer() else {
+                        continue;
+                    };
+                    let size = buffer.size();
+                    trace::trace_gst_new_sample(name.as_c_str(), size);
+                    match buffer.map_readable() {
+                        Ok(buffer) => {
+                            if let Ok(mut data) = pending_data.lock() {
+                                data.extend_from_slice(buffer.as_slice());
+                            }
+                            callback_fn(size.try_into().unwrap());
+                        }
+                        Err(err) => {
+                            eprintln!("Failed to map buffer: {err}");
+                            continue;
+                        }
+                    };
+                }
+            });
+
+            voice.appsink.set_callbacks(
+                gst_app::AppSinkCallbacks::builder()
+                    .new_sample(move |sink| {
+                        // FIXME: with 1.28, we could get 
"current-level-bytes" instead, and let read() pull
+                        let sample = sink.pull_sample().map_err(|_| 
gst::FlowError::Eos)?;
+                        tx.try_send(sample).map_err(|_| gst::FlowError::Eos)?;
+                        Ok(gst::FlowSuccess::Ok)
+                    })
+                    .build(),
+            );
+
+            self.set_active_in(voice, active);
+        }
+
+        if let Err(e) = &result {
+            eprintln!("Failed to create voice input: {e}");
+        }
+        result.ok()
+    }
+
+    fn close_out(&self, mut sw: Self::VoiceOut) {
+        match sw.kind {
+            VoiceOutKind::Mixed => {
+                self.set_active_out(&mut sw, false);
+                if let Some(bin) = sw
+                    .appsrc
+                    .parent()
+                    .and_then(|p| p.downcast::<gst::Bin>().ok())
+                {
+                    let _ = bin.set_state(gst::State::Null);
+                    let mp = self.mixer_pipeline.borrow();
+                    if let Some(pad) = bin.sink_pads().first() {
+                        if let Some(peer) = pad.peer() {
+                            let _ = pad.unlink(&peer);
+                            
mp.as_ref().unwrap().mixer.release_request_pad(&peer);
+                        }
+                    }
+                    if let Err(err) = 
mp.as_ref().unwrap().pipeline.remove(&bin) {
+                        eprintln!("Failed to remove voice from pipeline: 
{err}");
+                    }
+                }
+            }
+            VoiceOutKind::Standalone(_bus_watch_guard) => {
+                if let Err(e) = sw.pipeline.set_state(gst::State::Null) {
+                    eprintln!("Warning: Failed to set to null state: {e}");
+                }
+            }
+        }
+    }
+
+    fn close_in(&self, mut sw: Self::VoiceIn) {
+        self.set_active_in(&mut sw, false);
+        if let Err(e) = sw.pipeline.set_state(gst::State::Null) {
+            eprintln!("Warning: Failed to set to null state: {e}");
+        }
+    }
+
+    fn is_active_out(&self, sw: &Self::VoiceOut) -> bool {
+        sw.active.load(Ordering::Relaxed)
+    }
+
+    fn is_active_in(&self, sw: &Self::VoiceIn) -> bool {
+        sw.active
+    }
+
+    fn set_active_out(&self, sw: &mut Self::VoiceOut, on: bool) {
+        if sw.active.load(Ordering::Relaxed) == on {
+            return;
+        }
+
+        sw.active.store(on, Ordering::Relaxed);
+        sw.bytes_written = 0;
+        sw.active_time = if on { Some(Instant::now()) } else { None };
+        sw.offset_time = None;
+
+        match &sw.kind {
+            VoiceOutKind::Mixed => {
+                let Some(bin) = sw
+                    .appsrc
+                    .parent()
+                    .and_then(|p| p.downcast::<gst::Bin>().ok())
+                else {
+                    eprintln!("Failed to get parent bin for voice");
+                    return;
+                };
+
+                set_playing(bin.upcast_ref(), on);
+            }
+            VoiceOutKind::Standalone(_bus_watch_guard) => {
+                set_playing(sw.pipeline.upcast_ref(), on);
+            }
+        }
+    }
+
+    fn set_active_in(&self, sw: &mut Self::VoiceIn, on: bool) {
+        sw.active = on;
+        set_playing(sw.pipeline.upcast_ref(), on);
+    }
+
+    fn set_volume_out(&self, sw: &mut Self::VoiceOut, vol: &bindings::Volume) {
+        let volume = qemu_volume_to_gst(vol);
+
+        if let Some(ref stream_volume) = sw.stream_volume {
+            stream_volume.set_volume(gst_audio::StreamVolumeFormat::Linear, 
volume);
+            stream_volume.set_mute(vol.mute);
+        } else if let Some(sink) = find_sink_element(&sw.pipeline) {
+            set_element_volume(sink, volume, vol.mute);
+        } else {
+            eprintln!("Failed to find volume control in pipeline");
+        }
+    }
+
+    fn set_volume_in(&self, sw: &mut Self::VoiceIn, vol: &bindings::Volume) {
+        let volume = qemu_volume_to_gst(vol);
+
+        if let Some(stream_volume) = sw
+            .pipeline
+            .upcast_ref::<gst::Bin>()
+            .by_interface(gst_audio::StreamVolume::static_type())
+            .and_then(|elem| 
elem.dynamic_cast::<gst_audio::StreamVolume>().ok())
+        {
+            stream_volume.set_volume(gst_audio::StreamVolumeFormat::Linear, 
volume);
+            stream_volume.set_mute(vol.mute);
+        } else if let Some(source) = find_source_element(&sw.pipeline) {
+            set_element_volume(source, volume, vol.mute)
+        } else {
+            eprintln!("Failed to find volume control in input pipeline");
+        }
+    }
+
+    fn write(&self, sw: &mut Self::VoiceOut, buf: &[u8]) -> usize {
+        if buf.is_empty() || !sw.active.load(Ordering::Relaxed) {
+            return 0;
+        }
+
+        if let Some(timer) = sw.active_time {
+            trace::trace_gst_write(
+                sw.bytes_written,
+                timer.elapsed().as_millis() as u64,
+                sw.bytes_per_second,
+            )
+        }
+
+        let offset_time = sw
+            .offset_time
+            .get_or_insert_with(|| 
sw.pipeline.current_running_time().unwrap_or_default());
+
+        assert!(sw.bytes_per_second > 0);
+        let pts = gst::ClockTime::from_nseconds(
+            sw.bytes_written as u64 * gst::ClockTime::SECOND.nseconds()
+                / sw.bytes_per_second as u64
+                + offset_time.nseconds(),
+        );
+
+        let mut gst_buffer = match gst::Buffer::with_size(buf.len()) {
+            Ok(buffer) => buffer,
+            Err(e) => {
+                eprintln!("Failed to create GStreamer buffer: {e}");
+                return 0;
+            }
+        };
+
+        {
+            let Some(buffer_ref) = gst_buffer.get_mut() else {
+                eprintln!("Failed to get mutable reference to GStreamer 
buffer");
+                return 0;
+            };
+
+            let duration = gst::ClockTime::from_nseconds(
+                buf.len() as u64 * gst::ClockTime::SECOND.nseconds() / 
sw.bytes_per_second as u64,
+            );
+
+            buffer_ref.set_pts(pts);
+            buffer_ref.set_duration(duration);
+
+            let mut mapped_buffer = match buffer_ref.map_writable() {
+                Ok(mapped) => mapped,
+                Err(e) => {
+                    eprintln!("Failed to map GStreamer buffer for writing: 
{e}");
+                    return 0;
+                }
+            };
+            mapped_buffer.copy_from_slice(buf);
+        }
+
+        sw.bytes_written += buf.len();
+
+        match sw.appsrc.push_buffer(gst_buffer) {
+            Ok(_) => buf.len(),
+            Err(e) => {
+                eprintln!("Failed to push buffer to appsrc: {e}");
+                0
+            }
+        }
+    }
+
+    fn read(&self, sw: &mut Self::VoiceIn, buf: &mut [u8]) -> usize {
+        if buf.is_empty() || !sw.active {
+            return 0;
+        }
+
+        let mut total_read = 0;
+
+        while total_read < buf.len() {
+            // First, try to consume pending data from previous read
+            if let Ok(mut pending) = sw.pending_data.lock() {
+                if !pending.is_empty() {
+                    let bytes_to_copy = pending.len().min(buf.len() - 
total_read);
+                    buf[total_read..total_read + bytes_to_copy]
+                        .copy_from_slice(&pending[..bytes_to_copy]);
+                    pending.drain(..bytes_to_copy);
+                    total_read += bytes_to_copy;
+                    if total_read >= buf.len() {
+                        break;
+                    }
+                }
+            }
+
+            // Try to pull a new sample from the appsink
+            let sample = match 
sw.appsink.try_pull_sample(gst::ClockTime::ZERO) {
+                Some(sample) => sample,
+                None => break,
+            };
+
+            let buffer = match sample.buffer() {
+                Some(buffer) => buffer,
+                None => break,
+            };
+
+            let map = match buffer.map_readable() {
+                Ok(map) => map,
+                Err(e) => {
+                    eprintln!("Failed to map input buffer for reading: {e}");
+                    break;
+                }
+            };
+
+            let data = map.as_slice();
+            let needed = buf.len() - total_read;
+            let bytes_to_copy = data.len().min(needed);
+
+            buf[total_read..total_read + 
bytes_to_copy].copy_from_slice(&data[..bytes_to_copy]);
+            total_read += bytes_to_copy;
+
+            // If there's remaining data, save it for next read
+            if bytes_to_copy < data.len() {
+                if let Ok(mut pending) = sw.pending_data.lock() {
+                    pending.extend_from_slice(&data[bytes_to_copy..]);
+                }
+                break;
+            }
+        }
+
+        total_read
+    }
+
+    fn get_buffer_size_out(&self, sw: &mut Self::VoiceOut) -> i32 {
+        // Maybe it should query the pipeline for latency instead
+        // Tbh, it looks like this API is not very useful in QEMU
+        sw.appsrc.current_level_bytes() as i32
+    }
+
+    fn add_capture(
+        &self,
+        settings: &bindings::audsettings,
+        ops: Box<dyn crate::audio::AudioCaptureOps + Send>,
+    ) -> Option<Self::Capture> {
+        match self.try_add_capture(settings, ops) {
+            Ok(capture) => Some(capture),
+            Err(e) => {
+                eprintln!("Failed to add capture: {e}");
+                None
+            }
+        }
+    }
+
+    fn del_capture(&self, cap: Self::Capture) {
+        if let Ok(mut ops_guard) = cap.ops.lock() {
+            ops_guard.destroy();
+        }
+
+        if let Some(ghost_pad) = cap.bin.static_pad("sink") {
+            let _ = cap.tee_pad.unlink(&ghost_pad);
+        }
+
+        if let Some(tee) = cap.tee_pad.parent_element() {
+            tee.release_request_pad(&cap.tee_pad);
+        }
+
+        if let Err(e) = cap.bin.set_state(gst::State::Null) {
+            eprintln!("Warning: Failed to set capture bin to null state: {e}");
+        }
+
+        if let Some(pipeline) = cap
+            .bin
+            .parent()
+            .and_then(|p| p.downcast::<gst::Pipeline>().ok())
+        {
+            if let Err(e) = pipeline.remove(&cap.bin) {
+                eprintln!("Warning: Failed to remove capture bin from 
pipeline: {e}");
+            }
+        }
+    }
+
+    fn set_dbus_server(
+        &self,
+        _manager: *mut gio_sys::GDBusObjectManagerServer,
+        _p2p: bool,
+    ) -> Result<()> {
+        Err(Error::msg("DBus not supported by this backend"))
+    }
+}
+
+impl AudioGStreamer {
+    fn try_add_capture(
+        &self,
+        settings: &bindings::audsettings,
+        ops: Box<dyn crate::audio::AudioCaptureOps + Send>,
+    ) -> Result<Capture> {
+        let mixer_pipeline_ref = self.mixer_pipeline.borrow();
+        let mixer_pipeline = mixer_pipeline_ref
+            .as_ref()
+            .ok_or_else(|| Error::msg("Capture is only supported when 
mixing-engine is enabled"))?;
+
+        let caps = build_audio_caps(settings)?;
+        let appsink = 
gst_app::AppSink::builder().caps(&caps).sync(false).build();
+
+        let ops = Arc::new(Mutex::new(ops));
+        let ops_clone = ops.clone();
+        // Set up callback to pull samples from appsink
+        appsink.set_callbacks(
+            gst_app::AppSinkCallbacks::builder()
+                .new_sample(move |sink| {
+                    let sample = sink.pull_sample().map_err(|_| 
gst::FlowError::Eos)?;
+                    let buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
+
+                    let map = buffer.map_readable().map_err(|_| 
gst::FlowError::Error)?;
+                    let data = map.as_slice();
+
+                    trace::trace_gst_new_sample(c"capture", data.len());
+                    // Forward captured audio to ops
+                    if let Ok(mut ops_guard) = ops_clone.lock() {
+                        
ops_guard.notify(CaptureNotification::AUD_CNOTIFY_ENABLE);
+                        ops_guard.capture(data);
+                    }
+
+                    Ok(gst::FlowSuccess::Ok)
+                })
+                .build(),
+        );
+
+        let bin = gst::Bin::new();
+
+        let queue = create_element("queue")?;
+        let audioconvert = create_element("audioconvert")?;
+        let audioresample = create_element("audioresample")?;
+
+        bin.add_many([&queue, &audioconvert, &audioresample, 
appsink.upcast_ref()])?;
+
+        gst::Element::link_many([&queue, &audioconvert, &audioresample, 
appsink.upcast_ref()])?;
+
+        let queue_sink_pad = queue
+            .static_pad("sink")
+            .ok_or_else(|| Error::msg("Failed to get queue sink pad"))?;
+        let ghost_pad = gst::GhostPad::with_target(&queue_sink_pad)?;
+        ghost_pad.set_active(true)?;
+        bin.add_pad(&ghost_pad)?;
+
+        mixer_pipeline.pipeline.add(&bin)?;
+
+        let tee_pad = mixer_pipeline
+            .tee
+            .request_pad_simple("src_%u")
+            .ok_or_else(|| Error::msg("Failed to request pad from tee"))?;
+
+        // Link tee → bin (ghost pad)
+        tee_pad.link(&ghost_pad)?;
+
+        bin.sync_state_with_parent()?;
+
+        Ok(Capture { bin, tee_pad, ops })
+    }
+
+    unsafe fn init(mut this: ParentInit<Self>) {
+        uninit_field_mut!(*this, 
id).write(BqlCell::new(CString::new("invalid-id").unwrap()));
+        uninit_field_mut!(*this, opt).write(BqlRefCell::new(None));
+        uninit_field_mut!(*this, mixer_pipeline).write(BqlRefCell::new(None));
+    }
+}
+
+fn set_playing(elem: &gst::Element, on: bool) {
+    let target_state = if on {
+        gst::State::Playing
+    } else {
+        gst::State::Null
+    };
+
+    if let Err(e) = elem.set_state(target_state) {
+        eprintln!("Failed to set element state to {target_state:?}: {e}");
+    }
+}
+
+/// Build `GStreamer` caps for the given audio settings
+fn build_audio_caps(settings: &bindings::audsettings) -> Result<gst::Caps> {
+    let format = audio_format_to_gst_format(settings.fmt)?;
+
+    Ok(gst_audio::AudioCapsBuilder::new_interleaved()
+        .format(format)
+        .rate(settings.freq)
+        .channels(settings.nchannels)
+        .build())
+}
+
+/// Find the sink element in the pipeline
+fn find_sink_element(pipeline: &gst::Pipeline) -> Option<gst::Element> {
+    pipeline.iterate_sinks().into_iter().flatten().next()
+}
+
+/// Find the source element in the pipeline
+fn find_source_element(pipeline: &gst::Pipeline) -> Option<gst::Element> {
+    pipeline.iterate_sources().into_iter().flatten().next()
+}
+
+fn set_element_volume(elem: gst::Element, volume: f64, mute: bool) {
+    if elem.find_property("volume").is_some() {
+        elem.set_property("volume", volume);
+    }
+    if elem.find_property("mute").is_some() {
+        elem.set_property("mute", mute);
+    }
+}
+
+/// Convert QEMU volume (0-255 per channel) to `GStreamer` volume (0.0-1.0)
+/// Returns the average volume across all channels
+fn qemu_volume_to_gst(vol: &bindings::Volume) -> f64 {
+    let channels = vol.channels.min(bindings::AUDIO_MAX_CHANNELS as i32) as 
usize;
+
+    if channels == 0 {
+        return 0.0;
+    }
+
+    let total: u32 = vol.vol[..channels].iter().map(|&v| u32::from(v)).sum();
+
+    f64::from(total) / (channels as f64 * 255.0)
+}
+
+/// Build caps from `AudiodevPerDirectionOptions` for fixed settings
+fn build_caps_from_options(opt: &AudiodevPerDirectionOptions) -> 
Result<gst::Caps> {
+    let format = opt.format.map(audio_format_to_gst_format).transpose()?;
+    let rate = opt.frequency.map(|f| f as i32);
+    let channels = opt.channels.map(|c| c as i32);
+
+    Ok(gst_audio::AudioCapsBuilder::new_interleaved()
+        .format_if_some(format)
+        .rate_if_some(rate)
+        .channels_if_some(channels)
+        .build())
+}
+
+/// Create a shared mixer pipeline for mixing multiple audio voices
+fn create_mixer_pipeline(opt: &AudiodevGStreamerOptions) -> 
Result<MixerPipeline> {
+    let pipeline = gst::Pipeline::new();
+
+    let mixer = gst::ElementFactory::make("liveadder")
+        .property("ignore-inactive-pads", true)
+        .property("force-live", true)
+        .build()?;
+
+    let audioconvert = create_element("audioconvert")?;
+    let audioresample = create_element("audioresample")?;
+    let tee = create_element("tee")?;
+    let queue = create_element("queue")?;
+    let buffer_time_us = opt.out.as_ref().and_then(|o| o.buffer_length);
+    let audiosink = create_audiosink(&opt.sink, buffer_time_us)?;
+
+    let capsfilter = if opt
+        .out
+        .as_ref()
+        .and_then(|o| o.fixed_settings)
+        .unwrap_or(false)
+    {
+        let cf = create_element("capsfilter")?;
+        let caps = build_caps_from_options(opt.out.as_ref().unwrap())?;
+        cf.set_property("caps", &caps);
+        Some(cf)
+    } else {
+        None
+    };
+
+    let mut elements: Vec<&gst::Element> =
+        vec![&mixer, &audioconvert, &audioresample, &tee, &queue];
+    if let Some(ref cf) = capsfilter {
+        elements.push(cf);
+    }
+    elements.push(&audiosink);
+
+    pipeline.add_many(&elements)?;
+
+    gst::Element::link_many([&mixer, &audioconvert, &audioresample, &tee])?;
+
+    if let Some(ref cf) = capsfilter {
+        gst::Element::link_many([&tee, &queue, cf, &audiosink])?;
+    } else {
+        gst::Element::link_many([&tee, &queue, &audiosink])?;
+    }
+
+    let bus_watch = add_bus_watch(&pipeline)?;
+    set_playing(pipeline.upcast_ref(), true);
+
+    Ok(MixerPipeline {
+        pipeline,
+        mixer,
+        tee,
+        bus_watch,
+    })
+}
+
+/// Create a voice output connected to the shared mixer
+fn create_mixed_voice(
+    mixer_pipeline: &MixerPipeline,
+    settings: &bindings::audsettings,
+    name: &CStr,
+) -> Result<VoiceOut> {
+    // Create a separate bin for this voice
+    let bin = gst::Bin::new();
+
+    let appsrc = create_appsrc(settings, name)?;
+    let volume = create_element("volume")?;
+    let stream_volume = volume
+        .clone()
+        .dynamic_cast::<gst_audio::StreamVolume>()
+        .ok();
+    let identity = create_element("identity")?;
+    identity.set_property("sync", true);
+    let audioconvert = create_element("audioconvert")?;
+    let audioresample = create_element("audioresample")?;
+
+    bin.add_many([
+        appsrc.upcast_ref(),
+        &identity,
+        &volume,
+        &audioconvert,
+        &audioresample,
+    ])?;
+    gst::Element::link_many([
+        appsrc.upcast_ref(),
+        &identity,
+        &volume,
+        &audioconvert,
+        &audioresample,
+    ])?;
+
+    let src_pad = audioresample
+        .static_pad("src")
+        .ok_or_else(|| Error::msg("Failed to get source pad"))?;
+
+    let ghost_pad = gst::GhostPad::with_target(&src_pad)?;
+
+    ghost_pad.set_active(true)?;
+    bin.add_pad(&ghost_pad)?;
+
+    mixer_pipeline
+        .pipeline
+        .add(bin.upcast_ref::<gst::Element>())?;
+
+    bin.link(&mixer_pipeline.mixer)?;
+
+    Ok(VoiceOut {
+        kind: VoiceOutKind::Mixed,
+        pipeline: mixer_pipeline.pipeline.clone(),
+        stream_volume,
+        appsrc,
+        active: Arc::new(AtomicBool::new(false)),
+        active_time: None,
+        bytes_per_second: get_bytes_per_second(settings)?,
+        bytes_written: 0,
+        offset_time: None,
+    })
+}
+
+/// Create a standalone voice output without mixer or capture
+fn create_standalone_voice(
+    settings: &bindings::audsettings,
+    name: &CStr,
+    opt: &AudiodevGStreamerOptions,
+) -> Result<VoiceOut> {
+    let pipeline = gst::Pipeline::new();
+
+    let appsrc = create_appsrc(settings, name)?;
+    let identity = create_element("identity")?;
+    identity.set_property("sync", true);
+    let audioconvert = create_element("audioconvert")?;
+    let audioresample = create_element("audioresample")?;
+    let queue = create_element("queue")?;
+
+    let buffer_time_us = opt.out.as_ref().and_then(|o| o.buffer_length);
+    let audiosink = create_audiosink(&opt.sink, buffer_time_us)?;
+    let stream_volume = audiosink
+        .clone()
+        .dynamic_cast::<gst_audio::StreamVolume>()
+        .ok();
+
+    pipeline.add_many([
+        appsrc.upcast_ref(),
+        &identity,
+        &audioconvert,
+        &audioresample,
+        &queue,
+        &audiosink,
+    ])?;
+
+    gst::Element::link_many([
+        appsrc.upcast_ref(),
+        &identity,
+        &audioconvert,
+        &audioresample,
+        &queue,
+        &audiosink,
+    ])?;
+
+    let bus_watch = add_bus_watch(&pipeline)?;
+
+    Ok(VoiceOut {
+        kind: VoiceOutKind::Standalone(bus_watch),
+        pipeline,
+        stream_volume,
+        appsrc,
+        active: Arc::new(AtomicBool::new(false)),
+        active_time: None,
+        bytes_per_second: get_bytes_per_second(settings)?,
+        bytes_written: 0,
+        offset_time: None,
+    })
+}
+
+fn add_bus_watch(pipeline: &gst::Pipeline) -> Result<gst::bus::BusWatchGuard> {
+    // Add bus message handler to prevent queue overflow
+    let bus = pipeline
+        .bus()
+        .ok_or_else(|| Error::msg("Failed to get pipeline bus"))?;
+    let bus_watch = bus.add_watch(|_bus, msg| {
+        use gst::MessageView;
+        match msg.view() {
+            MessageView::Error(err) => {
+                eprintln!(
+                    "GStreamer error from {:?}: {} ({:?})",
+                    err.src().map(gst::prelude::GstObjectExt::path_string),
+                    err.error(),
+                    err.debug()
+                );
+            }
+            MessageView::Warning(warning) => {
+                eprintln!(
+                    "GStreamer warning from {:?}: {} ({:?})",
+                    warning.src().map(gst::prelude::GstObjectExt::path_string),
+                    warning.error(),
+                    warning.debug()
+                );
+            }
+            _ => {}
+        }
+        glib::ControlFlow::Continue
+    })?;
+
+    Ok(bus_watch)
+}
+
+/// Create an input voice for audio recording
+fn create_input_voice(
+    opt: &AudiodevGStreamerOptions,
+    settings: &bindings::audsettings,
+) -> Result<VoiceIn> {
+    let source_name = opt.source.as_deref().unwrap_or("autoaudiosrc");
+    let pipeline = gst::Pipeline::new();
+
+    let audiosrc = create_element(source_name)?;
+    let audioconvert = create_element("audioconvert")?;
+    let audioresample = create_element("audioresample")?;
+    let caps = build_audio_caps(settings)?;
+    let appsink = gst_app::AppSink::builder()
+        .caps(&caps)
+        .sync(false)
+        .max_time(Some(gst::ClockTime::from_mseconds(10)))
+        .build();
+
+    pipeline.add_many([
+        &audiosrc,
+        &audioconvert,
+        &audioresample,
+        appsink.upcast_ref(),
+    ])?;
+
+    gst::Element::link_many([
+        &audiosrc,
+        &audioconvert,
+        &audioresample,
+        appsink.upcast_ref(),
+    ])?;
+
+    Ok(VoiceIn {
+        pipeline,
+        appsink,
+        active: false,
+        pending_data: Arc::new(Mutex::new(Vec::new())),
+    })
+}
+
+fn create_audiosink(
+    elem_name: &Option<String>,
+    buffer_time_us: Option<u32>,
+) -> Result<gst::Element> {
+    const DEFAULT_BUFFER_TIME_US: u32 = 40_000;
+
+    let elem_name = elem_name.as_deref().unwrap_or("autoaudiosink");
+    let audiosink = create_element(elem_name)?;
+    let buffer_time_us: i64 = 
buffer_time_us.unwrap_or(DEFAULT_BUFFER_TIME_US).into();
+
+    if let Some(child_proxy) = audiosink.dynamic_cast_ref::<gst::ChildProxy>() 
{
+        child_proxy.connect_child_added(move |_proxy, child, _child_name| {
+            if let Some(elem) = child.downcast_ref::<gst::Element>() {
+                if elem.has_property("buffer-time") {
+                    elem.set_property("buffer-time", buffer_time_us);
+                } else {
+                    eprintln!("no buffer-time property on the sink");
+                }
+            }
+        });
+    }
+
+    Ok(audiosink)
+}
+
+fn create_appsrc(settings: &bindings::audsettings, name: &CStr) -> 
Result<gst_app::AppSrc> {
+    let caps = build_audio_caps(settings)?;
+
+    let appsrc = gst_app::AppSrc::builder()
+        .caps(&caps)
+        .is_live(true)
+        .format(gst::Format::Time)
+        // we don't want to block QEMU
+        .block(false)
+        .build();
+    // create a stream-id
+    appsrc.set_uri(&format!("appsrc://{}", name.to_string_lossy()))?;
+    Ok(appsrc)
+}
+
+/// Create a `GStreamer` element with error reporting
+fn create_element(factory_name: &str) -> Result<gst::Element> {
+    gst::ElementFactory::make(factory_name)
+        .build()
+        .map_err(|e| Error::msg(format!("Failed to create element 
'{factory_name}': {e}")))
+}
+
+fn get_bytes_per_second(settings: &bindings::audsettings) -> Result<usize> {
+    let bytes_per_sample = get_bytes_per_sample(settings.fmt)?;
+
+    Ok(settings.freq as usize * settings.nchannels as usize * bytes_per_sample 
as usize)
+}
+
+fn get_bytes_per_sample(fmt: bindings::AudioFormat) -> Result<usize> {
+    use bindings::AudioFormat::*;
+    match fmt {
+        AUDIO_FORMAT_U8 | AUDIO_FORMAT_S8 => Ok(1),
+        AUDIO_FORMAT_U16 | AUDIO_FORMAT_S16 => Ok(2),
+        AUDIO_FORMAT_U32 | AUDIO_FORMAT_S32 | AUDIO_FORMAT_F32 => Ok(4),
+        _ => Err(Error::msg(format!("Unsupported audio format: {fmt:?}"))),
+    }
+}
+
+fn audio_format_to_gst_format(fmt: bindings::AudioFormat) -> 
Result<gst_audio::AudioFormat> {
+    use bindings::AudioFormat::*;
+    use gst_audio::AudioFormat as GAF;
+
+    match fmt {
+        AUDIO_FORMAT_U8 => Ok(GAF::U8),
+        AUDIO_FORMAT_S8 => Ok(GAF::S8),
+        AUDIO_FORMAT_U16 => Ok(GAF::U16le),
+        AUDIO_FORMAT_S16 => Ok(GAF::S16le),
+        AUDIO_FORMAT_U32 => Ok(GAF::U32le),
+        AUDIO_FORMAT_S32 => Ok(GAF::S32le),
+        AUDIO_FORMAT_F32 => Ok(GAF::F32le),
+        _ => Err(Error::msg(format!("Unsupported audio format: {fmt:?}"))),
+    }
+}
diff --git a/rust/audio/src/lib.rs b/rust/audio/src/lib.rs
new file mode 100644
index 0000000000..3e899e2439
--- /dev/null
+++ b/rust/audio/src/lib.rs
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+pub mod bindings;
+
+mod audio;
+use std::ffi::CStr;
+
+pub use audio::*;
+
+mod gstreamer;
+
+pub const TYPE_AUDIO_GSTREAMER: &std::ffi::CStr = c"audio-gstreamer";
+
+#[derive(Default, Debug, PartialEq, Eq)]
+pub struct AudiodevGStreamerOptions {
+    pub in_: Option<AudiodevPerDirectionOptions>,
+    pub out: Option<AudiodevPerDirectionOptions>,
+    pub sink: Option<String>,
+    pub source: Option<String>,
+}
+
+#[derive(Default, Debug, PartialEq, Eq)]
+pub struct AudiodevPerDirectionOptions {
+    pub mixing_engine: Option<bool>,
+    pub fixed_settings: Option<bool>,
+    pub frequency: Option<u32>,
+    pub channels: Option<u32>,
+    pub voices: Option<u32>,
+    pub format: Option<bindings::AudioFormat>,
+    pub buffer_length: Option<u32>,
+}
+
+impl From<&bindings::AudiodevGStreamerOptions> for AudiodevGStreamerOptions {
+    fn from(value: &bindings::AudiodevGStreamerOptions) -> Self {
+        let in_ = (!value.in_.is_null())
+            .then(|| AudiodevPerDirectionOptions::from(unsafe { &*value.in_ 
}));
+        let out = (!value.out.is_null())
+            .then(|| AudiodevPerDirectionOptions::from(unsafe { &*value.out 
}));
+        let sink = (!value.sink.is_null()).then(|| {
+            unsafe { CStr::from_ptr(value.sink) }
+                .to_string_lossy()
+                .into_owned()
+        });
+        let source = (!value.source.is_null()).then(|| {
+            unsafe { CStr::from_ptr(value.source) }
+                .to_string_lossy()
+                .into_owned()
+        });
+
+        Self {
+            in_,
+            out,
+            sink,
+            source,
+        }
+    }
+}
+
+impl From<&bindings::AudiodevPerDirectionOptions> for 
AudiodevPerDirectionOptions {
+    fn from(value: &bindings::AudiodevPerDirectionOptions) -> Self {
+        Self {
+            mixing_engine: if value.has_mixing_engine {
+                Some(value.mixing_engine)
+            } else {
+                None
+            },
+            fixed_settings: if value.has_fixed_settings {
+                Some(value.fixed_settings)
+            } else {
+                None
+            },
+            frequency: if value.has_frequency {
+                Some(value.frequency)
+            } else {
+                None
+            },
+            channels: if value.has_channels {
+                Some(value.channels)
+            } else {
+                None
+            },
+            voices: if value.has_voices {
+                Some(value.voices)
+            } else {
+                None
+            },
+            format: if value.has_format {
+                Some(value.format)
+            } else {
+                None
+            },
+            buffer_length: if value.has_buffer_length {
+                Some(value.buffer_length)
+            } else {
+                None
+            },
+        }
+    }
+}
diff --git a/rust/meson.build b/rust/meson.build
index 9f0ed48481..8c2905a7f0 100644
--- a/rust/meson.build
+++ b/rust/meson.build
@@ -13,6 +13,11 @@ syn_rs_native = cargo_ws.subproject('syn').dependency()
 proc_macro2_rs_native = cargo_ws.subproject('proc-macro2').dependency()
 attrs_rs_native = cargo_ws.subproject('attrs').dependency()
 
+futures_rs = cargo_ws.subproject('futures').dependency()
+gio_sys_rs = cargo_ws.subproject('gio-sys').dependency()
+gst_app_rs = cargo_ws.subproject('gstreamer-app').dependency()
+gst_audio_rs = cargo_ws.subproject('gstreamer-audio').dependency()
+
 genrs = []
 
 subdir('qemu-macros')
@@ -29,6 +34,7 @@ subdir('hw/core')
 subdir('tests')
 subdir('trace')
 subdir('hw')
+subdir('audio')
 
 cargo = find_program('cargo', required: false)
 
-- 
2.51.1


Reply via email to