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
