diff --git a/Cargo.toml b/Cargo.toml index 2c3a24ff0..2a3ff69c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,8 +71,12 @@ opt-level = "z" # Node-API hosts that export napi_* from a DLL rather than the executable (NativeScript Windows) # need the patched symbol lookup (the `[canvas patch]` hunks in crates/vendor/napi-sys/src/functions.rs). +# WASAPI output in low-latency shared mode (IAudioClient3), so an `interactive` AudioContext on +# Windows gets a ~5 ms engine period instead of 10 ms plus a full ring buffer (the `[canvas patch]` +# hunks in crates/vendor/cpal/src/host/wasapi/device.rs). [patch.crates-io] napi-sys = { path = "crates/vendor/napi-sys" } +cpal = { path = "crates/vendor/cpal" } [workspace.dependencies.wgt] package = "wgpu-types" diff --git a/crates/vendor/cpal/Cargo.toml b/crates/vendor/cpal/Cargo.toml new file mode 100644 index 000000000..2256c0d06 --- /dev/null +++ b/crates/vendor/cpal/Cargo.toml @@ -0,0 +1,342 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.85" +name = "cpal" +version = "0.18.2" +build = "build.rs" +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Low-level cross-platform audio I/O library." +documentation = "https://docs.rs/cpal" +readme = "README.md" +keywords = [ + "audio", + "sound", + "playback", + "recording", + "cross-platform", +] +categories = ["multimedia::audio"] +license = "Apache-2.0" +repository = "https://github.com/RustAudio/cpal" + +[package.metadata.docs.rs] +features = [ + "asio", + "audioworklet", + "custom", + "jack", + "pulseaudio", + "realtime", + "realtime-dbus", + "wasm-bindgen", +] +rustdoc-args = [ + "--cfg", + "docsrs", +] +targets = [ + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "aarch64-apple-ios", + "wasm32-unknown-unknown", + "aarch64-linux-android", + "x86_64-unknown-freebsd", + "x86_64-unknown-netbsd", + "x86_64-unknown-dragonfly", +] + +[badges.maintenance] +status = "actively-developed" + +[features] +asio = [ + "dep:asio-sys", + "dep:num-traits", +] +audioworklet = [ + "dep:futures-channel", + "dep:futures-util", + "wasm-bindgen", + "web-sys/Blob", + "web-sys/BlobPropertyBag", + "web-sys/Url", + "web-sys/AudioWorklet", + "web-sys/AudioWorkletNode", + "web-sys/AudioWorkletNodeOptions", +] +custom = [] +default = [] +jack = ["dep:jack"] +pipewire = ["dep:pipewire"] +pulseaudio = [ + "dep:pulseaudio", + "dep:futures", + "dep:portable-atomic", +] +realtime = [ + "dep:audio_thread_priority", + "dep:alsa-sys", +] +realtime-dbus = [ + "realtime", + "audio_thread_priority/with_dbus", +] +wasm-bindgen = [ + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures", + "dep:futures-channel", + "dep:futures-util", +] + +[lib] +name = "cpal" +path = "src/lib.rs" + +[dependencies.dasp_sample] +version = "0.11" + +[dev-dependencies.anyhow] +version = "1.0" + +[dev-dependencies.clap] +version = ">=4.6, <5" +features = ["derive"] + +[dev-dependencies.hound] +version = "3.5" + +[dev-dependencies.ringbuf] +version = "0.4" + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies.futures-channel] +version = "0.3" +optional = true + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies.futures-util] +version = "0.3" +features = ["alloc"] +optional = true +default-features = false + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies.js-sys] +version = "0.3" + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies.wasm-bindgen] +version = "0.2" +optional = true + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies.wasm-bindgen-futures] +version = "0.4" +optional = true + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies.web-sys] +version = "0.3" +features = [ + "AudioContext", + "AudioContextOptions", + "AudioBuffer", + "AudioBufferSourceNode", + "AudioNode", + "AudioDestinationNode", + "Window", + "AudioContextState", +] + +[target.'cfg(any(target_os = "ios", target_os = "tvos", target_os = "visionos"))'.dependencies.block2] +version = "0.6" + +[target.'cfg(any(target_os = "ios", target_os = "tvos", target_os = "visionos"))'.dependencies.objc2-avf-audio] +version = "0.3" +features = [ + "std", + "AVAudioSession", + "AVAudioSessionTypes", +] +default-features = false + +[target.'cfg(any(target_os = "ios", target_os = "tvos", target_os = "visionos"))'.dependencies.objc2-foundation] +version = "0.3" +features = [ + "block2", + "NSDictionary", + "NSNotification", + "NSOperation", +] + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies.alsa] +version = "0.11" + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies.alsa-sys] +version = "0.4" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies.audio_thread_priority] +version = "0.35" +optional = true +default-features = false + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies.futures] +version = "0.3" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies.jack] +version = "0.13.5" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies.libc] +version = "0.2" + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies.pipewire] +version = "0.10" +features = ["v0_3_53"] +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies.pulseaudio] +version = "0.3" +optional = true + +[target.'cfg(not(target_has_atomic = "64"))'.dependencies.portable-atomic] +version = "1" +optional = true + +[target.'cfg(target_os = "android")'.dependencies.audio_thread_priority] +version = "0.35" +optional = true +default-features = false + +[target.'cfg(target_os = "android")'.dependencies.jni] +version = "0.22" + +[target.'cfg(target_os = "android")'.dependencies.libc] +version = "0.2" + +[target.'cfg(target_os = "android")'.dependencies.ndk] +version = "0.9" +features = [ + "audio", + "api-level-26", +] +default-features = false + +[target.'cfg(target_os = "android")'.dependencies.ndk-context] +version = "0.1" + +[target.'cfg(target_os = "android")'.dependencies.num-derive] +version = "0.4" + +[target.'cfg(target_os = "android")'.dependencies.num-traits] +version = "0.2" + +[target.'cfg(target_os = "macos")'.dependencies.jack] +version = "0.13.5" +optional = true + +[target.'cfg(target_os = "windows")'.dependencies.asio-sys] +version = "0.4.0" +optional = true + +[target.'cfg(target_os = "windows")'.dependencies.audio_thread_priority] +version = "0.35" +optional = true +default-features = false + +[target.'cfg(target_os = "windows")'.dependencies.jack] +version = "0.13.5" +optional = true + +[target.'cfg(target_os = "windows")'.dependencies.num-traits] +version = "0.2" +optional = true + +[target.'cfg(target_os = "windows")'.dependencies.windows] +version = "0.62" +features = [ + "Win32_Media", + "Win32_Media_Audio", + "Win32_Foundation", + "Win32_Devices_Properties", + "Win32_Media_KernelStreaming", + "Win32_System_Com_StructuredStorage", + "Win32_System_Threading", + "Win32_System_Performance", + "Win32_Security", + "Win32_System_SystemServices", + "Win32_System_Variant", + "Win32_Media_Multimedia", + "Win32_UI_Shell_PropertiesSystem", +] + +[target.'cfg(target_os = "windows")'.dependencies.windows-core] +version = "0.62" + +[target.'cfg(target_vendor = "apple")'.dependencies.coreaudio-rs] +version = "0.14.2" +features = [ + "core_audio", + "audio_toolbox", +] +default-features = false + +[target.'cfg(target_vendor = "apple")'.dependencies.mach2] +version = "0.6" + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2] +version = "0.6" + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2-audio-toolbox] +version = "0.3" +features = [ + "std", + "AUComponent", + "AudioUnitProperties", +] +default-features = false + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2-core-audio] +version = "0.3" +features = [ + "std", + "AudioHardware", + "AudioHardwareDeprecated", + "objc2", + "objc2-foundation", +] +default-features = false + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2-core-audio-types] +version = "0.3" +features = [ + "std", + "CoreAudioBaseTypes", +] +default-features = false + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2-core-foundation] +version = "0.3.1" + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2-foundation] +version = "0.3" +features = [ + "std", + "NSArray", + "NSString", + "NSValue", +] +default-features = false diff --git a/crates/vendor/cpal/Cargo.toml.orig b/crates/vendor/cpal/Cargo.toml.orig new file mode 100644 index 000000000..93e7dd176 --- /dev/null +++ b/crates/vendor/cpal/Cargo.toml.orig @@ -0,0 +1,259 @@ +[package] +name = "cpal" +version = "0.18.2" +description = "Low-level cross-platform audio I/O library." +repository = "https://github.com/RustAudio/cpal" +documentation = "https://docs.rs/cpal" +license = "Apache-2.0" +keywords = ["audio", "sound", "playback", "recording", "cross-platform"] +categories = ["multimedia::audio"] +edition = "2021" +rust-version = "1.85" + +[badges] +maintenance = { status = "actively-developed" } + +[features] +default = [] + +# Real-time audio thread scheduling +# Applies platform-specific real-time scheduling and performance modes to audio threads. +# Platform: Android, Windows (see `realtime-dbus` for Linux) +realtime = ["dep:audio_thread_priority", "dep:alsa-sys"] + +# Enables `realtime` and adds D-Bus/rtkit support, for RT scheduling on Linux desktop systems +# Requires: D-Bus development libraries (libdbus-1-dev or equivalent) +# Platform: Linux +realtime-dbus = ["realtime", "audio_thread_priority/with_dbus"] + +# ASIO backend for Windows +# Provides low-latency audio I/O by bypassing the Windows audio stack +# Requires: ASIO drivers and LLVM/Clang for build-time bindings +# See README for detailed setup instructions +asio = [ + "dep:asio-sys", + "dep:num-traits", +] + +# Audio Worklet backend for WebAssembly +# Provides lower-latency web audio processing compared to default Web Audio API +# Requires: Build with atomics support and Cross-Origin headers for SharedArrayBuffer +# Platform: WebAssembly (wasm32-unknown-unknown) +audioworklet = [ + "dep:futures-channel", + "dep:futures-util", + "wasm-bindgen", + "web-sys/Blob", + "web-sys/BlobPropertyBag", + "web-sys/Url", + "web-sys/AudioWorklet", + "web-sys/AudioWorkletNode", + "web-sys/AudioWorkletNodeOptions", +] + +# Support for user-defined custom hosts, devices, and streams +# See examples/custom.rs for usage +# Platform: All platforms +custom = [] + +# JACK Audio Connection Kit backend +# Provides low-latency connections between applications and audio hardware +# Requires: JACK server and client libraries installed on the system +# Platform: Linux, DragonFly BSD, FreeBSD, NetBSD, macOS, Windows +# Note: JACK must be installed separately on all platforms +jack = ["dep:jack"] + +# PipeWire backend +# Provides audio I/O on Linux and some BSDs via the PipeWire multimedia server +# Requires: PipeWire server and client libraries installed on the system +# Platform: Linux, DragonFly BSD, FreeBSD, NetBSD +pipewire = ["dep:pipewire"] + +# PulseAudio backend +# Provides audio I/O support on Linux and some BSDs using the PulseAudio sound server +# Requires: PulseAudio server and client libraries installed on the system +# Platform: Linux, DragonFly BSD, FreeBSD, NetBSD +pulseaudio = ["dep:pulseaudio", "dep:futures", "dep:portable-atomic"] + +# WebAssembly backend using wasm-bindgen +# Enables the Web Audio API backend for browser-based audio +# Required for any WebAssembly audio support +# Platform: WebAssembly (wasm32-unknown-unknown) +# Note: This is typically enabled automatically when targeting wasm32 +wasm-bindgen = [ + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures", + "dep:futures-channel", + "dep:futures-util", +] + +[dependencies] +dasp_sample = "0.11" + +# Polyfills 64-bit atomics on targets without native support (e.g. mips, powerpc, +# riscv32); used by the pulseaudio backend's lock-free latency tracking. +[target.'cfg(not(target_has_atomic = "64"))'.dependencies] +portable-atomic = { version = "1", optional = true } + +[dev-dependencies] +anyhow = "1.0" +hound = "3.5" +ringbuf = "0.4" +clap = { version = ">=4.6, <5", features = ["derive"] } + +# `windows` and `windows-core` must be the same minor version. +[target.'cfg(target_os = "windows")'.dependencies] +windows-core = "0.62" +windows = { version = "0.62", features = [ + "Win32_Media", + "Win32_Media_Audio", + "Win32_Foundation", + "Win32_Devices_Properties", + "Win32_Media_KernelStreaming", + "Win32_System_Com_StructuredStorage", + "Win32_System_Threading", + "Win32_System_Performance", + "Win32_Security", + "Win32_System_SystemServices", + "Win32_System_Variant", + "Win32_Media_Multimedia", + "Win32_UI_Shell_PropertiesSystem", +] } +audio_thread_priority = { version = "0.35", optional = true, default-features = false } +asio-sys = { version = "0.4.0", path = "asio-sys", optional = true } +num-traits = { version = "0.2", optional = true } +jack = { version = "0.13.5", optional = true } + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] +alsa = "0.11" +alsa-sys = { version = "0.4", optional = true } +libc = "0.2" +audio_thread_priority = { version = "0.35", optional = true, default-features = false } +jack = { version = "0.13.5", optional = true } +pulseaudio = { version = "0.3", optional = true } +futures = { version = "0.3", optional = true } +pipewire = { version = "0.10", optional = true, features = ["v0_3_53"] } + +[target.'cfg(target_vendor = "apple")'.dependencies] +mach2 = "0.6" +coreaudio-rs = { version = "0.14.2", default-features = false, features = [ + "core_audio", + "audio_toolbox", +] } +objc2-core-audio = { version = "0.3", default-features = false, features = [ + "std", + "AudioHardware", + "AudioHardwareDeprecated", + "objc2", + "objc2-foundation", +] } +objc2-audio-toolbox = { version = "0.3", default-features = false, features = [ + "std", + "AUComponent", + "AudioUnitProperties", +] } +objc2-core-audio-types = { version = "0.3", default-features = false, features = [ + "std", + "CoreAudioBaseTypes", +] } +objc2-core-foundation = { version = "0.3.1" } +objc2-foundation = { version = "0.3", default-features = false, features = [ + "std", + "NSArray", + "NSString", + "NSValue", +] } +objc2 = { version = "0.6" } + +[target.'cfg(target_os = "macos")'.dependencies] +jack = { version = "0.13.5", optional = true } + +[target.'cfg(any(target_os = "ios", target_os = "tvos", target_os = "visionos"))'.dependencies] +block2 = "0.6" +objc2-foundation = { version = "0.3", features = [ + "block2", + "NSDictionary", + "NSNotification", + "NSOperation", +] } +objc2-avf-audio = { version = "0.3", default-features = false, features = [ + "std", + "AVAudioSession", + "AVAudioSessionTypes", +] } + +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +wasm-bindgen = { version = "0.2", optional = true } +wasm-bindgen-futures = { version = "0.4", optional = true } +futures-channel = { version = "0.3", optional = true } +futures-util = { version = "0.3", default-features = false, optional = true, features = [ + "alloc", +] } +js-sys = { version = "0.3" } +web-sys = { version = "0.3", features = [ + "AudioContext", + "AudioContextOptions", + "AudioBuffer", + "AudioBufferSourceNode", + "AudioNode", + "AudioDestinationNode", + "Window", + "AudioContextState", +] } + +[target.'cfg(target_os = "android")'.dependencies] +audio_thread_priority = { version = "0.35", optional = true, default-features = false } +ndk = { version = "0.9", default-features = false, features = [ + "audio", + "api-level-26", +] } +ndk-context = "0.1" +jni = "0.22" +libc = "0.2" +num-derive = "0.4" +num-traits = "0.2" + +[[example]] +name = "beep" + +[[example]] +name = "enumerate" + +[[example]] +name = "feedback" + +[[example]] +name = "record_wav" + +[[example]] +name = "synth_tones" + +[[example]] +name = "custom" +required-features = ["custom"] + +[package.metadata.docs.rs] +# The docs.rs build environment does not have all the dependencies for all features +features = [ + "asio", + "audioworklet", + "custom", + "jack", + "pulseaudio", + "realtime", + "realtime-dbus", + "wasm-bindgen", +] +rustdoc-args = ["--cfg", "docsrs"] +targets = [ + "x86_64-unknown-linux-gnu", + "x86_64-pc-windows-msvc", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "aarch64-apple-ios", + "wasm32-unknown-unknown", + "aarch64-linux-android", + "x86_64-unknown-freebsd", + "x86_64-unknown-netbsd", + "x86_64-unknown-dragonfly", +] diff --git a/crates/vendor/cpal/LICENSE b/crates/vendor/cpal/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/crates/vendor/cpal/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/vendor/cpal/README.md b/crates/vendor/cpal/README.md new file mode 100644 index 000000000..ac5be8b27 --- /dev/null +++ b/crates/vendor/cpal/README.md @@ -0,0 +1,234 @@ +# CPAL - Cross-Platform Audio Library + +[![Actions Status](https://github.com/RustAudio/cpal/workflows/cpal/badge.svg)](https://github.com/RustAudio/cpal/actions) [![Crates.io](https://img.shields.io/crates/v/cpal.svg)](https://crates.io/crates/cpal) [![docs.rs](https://docs.rs/cpal/badge.svg)](https://docs.rs/cpal/) + +Low-level library for audio input and output, written in Rust. + +For higher-level audio playback and capture, consider [Rodio](https://github.com/RustAudio/rodio) or similar libraries. + +## Supported Functionality + +- Enumerate audio hosts, devices, and their supported stream configurations. +- Look up devices by stable ID or by default input/output role. +- Inspect device metadata: name, manufacturer, type, and bus type. +- Build input and output streams with compile-time or runtime sample formats. +- Play, pause, and query the buffer size and clock of a stream. + +## Supported Platforms + +| Platform | Default Backend | Optional Backends | +| -------- | --------------- | ----------------- | +| Android | AAudio | - | +| BSD | ALSA | JACK, PipeWire, PulseAudio | +| iOS | CoreAudio | - | +| Linux | ALSA | JACK, PipeWire, PulseAudio | +| macOS | CoreAudio | JACK | +| tvOS | CoreAudio | - | +| visionOS | CoreAudio | - | +| WebAssembly | Web Audio API | Audio Worklet | +| Windows | WASAPI | ASIO, JACK | + +## Linux Build Dependencies + +On Linux, building cpal requires the ALSA development files: `libasound2-dev` on Debian and Ubuntu, `alsa-lib-devel` on Fedora. + +ALSA is needed even when using JACK, PipeWire, or PulseAudio. + +The optional `realtime-dbus` feature additionally requires `libdbus-1-dev` (Debian/Ubuntu) or `dbus-devel` (Fedora). See [ALSA Real-Time Priority Promotion](#alsa-real-time-priority-promotion). + +## Minimum Supported Versions + +The minimum Rust version (MSRV) and minimum operating system / runtime version both depend on which audio backend and features you're using, as each platform has different dependencies: + +| Backend | Platforms | MSRV | Minimum OS / runtime | +| ------- | --------- | ---- | -------------------- | +| AAudio | Android | 1.85 | Android 8.0 (API 26) | +| ALSA | Linux, BSD | 1.85 | — | +| CoreAudio | macOS | 1.85 | macOS 14.2 (loopback recording requires 14.6+) | +| CoreAudio | iOS | 1.85 | — | +| CoreAudio | tvOS | nightly | — | +| CoreAudio | visionOS | 1.85 | — | +| JACK | Linux, BSD, macOS, Windows | 1.85 | — | +| PipeWire | Linux, BSD | 1.85 | PipeWire 0.3.53 | +| PulseAudio | Linux, BSD | 1.88 | — | +| WASAPI / ASIO | Windows | 1.85 | Windows 8 | +| WASM (`wasm32-unknown`) | WebAssembly | 1.85 | — | +| WASM (`wasm32-wasip1`) | WebAssembly | 1.85 | — | +| WASM (`audioworklet`) | WebAssembly | nightly | — | + +The `audioworklet` backend additionally requires `-Zbuild-std` with atomics support enabled. + +## Optional Features + +| Feature | Platform | Description | +| ------- | -------- | ----------- | +| `asio` | Windows | ASIO backend for low-latency audio, bypassing the Windows audio stack. Requires ASIO drivers and LLVM/Clang. See the [ASIO setup guide](#compiling-for-asio). | +| `audioworklet` | WebAssembly (`wasm32-unknown-unknown`) | Audio Worklet backend for lower-latency web audio than the default Web Audio API, running audio on a dedicated thread. Requires atomics support (`RUSTFLAGS="-C target-feature=+atomics,+bulk-memory,+mutable-globals"`) and `Cross-Origin` headers for `SharedArrayBuffer`. See the `audioworklet-beep` example. | +| `custom` | All | User-defined backend implementations for audio systems not natively supported by CPAL. See `examples/custom.rs`. | +| `jack` | Linux, BSD, macOS, Windows | JACK Audio Connection Kit backend for pro-audio routing and inter-application connectivity. Requires `libjack-jackd2-dev` (Debian/Ubuntu) or `jack-devel` (Fedora). | +| `pipewire` | Linux, BSD | PipeWire media server backend. Requires `libpipewire-0.3-dev` (Debian/Ubuntu) or `pipewire-devel` (Fedora). | +| `pulseaudio` | Linux, BSD | PulseAudio sound server backend. Requires `libpulse-dev` (Debian/Ubuntu) or `pulseaudio-libs-devel` (Fedora). | +| `realtime` | Android, Windows | Raises the audio callback thread to real-time or high-priority scheduling for lower latency. | +| `realtime-dbus` | Linux | Uses `rtkit` via D-Bus for RT scheduling on Linux desktop systems. Implies `realtime` on all platforms. Requires `libdbus-1-dev` on Linux. | +| `wasm-bindgen` | WebAssembly (`wasm32-unknown-unknown`) | Web Audio API backend for browser-based audio; required for any WebAssembly audio support. See the `wasm-beep` example. | + +See the [beep example](examples/beep.rs) for selecting the backend at runtime. + +## Compiling for ASIO + +### Locating the ASIO SDK + +The location of ASIO SDK is exposed to CPAL by setting the `CPAL_ASIO_DIR` environment variable. + +The build script will try to find the ASIO SDK by following these steps in order: + +1. Check if `CPAL_ASIO_DIR` is set and if so use the path to point to the SDK. +2. Check if the ASIO SDK is already installed in the temporary directory, if so use that and set the path of `CPAL_ASIO_DIR` to the output of `std::env::temp_dir().join("asio_sdk")`. +3. If the ASIO SDK is not already installed, download it from and install it in the temporary directory. The path of `CPAL_ASIO_DIR` will be set to the output of `std::env::temp_dir().join("asio_sdk")`. + +In an ideal situation you don't need to worry about this step. + +### Preparing the Build Environment + +1. **Install LLVM/Clang**: `bindgen`, the library used to generate bindings to the C++ SDK, requires clang. Download and install LLVM from under the "Pre-Built Binaries" section. + +2. **Set LIBCLANG_PATH**: Add the LLVM `bin` directory to a `LIBCLANG_PATH` environment variable. If you installed LLVM to the default directory, this should work in the command prompt: + + ```bat + setx LIBCLANG_PATH "C:\Program Files\LLVM\bin" + ``` + +3. **Install ASIO Drivers** (optional for testing): If you don't have any ASIO devices or drivers available, you can download and install ASIO4ALL from . Be sure to enable the "offline" feature during installation. + +4. **Visual Studio**: The build script assumes Microsoft Visual Studio is installed. It will try to find `vcvarsall.bat` and execute it with the right host and target architecture. If needed, you can manually execute it: + + ```bat + "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64 + ``` + + For more information see the [vcvarsall.bat documentation](https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line). + +### Using ASIO in Your Application + +1. **Enable the feature** in your `Cargo.toml`: + + ```toml + cpal = { version = "*", features = ["asio"] } + ``` + +2. **Select the ASIO backend** in your code: + + ```rust + let host = cpal::host_from_id(cpal::HostId::Asio) + .expect("failed to initialise ASIO host"); + ``` + +### Cross-Compilation + +When Windows is the host and target OS, the build script supports all cross-compilation targets supported by the MSVC compiler. + +It is also possible to compile Windows applications with ASIO support on Linux and macOS using the MinGW-w64 toolchain. + +**Requirements:** + +- Include the MinGW-w64 include directory in your `CPLUS_INCLUDE_PATH` environment variable +- Include the LLVM include directory in your `CPLUS_INCLUDE_PATH` environment variable + +**Example for macOS** (targeting `x86_64-pc-windows-gnu` with `mingw-w64` installed via brew): + +```sh +export CPLUS_INCLUDE_PATH="$CPLUS_INCLUDE_PATH:/opt/homebrew/Cellar/mingw-w64/11.0.1/toolchain-x86_64/x86_64-w64-mingw32/include" +``` + +## Compiling for WebAssembly + +If you are interested in using CPAL with WebAssembly, please see [this guide](https://github.com/RustAudio/cpal/wiki/Setting-up-a-new-CPAL-WASM-project) in our Wiki which walks through setting up a new project from scratch. Some of the examples in this repository also provide working configurations that you can use as reference. + +## Troubleshooting + +### No Default Device Available + +If you receive errors about no default input or output device: + +- **Linux/PipeWire:** Check that PipeWire is running: `pw-cli info` +- **Linux/PulseAudio:** Check that PulseAudio is running: `pulseaudio --check` +- **macOS:** Check System Preferences > Sound for available devices +- **Mobile (iOS/Android):** Ensure your app has microphone/audio permissions +- **Windows:** Verify your audio device is enabled in Sound Settings + +## ALSA, PipeWire, and PulseAudio + +When PipeWire or PulseAudio is running, it holds the ALSA `default` device exclusively. A second stream attempting to open it via the ALSA host will fail with a `DeviceBusy` error. To route audio through the sound server via ALSA, use the bridge devices `pipewire` or `pulse` instead of `default`. Better yet, use the `pipewire` or `pulseaudio` cpal features for native integration. + +On targets without a sound server, address devices directly as `hw:` or `plughw:`. + +### Buffer Size Issues + +`BufferSize::Default` uses the system-configured device default, which on **ALSA** can range from a PipeWire quantum (typically 1024 frames) to `u32::MAX` on misconfigured or exotic hardware. A very deep buffer causes samples to be consumed far faster than audible playback, making audio appear to fast-forward ahead of actual output. + +Configure the system and/or request a fixed size in your application: + +| System | File | Setting | +| ------ | ---- | ------- | +| ALSA | `~/.asoundrc` or `/etc/asound.conf` | `buffer_size`, `periods` * `period_size` | +| PipeWire | `~/.config/pipewire/pipewire.conf.d/` | `default.clock.quantum` | +| PulseAudio | `~/.config/pulse/daemon.conf` | `default-fragments` * `default-fragment-size-msec` | + +```rust +config.buffer_size = cpal::BufferSize::Fixed(1024); +``` + +Query `device.default_output_config()?.buffer_size()` for valid ranges. Smaller buffers reduce latency but increase CPU load and the risk of glitches. + +### ALSA Real-Time Priority Promotion + +RT promotion is only attempted for a whitelist of PCM types: direct hardware PCMs (`hw:`) and pure format-conversion plugins (linear, A-law, mu-law, ADPCM, float, IEC 958). + +`RealtimeDenied` is emitted on the error callback only when promotion is attempted but fails, which happens when the process lacks the resource limits to acquire `SCHED_FIFO`. While RT priority is desirable for low latency, the stream will continue to play at the default scheduling priority. + +On Linux, use `realtime-dbus`: `rtkit` arranges the necessary limits over D-Bus. + +Independently of RT scheduling, some systems need the user added to the `audio` group for ALSA device access via `udev` (`usermod -aG audio "$USER"`, then re-login). + +### Build Errors + +If you are unable to build the library: + +- Verify you have installed the required development libraries, as documented above +- **ASIO on Windows:** Verify `CPAL_ASIO_DIR` and `LIBCLANG_PATH` are set and LLVM is installed + +## Examples + +CPAL comes with several examples in `examples/`. + +Run an example with: + +```sh +cargo run --example beep +``` + +For platform-specific features, enable the relevant features: + +```sh +cargo run --example beep --features asio # Windows ASIO backend +cargo run --example beep --features jack # JACK backend +cargo run --example beep --features pipewire # PipeWire backend +cargo run --example beep --features pulseaudio # PulseAudio backend +``` + +## Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +## Resources + +- **Documentation:** [docs.rs/cpal](https://docs.rs/cpal) +- **Examples:** [examples/](examples/) directory in this repository +- **Higher-level playback and capture:** [Rodio](https://github.com/RustAudio/rodio) or similar libraries +- **Discord:** Join the [#cpal channel](https://discord.gg/vPmmSgJSPV) for questions and discussion +- **GitHub:** [Report issues](https://github.com/RustAudio/cpal/issues) and [view source code](https://github.com/RustAudio/cpal) +- **RustAudio:** Part of the [RustAudio organization](https://github.com/RustAudio) + +## License + +Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details. diff --git a/crates/vendor/cpal/build.rs b/crates/vendor/cpal/build.rs new file mode 100644 index 000000000..696db7bab --- /dev/null +++ b/crates/vendor/cpal/build.rs @@ -0,0 +1,10 @@ +use std::env; + +const CPAL_ASIO_DIR: &str = "CPAL_ASIO_DIR"; + +fn main() { + println!("cargo:rerun-if-env-changed={CPAL_ASIO_DIR}"); + if env::var(CPAL_ASIO_DIR).is_ok() { + println!("cargo:rustc-cfg=asio"); + } +} diff --git a/crates/vendor/cpal/src/device_description.rs b/crates/vendor/cpal/src/device_description.rs new file mode 100644 index 000000000..f797aa00b --- /dev/null +++ b/crates/vendor/cpal/src/device_description.rs @@ -0,0 +1,400 @@ +//! Device metadata and description types. +//! +//! This module provides structured information about audio devices including manufacturer, +//! device type, interface type, and connection details. Not all backends provide complete +//! information - availability depends on platform capabilities. + +use std::fmt; + +use crate::ChannelCount; + +/// Describes an audio device with structured metadata. +/// +/// This type provides structured information about an audio device beyond just its name. +/// Availability depends on the host implementation and platform capabilities. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeviceDescription { + /// Human-readable device name + name: String, + + /// Device manufacturer or vendor name + manufacturer: Option, + + /// Driver name + driver: Option, + + /// Categorization of device type + device_type: DeviceType, + + /// Connection/interface type + interface_type: InterfaceType, + + /// Direction: input, output, or duplex + direction: DeviceDirection, + + /// Physical address or connection identifier + address: Option, + + /// Additional description lines with non-structured, detailed information. + extended: Box<[String]>, +} + +/// Categorization of audio device types. +/// +/// This describes the kind of audio device (speaker, microphone, headset, etc.) +/// regardless of how it connects to the system. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[non_exhaustive] +pub enum DeviceType { + /// Speaker (built-in or external) + Speaker, + + /// Microphone (built-in or external) + Microphone, + + /// Headphones (audio output only) + Headphones, + + /// Headset (combined headphones + microphone) + Headset, + + /// Earpiece (phone-style speaker, typically for voice calls) + Earpiece, + + /// Handset (telephone-style handset with speaker and microphone) + Handset, + + /// Hearing aid device + HearingAid, + + /// Docking station audio + Dock, + + /// Radio/TV tuner + Tuner, + + /// Virtual/loopback device (software audio routing) + Virtual, + + /// Unknown or unclassified device type + #[default] + Unknown, +} + +/// How the device connects to the system (interface/connection type). +/// +/// This describes the physical or logical connection between the audio device +/// and the computer system. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[non_exhaustive] +pub enum InterfaceType { + /// Built-in to the system (integrated audio chipset) + BuiltIn, + + /// USB connection + Usb, + + /// Bluetooth wireless connection + Bluetooth, + + /// PCI or PCIe card (internal sound card) + Pci, + + /// FireWire connection (IEEE 1394) + FireWire, + + /// Thunderbolt connection + Thunderbolt, + + /// HDMI connection + Hdmi, + + /// Line-level analog connection (line in/out, aux) + Line, + + /// S/PDIF digital audio interface + Spdif, + + /// Network connection (Dante, AVB, AirPlay, IP audio, etc.) + Network, + + /// Virtual/loopback connection (software audio routing, not physical hardware) + Virtual, + + /// DisplayPort audio + DisplayPort, + + /// Aggregate device (combines multiple devices) + Aggregate, + + /// Unknown connection type + #[default] + Unknown, +} + +/// The direction(s) that a device supports. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +#[non_exhaustive] +pub enum DeviceDirection { + /// Input only (capture/recording) + Input, + + /// Output only (playback/rendering) + Output, + + /// Both input and output + Duplex, + + /// Direction unknown or not yet determined + #[default] + Unknown, +} + +impl DeviceDescription { + /// Returns the human-readable device name. + /// + /// This is always available and is the primary user-facing identifier. + pub fn name(&self) -> &str { + self.name.as_str() + } + + /// Returns the manufacturer/vendor name if available. + pub fn manufacturer(&self) -> Option<&str> { + self.manufacturer.as_deref() + } + + /// Returns the driver name if available. + pub fn driver(&self) -> Option<&str> { + self.driver.as_deref() + } + + /// Returns the device type categorization. + pub fn device_type(&self) -> DeviceType { + self.device_type + } + + /// Returns the interface/connection type. + pub fn interface_type(&self) -> InterfaceType { + self.interface_type + } + + /// Returns the device direction. + pub fn direction(&self) -> DeviceDirection { + self.direction + } + + /// Returns whether this device supports audio input (capture). + /// + /// This is a convenience method that checks if direction is `Input` or `Duplex`. + pub fn supports_input(&self) -> bool { + matches!( + self.direction, + DeviceDirection::Input | DeviceDirection::Duplex + ) + } + + /// Returns whether this device supports audio output (playback). + /// + /// This is a convenience method that checks if direction is `Output` or `Duplex`. + pub fn supports_output(&self) -> bool { + matches!( + self.direction, + DeviceDirection::Output | DeviceDirection::Duplex + ) + } + + /// Returns the physical address or connection identifier if available. + pub fn address(&self) -> Option<&str> { + self.address.as_deref() + } + + /// Returns additional description lines with detailed information. + pub fn extended(&self) -> impl Iterator { + self.extended.iter().map(String::as_str) + } +} + +impl fmt::Display for DeviceDescription { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name)?; + + if let Some(mfr) = &self.manufacturer { + write!(f, " ({})", mfr)?; + } + + if self.device_type != DeviceType::Unknown { + write!(f, " [{}]", self.device_type)?; + } + + if self.interface_type != InterfaceType::Unknown { + write!(f, " via {}", self.interface_type)?; + } + + Ok(()) + } +} + +impl fmt::Display for DeviceType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DeviceType::Speaker => write!(f, "Speaker"), + DeviceType::Microphone => write!(f, "Microphone"), + DeviceType::Headphones => write!(f, "Headphones"), + DeviceType::Headset => write!(f, "Headset"), + DeviceType::Earpiece => write!(f, "Earpiece"), + DeviceType::Handset => write!(f, "Handset"), + DeviceType::HearingAid => write!(f, "Hearing Aid"), + DeviceType::Dock => write!(f, "Dock"), + DeviceType::Tuner => write!(f, "Tuner"), + DeviceType::Virtual => write!(f, "Virtual"), + _ => write!(f, "Unknown"), + } + } +} + +impl fmt::Display for InterfaceType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InterfaceType::BuiltIn => write!(f, "Built-in"), + InterfaceType::Usb => write!(f, "USB"), + InterfaceType::Bluetooth => write!(f, "Bluetooth"), + InterfaceType::Pci => write!(f, "PCI"), + InterfaceType::FireWire => write!(f, "FireWire"), + InterfaceType::Thunderbolt => write!(f, "Thunderbolt"), + InterfaceType::Hdmi => write!(f, "HDMI"), + InterfaceType::Line => write!(f, "Line"), + InterfaceType::Spdif => write!(f, "S/PDIF"), + InterfaceType::Network => write!(f, "Network"), + InterfaceType::Virtual => write!(f, "Virtual"), + InterfaceType::DisplayPort => write!(f, "DisplayPort"), + InterfaceType::Aggregate => write!(f, "Aggregate"), + _ => write!(f, "Unknown"), + } + } +} + +impl fmt::Display for DeviceDirection { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + DeviceDirection::Input => write!(f, "Input"), + DeviceDirection::Output => write!(f, "Output"), + DeviceDirection::Duplex => write!(f, "Duplex"), + _ => write!(f, "Unknown"), + } + } +} + +/// Builder for constructing a `DeviceDescription`. +/// +/// This is primarily used by host implementations and custom hosts +/// to gradually build up device descriptions with available metadata. +#[derive(Debug, Clone)] +pub struct DeviceDescriptionBuilder { + name: String, + manufacturer: Option, + driver: Option, + device_type: DeviceType, + interface_type: InterfaceType, + direction: DeviceDirection, + address: Option, + extended: Vec, +} + +impl DeviceDescriptionBuilder { + /// Creates a new builder with the device name (required). + pub fn new(name: impl AsRef) -> Self { + Self { + name: name.as_ref().to_owned(), + manufacturer: None, + driver: None, + device_type: DeviceType::default(), + interface_type: InterfaceType::default(), + direction: DeviceDirection::default(), + address: None, + extended: Vec::new(), + } + } + + /// Sets the manufacturer name. + pub fn manufacturer(mut self, manufacturer: impl AsRef) -> Self { + self.manufacturer = Some(manufacturer.as_ref().to_owned()); + self + } + + /// Sets the driver name. + pub fn driver(mut self, driver: impl AsRef) -> Self { + self.driver = Some(driver.as_ref().to_owned()); + self + } + + /// Sets the device type. + pub fn device_type(mut self, device_type: DeviceType) -> Self { + self.device_type = device_type; + self + } + + /// Sets the interface type. + pub fn interface_type(mut self, interface_type: InterfaceType) -> Self { + self.interface_type = interface_type; + self + } + + /// Sets the device direction. + pub fn direction(mut self, direction: DeviceDirection) -> Self { + self.direction = direction; + self + } + + /// Sets the physical address. + pub fn address(mut self, address: impl AsRef) -> Self { + self.address = Some(address.as_ref().to_owned()); + self + } + + /// Sets the description lines, replacing any previously added lines. + pub fn extended>(mut self, lines: impl IntoIterator) -> Self { + self.extended = lines.into_iter().map(|s| s.as_ref().to_owned()).collect(); + self + } + + /// Adds a single description line. + pub fn add_extended_line(mut self, line: impl AsRef) -> Self { + self.extended.push(line.as_ref().to_owned()); + self + } + + /// Builds the [`DeviceDescription`]. + pub fn build(self) -> DeviceDescription { + DeviceDescription { + name: self.name, + manufacturer: self.manufacturer, + driver: self.driver, + device_type: self.device_type, + interface_type: self.interface_type, + direction: self.direction, + address: self.address, + extended: self.extended.into_boxed_slice(), + } + } +} + +/// Determines device direction from input/output capabilities. +pub(crate) fn direction_from_caps(has_input: bool, has_output: bool) -> DeviceDirection { + match (has_input, has_output) { + (true, true) => DeviceDirection::Duplex, + (true, false) => DeviceDirection::Input, + (false, true) => DeviceDirection::Output, + (false, false) => DeviceDirection::Unknown, + } +} + +/// Determines device direction from input/output channel counts. +#[allow(dead_code)] +pub(crate) fn direction_from_counts( + input_channels: Option, + output_channels: Option, +) -> DeviceDirection { + let has_input = input_channels.map(|n| n > 0).unwrap_or(false); + let has_output = output_channels.map(|n| n > 0).unwrap_or(false); + direction_from_caps(has_input, has_output) +} diff --git a/crates/vendor/cpal/src/error.rs b/crates/vendor/cpal/src/error.rs new file mode 100644 index 000000000..ffff4fb9c --- /dev/null +++ b/crates/vendor/cpal/src/error.rs @@ -0,0 +1,228 @@ +use std::{ + borrow::Cow, + error::Error as StdError, + fmt::{Display, Formatter}, +}; + +/// A list specifying general categories of CPAL error. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ErrorKind { + /// The device is temporarily busy. This can happen when another application or stream + /// is using the device. Retrying after a short delay may succeed. + DeviceBusy, + + /// The active audio route changed and the stream was automatically rerouted. + /// The stream remains active and no rebuild is required. + /// + /// Only fires for a stream built from the default device, on backends that support following + /// it. A stream built from a specific device does not follow a replacement; if that device + /// disappears, it reports [`DeviceNotAvailable`] instead. + /// + /// [`DeviceNotAvailable`]: ErrorKind::DeviceNotAvailable + DeviceChanged, + + /// The requested audio device is not available. + /// + /// This can happen if the device has been disconnected while the program is running, or if + /// the device identifier refers to a device that does not exist on this system. + DeviceNotAvailable, + + /// The audio host (server or subsystem) is not available on this system. + /// + /// This is distinct from [`DeviceNotAvailable`]: when a host (e.g. PulseAudio, PipeWire, JACK, + /// or kernel subsystem) is absent or not running, no devices can be reached through it. + /// + /// [`DeviceNotAvailable`]: ErrorKind::DeviceNotAvailable + HostUnavailable, + + /// Invalid input or argument. + InvalidInput, + + /// Access to the device or resource was denied by the operating system or audio subsystem. + /// + /// The device exists and may be functional, but the current process or user does not have + /// permission to use it. Common causes include microphone privacy settings (iOS, macOS), + /// missing audio group membership (Linux), or file permission errors. + /// + /// Unlike [`DeviceNotAvailable`], which signals absence, this variant signals an + /// authorization failure. + /// + /// [`DeviceNotAvailable`]: ErrorKind::DeviceNotAvailable + PermissionDenied, + + /// A real-time scheduling promotion or equivalent platform performance mode was refused. + /// Audio will still play, but may be subject to increased latency or glitches under load. + /// + /// Absence of this error does **not** mean real-time quality is active: the check may not + /// have been attempted (feature flag disabled, device ineligible, or the host manages + /// scheduling internally). + RealtimeDenied, + + /// An OS resource limit was reached, such as a system or process thread or memory limit. + ResourceExhausted, + + /// The stream configuration is no longer valid and must be rebuilt. + StreamInvalidated, + + /// The requested stream configuration is not supported. This includes unsupported sample + /// rates, channel counts, or sample formats. + UnsupportedConfig, + + /// The requested operation is not supported. This includes unsupported stream directions + /// (e.g., requesting input on an output-only device), unavailable features, or operations + /// not implemented by the backend. + UnsupportedOperation, + + /// A buffer underrun or overrun occurred, causing a potential audio glitch. + Xrun, + + /// The underlying platform audio API returned an error that CPAL cannot map to a more + /// specific error kind. + BackendError, + + /// A catch-all for errors that do not fall under any other CPAL error kind. + /// + /// CPAL itself emits this variant only for genuinely unclassifiable conditions. Treat them as + /// permanent: no retry strategy is possible without host-specific knowledge. + /// + /// New [`ErrorKind`] variants may be added in future releases to cover specific cases + /// currently reported as `Other`. + Other, +} + +impl Display for ErrorKind { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::DeviceBusy => f.write_str( + "The requested device is temporarily busy. Another application or stream may be using it.", + ), + Self::DeviceChanged => f.write_str( + "The audio route changed. The stream was automatically rerouted to a different device.", + ), + Self::DeviceNotAvailable => f.write_str( + "The requested audio device is not available. It may have been disconnected.", + ), + Self::HostUnavailable => f.write_str( + "The requested audio host is not available. The subsystem or daemon may not be installed or running.", + ), + Self::InvalidInput => f.write_str("Invalid input or argument."), + Self::PermissionDenied => f.write_str( + "Permission denied. Grant the required access and retry.", + ), + Self::RealtimeDenied => f.write_str( + "Real-time scheduling was refused for the audio thread. \ + Audio may be subject to increased latency or glitches under load.", + ), + Self::ResourceExhausted => f.write_str( + "An OS resource limit was reached. Freeing resources and retrying may succeed.", + ), + Self::StreamInvalidated => { + f.write_str("The stream configuration is no longer valid and must be rebuilt.") + } + Self::UnsupportedConfig => f.write_str( + "The requested stream configuration is not supported by the device.", + ), + Self::UnsupportedOperation => f.write_str("The requested operation is not supported."), + Self::Xrun => f.write_str("A buffer underrun or overrun occurred."), + Self::BackendError => f.write_str( + "The audio backend returned an unclassified error.", + ), + Self::Other => f.write_str("An error occurred."), + } + } +} + +/// Error type for all CPAL operations. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Error { + kind: ErrorKind, + message: Option>, +} + +impl Error { + /// Create a new error with the given kind and no message. + pub fn new(kind: ErrorKind) -> Self { + Self { + kind, + message: None, + } + } + + /// Create a new error with the given kind and a human-readable message. + pub fn with_message(kind: ErrorKind, message: impl Into>) -> Self { + Self { + kind, + message: Some(message.into()), + } + } + + /// Returns the error kind. + pub fn kind(&self) -> ErrorKind { + self.kind + } + + /// Returns the human-readable message, if any. + pub fn message(&self) -> Option<&str> { + self.message.as_deref() + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match &self.message { + Some(msg) => f.write_str(msg), + None => write!(f, "{}", self.kind), + } + } +} + +impl StdError for Error {} + +impl From for Error { + fn from(kind: ErrorKind) -> Self { + Self::new(kind) + } +} + +#[cfg(all( + feature = "realtime", + any( + target_os = "windows", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "android" + ) +))] +impl From for Error { + fn from(err: audio_thread_priority::AudioThreadPriorityError) -> Self { + use std::error::Error as StdError; + let msg = match err.source() { + Some(inner) => { + format!("Failed to promote audio thread to real-time priority: {err}: {inner}") + } + None => format!("Failed to promote audio thread to real-time priority: {err}"), + }; + Error::with_message(ErrorKind::RealtimeDenied, msg) + } +} + +/// Extension trait for attaching a context message to a [`Result`] whose error converts into +/// [`cpal::Error`]. +#[allow(dead_code)] +pub(crate) trait ResultExt { + /// Converts the error via [`Into`] and prepends `msg`, yielding + /// `": "` as the message. + fn context(self, msg: impl Display) -> Result; +} + +impl> ResultExt for Result { + fn context(self, msg: impl Display) -> Result { + self.map_err(|e| { + let e = e.into(); + Error::with_message(e.kind(), format!("{msg}: {e}")) + }) + } +} diff --git a/crates/vendor/cpal/src/host/aaudio/convert.rs b/crates/vendor/cpal/src/host/aaudio/convert.rs new file mode 100644 index 000000000..0b54a4e76 --- /dev/null +++ b/crates/vendor/cpal/src/host/aaudio/convert.rs @@ -0,0 +1,84 @@ +//! Time-conversion helpers for the AAudio backend. + +extern crate ndk; + +use crate::{Error, ErrorKind, StreamInstant}; + +/// Returns a [`StreamInstant`] for the current moment. +pub fn now_stream_instant() -> StreamInstant { + let mut ts = libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }; + let res = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut ts) }; + assert_eq!(res, 0, "clock_gettime(CLOCK_MONOTONIC) failed"); + StreamInstant::new(ts.tv_sec as u64, ts.tv_nsec as u32) +} + +/// Projects a hardware timestamp anchor to the instant of a specific frame position. +fn stream_instant_from_anchor( + anchor_frame: i64, + anchor_nanos: i64, + app_frame: i64, + sample_rate: u32, +) -> StreamInstant { + let offset_nanos = + (app_frame as i128 - anchor_frame as i128) * 1_000_000_000 / sample_rate as i128; + StreamInstant::from_nanos((anchor_nanos as i128 + offset_nanos).max(0) as u64) +} + +/// Returns the [`StreamInstant`] for when the first frame of the current output callback will +/// be presented at the DAC. +pub fn output_stream_instant(stream: &ndk::audio::AudioStream, sample_rate: u32) -> StreamInstant { + match stream.timestamp(ndk::audio::Clockid::Monotonic) { + Ok(ts) => stream_instant_from_anchor( + ts.frame_position, + ts.time_nanoseconds, + stream.frames_written(), + sample_rate, + ), + Err(_) => now_stream_instant(), + } +} + +/// Returns the [`StreamInstant`] for when the first frame of the current input callback was +/// captured at the ADC. +pub fn input_stream_instant(stream: &ndk::audio::AudioStream, sample_rate: u32) -> StreamInstant { + match stream.timestamp(ndk::audio::Clockid::Monotonic) { + Ok(ts) => stream_instant_from_anchor( + ts.frame_position, + ts.time_nanoseconds, + stream.frames_read(), + sample_rate, + ), + Err(_) => now_stream_instant(), + } +} + +impl From for Error { + fn from(error: ndk::audio::AudioError) -> Self { + use ndk::audio::AudioError::*; + match error { + Disconnected | Unavailable | NoService | InvalidHandle => { + Error::with_message(ErrorKind::DeviceNotAvailable, error.to_string()) + } + NoFreeHandles | NoMemory => { + Error::with_message(ErrorKind::ResourceExhausted, error.to_string()) + } + WouldBlock | Timeout => Error::with_message(ErrorKind::DeviceBusy, error.to_string()), + InvalidFormat | InvalidRate => { + Error::with_message(ErrorKind::UnsupportedConfig, error.to_string()) + } + IllegalArgument | Null | OutOfRange => { + Error::with_message(ErrorKind::InvalidInput, error.to_string()) + } + Internal | InvalidState => { + Error::with_message(ErrorKind::StreamInvalidated, error.to_string()) + } + Unimplemented => { + Error::with_message(ErrorKind::UnsupportedOperation, error.to_string()) + } + _ => Error::with_message(ErrorKind::BackendError, error.to_string()), + } + } +} diff --git a/crates/vendor/cpal/src/host/aaudio/java_interface.rs b/crates/vendor/cpal/src/host/aaudio/java_interface.rs new file mode 100644 index 000000000..c12e540f4 --- /dev/null +++ b/crates/vendor/cpal/src/host/aaudio/java_interface.rs @@ -0,0 +1,7 @@ +mod audio_features; +mod audio_manager; +mod definitions; +mod devices_info; +mod utils; + +pub use self::definitions::*; diff --git a/crates/vendor/cpal/src/host/aaudio/java_interface/audio_features.rs b/crates/vendor/cpal/src/host/aaudio/java_interface/audio_features.rs new file mode 100644 index 000000000..1c3df3368 --- /dev/null +++ b/crates/vendor/cpal/src/host/aaudio/java_interface/audio_features.rs @@ -0,0 +1,55 @@ +use super::{ + utils::{ + get_context, get_package_manager, has_system_feature, with_attached, Env, JObject, JResult, + }, + PackageManager, +}; + +/** + * The Android audio features + */ +#[derive(Debug, Clone, Copy)] +pub enum AudioFeature { + LowLatency, + Output, + Pro, + Microphone, + Midi, +} + +impl From for &'static str { + fn from(feature: AudioFeature) -> Self { + use AudioFeature::*; + match feature { + LowLatency => PackageManager::FEATURE_AUDIO_LOW_LATENCY, + Output => PackageManager::FEATURE_AUDIO_OUTPUT, + Pro => PackageManager::FEATURE_AUDIO_PRO, + Microphone => PackageManager::FEATURE_MICROPHONE, + Midi => PackageManager::FEATURE_MIDI, + } + } +} + +impl AudioFeature { + /** + * Check availability of an audio feature using Android Java API + */ + pub fn has(&self) -> Result { + let context = get_context(); + + with_attached(context, |env, activity| { + try_check_system_feature(env, &activity, (*self).into()) + }) + .map_err(|error| error.to_string()) + } +} + +fn try_check_system_feature<'j>( + env: &mut Env<'j>, + activity: &JObject<'j>, + feature: &str, +) -> JResult { + let package_manager = get_package_manager(env, activity)?; + + has_system_feature(env, &package_manager, feature) +} diff --git a/crates/vendor/cpal/src/host/aaudio/java_interface/audio_manager.rs b/crates/vendor/cpal/src/host/aaudio/java_interface/audio_manager.rs new file mode 100644 index 000000000..35021fff6 --- /dev/null +++ b/crates/vendor/cpal/src/host/aaudio/java_interface/audio_manager.rs @@ -0,0 +1,24 @@ +use super::{ + utils::{get_context, get_system_property, with_attached, Env, JResult}, + AudioManager, +}; + +impl AudioManager { + /// Get the AAudio mixer burst count from system property + pub fn get_mixer_bursts() -> Result { + let context = get_context(); + + with_attached(context, |env, _context| get_mixer_bursts(env)) + .map_err(|error| error.to_string()) + } +} + +fn get_mixer_bursts<'j>(env: &mut Env<'j>) -> JResult { + let mixer_bursts = get_system_property(env, "aaudio.mixer_bursts", "2")?; + + let mixer_bursts_string = String::from(mixer_bursts.mutf8_chars(env)?); + + mixer_bursts_string + .parse::() + .map_err(|e| jni::errors::Error::ParseFailed(e.to_string())) +} diff --git a/crates/vendor/cpal/src/host/aaudio/java_interface/definitions.rs b/crates/vendor/cpal/src/host/aaudio/java_interface/definitions.rs new file mode 100644 index 000000000..ffe6fb810 --- /dev/null +++ b/crates/vendor/cpal/src/host/aaudio/java_interface/definitions.rs @@ -0,0 +1,137 @@ +use num_derive::FromPrimitive; + +use crate::{DeviceDirection, SampleFormat}; + +pub(crate) struct Context; + +impl Context { + pub const AUDIO_SERVICE: &'static str = "audio"; +} + +pub(crate) struct PackageManager; + +impl PackageManager { + pub const FEATURE_AUDIO_LOW_LATENCY: &'static str = "android.hardware.audio.low_latency"; + pub const FEATURE_AUDIO_OUTPUT: &'static str = "android.hardware.audio.output"; + pub const FEATURE_AUDIO_PRO: &'static str = "android.hardware.audio.pro"; + pub const FEATURE_MICROPHONE: &'static str = "android.hardware.microphone"; + pub const FEATURE_MIDI: &'static str = "android.software.midi"; +} + +pub(crate) struct AudioManager; + +impl AudioManager { + pub const GET_DEVICES_INPUTS: i32 = 1 << 0; + pub const GET_DEVICES_OUTPUTS: i32 = 1 << 1; + pub const GET_DEVICES_ALL: i32 = Self::GET_DEVICES_INPUTS | Self::GET_DEVICES_OUTPUTS; +} + +/** + * The Android audio device info + */ +#[derive(Debug, Clone)] +pub struct AudioDeviceInfo { + /** + * Device identifier + */ + pub id: i32, + + /** + * The type of device + */ + pub device_type: AudioDeviceType, + + /** + * The device can be used for playback and/or capture + */ + pub direction: DeviceDirection, + + /** + * Device address + */ + pub address: String, + + /** + * Device product name + */ + pub product_name: String, + + /** + * Available channel configurations + */ + pub channel_counts: Box<[i32]>, + + /** + * Supported sample rates + */ + pub sample_rates: Box<[i32]>, + + /** + * Supported audio formats + */ + pub formats: Box<[SampleFormat]>, +} + +/** + * The type of audio device + */ +#[derive(Debug, Clone, Copy, Default, FromPrimitive)] +#[non_exhaustive] +#[repr(i32)] +pub enum AudioDeviceType { + #[default] + Unknown = 0, + AuxLine = 19, + BleBroadcast = 30, + BleHeadset = 26, + BleSpeaker = 27, + BluetoothA2DP = 8, + BluetoothSCO = 7, + BuiltinEarpiece = 1, + BuiltinMic = 15, + BuiltinSpeaker = 2, + BuiltinSpeakerSafe = 24, + Bus = 21, + Dock = 13, + Fm = 14, + FmTuner = 16, + Hdmi = 9, + HdmiArc = 10, + HdmiEarc = 29, + HearingAid = 23, + Ip = 20, + LineAnalog = 5, + LineDigital = 6, + RemoteSubmix = 25, + Telephony = 18, + TvTuner = 17, + UsbAccessory = 12, + UsbDevice = 11, + UsbHeadset = 22, + WiredHeadphones = 4, + WiredHeadset = 3, + Unsupported = -1, +} + +/// Converts DeviceDirection to Android AudioManager device flags. +pub(super) fn android_device_flags(direction: DeviceDirection) -> i32 { + match direction { + DeviceDirection::Input => AudioManager::GET_DEVICES_INPUTS, + DeviceDirection::Output => AudioManager::GET_DEVICES_OUTPUTS, + _ => AudioManager::GET_DEVICES_ALL, + } +} + +impl SampleFormat { + pub(crate) const ENCODING_PCM_16BIT: i32 = 2; + //pub(crate) const ENCODING_PCM_8BIT: i32 = 3; + pub(crate) const ENCODING_PCM_FLOAT: i32 = 4; + + pub(crate) fn from_encoding(encoding: i32) -> Option { + match encoding { + SampleFormat::ENCODING_PCM_16BIT => Some(SampleFormat::I16), + SampleFormat::ENCODING_PCM_FLOAT => Some(SampleFormat::F32), + _ => None, + } + } +} diff --git a/crates/vendor/cpal/src/host/aaudio/java_interface/devices_info.rs b/crates/vendor/cpal/src/host/aaudio/java_interface/devices_info.rs new file mode 100644 index 000000000..1d63b518e --- /dev/null +++ b/crates/vendor/cpal/src/host/aaudio/java_interface/devices_info.rs @@ -0,0 +1,93 @@ +use num_traits::FromPrimitive; + +use super::{ + android_device_flags, + utils::{ + call_method_no_args_ret_bool, call_method_no_args_ret_char_sequence, + call_method_no_args_ret_int, call_method_no_args_ret_int_array, + call_method_no_args_ret_string, get_context, get_devices, get_system_service, + with_attached, Env, JObject, JResult, + }, + AudioDeviceInfo, AudioDeviceType, Context, +}; +use crate::{DeviceDirection, SampleFormat}; + +impl AudioDeviceInfo { + /** + * Request audio devices using Android Java API + */ + pub fn request(direction: DeviceDirection) -> Result, String> { + let context = get_context(); + + with_attached(context, |env, context| { + let sdk_version = env + .get_static_field( + jni::jni_str!("android/os/Build$VERSION"), + jni::jni_str!("SDK_INT"), + jni::jni_sig!("I"), + )? + .i()?; + + if sdk_version >= 23 { + try_request_devices_info(env, &context, direction) + } else { + Err(jni::errors::Error::MethodNotFound { + name: "".into(), + sig: "".into(), + }) + } + }) + .map_err(|error| error.to_string()) + } +} + +fn try_request_devices_info<'j>( + env: &mut Env<'j>, + context: &JObject<'j>, + direction: DeviceDirection, +) -> JResult> { + let audio_manager = get_system_service(env, context, Context::AUDIO_SERVICE)?; + + let devices = get_devices(env, &audio_manager, android_device_flags(direction))?; + + let length = devices.len(env)?; + + (0..length) + .map(|index| { + let device = devices.get_element(env, index)?; + let id = call_method_no_args_ret_int(env, &device, "getId")?; + let address = call_method_no_args_ret_string(env, &device, "getAddress")?; + let address = String::from(address.mutf8_chars(env)?); + let product_name = + call_method_no_args_ret_char_sequence(env, &device, "getProductName")?; + let product_name = String::from(product_name.mutf8_chars(env)?); + let device_type = + FromPrimitive::from_i32(call_method_no_args_ret_int(env, &device, "getType")?) + .unwrap_or(AudioDeviceType::Unsupported); + + let is_source = call_method_no_args_ret_bool(env, &device, "isSource")?; + let is_sink = call_method_no_args_ret_bool(env, &device, "isSink")?; + let direction = crate::device_description::direction_from_caps(is_source, is_sink); + let channel_counts = + call_method_no_args_ret_int_array(env, &device, "getChannelCounts")? + .into_boxed_slice(); + let sample_rates = call_method_no_args_ret_int_array(env, &device, "getSampleRates")? + .into_boxed_slice(); + let formats = call_method_no_args_ret_int_array(env, &device, "getEncodings")? + .into_iter() + .filter_map(SampleFormat::from_encoding) + .collect::>(); + + Ok(AudioDeviceInfo { + id, + address, + product_name, + device_type, + direction, + channel_counts, + sample_rates, + formats, + }) + }) + .collect::, _>>() +} diff --git a/crates/vendor/cpal/src/host/aaudio/java_interface/utils.rs b/crates/vendor/cpal/src/host/aaudio/java_interface/utils.rs new file mode 100644 index 000000000..57a8c3fa9 --- /dev/null +++ b/crates/vendor/cpal/src/host/aaudio/java_interface/utils.rs @@ -0,0 +1,194 @@ +use jni::sys::jobject; +pub use jni::{ + errors::Result as JResult, + objects::{JIntArray, JObject, JObjectArray, JString}, + strings::JNIString, + Env, JavaVM, +}; +use ndk_context::AndroidContext; + +pub fn get_context() -> AndroidContext { + ndk_context::android_context() +} + +pub fn with_attached(context: AndroidContext, closure: F) -> JResult +where + for<'j> F: FnOnce(&mut Env<'j>, JObject<'j>) -> JResult, +{ + // jni 0.22: from_raw returns JavaVM directly and asserts non-null, + // so attach_current_thread and the closure are the only fallible steps. + let vm = unsafe { JavaVM::from_raw(context.vm().cast()) }; + let raw_context = context.context() as jobject; + vm.attach_current_thread(|env: &mut Env<'_>| { + let context_obj = unsafe { JObject::from_raw(env, raw_context) }; + closure(env, context_obj) + }) +} + +pub fn call_method_no_args_ret_int_array<'j>( + env: &mut Env<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult> { + let obj = env + .call_method(subject, JNIString::new(method), jni::jni_sig!("()[I"), &[])? + .l()?; + let array: JIntArray<'j> = unsafe { JIntArray::from_raw(env, obj.into_raw()) }; + let length = array.len(env)?; + let mut values = vec![0i32; length]; + array.get_region(env, 0, &mut values)?; + Ok(values) +} + +pub fn call_method_no_args_ret_int<'j>( + env: &mut Env<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult { + env.call_method(subject, JNIString::new(method), jni::jni_sig!("()I"), &[])? + .i() +} + +pub fn call_method_no_args_ret_bool<'j>( + env: &mut Env<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult { + env.call_method(subject, JNIString::new(method), jni::jni_sig!("()Z"), &[])? + .z() +} + +pub fn call_method_no_args_ret_string<'j>( + env: &mut Env<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult> { + let obj = env + .call_method( + subject, + JNIString::new(method), + jni::jni_sig!("()Ljava/lang/String;"), + &[], + )? + .l()?; + Ok(unsafe { JString::from_raw(env, obj.into_raw()) }) +} + +pub fn call_method_no_args_ret_char_sequence<'j>( + env: &mut Env<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult> { + let cseq = env + .call_method( + subject, + JNIString::new(method), + jni::jni_sig!("()Ljava/lang/CharSequence;"), + &[], + )? + .l()?; + + let s_obj = env + .call_method( + &cseq, + jni::jni_str!("toString"), + jni::jni_sig!("()Ljava/lang/String;"), + &[], + )? + .l()?; + Ok(unsafe { JString::from_raw(env, s_obj.into_raw()) }) +} + +pub fn call_method_string_arg_ret_bool<'j>( + env: &mut Env<'j>, + subject: &JObject<'j>, + name: &str, + arg: impl AsRef, +) -> JResult { + let arg_str = env.new_string(arg)?; + env.call_method( + subject, + JNIString::new(name), + jni::jni_sig!("(Ljava/lang/String;)Z"), + &[(&arg_str).into()], + )? + .z() +} + +pub fn call_method_string_arg_ret_object<'j>( + env: &mut Env<'j>, + subject: &JObject<'j>, + method: &str, + arg: &str, +) -> JResult> { + let arg_str = env.new_string(arg)?; + env.call_method( + subject, + JNIString::new(method), + jni::jni_sig!("(Ljava/lang/String;)Ljava/lang/Object;"), + &[(&arg_str).into()], + )? + .l() +} + +pub fn get_package_manager<'j>(env: &mut Env<'j>, subject: &JObject<'j>) -> JResult> { + env.call_method( + subject, + jni::jni_str!("getPackageManager"), + jni::jni_sig!("()Landroid/content/pm/PackageManager;"), + &[], + )? + .l() +} + +pub fn has_system_feature<'j>( + env: &mut Env<'j>, + subject: &JObject<'j>, + name: &str, +) -> JResult { + call_method_string_arg_ret_bool(env, subject, "hasSystemFeature", name) +} + +pub fn get_system_service<'j>( + env: &mut Env<'j>, + subject: &JObject<'j>, + name: &str, +) -> JResult> { + call_method_string_arg_ret_object(env, subject, "getSystemService", name) +} + +/// Read an Android system property +pub fn get_system_property<'j>( + env: &mut Env<'j>, + name: &str, + default_value: &str, +) -> JResult> { + let name_str = env.new_string(name)?; + let default_str = env.new_string(default_value)?; + let obj = env + .call_static_method( + jni::jni_str!("android/os/SystemProperties"), + jni::jni_str!("get"), + jni::jni_sig!("(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"), + &[(&name_str).into(), (&default_str).into()], + )? + .l()?; + Ok(unsafe { JString::from_raw(env, obj.into_raw()) }) +} + +pub fn get_devices<'j>( + env: &mut Env<'j>, + subject: &JObject<'j>, + flags: i32, +) -> JResult> { + let obj = env + .call_method( + subject, + jni::jni_str!("getDevices"), + jni::jni_sig!("(I)[Landroid/media/AudioDeviceInfo;"), + &[flags.into()], + )? + .l()?; + let arr = unsafe { JObjectArray::>::from_raw(env, obj.into_raw()) }; + Ok(arr) +} diff --git a/crates/vendor/cpal/src/host/aaudio/mod.rs b/crates/vendor/cpal/src/host/aaudio/mod.rs new file mode 100644 index 000000000..3b524d057 --- /dev/null +++ b/crates/vendor/cpal/src/host/aaudio/mod.rs @@ -0,0 +1,838 @@ +//! AAudio backend implementation. +//! +//! Default backend on Android. + +use std::{ + fmt, + hash::{Hash, Hasher}, + sync::{ + atomic::{AtomicI32, Ordering}, + Arc, Mutex, + }, + time::Duration, + vec::IntoIter as VecIntoIter, +}; + +use crate::{ + host::{emit_error, equilibrium::fill_equilibrium, ErrorCallbackArc}, + traits::{DeviceTrait, HostTrait, StreamTrait}, + BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, + DeviceId, DeviceType, Error, ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, + InterfaceType, OutputCallbackInfo, OutputStreamTimestamp, ResultExt, SampleFormat, SampleRate, + StreamConfig, StreamInstant, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, +}; + +extern crate ndk; +use self::ndk::audio::AudioStream; +use crate::host::try_emit_error; + +mod convert; +mod java_interface; + +use convert::{input_stream_instant, now_stream_instant, output_stream_instant}; +use java_interface::{AudioDeviceInfo, AudioDeviceType as AndroidDeviceType, AudioManager}; + +impl From for DeviceType { + fn from(device_type: AndroidDeviceType) -> Self { + match device_type { + AndroidDeviceType::BuiltinSpeaker + | AndroidDeviceType::BuiltinSpeakerSafe + | AndroidDeviceType::BleSpeaker => DeviceType::Speaker, + + AndroidDeviceType::BuiltinMic => DeviceType::Microphone, + + AndroidDeviceType::WiredHeadphones => DeviceType::Headphones, + + AndroidDeviceType::WiredHeadset + | AndroidDeviceType::UsbHeadset + | AndroidDeviceType::BleHeadset + | AndroidDeviceType::BluetoothSCO => DeviceType::Headset, + + AndroidDeviceType::BuiltinEarpiece => DeviceType::Earpiece, + + AndroidDeviceType::HearingAid => DeviceType::HearingAid, + + AndroidDeviceType::Dock => DeviceType::Dock, + + AndroidDeviceType::Fm | AndroidDeviceType::FmTuner | AndroidDeviceType::TvTuner => { + DeviceType::Tuner + } + + AndroidDeviceType::RemoteSubmix => DeviceType::Virtual, + + _ => DeviceType::Unknown, + } + } +} + +impl From for InterfaceType { + fn from(device_type: AndroidDeviceType) -> Self { + match device_type { + AndroidDeviceType::UsbDevice + | AndroidDeviceType::UsbAccessory + | AndroidDeviceType::UsbHeadset => InterfaceType::Usb, + + AndroidDeviceType::BluetoothA2DP + | AndroidDeviceType::BluetoothSCO + | AndroidDeviceType::BleHeadset + | AndroidDeviceType::BleSpeaker + | AndroidDeviceType::BleBroadcast => InterfaceType::Bluetooth, + + AndroidDeviceType::Hdmi | AndroidDeviceType::HdmiArc | AndroidDeviceType::HdmiEarc => { + InterfaceType::Hdmi + } + + AndroidDeviceType::LineAnalog + | AndroidDeviceType::LineDigital + | AndroidDeviceType::AuxLine => InterfaceType::Line, + + AndroidDeviceType::BuiltinEarpiece + | AndroidDeviceType::BuiltinMic + | AndroidDeviceType::BuiltinSpeaker + | AndroidDeviceType::BuiltinSpeakerSafe => InterfaceType::BuiltIn, + + AndroidDeviceType::Ip => InterfaceType::Network, + + AndroidDeviceType::RemoteSubmix => InterfaceType::Virtual, + + _ => InterfaceType::Unknown, + } + } +} + +// ITU-R BS.2051 standard surround channel counts; used as fallback when the device does not +// report its own via AudioDeviceInfo.getChannelCounts(). +const DEFAULT_CHANNEL_COUNTS: [i32; 5] = [1, 2, 4, 6, 8]; + +const SAMPLE_RATES: [i32; 15] = [ + 5512, 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000, + 176_400, 192_000, +]; + +/// The same default for blocking operations as Oboe uses +const DEFAULT_TIMEOUT_NANOS: i64 = 2_000_000_000; + +pub struct Host; + +#[derive(Clone, Debug)] +pub struct Device(Option); + +/// Stream wraps AudioStream in Arc> to provide Send + Sync semantics. +/// +/// While the underlying ndk::audio::AudioStream is neither Send nor Sync in ndk 0.9.0 +/// (see https://developer.android.com/ndk/guides/audio/aaudio/aaudio#thread-safety), +/// we wrap it in a mutex to enable safe concurrent access and manually implement Send + Sync. +/// +/// # Safety +/// +/// This is safe because: +/// - AAudio functions are designed to be called from any thread (the Android docs state +/// "AAudio is not thread-safe" meaning it lacks internal locking, not that it's unsafe) +/// - Audio callbacks are called on a dedicated AAudio thread and don't access Stream +/// - The Mutex ensures exclusive access for control operations (play, pause) +/// - The pointer in AudioStream (NonNull) is valid for the lifetime +/// of the stream and AAudio C API functions are thread-safe at the C level +#[derive(Clone)] +pub struct Stream { + inner: Arc>, + direction: DeviceDirection, +} + +// SAFETY: AudioStream can be safely sent between threads. The AAudio C API is thread-safe +// for moving stream ownership between threads. The NonNull pointer remains valid. +unsafe impl Send for Stream {} + +// SAFETY: AudioStream can be safely shared between threads when protected by a Mutex. +// All operations on the stream go through the mutex, ensuring exclusive access. +unsafe impl Sync for Stream {} + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +/// State for dynamic buffer tuning on output streams. +#[derive(Default)] +struct BufferTuningState { + previous_underrun_count: AtomicI32, + capacity: AtomicI32, + mixer_bursts: AtomicI32, +} + +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; +pub type Devices = std::vec::IntoIter; + +impl Host { + pub fn new() -> Result { + Ok(Host) + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + true + } + + fn devices(&self) -> Result { + if let Ok(devices) = AudioDeviceInfo::request(DeviceDirection::Duplex) { + Ok(devices + .into_iter() + .map(|d| Device(Some(d))) + .collect::>() + .into_iter()) + } else { + Ok(vec![Device(None)].into_iter()) + } + } + + fn default_input_device(&self) -> Option { + Some(Device(None)) + } + + fn default_output_device(&self) -> Option { + Some(Device(None)) + } +} + +fn buffer_size_range() -> SupportedBufferSize { + // The valid range for frames_per_data_callback is any positive i32, but the meaningful + // lower bound (frames_per_burst) is only known after open_stream. + SupportedBufferSize::Unknown +} + +fn default_supported_configs() -> VecIntoIter { + const FORMATS: [SampleFormat; 2] = [SampleFormat::I16, SampleFormat::F32]; + + let buffer_size = buffer_size_range(); + let mut output = + Vec::with_capacity(SAMPLE_RATES.len() * DEFAULT_CHANNEL_COUNTS.len() * FORMATS.len()); + for sample_format in &FORMATS { + for channel_count in &DEFAULT_CHANNEL_COUNTS { + for sample_rate in &SAMPLE_RATES { + output.push(SupportedStreamConfigRange { + channels: *channel_count as ChannelCount, + min_sample_rate: *sample_rate as SampleRate, + max_sample_rate: *sample_rate as SampleRate, + buffer_size, + sample_format: *sample_format, + }); + } + } + } + + output.into_iter() +} + +fn device_supported_configs(device: &AudioDeviceInfo) -> VecIntoIter { + let sample_rates: &[i32] = if !device.sample_rates.is_empty() { + &device.sample_rates + } else { + &SAMPLE_RATES + }; + + let channel_counts: &[i32] = if !device.channel_counts.is_empty() { + &device.channel_counts + } else { + &DEFAULT_CHANNEL_COUNTS + }; + + const ALL_FORMATS: [SampleFormat; 2] = [SampleFormat::I16, SampleFormat::F32]; + let formats: &[SampleFormat] = if !device.formats.is_empty() { + &device.formats + } else { + &ALL_FORMATS + }; + + let buffer_size = buffer_size_range(); + let mut output = Vec::with_capacity(sample_rates.len() * channel_counts.len() * formats.len()); + for sample_rate in sample_rates { + for channel_count in channel_counts { + let Ok(channels) = ChannelCount::try_from(*channel_count) else { + continue; + }; + if channels == 0 { + continue; + } + for format in formats { + output.push(SupportedStreamConfigRange { + channels, + min_sample_rate: *sample_rate as SampleRate, + max_sample_rate: *sample_rate as SampleRate, + buffer_size, + sample_format: *format, + }); + } + } + } + + output.into_iter() +} + +fn configure_for_device( + builder: ndk::audio::AudioStreamBuilder, + device: &Device, + config: StreamConfig, +) -> ndk::audio::AudioStreamBuilder { + let mut builder = if let Some(info) = &device.0 { + builder.device_id(info.id) + } else { + builder + }; + builder = builder.sample_rate(config.sample_rate as i32); + + // Following the pattern from Oboe and Google's AAudio, we let AAudio choose the optimal + // callback size dynamically by default. See + // - https://developer.android.com/ndk/reference/group/audio#aaudiostreambuilder_setframesperdatacallback + // - https://developer.android.com/ndk/guides/audio/audio-latency#buffer-size + if let BufferSize::Fixed(size) = config.buffer_size { + // For fixed sizes, the user explicitly wants control over the callback size. + builder = builder + .frames_per_data_callback(size.min(i32::MAX as FrameCount) as i32) + .buffer_capacity_in_frames(size.saturating_mul(2).min(i32::MAX as FrameCount) as i32); + } + + #[cfg(feature = "realtime")] + { + builder = builder.performance_mode(ndk::audio::AudioPerformanceMode::LowLatency); + } + + builder +} + +fn build_input_stream( + device: &Device, + config: StreamConfig, + mut data_callback: D, + error_callback: E, + builder: ndk::audio::AudioStreamBuilder, + sample_format: SampleFormat, +) -> Result +where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, +{ + let builder = configure_for_device(builder, device, config); + let channel_count = config.channels as i32; + let sample_rate = config.sample_rate; + + let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); + let error_callback_for_stream = error_callback.clone(); + let error_callback_for_data = error_callback.clone(); + let error_callback_for_xrun = error_callback.clone(); + let last_xrun_count = Arc::new(AtomicI32::new(0)); + + #[cfg(feature = "realtime")] + let mut rt_checked = false; + #[cfg(feature = "realtime")] + let error_callback_for_rt = error_callback.clone(); + + let stream = builder + .data_callback(Box::new(move |stream, data, num_frames| { + #[cfg(feature = "realtime")] + if !rt_checked { + if stream.performance_mode() != ndk::audio::AudioPerformanceMode::LowLatency { + if try_emit_error( + &error_callback_for_rt, + Error::new(ErrorKind::RealtimeDenied), + ) + .is_ok() + { + rt_checked = true; + } + } else { + rt_checked = true; + } + } + + let xrun_count = stream.x_run_count(); + if last_xrun_count.swap(xrun_count, Ordering::Relaxed) < xrun_count { + let _ = try_emit_error(&error_callback_for_xrun, ErrorKind::Xrun.into()); + } + + let Some(n_samples) = u64::try_from(num_frames) + .ok() + .and_then(|f| f.checked_mul(channel_count as u64)) + .and_then(|n| usize::try_from(n).ok()) + else { + emit_error( + &error_callback_for_data, + Error::with_message( + ErrorKind::BackendError, + format!( + "AAudio provided an invalid frame count in the data callback ({num_frames} frames with {channel_count} channels)", + ), + ), + ); + return ndk::audio::AudioCallbackResult::Stop; + }; + let cb_info = InputCallbackInfo { + timestamp: InputStreamTimestamp { + callback: now_stream_instant(), + capture: input_stream_instant(stream, sample_rate), + }, + }; + (data_callback)( + &unsafe { Data::from_parts(data as *mut _, n_samples, sample_format) }, + &cb_info, + ); + ndk::audio::AudioCallbackResult::Continue + })) + .error_callback(Box::new(move |_stream, error| { + emit_error(&error_callback_for_stream, Error::from(error)); + })) + .open_stream()?; + + // SAFETY: Stream implements Send + Sync (see unsafe impl below). Arc> + // is safe because the Mutex provides exclusive access and AudioStream's thread safety + // is documented in the AAudio C API. + #[allow(clippy::arc_with_non_send_sync)] + Ok(Stream { + inner: Arc::new(Mutex::new(stream)), + direction: DeviceDirection::Input, + }) +} + +fn build_output_stream( + device: &Device, + config: StreamConfig, + mut data_callback: D, + error_callback: E, + builder: ndk::audio::AudioStreamBuilder, + sample_format: SampleFormat, +) -> Result +where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, +{ + let builder = configure_for_device(builder, device, config); + let channel_count = config.channels as i32; + let sample_rate = config.sample_rate; + let tune_dynamically = config.buffer_size == BufferSize::Default; + + let tuning = Arc::new(BufferTuningState::default()); + let tuning_for_callback = tuning.clone(); + + let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); + let error_callback_for_stream = error_callback.clone(); + let error_callback_for_data = error_callback.clone(); + let error_callback_for_xrun = error_callback.clone(); + let last_xrun_count = Arc::new(AtomicI32::new(0)); + + #[cfg(feature = "realtime")] + let mut rt_checked = false; + #[cfg(feature = "realtime")] + let error_callback_for_rt = error_callback.clone(); + + let stream = builder + .data_callback(Box::new(move |stream, data, num_frames| { + #[cfg(feature = "realtime")] + if !rt_checked { + if stream.performance_mode() != ndk::audio::AudioPerformanceMode::LowLatency { + if try_emit_error( + &error_callback_for_rt, + Error::new(ErrorKind::RealtimeDenied), + ) + .is_ok() + { + rt_checked = true; + } + } else { + rt_checked = true; + } + } + + let xrun_count = stream.x_run_count(); + if last_xrun_count.swap(xrun_count, Ordering::Relaxed) < xrun_count { + let _ = try_emit_error(&error_callback_for_xrun, ErrorKind::Xrun.into()); + } + + let Some(n_samples) = u64::try_from(num_frames) + .ok() + .and_then(|f| f.checked_mul(channel_count as u64)) + .and_then(|n| usize::try_from(n).ok()) + else { + emit_error( + &error_callback_for_data, + Error::with_message( + ErrorKind::BackendError, + format!( + "AAudio provided an invalid frame count in the data callback ({num_frames} frames with {channel_count} channels)", + ), + ), + ); + return ndk::audio::AudioCallbackResult::Stop; + }; + + // Pre-fill with equilibrium so unwritten frames are silent. + let byte_count = n_samples * sample_format.sample_size(); + // SAFETY: `data` is the buffer pointer provided by AAudio for this callback. + unsafe { + fill_equilibrium( + std::slice::from_raw_parts_mut(data as *mut u8, byte_count), + sample_format, + ); + } + + // Deliver audio data to user callback + let cb_info = OutputCallbackInfo { + timestamp: OutputStreamTimestamp { + callback: now_stream_instant(), + playback: output_stream_instant(stream, sample_rate), + }, + }; + (data_callback)( + &mut unsafe { Data::from_parts(data as *mut _, n_samples, sample_format) }, + &cb_info, + ); + + // Dynamic buffer tuning for output streams + // See: https://developer.android.com/ndk/guides/audio/aaudio/aaudio#tuning-buffers + if tune_dynamically { + let underrun_count = stream.x_run_count(); + let previous = tuning_for_callback + .previous_underrun_count + .load(Ordering::Relaxed); + + if underrun_count > previous { + // The number of frames per burst can vary dynamically + let mut burst_size = stream.frames_per_burst(); + if burst_size <= 0 { + burst_size = 256; // fallback from AAudio documentation + } else if burst_size < 16 { + burst_size = 16; // floor from Oboe + } + + let new_mixer_bursts = tuning_for_callback + .mixer_bursts + .load(Ordering::Relaxed) + .saturating_add(1); + let mut buffer_size = burst_size * new_mixer_bursts; + + let buffer_capacity = tuning_for_callback.capacity.load(Ordering::Relaxed); + if buffer_size > buffer_capacity { + buffer_size = buffer_capacity; + } + + if stream.set_buffer_size_in_frames(buffer_size).is_ok() { + tuning_for_callback + .mixer_bursts + .store(new_mixer_bursts, Ordering::Relaxed); + } + + tuning_for_callback + .previous_underrun_count + .store(underrun_count, Ordering::Relaxed); + } + } + + ndk::audio::AudioCallbackResult::Continue + })) + .error_callback(Box::new(move |_stream, error| { + emit_error(&error_callback_for_stream, Error::from(error)); + })) + .open_stream()?; + + // After stream opens, query and cache the values + let capacity = stream.buffer_capacity_in_frames(); + tuning.capacity.store(capacity, Ordering::Relaxed); + + let mixer_bursts = match AudioManager::get_mixer_bursts() { + Ok(bursts) => bursts.max(0), + Err(_) => { + let burst_size = stream.frames_per_burst(); + if burst_size > 0 { + stream.buffer_size_in_frames() / burst_size + } else { + 0 // defer to dynamic tuning + } + } + }; + tuning.mixer_bursts.store(mixer_bursts, Ordering::Relaxed); + + // SAFETY: Stream implements Send + Sync (see unsafe impl below). Arc> + // is safe because the Mutex provides exclusive access and AudioStream's thread safety + // is documented in the AAudio C API. + #[allow(clippy::arc_with_non_send_sync)] + Ok(Stream { + inner: Arc::new(Mutex::new(stream)), + direction: DeviceDirection::Output, + }) +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn description(&self) -> Result { + match &self.0 { + None => Ok(DeviceDescriptionBuilder::new("Default Device").build()), + Some(info) => { + let device_type: DeviceType = info.device_type.into(); + let name = match device_type { + DeviceType::Unknown => info.product_name.clone(), + _ => format!("{} ({})", info.product_name, device_type), + }; + let mut builder = DeviceDescriptionBuilder::new(name) + .device_type(device_type) + .interface_type(info.device_type.into()) + .direction(info.direction); + + // Add address if not empty + if !info.address.is_empty() { + builder = builder.address(&info.address); + } + + Ok(builder.build()) + } + } + } + + fn id(&self) -> Result { + let device_str = match &self.0 { + None => "-1".to_string(), // Default device + Some(info) => info.id.to_string(), + }; + Ok(DeviceId::new(crate::platform::HostId::AAudio, device_str)) + } + + fn supported_input_configs(&self) -> Result { + if let Some(info) = &self.0 { + // Output-only devices do not support input + if matches!(info.direction, DeviceDirection::Output) { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Device does not support input", + )); + } + Ok(device_supported_configs(info)) + } else { + Ok(default_supported_configs()) + } + } + + fn supported_output_configs(&self) -> Result { + if let Some(info) = &self.0 { + // Input-only devices do not support output + if matches!(info.direction, DeviceDirection::Input) { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Device does not support output", + )); + } + Ok(device_supported_configs(info)) + } else { + Ok(default_supported_configs()) + } + } + + fn default_input_config(&self) -> Result { + let mut configs: Vec<_> = self.supported_input_configs()?.collect(); + configs.sort_by(|a, b| b.cmp_default_heuristics(a)); + let range = configs.into_iter().next().ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "No supported input configuration", + ) + })?; + let config = range + .try_with_standard_sample_rate() + .unwrap_or_else(|| range.with_max_sample_rate()); + Ok(config) + } + + fn default_output_config(&self) -> Result { + let mut configs: Vec<_> = self.supported_output_configs()?.collect(); + configs.sort_by(|a, b| b.cmp_default_heuristics(a)); + let range = configs.into_iter().next().ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "No supported output configuration", + ) + })?; + let config = range + .try_with_standard_sample_rate() + .unwrap_or_else(|| range.with_max_sample_rate()); + Ok(config) + } + + fn build_input_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + crate::validate_stream_config(&config)?; + if config.sample_rate > i32::MAX as u32 { + return Err(Error::with_message( + ErrorKind::InvalidInput, + format!("sample rate exceeds AAudio's limit of {}", i32::MAX), + )); + } + let format = match sample_format { + SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16, + SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float, + sample_format => { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Sample format {sample_format} is not supported"), + )) + } + }; + let builder = ndk::audio::AudioStreamBuilder::new()? + .direction(ndk::audio::AudioDirection::Input) + .channel_count(config.channels as i32) + .format(format); + + // Keep `capture` monotonic: a transient getTimestamp() failure falls back to `now()` + // (no latency offset), so the next successful read can pull `capture` backward. + let data_callback = crate::host::monotonic_input_callback(data_callback); + build_input_stream( + self, + config, + data_callback, + error_callback, + builder, + sample_format, + ) + } + + fn build_output_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + crate::validate_stream_config(&config)?; + if config.sample_rate > i32::MAX as u32 { + return Err(Error::with_message( + ErrorKind::InvalidInput, + format!("sample rate exceeds AAudio's limit of {}", i32::MAX), + )); + } + let format = match sample_format { + SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16, + SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float, + sample_format => { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Sample format {sample_format} is not supported"), + )) + } + }; + let builder = ndk::audio::AudioStreamBuilder::new()? + .direction(ndk::audio::AudioDirection::Output) + .channel_count(config.channels as i32) + .format(format); + + // Keep `playback` monotonic: a transient getTimestamp() failure falls back to `now()` + // (no latency offset), pulling `playback` backward. + let data_callback = crate::host::monotonic_output_callback(data_callback); + build_output_stream( + self, + config, + data_callback, + error_callback, + builder, + sample_format, + ) + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + match (&self.0, &other.0) { + (None, None) => true, + (Some(a), Some(b)) => a.id == b.id, + _ => false, + } + } +} + +impl Eq for Device {} + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let desc = self.description().map_err(|_| fmt::Error)?; + f.write_str(desc.name()) + } +} + +impl Hash for Device { + fn hash(&self, state: &mut H) { + self.0.as_ref().map(|i| i.id).hash(state); + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), Error> { + let stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") + })?; + + stream.request_start().context("Failed to start stream")?; + stream + .wait_for_state_change( + ndk::audio::AudioStreamState::Starting, + DEFAULT_TIMEOUT_NANOS, + ) + .map(|_| ()) + .context("Failed to wait for stream to start") + } + + fn pause(&self) -> Result<(), Error> { + match self.direction { + DeviceDirection::Output => { + let stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") + })?; + + stream.request_pause().context("Failed to pause stream")?; + stream + .wait_for_state_change( + ndk::audio::AudioStreamState::Pausing, + DEFAULT_TIMEOUT_NANOS, + ) + .map(|_| ()) + .context("Failed to wait for stream to pause") + } + _ => Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Pause is not supported on input streams", + )), + } + } + + fn now(&self) -> StreamInstant { + now_stream_instant() + } + + fn buffer_size(&self) -> Result { + let stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") + })?; + + // frames_per_data_callback is only set for BufferSize::Fixed; for Default AAudio + // schedules callbacks at the burst size, so that is the best available estimate. + let frames = match stream.frames_per_data_callback() { + Some(size) if size > 0 => size, + _ => stream.frames_per_burst(), + }; + Ok(frames as FrameCount) + } +} diff --git a/crates/vendor/cpal/src/host/alsa/enumerate.rs b/crates/vendor/cpal/src/host/alsa/enumerate.rs new file mode 100644 index 000000000..d9bd648e0 --- /dev/null +++ b/crates/vendor/cpal/src/host/alsa/enumerate.rs @@ -0,0 +1,157 @@ +use std::collections::HashSet; + +use super::{alsa, Device, Host}; +use crate::{DeviceDirection, Error}; + +const HW_PREFIX: &str = "hw"; +const PLUGHW_PREFIX: &str = "plughw"; + +/// Information about a physical device +struct PhysicalDevice { + card_index: u32, + card_name: Option, + device_index: u32, + device_name: Option, + direction: DeviceDirection, +} + +/// Iterator over available ALSA PCM devices (physical hardware and virtual/plugin devices). +pub type Devices = std::vec::IntoIter; + +impl Host { + /// Enumerates all available ALSA PCM devices (physical hardware and virtual/plugin devices). + /// + /// We enumerate both ALSA hints and physical devices because: + /// - Hints provide virtual devices, user configs, and card-specific devices with metadata + /// - Physical probing provides traditional numeric naming (hw:CARD=0,DEV=0) for compatibility + pub(super) fn enumerate_devices(&self) -> Result { + let mut devices = Vec::new(); + let mut seen_pcm_ids = HashSet::new(); + + let physical_devices = physical_devices(); + + // Add all hint devices, including virtual devices + if let Ok(hints) = alsa::device_name::HintIter::new_str(None, "pcm") { + for hint in hints { + if let Some(pcm_id) = hint.name { + // Per ALSA docs (https://alsa-project.org/alsa-doc/alsa-lib/group___hint.html), + // NULL IOID means both Input/Output. Whether a stream can actually open in a + // given direction can only be determined by attempting to open it. + let direction = hint.direction.map_or(DeviceDirection::Duplex, Into::into); + seen_pcm_ids.insert(pcm_id.clone()); + let device = Device { + pcm_id, + desc: hint.desc, + direction, + _context: self.inner.clone(), + }; + + devices.push(device); + } + } + } + + // Add hw:/plughw: for all physical devices with numeric index (traditional naming) + for phys_dev in physical_devices { + for prefix in [HW_PREFIX, PLUGHW_PREFIX] { + let pcm_id = format!( + "{}:CARD={},DEV={}", + prefix, phys_dev.card_index, phys_dev.device_index + ); + + if seen_pcm_ids.insert(pcm_id.clone()) { + devices.push(Device { + pcm_id, + desc: Some(format_device_description(&phys_dev, prefix)), + direction: phys_dev.direction, + _context: self.inner.clone(), + }); + } + } + } + + Ok(devices.into_iter()) + } +} + +/// Formats device description in ALSA style: "Card Name, Device Name\nPurpose" +fn format_device_description(phys_dev: &PhysicalDevice, prefix: &str) -> String { + // "Card Name, Device Name" or variations + let first_line = match (&phys_dev.card_name, &phys_dev.device_name) { + (Some(card), Some(device)) => format!("{}, {}", card, device), + (Some(card), None) => card.clone(), + (None, Some(device)) => device.clone(), + (None, None) => format!("Card {}", phys_dev.card_index), + }; + + // ALSA standard description + let second_line = match prefix { + HW_PREFIX => "Direct hardware device without any conversions", + PLUGHW_PREFIX => "Hardware device with all software conversions", + _ => "", + }; + + format!("{}\n{}", first_line, second_line) +} + +fn physical_devices() -> Vec { + let mut devices = Vec::new(); + for card in alsa::card::Iter::new().filter_map(Result::ok) { + let card_index = card.get_index() as u32; + let ctl = match alsa::Ctl::new(&format!("{}:{}", HW_PREFIX, card_index), false) { + Ok(ctl) => ctl, + Err(_) => continue, + }; + let card_name = ctl + .card_info() + .ok() + .and_then(|info| info.get_name().ok().map(|s| s.to_owned())); + + for device_index in alsa::ctl::DeviceIter::new(&ctl) { + let device_index = device_index as u32; + let playback_info = ctl + .pcm_info(device_index, 0, alsa::Direction::Playback) + .ok(); + let capture_info = ctl.pcm_info(device_index, 0, alsa::Direction::Capture).ok(); + + let (direction, device_name) = match (&playback_info, &capture_info) { + (Some(p_info), Some(_c_info)) => ( + DeviceDirection::Duplex, + p_info.get_name().ok().map(|s| s.to_owned()), + ), + (Some(p_info), None) => ( + DeviceDirection::Output, + p_info.get_name().ok().map(|s| s.to_owned()), + ), + (None, Some(c_info)) => ( + DeviceDirection::Input, + c_info.get_name().ok().map(|s| s.to_owned()), + ), + (None, None) => { + // Device doesn't exist - skip + continue; + } + }; + + let device_name = device_name.unwrap_or_else(|| format!("Device {}", device_index)); + devices.push(PhysicalDevice { + card_index, + card_name: card_name.clone(), + device_index, + device_name: Some(device_name), + direction, + }); + } + } + + devices +} + +impl From for DeviceDirection { + fn from(direction: alsa::Direction) -> Self { + match direction { + alsa::Direction::Playback => DeviceDirection::Output, + alsa::Direction::Capture => DeviceDirection::Input, + } + } +} diff --git a/crates/vendor/cpal/src/host/alsa/mod.rs b/crates/vendor/cpal/src/host/alsa/mod.rs new file mode 100644 index 000000000..018e6ddd3 --- /dev/null +++ b/crates/vendor/cpal/src/host/alsa/mod.rs @@ -0,0 +1,1725 @@ +//! ALSA backend implementation. +//! +//! Default backend on Linux and BSD systems. + +extern crate alsa; +#[cfg(feature = "realtime")] +extern crate alsa_sys; +extern crate libc; + +use std::{ + collections::HashMap, + fmt, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, + thread::{self, JoinHandle}, + time::Duration, + vec::IntoIter as VecIntoIter, +}; + +use self::alsa::poll::Descriptors; +pub use self::enumerate::Devices; +use crate::{ + host::{ + equilibrium::{fill_equilibrium, DSD_EQUILIBRIUM_BYTE, U8_EQUILIBRIUM_BYTE}, + frames_to_duration, + latch::Latch, + }, + iter::{SupportedInputConfigs, SupportedOutputConfigs}, + traits::{DeviceTrait, HostTrait, StreamTrait}, + BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, + DeviceId, Error, ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, + OutputCallbackInfo, OutputStreamTimestamp, SampleFormat, SampleRate, StreamConfig, + StreamInstant, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + COMMON_SAMPLE_RATES, +}; + +mod enumerate; + +// ALSA Buffer Size Behavior +// ========================= +// +// ## ALSA Latency Model +// +// **Hardware vs Software Buffer**: ALSA maintains a software buffer in memory that feeds +// a hardware buffer in the audio device. Audio latency is determined by how much data +// sits in the software buffer before being transferred to hardware. +// +// **Period-Based Transfer**: ALSA transfers data in chunks called "periods". When one +// period worth of data has been consumed by hardware, ALSA triggers a callback to refill +// that period in the software buffer. +// +// ## BufferSize::Fixed Behavior +// +// When `BufferSize::Fixed(x)` is specified, cpal attempts to configure the period size +// to approximately `x` frames to achieve the requested callback size. However, the +// actual callback size may differ from the request: +// +// - ALSA may round the period size to hardware-supported values +// - Different devices have different period size constraints +// - The callback size is not guaranteed to exactly match the request +// - If the requested size cannot be accommodated, ALSA will choose the nearest +// supported configuration +// +// This mirrors the behavior documented in the cpal API where `BufferSize::Fixed(x)` +// requests but does not guarantee a specific callback size. +// +// ## BufferSize::Default Behavior +// +// When `BufferSize::Default` is specified, cpal does NOT set explicit period size or +// period count constraints, allowing the device/driver to choose sensible defaults. +// +// **Why not set defaults?** Different audio systems have different behaviors: +// +// - **Native ALSA hardware**: Typically chooses reasonable defaults (e.g., 512-2048 +// frame periods with 2-4 periods) +// +// - **PipeWire-ALSA plugin**: Allocates a large ring buffer (~1M frames at 48kHz) but +// uses small periods (512-1024 frames). Critically, if you request `set_periods(2)` +// without specifying period size, PipeWire calculates period = buffer/2, resulting +// in pathologically large periods (~524K frames = 10 seconds). See issues #1029 and +// #1036. +// +// By not constraining period configuration, PipeWire-ALSA can use its optimized defaults +// (small periods with many-period buffer), while native ALSA hardware uses its own defaults. +// +// **Startup latency**: Regardless of buffer size, cpal uses double-buffering for startup +// (start_threshold = 2 periods), ensuring low latency even with large multi-period ring +// buffers. + +const DEFAULT_DEVICE: &str = "default"; +const DEFAULT_PERIODS: alsa::pcm::Frames = 2; + +// Some ALSA plugins (e.g. alsaequal, certain USB drivers) are not reentrant. +static ALSA_OPEN_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); + +fn open_pcm(pcm_id: &str, direction: alsa::Direction) -> Result { + let _guard = ALSA_OPEN_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + alsa::pcm::PCM::new(pcm_id, direction, true).map_err(|e| { + let e = Error::from(e); + if e.kind() == ErrorKind::UnsupportedConfig { + let dir = match direction { + alsa::Direction::Capture => "input", + alsa::Direction::Playback => "output", + }; + Error::with_message( + ErrorKind::UnsupportedOperation, + format!("Device does not support {dir}"), + ) + } else { + e + } + }) +} + +// TODO: Not yet defined in rust-lang/libc crate +const LIBC_ENOTSUPP: libc::c_int = 524; + +/// The default Linux and BSD host type. +#[derive(Debug, Clone)] +pub struct Host { + inner: Arc, +} + +impl Host { + pub fn new() -> Result { + let inner = AlsaContext::new().map_err(|e| { + Error::with_message( + ErrorKind::HostUnavailable, + format!("ALSA is not available: {e}"), + ) + })?; + Ok(Self { + inner: Arc::new(inner), + }) + } + + // "default" is a virtual ALSA device that redirects to the configured default. We cannot + // determine its actual capabilities without opening it, so we return Unknown direction. + fn default_device(&self) -> Device { + Device { + pcm_id: DEFAULT_DEVICE.to_owned(), + desc: Some("Default Audio Device".to_owned()), + direction: DeviceDirection::Unknown, + _context: self.inner.clone(), + } + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + // Assume ALSA is always available on Linux and BSD. + true + } + + fn devices(&self) -> Result { + self.enumerate_devices() + } + + fn device_by_id(&self, id: &DeviceId) -> Option { + let canonical_id = DeviceId::new(id.host(), canonical_pcm_id(id.id())); + self.devices() + .ok()? + .find(|d| d.id().ok().as_ref() == Some(&canonical_id)) + } + + fn default_input_device(&self) -> Option { + Some(self.default_device()) + } + + fn default_output_device(&self) -> Option { + Some(self.default_device()) + } +} + +/// Global count of active ALSA context instances. +static ALSA_CONTEXT_COUNT: Mutex = Mutex::new(0); + +/// ALSA backend context shared between `Host`, `Device`, and `Stream` via `Arc`. +#[derive(Debug)] +pub(super) struct AlsaContext; + +impl AlsaContext { + fn new() -> Result { + let mut count = ALSA_CONTEXT_COUNT.lock().unwrap_or_else(|e| e.into_inner()); + if *count == 0 { + alsa::config::update()?; + } + *count += 1; + Ok(Self) + } +} + +impl Drop for AlsaContext { + fn drop(&mut self) { + let mut count = ALSA_CONTEXT_COUNT.lock().unwrap_or_else(|e| e.into_inner()); + *count = count.saturating_sub(1); + if *count == 0 { + let _ = alsa::config::update_free_global(); + } + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn description(&self) -> Result { + Self::description(self) + } + + fn id(&self) -> Result { + Self::id(self) + } + + // Override trait defaults to avoid opening devices during enumeration. + // + // ALSA does not guarantee transactional cleanup on failed snd_pcm_open(). Opening plugins like + // alsaequal that fail with EPERM can leak FDs, poisoning the ALSA backend for the process + // lifetime (subsequent device opens fail with EBUSY until process exit). + fn supports_input(&self) -> bool { + matches!( + self.direction, + DeviceDirection::Input | DeviceDirection::Duplex + ) + } + + fn supports_output(&self) -> bool { + matches!( + self.direction, + DeviceDirection::Output | DeviceDirection::Duplex + ) + } + + fn supported_input_configs(&self) -> Result { + Self::supported_input_configs(self) + } + + fn supported_output_configs(&self) -> Result { + Self::supported_output_configs(self) + } + + fn default_input_config(&self) -> Result { + Self::default_input_config(self) + } + + fn default_output_config(&self) -> Result { + Self::default_output_config(self) + } + + fn build_input_stream_raw( + &self, + conf: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + // Keep `capture` monotonic: avail_delay() varies between cycles, and a capture overrun + // can make it jump up enough to pull `capture` backward. + let data_callback = crate::host::monotonic_input_callback(data_callback); + let stream_inner = + self.build_stream_inner(conf, sample_format, alsa::Direction::Capture)?; + let stream = Self::Stream::new_input( + Arc::new(stream_inner), + data_callback, + error_callback, + timeout, + ); + Ok(stream) + } + + fn build_output_stream_raw( + &self, + conf: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + // Keep `playback` monotonic: avail_delay() varies between cycles, and a playback + // underrun can drain the buffer enough to pull `playback` backward. + let data_callback = crate::host::monotonic_output_callback(data_callback); + let stream_inner = + self.build_stream_inner(conf, sample_format, alsa::Direction::Playback)?; + let stream = Self::Stream::new_output( + Arc::new(stream_inner), + data_callback, + error_callback, + timeout, + ); + Ok(stream) + } +} + +#[derive(Debug)] +struct TriggerSender(libc::c_int); + +#[derive(Debug)] +struct TriggerReceiver(libc::c_int); + +impl TriggerSender { + fn wakeup(&self) { + let buf = 1u64; + loop { + let ret = unsafe { libc::write(self.0, &buf as *const u64 as *const _, 8) }; + if ret == 8 { + return; + } + // write() can be interrupted by a signal before writing any bytes; retry. + assert_eq!(ret, -1, "wakeup: unexpected return value {ret}"); + let err = std::io::Error::last_os_error(); + if err.kind() != std::io::ErrorKind::Interrupted { + panic!("wakeup: {err}"); + } + } + } +} + +impl TriggerReceiver { + fn clear_pipe(&self) { + let mut out = 0u64; + loop { + let ret = unsafe { libc::read(self.0, &mut out as *mut u64 as *mut _, 8) }; + if ret == 8 { + return; + } + // read() can be interrupted by a signal before reading any bytes; retry. + assert_eq!(ret, -1, "clear_pipe: unexpected return value {ret}"); + let err = std::io::Error::last_os_error(); + if err.kind() != std::io::ErrorKind::Interrupted { + panic!("clear_pipe: {err}"); + } + } + } +} + +fn trigger() -> (TriggerSender, Arc) { + let mut fds = [0, 0]; + match unsafe { libc::pipe(fds.as_mut_ptr()) } { + 0 => (TriggerSender(fds[1]), Arc::new(TriggerReceiver(fds[0]))), + _ => panic!("Could not create pipe"), + } +} + +impl Drop for TriggerSender { + fn drop(&mut self) { + unsafe { + libc::close(self.0); + } + } +} + +impl Drop for TriggerReceiver { + fn drop(&mut self) { + unsafe { + libc::close(self.0); + } + } +} + +#[derive(Clone, Debug)] +pub struct Device { + pcm_id: String, + desc: Option, + direction: DeviceDirection, + _context: Arc, +} + +impl Device { + fn build_stream_inner( + &self, + conf: StreamConfig, + sample_format: SampleFormat, + stream_type: alsa::Direction, + ) -> Result { + crate::validate_stream_config(&conf)?; + + let handle = open_pcm(&self.pcm_id, stream_type)?; + + let hw_params = set_hw_params_from_format(&handle, conf, sample_format)?; + let (buffer_size, period_size) = set_sw_params_from_format(&handle, stream_type)?; + if buffer_size == 0 || period_size == 0 { + return Err(ErrorKind::DeviceNotAvailable.into()); + } + + handle.prepare()?; + + if handle.count() == 0 { + return Err(ErrorKind::DeviceNotAvailable.into()); + } + + // A zero get_htstamp() at prepare time indicates the device does not support hardware timestamps (e.g. PulseAudio ALSA plugin). + // Related: https://bugs.freedesktop.org/show_bug.cgi?id=88503 + let creation_ts = handle.status()?.get_htstamp(); + let timestamp_mode = if creation_ts.tv_sec == 0 && creation_ts.tv_nsec == 0 { + TimestampMode::CreationInstant + } else if hw_params.supports_audio_ts_type(alsa::pcm::AudioTstampType::LinkSynchronized) { + TimestampMode::AudioLink + } else { + TimestampMode::SystemClock + }; + drop(hw_params); + + let period_size = period_size as usize; + let frame_size = sample_format.sample_size() * conf.channels as usize; + + let stream_inner = StreamInner { + dropping: AtomicBool::new(false), + direction: stream_type.into(), + handle, + sample_format, + sample_rate: conf.sample_rate, + frame_size, + period_size, + period_samples: period_size * conf.channels as usize, + equilibrium: EquilibriumFill::new(sample_format, period_size * frame_size), + timestamp_mode, + creation_ts, + creation_instant: std::time::Instant::now(), + _context: self._context.clone(), + }; + + Ok(stream_inner) + } + + fn description(&self) -> Result { + let name = self + .desc + .as_ref() + .and_then(|desc| desc.lines().next()) + .unwrap_or(self.pcm_id.as_str()); + + let mut builder = DeviceDescriptionBuilder::new(name) + .driver(self.pcm_id.as_str()) + .direction(self.direction); + + if let Some(ref desc) = self.desc { + builder = builder.extended(desc.lines().map(|l| l.trim()).filter(|l| !l.is_empty())); + } + + Ok(builder.build()) + } + + fn id(&self) -> Result { + Ok(DeviceId::new(crate::platform::HostId::Alsa, &self.pcm_id)) + } + + fn supported_configs( + &self, + stream_t: alsa::Direction, + ) -> Result, Error> { + let pcm = open_pcm(&self.pcm_id, stream_t)?; + + let hw_params = alsa::pcm::HwParams::any(&pcm)?; + + // Test both LE and BE formats to detect what the hardware actually supports. + // LE is listed first as it's the common case for most audio hardware. + // Hardware reports its supported formats regardless of CPU endianness. + const FORMATS: [(SampleFormat, alsa::pcm::Format); 23] = [ + (SampleFormat::I8, alsa::pcm::Format::S8), + (SampleFormat::U8, alsa::pcm::Format::U8), + (SampleFormat::I16, alsa::pcm::Format::S16LE), + (SampleFormat::I16, alsa::pcm::Format::S16BE), + (SampleFormat::U16, alsa::pcm::Format::U16LE), + (SampleFormat::U16, alsa::pcm::Format::U16BE), + (SampleFormat::I24, alsa::pcm::Format::S24LE), + (SampleFormat::I24, alsa::pcm::Format::S24BE), + (SampleFormat::U24, alsa::pcm::Format::U24LE), + (SampleFormat::U24, alsa::pcm::Format::U24BE), + (SampleFormat::I32, alsa::pcm::Format::S32LE), + (SampleFormat::I32, alsa::pcm::Format::S32BE), + (SampleFormat::U32, alsa::pcm::Format::U32LE), + (SampleFormat::U32, alsa::pcm::Format::U32BE), + (SampleFormat::F32, alsa::pcm::Format::FloatLE), + (SampleFormat::F32, alsa::pcm::Format::FloatBE), + (SampleFormat::F64, alsa::pcm::Format::Float64LE), + (SampleFormat::F64, alsa::pcm::Format::Float64BE), + (SampleFormat::DsdU8, alsa::pcm::Format::DSDU8), + (SampleFormat::DsdU16, alsa::pcm::Format::DSDU16LE), + (SampleFormat::DsdU16, alsa::pcm::Format::DSDU16BE), + (SampleFormat::DsdU32, alsa::pcm::Format::DSDU32LE), + (SampleFormat::DsdU32, alsa::pcm::Format::DSDU32BE), + //SND_PCM_FORMAT_IEC958_SUBFRAME_LE, + //SND_PCM_FORMAT_IEC958_SUBFRAME_BE, + //SND_PCM_FORMAT_MU_LAW, + //SND_PCM_FORMAT_A_LAW, + //SND_PCM_FORMAT_IMA_ADPCM, + //SND_PCM_FORMAT_MPEG, + //SND_PCM_FORMAT_GSM, + //SND_PCM_FORMAT_SPECIAL, + //SND_PCM_FORMAT_S24_3LE, + //SND_PCM_FORMAT_S24_3BE, + //SND_PCM_FORMAT_U24_3LE, + //SND_PCM_FORMAT_U24_3BE, + //SND_PCM_FORMAT_S20_3LE, + //SND_PCM_FORMAT_S20_3BE, + //SND_PCM_FORMAT_U20_3LE, + //SND_PCM_FORMAT_U20_3BE, + //SND_PCM_FORMAT_S18_3LE, + //SND_PCM_FORMAT_S18_3BE, + //SND_PCM_FORMAT_U18_3LE, + //SND_PCM_FORMAT_U18_3BE, + ]; + + let min_rate = hw_params.get_rate_min()?; + let max_rate = hw_params.get_rate_max()?; + + let sample_rates = if min_rate == max_rate || hw_params.test_rate(min_rate + 1).is_ok() { + // Fixed rate or continuous range. + vec![(min_rate, max_rate)] + } else { + // Discrete rates: probe the standard list plus the hardware's own min and max so + // that rates outside `COMMON_SAMPLE_RATES` are not missed. + let mut probe: Vec = COMMON_SAMPLE_RATES.to_vec(); + probe.push(min_rate); + probe.push(max_rate); + probe.sort_unstable(); + probe.dedup(); + probe + .into_iter() + .filter(|&r| (min_rate..=max_rate).contains(&r) && hw_params.test_rate(r).is_ok()) + .map(|r| (r, r)) + .collect() + }; + + let min_channels = hw_params.get_channels_min()?; + // 64 = AES10 (MADI) maximum; also prevents spinning on plugins like plughw that report u32::MAX. + const CHANNEL_ENUM_CAP: u32 = 64; + let max_channels = hw_params + .get_channels_max()? + .min(CHANNEL_ENUM_CAP) + .min(ChannelCount::MAX as u32); + + let supported_channels: Vec = + if min_channels == max_channels || hw_params.test_channels(min_channels + 1).is_ok() { + (min_channels..=max_channels) + .map(|c| c as ChannelCount) + .collect() + } else { + (min_channels..=max_channels) + .filter(|&c| hw_params.test_channels(c).is_ok()) + .map(|c| c as ChannelCount) + .collect() + }; + + let mut output = + Vec::with_capacity(FORMATS.len() * supported_channels.len() * sample_rates.len()); + let mut seen_formats: Vec = Vec::with_capacity(FORMATS.len()); + + // Key: (channels, physical width in bits) with 4 physical widths (8/16/32/64 bits) + let mut buffer_size_cache: HashMap<(ChannelCount, u32), SupportedBufferSize> = + HashMap::with_capacity(supported_channels.len() * 4); + + for &(sample_format, alsa_format) in FORMATS.iter() { + if seen_formats.contains(&sample_format) || hw_params.test_format(alsa_format).is_err() + { + continue; + } + seen_formats.push(sample_format); + let width = alsa_format.physical_width().unwrap_or(0) as u32; + + for &channels in &supported_channels { + let buffer_size = + *buffer_size_cache + .entry((channels, width)) + .or_insert_with(|| { + supported_period_size_range(&hw_params, alsa_format, channels) + }); + + for &(min_rate, max_rate) in sample_rates.iter() { + output.push(SupportedStreamConfigRange { + channels, + min_sample_rate: min_rate, + max_sample_rate: max_rate, + buffer_size, + sample_format, + }); + } + } + } + + Ok(output.into_iter()) + } + + fn supported_input_configs(&self) -> Result { + self.supported_configs(alsa::Direction::Capture) + } + + fn supported_output_configs(&self) -> Result { + self.supported_configs(alsa::Direction::Playback) + } + + // ALSA does not offer default stream formats, so instead we compare all supported formats by + // the `SupportedStreamConfigRange::cmp_default_heuristics` order and select the greatest. + fn default_config(&self, stream_t: alsa::Direction) -> Result { + let mut formats: Vec<_> = self.supported_configs(stream_t)?.collect(); + + formats.sort_by(|a, b| a.cmp_default_heuristics(b)); + + match formats.into_iter().next_back() { + Some(f) => Ok(f + .try_with_standard_sample_rate() + .unwrap_or_else(|| f.with_max_sample_rate())), + None => Err(Error::with_message( + ErrorKind::UnsupportedConfig, + "No supported configuration", + )), + } + } + + fn default_input_config(&self) -> Result { + self.default_config(alsa::Direction::Capture) + } + + fn default_output_config(&self) -> Result { + self.default_config(alsa::Direction::Playback) + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + self.pcm_id == other.pcm_id + } +} + +impl Eq for Device {} + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let desc = self.description().map_err(|_| fmt::Error)?; + f.write_str(desc.name()) + } +} + +impl std::hash::Hash for Device { + fn hash(&self, state: &mut H) { + self.pcm_id.hash(state); + } +} + +/// Strategy for pre-filling an output buffer with the equilibrium value. +#[derive(Debug)] +enum EquilibriumFill { + /// Equilibrium is represented as a single repeating byte value. + Byte(u8), + /// A period-sized buffer pre-filled with the equilibrium value. + Template(Box<[u8]>), +} + +impl EquilibriumFill { + /// Compute the equilibrium-fill strategy for the given sample format at stream creation. + fn new(sample_format: SampleFormat, period_bytes: usize) -> Self { + if sample_format.is_int() || sample_format.is_float() { + Self::Byte(0) + } else if sample_format == SampleFormat::U8 { + Self::Byte(U8_EQUILIBRIUM_BYTE) + } else if sample_format.is_dsd() { + Self::Byte(DSD_EQUILIBRIUM_BYTE) + } else { + // Multi-byte unsigned integer formats require a fill equal to the midpoint of their + // range. + debug_assert!(sample_format.is_uint()); + let mut template = vec![0u8; period_bytes].into_boxed_slice(); + fill_equilibrium(&mut template, sample_format); + Self::Template(template) + } + } + + #[inline] + fn fill(&self, buffer: &mut [u8]) { + match self { + Self::Byte(b) => buffer.fill(*b), + Self::Template(t) => buffer.copy_from_slice(t), + } + } +} + +// How callback timestamps are produced. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TimestampMode { + // Hardware timestamps are unavailable (e.g. PulseAudio ALSA plugin returns zero htstamp). + // Timestamps are monotonic elapsed time since stream creation, sourced from Instant::now(). + CreationInstant, + + // The kernel records the monotonic clock at each DMA interrupt in htstamp. + // Subtracting creation_ts (same clock, captured at prepare time) gives elapsed time + // since stream creation. Uses CLOCK_MONOTONIC_RAW when available, CLOCK_MONOTONIC otherwise. + SystemClock, + + // The hardware maps the audio sample counter to CLOCK_MONOTONIC_RAW via TSC + // cross-timestamps (LinkSynchronized), giving a timestamp that tracks the actual audio + // clock rather than DMA interrupt delivery time. Higher fidelity than SystemClock. + AudioLink, +} + +#[derive(Debug)] +struct StreamInner { + // Flag used to check when to stop polling, regardless of the state of the stream + // (e.g. broken due to a disconnected device). + dropping: AtomicBool, + + // Stream direction. + direction: DeviceDirection, + + // The ALSA handle. + handle: alsa::pcm::PCM, + + // Format of the samples. + sample_format: SampleFormat, + + // Sample rate of the stream. + sample_rate: SampleRate, + + // Cached values for performance in audio callback hot path. + frame_size: usize, + period_size: usize, + period_samples: usize, + equilibrium: EquilibriumFill, + + // How callback timestamps are produced. + timestamp_mode: TimestampMode, + + // htstamp value from the status query at prepare() time. + // Used as the creation-time anchor for SystemClock and AudioLink calculations. + creation_ts: libc::timespec, + + // Monotonic instant captured at stream creation. Timestamp origin for CreationInstant + // mode and last-resort fallback if the status query in now() fails. + creation_instant: std::time::Instant, + + // Keep ALSA context alive to prevent premature ALSA config cleanup. + _context: Arc, +} + +// Assume that the ALSA library is built with thread safe option. +unsafe impl Sync for StreamInner {} + +#[derive(Debug)] +pub struct Stream { + /// The high-priority audio processing thread calling callbacks. + /// Option used for moving out in destructor. + thread: Option>, + + /// Handle to the underlying stream for playback controls. + inner: Arc, + + /// Used to signal to stop processing. + trigger: TriggerSender, + + /// Keeps the read end of the self-pipe alive for the lifetime of the Stream, so that + /// `trigger.wakeup()` never writes to a closed pipe, even if the worker exited early. + _rx: Arc, + + /// Latch that blocks the worker thread until `play()` is called for the first time. + latch: Latch, +} + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +impl StreamInner { + #[inline] + fn callback_instant(&self, status: &alsa::pcm::Status) -> StreamInstant { + // For playback the PCM starts in PREPARED state while the output buffer fills; + // snd_pcm_start() fires automatically at start_threshold, moving it to RUNNING. + // Therefore, callbacks arrive before RUNNING state. Using creation_ts as the + // anchor for all modes means timestamps advance monotonically through both the + // initial buffer fill and any later xrun recovery. + match self.timestamp_mode { + TimestampMode::CreationInstant => { + let d = std::time::Instant::now().duration_since(self.creation_instant); + StreamInstant::new(d.as_secs(), d.subsec_nanos()) + } + TimestampMode::SystemClock => { + // htstamp is the time of the most recent DMA interrupt on the configured + // monotonic clock. Subtracting creation_ts (same clock, prepare() time) + // gives elapsed time since stream creation in any PCM state. + htstamp_elapsed(status, self.creation_ts) + } + TimestampMode::AudioLink => { + // audio_htstamp measures elapsed time since snd_pcm_start() via hardware + // sample counter and TSC cross-timestamp, so it is only valid in RUNNING state. + if status.get_state() != alsa::pcm::State::Running { + // After xrun recovery, snd_pcm_prepare() does not reset trigger_htstamp + // (only snd_pcm_start() does), so it keeps its pre-xrun value while the + // hardware counter has not yet restarted. + htstamp_elapsed(status, self.creation_ts) + } else { + // When running, add (trigger_ts − creation_ts) to express elapsed time + // since stream creation rather than since the last snd_pcm_start(). + let trigger_ts = status.get_trigger_htstamp(); + let trigger_offset = timespec_diff_nanos(trigger_ts, self.creation_ts); + if trigger_offset < 0 { + // trigger_ts predates creation_ts (driver bug); fall back to + // htstamp − creation_ts to preserve a monotone result. + htstamp_elapsed(status, self.creation_ts) + } else { + let audio_ts = status.get_audio_htstamp(); + let nanos = timespec_to_nanos(audio_ts) + trigger_offset; + StreamInstant::from_nanos(nanos as u64) + } + } + } + } + } + + #[cfg(feature = "realtime")] + fn is_rt_eligible(&self) -> bool { + use alsa_sys::*; + // SAFETY: `alsa::pcm::PCM` is `pub struct PCM(*mut snd_pcm_t, Cell)`. The crate + // does not expose a public `as_ptr()`, but we can cast and read from it. + // TODO: replace with `self.handle.as_ptr()` once alsa-rs exposes it publicly. + let raw = unsafe { + (&self.handle as *const alsa::pcm::PCM) + .cast::<*mut snd_pcm_t>() + .read() + }; + let pcm_type = unsafe { snd_pcm_type(raw) }; + + // Only attempt RT promotion for types known not to spin and not to chain to a + // server-backed backend. Therefore, we exclude: + // - NULL: always-ready poll() spins and exhausts RLIMIT_RTTIME, causing SIGXCPU. + // - IOPLUG/EXTPLUG: may route to PulseAudio, causing priority inversion and SIGXCPU. + // - HOOKS, SOFTVOL, PLUG, RATE, ROUTE, COPY: that can chain to either of the above. + matches!( + pcm_type, + SND_PCM_TYPE_HW + | SND_PCM_TYPE_LINEAR + | SND_PCM_TYPE_ALAW + | SND_PCM_TYPE_MULAW + | SND_PCM_TYPE_ADPCM + | SND_PCM_TYPE_LINEAR_FLOAT + | SND_PCM_TYPE_IEC958 + ) + } +} + +struct StreamWorkerContext { + descriptors: Box<[libc::pollfd]>, + transfer_buffer: Box<[u8]>, + poll_timeout: i32, +} + +impl StreamWorkerContext { + fn new(poll_timeout: &Option, stream: &StreamInner, rx: &TriggerReceiver) -> Self { + let poll_timeout: i32 = if let Some(d) = poll_timeout { + // Round up: a nonzero sub-millisecond timeout must not floor to 0 (a non-blocking poll), + // but an explicit Duration::ZERO stays 0 so a non-blocking poll can still be requested. + d.as_nanos().div_ceil(1_000_000).min(i32::MAX as u128) as i32 + } else { + -1 // Don't timeout, wait forever. + }; + + // Pre-allocate a period-sized working buffer. Contents are overwritten each callback. + let transfer_buffer = vec![0u8; stream.period_size * stream.frame_size].into_boxed_slice(); + + // Pre-allocate and initialize descriptors vector: 1 for self-pipe + ALSA descriptors. + // The descriptor count is constant for the lifetime of stream parameters, and + // poll() overwrites revents on each call, so we only need to set up fd and events once. + let num_descriptors = stream.handle.count(); + let total_descriptors = 1 + num_descriptors; + let mut descriptors = vec![ + libc::pollfd { + fd: 0, + events: 0, + revents: 0 + }; + total_descriptors + ] + .into_boxed_slice(); + + // Set up self-pipe descriptor at index 0 + descriptors[0] = libc::pollfd { + fd: rx.0, + events: libc::POLLIN, + revents: 0, + }; + + // Set up ALSA descriptors starting at index 1 + let filled = stream + .handle + .fill(&mut descriptors[1..]) + .expect("Failed to fill ALSA descriptors"); + debug_assert_eq!(filled, num_descriptors); + + Self { + descriptors, + transfer_buffer, + poll_timeout, + } + } +} + +fn input_stream_worker( + rx: Arc, + stream: &StreamInner, + data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static), + error_callback: &mut (dyn FnMut(Error) + Send + 'static), + timeout: Option, +) { + #[cfg(feature = "realtime")] + if stream.is_rt_eligible() { + let period_frames = u32::try_from(stream.period_size).unwrap_or(0); + if let Err(err) = audio_thread_priority::promote_current_thread_to_real_time( + period_frames, + stream.sample_rate, + ) { + error_callback(err.into()); + } + } + + let mut ctxt = StreamWorkerContext::new(&timeout, stream, &rx); + loop { + if stream.dropping.load(Ordering::Acquire) { + return; + } + let result = match poll_for_period(&rx, stream, &mut ctxt) { + Ok(Poll::Pending) => continue, + Ok(Poll::Ready { + status, + delay_frames, + }) => process_input( + stream, + &mut ctxt.transfer_buffer, + status, + delay_frames, + data_callback, + ), + Err(err) => Err(err), + }; + if let Err(err) = result { + match err.kind() { + ErrorKind::Xrun => { + error_callback(err); + if let Err(err) = stream.handle.prepare() { + error_callback(err.into()); + } else if let Err(err) = stream.handle.start() { + error_callback(err.into()); + } + } + ErrorKind::DeviceNotAvailable => { + error_callback(err); + return; + } + _ => error_callback(err), + } + } + } +} + +fn output_stream_worker( + rx: Arc, + stream: &StreamInner, + data_callback: &mut (dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static), + error_callback: &mut (dyn FnMut(Error) + Send + 'static), + timeout: Option, +) { + #[cfg(feature = "realtime")] + if stream.is_rt_eligible() { + let period_frames = u32::try_from(stream.period_size).unwrap_or(0); + if let Err(err) = audio_thread_priority::promote_current_thread_to_real_time( + period_frames, + stream.sample_rate, + ) { + error_callback(err.into()); + } + } + + let mut ctxt = StreamWorkerContext::new(&timeout, stream, &rx); + + loop { + if stream.dropping.load(Ordering::Acquire) { + return; + } + let result = match poll_for_period(&rx, stream, &mut ctxt) { + Ok(Poll::Pending) => continue, + Ok(Poll::Ready { + status, + delay_frames, + }) => process_output( + stream, + &mut ctxt.transfer_buffer, + status, + delay_frames, + data_callback, + ), + Err(err) => Err(err), + }; + if let Err(err) = result { + match err.kind() { + ErrorKind::Xrun => { + error_callback(err); + if let Err(err) = stream.handle.prepare() { + error_callback(err.into()); + } + // No need to call start() for output streams after prepare(); + // ALSA automatically restarts them when the buffer is refilled + // and the stream is triggered again. + } + ErrorKind::DeviceNotAvailable => { + error_callback(err); + return; + } + _ => error_callback(err), + } + } + } +} + +/// Attempt hardware resume from a suspend event (`ESTRPIPE`). +fn try_resume(handle: &alsa::PCM) -> Result { + let hw_params = handle.hw_params_current()?; + if !hw_params.can_resume() { + return Err(Error::with_message( + ErrorKind::Xrun, // treat as xrun so the worker calls prepare() + "Device does not support suspend/resume", + )); + } + + match handle.resume() { + Ok(()) => { + if handle + .info() + .map(|i| i.get_stream() == alsa::Direction::Capture) + .unwrap_or(false) + { + // A successful `resume()` may leave the device `PREPARED` rather than `RUNNING`. + // `start()` to ensure the capture actually resumes. + if let Err(e) = handle.start() { + // `EBUSY` is ignored because it means the device is already running. + if e.errno() != libc::EBUSY { + return Err(e.into()); + } + } + } + Ok(Poll::Pending) + } + // device is still resuming; poll again until it is ready. + Err(e) if e.errno() == libc::EAGAIN => Ok(Poll::Pending), + // hardware does not support soft resume; treat as xrun so the worker calls prepare() + Err(e) if e.errno() == libc::ENOSYS => Err(ErrorKind::Xrun.into()), + Err(e) => Err(e.into()), + } +} + +enum Poll { + Pending, + Ready { + status: alsa::pcm::Status, + delay_frames: usize, + }, +} + +// This block is shared between both input and output stream worker functions. +fn poll_for_period( + rx: &TriggerReceiver, + stream: &StreamInner, + ctxt: &mut StreamWorkerContext, +) -> Result { + let StreamWorkerContext { + ref mut descriptors, + ref poll_timeout, + .. + } = *ctxt; + + let res = alsa::poll::poll(descriptors, *poll_timeout)?; + if res == 0 { + // Timeout expired with no events. Query PCM state to handle cases where + // POLLERR/POLLHUP was not delivered before the timeout fired (e.g. some + // power-management suspend paths or VM/container ALSA shims). + match stream.handle.state() { + alsa::pcm::State::Disconnected => { + return Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "Device disconnected", + )); + } + // Xrun with POLLERR missed: recover the same way the POLLERR path does. + alsa::pcm::State::XRun => { + return Err(ErrorKind::Xrun.into()); + } + // Suspend with POLLHUP/POLLERR missed: attempt hardware resume. + alsa::pcm::State::Suspended => return try_resume(&stream.handle), + // No events and no error state: spurious wakeup, poll again. + _ => {} + } + return Ok(Poll::Pending); + } + + if descriptors[0].revents != 0 { + // Self-pipe fired: the stream is being dropped. Clear the pipe and let the + // worker loop detect the dropping flag on the next iteration. + rx.clear_pipe(); + return Ok(Poll::Pending); + } + + let revents = stream.handle.revents(&descriptors[1..])?; + // No events: spurious wakeup, poll again. + if revents.is_empty() { + return Ok(Poll::Pending); + } + // POLLHUP/POLLNVAL: the device has been disconnected. + if revents.intersects(alsa::poll::Flags::HUP | alsa::poll::Flags::NVAL) { + return Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "Device disconnected", + )); + } + // POLLERR signals an xrun or suspend; avail_delay() below returns an error accordingly. + // POLLIN/POLLOUT: data is ready, fall through to process it. + let (avail_frames, delay_frames) = match stream.handle.avail_delay() { + // Suspend: try hardware resume first; fall back to prepare() if unsupported. + // BSD compat: check via PCM state rather than the Linux-specific ESTRPIPE errno. + Err(_) if matches!(stream.handle.state(), alsa::pcm::State::Suspended) => { + return try_resume(&stream.handle); + } + // Xrun: recover via prepare() (+ start() for capture, handled by the worker). + Err(err) if err.errno() == libc::EPIPE => return Err(ErrorKind::Xrun.into()), + res => res, + }?; + // ALSA can have spurious wakeups where poll returns but avail < avail_min. + // This is documented to occur with dmix (timer-driven) and other plugins. + // Verify we have room for at least one full period before processing. + // See: https://bugzilla.kernel.org/show_bug.cgi?id=202499 + // + // Compare in Frames (i64) so that a negative avail_frames from a buggy driver + // naturally fails the guard rather than wrapping to a huge usize that passes it. + if avail_frames < stream.period_size as alsa::pcm::Frames { + return Ok(Poll::Pending); + } + + let audio_ts_type = match stream.timestamp_mode { + TimestampMode::AudioLink => alsa::pcm::AudioTstampType::LinkSynchronized, + TimestampMode::SystemClock | TimestampMode::CreationInstant => { + alsa::pcm::AudioTstampType::Compat + } + }; + // From the guard above we know that this poll is not a spurious wakeup, + // so we also know we can query the device in a stable state. + let status = alsa::pcm::StatusBuilder::new() + .audio_htstamp_config(audio_ts_type, false) + .build(&stream.handle)?; + + Ok(Poll::Ready { + status, + delay_frames: delay_frames.max(0) as usize, + }) +} + +// Read input data from ALSA and deliver it to the user. +fn process_input( + stream: &StreamInner, + buffer: &mut [u8], + status: alsa::pcm::Status, + delay_frames: usize, + data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static), +) -> Result<(), Error> { + let mut frames_read = 0; + while frames_read < stream.period_size { + match stream + .handle + .io_bytes() + .readi(&mut buffer[frames_read * stream.frame_size..]) + { + Ok(n) => frames_read += n, + // EAGAIN = no frames available: skip this cycle if no progress was made, + // otherwise treat as an underrun (partial period cannot be delivered safely). + Err(err) if err.errno() == libc::EAGAIN => { + if frames_read == 0 { + return Ok(()); + } else { + return Err(ErrorKind::Xrun.into()); + } + } + // EPIPE = xrun: full underrun recovery (prepare + start) required. + Err(err) if err.errno() == libc::EPIPE => return Err(ErrorKind::Xrun.into()), + // Suspend: try soft resume first, falling back to underrun recovery if the + // hardware doesn't support it. BSD compat: check via PCM state rather than the + // Linux-specific ESTRPIPE errno. + Err(_) if matches!(stream.handle.state(), alsa::pcm::State::Suspended) => { + return try_resume(&stream.handle).map(|_| ()); + } + Err(err) => return Err(err.into()), + } + } + let data = buffer.as_mut_ptr() as *mut (); + let data = unsafe { Data::from_parts(data, stream.period_samples, stream.sample_format) }; + let callback_instant = stream.callback_instant(&status); + let delay_duration = frames_to_duration(delay_frames as FrameCount, stream.sample_rate); + let capture = callback_instant + .checked_sub(delay_duration) + .unwrap_or(StreamInstant::ZERO); + let timestamp = InputStreamTimestamp { + callback: callback_instant, + capture, + }; + let info = InputCallbackInfo { timestamp }; + data_callback(&data, &info); + + Ok(()) +} + +// Request data from the user's function and write it via ALSA. +fn process_output( + stream: &StreamInner, + buffer: &mut [u8], + status: alsa::pcm::Status, + delay_frames: usize, + data_callback: &mut (dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static), +) -> Result<(), Error> { + // Pre-fill buffer with equilibrium; user callback overwrites what it wants. + stream.equilibrium.fill(buffer); + + let data = buffer.as_mut_ptr() as *mut (); + let mut data = unsafe { Data::from_parts(data, stream.period_samples, stream.sample_format) }; + let callback_instant = stream.callback_instant(&status); + let delay_duration = frames_to_duration(delay_frames as FrameCount, stream.sample_rate); + let playback = callback_instant + delay_duration; + let timestamp = OutputStreamTimestamp { + callback: callback_instant, + playback, + }; + let info = OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); + + let mut frames_written = 0; + while frames_written < stream.period_size { + match stream + .handle + .io_bytes() + .writei(&buffer[frames_written * stream.frame_size..]) + { + Ok(n) => frames_written += n, + // EAGAIN = device cannot currently accept more frames: skip this cycle if no + // progress was made, otherwise treat as an underrun (partial period cannot be + // completed safely). + Err(err) if err.errno() == libc::EAGAIN => { + if frames_written == 0 { + return Ok(()); + } else { + return Err(ErrorKind::Xrun.into()); + } + } + // EPIPE = xrun: full underrun recovery (prepare) required. + Err(err) if err.errno() == libc::EPIPE => return Err(ErrorKind::Xrun.into()), + // Suspend: try soft resume first, falling back to underrun recovery if the + // hardware doesn't support it. BSD compat: check via PCM state rather than the + // Linux-specific ESTRPIPE errno. + Err(_) if matches!(stream.handle.state(), alsa::pcm::State::Suspended) => { + return try_resume(&stream.handle).map(|_| ()); + } + Err(err) => return Err(err.into()), + } + } + Ok(()) +} + +// Adapted from `timestamp2ns` here: +// https://fossies.org/linux/alsa-lib/test/audio_time.c +#[inline] +#[allow(clippy::unnecessary_cast)] +fn timespec_to_nanos(ts: libc::timespec) -> i64 { + ts.tv_sec as i64 * 1_000_000_000 + ts.tv_nsec as i64 +} + +// Adapted from `timediff` here: +// https://fossies.org/linux/alsa-lib/test/audio_time.c +#[inline] +fn timespec_diff_nanos(a: libc::timespec, b: libc::timespec) -> i64 { + timespec_to_nanos(a) - timespec_to_nanos(b) +} + +// StreamInstant representing how long htstamp is ahead of origin, clamped to zero. +// Used as the creation-relative timestamp source for SystemClock and AudioLink fallback paths. +#[inline] +fn htstamp_elapsed(status: &alsa::pcm::Status, origin: libc::timespec) -> StreamInstant { + let nanos = timespec_diff_nanos(status.get_htstamp(), origin); + StreamInstant::from_nanos(nanos.max(0) as u64) +} + +impl Stream { + /// Releases the latch so the worker thread can begin processing audio callbacks. + fn signal_ready(&self) { + self.latch.release(); + } + + fn new_input( + inner: Arc, + mut data_callback: D, + mut error_callback: E, + timeout: Option, + ) -> Stream + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + let (tx, rx) = trigger(); + let rx_thread = rx.clone(); + let stream = inner.clone(); + + // The latch is released by play(); the worker blocks here until then, keeping the PCM + // in PREPARED state with no DMA activity. + let mut latch = Latch::new(); + let waiter = latch.waiter(); + + let thread = thread::Builder::new() + .name("cpal_alsa_in".to_owned()) + .spawn(move || { + waiter.wait(); + input_stream_worker( + rx_thread, + &stream, + &mut data_callback, + &mut error_callback, + timeout, + ); + }) + .unwrap(); + latch.add_thread(thread.thread().clone()); + + Self { + thread: Some(thread), + inner, + trigger: tx, + _rx: rx, + latch, + } + } + + fn new_output( + inner: Arc, + mut data_callback: D, + mut error_callback: E, + timeout: Option, + ) -> Stream + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + let (tx, rx) = trigger(); + let rx_thread = rx.clone(); + let stream = inner.clone(); + + // The latch is released by play(); the worker blocks here until then, keeping the PCM + // in PREPARED state with no DMA activity. + let mut latch = Latch::new(); + let waiter = latch.waiter(); + + let thread = thread::Builder::new() + .name("cpal_alsa_out".to_owned()) + .spawn(move || { + waiter.wait(); + output_stream_worker( + rx_thread, + &stream, + &mut data_callback, + &mut error_callback, + timeout, + ); + }) + .unwrap(); + latch.add_thread(thread.thread().clone()); + + Self { + thread: Some(thread), + inner, + trigger: tx, + _rx: rx, + latch, + } + } +} + +impl Drop for Stream { + fn drop(&mut self) { + // Unblock the worker in case the stream is dropped before play() was called. + // Idempotent: no effect if the worker is already running. + self.signal_ready(); + self.inner.dropping.store(true, Ordering::Release); + self.trigger.wakeup(); + if let Some(handle) = self.thread.take() { + let _ = handle.join(); + } + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), Error> { + self.signal_ready(); // idempotent: no-op after first call + match self.inner.handle.state() { + // Calling start() on an empty output buffer would trigger an immediate XRUN. + alsa::pcm::State::Prepared if self.inner.direction == DeviceDirection::Input => { + self.inner.handle.start()?; + } + alsa::pcm::State::Paused => { + self.inner.handle.pause(false)?; + } + _ => {} + } + Ok(()) + } + + fn pause(&self) -> Result<(), Error> { + let hw_params = self.inner.handle.hw_params_current()?; + if !hw_params.can_pause() { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Device does not support pausing", + )); + } + if self.inner.handle.state() != alsa::pcm::State::Paused { + self.inner.handle.pause(true)?; + } + // TODO: when can_pause() is false, considering implementing a software fallback + Ok(()) + } + + fn now(&self) -> StreamInstant { + if self.inner.timestamp_mode != TimestampMode::CreationInstant { + let audio_ts_type = match self.inner.timestamp_mode { + TimestampMode::AudioLink => alsa::pcm::AudioTstampType::LinkSynchronized, + _ => alsa::pcm::AudioTstampType::Compat, + }; + if let Ok(status) = alsa::pcm::StatusBuilder::new() + .audio_htstamp_config(audio_ts_type, false) + .build(&self.inner.handle) + { + return self.inner.callback_instant(&status); + } + } + + let d = std::time::Instant::now().duration_since(self.inner.creation_instant); + StreamInstant::new(d.as_secs(), d.subsec_nanos()) + } + + fn buffer_size(&self) -> Result { + Ok(self.inner.period_size as FrameCount) + } +} + +fn supported_period_size_range( + hw_params: &alsa::pcm::HwParams<'_>, + alsa_format: alsa::pcm::Format, + channels: ChannelCount, +) -> SupportedBufferSize { + let p = hw_params.clone(); + if p.set_access(alsa::pcm::Access::RWInterleaved).is_err() + || p.set_channels(channels as u32).is_err() + || p.set_format(alsa_format).is_err() + { + return SupportedBufferSize::Unknown; + } + let Some((min, max)) = hw_params_period_size_min_max(&p) else { + return SupportedBufferSize::Unknown; + }; + let min_frames = min.max(1); + // cpal double-buffers (ring = DEFAULT_PERIODS × period), so the achievable + // period maximum is also bounded by max_buffer / DEFAULT_PERIODS. + let effective_max = match p.get_buffer_size_max() { + Ok(max_buf) if max_buf > 0 => max.min(max_buf / DEFAULT_PERIODS), + _ => max, + }; + if effective_max >= min_frames { + let Ok(min) = min_frames.try_into() else { + return SupportedBufferSize::Unknown; + }; + SupportedBufferSize::Range { + min, + max: effective_max.try_into().unwrap_or(FrameCount::MAX), + } + } else { + SupportedBufferSize::Unknown + } +} + +fn hw_params_period_size_min_max( + hw_params: &alsa::pcm::HwParams, +) -> Option<(alsa::pcm::Frames, alsa::pcm::Frames)> { + let min = hw_params.get_period_size_min().ok()?; + let max = hw_params.get_period_size_max().ok()?; + // min=0 means no hardware lower bound (PipeWire reports this on unconstrained params); + // it is handled in the caller by clamping to 1. max <= 0 is degenerate (or ULONG_MAX + // wrapping negative), so we return None in that case rather than a misleading range. + (max > 0 && max >= min).then_some((min, max)) +} + +fn init_hw_params<'a>( + pcm_handle: &'a alsa::pcm::PCM, + config: StreamConfig, + sample_format: SampleFormat, +) -> Result, Error> { + let hw_params = alsa::pcm::HwParams::any(pcm_handle)?; + hw_params.set_access(alsa::pcm::Access::RWInterleaved)?; + + // Determine which endianness the hardware actually supports for this format. + // We prefer native endian (no conversion needed) but fall back to the opposite + // endian if that's all the hardware supports (e.g., LE USB DAC on BE system). + let alsa_format = sample_format_to_alsa_format(&hw_params, sample_format)?; + hw_params.set_format(alsa_format)?; + + hw_params.set_rate(config.sample_rate, alsa::ValueOr::Nearest)?; + hw_params.set_channels(config.channels as u32)?; + Ok(hw_params) +} + +/// Convert SampleFormat to the appropriate alsa::pcm::Format based on what the hardware supports. +/// Prefers native endian, falls back to non-native if that's all the hardware supports. +fn sample_format_to_alsa_format( + hw_params: &alsa::pcm::HwParams, + sample_format: SampleFormat, +) -> Result { + use alsa::pcm::Format; + + // For each sample format, define (native_endian_format, opposite_endian_format) pairs + let (native, opposite) = match sample_format { + SampleFormat::I8 => return Ok(Format::S8), // No endianness + SampleFormat::U8 => return Ok(Format::U8), // No endianness + #[cfg(target_endian = "little")] + SampleFormat::I16 => (Format::S16LE, Format::S16BE), + #[cfg(target_endian = "big")] + SampleFormat::I16 => (Format::S16BE, Format::S16LE), + #[cfg(target_endian = "little")] + SampleFormat::U16 => (Format::U16LE, Format::U16BE), + #[cfg(target_endian = "big")] + SampleFormat::U16 => (Format::U16BE, Format::U16LE), + #[cfg(target_endian = "little")] + SampleFormat::I24 => (Format::S24LE, Format::S24BE), + #[cfg(target_endian = "big")] + SampleFormat::I24 => (Format::S24BE, Format::S24LE), + #[cfg(target_endian = "little")] + SampleFormat::U24 => (Format::U24LE, Format::U24BE), + #[cfg(target_endian = "big")] + SampleFormat::U24 => (Format::U24BE, Format::U24LE), + #[cfg(target_endian = "little")] + SampleFormat::I32 => (Format::S32LE, Format::S32BE), + #[cfg(target_endian = "big")] + SampleFormat::I32 => (Format::S32BE, Format::S32LE), + #[cfg(target_endian = "little")] + SampleFormat::U32 => (Format::U32LE, Format::U32BE), + #[cfg(target_endian = "big")] + SampleFormat::U32 => (Format::U32BE, Format::U32LE), + #[cfg(target_endian = "little")] + SampleFormat::F32 => (Format::FloatLE, Format::FloatBE), + #[cfg(target_endian = "big")] + SampleFormat::F32 => (Format::FloatBE, Format::FloatLE), + #[cfg(target_endian = "little")] + SampleFormat::F64 => (Format::Float64LE, Format::Float64BE), + #[cfg(target_endian = "big")] + SampleFormat::F64 => (Format::Float64BE, Format::Float64LE), + SampleFormat::DsdU8 => return Ok(Format::DSDU8), + #[cfg(target_endian = "little")] + SampleFormat::DsdU16 => (Format::DSDU16LE, Format::DSDU16BE), + #[cfg(target_endian = "big")] + SampleFormat::DsdU16 => (Format::DSDU16BE, Format::DSDU16LE), + #[cfg(target_endian = "little")] + SampleFormat::DsdU32 => (Format::DSDU32LE, Format::DSDU32BE), + #[cfg(target_endian = "big")] + SampleFormat::DsdU32 => (Format::DSDU32BE, Format::DSDU32LE), + _ => { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Sample format {sample_format} is not supported"), + )) + } + }; + + // Try native endian first (optimal - no conversion needed) + if hw_params.test_format(native).is_ok() { + return Ok(native); + } + + // Fall back to opposite endian if hardware only supports that + if hw_params.test_format(opposite).is_ok() { + return Ok(opposite); + } + + Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Sample format {sample_format} is not supported in any byte order"), + )) +} + +fn set_hw_params_from_format( + pcm_handle: &alsa::pcm::PCM, + config: StreamConfig, + sample_format: SampleFormat, +) -> Result, Error> { + let hw_params = init_hw_params(pcm_handle, config, sample_format)?; + + // When BufferSize::Fixed(x) is specified, we configure double-buffering with + // buffer_size = 2x and period_size = x. This provides consistent low-latency + // behavior across different ALSA implementations and hardware. + if let BufferSize::Fixed(period_size) = config.buffer_size { + let period_size = period_size as alsa::pcm::Frames; + + // Validate the requested size against the device's supported ranges using the same PCM + // handle we'll use for streaming. This avoids a second PCM open (which can disturb + // hardware clock state on some drivers) while still catching wildly out-of-range + // requests before set_period_size_near silently rounds them. + if let Some((min_period, max_period)) = hw_params_period_size_min_max(&hw_params) { + if !(min_period..=max_period).contains(&period_size) { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Buffer size {period_size} is not in the supported range {min_period}..={max_period}"), + )); + } + } + + let buffer_size = DEFAULT_PERIODS * period_size; + if let Ok(max_buffer) = hw_params.get_buffer_size_max() { + if max_buffer > 0 && buffer_size > max_buffer { + let effective_max = max_buffer / DEFAULT_PERIODS; + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Buffer size {period_size} exceeds the maximum supported value of {effective_max}"), + )); + } + } + + hw_params.set_buffer_size_near(buffer_size)?; + hw_params.set_period_size_near(period_size, alsa::ValueOr::Nearest)?; + } + + // Apply hardware parameters + pcm_handle.hw_params(&hw_params)?; + + // For BufferSize::Default, constrain to device's configured period with 2-period buffering. + // PipeWire-ALSA picks a good period size but pairs it with many periods (huge buffer). + // We need to re-initialize hw_params and set BOTH period and buffer to constrain properly. + if config.buffer_size == BufferSize::Default { + if let Ok(period_size) = hw_params.get_period_size() { + // Re-initialize hw_params to clear previous constraints + let hw_params = init_hw_params(pcm_handle, config, sample_format)?; + + // Set both period (to device's chosen value) and buffer (to 2 periods) + hw_params.set_period_size_near(period_size, alsa::ValueOr::Nearest)?; + hw_params.set_buffer_size_near(DEFAULT_PERIODS * period_size)?; + + // Re-apply with new constraints + pcm_handle.hw_params(&hw_params)?; + } + } + + pcm_handle.hw_params_current().map_err(Into::into) +} + +fn set_sw_params_from_format( + pcm_handle: &alsa::pcm::PCM, + stream_type: alsa::Direction, +) -> Result<(alsa::pcm::Frames, alsa::pcm::Frames), Error> { + let sw_params = pcm_handle.sw_params_current()?; + let (buffer_size, period_size) = pcm_handle + .get_params() + .map(|(b, p)| (b as alsa::pcm::Frames, p as alsa::pcm::Frames))?; + + let start_threshold = match stream_type { + alsa::Direction::Playback => { + // Start playback when 2 periods are filled. This ensures consistent low-latency + // startup regardless of total buffer size (whether 2 or more periods). + DEFAULT_PERIODS * period_size + } + alsa::Direction::Capture => 1, + }; + sw_params.set_start_threshold(start_threshold)?; + sw_params.set_avail_min(period_size)?; + + sw_params.set_tstamp_mode(true)?; + sw_params.set_tstamp_type(alsa::pcm::TstampType::MonotonicRaw)?; + + // tstamp_type param cannot be changed after the device is opened. + // The default tstamp_type value on most Linux systems is "monotonic", + // let's try to use it if setting the tstamp_type fails. + if pcm_handle.sw_params(&sw_params).is_err() { + sw_params.set_tstamp_type(alsa::pcm::TstampType::Monotonic)?; + pcm_handle.sw_params(&sw_params)?; + } + + Ok((buffer_size, period_size)) +} + +fn canonical_pcm_id(pcm_id: &str) -> String { + if let Some((prefix, rest)) = pcm_id.split_once(':') { + let (card_str, device_str) = match rest.split_once(',') { + Some((c, d)) => (c.trim(), d.trim()), + None => (rest.trim(), "0"), + }; + if card_str.contains('=') { + if !rest.contains(',') { + return format!("{prefix}:{rest},DEV=0"); + } + } else if let Ok(device) = device_str.parse::() { + return format!("{prefix}:CARD={card_str},DEV={device}"); + } + } + pcm_id.to_owned() +} + +impl From for Error { + fn from(err: alsa::Error) -> Self { + match err.errno() { + libc::ENODEV | libc::ENOENT | LIBC_ENOTSUPP => ErrorKind::DeviceNotAvailable.into(), + libc::EPERM | libc::EACCES => ErrorKind::PermissionDenied.into(), + libc::EBUSY | libc::EAGAIN => ErrorKind::DeviceBusy.into(), + libc::EINVAL => ErrorKind::UnsupportedConfig.into(), + libc::ENOSYS => ErrorKind::UnsupportedOperation.into(), + libc::EPIPE => ErrorKind::Xrun.into(), + _ => Error::with_message(ErrorKind::BackendError, err.to_string()), + } + } +} diff --git a/crates/vendor/cpal/src/host/asio/device.rs b/crates/vendor/cpal/src/host/asio/device.rs new file mode 100644 index 000000000..e99dc6fc0 --- /dev/null +++ b/crates/vendor/cpal/src/host/asio/device.rs @@ -0,0 +1,253 @@ +use std::{ + fmt, + hash::{Hash, Hasher}, + sync::{atomic::AtomicU32, Arc}, +}; + +use super::sys; +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; +use crate::{ + host::com, ChannelCount, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error, + ErrorKind, FrameCount, SampleFormat, SampleRate, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, +}; + +/// A ASIO Device +#[derive(Clone)] +pub struct Device { + name: String, + + // Metadata cached during enumeration + channels_in: ChannelCount, + channels_out: ChannelCount, + sample_rate: SampleRate, + buffer_size_min: FrameCount, + buffer_size_max: FrameCount, + input_sample_format: Option, + output_sample_format: Option, + supported_sample_rates: Box<[SampleRate]>, + + pub(super) current_callback_flag: Arc, +} + +/// All available devices. +pub struct Devices { + asio: Arc, + drivers: std::vec::IntoIter, + current_driver: Option, +} + +impl Device { + pub fn description(&self) -> Result { + let direction = crate::device_description::direction_from_counts( + Some(self.channels_in), + Some(self.channels_out), + ); + + Ok(DeviceDescriptionBuilder::new(&self.name) + .driver(&self.name) + .direction(direction) + .build()) + } + + pub fn id(&self) -> Result { + Ok(DeviceId::new(crate::platform::HostId::Asio, &self.name)) + } + + /// Gets the supported input configs. + /// TODO currently only supports the default. + /// Need to find all possible configs. + pub fn supported_input_configs(&self) -> Result { + let default = self.default_input_config()?; + Ok(self.configs_for(default).into_iter()) + } + + /// Gets the supported output configs. + /// TODO currently only supports the default. + /// Need to find all possible configs. + pub fn supported_output_configs(&self) -> Result { + let default = self.default_output_config()?; + Ok(self.configs_for(default).into_iter()) + } + + /// Returns the default input config + pub fn default_input_config(&self) -> Result { + self.default_config(self.channels_in, self.input_sample_format) + } + + /// Returns the default output config + pub fn default_output_config(&self) -> Result { + self.default_config(self.channels_out, self.output_sample_format) + } + + fn default_config( + &self, + channels: ChannelCount, + sample_format: Option, + ) -> Result { + if channels == 0 { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Device reports no channels for this direction", + )); + } + let sample_format = sample_format.ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedOperation, + "No supported sample format", + ) + })?; + Ok(SupportedStreamConfig { + channels, + sample_rate: self.sample_rate, + buffer_size: SupportedBufferSize::Range { + min: self.buffer_size_min, + max: self.buffer_size_max, + }, + sample_format, + }) + } + + fn configs_for(&self, default: SupportedStreamConfig) -> Vec { + let mut configs = Vec::with_capacity(default.channels as usize); + for &rate in &self.supported_sample_rates { + for channels in 1..=default.channels { + configs.push(SupportedStreamConfigRange { + channels, + min_sample_rate: rate, + max_sample_rate: rate, + buffer_size: default.buffer_size, + sample_format: default.sample_format, + }); + } + } + configs + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash(&self, state: &mut H) { + self.name.hash(state); + } +} + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let desc = self.description().map_err(|_| fmt::Error)?; + f.write_str(desc.name()) + } +} + +impl fmt::Debug for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Device") + .field("name", &self.name) + .finish_non_exhaustive() + } +} + +impl Devices { + pub fn new(asio: Arc) -> Result { + // Make sure that COM is initialized. + com::com_initialized(); + let drivers = asio.driver_names().into_iter(); + Ok(Self { + asio, + drivers, + current_driver: None, + }) + } +} + +impl Iterator for Devices { + type Item = Device; + + /// Enumerate devices by briefly loading each driver to capture its metadata. + fn next(&mut self) -> Option { + // Drop the previously loaded driver before attempting to load the next one. + self.current_driver = None; + + loop { + let name = self.drivers.next()?; + match self.asio.load_driver(&name) { + Ok(driver) => { + let Ok(channels) = driver.channels() else { + continue; + }; + if channels.ins == 0 && channels.outs == 0 { + continue; + } + + // Some drivers (e.g. Realtek ASIO) return 0 for sample_rate() until a + // stream is active. Treat 0 as "not yet known" rather than skipping. + let sample_rate = driver.sample_rate().unwrap_or(0.0); + + let Ok(buffer_size_range) = driver.buffersize_range() else { + continue; + }; + + let input_sample_format = driver + .input_data_type() + .ok() + .and_then(|t| convert_data_type(&t)); + let output_sample_format = driver + .output_data_type() + .ok() + .and_then(|t| convert_data_type(&t)); + + let supported_sample_rates: Box<[SampleRate]> = crate::COMMON_SAMPLE_RATES + .iter() + .copied() + .filter(|&r| driver.can_sample_rate(r.into()).unwrap_or(false)) + .collect(); + + self.current_driver = Some(driver); + + return Some(Device { + name, + channels_in: channels.ins as ChannelCount, + channels_out: channels.outs as ChannelCount, + sample_rate: sample_rate as SampleRate, + buffer_size_min: buffer_size_range.min as FrameCount, + buffer_size_max: buffer_size_range.max as FrameCount, + input_sample_format, + output_sample_format, + supported_sample_rates, + // Initialize with sentinel value so it never matches global flag state (0 or 1). + current_callback_flag: Arc::new(AtomicU32::new(u32::MAX)), + }); + } + // A different driver is already loaded (e.g. an active Stream holds it). Stop + // cleanly rather than spinning through the rest of the list. + Err(sys::LoadDriverError::DriverAlreadyExists) => return None, + // Driver failed to load for its own reasons; skip and try the next. + Err(_) => continue, + } + } + } +} + +pub(crate) fn convert_data_type(ty: &sys::AsioSampleType) -> Option { + let fmt = match *ty { + sys::AsioSampleType::ASIOSTInt16MSB => SampleFormat::I16, + sys::AsioSampleType::ASIOSTInt16LSB => SampleFormat::I16, + sys::AsioSampleType::ASIOSTInt24MSB => SampleFormat::I24, + sys::AsioSampleType::ASIOSTInt24LSB => SampleFormat::I24, + sys::AsioSampleType::ASIOSTInt32MSB => SampleFormat::I32, + sys::AsioSampleType::ASIOSTInt32LSB => SampleFormat::I32, + sys::AsioSampleType::ASIOSTFloat32MSB => SampleFormat::F32, + sys::AsioSampleType::ASIOSTFloat32LSB => SampleFormat::F32, + sys::AsioSampleType::ASIOSTFloat64MSB => SampleFormat::F64, + sys::AsioSampleType::ASIOSTFloat64LSB => SampleFormat::F64, + _ => return None, + }; + Some(fmt) +} diff --git a/crates/vendor/cpal/src/host/asio/mod.rs b/crates/vendor/cpal/src/host/asio/mod.rs new file mode 100644 index 000000000..c529ecee3 --- /dev/null +++ b/crates/vendor/cpal/src/host/asio/mod.rs @@ -0,0 +1,164 @@ +//! ASIO backend implementation. +//! +//! ASIO is available on Windows with the `asio` feature. +//! See the project README for setup instructions. + +extern crate asio_sys as sys; + +use std::{ + sync::{Arc, OnceLock}, + time::Duration, +}; + +pub use self::{ + device::{Device, Devices, SupportedInputConfigs, SupportedOutputConfigs}, + stream::Stream, +}; +use crate::{ + host::com, + traits::{DeviceTrait, HostTrait, StreamTrait}, + Data, DeviceDescription, DeviceId, Error, FrameCount, InputCallbackInfo, OutputCallbackInfo, + SampleFormat, StreamConfig, StreamInstant, SupportedStreamConfig, +}; + +mod device; +mod stream; + +/// Global ASIO instance shared across all Host instances. +/// +/// ASIO only supports loading a single driver at a time globally, so all Host instances +/// must share the same underlying sys::Asio wrapper to properly coordinate driver access. +static GLOBAL_ASIO: OnceLock> = OnceLock::new(); + +/// The host for ASIO. +#[derive(Debug)] +pub struct Host { + asio: Arc, +} + +impl Host { + pub fn new() -> Result { + com::com_initialized(); + let asio = GLOBAL_ASIO + .get_or_init(|| Arc::new(sys::Asio::new())) + .clone(); + let host = Host { asio }; + Ok(host) + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + true + //unimplemented!("check how to do this using asio-sys") + } + + fn devices(&self) -> Result { + Devices::new(self.asio.clone()) + } + + fn default_input_device(&self) -> Option { + // ASIO has no concept of a default device, so just use the first. + self.input_devices().ok().and_then(|mut ds| ds.next()) + } + + fn default_output_device(&self) -> Option { + // ASIO has no concept of a default device, so just use the first. + self.output_devices().ok().and_then(|mut ds| ds.next()) + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn description(&self) -> Result { + Device::description(self) + } + + fn id(&self) -> Result { + Device::id(self) + } + + fn supported_input_configs(&self) -> Result { + Device::supported_input_configs(self) + } + + fn supported_output_configs(&self) -> Result { + Device::supported_output_configs(self) + } + + fn default_input_config(&self) -> Result { + Device::default_input_config(self) + } + + fn default_output_config(&self) -> Result { + Device::default_output_config(self) + } + + fn build_input_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + Device::build_input_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + } + + fn build_output_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + Device::build_output_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), Error> { + Stream::play(self) + } + + fn pause(&self) -> Result<(), Error> { + Stream::pause(self) + } + + fn now(&self) -> StreamInstant { + Stream::now(self) + } + + fn buffer_size(&self) -> Result { + Stream::buffer_size(self) + } +} diff --git a/crates/vendor/cpal/src/host/asio/stream.rs b/crates/vendor/cpal/src/host/asio/stream.rs new file mode 100644 index 000000000..bf111d33d --- /dev/null +++ b/crates/vendor/cpal/src/host/asio/stream.rs @@ -0,0 +1,1460 @@ +extern crate asio_sys as sys; +extern crate num_traits; + +use std::{ + sync::{ + atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering}, + mpsc, Arc, Mutex, + }, + time::Duration, +}; + +use self::num_traits::{FromPrimitive, PrimInt}; +use super::Device; +use crate::{ + host::{ + com, + equilibrium::fill_equilibrium, + error_emit::{emit_error, try_emit_error}, + frames_to_duration, + }, + BufferSize, Data, Error, ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, + OutputCallbackInfo, OutputStreamTimestamp, Sample, SampleFormat, SampleRate, StreamConfig, + StreamInstant, I24, +}; + +/// Shared state for extending the 32-bit `timeGetTime()` millisecond counter into a +/// monotonic 64-bit nanosecond value, shared between `now()` and audio callbacks. +#[derive(Default)] +struct TimeBase { + last_ns: AtomicU64, + epoch_ns: AtomicU64, +} + +/// Nanosecond span of one full `timeGetTime()` wrap period (~49.7 days). +const TIMEGETIME_WRAP_NS: u64 = (u32::MAX as u64 + 1) * 1_000_000; + +impl TimeBase { + /// Convert a nanosecond timestamp to a monotonic `StreamInstant`. + fn to_stream_instant(&self, ns: u64) -> StreamInstant { + // `Relaxed` is sufficient: callbacks run on a single ASIO thread. The only + // cross-thread caller is `now()`, which may race at wrap time (~1µs every 49.7 days). + let prev = self.last_ns.swap(ns, Ordering::Relaxed); + let epoch = if ns < prev { + self.epoch_ns + .fetch_add(TIMEGETIME_WRAP_NS, Ordering::Relaxed) + + TIMEGETIME_WRAP_NS + } else { + self.epoch_ns.load(Ordering::Relaxed) + }; + StreamInstant::from_nanos(epoch + ns) + } +} + +/// Matches the `startTimer(500)` call JUCE uses for debouncing ASIO driver event notifications. +const ASIO_EVENT_DEBOUNCE: Duration = Duration::from_millis(500); + +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum StreamState { + Starting = 0, + Paused = 1, + Playing = 2, +} + +impl StreamState { + fn load(atom: &AtomicU8, order: Ordering) -> Self { + match atom.load(order) { + 1 => Self::Paused, + 2 => Self::Playing, + _ => Self::Starting, + } + } + + fn store(self, atom: &AtomicU8, order: Ordering) { + atom.store(self as u8, order); + } +} + +pub struct Stream { + state: Arc, + driver: Arc, + asio_streams: Arc>, + callback_id: sys::BufferCallbackId, + driver_event_callback_id: sys::DriverEventCallbackId, + time_base: Arc, +} + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +impl Stream { + pub fn now(&self) -> StreamInstant { + // `ASIOTimeInfo::systemTime` is specified by the ASIO SDK as nanoseconds + // derived from `timeGetTime()`, so calling it here gives a value on the + // same clock as the `system_time` field delivered to every callback. + let ms = unsafe { windows::Win32::Media::timeGetTime() }; + self.time_base.to_stream_instant(ms as u64 * 1_000_000) + } + + pub fn play(&self) -> Result<(), Error> { + StreamState::Playing.store(&self.state, Ordering::Release); + Ok(()) + } + + pub fn pause(&self) -> Result<(), Error> { + StreamState::Paused.store(&self.state, Ordering::Release); + Ok(()) + } + + pub fn buffer_size(&self) -> Result { + let streams = self.asio_streams.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") + })?; + Ok(streams + .output + .as_ref() + .or(streams.input.as_ref()) + .expect("ASIO stream has neither input nor output") + .buffer_size as FrameCount) + } +} + +impl Device { + pub fn build_input_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + crate::validate_stream_config(&config)?; + // Keep `capture` monotonic: kAsioLatenciesChanged (e.g. the user changes the buffer + // size in the ASIO driver's control panel) can raise inputLatency, pulling `capture` + // backward. + let mut data_callback = crate::host::monotonic_input_callback(data_callback); + com::com_initialized(); + let description = self.description()?; + let driver = super::GLOBAL_ASIO + .get() + .ok_or_else(|| { + Error::with_message( + ErrorKind::DeviceNotAvailable, + "ASIO driver is not initialized", + ) + })? + .load_driver(description.name()) + .map_err(load_driver_err)?; + + let stream_type = driver.input_data_type().map_err(build_stream_err)?; + + // Ensure that the desired sample type is supported. + let expected_sample_format = + super::device::convert_data_type(&stream_type).ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "Input sample format is not supported", + ) + })?; + if sample_format != expected_sample_format { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Sample format {sample_format} is not supported; expected {expected_sample_format}" + ), + )); + } + + let num_channels = config.channels; + let buffer_size = self.get_or_create_input_stream(&driver, config, sample_format)?; + let cpal_num_samples = buffer_size * num_channels as usize; + + // Create the buffer depending on the size of the data type. + let len_bytes = cpal_num_samples * sample_format.sample_size(); + let mut interleaved = vec![0u8; len_bytes]; + + // Query hardware input latency (order matters: needs buffers created above). + // Wrapped in Arc so the message callback can update it on + // kAsioLatenciesChanged without touching the buffer callback. + let hardware_input_latency = Arc::new(AtomicU32::new( + driver + .latencies() + .map(|latencies| latencies.input.max(0) as u32) + .unwrap_or(0), + )); + + let state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let driver_event_callback_id = self + .add_event_callback( + &driver, + error_callback, + Arc::clone(&hardware_input_latency), + true, + Arc::clone(&state), + ) + .inspect_err(|_| { + // Roll back the input stream stored by get_or_create_input_stream. + if let Ok(mut streams) = driver.streams().lock() { + streams.input = None; + } + })?; + + let state_cb = Arc::clone(&state); + let asio_streams = driver.streams(); + let mut current_buffer_size = buffer_size as i32; + let mut last_buffer_index: i32 = -1; + + let time_base = Arc::new(TimeBase::default()); + let time_base_cb = Arc::clone(&time_base); + + // Set the input callback. + // This is most performance critical part of the ASIO bindings. + let callback_id = driver.add_callback(move |callback_info| unsafe { + // If not playing, return early. + if StreamState::load(&state_cb, Ordering::Acquire) != StreamState::Playing { + return; + } + + // Guard against non-conformant drivers (e.g. Focusrite USB ASIO, ReaRoute) that + // fire the buffer callback multiple times per buffer cycle with the same buffer + // index. + if callback_info.buffer_index == last_buffer_index { + return; + } + last_buffer_index = callback_info.buffer_index; + + // There is 0% chance of lock contention the host only locks when recreating streams. + let stream_lock = asio_streams.lock().unwrap(); + let asio_stream = match stream_lock.input { + Some(ref asio_stream) => asio_stream, + None => return, + }; + + // Resize the buffer only when the driver issues a buffer size change request. + // In normal operation this branch is never taken. + if asio_stream.buffer_size != current_buffer_size { + current_buffer_size = asio_stream.buffer_size; + interleaved.resize( + current_buffer_size as usize + * num_channels as usize + * sample_format.sample_size(), + 0, + ); + } + + let hardware_input_latency = hardware_input_latency.load(Ordering::Relaxed) as usize; + + let callback_instant = time_base_cb.to_stream_instant(callback_info.system_time); + + /// 1. Write from the ASIO buffer to the interleaved CPAL buffer. + /// 2. Deliver the CPAL buffer to the user callback. + #[allow(clippy::too_many_arguments)] + unsafe fn process_input_callback( + data_callback: &mut D, + interleaved: &mut [u8], + asio_stream: &sys::AsioStream, + asio_info: &sys::CallbackInfo, + sample_rate: SampleRate, + format: SampleFormat, + from_endianness: F, + hardware_latency_frames: usize, + callback_instant: StreamInstant, + ) where + A: Copy, + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + F: Fn(A) -> A, + { + // 1. Write the ASIO channels to the CPAL buffer. + let interleaved: &mut [A] = cast_slice_mut(interleaved); + let n_frames = asio_stream.buffer_size as usize; + let n_channels = interleaved.len() / n_frames; + let buffer_index = asio_info.buffer_index as usize; + for ch_ix in 0..n_channels { + let asio_channel = + asio_channel_slice::(asio_stream, buffer_index, ch_ix, None); + for (frame, s_asio) in interleaved.chunks_mut(n_channels).zip(asio_channel) { + frame[ch_ix] = from_endianness(*s_asio); + } + } + + // 2. Deliver the interleaved buffer to the callback. + apply_input_callback_to_data::( + data_callback, + interleaved, + callback_instant, + sample_rate, + format, + hardware_latency_frames, + ); + } + + match (&stream_type, sample_format) { + (&sys::AsioSampleType::ASIOSTInt16LSB, SampleFormat::I16) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::I16, + from_le, + hardware_input_latency, + callback_instant, + ); + } + (&sys::AsioSampleType::ASIOSTInt16MSB, SampleFormat::I16) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::I16, + from_be, + hardware_input_latency, + callback_instant, + ); + } + + (&sys::AsioSampleType::ASIOSTFloat32LSB, SampleFormat::F32) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F32, + from_le, + hardware_input_latency, + callback_instant, + ); + } + (&sys::AsioSampleType::ASIOSTFloat32MSB, SampleFormat::F32) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F32, + from_be, + hardware_input_latency, + callback_instant, + ); + } + + (&sys::AsioSampleType::ASIOSTInt32LSB, SampleFormat::I32) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::I32, + from_le, + hardware_input_latency, + callback_instant, + ); + } + (&sys::AsioSampleType::ASIOSTInt32MSB, SampleFormat::I32) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::I32, + from_be, + hardware_input_latency, + callback_instant, + ); + } + + (&sys::AsioSampleType::ASIOSTFloat64LSB, SampleFormat::F64) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F64, + from_le, + hardware_input_latency, + callback_instant, + ); + } + (&sys::AsioSampleType::ASIOSTFloat64MSB, SampleFormat::F64) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F64, + from_be, + hardware_input_latency, + callback_instant, + ); + } + + (&sys::AsioSampleType::ASIOSTInt24LSB, SampleFormat::I24) => { + process_input_callback_i24( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + true, + hardware_input_latency, + callback_instant, + ); + } + (&sys::AsioSampleType::ASIOSTInt24MSB, SampleFormat::I24) => { + process_input_callback_i24( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + false, + hardware_input_latency, + callback_instant, + ); + } + + unsupported_format_pair => unreachable!( + "`build_input_stream_raw` should have returned with unsupported \ + format {:?}", + unsupported_format_pair + ), + } + }); + + let driver = Arc::new(driver); + let asio_streams = driver.streams(); + + if let Err(e) = driver.start() { + driver.remove_event_callback(driver_event_callback_id); + driver.remove_callback(callback_id); + if let Ok(mut streams) = asio_streams.lock() { + streams.input = None; + } + return Err(build_stream_err(e)); + } + + StreamState::Paused.store(&state, Ordering::Release); + Ok(Stream { + state, + driver, + asio_streams, + callback_id, + driver_event_callback_id, + time_base: Arc::clone(&time_base), + }) + } + + pub fn build_output_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + crate::validate_stream_config(&config)?; + // Keep `playback` monotonic: kAsioLatenciesChanged (e.g. the user changes the buffer + // size in the ASIO driver's control panel) can lower outputLatency, pulling `playback` + // backward. + let mut data_callback = crate::host::monotonic_output_callback(data_callback); + com::com_initialized(); + let description = self.description()?; + let driver = super::GLOBAL_ASIO + .get() + .ok_or_else(|| { + Error::with_message( + ErrorKind::DeviceNotAvailable, + "ASIO driver is not initialized", + ) + })? + .load_driver(description.name()) + .map_err(load_driver_err)?; + + let stream_type = driver.output_data_type().map_err(build_stream_err)?; + + // Ensure that the desired sample type is supported. + let expected_sample_format = + super::device::convert_data_type(&stream_type).ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "Output sample format is not supported", + ) + })?; + if sample_format != expected_sample_format { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Sample format {sample_format} is not supported; expected {expected_sample_format}" + ), + )); + } + + let num_channels = config.channels; + let buffer_size = self.get_or_create_output_stream(&driver, config, sample_format)?; + let cpal_num_samples = buffer_size * num_channels as usize; + + // Create the buffer depending on data type. + let len_bytes = cpal_num_samples * sample_format.sample_size(); + let mut interleaved = vec![0u8; len_bytes]; + let current_callback_flag = self.current_callback_flag.clone(); + + // Query hardware output latency (order matters: needs buffers created above). + // Wrapped in Arc so the message callback can update it on + // kAsioLatenciesChanged without touching the buffer callback. + let hardware_output_latency = Arc::new(AtomicU32::new( + driver + .latencies() + .map(|latencies| latencies.output.max(0) as u32) + .unwrap_or(0), + )); + + let state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let driver_event_callback_id = self + .add_event_callback( + &driver, + error_callback, + Arc::clone(&hardware_output_latency), + false, + Arc::clone(&state), + ) + .inspect_err(|_| { + // Roll back the output stream stored by get_or_create_output_stream. + if let Ok(mut streams) = driver.streams().lock() { + streams.output = None; + } + })?; + + let state_cb = Arc::clone(&state); + let asio_streams = driver.streams(); + let mut current_buffer_size = buffer_size as i32; + let mut last_buffer_index: i32 = -1; + + let time_base = Arc::new(TimeBase::default()); + let time_base_cb = Arc::clone(&time_base); + + let callback_id = driver.add_callback(move |callback_info| unsafe { + // If not playing, return early. + if StreamState::load(&state_cb, Ordering::Acquire) != StreamState::Playing { + return; + } + + // Guard against non-conformant drivers (e.g. Focusrite USB ASIO, ReaRoute) that + // fire the buffer callback multiple times per buffer cycle with the same buffer + // index. + if callback_info.buffer_index == last_buffer_index { + return; + } + last_buffer_index = callback_info.buffer_index; + + // There is 0% chance of lock contention the host only locks when recreating streams. + let mut stream_lock = asio_streams.lock().unwrap(); + let asio_stream = match stream_lock.output { + Some(ref mut asio_stream) => asio_stream, + None => return, + }; + + // Resize the buffer only when the driver issues a buffer size change request. + // In normal operation this branch is never taken. + if asio_stream.buffer_size != current_buffer_size { + current_buffer_size = asio_stream.buffer_size; + interleaved.resize( + current_buffer_size as usize + * num_channels as usize + * sample_format.sample_size(), + 0, + ); + } + + let hardware_output_latency = hardware_output_latency.load(Ordering::Relaxed) as usize; + + let callback_instant = time_base_cb.to_stream_instant(callback_info.system_time); + + // Silence the ASIO buffer that is about to be used. + // + // Check if any other callbacks have already silenced the buffer associated with + // the current callback. The flag is updated once per buffer switch. + let silence = + current_callback_flag.load(Ordering::Acquire) != callback_info.callback_flag; + + if silence { + current_callback_flag.store(callback_info.callback_flag, Ordering::Release); + } + + /// 1. Render the given callback to the given buffer of interleaved samples. + /// 2. If required, silence the ASIO buffer. + /// 3. Finally, write the interleaved data to the non-interleaved ASIO buffer, + /// performing endianness conversions as necessary. + #[allow(clippy::too_many_arguments)] + unsafe fn process_output_callback( + data_callback: &mut D, + interleaved: &mut [u8], + silence_asio_buffer: bool, + asio_stream: &mut sys::AsioStream, + asio_info: &sys::CallbackInfo, + sample_rate: SampleRate, + format: SampleFormat, + mix_samples: F, + hardware_latency_frames: usize, + callback_instant: StreamInstant, + ) where + A: Copy + Sample, + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + F: Fn(A, A) -> A, + { + let interleaved: &mut [A] = cast_slice_mut(interleaved); + apply_output_callback_to_data::( + data_callback, + interleaved, + callback_instant, + sample_rate, + format, + hardware_latency_frames, + ); + let n_channels = interleaved.len() / asio_stream.buffer_size as usize; + let buffer_index = asio_info.buffer_index as usize; + + // Write interleaved samples to ASIO channels, one channel at a time. + for ch_ix in 0..n_channels { + let asio_channel = + asio_channel_slice_mut::(asio_stream, buffer_index, ch_ix, None); + if silence_asio_buffer { + asio_channel.fill(A::EQUILIBRIUM); + } + for (frame, s_asio) in interleaved.chunks(n_channels).zip(asio_channel) { + *s_asio = mix_samples(*s_asio, frame[ch_ix]); + } + } + } + + fill_equilibrium(&mut interleaved, sample_format); + match (sample_format, &stream_type) { + (SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt16LSB) => { + process_output_callback::( + &mut data_callback, + &mut interleaved, + silence, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::I16, + |old_sample, new_sample| { + from_le(old_sample).saturating_add(new_sample).to_le() + }, + hardware_output_latency, + callback_instant, + ); + } + (SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt16MSB) => { + process_output_callback::( + &mut data_callback, + &mut interleaved, + silence, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::I16, + |old_sample, new_sample| { + from_be(old_sample).saturating_add(new_sample).to_be() + }, + hardware_output_latency, + callback_instant, + ); + } + (SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat32LSB) => { + process_output_callback::( + &mut data_callback, + &mut interleaved, + silence, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F32, + |old_sample, new_sample| { + (f32::from_bits(from_le(old_sample)) + f32::from_bits(new_sample)) + .to_bits() + .to_le() + }, + hardware_output_latency, + callback_instant, + ); + } + + (SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat32MSB) => { + process_output_callback::( + &mut data_callback, + &mut interleaved, + silence, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F32, + |old_sample, new_sample| { + (f32::from_bits(from_be(old_sample)) + f32::from_bits(new_sample)) + .to_bits() + .to_be() + }, + hardware_output_latency, + callback_instant, + ); + } + + (SampleFormat::I32, &sys::AsioSampleType::ASIOSTInt32LSB) => { + process_output_callback::( + &mut data_callback, + &mut interleaved, + silence, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::I32, + |old_sample, new_sample| { + from_le(old_sample).saturating_add(new_sample).to_le() + }, + hardware_output_latency, + callback_instant, + ); + } + (SampleFormat::I32, &sys::AsioSampleType::ASIOSTInt32MSB) => { + process_output_callback::( + &mut data_callback, + &mut interleaved, + silence, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::I32, + |old_sample, new_sample| { + from_be(old_sample).saturating_add(new_sample).to_be() + }, + hardware_output_latency, + callback_instant, + ); + } + + (SampleFormat::F64, &sys::AsioSampleType::ASIOSTFloat64LSB) => { + process_output_callback::( + &mut data_callback, + &mut interleaved, + silence, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F64, + |old_sample, new_sample| { + (f64::from_bits(from_le(old_sample)) + f64::from_bits(new_sample)) + .to_bits() + .to_le() + }, + hardware_output_latency, + callback_instant, + ); + } + + (SampleFormat::F64, &sys::AsioSampleType::ASIOSTFloat64MSB) => { + process_output_callback::( + &mut data_callback, + &mut interleaved, + silence, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F64, + |old_sample, new_sample| { + (f64::from_bits(from_be(old_sample)) + f64::from_bits(new_sample)) + .to_bits() + .to_be() + }, + hardware_output_latency, + callback_instant, + ); + } + + (SampleFormat::I24, &sys::AsioSampleType::ASIOSTInt24LSB) => { + process_output_callback_i24::<_>( + &mut data_callback, + &mut interleaved, + silence, + true, + asio_stream, + callback_info, + config.sample_rate, + hardware_output_latency, + callback_instant, + ); + } + + (SampleFormat::I24, &sys::AsioSampleType::ASIOSTInt24MSB) => { + process_output_callback_i24::<_>( + &mut data_callback, + &mut interleaved, + silence, + false, + asio_stream, + callback_info, + config.sample_rate, + hardware_output_latency, + callback_instant, + ); + } + + unsupported_format_pair => unreachable!( + "`build_output_stream_raw` should have returned with unsupported \ + format {unsupported_format_pair:?}" + ), + } + }); + + let driver = Arc::new(driver); + let asio_streams = driver.streams(); + + if let Err(e) = driver.start() { + driver.remove_event_callback(driver_event_callback_id); + driver.remove_callback(callback_id); + if let Ok(mut streams) = asio_streams.lock() { + streams.output = None; + } + return Err(build_stream_err(e)); + } + + StreamState::Paused.store(&state, Ordering::Release); + Ok(Stream { + state, + driver, + asio_streams, + callback_id, + driver_event_callback_id, + time_base: Arc::clone(&time_base), + }) + } + + /// Create a new CPAL Input Stream. + /// + /// If there is no existing ASIO Input Stream it will be created. + /// + /// On success, the buffer size of the stream is returned. + fn get_or_create_input_stream( + &self, + driver: &sys::Driver, + config: StreamConfig, + sample_format: SampleFormat, + ) -> Result { + let num_asio_channels = self.default_input_config()?.channels; + check_config(driver, config, sample_format, num_asio_channels)?; + let num_channels = config.channels as usize; + let asio_streams = driver.streams(); + let mut streams = asio_streams.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") + })?; + + let buffer_size = match config.buffer_size { + BufferSize::Fixed(v) => Some(v as i32), + BufferSize::Default => None, + }; + + // Either create a stream if thers none or had back the + // size of the current one. + match streams.input { + Some(ref input) => Ok(input.buffer_size as usize), + None => { + let output = streams.output.take(); + driver + .prepare_input_stream(output, num_channels, buffer_size) + .map(|new_streams| { + let bs = match new_streams.input { + Some(ref inp) => inp.buffer_size as usize, + None => unreachable!(), + }; + *streams = new_streams; + bs + }) + .map_err(build_stream_err) + } + } + } + + /// Create a new CPAL Output Stream. + /// + /// If there is no existing ASIO Output Stream it will be created. + fn get_or_create_output_stream( + &self, + driver: &sys::Driver, + config: StreamConfig, + sample_format: SampleFormat, + ) -> Result { + let num_asio_channels = self.default_output_config()?.channels; + check_config(driver, config, sample_format, num_asio_channels)?; + let num_channels = config.channels as usize; + let asio_streams = driver.streams(); + let mut streams = asio_streams.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") + })?; + + let buffer_size = match config.buffer_size { + BufferSize::Fixed(v) => Some(v as i32), + BufferSize::Default => None, + }; + + // Either create a stream if thers none or had back the + // size of the current one. + match streams.output { + Some(ref output) => Ok(output.buffer_size as usize), + None => { + let input = streams.input.take(); + driver + .prepare_output_stream(input, num_channels, buffer_size) + .map(|new_streams| { + let bs = match new_streams.output { + Some(ref out) => out.buffer_size as usize, + None => unreachable!(), + }; + *streams = new_streams; + bs + }) + .map_err(build_stream_err) + } + } + } + + fn add_event_callback( + &self, + driver: &sys::Driver, + error_callback: E, + hardware_latency: Arc, + is_input: bool, + state: Arc, + ) -> Result + where + E: FnMut(Error) + Send + 'static, + { + let error_callback_shared = Arc::new(Mutex::new(error_callback)); + let configured_sample_rate = match driver.sample_rate() { + Ok(r) if r > 0.0 => Some(r), + _ => { + // Some drivers do not report a sample rate before a stream has started. + None + } + }; + let driver_for_latency = driver.clone(); + let asio_streams_for_event = driver.streams(); + + // Debounce timer: wait for ASIO_EVENT_DEBOUNCE of silence after the most recent event + // before delivering to the user. Exits when `timer_tx` is dropped, which happens when the + // event callback closure is removed during stream teardown. + let (timer_tx, timer_rx) = mpsc::channel::(); + let error_cb_for_timer = Arc::clone(&error_callback_shared); + std::thread::Builder::new() + .name("cpal-asio-event-timer".into()) + .spawn(move || { + let mut pending: Option = None; + loop { + // Use recv() when idle (no timeout needed) so we don't spin. + let result = if pending.is_some() { + timer_rx.recv_timeout(ASIO_EVENT_DEBOUNCE) + } else { + timer_rx + .recv() + .map_err(|_| mpsc::RecvTimeoutError::Disconnected) + }; + match result { + Ok(err) => { + // New event; restart the grace window. + pending = Some(err); + } + Err(mpsc::RecvTimeoutError::Timeout) => { + // Grace period elapsed with no new events: now deliver. + if let Some(err) = pending.take() { + emit_error(&error_cb_for_timer, err); + } + } + Err(mpsc::RecvTimeoutError::Disconnected) => return, + } + } + }) + .map_err(|e| { + Error::with_message( + ErrorKind::ResourceExhausted, + format!("Failed to spawn event timer thread: {e}"), + ) + })?; + + Ok(driver.add_event_callback(move |event| { + match event { + sys::AsioDriverEvent::Message { + selector: msg, + value, + } => match msg { + sys::AsioMessageSelectors::kAsioSelectorSupported => { + // Signal which selectors this stream opts into. + matches!( + sys::AsioMessageSelectors::from_i64(value as i64), + Some(sys::AsioMessageSelectors::kAsioBufferSizeChange) + | Some(sys::AsioMessageSelectors::kAsioOverload) + ) + } + sys::AsioMessageSelectors::kAsioResetRequest => { + // Guard on Starting: some USB ASIO drivers (ASIO4ALL, Focusrite, etc.) + // fire spurious reset/resync requests during driver.start(). + if StreamState::load(&state, Ordering::Acquire) != StreamState::Starting { + let _ = timer_tx.send(Error::with_message( + ErrorKind::StreamInvalidated, + "Stream reset was requested by the ASIO driver", + )); + } + true + } + sys::AsioMessageSelectors::kAsioResyncRequest => { + // Per the ASIO spec (and matching JUCE's behavior), kAsioResyncRequest + // means the driver needs a full stop/reinit/start. It is *not* a simple + // xrun notification. + if StreamState::load(&state, Ordering::Acquire) != StreamState::Starting { + let _ = timer_tx.send(Error::with_message( + ErrorKind::StreamInvalidated, + "Stream resynchronization was requested by the ASIO driver", + )); + } + true + } + sys::AsioMessageSelectors::kAsioOverload => { + if StreamState::load(&state, Ordering::Acquire) == StreamState::Playing { + let _ = + try_emit_error(&error_callback_shared, Error::new(ErrorKind::Xrun)); + } + true + } + sys::AsioMessageSelectors::kAsioLatenciesChanged => { + if let Ok(latencies) = driver_for_latency.latencies() { + let latency = if is_input { + latencies.input + } else { + latencies.output + }; + hardware_latency.store(latency.max(0) as u32, Ordering::Relaxed); + } + false + } + sys::AsioMessageSelectors::kAsioBufferSizeChange => { + if value > 0 { + let mut streams = asio_streams_for_event + .lock() + .unwrap_or_else(|e| e.into_inner()); + let stream = if is_input { + streams.input.as_mut() + } else { + streams.output.as_mut() + }; + if let Some(s) = stream { + s.buffer_size = value; + } + } + true + } + _ => false, + }, + sys::AsioDriverEvent::SampleRateChanged(new_rate) => { + let should_notify = match configured_sample_rate { + Some(rate) => (new_rate - rate).abs() >= 1.0, + None => { + // Unknown baseline: any reported change is treated as invalidating. + true + } + }; + if should_notify + && StreamState::load(&state, Ordering::Acquire) != StreamState::Starting + { + let _ = timer_tx.send(Error::with_message( + ErrorKind::StreamInvalidated, + format!("Sample rate changed to {new_rate} Hz by the ASIO driver"), + )); + } + false + } + } + })) + } +} + +impl Drop for Stream { + fn drop(&mut self) { + self.driver.remove_callback(self.callback_id); + self.driver + .remove_event_callback(self.driver_event_callback_id); + } +} + +/// Check whether or not the desired config is supported by the stream. +/// +/// Checks sample rate, data type, number of channels, and buffer size. +fn check_config( + driver: &sys::Driver, + config: StreamConfig, + sample_format: SampleFormat, + num_asio_channels: u16, +) -> Result<(), Error> { + let StreamConfig { + channels, + sample_rate, + buffer_size, + } = config; + + // Validate buffer size if `Fixed` is specified. This is necessary because ASIO's + // `create_buffers` only validates the upper bound (returns `InvalidBufferSize` if > max) but + // does NOT validate the lower bound. Passing a buffer size below min would be accepted but + // behavior is unspecified. + if let BufferSize::Fixed(requested_size) = buffer_size { + let range = driver.buffersize_range().map_err(build_stream_err)?; + let requested_size_i32 = requested_size as i32; + if !(range.min..=range.max).contains(&requested_size_i32) { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Buffer size {requested_size} is not in the supported range {min}..={max}", + min = range.min, + max = range.max + ), + )); + } + if let sys::BufferPreference::Stepped { step, .. } = range.preferred { + let offset = requested_size_i32 - range.min; + if offset % step as i32 != 0 { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Buffer size {requested_size} is not valid; sizes must start at {min} and increment by {step}", + min = range.min + ), + )); + } + } + } + + // Try and set the sample rate to what the user selected. + let sample_rate = sample_rate.into(); + if sample_rate != driver.sample_rate().map_err(build_stream_err)? { + if driver + .can_sample_rate(sample_rate) + .map_err(build_stream_err)? + { + driver + .set_sample_rate(sample_rate) + .map_err(build_stream_err)?; + } else { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Sample rate {sample_rate} Hz is not supported"), + )); + } + } + // unsigned formats are not supported by asio + match sample_format { + SampleFormat::I16 | SampleFormat::I24 | SampleFormat::I32 | SampleFormat::F32 => (), + _ => { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Sample format {sample_format} is not supported"), + )) + } + } + if channels > num_asio_channels { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Channel count {channels} exceeds the maximum of {num_asio_channels}"), + )); + } + Ok(()) +} + +/// Cast a byte slice into a mutable slice of desired type. +/// +/// Safety: it's up to the caller to ensure that the input slice has valid bit representations. +#[inline] +unsafe fn cast_slice_mut(v: &mut [u8]) -> &mut [T] { + debug_assert!(v.len() % std::mem::size_of::() == 0); + std::slice::from_raw_parts_mut(v.as_mut_ptr() as *mut T, v.len() / std::mem::size_of::()) +} + +/// Helper function to convert from little endianness. +#[inline] +fn from_le(t: T) -> T { + T::from_le(t) +} + +/// Helper function to convert from big endianness. +#[inline] +fn from_be(t: T) -> T { + T::from_be(t) +} + +/// Shorthand for retrieving the asio buffer slice associated with a channel. +/// +/// The channel length is automatically inferred from the buffer size or some +/// value can be passed to enforce a certain length (for odd sized sample formats) +#[inline] +unsafe fn asio_channel_slice( + asio_stream: &sys::AsioStream, + buffer_index: usize, + channel_index: usize, + requested_channel_length: Option, +) -> &[T] { + let channel_length = requested_channel_length.unwrap_or(asio_stream.buffer_size as usize); + let buff_ptr: *const T = + asio_stream.buffer_infos[channel_index].buffers[buffer_index] as *const _; + std::slice::from_raw_parts(buff_ptr, channel_length) +} + +/// Shorthand for retrieving the asio buffer slice associated with a channel. +/// +/// The channel length is automatically inferred from the buffer size or some +/// value can be passed to enforce a certain length (for odd sized sample formats) +#[inline] +unsafe fn asio_channel_slice_mut( + asio_stream: &mut sys::AsioStream, + buffer_index: usize, + channel_index: usize, + requested_channel_length: Option, +) -> &mut [T] { + let channel_length = requested_channel_length.unwrap_or(asio_stream.buffer_size as usize); + let buff_ptr: *mut T = asio_stream.buffer_infos[channel_index].buffers[buffer_index] as *mut _; + std::slice::from_raw_parts_mut(buff_ptr, channel_length) +} + +fn load_driver_err(e: sys::LoadDriverError) -> Error { + match e { + sys::LoadDriverError::LoadDriverFailed | sys::LoadDriverError::DriverAlreadyExists => { + Error::with_message(ErrorKind::DeviceNotAvailable, e.to_string()) + } + sys::LoadDriverError::InitializationFailed(asio_err) => build_stream_err(asio_err), + } +} + +fn build_stream_err(e: sys::AsioError) -> Error { + match e { + sys::AsioError::NoDrivers | sys::AsioError::HardwareMalfunction => { + Error::with_message(ErrorKind::DeviceNotAvailable, e.to_string()) + } + sys::AsioError::InvalidInput | sys::AsioError::BadMode => { + Error::with_message(ErrorKind::InvalidInput, e.to_string()) + } + sys::AsioError::InvalidBufferSize | sys::AsioError::NoRate => { + Error::with_message(ErrorKind::UnsupportedConfig, e.to_string()) + } + sys::AsioError::HardwareStuck => Error::with_message(ErrorKind::DeviceBusy, e.to_string()), + err => Error::with_message(ErrorKind::BackendError, err.to_string()), + } +} + +/// Convert i24 bytes to i32 +#[inline] +fn i24_bytes_to_i32(i24_bytes: &[u8; 3], little_endian: bool) -> i32 { + let sample = if little_endian { + i32::from_le_bytes([i24_bytes[0], i24_bytes[1], i24_bytes[2], 0u8]) + } else { + i32::from_le_bytes([i24_bytes[2], i24_bytes[1], i24_bytes[0], 0u8]) + }; + if sample & 0x800000 != 0 { + sample | -0x1000000 + } else { + sample + } +} + +#[allow(clippy::too_many_arguments)] +unsafe fn process_output_callback_i24( + data_callback: &mut D, + interleaved: &mut [u8], + silence_asio_buffer: bool, + little_endian: bool, + asio_stream: &mut sys::AsioStream, + asio_info: &sys::CallbackInfo, + sample_rate: SampleRate, + hardware_latency_frames: usize, + callback_instant: StreamInstant, +) where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, +{ + let format = SampleFormat::I24; + let interleaved: &mut [I24] = cast_slice_mut(interleaved); + apply_output_callback_to_data::( + data_callback, + interleaved, + callback_instant, + sample_rate, + format, + hardware_latency_frames, + ); + + // Size of samples in the ASIO buffer (has to be 3 in this case) + let asio_sample_size_bytes = 3; + let n_channels = interleaved.len() / asio_stream.buffer_size as usize; + let buffer_index = asio_info.buffer_index as usize; + + // Write interleaved samples to ASIO channels, one channel at a time. + for ch_ix in 0..n_channels { + // Take channel as u8 array ([u8; 3] packets to represent i24) + let asio_channel = asio_channel_slice_mut( + asio_stream, + buffer_index, + ch_ix, + Some(asio_stream.buffer_size as usize * asio_sample_size_bytes), + ); + + if silence_asio_buffer { + fill_equilibrium(asio_channel, format); + } + + // Fill in every channel from the interleaved vector + for (channel_sample, sample_in_buffer) in asio_channel + .chunks_mut(asio_sample_size_bytes) + .zip(interleaved.iter().skip(ch_ix).step_by(n_channels)) + { + // Add samples from buffer if no silence was applied, otherwise just overwrite + let result = if silence_asio_buffer { + sample_in_buffer.inner() + } else { + let sample = i24_bytes_to_i32( + &[channel_sample[0], channel_sample[1], channel_sample[2]], + little_endian, + ); + (sample_in_buffer.inner() + sample).clamp(-8388608, 8388607) + }; + let bytes = result.to_le_bytes(); + if little_endian { + channel_sample[0] = bytes[0]; + channel_sample[1] = bytes[1]; + channel_sample[2] = bytes[2]; + } else { + channel_sample[2] = bytes[0]; + channel_sample[1] = bytes[1]; + channel_sample[0] = bytes[2]; + } + } + } +} + +#[allow(clippy::too_many_arguments)] +unsafe fn process_input_callback_i24( + data_callback: &mut D, + interleaved: &mut [u8], + asio_stream: &sys::AsioStream, + asio_info: &sys::CallbackInfo, + sample_rate: SampleRate, + little_endian: bool, + hardware_latency_frames: usize, + callback_instant: StreamInstant, +) where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, +{ + let format = SampleFormat::I24; + + // 1. Write the ASIO channels to the CPAL buffer. + let interleaved: &mut [I24] = cast_slice_mut(interleaved); + let n_frames = asio_stream.buffer_size as usize; + let n_channels = interleaved.len() / n_frames; + let buffer_index = asio_info.buffer_index as usize; + let asio_sample_size_bytes = 3; + + for ch_ix in 0..n_channels { + let asio_channel = asio_channel_slice::( + asio_stream, + buffer_index, + ch_ix, + Some(n_frames * asio_sample_size_bytes), + ); + for (channel_sample, sample_in_buffer) in asio_channel + .chunks(asio_sample_size_bytes) + .zip(interleaved.iter_mut().skip(ch_ix).step_by(n_channels)) + { + let sample = i24_bytes_to_i32( + &[channel_sample[0], channel_sample[1], channel_sample[2]], + little_endian, + ); + *sample_in_buffer = I24::new(sample).unwrap(); + } + } + + // 2. Deliver the interleaved buffer to the callback. + apply_input_callback_to_data::( + data_callback, + interleaved, + callback_instant, + sample_rate, + format, + hardware_latency_frames, + ); +} + +/// Apply the output callback to the interleaved buffer. +#[inline] +unsafe fn apply_output_callback_to_data( + data_callback: &mut D, + interleaved: &mut [A], + callback_instant: StreamInstant, + sample_rate: SampleRate, + sample_format: SampleFormat, + hardware_latency_frames: usize, +) where + A: Copy, + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, +{ + let mut data = Data::from_parts( + interleaved.as_mut_ptr() as *mut (), + interleaved.len(), + sample_format, + ); + let delay = frames_to_duration(hardware_latency_frames as FrameCount, sample_rate); + let playback = callback_instant + delay; + let timestamp = OutputStreamTimestamp { + callback: callback_instant, + playback, + }; + let info = OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); +} + +/// Apply the input callback to the interleaved buffer. +#[inline] +unsafe fn apply_input_callback_to_data( + data_callback: &mut D, + interleaved: &mut [A], + callback_instant: StreamInstant, + sample_rate: SampleRate, + format: SampleFormat, + hardware_latency_frames: usize, +) where + A: Copy, + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, +{ + let data = Data::from_parts( + interleaved.as_mut_ptr() as *mut (), + interleaved.len(), + format, + ); + let delay = frames_to_duration(hardware_latency_frames as FrameCount, sample_rate); + let capture = callback_instant + .checked_sub(delay) + .unwrap_or(StreamInstant::ZERO); + let timestamp = InputStreamTimestamp { + callback: callback_instant, + capture, + }; + let info = InputCallbackInfo { timestamp }; + data_callback(&data, &info); +} diff --git a/crates/vendor/cpal/src/host/audioworklet/dependent_module.rs b/crates/vendor/cpal/src/host/audioworklet/dependent_module.rs new file mode 100644 index 000000000..75db7d14e --- /dev/null +++ b/crates/vendor/cpal/src/host/audioworklet/dependent_module.rs @@ -0,0 +1,62 @@ +// This file is based on code from: +// https://github.com/rustwasm/wasm-bindgen/blob/main/examples/wasm-audio-worklet/src/dependent_module.rs +// +// The original code is licensed under either of: +// - MIT license (https://opensource.org/licenses/MIT) +// - Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0) +// at your option. +// +// Copyright (c) 2017-2024 The wasm-bindgen Developers +// +// This file incorporates code from the above source under the Apache License, Version 2.0 license. +// Please see the original repository for more details. +// +// See this issue for a further explanation of what this file does: https://github.com/rustwasm/wasm-bindgen/issues/3019 + +use js_sys::{wasm_bindgen, Array, JsString}; +use wasm_bindgen::prelude::*; +use web_sys::{Blob, BlobPropertyBag, Url}; + +// This is a not-so-clean approach to get the current bindgen ES module URL +// in Rust. This will fail at run time on bindgen targets not using ES modules. +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen] + type ImportMeta; + + #[wasm_bindgen(method, getter)] + fn url(this: &ImportMeta) -> JsString; + + #[wasm_bindgen(thread_local_v2, js_namespace = import, js_name = meta)] + static IMPORT_META: ImportMeta; +} + +pub fn on_the_fly(code: &str) -> Result { + // Generate the import of the bindgen ES module, assuming `--target web`. + let header = format!( + "import init, * as bindgen from '{}';\n\n", + IMPORT_META.with(ImportMeta::url), + ); + + let options = BlobPropertyBag::new(); + options.set_type("text/javascript"); + Url::create_object_url_with_blob(&Blob::new_with_str_sequence_and_options( + &Array::of2(&JsValue::from(header.as_str()), &JsValue::from(code)), + &options, + )?) +} + +// dependent_module! takes a local file name to a JS module as input and +// returns a URL to a slightly modified module in run time. This modified module +// has an additional import statement in the header that imports the current +// bindgen JS module under the `bindgen` alias, and the separate init function. +// How this URL is produced does not matter for the macro user. on_the_fly +// creates a blob URL in run time. A better, more sophisticated solution +// would add wasm_bindgen support to put such a module in pkg/ during build time +// and return a URL to this file instead (described in #3019). +#[macro_export] +macro_rules! dependent_module { + ($file_name:expr) => { + $crate::host::audioworklet::dependent_module::on_the_fly(include_str!($file_name)) + }; +} diff --git a/crates/vendor/cpal/src/host/audioworklet/mod.rs b/crates/vendor/cpal/src/host/audioworklet/mod.rs new file mode 100644 index 000000000..4c0f6c484 --- /dev/null +++ b/crates/vendor/cpal/src/host/audioworklet/mod.rs @@ -0,0 +1,651 @@ +//! Audio Worklet backend implementation. +//! +//! Available on WebAssembly with the `audioworklet` feature. Requires atomics support. +//! See the `audioworklet-beep` example for setup instructions. + +use std::{ + fmt, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + time::Duration, +}; + +use futures_channel::mpsc; +use futures_util::StreamExt as _; +use js_sys::wasm_bindgen; +use wasm_bindgen::prelude::*; + +use crate::{ + host::frames_to_duration, + traits::{DeviceTrait, HostTrait, StreamTrait}, + BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, + DeviceId, Error, ErrorKind, FrameCount, InputCallbackInfo, OutputCallbackInfo, + OutputStreamTimestamp, Sample, SampleFormat, SampleRate, StreamConfig, StreamInstant, + SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, +}; + +mod dependent_module; +use crate::dependent_module; + +/// Content is false if the iterator is empty. +pub struct Devices(bool); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Device; + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let desc = self.description().map_err(|_| fmt::Error)?; + f.write_str(desc.name()) + } +} + +pub struct Host; + +// `Stream`'s fields are all Send+Sync (a channel sender plus atomics). +// Any JS-backed resource (e.g. `web_sys::Window`, `Closure`) must live as a local +// inside the `spawn_local` task in `build_*_stream_raw`. +pub struct Stream { + command_tx: mpsc::UnboundedSender, + current_time_bits: Arc, + buffer_size_frames: Arc, +} + +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +enum Command { + Play, + Pause, +} + +/// How often the main thread re-reads `outputLatency` to publish it to the worklet. +const LATENCY_POLL_INTERVAL: Duration = Duration::from_millis(500); + +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; + +// https://webaudio.github.io/web-audio-api/#dom-audioworkletnode-audioworkletnode +const MIN_CHANNELS: ChannelCount = 1; +const MAX_CHANNELS: ChannelCount = 32; + +// https://webaudio.github.io/web-audio-api/#supported-sample-rates +const MIN_SAMPLE_RATE: SampleRate = 3_000; +const MAX_SAMPLE_RATE: SampleRate = 768_000; + +// https://webaudio.github.io/web-audio-api/#audio-processing-model +const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; + +// https://webaudio.github.io/web-audio-api/#render-quantum-size +const DEFAULT_RENDER_SIZE: u64 = 128; + +fn render_quantum_size_supported() -> bool { + (|| -> Option { + let global = js_sys::global(); + let ctor = js_sys::Reflect::get(&global, &JsValue::from("AudioContext")).ok()?; + let proto = js_sys::Reflect::get(&ctor, &JsValue::from("prototype")).ok()?; + js_sys::Reflect::has(&proto, &JsValue::from("renderQuantumSize")).ok() + })() + .unwrap_or(false) +} + +fn supported_render_quantum_range(sample_rate: SampleRate) -> SupportedBufferSize { + // https://webaudio.github.io/web-audio-api/#supported-render-quantum-sizes + if render_quantum_size_supported() { + SupportedBufferSize::Range { + min: 1, + max: sample_rate.saturating_mul(6), + } + } else { + SupportedBufferSize::Range { + min: DEFAULT_RENDER_SIZE as FrameCount, + max: DEFAULT_RENDER_SIZE as FrameCount, + } + } +} + +impl Host { + pub fn new() -> Result { + if Self::is_available() { + Ok(Host) + } else { + Err(Error::with_message( + ErrorKind::HostUnavailable, + "AudioWorklet is not available", + )) + } + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + if let Some(window) = web_sys::window() { + let has_audio_worklet = + js_sys::Reflect::has(&window, &JsValue::from_str("AudioWorklet")).unwrap_or(false); + + let cross_origin_isolated = + js_sys::Reflect::get(&window, &JsValue::from_str("crossOriginIsolated")) + .ok() + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + has_audio_worklet && cross_origin_isolated + } else { + false + } + } + + fn devices(&self) -> Result { + Devices::new() + } + + fn default_input_device(&self) -> Option { + // TODO + None + } + + fn default_output_device(&self) -> Option { + if Self::is_available() { + Some(Device) + } else { + None + } + } +} + +impl Devices { + fn new() -> Result { + Ok(Devices(Host::is_available())) + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Default Device") + .direction(DeviceDirection::Output) + .build()) + } + + fn id(&self) -> Result { + Ok(DeviceId::new( + crate::platform::HostId::AudioWorklet, + "default", + )) + } + + fn supported_input_configs(&self) -> Result { + // TODO + Ok(Vec::new().into_iter()) + } + + fn supported_output_configs(&self) -> Result { + // In actuality the number of supported channels cannot be fully known until + // the browser attempts to initialized the AudioWorklet. + + let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS) + .flat_map(|channels| { + crate::COMMON_SAMPLE_RATES + .iter() + .copied() + .filter(|&r| (MIN_SAMPLE_RATE..=MAX_SAMPLE_RATE).contains(&r)) + .map(move |rate| SupportedStreamConfigRange { + channels, + min_sample_rate: rate, + max_sample_rate: rate, + buffer_size: supported_render_quantum_range(rate), + sample_format: SUPPORTED_SAMPLE_FORMAT, + }) + }) + .collect(); + Ok(configs.into_iter()) + } + + fn default_input_config(&self) -> Result { + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Device does not support input", + )) + } + + fn default_output_config(&self) -> Result { + let range = self + .supported_output_configs()? + .max_by(|a, b| a.cmp_default_heuristics(b)) + .ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "No supported output configuration", + ) + })?; + let config = range + .try_with_standard_sample_rate() + .unwrap_or_else(|| range.with_max_sample_rate()); + + Ok(config) + } + + fn build_input_stream_raw( + &self, + _config: StreamConfig, + _sample_format: SampleFormat, + _data_callback: D, + _error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Device does not support input", + )) + } + + /// Create an output stream. + /// + /// # Async completion + /// + /// This function returns `Ok` synchronously once the [`AudioContext`] is created, before the + /// AudioWorklet module has been loaded or the [`AudioWorkletNode`] has been initialized. The + /// actual worklet setup runs asynchronously via [`wasm_bindgen_futures::spawn_local`]. If + /// setup fails (e.g. `add_module` or `AudioWorkletNode` construction throws), the error is + /// delivered to `error_callback` after the caller already holds a [`Stream`]. There is no + /// way to surface such errors synchronously given the Web Audio API's design. + /// + /// [`AudioContext`]: web_sys::AudioContext + /// [`AudioWorkletNode`]: web_sys::AudioWorkletNode + fn build_output_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + mut error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + crate::validate_stream_config(&config)?; + // Keep `playback` monotonic: the polled outputLatency can drop when the + // page calls `setSinkId()` to switch output devices, pulling `playback` backward. + let mut data_callback = crate::host::monotonic_output_callback(data_callback); + if config.channels > MAX_CHANNELS { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Channel count {} exceeds the maximum of {MAX_CHANNELS}", + config.channels + ), + )); + } + if sample_format != SUPPORTED_SAMPLE_FORMAT { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Sample format {sample_format} is not supported; required format is {SUPPORTED_SAMPLE_FORMAT}" + ), + )); + } + if !(MIN_SAMPLE_RATE..=MAX_SAMPLE_RATE).contains(&config.sample_rate) { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Sample rate {} Hz is not in the supported range {MIN_SAMPLE_RATE}..={MAX_SAMPLE_RATE} Hz", + config.sample_rate + ), + )); + } + + if let BufferSize::Fixed(n) = config.buffer_size { + if let SupportedBufferSize::Range { min, max } = + supported_render_quantum_range(config.sample_rate) + { + if !(min..=max).contains(&n) { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Buffer size {n} is not in the supported render quantum range {min}..={max}" + ), + )); + } + } + } + + let stream_opts = web_sys::AudioContextOptions::new(); + stream_opts.set_sample_rate(config.sample_rate as f32); + if let BufferSize::Fixed(n) = config.buffer_size { + let _ = js_sys::Reflect::set( + stream_opts.as_ref(), + &JsValue::from_str("renderSizeHint"), + &JsValue::from_f64(n as f64), + ); + } + + let audio_context = + web_sys::AudioContext::new_with_context_options(&stream_opts).map_err(|_| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "Failed to create audio context", + ) + })?; + + let destination = audio_context.destination(); + + // Chrome rounds renderSizeHint to a power of two; read back the actual quantum. + let actual_render_quantum = + js_sys::Reflect::get(audio_context.as_ref(), &JsValue::from("renderQuantumSize")) + .ok() + .and_then(|v| v.as_f64()) + .map(|v| v as u64); + + if config.channels as u32 > destination.max_channel_count() { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Channel count {} exceeds the destination's maximum of {}", + config.channels, + destination.max_channel_count() + ), + )); + } + destination.set_channel_count(config.channels as u32); + + let initial_quantum = actual_render_quantum.unwrap_or(match config.buffer_size { + BufferSize::Fixed(n) => n as u64, + BufferSize::Default => DEFAULT_RENDER_SIZE, + }); + let buffer_size_frames = Arc::new(AtomicU64::new(initial_quantum)); + let buffer_size_frames_cb = buffer_size_frames.clone(); + + let current_time_bits = Arc::new(AtomicU64::new(audio_context.current_time().to_bits())); + let current_time_bits_cb = current_time_bits.clone(); + let current_time_bits_init = current_time_bits.clone(); + + let (command_tx, mut command_rx) = mpsc::unbounded::(); + + // The worklet realm cannot read AudioContext properties, so share the value via an atomic. + let latency_nanos = Arc::new(AtomicU64::new(total_latency_nanos(&audio_context))); + let latency_nanos_cb = latency_nanos.clone(); + let ctx = audio_context.clone(); + wasm_bindgen_futures::spawn_local(async move { + let result: Result<(), JsValue> = async move { + let mod_url = dependent_module!("worklet.js")?; + wasm_bindgen_futures::JsFuture::from(ctx.audio_worklet()?.add_module(&mod_url)?) + .await?; + + let options = web_sys::AudioWorkletNodeOptions::new(); + + let js_array = js_sys::Array::new(); + js_array.push(&JsValue::from_f64(destination.channel_count() as _)); + + options.set_output_channel_count(&js_array); + options.set_number_of_inputs(0); + + options.set_processor_options(Some(&js_sys::Array::of3( + &wasm_bindgen::module(), + &wasm_bindgen::memory(), + &WasmAudioProcessor::new(Box::new( + move |interleaved_data, frame_size, sample_rate, now| { + buffer_size_frames_cb.store(frame_size as u64, Ordering::Relaxed); + current_time_bits_cb.store(now.to_bits(), Ordering::Relaxed); + let data = interleaved_data.as_mut_ptr() as *mut (); + let mut data = unsafe { + Data::from_parts(data, interleaved_data.len(), sample_format) + }; + + let callback = StreamInstant::from_secs_f64(now); + let buffer_duration = + frames_to_duration(frame_size as FrameCount, sample_rate); + let latency = + Duration::from_nanos(latency_nanos_cb.load(Ordering::Relaxed)); + let playback = callback + (buffer_duration + latency); + let timestamp = OutputStreamTimestamp { callback, playback }; + let info = OutputCallbackInfo { timestamp }; + (data_callback)(&mut data, &info); + }, + )) + .pack() + .into(), + ))); + // This name 'CpalProcessor' must match the name registered in worklet.js + let audio_worklet_node = + web_sys::AudioWorkletNode::new_with_options(&ctx, "CpalProcessor", &options)?; + + audio_worklet_node.connect_with_audio_node(&destination)?; + Ok(()) + } + .await; + + if let Err(err) = result { + let message = err + .as_string() + .unwrap_or_else(|| "Failed to initialize audio worklet".to_string()); + error_callback(Error::with_message( + ErrorKind::UnsupportedOperation, + message, + )); + + // Close AudioContext and exit; dropping command_rx closes the channel, + // so subsequent play()/pause() calls return HostUnavailable. + let _ = audio_context.close(); + return; + } + + current_time_bits_init.store(audio_context.current_time().to_bits(), Ordering::Relaxed); + + // outputLatency can change at runtime (e.g. an output-device switch) but is only + // readable on the main thread, so poll it here and publish it to the worklet via the + // shared atomic. + let _latency_poller = web_sys::window().and_then(|window| { + let poll_ctx = audio_context.clone(); + let poll_latency = latency_nanos.clone(); + let closure = Closure::::new(move || { + poll_latency.store(total_latency_nanos(&poll_ctx), Ordering::Relaxed); + }); + window + .set_interval_with_callback_and_timeout_and_arguments_0( + closure.as_ref().unchecked_ref(), + LATENCY_POLL_INTERVAL.as_millis() as i32, + ) + .ok() + .map(|interval_id| LatencyPoller { + window, + interval_id, + _closure: closure, + }) + }); + + // Process play/pause commands from any thread until Stream is dropped. + // Dropping Stream closes command_tx, which terminates this loop. + while let Some(cmd) = command_rx.next().await { + match cmd { + Command::Play => { + if audio_context.resume().is_err() { + error_callback(Error::with_message( + ErrorKind::DeviceNotAvailable, + "Failed to resume audio context", + )); + } + } + Command::Pause => { + if audio_context.suspend().is_err() { + error_callback(Error::with_message( + ErrorKind::DeviceNotAvailable, + "Failed to suspend audio context", + )); + } + } + } + } + + // Stream dropped: close the AudioContext on the main thread. + let _ = audio_context.close(); + }); + + Ok(Self::Stream { + command_tx, + current_time_bits, + buffer_size_frames, + }) + } +} + +impl StreamTrait for Stream { + fn buffer_size(&self) -> Result { + Ok(self.buffer_size_frames.load(Ordering::Relaxed) as FrameCount) + } + + fn play(&self) -> Result<(), Error> { + self.command_tx.unbounded_send(Command::Play).map_err(|_| { + Error::with_message( + ErrorKind::HostUnavailable, + "audio worklet initialization failed", + ) + }) + } + + fn pause(&self) -> Result<(), Error> { + self.command_tx.unbounded_send(Command::Pause).map_err(|_| { + Error::with_message( + ErrorKind::HostUnavailable, + "audio worklet initialization failed", + ) + }) + } + + fn now(&self) -> StreamInstant { + StreamInstant::from_secs_f64(f64::from_bits( + self.current_time_bits.load(Ordering::Relaxed), + )) + } +} + +impl Iterator for Devices { + type Item = Device; + + fn next(&mut self) -> Option { + if self.0 { + self.0 = false; + Some(Device) + } else { + None + } + } +} + +type AudioProcessorCallback = Box; + +/// WasmAudioProcessor provides an interface for the JavaScript code +/// running in the AudioWorklet to interact with Rust. +#[wasm_bindgen] +pub struct WasmAudioProcessor { + interleaved_buffer: Vec, + // Passes in an interleaved scratch buffer, frame size, sample rate, and current time. + callback: AudioProcessorCallback, +} + +impl WasmAudioProcessor { + pub fn new(callback: AudioProcessorCallback) -> Self { + Self { + interleaved_buffer: Vec::new(), + callback, + } + } +} + +#[wasm_bindgen] +impl WasmAudioProcessor { + pub fn process( + &mut self, + channels: u32, + frame_size: u32, + sample_rate: u32, + current_time: f64, + ) -> u32 { + let frame_size = frame_size as usize; + + // Ensure there's enough space in the output buffer + // This likely only occurs once, or very few times. + let interleaved_buffer_size = channels as usize * frame_size; + self.interleaved_buffer.resize( + interleaved_buffer_size.max(self.interleaved_buffer.len()), + f32::EQUILIBRIUM, + ); + + self.interleaved_buffer[..interleaved_buffer_size].fill(f32::EQUILIBRIUM); + + (self.callback)( + &mut self.interleaved_buffer[..interleaved_buffer_size], + frame_size as u32, + sample_rate, + current_time, + ); + + // Returns a pointer to the raw interleaved buffer to Javascript so + // it can deinterleave it into the output buffers. + // + // Deinterleaving is done on the Javascript side because it's simpler and it may be faster. + // Doing it this way avoids an extra copy and the JS deinterleaving code + // is likely heavily optimized by the browser's JS engine, + // although I have not tested that assumption. + self.interleaved_buffer.as_mut_ptr() as _ + } + + /// Converts this `WasmAudioProcessor` into a raw pointer (as `usize`) for FFI use. + /// + /// Transfers ownership of the processor to the caller. The returned pointer must be passed to + /// [`unpack`] exactly once. Failing to call [`unpack`] will leak the allocation. + /// + /// [`unpack`]: Self::unpack + pub fn pack(self) -> usize { + Box::into_raw(Box::new(self)) as usize + } + /// # Safety + /// + /// The `val` parameter must be a value previously returned by `Self::pack`. + /// It must not have already been unpacked or deallocated, and must not be used after this call. + /// Using an invalid or already-consumed pointer will result in undefined behavior. + pub unsafe fn unpack(val: usize) -> Self { + *Box::from_raw(val as *mut _) + } +} + +/// Drives a `setInterval` that refreshes the shared output-latency value. +struct LatencyPoller { + window: web_sys::Window, + interval_id: i32, + _closure: Closure, +} + +impl Drop for LatencyPoller { + fn drop(&mut self) { + self.window.clear_interval_with_handle(self.interval_id); + } +} + +/// Reads the playback buffer depth from a context. +fn total_latency_nanos(ctx: &web_sys::AudioContext) -> u64 { + let read = |key: &str| { + js_sys::Reflect::get(ctx.as_ref(), &JsValue::from(key)) + .ok() + .and_then(|v| v.as_f64()) + .unwrap_or(0.0) + }; + // `baseLatency` is fixed for the context lifetime; `outputLatency` can change. + let secs = read("baseLatency") + read("outputLatency"); + if secs.is_finite() && secs > 0.0 { + (secs * 1_000_000_000.0).round() as u64 + } else { + 0 + } +} diff --git a/crates/vendor/cpal/src/host/audioworklet/worklet.js b/crates/vendor/cpal/src/host/audioworklet/worklet.js new file mode 100644 index 000000000..6c604e617 --- /dev/null +++ b/crates/vendor/cpal/src/host/audioworklet/worklet.js @@ -0,0 +1,47 @@ +registerProcessor( + 'CpalProcessor', + class WasmProcessor extends AudioWorkletProcessor { + constructor(options) { + super(); + let [module, memory, handle] = options.processorOptions; + bindgen.initSync({ module, memory }); + this.processor = bindgen.WasmAudioProcessor.unpack(handle); + this.memory = memory; + this.wasm_memory = new Float32Array(memory.buffer); + } + + process(inputs, outputs) { + // Check if memory grew and update view + if (this.wasm_memory.buffer !== this.memory.buffer) { + this.wasm_memory = new Float32Array(this.memory.buffer); + } + + const channels = outputs[0]; + const channels_count = channels.length; + const frame_size = channels[0].length; + const interleaved_ptr = this.processor.process(channels_count, frame_size, sampleRate, currentTime); + + const interleaved_start = interleaved_ptr / 4; // Convert byte offset to f32 index + const interleaved = this.wasm_memory; + + const total_samples = frame_size * channels_count; + if (interleaved_start + total_samples > this.wasm_memory.length) { + console.error('CpalProcessor: Audio buffer out of bounds! Ptr:', interleaved_ptr, 'Len:', total_samples); + return false; // Safely stop the node + } + + // Deinterleave: read strided from Wasm, write sequential to output + for (let ch = 0; ch < channels_count; ch++) { + const channel = channels[ch]; + let read_pos = interleaved_start + ch; + + for (let i = 0; i < frame_size; i++) { + channel[i] = interleaved[read_pos]; + read_pos += channels_count; + } + } + + return true; + } + }, +); diff --git a/crates/vendor/cpal/src/host/com.rs b/crates/vendor/cpal/src/host/com.rs new file mode 100644 index 000000000..db76bfc2e --- /dev/null +++ b/crates/vendor/cpal/src/host/com.rs @@ -0,0 +1,65 @@ +//! Handles COM initialization and cleanup. + +use std::{io::Error as IoError, marker::PhantomData}; + +use windows::Win32::{ + Foundation::RPC_E_CHANGED_MODE, + System::Com::{CoInitializeEx, CoTaskMemFree, CoUninitialize, COINIT_APARTMENTTHREADED}, +}; + +thread_local!(static COM_INITIALIZED: ComInitialized = { + unsafe { + // Try to initialize COM with STA by default to avoid compatibility issues with the ASIO + // backend (where CoInitialize() is called by the ASIO SDK) or winit (where drag and drop + // requires STA). + // This call can fail with RPC_E_CHANGED_MODE if another library initialized COM with MTA. + // That's OK though since COM ensures thread-safety/compatibility through marshalling when + // necessary. + let result = CoInitializeEx(None, COINIT_APARTMENTTHREADED); + if result.is_ok() || result == RPC_E_CHANGED_MODE { + ComInitialized { + result, + _ptr: PhantomData, + } + } else { + // COM initialization failed in another way, something is really wrong. + panic!( + "Failed to initialize COM: {}", + IoError::from_raw_os_error(result.0) + ); + } + } +}); + +/// RAII object that guards the fact that COM is initialized. +/// +// We store a raw pointer because it's the only way at the moment to remove `Send`/`Sync` from the +// object. +struct ComInitialized { + result: windows::core::HRESULT, + _ptr: PhantomData<*mut ()>, +} + +impl Drop for ComInitialized { + fn drop(&mut self) { + // Need to avoid calling CoUninitialize() if CoInitializeEx failed since it may have + // returned RPC_E_MODE_CHANGED - which is OK, see above. + if self.result.is_ok() { + unsafe { CoUninitialize() }; + } + } +} + +/// RAII wrapper for COM-allocated wide strings freed with `CoTaskMemFree`. +pub(super) struct ComString(pub windows::core::PWSTR); + +impl Drop for ComString { + fn drop(&mut self) { + unsafe { CoTaskMemFree(Some(self.0.as_ptr() as *mut _)) } + } +} + +/// Ensures that COM is initialized in this thread. +pub fn com_initialized() { + COM_INITIALIZED.with(|_| {}); +} diff --git a/crates/vendor/cpal/src/host/coreaudio/ios/enumerate.rs b/crates/vendor/cpal/src/host/coreaudio/ios/enumerate.rs new file mode 100644 index 000000000..32afcaa5a --- /dev/null +++ b/crates/vendor/cpal/src/host/coreaudio/ios/enumerate.rs @@ -0,0 +1,23 @@ +use std::vec::IntoIter as VecIntoIter; + +use super::Device; +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; + +// TODO: Support enumerating earpiece vs headset vs speaker etc? +pub struct Devices(pub(super) VecIntoIter); + +impl Iterator for Devices { + type Item = Device; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +pub fn default_input_device() -> Option { + Some(Device) +} + +pub fn default_output_device() -> Option { + Some(Device) +} diff --git a/crates/vendor/cpal/src/host/coreaudio/ios/mod.rs b/crates/vendor/cpal/src/host/coreaudio/ios/mod.rs new file mode 100644 index 000000000..8f8dc0b8c --- /dev/null +++ b/crates/vendor/cpal/src/host/coreaudio/ios/mod.rs @@ -0,0 +1,726 @@ +//! CoreAudio implementation for iOS using AVAudioSession and RemoteIO Audio Units. + +use std::{ + fmt, + ptr::NonNull, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, + time::Duration, +}; + +use coreaudio::audio_unit::{ + render_callback::{self, data}, + AudioUnit, Element, Scope, +}; +use objc2_audio_toolbox::{kAudioOutputUnitProperty_EnableIO, kAudioUnitProperty_StreamFormat}; +use objc2_avf_audio::AVAudioSession; +use objc2_core_audio_types::AudioBuffer; + +use self::enumerate::{ + default_input_device, default_output_device, Devices, SupportedInputConfigs, + SupportedOutputConfigs, +}; +use super::{asbd_from_config, host_time_to_stream_instant}; +use crate::{ + host::{ + equilibrium::fill_equilibrium, frames_to_duration, latch::Latch, try_emit_error, + ErrorCallbackArc, + }, + traits::{DeviceTrait, HostTrait, StreamTrait}, + BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error, + ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, OutputCallbackInfo, + OutputStreamTimestamp, ResultExt, SampleFormat, SampleRate, StreamConfig, StreamInstant, + SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, +}; + +pub mod enumerate; +mod session_event_manager; +use session_event_manager::SessionEventManager; + +// These days the default of iOS is now F32 and no longer I16 +const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Device; + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let desc = self.description().map_err(|_| fmt::Error)?; + f.write_str(desc.name()) + } +} + +pub struct Host; + +impl Host { + pub fn new() -> Result { + Ok(Host) + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + true + } + + fn devices(&self) -> Result { + Ok(Devices(vec![Device].into_iter())) + } + + fn default_input_device(&self) -> Option { + default_input_device() + } + + fn default_output_device(&self) -> Option { + default_output_device() + } +} + +impl Device { + fn description(&self) -> Result { + // Query AVAudioSession to determine actual input/output availability + // SAFETY: AVAudioSession::sharedInstance() returns the global audio session singleton + let direction = unsafe { + let audio_session = AVAudioSession::sharedInstance(); + let input_channels = Some(audio_session.inputNumberOfChannels() as ChannelCount); + let output_channels = Some(audio_session.outputNumberOfChannels() as ChannelCount); + + crate::device_description::direction_from_counts(input_channels, output_channels) + }; + + Ok(DeviceDescriptionBuilder::new("Default Device") + .direction(direction) + .build()) + } + + fn id(&self) -> Result { + Ok(DeviceId::new(crate::platform::HostId::CoreAudio, "default")) + } + + fn supported_input_configs(&self) -> Result { + Ok(get_supported_stream_configs(true)) + } + + fn supported_output_configs(&self) -> Result { + Ok(get_supported_stream_configs(false)) + } + + fn default_input_config(&self) -> Result { + // Get the primary (exact channel count) config from supported configs + let range = get_supported_stream_configs(true).next().ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "No supported input configuration", + ) + })?; + Ok(range + .try_with_standard_sample_rate() + .unwrap_or_else(|| range.with_max_sample_rate())) + } + + fn default_output_config(&self) -> Result { + let range = get_supported_stream_configs(false) + .max_by(|a, b| a.cmp_default_heuristics(b)) + .ok_or_else(|| { + Error::with_message( + ErrorKind::UnsupportedConfig, + "No supported output configuration", + ) + })?; + Ok(range + .try_with_standard_sample_rate() + .unwrap_or_else(|| range.with_max_sample_rate())) + } +} + +#[derive(Clone, Copy, Default, PartialEq, Eq)] +enum PlaybackState { + #[default] + Stopped, + Playing, + Interrupted, +} + +struct StreamInner { + state: PlaybackState, + audio_unit: AudioUnit, +} + +impl StreamInner { + fn stop_for_interruption(&mut self) { + if self.state != PlaybackState::Playing { + return; + } + // Unlike pause(), the OS is the one that actually halted the unit here; this + // call only resyncs AudioUnit's own bookkeeping, so its result carries nothing. + let _ = self.audio_unit.stop(); + self.state = PlaybackState::Interrupted; + } + + fn resume_after_interruption(&mut self) { + if self.state != PlaybackState::Interrupted { + return; + } + if self.audio_unit.start().is_ok() { + self.state = PlaybackState::Playing; + } + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn description(&self) -> Result { + Device::description(self) + } + + fn id(&self) -> Result { + Device::id(self) + } + + fn supported_input_configs(&self) -> Result { + Device::supported_input_configs(self) + } + + fn supported_output_configs(&self) -> Result { + Device::supported_output_configs(self) + } + + fn default_input_config(&self) -> Result { + Device::default_input_config(self) + } + + fn default_output_config(&self) -> Result { + Device::default_output_config(self) + } + + fn build_input_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + crate::validate_stream_config(&config)?; + // Keep `capture` monotonic: a route change (e.g. the user connects a headset) can + // raise the input buffer depth, pulling `capture` backward. + let data_callback = crate::host::monotonic_input_callback(data_callback); + // Configure buffer size and create audio unit + let mut audio_unit = setup_stream_audio_unit(config, sample_format, true)?; + // Buffer depth used to offset capture timestamps. AVAudioSession auto-reroutes to a new device, + // which changes this depth, and is then refreshed by the session event manager. + let latency_frames = Arc::new(AtomicUsize::new(input_latency_frames())); + + let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); + let session_error_callback = error_callback.clone(); + let session_latency_frames = latency_frames.clone(); + + // Set up input callback + setup_input_callback( + &mut audio_unit, + sample_format, + config.sample_rate, + latency_frames, + data_callback, + move |e| { + let _ = try_emit_error(&error_callback, e); + }, + )?; + + let inner = Arc::new(Mutex::new(StreamInner { + state: PlaybackState::default(), + audio_unit, + })); + let session_manager = SessionEventManager::new( + session_error_callback, + Latch::new(), + Some((session_latency_frames, true)), + Arc::downgrade(&inner), + ); + let stream = Stream { + inner, + session_manager, + }; + stream.signal_ready(); + Ok(stream) + } + + /// Create an output stream. + fn build_output_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + crate::validate_stream_config(&config)?; + // Keep `playback` monotonic: a route change (e.g. the user disconnects a headset + // and audio falls back to the built-in speaker) can lower the output buffer + // depth, pulling `playback` backward. + let data_callback = crate::host::monotonic_output_callback(data_callback); + // Configure buffer size and create audio unit + let mut audio_unit = setup_stream_audio_unit(config, sample_format, false)?; + + // Buffer depth used to offset playback timestamps. AVAudioSession auto-reroutes to a new device, + // which changes this depth, and is then refreshed by the session event manager. + let latency_frames = Arc::new(AtomicUsize::new(output_latency_frames())); + + let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); + let session_error_callback = error_callback.clone(); + let session_latency_frames = latency_frames.clone(); + + // Set up output callback + setup_output_callback( + &mut audio_unit, + sample_format, + config.sample_rate, + latency_frames, + data_callback, + move |e| { + let _ = try_emit_error(&error_callback, e); + }, + )?; + + let inner = Arc::new(Mutex::new(StreamInner { + state: PlaybackState::default(), + audio_unit, + })); + let session_manager = SessionEventManager::new( + session_error_callback, + Latch::new(), + Some((session_latency_frames, false)), + Arc::downgrade(&inner), + ); + let stream = Stream { + inner, + session_manager, + }; + stream.signal_ready(); + Ok(stream) + } +} + +pub struct Stream { + inner: Arc>, + session_manager: SessionEventManager, +} + +impl Stream { + fn signal_ready(&self) { + self.session_manager.signal_ready(); + } +} + +impl Drop for Stream { + fn drop(&mut self) { + // Ensure the latch is released even if signal_ready() was never called (error path). + self.session_manager.signal_ready(); + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), Error> { + let mut stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") + })?; + if stream.state != PlaybackState::Playing { + stream + .audio_unit + .start() + .context("Failed to start audio unit")?; + stream.state = PlaybackState::Playing; + } + Ok(()) + } + + fn pause(&self) -> Result<(), Error> { + let mut stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") + })?; + if stream.state == PlaybackState::Playing { + stream + .audio_unit + .stop() + .context("Failed to stop audio unit")?; + } + stream.state = PlaybackState::Stopped; + Ok(()) + } + + fn now(&self) -> StreamInstant { + let m_host_time = unsafe { mach2::mach_time::mach_absolute_time() }; + host_time_to_stream_instant(m_host_time).expect("mach_timebase_info failed") + } + + fn buffer_size(&self) -> Result { + Ok(get_device_buffer_frames() as FrameCount) + } +} + +fn create_audio_unit() -> Result { + AudioUnit::new_uninitialized(coreaudio::audio_unit::IOType::RemoteIO) +} + +fn configure_for_recording(audio_unit: &mut AudioUnit) -> Result<(), coreaudio::Error> { + // Enable mic recording + let enable_input = 1u32; + audio_unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Input, + Element::Input, + Some(&enable_input), + )?; + + // Disable output + let disable_output = 0u32; + audio_unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Output, + Element::Output, + Some(&disable_output), + )?; + + Ok(()) +} + +/// Configure AVAudioSession with the requested buffer size. +/// +/// Note: iOS may not honor the exact request due to system constraints. +fn set_audio_session_buffer_size( + buffer_size: FrameCount, + sample_rate: SampleRate, +) -> Result<(), Error> { + // SAFETY: AVAudioSession::sharedInstance() returns the global audio session singleton + let audio_session = unsafe { AVAudioSession::sharedInstance() }; + + // Calculate preferred buffer duration in seconds + let buffer_duration = buffer_size as f64 / sample_rate as f64; + + // Set the preferred IO buffer duration + // SAFETY: setPreferredIOBufferDuration_error is safe to call with valid duration + unsafe { + audio_session + .setPreferredIOBufferDuration_error(buffer_duration) + .map_err(|e| { + Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Failed to set preferred I/O buffer duration: {e}"), + ) + })?; + } + + Ok(()) +} + +/// Get the actual buffer size from AVAudioSession. +/// +/// This queries the current IO buffer duration from AVAudioSession and converts +/// it to frames based on the current sample rate. +fn get_device_buffer_frames() -> usize { + // SAFETY: AVAudioSession methods are safe to call on the singleton instance + unsafe { + let audio_session = AVAudioSession::sharedInstance(); + let buffer_duration = audio_session.IOBufferDuration(); + let sample_rate = audio_session.sampleRate(); + // Round: the duration comes from an integer frame count, so the product is a whole number + // that floating-point can render as N.9999, which truncation would drop to N-1. + (buffer_duration * sample_rate).round() as usize + } +} + +/// Total capture buffer depth in frames: the IO buffer plus the hardware input latency. +pub(super) fn input_latency_frames() -> usize { + // SAFETY: AVAudioSession methods are safe to call on the singleton instance + let extra = unsafe { + let audio_session = AVAudioSession::sharedInstance(); + (audio_session.inputLatency() * audio_session.sampleRate()).round() as usize + }; + get_device_buffer_frames() + extra +} + +/// Total playback buffer depth in frames: the IO buffer plus the hardware output latency. +pub(super) fn output_latency_frames() -> usize { + // SAFETY: AVAudioSession methods are safe to call on the singleton instance + let extra = unsafe { + let audio_session = AVAudioSession::sharedInstance(); + (audio_session.outputLatency() * audio_session.sampleRate()).round() as usize + }; + get_device_buffer_frames() + extra +} + +// Typical iOS hardware buffer frame limits according to Apple Technical Q&A QA1631. +const BUFFER_SIZE_MIN: FrameCount = 256; +const BUFFER_SIZE_MAX: FrameCount = 4096; + +/// Get supported stream config ranges for input (is_input=true) or output (is_input=false). +fn get_supported_stream_configs(is_input: bool) -> std::vec::IntoIter { + // SAFETY: AVAudioSession methods are safe to call on the singleton instance + let (sample_rate, max_channels) = unsafe { + let audio_session = AVAudioSession::sharedInstance(); + let sample_rate = audio_session.sampleRate() as SampleRate; + let max_channels = if is_input { + audio_session.inputNumberOfChannels() as ChannelCount + } else { + audio_session.outputNumberOfChannels() as ChannelCount + }; + (sample_rate, max_channels) + }; + + let buffer_size = SupportedBufferSize::Range { + min: BUFFER_SIZE_MIN, + max: BUFFER_SIZE_MAX, + }; + + // For input, only return the exact channel count (no flexibility) + // For output, support flexible channel counts up to the hardware maximum + let min_channels = if is_input { max_channels } else { 1 }; + + let configs: Vec<_> = (min_channels..=max_channels) + .map(|channels| SupportedStreamConfigRange { + channels, + min_sample_rate: sample_rate, + max_sample_rate: sample_rate, + buffer_size, + sample_format: SUPPORTED_SAMPLE_FORMAT, + }) + .collect(); + + configs.into_iter() +} + +/// Setup audio unit with common configuration for input or output streams. +fn setup_stream_audio_unit( + config: StreamConfig, + sample_format: SampleFormat, + is_input: bool, +) -> Result { + if let BufferSize::Fixed(buffer_size) = config.buffer_size { + if !(BUFFER_SIZE_MIN..=BUFFER_SIZE_MAX).contains(&buffer_size) { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Buffer size {buffer_size} is not in the supported range \ + {BUFFER_SIZE_MIN}..={BUFFER_SIZE_MAX}" + ), + )); + } + set_audio_session_buffer_size(buffer_size, config.sample_rate)?; + } + + let mut audio_unit = create_audio_unit()?; + + if is_input { + configure_for_recording(&mut audio_unit)?; + } + + // Set the stream format in interleaved mode + // For input: Output scope of Input element (data coming out of input) + // For output: Input scope of Output element (data going into output) + let (scope, element) = if is_input { + (Scope::Output, Element::Input) + } else { + (Scope::Input, Element::Output) + }; + + let asbd = asbd_from_config(config, sample_format); + audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?; + + audio_unit.initialize()?; + + Ok(audio_unit) +} + +/// Extract AudioBuffer and convert to Data, handling differences between input and output. +/// +/// # Safety +/// +/// Caller must ensure: +/// - `args.data.data` points to valid AudioBufferList +/// - For input: AudioBufferList has at least one buffer +/// - Buffer data remains valid for the callback duration +#[inline] +unsafe fn extract_audio_buffer( + args: &render_callback::Args, + bytes_per_channel: usize, + sample_format: SampleFormat, + is_input: bool, +) -> (AudioBuffer, Data) { + let buffer = if is_input { + // Input: access through buffer array + let first_buf_ptr = core::ptr::addr_of!((*args.data.data).mBuffers) as *const AudioBuffer; + core::ptr::read_unaligned(first_buf_ptr) + } else { + // Output: direct access + let buf_ptr = core::ptr::addr_of!((*args.data.data).mBuffers[0]); + core::ptr::read_unaligned(buf_ptr) + }; + + let mut data_ptr = buffer.mData as *mut (); + let mut len = buffer.mDataByteSize as usize / bytes_per_channel; + + // SAFETY: slice::from_raw_parts requires a non-null pointer. + if data_ptr.is_null() { + data_ptr = NonNull::dangling().as_ptr(); + len = 0; + } + + let data = Data::from_parts(data_ptr, len, sample_format); + + (buffer, data) +} + +/// Setup input callback with proper latency calculation. +fn setup_input_callback( + audio_unit: &mut AudioUnit, + sample_format: SampleFormat, + sample_rate: SampleRate, + latency_frames: Arc, + mut data_callback: D, + mut error_callback: E, +) -> Result<(), Error> +where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, +{ + let bytes_per_channel = sample_format.sample_size(); + type Args = render_callback::Args; + + audio_unit.set_input_callback(move |args: Args| { + // SAFETY: CoreAudio provides valid AudioBufferList for the callback duration + let (buffer, data) = + unsafe { extract_audio_buffer(&args, bytes_per_channel, sample_format, true) }; + + let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { + Err(err) => { + error_callback(err); + return Err(()); + } + Ok(cb) => cb, + }; + + // Refreshed on route changes by the session event manager; fall back to this buffer's + // own frame count if the depth is unknown (zero). + let latency_frames = match latency_frames.load(Ordering::Relaxed) { + 0 => { + let channels = buffer.mNumberChannels as usize; + data.len().checked_div(channels).unwrap_or(0) + } + n => n, + }; + let delay = frames_to_duration(latency_frames as FrameCount, sample_rate); + let capture = callback.checked_sub(delay).unwrap_or(StreamInstant::ZERO); + let timestamp = InputStreamTimestamp { callback, capture }; + + let info = InputCallbackInfo { timestamp }; + data_callback(&data, &info); + Ok(()) + })?; + + Ok(()) +} + +/// Setup output callback with proper latency calculation. +fn setup_output_callback( + audio_unit: &mut AudioUnit, + sample_format: SampleFormat, + sample_rate: SampleRate, + latency_frames: Arc, + mut data_callback: D, + mut error_callback: E, +) -> Result<(), Error> +where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, +{ + let bytes_per_channel = sample_format.sample_size(); + type Args = render_callback::Args; + + audio_unit.set_render_callback(move |args: Args| { + // SAFETY: CoreAudio provides valid AudioBufferList for the callback duration + let (buffer, mut data) = + unsafe { extract_audio_buffer(&args, bytes_per_channel, sample_format, false) }; + + if !buffer.mData.is_null() { + let bytes = unsafe { + std::slice::from_raw_parts_mut( + buffer.mData as *mut u8, + buffer.mDataByteSize as usize, + ) + }; + fill_equilibrium(bytes, sample_format); + } + + let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { + Err(err) => { + error_callback(err); + return Err(()); + } + Ok(cb) => cb, + }; + + // Refreshed on route changes by the session event manager; fall back to this buffer's + // own frame count if the depth is unknown (zero). + let latency_frames = match latency_frames.load(Ordering::Relaxed) { + 0 => { + let channels = buffer.mNumberChannels as usize; + data.len().checked_div(channels).unwrap_or(0) + } + n => n, + }; + let delay = frames_to_duration(latency_frames as FrameCount, sample_rate); + let playback = callback + delay; + let timestamp = OutputStreamTimestamp { callback, playback }; + + let info = OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); + Ok(()) + })?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::{BufferSize, SampleRate, StreamConfig}; + + #[test] + fn test_ios_fixed_buffer_size() { + let host = crate::default_host(); + let device = host.default_output_device().unwrap(); + + let config = StreamConfig { + channels: 2, + sample_rate: SampleRate(48000), + buffer_size: BufferSize::Fixed(512), + }; + + let result = device.build_output_stream( + &config, + |_data: &mut [f32], _info: &OutputCallbackInfo| {}, + |_err| {}, + None, + ); + + assert!( + result.is_ok(), + "BufferSize::Fixed should be supported on iOS via AVAudioSession" + ); + } +} diff --git a/crates/vendor/cpal/src/host/coreaudio/ios/session_event_manager.rs b/crates/vendor/cpal/src/host/coreaudio/ios/session_event_manager.rs new file mode 100644 index 000000000..ea5c6f5b5 --- /dev/null +++ b/crates/vendor/cpal/src/host/coreaudio/ios/session_event_manager.rs @@ -0,0 +1,221 @@ +//! Monitors AVAudioSession lifecycle events, recovering the stream from an interruption and +//! reporting the rest as stream errors. + +use std::{ + ptr::NonNull, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, Weak, + }, +}; + +use block2::RcBlock; +use objc2_avf_audio::{ + AVAudioSession, AVAudioSessionInterruptionNotification, AVAudioSessionInterruptionOptionKey, + AVAudioSessionInterruptionOptions, AVAudioSessionInterruptionType, + AVAudioSessionInterruptionTypeKey, AVAudioSessionMediaServicesWereLostNotification, + AVAudioSessionMediaServicesWereResetNotification, AVAudioSessionRouteChangeNotification, + AVAudioSessionRouteChangeReason, AVAudioSessionRouteChangeReasonKey, +}; +use objc2_foundation::{NSNotification, NSNotificationCenter, NSNumber, NSString}; + +use super::{input_latency_frames, output_latency_frames, StreamInner}; +use crate::{ + host::{emit_error, latch::Latch, ErrorCallbackArc}, + Error, ErrorKind, +}; + +/// Runs `f` against the stream, if it is still alive and its lock is intact. +fn with_stream(stream: &Weak>, f: impl FnOnce(&mut StreamInner)) { + if let Some(inner) = stream.upgrade() { + if let Ok(mut inner) = inner.lock() { + f(&mut inner); + } + } +} + +/// Reads the number stored under `key` in a notification's `userInfo`. +fn user_info_number(notification: &NSNotification, key: Option<&NSString>) -> Option { + let user_info = notification.userInfo()?; + let value = user_info.objectForKey(key?)?; + Some(value.downcast::().ok()?.unsignedIntegerValue()) +} + +/// Shared buffer-depth value to refresh on route changes, paired with `is_input` to select the +/// input or output latency. `true` means an input stream. +type LatencyRefresh = (Arc, bool); + +fn route_change_error(notification: &NSNotification) -> Option { + let key = unsafe { AVAudioSessionRouteChangeReasonKey }; + let reason = AVAudioSessionRouteChangeReason(user_info_number(notification, key)?); + match reason { + AVAudioSessionRouteChangeReason::OldDeviceUnavailable => Some(Error::with_message( + ErrorKind::DeviceChanged, + "Audio route changed", + )), + + AVAudioSessionRouteChangeReason::CategoryChange + | AVAudioSessionRouteChangeReason::Override + | AVAudioSessionRouteChangeReason::RouteConfigurationChange => Some(Error::with_message( + ErrorKind::StreamInvalidated, + "Audio route changed", + )), + + AVAudioSessionRouteChangeReason::NoSuitableRouteForCategory => Some(Error::with_message( + ErrorKind::DeviceNotAvailable, + "No suitable audio route for the session category", + )), + + _ => None, + } +} + +pub(super) struct SessionEventManager { + latch: Latch, + observers: Vec< + objc2::rc::Retained>, + >, +} + +// SAFETY: NSNotificationCenter is thread-safe on iOS. The observer tokens stored here are opaque +// handles used only to call removeObserver in Drop; no data is read or written through them. +unsafe impl Send for SessionEventManager {} +unsafe impl Sync for SessionEventManager {} + +impl SessionEventManager { + pub(super) fn new( + error_callback: ErrorCallbackArc, + latch: Latch, + latency_refresh: Option, + stream: Weak>, + ) -> Self { + let nc = NSNotificationCenter::defaultCenter(); + let mut observers = Vec::new(); + let waiter = latch.waiter(); + + // The OS stops the unit itself, and the session it stops is inactive on the way back. + { + let w = waiter.clone(); + let stream = stream.clone(); + let block = RcBlock::new(move |notif: NonNull| { + if !w.is_released() { + return; + } + let notif = unsafe { notif.as_ref() }; + let interruption_type_key = unsafe { AVAudioSessionInterruptionTypeKey }; + let Some(kind) = user_info_number(notif, interruption_type_key) else { + return; + }; + if AVAudioSessionInterruptionType(kind) == AVAudioSessionInterruptionType::Began { + with_stream(&stream, StreamInner::stop_for_interruption); + return; + } + let interruption_option_key = unsafe { AVAudioSessionInterruptionOptionKey }; + let options = AVAudioSessionInterruptionOptions( + user_info_number(notif, interruption_option_key).unwrap_or(0), + ); + if !options.contains(AVAudioSessionInterruptionOptions::ShouldResume) { + return; + } + let session = unsafe { AVAudioSession::sharedInstance() }; + if unsafe { session.setActive_error(true) }.is_ok() { + with_stream(&stream, StreamInner::resume_after_interruption); + } + }); + if let Some(name) = unsafe { AVAudioSessionInterruptionNotification } { + let observer = unsafe { + nc.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block) + }; + observers.push(observer); + } + } + + { + let cb = error_callback.clone(); + let w = waiter.clone(); + let block = RcBlock::new(move |notif: NonNull| { + if w.is_released() { + // The route may have changed the active device; recompute the buffer depth so + // capture/playback timestamps track the new latency. + if let Some((frames, is_input)) = &latency_refresh { + let depth = if *is_input { + input_latency_frames() + } else { + output_latency_frames() + }; + frames.store(depth, Ordering::Relaxed); + } + let notif = unsafe { notif.as_ref() }; + if let Some(err) = route_change_error(notif) { + emit_error(&cb, err); + } + } + }); + if let Some(name) = unsafe { AVAudioSessionRouteChangeNotification } { + let observer = unsafe { + nc.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block) + }; + observers.push(observer); + } + } + + { + let cb = error_callback.clone(); + let w = waiter.clone(); + let block = RcBlock::new(move |_: NonNull| { + if w.is_released() { + emit_error( + &cb, + Error::with_message( + ErrorKind::DeviceNotAvailable, + "Audio media services were lost", + ), + ); + } + }); + if let Some(name) = unsafe { AVAudioSessionMediaServicesWereLostNotification } { + let observer = unsafe { + nc.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block) + }; + observers.push(observer); + } + } + + { + let cb = error_callback.clone(); + let w = waiter; + let block = RcBlock::new(move |_: NonNull| { + if w.is_released() { + emit_error( + &cb, + Error::with_message( + ErrorKind::StreamInvalidated, + "Audio media services were reset", + ), + ); + } + }); + if let Some(name) = unsafe { AVAudioSessionMediaServicesWereResetNotification } { + let observer = unsafe { + nc.addObserverForName_object_queue_usingBlock(Some(name), None, None, &block) + }; + observers.push(observer); + } + } + + Self { latch, observers } + } + + pub(super) fn signal_ready(&self) { + self.latch.release(); + } +} + +impl Drop for SessionEventManager { + fn drop(&mut self) { + let nc = NSNotificationCenter::defaultCenter(); + for observer in &self.observers { + unsafe { nc.removeObserver(observer.as_ref()) }; + } + } +} diff --git a/crates/vendor/cpal/src/host/coreaudio/macos/device.rs b/crates/vendor/cpal/src/host/coreaudio/macos/device.rs new file mode 100644 index 000000000..94028d73b --- /dev/null +++ b/crates/vendor/cpal/src/host/coreaudio/macos/device.rs @@ -0,0 +1,1091 @@ +use std::{ + fmt, + mem::{self, size_of}, + ptr::{null, NonNull}, + sync::{ + atomic::{AtomicUsize, Ordering}, + mpsc::{channel, RecvTimeoutError}, + Arc, Mutex, + }, + time::{Duration, Instant}, +}; + +use coreaudio::audio_unit::{ + audio_format::LinearPcmFlags, + macos_helpers::{ + audio_unit_from_device_id_uninitialized, find_matching_physical_format, get_device_name, + set_device_physical_stream_format, RateListener, + }, + render_callback::{self, data}, + AudioUnit, Element, SampleFormat as CoreAudioSampleFormat, Scope, StreamFormat, +}; +use objc2_audio_toolbox::{ + kAudioOutputUnitProperty_CurrentDevice, kAudioUnitProperty_StreamFormat, +}; +use objc2_core_audio::{ + kAudioAggregateDeviceClassID, kAudioDevicePropertyAvailableNominalSampleRates, + kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange, + kAudioDevicePropertyDeviceUID, kAudioDevicePropertyLatency, + kAudioDevicePropertyNominalSampleRate, kAudioDevicePropertySafetyOffset, + kAudioDevicePropertyStreamConfiguration, kAudioDevicePropertyStreamFormat, + kAudioObjectPropertyClass, kAudioObjectPropertyElementMain, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput, AudioClassID, AudioDeviceID, + AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize, AudioObjectID, + AudioObjectPropertyAddress, AudioObjectPropertyScope, AudioObjectSetPropertyData, +}; +use objc2_core_audio_types::{ + AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, +}; +use objc2_core_foundation::{CFRetained, CFString}; + +pub use super::enumerate::{SupportedInputConfigs, SupportedOutputConfigs}; +use super::{ + asbd_from_config, check_os_status, host_time_to_stream_instant, DefaultOutputMonitor, + DisconnectManager, Monitor, Stream, +}; +use crate::{ + host::{ + coreaudio::macos::{loopback::LoopbackDevice, StreamInner}, + equilibrium::fill_equilibrium, + frames_to_duration, try_emit_error, ErrorCallbackArc, + }, + traits::DeviceTrait, + BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error, + ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, InterfaceType, + OutputCallbackInfo, OutputStreamTimestamp, ResultExt, SampleFormat, SampleRate, StreamConfig, + StreamInstant, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, +}; + +/// Try to find a matching physical stream format on the device and apply it. +/// +/// Setting the physical format ensures the hardware runs at the requested bit depth and sample +/// rate without unnecessary conversions. +fn set_physical_format( + device_id: AudioDeviceID, + sample_rate: SampleRate, + channels: ChannelCount, + sample_format: SampleFormat, +) -> Result { + let core_format = match sample_format { + SampleFormat::I8 => CoreAudioSampleFormat::I8, + SampleFormat::I16 => CoreAudioSampleFormat::I16, + SampleFormat::I24 => CoreAudioSampleFormat::I24, + SampleFormat::I32 => CoreAudioSampleFormat::I32, + SampleFormat::F32 => CoreAudioSampleFormat::F32, + _ => return Err(coreaudio::Error::UnsupportedStreamFormat), + }; + let stream_format = StreamFormat { + sample_rate: sample_rate as f64, + sample_format: core_format, + flags: LinearPcmFlags::empty(), + channels: channels as u32, + }; + let asbd = find_matching_physical_format(device_id, stream_format) + .ok_or(coreaudio::Error::UnsupportedStreamFormat)?; + set_device_physical_stream_format(device_id, asbd).map(|_| asbd) +} + +/// Set the device's nominal sample rate via `kAudioDevicePropertyNominalSampleRate`. +/// +/// Unlike [`set_physical_format`], this only changes the device clock rate. The AudioUnit bridges +/// any remaining format difference to the virtual stream format seen by the callback. +fn set_sample_rate( + audio_device_id: AudioObjectID, + target_sample_rate: SampleRate, + timeout: Option, +) -> Result<(), Error> { + // Get the current sample rate. + let mut property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyNominalSampleRate, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + let mut sample_rate: f64 = 0.0; + let mut data_size = mem::size_of::() as u32; + let status = unsafe { + AudioObjectGetPropertyData( + audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + NonNull::from(&mut sample_rate).cast(), + ) + }; + coreaudio::Error::from_os_status(status)?; + + // If the requested sample rate is different to the device sample rate, update the device. + if (sample_rate - target_sample_rate as f64).abs() >= 1.0 { + // Get available sample rate ranges. + property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; + let mut data_size = 0u32; + let status = unsafe { + AudioObjectGetPropertyDataSize( + audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + ) + }; + coreaudio::Error::from_os_status(status)?; + let n_ranges = data_size as usize / mem::size_of::(); + let mut ranges: Vec = Vec::with_capacity(n_ranges); + let status = unsafe { + AudioObjectGetPropertyData( + audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + NonNull::new(ranges.as_mut_ptr()).unwrap().cast(), + ) + }; + coreaudio::Error::from_os_status(status)?; + unsafe { + ranges.set_len(n_ranges); + } + + // Now that we have the available ranges, pick the one matching the desired rate. + let sample_rate = target_sample_rate; + if !ranges + .iter() + .any(|r| sample_rate as f64 >= r.mMinimum && sample_rate as f64 <= r.mMaximum) + { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Sample rate {sample_rate} Hz is not supported"), + )); + } + + // Register the listener before setting the property so we don't miss the notification. + let (sender, receiver) = channel::(); + let mut listener = RateListener::new(audio_device_id, Some(sender)); + listener.register()?; + + // Set the nominal sample rate. + property_address.mSelector = kAudioDevicePropertyNominalSampleRate; + let rate = sample_rate as f64; + let data_size = mem::size_of::() as u32; + let status = unsafe { + AudioObjectSetPropertyData( + audio_device_id, + NonNull::from(&property_address), + 0, + null(), + data_size, + NonNull::from(&rate).cast(), + ) + }; + coreaudio::Error::from_os_status(status)?; + + // Wait for the reported_rate to change. + // + // This should not take longer than a few ms. Use the caller's timeout if provided, + // otherwise default to 1 second. We loop over potentially several events from the + // channel to ensure that we catch the expected change in sample rate. + let mut remaining = timeout.unwrap_or(Duration::from_secs(1)); + let start = Instant::now(); + loop { + match receiver.recv_timeout(remaining) { + Ok(reported_rate) => { + if (reported_rate - target_sample_rate as f64).abs() < 1.0 { + break; + } + } + Err(RecvTimeoutError::Timeout) => { + return Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "Sample rate update timed out", + )); + } + Err(RecvTimeoutError::Disconnected) => { + return Err(Error::with_message( + ErrorKind::StreamInvalidated, + "Sample rate listener disconnected unexpectedly", + )); + } + } + remaining = remaining + .checked_sub(start.elapsed()) + .unwrap_or(Duration::ZERO); + } + // listener dropped here; its Drop impl calls unregister() automatically. + } + Ok(()) +} + +#[derive(Clone, Copy)] +enum AudioUnitMode { + /// HAL Output AudioUnit with input enabled, pinned to a specific device. + Input, + /// HAL Output AudioUnit for output, pinned to a specific device. + Output, + /// DefaultOutput AudioUnit; follows the system default output device automatically. + DefaultOutput, +} + +fn audio_unit_from_device( + device: &Device, + mode: AudioUnitMode, +) -> Result { + match mode { + AudioUnitMode::DefaultOutput => { + AudioUnit::new_uninitialized(coreaudio::audio_unit::IOType::DefaultOutput) + } + AudioUnitMode::Input => { + audio_unit_from_device_id_uninitialized(device.audio_device_id, true) + } + AudioUnitMode::Output => { + // Do not use audio_unit_from_device_id_uninitialized here: that function compares the + // device ID against the live system default and silently switches to DefaultOutput + // mode if they match. We explicitly pin HalOutput unit here. + let mut audio_unit = + AudioUnit::new_uninitialized(coreaudio::audio_unit::IOType::HalOutput)?; + // Device selection is a device-level property: + // always use Scope::Global + Element::Output + audio_unit.set_property( + kAudioOutputUnitProperty_CurrentDevice, + Scope::Global, + Element::Output, + Some(&device.audio_device_id), + )?; + Ok(audio_unit) + } + } +} + +fn get_io_buffer_frame_size_range(device_id: AudioDeviceID) -> Result { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyBufferFrameSizeRange, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + // SAFETY: AudioObjectGetPropertyData writes exactly one AudioValueRange into the output + // pointer when querying kAudioDevicePropertyBufferFrameSizeRange. We verify the status + // before reading the value. + let mut range: AudioValueRange = unsafe { mem::zeroed() }; + let mut data_size = mem::size_of::() as u32; + let status = unsafe { + AudioObjectGetPropertyData( + device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + NonNull::from(&mut range).cast(), + ) + }; + check_os_status(status)?; + Ok(SupportedBufferSize::Range { + min: range.mMinimum as u32, + max: range.mMaximum as u32, + }) +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn description(&self) -> Result { + Device::description(self) + } + + fn id(&self) -> Result { + Device::id(self) + } + + fn supported_input_configs(&self) -> Result { + Device::supported_input_configs(self) + } + + fn supported_output_configs(&self) -> Result { + Device::supported_output_configs(self) + } + + fn default_input_config(&self) -> Result { + Device::default_input_config(self) + } + + fn default_output_config(&self) -> Result { + Device::default_output_config(self) + } + + fn build_input_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + Device::build_input_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + } + + fn build_output_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + Device::build_output_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + } +} + +#[derive(Clone)] +pub struct Device { + pub(crate) audio_device_id: AudioDeviceID, + pub(crate) is_default_output: bool, +} + +impl Device { + /// Construct a new device given its ID. + /// Useful for constructing hidden devices. + pub fn new(audio_device_id: AudioDeviceID) -> Self { + Self { + audio_device_id, + is_default_output: false, + } + } + + /// Checks if this device is an aggregate device. + /// + /// Aggregate devices combine multiple physical devices into a single logical device. + fn is_aggregate_device(&self) -> bool { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioObjectPropertyClass, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + let mut class_id: AudioClassID = 0; + let data_size = size_of::() as u32; + + // SAFETY: AudioObjectGetPropertyData is documented to write an AudioClassID + // for kAudioObjectPropertyClass. We check the status before using the value. + let status = unsafe { + AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::from(&mut class_id).cast(), + ) + }; + + // If successful, check if it's an aggregate device + status == 0 && class_id == kAudioAggregateDeviceClassID + } + + fn description(&self) -> Result { + let name = get_device_name(self.audio_device_id).context("Failed to get device name")?; + + let input_configs = self + .supported_input_configs() + .map(|configs| configs.count() as ChannelCount) + .ok(); + let output_configs = self + .supported_output_configs() + .map(|configs| configs.count() as ChannelCount) + .ok(); + + let direction = + crate::device_description::direction_from_counts(input_configs, output_configs); + + let mut builder = DeviceDescriptionBuilder::new(name).direction(direction); + + // Check if this is an aggregate device + if self.is_aggregate_device() { + builder = builder.interface_type(InterfaceType::Aggregate); + } + + Ok(builder.build()) + } + + fn id(&self) -> Result { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyDeviceUID, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + // CFString is returned under the create rule, so take ownership of the +1 reference. + let mut uid: *mut CFString = std::ptr::null_mut(); + let mut data_size = size_of::<*mut CFString>() as u32; + + // SAFETY: AudioObjectGetPropertyData is documented to write a CFString pointer + // for kAudioDevicePropertyDeviceUID. We check the status code before use. + let status = unsafe { + AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + NonNull::from(&mut uid).cast(), + ) + }; + check_os_status(status)?; + + // SAFETY: Status was successful, meaning the API call succeeded. + // We now check if the returned uid is non-null before use. + if !uid.is_null() { + let uid_string = + unsafe { CFRetained::from_raw(NonNull::new(uid).unwrap()).to_string() }; + Ok(DeviceId::new( + crate::platform::HostId::CoreAudio, + uid_string, + )) + } else { + Err(ErrorKind::DeviceNotAvailable.into()) + } + } + + // Logic re-used between `supported_input_configs` and `supported_output_configs`. + #[allow(clippy::cast_ptr_alignment)] + fn supported_configs( + &self, + scope: AudioObjectPropertyScope, + ) -> Result { + let mut property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyStreamConfiguration, + mScope: scope, + mElement: kAudioObjectPropertyElementMain, + }; + + unsafe { + // Retrieve the devices audio buffer list. + let mut data_size = 0u32; + let status = AudioObjectGetPropertyDataSize( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + ); + check_os_status(status)?; + + let mut audio_buffer_list: Vec = vec![]; + audio_buffer_list.reserve_exact(data_size as usize); + let status = AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + NonNull::new(audio_buffer_list.as_mut_ptr()).unwrap().cast(), + ); + check_os_status(status)?; + + let audio_buffer_list = audio_buffer_list.as_mut_ptr() as *mut AudioBufferList; + + // Read the number of buffers without assuming alignment (avoid UB). + let nb_ptr = core::ptr::addr_of!((*audio_buffer_list).mNumberBuffers); + let n_buffers = core::ptr::read_unaligned(nb_ptr) as usize; + // If there are no buffers, skip. + if n_buffers == 0 { + return Ok(vec![].into_iter()); + } + + // Count the number of channels as the sum of all channels in all output buffers. + let first_buf_ptr = + core::ptr::addr_of!((*audio_buffer_list).mBuffers) as *const AudioBuffer; + let mut n_channels = 0usize; + for i in 0..n_buffers { + let buf_ptr = first_buf_ptr.add(i); + // Read potentially unaligned + let buf: AudioBuffer = core::ptr::read_unaligned(buf_ptr); + n_channels += buf.mNumberChannels as usize; + } + + // TODO: macOS should support U8, I16, I32, F32 and F64. This should allow for using + // I16 but just use F32 for now as it's the default anyway. + let sample_format = SampleFormat::F32; + + // Get available sample rate ranges. + // The property "kAudioDevicePropertyAvailableNominalSampleRates" returns a list of pairs of + // minimum and maximum sample rates but most of the devices returns pairs of same values though the underlying mechanism is unclear. + // This may cause issues when, for example, sorting the configs by the sample rates. + // We follows the implementation of RtAudio, which returns single element of config + // when all the pairs have the same values and returns multiple elements otherwise. + // See https://github.com/thestk/rtaudio/blob/master/RtAudio.cpp#L1369C1-L1375C39 + + property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; + let mut data_size = 0u32; + let status = AudioObjectGetPropertyDataSize( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + ); + check_os_status(status)?; + + let n_ranges = data_size as usize / mem::size_of::(); + let mut ranges: Vec = Vec::with_capacity(n_ranges); + let status = AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + NonNull::new(ranges.as_mut_ptr()).unwrap().cast(), + ); + check_os_status(status)?; + + ranges.set_len(n_ranges); + + #[allow(non_upper_case_globals)] + match scope { + kAudioObjectPropertyScopeInput | kAudioObjectPropertyScopeOutput => {} + _ => { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Unexpected audio property scope", + )) + } + } + let buffer_size = get_io_buffer_frame_size_range(self.audio_device_id)?; + + // Most hardware reports discrete rates (mMinimum == mMaximum); some aggregate or + // virtual devices report continuous ranges. + let fmts: Vec<_> = ranges + .iter() + .map(|range| SupportedStreamConfigRange { + channels: n_channels as ChannelCount, + min_sample_rate: range.mMinimum as u32, + max_sample_rate: range.mMaximum as u32, + buffer_size, + sample_format, + }) + .collect(); + Ok(fmts.into_iter()) + } + } + + fn supported_input_configs(&self) -> Result { + self.supported_configs(kAudioObjectPropertyScopeInput) + } + + fn supported_output_configs(&self) -> Result { + self.supported_configs(kAudioObjectPropertyScopeOutput) + } + + fn default_config( + &self, + scope: AudioObjectPropertyScope, + ) -> Result { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyStreamFormat, + mScope: scope, + mElement: kAudioObjectPropertyElementMain, + }; + + unsafe { + let mut asbd: AudioStreamBasicDescription = mem::zeroed(); + let mut data_size = mem::size_of::() as u32; + let status = AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + NonNull::from(&mut asbd).cast(), + ); + check_os_status(status)?; + + let sample_format = { + let audio_format = coreaudio::audio_unit::AudioFormat::from_format_and_flag( + asbd.mFormatID, + Some(asbd.mFormatFlags), + ); + let flags = match audio_format { + Some(coreaudio::audio_unit::AudioFormat::LinearPCM(flags)) => flags, + _ => { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + "Audio format is not linear PCM", + )) + } + }; + let maybe_sample_format = + coreaudio::audio_unit::SampleFormat::from_flags_and_bits_per_sample( + flags, + asbd.mBitsPerChannel, + ); + match maybe_sample_format { + Some(coreaudio::audio_unit::SampleFormat::F32) => SampleFormat::F32, + Some(coreaudio::audio_unit::SampleFormat::I16) => SampleFormat::I16, + _ => { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + "Sample format is not supported; supported formats are F32 and I16", + )) + } + } + }; + + #[allow(non_upper_case_globals)] + match scope { + kAudioObjectPropertyScopeInput | kAudioObjectPropertyScopeOutput => {} + _ => { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Unexpected audio property scope", + )) + } + } + let buffer_size = get_io_buffer_frame_size_range(self.audio_device_id)?; + + let config = SupportedStreamConfig { + sample_rate: asbd.mSampleRate as _, + channels: asbd.mChannelsPerFrame as _, + buffer_size, + sample_format, + }; + Ok(config) + } + } + + fn default_input_config(&self) -> Result { + self.default_config(kAudioObjectPropertyScopeInput) + } + + fn default_output_config(&self) -> Result { + self.default_config(kAudioObjectPropertyScopeOutput) + } + + /// Check if this device supports input (recording). + fn supports_input(&self) -> bool { + // Check if the device has input channels by trying to get its input configuration + self.supported_input_configs() + .map(|mut configs| configs.next().is_some()) + .unwrap_or(false) + } +} + +impl Device { + #[allow(clippy::cast_ptr_alignment)] + #[allow(clippy::while_immutable_condition)] + #[allow(clippy::float_cmp)] + fn build_input_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + mut data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + crate::validate_stream_config(&config)?; + + // Input is not automatically rerouted, so its buffer depth is constant and its timestamp monotonic. + + // Set the physical stream format (bit depth + sample rate) on the hardware device. + // This avoids unnecessary format conversions, which is especially important on aggregate + // devices. Falls back to sample-rate-only if no matching physical format is available. + if set_physical_format( + self.audio_device_id, + config.sample_rate, + config.channels, + sample_format, + ) + .is_err() + { + set_sample_rate(self.audio_device_id, config.sample_rate, timeout)?; + } + + let mut loopback_aggregate: Option = None; + let mut audio_unit = if self.supports_input() { + audio_unit_from_device(self, AudioUnitMode::Input)? + } else { + loopback_aggregate.replace(LoopbackDevice::from_device(self)?); + audio_unit_from_device( + &loopback_aggregate.as_ref().unwrap().aggregate_device, + AudioUnitMode::Input, + )? + }; + + // The scope and element for working with a device's input stream. + let scope = Scope::Output; + let element = Element::Input; + + // Configure stream format and buffer size for predictable callback behavior. + let effective_device_id = loopback_aggregate + .as_ref() + .map(|l| l.aggregate_device.audio_device_id) + .unwrap_or(self.audio_device_id); + configure_stream_format_and_buffer( + &mut audio_unit, + config, + sample_format, + scope, + element, + effective_device_id, + )?; + + let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); + let error_callback_disconnect = error_callback.clone(); + + // Register the callback that is being called by coreaudio whenever it needs data to be + // fed to the audio buffer. + let (bytes_per_channel, sample_rate, device_buffer_frames, extra_latency_frames) = + setup_callback_vars(&audio_unit, config, sample_format, Scope::Input); + + type Args = render_callback::Args; + audio_unit.set_input_callback(move |args: Args| unsafe { + // SAFETY: We configure the stream format as interleaved (via asbd_from_config which + // does not set kAudioFormatFlagIsNonInterleaved). Interleaved format always has + // exactly one buffer containing all channels, so mBuffers[0] is always valid. + let AudioBuffer { + mNumberChannels: channels, + mDataByteSize: data_byte_size, + mData: data, + } = (*args.data.data).mBuffers[0]; + + let data = data as *mut (); + let len = data_byte_size as usize / bytes_per_channel; + let data = Data::from_parts(data, len, sample_format); + + let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { + Err(err) => { + let _ = try_emit_error(&error_callback, err); + return Err(()); + } + Ok(cb) => cb, + }; + let buffer_frames = len / channels as usize; + let latency_frames = + device_buffer_frames.unwrap_or(buffer_frames) + extra_latency_frames; + let delay = frames_to_duration(latency_frames as FrameCount, sample_rate); + let capture = callback.checked_sub(delay).unwrap_or(StreamInstant::ZERO); + let timestamp = InputStreamTimestamp { callback, capture }; + + let info = InputCallbackInfo { timestamp }; + data_callback(&data, &info); + Ok(()) + })?; + + // All properties and callbacks are now configured on the uninitialized unit. + // Initialize here so CoreAudio allocates its internal buffers for the actual format. + audio_unit.initialize()?; + + let inner_arc = Arc::new(Mutex::new(StreamInner { + playing: false, + audio_unit, + _device_id: self.audio_device_id, + _loopback_device: loopback_aggregate, + })); + let weak_inner = Arc::downgrade(&inner_arc); + let monitor: Box = Box::new(DisconnectManager::new( + self.audio_device_id, + weak_inner, + error_callback_disconnect, + )?); + let stream = Stream::new(inner_arc, monitor); + stream.signal_ready(); + Ok(stream) + } + + fn build_output_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + crate::validate_stream_config(&config)?; + + // Keep `playback` monotonic: a default output device reroute can lower the device buffer depth, + // pulling `playback` backward. + let mut data_callback = crate::host::monotonic_output_callback(data_callback); + + // Best-effort: set the physical stream format (bit depth + sample rate) on the hardware. + // This avoids unnecessary conversions, especially on aggregate devices. Not an error if + // it fails — the AudioUnit will handle format conversion as before. + if set_physical_format( + self.audio_device_id, + config.sample_rate, + config.channels, + sample_format, + ) + .is_err() + { + set_sample_rate(self.audio_device_id, config.sample_rate, timeout)?; + } + + let mode = if self.is_default_output { + AudioUnitMode::DefaultOutput + } else { + AudioUnitMode::Output + }; + let mut audio_unit = audio_unit_from_device(self, mode)?; + + // The scope and element for working with a device's output stream. + let scope = Scope::Input; + let element = Element::Output; + + // Configure device buffer (see comprehensive documentation in input stream above) + configure_stream_format_and_buffer( + &mut audio_unit, + config, + sample_format, + scope, + element, + self.audio_device_id, + )?; + + let error_callback: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); + let error_callback_for_render = error_callback.clone(); + + // Register the callback that is being called by coreaudio whenever it needs data to be + // fed to the audio buffer. + let (bytes_per_channel, sample_rate, device_buffer_frames, extra_latency_frames) = + setup_callback_vars(&audio_unit, config, sample_format, Scope::Output); + + // A DefaultOutput unit auto-reroutes to a new device without rebuilding the stream, + // which changes this depth, and is then refreshed by DefaultOutputMonitor. + let latency_frames = Arc::new(AtomicUsize::new( + device_buffer_frames.map_or(0, |frames| frames + extra_latency_frames), + )); + let callback_latency_frames = latency_frames.clone(); + + type Args = render_callback::Args; + audio_unit.set_render_callback(move |args: Args| unsafe { + // SAFETY: We configure the stream format as interleaved (via asbd_from_config which + // does not set kAudioFormatFlagIsNonInterleaved). Interleaved format always has + // exactly one buffer containing all channels, so mBuffers[0] is always valid. + let AudioBuffer { + mNumberChannels: channels, + mDataByteSize: data_byte_size, + mData: data, + } = (*args.data.data).mBuffers[0]; + + let data = data as *mut (); + let len = data_byte_size as usize / bytes_per_channel; + + let bytes = std::slice::from_raw_parts_mut(data as *mut u8, data_byte_size as usize); + fill_equilibrium(bytes, sample_format); + + let mut data = Data::from_parts(data, len, sample_format); + + let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { + Err(err) => { + let _ = try_emit_error(&error_callback_for_render, err); + return Err(()); + } + Ok(cb) => cb, + }; + let latency_frames = match callback_latency_frames.load(Ordering::Relaxed) { + // Depth unknown (query failed): estimate the device buffer from this callback, but + // still add the safety offset and device latency, matching the input path. + 0 => len / channels as usize + extra_latency_frames, + n => n, + }; + let delay = frames_to_duration(latency_frames as FrameCount, sample_rate); + let playback = callback + delay; + let timestamp = OutputStreamTimestamp { callback, playback }; + + let info = OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); + Ok(()) + })?; + + // All properties and callbacks are now configured on the uninitialized unit. + // Initialize here so CoreAudio allocates its internal buffers for the actual format. + audio_unit.initialize()?; + + let inner_arc = Arc::new(Mutex::new(StreamInner { + playing: false, + audio_unit, + _device_id: self.audio_device_id, + _loopback_device: None, + })); + let weak_inner = Arc::downgrade(&inner_arc); + let monitor: Box = if matches!(mode, AudioUnitMode::DefaultOutput) { + // Refresh the buffer depth whenever the default device reroutes automatically. + Box::new(DefaultOutputMonitor::new( + weak_inner, + error_callback, + Some((latency_frames, Scope::Output)), + )?) + } else { + Box::new(DisconnectManager::new( + self.audio_device_id, + weak_inner, + error_callback, + )?) + }; + let stream = Stream::new(inner_arc, monitor); + stream.signal_ready(); + Ok(stream) + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + self.audio_device_id == other.audio_device_id + } +} + +impl Eq for Device {} + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let desc = self.description().map_err(|_| fmt::Error)?; + f.write_str(desc.name()) + } +} + +impl std::hash::Hash for Device { + fn hash(&self, state: &mut H) { + self.audio_device_id.hash(state); + } +} + +impl fmt::Debug for Device { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Device") + .field("audio_device_id", &self.audio_device_id) + .field("name", &self.description().map(|d| d.name().to_owned())) + .finish() + } +} + +/// Configure stream format and buffer size for CoreAudio stream. +/// +/// This handles the common setup tasks for both input and output streams: +/// - Sets the stream format (ASBD) +/// - Configures buffer size for Fixed buffer size requests +fn configure_stream_format_and_buffer( + audio_unit: &mut AudioUnit, + config: StreamConfig, + sample_format: SampleFormat, + scope: Scope, + element: Element, + device_id: AudioDeviceID, +) -> Result<(), Error> { + // Set the stream format using stream-specific scope/element + // - Input streams: scope=Output, element=Input (configuring output format of input element) + // - Output streams: scope=Input, element=Output (configuring input format of output element) + let asbd = asbd_from_config(config, sample_format); + audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?; + + // Configure device buffer size if requested + if let BufferSize::Fixed(buffer_size) = config.buffer_size { + // Pre-validate against the hardware range so callers get a human-readable error. + if let Ok(SupportedBufferSize::Range { min, max }) = + get_io_buffer_frame_size_range(device_id) + { + if !(min..=max).contains(&buffer_size) { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Buffer size {buffer_size} is not in the supported range {min}..={max}" + ), + )); + } + } + // IMPORTANT: Buffer frame size is a DEVICE-LEVEL property, not stream-specific. + // Unlike stream format above, we ALWAYS use Scope::Global + Element::Output + // for device properties, regardless of whether this is an input or output stream. + // This is consistent with other device properties like: + // - kAudioOutputUnitProperty_CurrentDevice + // - kAudioDevicePropertyBufferFrameSizeRange + // The Element::Output here doesn't mean "output stream only" - it's the + // canonical element used for device-wide properties in Core Audio. + audio_unit.set_property( + kAudioDevicePropertyBufferFrameSize, + Scope::Global, + Element::Output, + Some(&buffer_size), + )?; + } + + Ok(()) +} + +/// Returns the sum of the device latency and safety offset in frames. +pub(crate) fn get_device_extra_latency_frames(audio_unit: &AudioUnit, scope: Scope) -> usize { + let device_latency: u32 = audio_unit + .get_property(kAudioDevicePropertyLatency, scope, Element::Output) + .unwrap_or(0); + let safety_offset: u32 = audio_unit + .get_property(kAudioDevicePropertySafetyOffset, scope, Element::Output) + .unwrap_or(0); + (device_latency + safety_offset) as usize +} + +/// Setup common callback variables, querying both the I/O buffer size and extra hardware latency. +/// +/// Returns `(bytes_per_channel, sample_rate, device_buffer_frames, extra_latency_frames)` +fn setup_callback_vars( + audio_unit: &AudioUnit, + config: StreamConfig, + sample_format: SampleFormat, + scope: Scope, +) -> (usize, SampleRate, Option, usize) { + let bytes_per_channel = sample_format.sample_size(); + let sample_rate = config.sample_rate; + + let device_buffer_frames = get_device_buffer_frame_size(audio_unit).ok(); + let extra_latency_frames = get_device_extra_latency_frames(audio_unit, scope); + + ( + bytes_per_channel, + sample_rate, + device_buffer_frames, + extra_latency_frames, + ) +} + +/// Query the current device buffer frame size from CoreAudio. +/// +/// Buffer frame size is a device-level property that always uses Scope::Global + Element::Output, +/// regardless of whether the audio unit is configured for input or output streams. +pub(crate) fn get_device_buffer_frame_size( + audio_unit: &AudioUnit, +) -> Result { + // Device-level property: always use Scope::Global + Element::Output + // This is consistent with how we set the buffer size and query the buffer size range + let frames: u32 = audio_unit.get_property( + kAudioDevicePropertyBufferFrameSize, + Scope::Global, + Element::Output, + )?; + Ok(frames as usize) +} diff --git a/crates/vendor/cpal/src/host/coreaudio/macos/enumerate.rs b/crates/vendor/cpal/src/host/coreaudio/macos/enumerate.rs new file mode 100644 index 000000000..f17f14dc3 --- /dev/null +++ b/crates/vendor/cpal/src/host/coreaudio/macos/enumerate.rs @@ -0,0 +1,141 @@ +use std::{ + mem, + ptr::{null, NonNull}, + vec::IntoIter as VecIntoIter, +}; + +use objc2_core_audio::{ + kAudioHardwareNoError, kAudioHardwarePropertyDefaultInputDevice, + kAudioHardwarePropertyDefaultOutputDevice, kAudioHardwarePropertyDevices, + kAudioObjectPropertyElementMain, kAudioObjectPropertyScopeGlobal, kAudioObjectSystemObject, + AudioDeviceID, AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize, AudioObjectID, + AudioObjectPropertyAddress, +}; + +use super::{check_os_status, Device}; +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; +use crate::Error; + +unsafe fn audio_devices() -> Result, Error> { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyDevices, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + macro_rules! try_status_or_return { + ($status:expr) => { + if $status != kAudioHardwareNoError as i32 { + return Err(check_os_status($status).unwrap_err()); + } + }; + } + + let mut data_size = 0u32; + let status = AudioObjectGetPropertyDataSize( + kAudioObjectSystemObject as AudioObjectID, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + ); + try_status_or_return!(status); + + let device_count = data_size / mem::size_of::() as u32; + let mut audio_devices = vec![]; + audio_devices.reserve_exact(device_count as usize); + + let status = AudioObjectGetPropertyData( + kAudioObjectSystemObject as AudioObjectID, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&mut data_size), + NonNull::new(audio_devices.as_mut_ptr()).unwrap().cast(), + ); + try_status_or_return!(status); + + audio_devices.set_len(device_count as usize); + + Ok(audio_devices) +} + +pub struct Devices(VecIntoIter); + +impl Devices { + pub fn new() -> Result { + let devices = unsafe { audio_devices() }?; + Ok(Self(devices.into_iter())) + } +} + +impl Iterator for Devices { + type Item = Device; + + fn next(&mut self) -> Option { + self.0.next().map(|id| Self::Item { + audio_device_id: id, + is_default_output: false, + }) + } +} + +pub fn default_input_device() -> Option { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyDefaultInputDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + let mut audio_device_id: AudioDeviceID = 0; + let data_size = mem::size_of::() as u32; + let status = unsafe { + AudioObjectGetPropertyData( + kAudioObjectSystemObject as AudioObjectID, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::from(&mut audio_device_id).cast(), + ) + }; + if status != kAudioHardwareNoError { + return None; + } + + let device = Device { + audio_device_id, + is_default_output: false, + }; + Some(device) +} + +pub fn default_output_device() -> Option { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyDefaultOutputDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + let mut audio_device_id: AudioDeviceID = 0; + let data_size = mem::size_of::() as u32; + let status = unsafe { + AudioObjectGetPropertyData( + kAudioObjectSystemObject as AudioObjectID, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::from(&mut audio_device_id).cast(), + ) + }; + if status != kAudioHardwareNoError { + return None; + } + + let device = Device { + audio_device_id, + is_default_output: true, + }; + Some(device) +} diff --git a/crates/vendor/cpal/src/host/coreaudio/macos/loopback.rs b/crates/vendor/cpal/src/host/coreaudio/macos/loopback.rs new file mode 100644 index 000000000..d57e093ad --- /dev/null +++ b/crates/vendor/cpal/src/host/coreaudio/macos/loopback.rs @@ -0,0 +1,260 @@ +//! Manages loopback recording (recording system audio output) + +use std::{ + ffi::{c_void, CStr}, + mem::MaybeUninit, + ptr::NonNull, + sync::atomic::{AtomicU32, Ordering}, +}; + +static AGGREGATE_INSTANCE_COUNTER: AtomicU32 = AtomicU32::new(0); + +use objc2::{rc::Retained, AnyThread}; +use objc2_core_audio::{ + kAudioAggregateDeviceNameKey, kAudioAggregateDeviceTapAutoStartKey, + kAudioAggregateDeviceTapListKey, kAudioAggregateDeviceUIDKey, kAudioDevicePropertyDeviceUID, + kAudioEndPointDeviceIsPrivateKey, kAudioObjectPropertyElementMain, + kAudioObjectPropertyScopeGlobal, kAudioSubTapDriftCompensationKey, kAudioSubTapUIDKey, + AudioHardwareCreateAggregateDevice, AudioHardwareCreateProcessTap, + AudioHardwareDestroyAggregateDevice, AudioHardwareDestroyProcessTap, + AudioObjectGetPropertyData, AudioObjectID, AudioObjectPropertyAddress, CATapDescription, + CATapMuteBehavior, +}; +use objc2_core_foundation::{ + kCFAllocatorDefault, kCFTypeArrayCallBacks, kCFTypeDictionaryKeyCallBacks, + kCFTypeDictionaryValueCallBacks, CFArray, CFDictionary, CFMutableDictionary, CFRetained, + CFString, +}; +use objc2_foundation::{NSArray, NSNumber, NSString}; + +use super::device::Device; +use crate::{host::coreaudio::check_os_status, Error, ErrorKind}; +type CFStringRef = *mut std::os::raw::c_void; + +impl Device { + fn uid(&self) -> Result, Error> { + let mut cfstring: CFStringRef = std::ptr::null_mut(); + let mut size = std::mem::size_of::() as u32; + + let property = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyDeviceUID, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + let status = unsafe { + AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property), + 0, + std::ptr::null(), + NonNull::from(&mut size), + NonNull::from(&mut cfstring).cast(), + ) + }; + check_os_status(status)?; + + if cfstring.is_null() { + return Err(ErrorKind::DeviceNotAvailable.into()); + } + + let ns_string: Retained = unsafe { + // unwrap cause cfstring!=null as checked before + Retained::retain(cfstring as *mut NSString).unwrap() + }; + + Ok(ns_string) + } +} + +/// An aggregate device with tap for recording system output. +/// +/// Its main difference with [`Device`] is that it's destroyed when dropped. +/// +/// It also doesn't implement the [`DeviceTrait`] as users shouldn't be using it. Its +/// main purpose is to destroy the created aggregate device when loopback recording +/// is done. +#[derive(PartialEq, Eq)] +pub struct LoopbackDevice { + pub tap_id: AudioObjectID, + pub aggregate_device: Device, +} + +impl LoopbackDevice { + /// Create a [`LoopbackDevice`] that records the sound + /// output of `device`. + pub fn from_device(device: &Device) -> Result { + // 1 - Create tap + + let pid = std::process::id(); + let instance = AGGREGATE_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed); + + // Empty list of processes as we want to record all processes + let processes = NSArray::new(); + let device_uid = device.uid()?; + let tap_desc = unsafe { + CATapDescription::initWithProcesses_andDeviceUID_withStream( + CATapDescription::alloc(), + &processes, + device_uid.as_ref(), + 0, + ) + }; + unsafe { + tap_desc.setMuteBehavior(CATapMuteBehavior::Unmuted); // captured audio still goes to speakers + tap_desc.setName(&NSString::from_str(&format!( + "cpal output recorder {pid}.{instance}" + ))); + tap_desc.setPrivate(true); // the Aggregate Device would be private + tap_desc.setExclusive(true); // the process list means exclude them + }; + + let mut tap_obj_id: MaybeUninit = MaybeUninit::uninit(); + let tap_obj_id = unsafe { + let status = + AudioHardwareCreateProcessTap(Some(tap_desc.as_ref()), tap_obj_id.as_mut_ptr()); + check_os_status(status)?; + tap_obj_id.assume_init() + }; + let tap_uid = unsafe { tap_desc.UUID().UUIDString() }; + + // 2 - Create aggregate device + let aggregate_device_properties = create_audio_aggregate_device_properties( + tap_uid, + &format!("com.cpal.LoopbackRecordAggregateDevice.{pid}.{instance}"), + &format!("Cpal loopback aggregate {pid}.{instance}"), + ); + let mut aggregate_device_id: AudioObjectID = 0; + let status = unsafe { + AudioHardwareCreateAggregateDevice( + aggregate_device_properties.as_ref(), + NonNull::from(&mut aggregate_device_id), + ) + }; + check_os_status(status)?; + + Ok(Self { + tap_id: tap_obj_id, + aggregate_device: Device::new(aggregate_device_id), + }) + } +} + +impl Drop for LoopbackDevice { + fn drop(&mut self) { + unsafe { + // We don't check status to avoid panic during `drop` + let _status = + AudioHardwareDestroyAggregateDevice(self.aggregate_device.audio_device_id); + let _status = AudioHardwareDestroyProcessTap(self.tap_id); + } + } +} + +fn to_cfstring(cstr: &'static CStr) -> CFRetained { + unsafe { + CFString::with_c_string( + kCFAllocatorDefault, + cstr.as_ptr(), + 0x08000100, /* UTF8 */ + ) + } + .unwrap() +} + +/// Rust reimplementation of the following: +/// ```c +/// tap_uid = [[tap_description UUID] UUIDString]; +/// taps = @[ +/// @{ +/// @kAudioSubTapUIDKey : (NSString*)tap_uid, +/// @kAudioSubTapDriftCompensationKey : @YES, +/// }, +/// ]; +/// +/// aggregate_device_properties = @{ +/// @kAudioAggregateDeviceNameKey : @"MiniMetersAggregateDevice", +/// @kAudioAggregateDeviceUIDKey : +/// @"com.josephlyncheski.MiniMetersAggregateDevice", +/// @kAudioAggregateDeviceTapListKey : taps, +/// @kAudioAggregateDeviceTapAutoStartKey : @YES, +/// @kAudioAggregateDeviceIsPrivateKey : @YES, +/// }; +/// ``` +pub fn create_audio_aggregate_device_properties( + tap_uid: Retained, + agg_uid: &str, + agg_name: &str, +) -> CFRetained { + let tap_inner = unsafe { + let dict = CFMutableDictionary::new( + kCFAllocatorDefault, + 2, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ) + .unwrap(); + + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioSubTapUIDKey) as *const _ as *const c_void, + &*tap_uid as *const _ as *const c_void, + ); + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioSubTapDriftCompensationKey) as *const _ as *const c_void, + &*NSNumber::initWithBool(NSNumber::alloc(), true) as *const _ as *const c_void, + ); + + dict + }; + let _taps_list = [tap_inner]; + let taps = unsafe { + CFArray::new( + kCFAllocatorDefault, + _taps_list.as_ptr() as *mut *const c_void, + _taps_list.len() as _, + &kCFTypeArrayCallBacks, + ) + .unwrap() + }; + let aggregate_dev_properties = unsafe { + let dict = CFMutableDictionary::new( + kCFAllocatorDefault, + 5, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ) + .unwrap(); + + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioAggregateDeviceNameKey) as *const _ as *const c_void, + &*CFString::from_str(agg_name) as *const _ as *const c_void, + ); + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioAggregateDeviceUIDKey) as *const _ as *const c_void, + &*CFString::from_str(agg_uid) as *const _ as *const c_void, + ); + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioAggregateDeviceTapListKey) as *const _ as *const c_void, + &*taps as *const _ as *const c_void, + ); + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioAggregateDeviceTapAutoStartKey) as *const _ as *const c_void, + &*NSNumber::initWithBool(NSNumber::alloc(), true) as *const _ as *const c_void, + ); + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioEndPointDeviceIsPrivateKey) as *const _ as *const c_void, + &*NSNumber::initWithBool(NSNumber::alloc(), true) as *const _ as *const c_void, + ); + + CFRetained::cast_unchecked::(dict) + }; + + aggregate_dev_properties +} diff --git a/crates/vendor/cpal/src/host/coreaudio/macos/mod.rs b/crates/vendor/cpal/src/host/coreaudio/macos/mod.rs new file mode 100644 index 000000000..eef318e61 --- /dev/null +++ b/crates/vendor/cpal/src/host/coreaudio/macos/mod.rs @@ -0,0 +1,559 @@ +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + mpsc, Arc, Mutex, Weak, +}; + +use coreaudio::audio_unit::{AudioUnit, Scope}; +use objc2_core_audio::{ + kAudioDeviceProcessorOverload, kAudioDevicePropertyDeviceIsAlive, + kAudioDevicePropertyNominalSampleRate, kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyElementMain, kAudioObjectPropertyScopeGlobal, kAudioObjectSystemObject, + AudioDeviceID, AudioObjectID, AudioObjectPropertyAddress, +}; +use property_listener::AudioObjectPropertyListener; + +pub use self::enumerate::{default_input_device, default_output_device, Devices}; +use super::{asbd_from_config, check_os_status, host_time_to_stream_instant, OSStatus}; +use crate::{ + host::{coreaudio::macos::loopback::LoopbackDevice, emit_error, latch::Latch, try_emit_error}, + traits::{HostTrait, StreamTrait}, + Error, ErrorKind, FrameCount, ResultExt, StreamInstant, +}; + +mod device; +pub mod enumerate; +mod loopback; +mod property_listener; +pub use device::Device; + +/// CoreAudio host, the default host on macOS. +#[derive(Debug)] +pub struct Host; + +impl Host { + pub fn new() -> Result { + Ok(Host) + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + // Assume coreaudio is always available + true + } + + fn devices(&self) -> Result { + Devices::new() + } + + fn default_input_device(&self) -> Option { + default_input_device() + } + + fn default_output_device(&self) -> Option { + default_output_device() + } +} + +/// Type alias for the error callback to reduce complexity +type ErrorCallback = dyn FnMut(Error) + Send; + +/// Spawns a dedicated thread that registers a single property listener, calling `on_change` on +/// each firing. The listener is deregistered when the returned `Sender<()>` is dropped. +fn spawn_property_listener_thread( + object_id: AudioObjectID, + address: AudioObjectPropertyAddress, + on_change: F, +) -> Result, Error> +where + F: FnMut() + Send + 'static, +{ + let (shutdown_tx, shutdown_rx) = mpsc::channel::<()>(); + let (ready_tx, ready_rx) = mpsc::channel(); + + std::thread::spawn(move || { + let listener = AudioObjectPropertyListener::new(object_id, address, on_change); + match listener { + Ok(_l) => { + let _ = ready_tx.send(Ok(())); + let _ = shutdown_rx.recv(); + } + Err(e) => { + let _ = ready_tx.send(Err(e)); + } + } + }); + + ready_rx.recv().map_err(|_| { + Error::with_message( + ErrorKind::StreamInvalidated, + "property listener thread terminated unexpectedly", + ) + })??; + + Ok(shutdown_tx) +} + +/// Registers a `kAudioDeviceProcessorOverload` listener on `device_id`, reporting `Xrun` through +/// `error_callback` on each firing (overload notifications fire on the RT thread). +fn spawn_overload_listener( + device_id: AudioDeviceID, + error_callback: &Arc>, +) -> Result, Error> { + let error_callback = error_callback.clone(); + spawn_property_listener_thread( + device_id, + AudioObjectPropertyAddress { + mSelector: kAudioDeviceProcessorOverload, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }, + move || { + let _ = try_emit_error(&error_callback, ErrorKind::Xrun.into()); + }, + ) +} + +/// A device monitor that can signal when the owning `Stream` handle has been returned to the +/// caller, allowing the delivery thread to start processing events. +pub(super) trait Monitor: Send + Sync { + /// Unblocks the delivery thread. Called after `Stream::new()` and from `Stream::drop()`. + fn signal_ready(&self); +} + +/// Manages device disconnection listener on a dedicated thread to ensure the +/// AudioObjectPropertyListener is always created and dropped on the same thread. +/// This avoids potential threading issues with CoreAudio APIs. +/// +/// When a device disconnects, this manager: +/// 1. Attempts to pause the stream to stop audio I/O +/// 2. Calls the error callback with `ErrorKind::DeviceNotAvailable` +/// +/// The dedicated thread architecture ensures `Stream` can implement `Send`. +struct DisconnectManager { + latch: Latch, + _shutdown_tx: mpsc::Sender<()>, +} + +impl DisconnectManager { + fn new( + device_id: AudioDeviceID, + stream_weak: Weak>, + error_callback: Arc>, + ) -> Result { + let (shutdown_tx, shutdown_rx) = mpsc::channel(); + let (disconnect_tx, disconnect_rx) = mpsc::channel::(); + let (ready_tx, ready_rx) = mpsc::channel(); + + // Spawn a dedicated thread to own all listeners. CoreAudio requires that + // AudioObjectPropertyListeners are added and removed on the same thread. + let disconnect_tx_alive = disconnect_tx.clone(); + let disconnect_tx_rate = disconnect_tx; + let error_callback_overload = error_callback.clone(); + std::thread::spawn(move || { + let alive_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyDeviceIsAlive, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + let alive_listener = + AudioObjectPropertyListener::new(device_id, alive_address, move || { + let _ = disconnect_tx_alive.send(Error::with_message( + ErrorKind::DeviceNotAvailable, + "Device disconnected", + )); + }); + + let rate_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyNominalSampleRate, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + let rate_listener = + AudioObjectPropertyListener::new(device_id, rate_address, move || { + let _ = disconnect_tx_rate.send(Error::with_message( + ErrorKind::StreamInvalidated, + "Device sample rate changed", + )); + }); + + // Overload notifications fire on the RT thread. + let overload_address = AudioObjectPropertyAddress { + mSelector: kAudioDeviceProcessorOverload, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + let overload_listener = + AudioObjectPropertyListener::new(device_id, overload_address, move || { + let _ = try_emit_error(&error_callback_overload, ErrorKind::Xrun.into()); + }); + + match (alive_listener, rate_listener, overload_listener) { + (Ok(_alive), Ok(_rate), Ok(_overload)) => { + let _ = ready_tx.send(Ok(())); + // Block until the stream is dropped; listeners are removed on drop. + let _ = shutdown_rx.recv(); + } + (Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => { + let _ = ready_tx.send(Err(e)); + } + } + }); + + ready_rx.recv().map_err(|_| { + Error::with_message( + ErrorKind::StreamInvalidated, + "Stream monitor terminated unexpectedly", + ) + })??; + + let mut latch = Latch::new(); + let waiter = latch.waiter(); + + let handle = std::thread::Builder::new() + .name("cpal-coreaudio-disconnect".into()) + .spawn(move || { + // If the Latch is dropped without being released (error path), exit cleanly. + if !waiter.wait() { + return; + } + while let Ok(err) = disconnect_rx.recv() { + if let Some(stream_arc) = stream_weak.upgrade() { + if let Ok(mut stream_inner) = stream_arc.try_lock() { + let _ = stream_inner.pause(); + } + emit_error(&error_callback, err); + } else { + break; + } + } + }) + .map_err(|e| { + Error::with_message( + ErrorKind::ResourceExhausted, + format!("Failed to spawn disconnect thread: {e}"), + ) + })?; + + latch.add_thread(handle.thread().clone()); + Ok(DisconnectManager { + latch, + _shutdown_tx: shutdown_tx, + }) + } +} + +impl Monitor for DisconnectManager { + fn signal_ready(&self) { + self.latch.release(); + } +} + +/// Manages the system default output device change listener on a dedicated thread. +/// +/// When the system default output device changes: +/// - If a new valid default exists, AudioUnit reroutes and `DeviceChanged` is reported. +/// - If there is no new default, the stream is paused and `DeviceNotAvailable` is reported. +struct DefaultOutputMonitor { + latch: Latch, + _shutdown_tx: mpsc::Sender<()>, +} + +impl DefaultOutputMonitor { + fn new( + stream_weak: Weak>, + error_callback: Arc>, + latency_refresh: Option<(Arc, Scope)>, + ) -> Result { + let (change_tx, change_rx) = mpsc::channel::<()>(); + let shutdown_tx = spawn_property_listener_thread( + kAudioObjectSystemObject as AudioObjectID, + AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyDefaultOutputDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }, + move || { + let _ = change_tx.send(()); + }, + )?; + + // The overload (xrun) listener targets a specific device, so it must be re-registered + // against whatever device is current whenever the default output reroutes. Held only to + // shut down the previous listener thread on drop when reassigned below. + let overload_error_callback = error_callback.clone(); + let mut _overload_shutdown_tx = default_output_device() + .map(|device| spawn_overload_listener(device.audio_device_id, &overload_error_callback)) + .transpose()?; + + let mut latch = Latch::new(); + let waiter = latch.waiter(); + + let handle = std::thread::Builder::new() + .name("cpal-coreaudio-default-output".into()) + .spawn(move || { + if !waiter.wait() { + return; + } + while let Ok(()) = change_rx.recv() { + let Some(stream) = stream_weak.upgrade() else { + break; + }; + match default_output_device() { + None => { + _overload_shutdown_tx = None; + if let Ok(mut inner) = stream.try_lock() { + let _ = inner.pause(); + } + emit_error( + &error_callback, + Error::with_message( + ErrorKind::DeviceNotAvailable, + "no default output device", + ), + ); + } + Some(device) => { + // DefaultOutput AudioUnit rerouted automatically: recompute and notify + // the buffer depth for the new device. + if let Some((frames, scope)) = &latency_refresh { + if let Ok(inner) = stream.lock() { + let depth = + device::get_device_buffer_frame_size(&inner.audio_unit) + .ok() + .map_or(0, |buffer| { + buffer + + device::get_device_extra_latency_frames( + &inner.audio_unit, + *scope, + ) + }); + frames.store(depth, Ordering::Relaxed); + } + } + _overload_shutdown_tx = spawn_overload_listener( + device.audio_device_id, + &overload_error_callback, + ) + .ok(); + emit_error( + &error_callback, + Error::with_message( + ErrorKind::DeviceChanged, + "default output device changed", + ), + ); + } + } + } + }) + .map_err(|e| { + Error::with_message( + ErrorKind::ResourceExhausted, + format!("failed to spawn default-output monitor thread: {e}"), + ) + })?; + + latch.add_thread(handle.thread().clone()); + Ok(DefaultOutputMonitor { + latch, + _shutdown_tx: shutdown_tx, + }) + } +} + +impl Monitor for DefaultOutputMonitor { + fn signal_ready(&self) { + self.latch.release(); + } +} + +struct StreamInner { + playing: bool, + audio_unit: AudioUnit, + // Track the device with which the audio unit was spawned + _device_id: AudioDeviceID, + /// Manage the lifetime of the aggregate device used for loopback recording + _loopback_device: Option, +} + +impl StreamInner { + fn play(&mut self) -> Result<(), Error> { + if !self.playing { + self.audio_unit + .start() + .context("Failed to start audio unit")?; + self.playing = true; + } + Ok(()) + } + + fn pause(&mut self) -> Result<(), Error> { + if self.playing { + self.audio_unit + .stop() + .context("Failed to stop audio unit")?; + self.playing = false; + } + Ok(()) + } +} + +pub struct Stream { + inner: Arc>, + monitor: Box, +} + +impl Stream { + fn new(inner: Arc>, monitor: Box) -> Self { + Self { inner, monitor } + } + + fn signal_ready(&self) { + self.monitor.signal_ready(); + } +} + +impl Drop for Stream { + fn drop(&mut self) { + // Unblock monitor delivery threads if the stream is dropped early. + self.monitor.signal_ready(); + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), Error> { + self.inner + .lock() + .map_err(|_| Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned"))? + .play() + } + + fn pause(&self) -> Result<(), Error> { + self.inner + .lock() + .map_err(|_| Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned"))? + .pause() + } + + fn now(&self) -> StreamInstant { + let m_host_time = unsafe { mach2::mach_time::mach_absolute_time() }; + host_time_to_stream_instant(m_host_time).expect("mach_timebase_info failed") + } + + fn buffer_size(&self) -> Result { + let stream = self.inner.lock().map_err(|_| { + Error::with_message(ErrorKind::StreamInvalidated, "Stream lock poisoned") + })?; + device::get_device_buffer_frame_size(&stream.audio_unit) + .map(|size| size as FrameCount) + .context("Failed to get buffer frame size") + } +} + +#[cfg(test)] +mod test { + use crate::{ + default_host, + traits::{DeviceTrait, HostTrait, StreamTrait}, + InputCallbackInfo, OutputCallbackInfo, Sample, + }; + + #[test] + fn test_play() { + let host = default_host(); + let device = host.default_output_device().unwrap(); + + let mut supported_configs_range = device.supported_output_configs().unwrap(); + let supported_config = supported_configs_range + .next() + .unwrap() + .with_max_sample_rate(); + let config = supported_config.config(); + + let stream = device + .build_output_stream( + config, + write_silence::, + move |err| println!("Error: {err}"), + None, // None=blocking, Some(Duration)=timeout + ) + .unwrap(); + stream.play().unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + + #[test] + fn test_record() { + let host = default_host(); + let device = host.default_input_device().unwrap(); + println!("Device: {:?}", device.description()); + + let mut supported_configs_range = device.supported_input_configs().unwrap(); + println!("Supported configs:"); + for config in supported_configs_range.clone() { + println!("{:?}", config) + } + let supported_config = supported_configs_range + .next() + .unwrap() + .with_max_sample_rate(); + let config = supported_config.config(); + + let stream = device + .build_input_stream( + config, + move |data: &[f32], _: &InputCallbackInfo| { + // react to stream events and read or write stream data here. + println!("Got data: {:?}", &data[..25]); + }, + move |err| println!("Error: {err}"), + None, // None=blocking, Some(Duration)=timeout + ) + .unwrap(); + stream.play().unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + + #[test] + fn test_record_output() { + if std::env::var("CI").is_ok() { + println!("Skipping test_record_output in CI environment due to permissions"); + return; + } + + let host = default_host(); + let device = host.default_output_device().unwrap(); + + let mut supported_configs_range = device.supported_output_configs().unwrap(); + let supported_config = supported_configs_range + .next() + .unwrap() + .with_max_sample_rate(); + let config = supported_config.config(); + + println!("Building input stream"); + let stream = device + .build_input_stream( + config, + move |data: &[f32], _: &InputCallbackInfo| { + // react to stream events and read or write stream data here. + println!("Got data: {:?}", &data[..25]); + }, + move |err| println!("Error: {err}"), + None, // None=blocking, Some(Duration)=timeout + ) + .unwrap(); + stream.play().unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + + fn write_silence(data: &mut [T], _: &OutputCallbackInfo) { + for sample in data.iter_mut() { + *sample = Sample::EQUILIBRIUM; + } + } +} diff --git a/crates/vendor/cpal/src/host/coreaudio/macos/property_listener.rs b/crates/vendor/cpal/src/host/coreaudio/macos/property_listener.rs new file mode 100644 index 000000000..24f36f597 --- /dev/null +++ b/crates/vendor/cpal/src/host/coreaudio/macos/property_listener.rs @@ -0,0 +1,89 @@ +//! Helper code for registering audio object property listeners. +use std::ptr::NonNull; + +use objc2_core_audio::{ + AudioObjectAddPropertyListener, AudioObjectID, AudioObjectPropertyAddress, + AudioObjectRemovePropertyListener, +}; + +use super::OSStatus; +use crate::Error; + +/// A double-indirection to be able to pass a closure (a fat pointer) +/// via a single c_void. +struct PropertyListenerCallbackWrapper(Box); + +/// Maintain an audio object property listener. +/// The listener will be removed when this type is dropped. +pub struct AudioObjectPropertyListener { + callback: Box, + property_address: AudioObjectPropertyAddress, + audio_object_id: AudioObjectID, + removed: bool, +} + +impl AudioObjectPropertyListener { + /// Attach the provided callback as a audio object property listener. + pub fn new( + audio_object_id: AudioObjectID, + property_address: AudioObjectPropertyAddress, + callback: F, + ) -> Result { + let callback = Box::new(PropertyListenerCallbackWrapper(Box::new(callback))); + unsafe { + coreaudio::Error::from_os_status(AudioObjectAddPropertyListener( + audio_object_id, + NonNull::from(&property_address), + Some(property_listener_handler_shim), + &*callback as *const _ as *mut _, + ))?; + }; + Ok(Self { + callback, + audio_object_id, + property_address, + removed: false, + }) + } + + /// Explicitly remove the property listener. + /// Use this method if you need to explicitly handle failure to remove + /// the property listener. + #[allow(dead_code)] + pub fn remove(mut self) -> Result<(), Error> { + self.remove_inner() + } + + fn remove_inner(&mut self) -> Result<(), Error> { + unsafe { + coreaudio::Error::from_os_status(AudioObjectRemovePropertyListener( + self.audio_object_id, + NonNull::from(&self.property_address), + Some(property_listener_handler_shim), + &*self.callback as *const _ as *mut _, + ))?; + } + self.removed = true; + Ok(()) + } +} + +impl Drop for AudioObjectPropertyListener { + fn drop(&mut self) { + if !self.removed { + let _ = self.remove_inner(); + } + } +} + +/// Callback used to call user-provided closure as a property listener. +unsafe extern "C-unwind" fn property_listener_handler_shim( + _: AudioObjectID, + _: u32, + _: NonNull, + callback: *mut ::std::os::raw::c_void, +) -> OSStatus { + let wrapper = callback as *mut PropertyListenerCallbackWrapper; + (*wrapper).0(); + 0 +} diff --git a/crates/vendor/cpal/src/host/coreaudio/mod.rs b/crates/vendor/cpal/src/host/coreaudio/mod.rs new file mode 100644 index 000000000..82c0b506c --- /dev/null +++ b/crates/vendor/cpal/src/host/coreaudio/mod.rs @@ -0,0 +1,143 @@ +//! CoreAudio backend implementation. +//! +//! Default backend on macOS, iOS, and tvOS. + +use objc2_core_audio_types::{ + kAudioFormatFlagIsFloat, kAudioFormatFlagIsPacked, kAudioFormatFlagIsSignedInteger, + kAudioFormatLinearPCM, AudioStreamBasicDescription, +}; + +use crate::{Error, ErrorKind, SampleFormat, StreamConfig, StreamInstant}; + +// iOS and tvOS share the same CoreAudio / AudioUnit surface (RemoteIO, +// AVAudioSession), so both target the `ios` submodule. +#[cfg(not(target_os = "macos"))] +mod ios; +#[cfg(target_os = "macos")] +mod macos; + +#[cfg(not(target_os = "macos"))] +#[allow(unused_imports)] +pub use self::ios::{ + enumerate::{Devices, SupportedInputConfigs, SupportedOutputConfigs}, + Device, Host, Stream, +}; +#[cfg(target_os = "macos")] +pub use self::macos::{Host, Stream}; + +// Common helper methods used by both macOS and iOS + +fn check_os_status(os_status: OSStatus) -> Result<(), Error> { + coreaudio::Error::from_os_status(os_status).map_err(Error::from) +} + +// Create a coreaudio AudioStreamBasicDescription from a CPAL Format. +fn asbd_from_config( + config: StreamConfig, + sample_format: SampleFormat, +) -> AudioStreamBasicDescription { + let n_channels = config.channels as usize; + let sample_rate = config.sample_rate; + let bytes_per_channel = sample_format.sample_size(); + let bits_per_channel = bytes_per_channel * 8; + let bytes_per_frame = n_channels * bytes_per_channel; + let frames_per_packet = 1; + let bytes_per_packet = frames_per_packet * bytes_per_frame; + let format_flags = match sample_format { + SampleFormat::F32 | SampleFormat::F64 => kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked, + SampleFormat::I8 + | SampleFormat::I16 + | SampleFormat::I24 + | SampleFormat::I32 + | SampleFormat::I64 => kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked, + _ => kAudioFormatFlagIsPacked, + }; + AudioStreamBasicDescription { + mBitsPerChannel: bits_per_channel as _, + mBytesPerFrame: bytes_per_frame as _, + mChannelsPerFrame: n_channels as _, + mBytesPerPacket: bytes_per_packet as _, + mFramesPerPacket: frames_per_packet as _, + mFormatFlags: format_flags, + mFormatID: kAudioFormatLinearPCM, + mSampleRate: sample_rate as _, + mReserved: 0, + } +} + +#[inline] +fn host_time_to_stream_instant(m_host_time: u64) -> Result { + let mut info: mach2::mach_time::mach_timebase_info = Default::default(); + let res = unsafe { mach2::mach_time::mach_timebase_info(&mut info) }; + check_os_status(res)?; + let nanos = m_host_time as u128 * info.numer as u128 / info.denom as u128; + let secs = u64::try_from(nanos / 1_000_000_000) + .map_err(|_| Error::with_message(ErrorKind::Other, "Timestamp conversion overflowed"))?; + let subsec_nanos = (nanos % 1_000_000_000) as u32; + Ok(StreamInstant::new(secs, subsec_nanos)) +} + +impl From for Error { + fn from(err: coreaudio::Error) -> Self { + use coreaudio::error::{AudioCodecError, AudioError, AudioFormatError, AudioUnitError}; + let msg = format!("{err}"); + match err { + coreaudio::Error::RenderCallbackBufferFormatDoesNotMatchAudioUnitStreamFormat + | coreaudio::Error::NoKnownSubtype + | coreaudio::Error::UnsupportedSampleRate + | coreaudio::Error::UnsupportedStreamFormat + | coreaudio::Error::NonInterleavedInputOnlySupportsMono + | coreaudio::Error::AudioUnit(AudioUnitError::FormatNotSupported) + | coreaudio::Error::AudioUnit(AudioUnitError::InvalidPropertyValue) + | coreaudio::Error::AudioUnit(AudioUnitError::TooManyFramesToProcess) + | coreaudio::Error::AudioCodec(AudioCodecError::UnsupportedFormat) + | coreaudio::Error::AudioFormat(AudioFormatError::UnsupportedDataFormat) + | coreaudio::Error::AudioFormat(AudioFormatError::UnknownFormat) => { + Error::with_message(ErrorKind::UnsupportedConfig, msg) + } + + coreaudio::Error::SystemSoundClientMessageTimedOut + | coreaudio::Error::NoMatchingDefaultAudioUnitFound + | coreaudio::Error::AudioUnit(AudioUnitError::NoConnection) + | coreaudio::Error::AudioUnit(AudioUnitError::FailedInitialization) + | coreaudio::Error::Audio(AudioError::FileNotFound) => { + Error::with_message(ErrorKind::DeviceNotAvailable, msg) + } + + coreaudio::Error::AudioUnit(AudioUnitError::Unauthorized) + | coreaudio::Error::Audio(AudioError::FilePermission) => { + Error::with_message(ErrorKind::PermissionDenied, msg) + } + + coreaudio::Error::AudioUnit(AudioUnitError::InvalidProperty) + | coreaudio::Error::AudioUnit(AudioUnitError::InvalidParameter) + | coreaudio::Error::AudioUnit(AudioUnitError::InvalidElement) + | coreaudio::Error::AudioUnit(AudioUnitError::InvalidScope) + | coreaudio::Error::AudioUnit(AudioUnitError::PropertyNotInUse) + | coreaudio::Error::AudioCodec(AudioCodecError::UnknownProperty) + | coreaudio::Error::AudioCodec(AudioCodecError::BadPropertySize) + | coreaudio::Error::AudioFormat(AudioFormatError::UnsupportedProperty) + | coreaudio::Error::AudioFormat(AudioFormatError::BadPropertySize) + | coreaudio::Error::AudioFormat(AudioFormatError::BadSpecifierSize) + | coreaudio::Error::Audio(AudioError::Param) + | coreaudio::Error::Audio(AudioError::BadFilePath) => { + Error::with_message(ErrorKind::InvalidInput, msg) + } + + coreaudio::Error::Audio(AudioError::Unimplemented) + | coreaudio::Error::AudioUnit(AudioUnitError::PropertyNotWritable) + | coreaudio::Error::AudioUnit(AudioUnitError::InvalidOfflineRender) + | coreaudio::Error::AudioCodec(AudioCodecError::IllegalOperation) => { + Error::with_message(ErrorKind::UnsupportedOperation, msg) + } + + _ => Error::with_message(ErrorKind::BackendError, msg), + } + } +} + +pub(crate) type OSStatus = i32; + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); diff --git a/crates/vendor/cpal/src/host/custom/mod.rs b/crates/vendor/cpal/src/host/custom/mod.rs new file mode 100644 index 000000000..d51acd26d --- /dev/null +++ b/crates/vendor/cpal/src/host/custom/mod.rs @@ -0,0 +1,472 @@ +//! Custom host backend. +//! +//! Allows user-defined host implementations with the `custom` feature. +//! See `examples/custom.rs` for usage. + +use core::time::Duration; +use std::fmt; + +use crate::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + Data, DeviceDescription, DeviceId, Error, ErrorKind, FrameCount, InputCallbackInfo, + OutputCallbackInfo, SampleFormat, StreamConfig, StreamInstant, SupportedStreamConfig, + SupportedStreamConfigRange, +}; + +/// A host that can be used to write custom [`HostTrait`] implementations. +/// +/// # Usage +/// +/// A [`CustomHost`](Host) can be used on its own, but most crates that depend on `cpal` use a [`cpal::Host`](crate::Host) instead. +/// You can turn a `CustomHost` into a `Host` fairly easily: +/// +/// ```ignore +/// let custom = cpal::platform::CustomHost::from_host(/* ... */); +/// let host = cpal::Host::from(custom); +/// ``` +/// +/// Custom hosts are marked as unavailable and will not appear in [`cpal::available_hosts`](crate::available_hosts). +pub struct Host(Box); + +impl Host { + // this only exists for impl_platform_host, which requires it + pub(crate) fn new() -> Result { + Err(Error::new(ErrorKind::HostUnavailable)) + } + + /// Construct a custom host from an arbitrary [`HostTrait`] implementation. + pub fn from_host(host: T) -> Self + where + T: HostTrait + Send + Sync + 'static, + T::Device: Send + Sync + Clone, + ::SupportedInputConfigs: Clone, + ::SupportedOutputConfigs: Clone, + ::Stream: Send + Sync, + { + Self(Box::new(host)) + } +} + +/// A device that can be used to write custom [`DeviceTrait`] implementations. +/// +/// # Usage +/// +/// A [`CustomDevice`](Device) can be used on its own, but most crates that depend on `cpal` use a [`cpal::Device`](crate::Device) instead. +/// You can turn a `Device` into a `Device` fairly easily: +/// +/// ```ignore +/// let custom = cpal::platform::CustomDevice::from_device(/* ... */); +/// let device = cpal::Device::from(custom); +/// ``` +/// +/// `rodio`, for example, lets you build an `OutputStream` with a [`cpal::Device`](crate::Device): +/// ```ignore +/// let custom = cpal::platform::CustomDevice::from_device(/* ... */); +/// let device = cpal::Device::from(custom); +/// +/// let stream_builder = rodio::OutputStreamBuilder::from_device(device).expect("failed to build stream"); +/// ``` +pub struct Device(Box); + +impl Device { + /// Construct a custom device from an arbitrary [`DeviceTrait`] implementation. + pub fn from_device(device: T) -> Self + where + T: DeviceTrait + Send + Sync + Clone + 'static, + T::SupportedInputConfigs: Clone, + T::SupportedOutputConfigs: Clone, + T::Stream: Send + Sync, + { + Self(Box::new(device)) + } +} + +impl Clone for Device { + fn clone(&self) -> Self { + self.0.clone() + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + DeviceTrait::id(self).ok() == DeviceTrait::id(other).ok() + } +} + +impl Eq for Device {} + +impl std::hash::Hash for Device { + fn hash(&self, state: &mut H) { + DeviceTrait::id(self).ok().hash(state); + } +} + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let desc = DeviceTrait::description(self).map_err(|_| fmt::Error)?; + f.write_str(desc.name()) + } +} + +impl fmt::Debug for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Device") + .field(&DeviceTrait::description(self).map(|d| d.name().to_owned())) + .finish() + } +} + +/// A stream that can be used with custom [`StreamTrait`] implementations. +pub struct Stream(Box); + +impl Stream { + /// Construct a custom stream from an arbitrary [`StreamTrait`] implementation. + pub fn from_stream(stream: T) -> Self + where + T: StreamTrait + Send + Sync + 'static, + { + Self(Box::new(stream)) + } +} + +// dyn-compatible versions of DeviceTrait, HostTrait, and StreamTrait +// these only accept/return things via trait objects + +type Devices = Box>; +trait HostErased: Send + Sync { + fn devices(&self) -> Result; + fn default_input_device(&self) -> Option; + fn default_output_device(&self) -> Option; +} + +pub struct SupportedConfigs(Box); + +// A trait for supported configs. This only adds a dyn compatible clone function +// This is required because `SupportedInputConfigsInner` & `SupportedOutputConfigsInner` are `Clone` +trait SupportedConfigsErased: Iterator { + fn clone(&self) -> SupportedConfigs; +} + +impl SupportedConfigsErased for T +where + T: Iterator + Clone + 'static, +{ + fn clone(&self) -> SupportedConfigs { + SupportedConfigs(Box::new(Clone::clone(self))) + } +} + +impl Iterator for SupportedConfigs { + type Item = SupportedStreamConfigRange; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl Clone for SupportedConfigs { + fn clone(&self) -> Self { + self.0.clone() + } +} + +type ErrorCallback = Box; +type InputCallback = Box; +type OutputCallback = Box; + +trait DeviceErased: Send + Sync { + fn description(&self) -> Result; + fn id(&self) -> Result; + fn supports_input(&self) -> bool; + fn supports_output(&self) -> bool; + fn supported_input_configs(&self) -> Result; + fn supported_output_configs(&self) -> Result; + fn default_input_config(&self) -> Result; + fn default_output_config(&self) -> Result; + fn build_input_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: InputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result; + fn build_output_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: OutputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result; + // Required because `DeviceInner` is clone + fn clone(&self) -> Device; +} + +trait StreamErased: Send + Sync { + fn play(&self) -> Result<(), Error>; + fn pause(&self) -> Result<(), Error>; + fn now(&self) -> StreamInstant; + fn buffer_size(&self) -> Result; +} + +fn device_to_erased(d: impl DeviceErased + 'static) -> Device { + Device(Box::new(d)) +} + +impl HostErased for T +where + T: HostTrait + Send + Sync, + T::Devices: 'static, + T::Device: DeviceErased + 'static, +{ + fn devices(&self) -> Result { + let iter = ::devices(self)?; + let erased = Box::new(iter.map(device_to_erased)); + Ok(erased) + } + + fn default_input_device(&self) -> Option { + ::default_input_device(self).map(device_to_erased) + } + + fn default_output_device(&self) -> Option { + ::default_output_device(self).map(device_to_erased) + } +} + +fn supported_configs_to_erased( + i: impl Iterator + Clone + 'static, +) -> SupportedConfigs { + SupportedConfigs(Box::new(i)) +} + +fn stream_to_erased(s: impl StreamTrait + Send + Sync + 'static) -> Stream { + Stream(Box::new(s)) +} + +impl DeviceErased for T +where + T: DeviceTrait + Send + Sync + Clone + 'static, + T::SupportedInputConfigs: Clone + 'static, + T::SupportedOutputConfigs: Clone + 'static, + T::Stream: Send + Sync + 'static, +{ + fn description(&self) -> Result { + ::description(self) + } + + fn id(&self) -> Result { + ::id(self) + } + + fn supports_input(&self) -> bool { + ::supports_input(self) + } + + fn supports_output(&self) -> bool { + ::supports_output(self) + } + + fn supported_input_configs(&self) -> Result { + ::supported_input_configs(self).map(supported_configs_to_erased) + } + + fn supported_output_configs(&self) -> Result { + ::supported_output_configs(self).map(supported_configs_to_erased) + } + + fn default_input_config(&self) -> Result { + ::default_input_config(self) + } + + fn default_output_config(&self) -> Result { + ::default_output_config(self) + } + + fn build_input_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: InputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result { + ::build_input_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + .map(stream_to_erased) + } + + fn build_output_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: OutputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result { + ::build_output_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + .map(stream_to_erased) + } + + fn clone(&self) -> Device { + device_to_erased(Clone::clone(self)) + } +} + +impl StreamErased for T +where + T: StreamTrait + Send + Sync, +{ + fn play(&self) -> Result<(), Error> { + ::play(self) + } + + fn pause(&self) -> Result<(), Error> { + ::pause(self) + } + + fn now(&self) -> StreamInstant { + ::now(self) + } + + fn buffer_size(&self) -> Result { + ::buffer_size(self) + } +} + +// implementations of HostTrait, DeviceTrait, and StreamTrait for custom versions + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + false + } + + fn devices(&self) -> Result { + self.0.devices() + } + + fn default_input_device(&self) -> Option { + self.0.default_input_device() + } + + fn default_output_device(&self) -> Option { + self.0.default_output_device() + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedConfigs; + + type SupportedOutputConfigs = SupportedConfigs; + + type Stream = Stream; + + fn description(&self) -> Result { + self.0.description() + } + + fn id(&self) -> Result { + self.0.id() + } + + fn supports_input(&self) -> bool { + self.0.supports_input() + } + + fn supports_output(&self) -> bool { + self.0.supports_output() + } + + fn supported_input_configs(&self) -> Result { + self.0.supported_input_configs() + } + + fn supported_output_configs(&self) -> Result { + self.0.supported_output_configs() + } + + fn default_input_config(&self) -> Result { + self.0.default_input_config() + } + + fn default_output_config(&self) -> Result { + self.0.default_output_config() + } + + fn build_input_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + self.0.build_input_stream_raw( + config, + sample_format, + Box::new(data_callback), + Box::new(error_callback), + timeout, + ) + } + + fn build_output_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + self.0.build_output_stream_raw( + config, + sample_format, + Box::new(data_callback), + Box::new(error_callback), + timeout, + ) + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), Error> { + self.0.play() + } + + fn pause(&self) -> Result<(), Error> { + self.0.pause() + } + + fn now(&self) -> StreamInstant { + self.0.now() + } + + fn buffer_size(&self) -> Result { + self.0.buffer_size() + } +} diff --git a/crates/vendor/cpal/src/host/equilibrium.rs b/crates/vendor/cpal/src/host/equilibrium.rs new file mode 100644 index 000000000..8b4d960c1 --- /dev/null +++ b/crates/vendor/cpal/src/host/equilibrium.rs @@ -0,0 +1,56 @@ +use crate::{Sample, SampleFormat, U24}; + +pub const DSD_EQUILIBRIUM_BYTE: u8 = 0x69; +pub const U8_EQUILIBRIUM_BYTE: u8 = 0x80; + +/// Fill `buffer` with the equilibrium value for any `sample_format`. +#[inline] +pub fn fill_equilibrium(buffer: &mut [u8], sample_format: SampleFormat) { + macro_rules! fill_typed { + ($sample_type:ty) => {{ + let sample_size = std::mem::size_of::<$sample_type>(); + + debug_assert_eq!( + buffer.len() % sample_size, + 0, + "Buffer size must be aligned to sample size for format {:?}", + sample_format + ); + + let num_samples = buffer.len() / sample_size; + let equilibrium = <$sample_type as Sample>::EQUILIBRIUM; + + // Safety: buffer length is verified to be a multiple of the sample size above. + let samples = unsafe { + std::slice::from_raw_parts_mut( + buffer.as_mut_ptr() as *mut $sample_type, + num_samples, + ) + }; + + for sample in samples { + *sample = equilibrium; + } + }}; + } + + if sample_format.is_int() || sample_format.is_float() { + buffer.fill(0); + } else if sample_format == SampleFormat::U8 { + buffer.fill(U8_EQUILIBRIUM_BYTE); + } else if sample_format.is_dsd() { + buffer.fill(DSD_EQUILIBRIUM_BYTE); + } else { + // Multi-byte unsigned integer formats require a fill equal to the midpoint of their range. + debug_assert!(sample_format.is_uint()); + match sample_format { + SampleFormat::U16 => fill_typed!(u16), + SampleFormat::U24 => fill_typed!(U24), + SampleFormat::U32 => fill_typed!(u32), + SampleFormat::U64 => fill_typed!(u64), + _ => unimplemented!( + "failed to fill equilibrium for unsupported unsigned format {sample_format:?}" + ), + } + } +} diff --git a/crates/vendor/cpal/src/host/error_emit.rs b/crates/vendor/cpal/src/host/error_emit.rs new file mode 100644 index 000000000..daf8e7632 --- /dev/null +++ b/crates/vendor/cpal/src/host/error_emit.rs @@ -0,0 +1,40 @@ +//! Error delivery helpers for audio callbacks. +//! +//! Pick the helper based on what you need: +//! - Must not block (RT process callback): [`try_emit_error`] +//! - Caller must not miss the error: [`emit_error`] (blocks until callback available) + +use std::sync::{Mutex, TryLockError}; + +use crate::Error; + +/// Deliver an error, blocking until the callback is available. +pub(crate) fn emit_error(callback: &Mutex, error: Error) +where + E: FnMut(Error) + Send + ?Sized, +{ + let mut cb = callback.lock().unwrap_or_else(|e| e.into_inner()); + (*cb)(error); +} + +/// Try to deliver an error without blocking. +/// +/// Returns `Ok(())` if the callback was invoked, or `Err(error)` if the lock was contended and +/// the error could not be delivered. +#[allow(dead_code)] +pub(crate) fn try_emit_error(callback: &Mutex, error: Error) -> Result<(), Error> +where + E: FnMut(Error) + Send + ?Sized, +{ + match callback.try_lock() { + Ok(mut cb) => { + (*cb)(error); + Ok(()) + } + Err(TryLockError::Poisoned(e)) => { + (*e.into_inner())(error); + Ok(()) + } + Err(TryLockError::WouldBlock) => Err(error), + } +} diff --git a/crates/vendor/cpal/src/host/jack/device.rs b/crates/vendor/cpal/src/host/jack/device.rs new file mode 100644 index 000000000..18b0a9e9e --- /dev/null +++ b/crates/vendor/cpal/src/host/jack/device.rs @@ -0,0 +1,376 @@ +use std::{ + fmt, + hash::{Hash, Hasher}, + time::Duration, +}; + +use super::{stream::Stream, JACK_SAMPLE_FORMAT}; +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; +use crate::{ + traits::DeviceTrait, BufferSize, ChannelCount, Data, DeviceDescription, + DeviceDescriptionBuilder, DeviceDirection, DeviceId, Error, ErrorKind, InputCallbackInfo, + OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, +}; + +const DEFAULT_NUM_CHANNELS: ChannelCount = 2; + +// 64 = AES10 (MADI) maximum; JACK itself imposes no per-client channel limit. +const MAX_CHANNELS: ChannelCount = 64; + +#[derive(Clone, Debug)] +pub struct Device { + name: String, + sample_rate: SampleRate, + buffer_size: SupportedBufferSize, + direction: DeviceDirection, + start_server_automatically: bool, + connect_ports_automatically: bool, +} + +impl Device { + fn new_device( + name: String, + connect_ports_automatically: bool, + start_server_automatically: bool, + direction: DeviceDirection, + ) -> Result { + let client_options = super::get_client_options(start_server_automatically); + + // Create a dummy client to find out the sample rate of the server to be able to provide it + // as a possible config. This client will be dropped, and a new one will be created when + // making the stream. This is a hack due to the fact that the Client must be moved to + // create the AsyncClient. + let client = super::get_client(&name, client_options)?; + if !matches!(direction, DeviceDirection::Input | DeviceDirection::Output) { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + format!("JACK does not support {direction:?} direction"), + )); + } + Ok(Self { + // The name given to the client by JACK, could potentially be different from the name + // supplied e.g. if there is a name collision + name: client.name().to_owned(), + sample_rate: client.sample_rate(), + buffer_size: SupportedBufferSize::Range { + min: client.buffer_size(), + max: client.buffer_size(), + }, + direction, + start_server_automatically, + connect_ports_automatically, + }) + } + + fn id(&self) -> Result { + // `self.name` carries the process ID (see `Host::new`) so that concurrent cpal + // instances get distinct JACK client names. It must not leak into `DeviceId`, + // which callers persist across restarts (see #1279): a synthetic device's + // direction is the only part of its identity that's actually stable. + let id = if self.is_input() { "input" } else { "output" }; + Ok(DeviceId::new(crate::platform::HostId::Jack, id)) + } + + pub fn default_output_device( + name: &str, + connect_ports_automatically: bool, + start_server_automatically: bool, + ) -> Result { + let output_client_name = format!("{}_out", name); + Device::new_device( + output_client_name, + connect_ports_automatically, + start_server_automatically, + DeviceDirection::Output, + ) + } + + pub fn default_input_device( + name: &str, + connect_ports_automatically: bool, + start_server_automatically: bool, + ) -> Result { + let input_client_name = format!("{}_in", name); + Device::new_device( + input_client_name, + connect_ports_automatically, + start_server_automatically, + DeviceDirection::Input, + ) + } + + pub fn default_config(&self) -> Result { + let channels = DEFAULT_NUM_CHANNELS; + let sample_rate = self.sample_rate; + let buffer_size = self.buffer_size; + // The sample format for JACK audio ports is always "32-bit float mono audio" in the current implementation. + // Custom formats are allowed within JACK, but this is of niche interest. + // The format can be found programmatically by calling jack::PortSpec::port_type() on a created port. + let sample_format = JACK_SAMPLE_FORMAT; + Ok(SupportedStreamConfig { + channels, + sample_rate, + buffer_size, + sample_format, + }) + } + + pub fn supported_configs(&self) -> Vec { + let f = match self.default_config() { + Err(_) => return vec![], + Ok(f) => f, + }; + + (1..=MAX_CHANNELS) + .map(|channels| SupportedStreamConfigRange { + channels, + min_sample_rate: f.sample_rate, + max_sample_rate: f.sample_rate, + buffer_size: f.buffer_size, + sample_format: f.sample_format, + }) + .collect() + } + + pub fn is_input(&self) -> bool { + matches!(self.direction, DeviceDirection::Input) + } + + pub fn is_output(&self) -> bool { + matches!(self.direction, DeviceDirection::Output) + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn description(&self) -> Result { + // Not `self.name`: that's the JACK client name, which carries a process ID + // uniquifier (see `Host::new`) and isn't meant as a user-facing device label. + let name = if self.is_input() { + "JACK Input" + } else { + "JACK Output" + }; + Ok(DeviceDescriptionBuilder::new(name) + .direction(self.direction) + .build()) + } + + fn id(&self) -> Result { + Device::id(self) + } + + fn supported_input_configs(&self) -> Result { + Ok(self.supported_configs().into_iter()) + } + + fn supported_output_configs(&self) -> Result { + Ok(self.supported_configs().into_iter()) + } + + /// Returns the default input config + /// The sample format for JACK audio ports is always "32-bit float mono audio" unless using a custom type. + /// The sample rate is set by the JACK server. + fn default_input_config(&self) -> Result { + self.default_config() + } + + /// Returns the default output config + /// The sample format for JACK audio ports is always "32-bit float mono audio" unless using a custom type. + /// The sample rate is set by the JACK server. + fn default_output_config(&self) -> Result { + self.default_config() + } + + fn build_input_stream_raw( + &self, + conf: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + if self.is_output() { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Device does not support input", + )); + } + crate::validate_stream_config(&conf)?; + if sample_format != JACK_SAMPLE_FORMAT { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Sample format {sample_format} is not supported; required format is {JACK_SAMPLE_FORMAT}"), + )); + } + + // Keep `capture` monotonic: re-patching cpal's ports to a different hardware port + // can raise the capture port's latency, pulling `capture` backward. + let data_callback = crate::host::monotonic_input_callback(data_callback); + let name = self.name.clone(); + let start_server_automatically = self.start_server_automatically; + let connect_ports_automatically = self.connect_ports_automatically; + + let build = move || -> Result { + let client_options = super::get_client_options(start_server_automatically); + let client = super::get_client(&name, client_options)?; + if conf.sample_rate != client.sample_rate() { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Sample rate {} Hz does not match the server rate {} Hz", + conf.sample_rate, + client.sample_rate() + ), + )); + } + if let BufferSize::Fixed(size) = conf.buffer_size { + if size != client.buffer_size() { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Buffer size {size} does not match the server buffer size {}", + client.buffer_size() + ), + )); + } + } + let mut stream = + Stream::new_input(client, conf.channels, data_callback, error_callback)?; + if connect_ports_automatically { + stream.connect_to_system_inputs()?; + } + Ok(stream) + }; + + if let Some(dur) = timeout { + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + tx.send(build()).ok(); + }); + match rx.recv_timeout(dur) { + Ok(result) => result, + Err(_) => Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "timed out waiting for JACK server", + )), + } + } else { + build() + } + } + + fn build_output_stream_raw( + &self, + conf: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + if self.is_input() { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Device does not support output", + )); + } + crate::validate_stream_config(&conf)?; + if sample_format != JACK_SAMPLE_FORMAT { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("Sample format {sample_format} is not supported; required format is {JACK_SAMPLE_FORMAT}"), + )); + } + + // Keep `playback` monotonic: re-patching cpal's ports to a different hardware port + // can lower the playback port's latency, pulling `playback` backward. + let data_callback = crate::host::monotonic_output_callback(data_callback); + let name = self.name.clone(); + let start_server_automatically = self.start_server_automatically; + let connect_ports_automatically = self.connect_ports_automatically; + + let build = move || -> Result { + // Create a fresh client to validate against live server state. + let client_options = super::get_client_options(start_server_automatically); + let client = super::get_client(&name, client_options)?; + if conf.sample_rate != client.sample_rate() { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Sample rate {} Hz does not match the server rate {} Hz", + conf.sample_rate, + client.sample_rate() + ), + )); + } + if let BufferSize::Fixed(size) = conf.buffer_size { + if size != client.buffer_size() { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Buffer size {size} does not match the server buffer size {}", + client.buffer_size() + ), + )); + } + } + let mut stream = + Stream::new_output(client, conf.channels, data_callback, error_callback)?; + if connect_ports_automatically { + stream.connect_to_system_outputs()?; + } + Ok(stream) + }; + + if let Some(dur) = timeout { + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + tx.send(build()).ok(); + }); + match rx.recv_timeout(dur) { + Ok(result) => result, + Err(_) => Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "timed out waiting for JACK server", + )), + } + } else { + build() + } + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + // Device::id() can never fail in this implementation + self.id().unwrap() == other.id().unwrap() + } +} + +impl Eq for Device {} + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let desc = self.description().map_err(|_| fmt::Error)?; + f.write_str(desc.name()) + } +} + +impl Hash for Device { + fn hash(&self, state: &mut H) { + // Device::id() can never fail in this implementation + self.id().unwrap().hash(state); + } +} diff --git a/crates/vendor/cpal/src/host/jack/mod.rs b/crates/vendor/cpal/src/host/jack/mod.rs new file mode 100644 index 000000000..105e778b5 --- /dev/null +++ b/crates/vendor/cpal/src/host/jack/mod.rs @@ -0,0 +1,234 @@ +//! JACK backend implementation. +//! +//! Available on all platforms with the `jack` feature. Requires JACK server and client libraries. + +extern crate jack; + +use crate::{traits::HostTrait, Error, ErrorKind, SampleFormat}; + +mod device; +mod stream; + +#[allow(unused_imports)] // Re-exported for public API via platform module +pub use self::{ + device::{Device, SupportedInputConfigs, SupportedOutputConfigs}, + stream::Stream, +}; + +const JACK_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; + +pub type Devices = std::vec::IntoIter; + +/// The JACK host, providing access to JACK audio devices. +/// +/// # JACK-Specific Configuration +/// +/// JACK provides configuration options to control connection and server behavior: +/// - Port auto-connection via [`set_connect_automatically`](Host::set_connect_automatically) +/// - Server auto-start via [`set_start_server_automatically`](Host::set_start_server_automatically) +#[derive(Debug)] +pub struct Host { + /// The name that the client will have in JACK. + /// Until we have duplex streams two clients will be created adding "out" or "in" to the name + /// since names have to be unique. + name: String, + /// If ports are to be connected to the system (soundcard) ports automatically (default is true). + connect_ports_automatically: bool, + /// If the JACK server should be started automatically if it isn't already when creating a Client (default is false). + start_server_automatically: bool, + /// A list of the devices that have been created from this Host. + devices_created: Vec, +} + +impl Host { + pub fn new() -> Result { + let mut host = Host { + name: format!("cpal_client_{}", std::process::id()), + connect_ports_automatically: true, + start_server_automatically: false, + devices_created: vec![], + }; + // Devices don't exist for JACK, they have to be created + host.initialize_default_devices(); + Ok(host) + } + /// Configures whether created ports should automatically connect to system playback/capture + /// ports. + /// + /// When enabled (default), output streams connect to system playback ports and input streams + /// connect to system capture ports automatically. When disabled, users must manually connect + /// ports using JACK tools or APIs. + /// + /// Default: `true` + pub fn set_connect_automatically(&mut self, do_connect: bool) { + self.connect_ports_automatically = do_connect; + } + + /// Configures whether the JACK server should automatically start if not already running. + /// + /// When enabled, attempting to create a JACK client will start the JACK server if it's not + /// running. When disabled (default), client creation fails if the server is not running. + /// + /// Default: `false` + pub fn set_start_server_automatically(&mut self, do_start_server: bool) { + self.start_server_automatically = do_start_server; + } + + pub fn input_device_with_name(&mut self, name: &str) -> Option { + self.name = name.to_owned(); + self.default_input_device() + } + + pub fn output_device_with_name(&mut self, name: &str) -> Option { + self.name = name.to_owned(); + self.default_output_device() + } + + fn initialize_default_devices(&mut self) { + let in_device_res = Device::default_input_device( + &self.name, + self.connect_ports_automatically, + self.start_server_automatically, + ); + + if let Ok(device) = in_device_res { + self.devices_created.push(device); + } + + let out_device_res = Device::default_output_device( + &self.name, + self.connect_ports_automatically, + self.start_server_automatically, + ); + if let Ok(device) = out_device_res { + self.devices_created.push(device); + } + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + /// JACK is available if + /// - the jack feature flag is set + /// - libjack is installed (wouldn't compile without it) + /// - the JACK server can be started + /// + /// If the code compiles the necessary jack libraries are installed. + /// There is no way to know if the user has set up a correct JACK configuration e.g. with + /// qjackctl. + /// Users can choose to automatically start the server if it isn't already started when + /// creating a client so checking if the server is running could give a false negative in some + /// use cases. For these reasons this function should always return true. + fn is_available() -> bool { + true + } + + fn devices(&self) -> Result { + Ok(self.devices_created.clone().into_iter()) + } + + fn default_input_device(&self) -> Option { + for device in &self.devices_created { + if device.is_input() { + return Some(device.clone()); + } + } + None + } + + fn default_output_device(&self) -> Option { + for device in &self.devices_created { + if device.is_output() { + return Some(device.clone()); + } + } + None + } +} + +fn get_client_options(start_server_automatically: bool) -> jack::ClientOptions { + let mut client_options = jack::ClientOptions::empty(); + client_options.set( + jack::ClientOptions::NO_START_SERVER, + !start_server_automatically, + ); + client_options +} + +impl From for Error { + fn from(err: jack::Error) -> Self { + let msg = format!("{err}"); + match err { + jack::Error::ClientError(_) + | jack::Error::ClientActivationError + | jack::Error::ClientDeactivationError + | jack::Error::LibraryError(_) + | jack::Error::WeakFunctionNotFound(_) + | jack::Error::RingbufferCreateFailed => { + Error::with_message(ErrorKind::DeviceNotAvailable, msg) + } + + jack::Error::ClientIsNoLongerAlive | jack::Error::ClientPanicked => { + Error::with_message(ErrorKind::StreamInvalidated, msg) + } + + jack::Error::SetBufferSizeError | jack::Error::NotEnoughSpace => { + Error::with_message(ErrorKind::UnsupportedConfig, msg) + } + + jack::Error::InvalidDeactivation | jack::Error::FreewheelError => { + Error::with_message(ErrorKind::UnsupportedOperation, msg) + } + + jack::Error::PortNamingError | jack::Error::PortAliasError => { + Error::with_message(ErrorKind::InvalidInput, msg) + } + + _ => Error::with_message(ErrorKind::BackendError, msg), + } + } +} + +fn get_client(name: &str, client_options: jack::ClientOptions) -> Result { + let (client, status) = jack::Client::new(name, client_options)?; + if status.intersects(jack::ClientStatus::VERSION_ERROR) { + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Client protocol version does not match the JACK server", + )) + } else if status.intersects(jack::ClientStatus::INVALID_OPTION) { + Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "JACK client operation contained an invalid or unsupported option", + )) + } else if status.intersects(jack::ClientStatus::SERVER_ERROR) { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "Error communicating with the JACK server", + )) + } else if status.intersects(jack::ClientStatus::SERVER_FAILED) { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "Could not connect to the JACK server", + )) + } else if status.intersects(jack::ClientStatus::INIT_FAILURE) { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "Unable to initialize JACK client", + )) + } else if status.intersects(jack::ClientStatus::SHM_FAILURE) { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "Unable to access JACK shared memory", + )) + } else if status.intersects(jack::ClientStatus::NO_SUCH_CLIENT) { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "Requested JACK client does not exist", + )) + } else { + Ok(client) + } +} diff --git a/crates/vendor/cpal/src/host/jack/stream.rs b/crates/vendor/cpal/src/host/jack/stream.rs new file mode 100644 index 000000000..8d6cf679c --- /dev/null +++ b/crates/vendor/cpal/src/host/jack/stream.rs @@ -0,0 +1,586 @@ +use std::sync::{ + atomic::{AtomicU8, Ordering}, + Arc, Mutex, +}; + +use super::JACK_SAMPLE_FORMAT; +use crate::{ + host::{emit_error, frames_to_duration, try_emit_error, ErrorCallbackArc}, + traits::StreamTrait, + ChannelCount, Data, Error, ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, + OutputCallbackInfo, OutputStreamTimestamp, ResultExt, Sample, SampleRate, StreamInstant, +}; + +#[repr(u8)] +#[derive(Clone, Copy, PartialEq, Eq)] +enum StreamState { + Starting = 0, + Paused = 1, + Playing = 2, +} + +impl StreamState { + fn load(atom: &AtomicU8, order: Ordering) -> Self { + match atom.load(order) { + 1 => Self::Paused, + 2 => Self::Playing, + _ => Self::Starting, + } + } + + fn store(self, atom: &AtomicU8, order: Ordering) { + atom.store(self as u8, order); + } +} + +pub struct Stream { + state: Arc, + async_client: jack::AsyncClient, + // Port names are stored in order to connect them to other ports in jack automatically + input_port_names: Box<[String]>, + output_port_names: Box<[String]>, +} + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +impl Stream { + pub fn new_input( + client: jack::Client, + channels: ChannelCount, + data_callback: D, + error_callback: E, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + let mut ports = vec![]; + let mut port_names: Vec = vec![]; + for i in 0..channels { + let port = client + .register_port(&format!("in_{}", i), jack::AudioIn::default()) + .context(format!("Failed to register input port {i}"))?; + if let Ok(port_name) = port.name() { + port_names.push(port_name); + } + ports.push(port); + } + + let state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let error_callback_ptr: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); + + let input_process_handler = LocalProcessHandler::new( + vec![], + ports, + client.sample_rate(), + client.buffer_size() as usize, + Some(Box::new(data_callback)), + None, + state.clone(), + #[cfg(feature = "realtime")] + error_callback_ptr.clone(), + ); + + let notification_handler = JackNotificationHandler::new( + error_callback_ptr, + state.clone(), + client.sample_rate() as jack::Frames, + ); + + let async_client = client + .activate_async(notification_handler, input_process_handler) + .context("Failed to activate client")?; + + StreamState::Paused.store(&state, Ordering::Release); + Ok(Self { + state, + async_client, + input_port_names: port_names.into_boxed_slice(), + output_port_names: Default::default(), + }) + } + + pub fn new_output( + client: jack::Client, + channels: ChannelCount, + data_callback: D, + error_callback: E, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + let mut ports = vec![]; + let mut port_names: Vec = vec![]; + for i in 0..channels { + let port = client + .register_port(&format!("out_{}", i), jack::AudioOut::default()) + .context(format!("Failed to register output port {i}"))?; + if let Ok(port_name) = port.name() { + port_names.push(port_name); + } + ports.push(port); + } + + let state = Arc::new(AtomicU8::new(StreamState::Starting as u8)); + let error_callback_ptr: ErrorCallbackArc = Arc::new(Mutex::new(error_callback)); + + let output_process_handler = LocalProcessHandler::new( + ports, + vec![], + client.sample_rate(), + client.buffer_size() as usize, + None, + Some(Box::new(data_callback)), + state.clone(), + #[cfg(feature = "realtime")] + error_callback_ptr.clone(), + ); + + let notification_handler = JackNotificationHandler::new( + error_callback_ptr, + state.clone(), + client.sample_rate() as jack::Frames, + ); + + let async_client = client + .activate_async(notification_handler, output_process_handler) + .context("Failed to activate client")?; + + StreamState::Paused.store(&state, Ordering::Release); + Ok(Self { + state, + async_client, + input_port_names: Box::default(), + output_port_names: port_names.into_boxed_slice(), + }) + } + + /// Connects the stream's output ports to as many system playback ports as are available; + /// must be called after the client is activated. A stream may have more output channels + /// than physical ports (e.g. feeding a downstream JACK client); the surplus is simply left + /// unconnected for manual patching. + /// + /// Returns `Err` only if an individual port-connection call fails, rolling back any + /// connections already made so the JACK graph is left unchanged. + pub fn connect_to_system_outputs(&mut self) -> Result<(), Error> { + let client = self.async_client.as_client(); + let system_ports = client.ports(Some("system:playback_.*"), None, jack::PortFlags::empty()); + + // Connect outputs from this client to the system playback inputs. + for (i, (our_port, system_port)) in + self.output_port_names.iter().zip(&system_ports).enumerate() + { + if let Err(e) = client.connect_ports_by_name(our_port, system_port) { + for (prev_our, prev_sys) in + self.output_port_names[..i].iter().zip(&system_ports[..i]) + { + let _ = client.disconnect_ports_by_name(prev_our, prev_sys); + } + + return Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("JACK failed to connect port '{our_port}' to '{system_port}': {e}"), + )); + } + } + Ok(()) + } + + /// Connects the stream's input ports to as many system capture ports as are available; must + /// be called after the client is activated. A stream may have more input channels than + /// physical ports (e.g. sourced from an upstream JACK client); the surplus is simply left + /// unconnected for manual patching. + /// + /// Returns `Err` only if an individual port-connection call fails, rolling back any + /// connections already made so the JACK graph is left unchanged. + pub fn connect_to_system_inputs(&mut self) -> Result<(), Error> { + let client = self.async_client.as_client(); + let system_ports = client.ports(Some("system:capture_.*"), None, jack::PortFlags::empty()); + + // Connect inputs from system capture ports to this client. + for (i, (system_port, our_port)) in + system_ports.iter().zip(&self.input_port_names).enumerate() + { + if let Err(e) = client.connect_ports_by_name(system_port, our_port) { + for (prev_sys, prev_our) in + system_ports[..i].iter().zip(&self.input_port_names[..i]) + { + let _ = client.disconnect_ports_by_name(prev_sys, prev_our); + } + + return Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("JACK failed to connect port '{system_port}' to '{our_port}': {e}"), + )); + } + } + Ok(()) + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), Error> { + StreamState::Playing.store(&self.state, Ordering::Relaxed); + Ok(()) + } + + fn pause(&self) -> Result<(), Error> { + StreamState::Paused.store(&self.state, Ordering::Relaxed); + Ok(()) + } + + fn now(&self) -> StreamInstant { + micros_to_stream_instant(self.async_client.as_client().time()) + } + + fn buffer_size(&self) -> Result { + Ok(self.async_client.as_client().buffer_size() as FrameCount) + } +} + +type InputDataCallback = Box; +type OutputDataCallback = Box; + +struct LocalProcessHandler { + /// No new ports are allowed to be created after the creation of the LocalProcessHandler as that would invalidate the buffer sizes + out_ports: Vec>, + in_ports: Vec>, + + sample_rate: SampleRate, + buffer_size: usize, + input_data_callback: Option, + output_data_callback: Option, + + // JACK audio samples are 32-bit float (unless you do some custom dark magic) + temp_input_buffer: Vec, + temp_output_buffer: Vec, + state: Arc, + #[cfg(feature = "realtime")] + error_callback: ErrorCallbackArc, + #[cfg(feature = "realtime")] + rt_checked: bool, +} + +impl LocalProcessHandler { + #[allow(clippy::too_many_arguments)] + fn new( + out_ports: Vec>, + in_ports: Vec>, + sample_rate: SampleRate, + buffer_size: usize, + input_data_callback: Option, + output_data_callback: Option, + state: Arc, + #[cfg(feature = "realtime")] error_callback: ErrorCallbackArc, + ) -> Self { + let temp_input_buffer = vec![f32::EQUILIBRIUM; in_ports.len() * buffer_size]; + let temp_output_buffer = vec![f32::EQUILIBRIUM; out_ports.len() * buffer_size]; + + Self { + out_ports, + in_ports, + sample_rate, + buffer_size, + input_data_callback, + output_data_callback, + temp_input_buffer, + temp_output_buffer, + state, + #[cfg(feature = "realtime")] + error_callback, + #[cfg(feature = "realtime")] + rt_checked: false, + } + } +} + +fn temp_buffer_to_data(temp_input_buffer: &mut [f32], total_buffer_size: usize) -> Data { + let slice = &mut temp_input_buffer[0..total_buffer_size]; + let data: *mut () = slice.as_mut_ptr().cast(); + let len = total_buffer_size; + unsafe { Data::from_parts(data, len, JACK_SAMPLE_FORMAT) } +} + +impl jack::ProcessHandler for LocalProcessHandler { + fn process( + &mut self, + client: &jack::Client, + process_scope: &jack::ProcessScope, + ) -> jack::Control { + if StreamState::load(&self.state, Ordering::Relaxed) != StreamState::Playing { + // JACK does not zero-fill output port buffers before calling the process handler + for port in &mut self.out_ports { + port.as_mut_slice(process_scope).fill(f32::EQUILIBRIUM); + } + return jack::Control::Continue; + } + + #[cfg(feature = "realtime")] + { + if !self.rt_checked { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + ))] + let denied = { + let sched = unsafe { libc::sched_getscheduler(0) }; + sched != libc::SCHED_FIFO && sched != libc::SCHED_RR + }; + + #[cfg(target_vendor = "apple")] + let denied = { + use mach2::{ + boolean::boolean_t, + kern_return::KERN_SUCCESS, + mach_init::mach_thread_self, + mach_port::mach_port_deallocate, + thread_policy::{ + thread_policy_get, thread_policy_t, + thread_time_constraint_policy_data_t, THREAD_TIME_CONSTRAINT_POLICY, + THREAD_TIME_CONSTRAINT_POLICY_COUNT, + }, + traps::mach_task_self, + }; + let mut policy: thread_time_constraint_policy_data_t = + unsafe { std::mem::zeroed() }; + let mut count = THREAD_TIME_CONSTRAINT_POLICY_COUNT; + let mut get_default: boolean_t = 0; + // SAFETY: mach_thread_self() returns a send right that we must release. + let thread_port = unsafe { mach_thread_self() }; + let kr = unsafe { + thread_policy_get( + thread_port, + THREAD_TIME_CONSTRAINT_POLICY, + &mut policy as *mut _ as thread_policy_t, + &mut count, + &mut get_default, + ) + }; + unsafe { mach_port_deallocate(mach_task_self(), thread_port) }; + kr != KERN_SUCCESS || get_default != 0 || policy.period == 0 + }; + + #[cfg(target_os = "windows")] + let denied = { + use windows::Win32::System::Threading; + let priority = + unsafe { Threading::GetThreadPriority(Threading::GetCurrentThread()) }; + priority < Threading::THREAD_PRIORITY_ABOVE_NORMAL.0 + }; + + if denied { + if try_emit_error(&self.error_callback, Error::new(ErrorKind::RealtimeDenied)) + .is_ok() + { + self.rt_checked = true; + } + } else { + self.rt_checked = true; + } + } + } + + // This should be equal to self.buffer_size, but the implementation will + // work even if it is less. Will panic in `temp_buffer_to_data` if greater. + let current_frame_count = process_scope.n_frames() as usize; + + // Get timestamp data + let (current_start_usecs, next_usecs_opt) = match process_scope.cycle_times() { + Ok(times) => (times.current_usecs, Some(times.next_usecs)), + Err(_) => { + // JACK was unable to get the current time information. + // Fall back to jack_get_time(), which is the same clock source + // used by now() and cycle_times(), so the epoch stays consistent. + (client.time(), None) + } + }; + let start_cycle_instant = micros_to_stream_instant(current_start_usecs); + let start_callback_instant = start_cycle_instant + + frames_to_duration( + process_scope.frames_since_cycle_start() as FrameCount, + self.sample_rate, + ); + + if let Some(input_callback) = &mut self.input_data_callback { + // Let's get the data from the input ports and run the callback + + let num_in_channels = self.in_ports.len(); + + // Read the data from the input ports into the temporary buffer + // Go through every channel and store its data in the temporary input buffer + for ch_ix in 0..num_in_channels { + let input_channel = &self.in_ports[ch_ix].as_slice(process_scope); + for i in 0..current_frame_count { + self.temp_input_buffer[ch_ix + i * num_in_channels] = input_channel[i]; + } + } + // Create a slice of exactly current_frame_count frames + let data = temp_buffer_to_data( + &mut self.temp_input_buffer, + current_frame_count * num_in_channels, + ); + // Create timestamp + let callback = start_callback_instant; + // `capture` is when the first frame in this buffer was sampled at the ADC. JACK's + // capture latency is the hardware-to-port distance, measured from the cycle start. + let latency = hardware_latency_frames(&self.in_ports, jack::LatencyType::Capture) + .map(|frames| frames_to_duration(frames, self.sample_rate)) + .unwrap_or_default(); + let capture = start_cycle_instant + .checked_sub(latency) + .unwrap_or(StreamInstant::ZERO); + let timestamp = InputStreamTimestamp { callback, capture }; + let info = InputCallbackInfo { timestamp }; + input_callback(&data, &info); + } + + if let Some(output_callback) = &mut self.output_data_callback { + let num_out_channels = self.out_ports.len(); + + let total = current_frame_count * num_out_channels; + self.temp_output_buffer[..total].fill(f32::EQUILIBRIUM); + + // Create a slice of exactly current_frame_count frames + let mut data = temp_buffer_to_data(&mut self.temp_output_buffer, total); + // Create timestamp + let callback = start_callback_instant; + // `playback` is when the first frame written here reaches the DAC. + let playback = + match hardware_latency_frames(&self.out_ports, jack::LatencyType::Playback) { + // Prefer JACK's port-to-hardware latency, measured from the cycle start. + Some(frames) => { + start_cycle_instant + frames_to_duration(frames, self.sample_rate) + } + // When no latency is reported, fall back to next_usecs, the hardware + // deadline for this cycle. + None => match next_usecs_opt { + Some(next_usecs) => micros_to_stream_instant(next_usecs), + // Fallback to one buffer ahead if that is unavailable too. + None => { + start_cycle_instant + + frames_to_duration( + current_frame_count as FrameCount, + self.sample_rate, + ) + } + }, + }; + let timestamp = OutputStreamTimestamp { callback, playback }; + let info = OutputCallbackInfo { timestamp }; + output_callback(&mut data, &info); + + // Deinterlace + for ch_ix in 0..num_out_channels { + let output_channel = &mut self.out_ports[ch_ix].as_mut_slice(process_scope); + for i in 0..current_frame_count { + output_channel[i] = self.temp_output_buffer[ch_ix + i * num_out_channels]; + } + } + } + + // Continue as normal + jack::Control::Continue + } + + fn buffer_size(&mut self, _: &jack::Client, size: jack::Frames) -> jack::Control { + // The `buffer_size` callback is actually called on the process thread, but + // it does not need to be suitable for real-time use. Thus we can simply allocate + // new buffers here. Details: https://github.com/RustAudio/rust-jack/issues/137 + let new_size = size as usize; + if new_size != self.buffer_size { + self.buffer_size = new_size; + self.temp_input_buffer = vec![f32::EQUILIBRIUM; self.in_ports.len() * new_size]; + self.temp_output_buffer = vec![f32::EQUILIBRIUM; self.out_ports.len() * new_size]; + } + + jack::Control::Continue + } +} + +#[inline] +fn micros_to_stream_instant(micros: u64) -> StreamInstant { + StreamInstant::from_micros(micros) +} + +/// Maximum latency, in frames, between `ports` and the hardware for the given direction, +/// or `None` if JACK reports zero. +#[inline] +fn hardware_latency_frames( + ports: &[jack::Port], + mode: jack::LatencyType, +) -> Option { + let frames = ports + .iter() + .map(|port| { + // This reads a cached value and is documented as safe to call from the process callback. + port.get_latency_range(mode).1 + }) + .max() // conservative: use worst-case latency across all ports + .unwrap_or(0); + (frames > 0).then_some(frames as FrameCount) +} + +/// Receives notifications from the JACK server on JACK's notification thread (single-threaded). +struct JackNotificationHandler { + error_callback_ptr: ErrorCallbackArc, + state: Arc, + configured_sample_rate: jack::Frames, +} + +impl JackNotificationHandler { + pub fn new( + error_callback_ptr: ErrorCallbackArc, + state: Arc, + configured_sample_rate: jack::Frames, + ) -> Self { + JackNotificationHandler { + error_callback_ptr, + state, + configured_sample_rate, + } + } +} + +impl jack::NotificationHandler for JackNotificationHandler { + unsafe fn shutdown(&mut self, _status: jack::ClientStatus, reason: &str) { + if StreamState::load(&self.state, Ordering::Acquire) == StreamState::Starting { + return; + } + emit_error( + &self.error_callback_ptr, + Error::with_message( + ErrorKind::DeviceNotAvailable, + format!("JACK server shut down: {reason}"), + ), + ); + } + + fn sample_rate(&mut self, _: &jack::Client, srate: jack::Frames) -> jack::Control { + if srate == self.configured_sample_rate { + // One of these notifications is sent every time a client is started. + return jack::Control::Continue; + } + if StreamState::load(&self.state, Ordering::Acquire) != StreamState::Starting { + emit_error( + &self.error_callback_ptr, + Error::with_message( + ErrorKind::StreamInvalidated, + format!("JACK server changed sample rate to {srate} Hz"), + ), + ); + } + jack::Control::Quit + } + + fn xrun(&mut self, _: &jack::Client) -> jack::Control { + if StreamState::load(&self.state, Ordering::Acquire) != StreamState::Starting { + let _ = try_emit_error(&self.error_callback_ptr, ErrorKind::Xrun.into()); + } + jack::Control::Continue + } +} diff --git a/crates/vendor/cpal/src/host/latch.rs b/crates/vendor/cpal/src/host/latch.rs new file mode 100644 index 000000000..6d979640d --- /dev/null +++ b/crates/vendor/cpal/src/host/latch.rs @@ -0,0 +1,110 @@ +//! Stream-readiness latch used by backends with dedicated worker threads. +//! +//! Prevents worker threads from invoking user callbacks before the `Stream` handle has been +//! returned to the caller. + +use std::{ + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Weak, + }, + thread::Thread, +}; + +/// Signals worker threads that the stream handle has been given to the caller. +#[derive(Debug)] +pub(crate) struct Latch { + /// `Option` so `Drop` can move it out before unparking, closing the window where a thread + /// could wake, see the Arc still alive (flag=false), re-park, then be orphaned. + flag: Option>, + threads: Vec, +} + +/// Held by a worker thread. Parks until the matching [`Latch`] is released. +#[derive(Clone, Debug)] +pub(crate) struct LatchWaiter(Weak); + +impl Latch { + /// Creates a new stream-readiness latch. + pub(crate) fn new() -> Self { + Self { + flag: Some(Arc::new(AtomicBool::new(false))), + threads: Vec::new(), + } + } + + /// Returns a waiter that unblocks when this latch is released. + pub(crate) fn waiter(&self) -> LatchWaiter { + LatchWaiter(Arc::downgrade( + self.flag + .as_ref() + .expect("waiter called on a dropped Latch"), + )) + } + + /// Registers a thread to be unparked when [`release`](Self::release) is called. + #[cfg(any( + target_os = "macos", + target_os = "windows", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + ))] + pub(crate) fn add_thread(&mut self, thread: Thread) { + self.threads.push(thread); + } + + /// Releases the latch and unparks all registered threads. + pub(crate) fn release(&self) { + if let Some(flag) = &self.flag { + flag.store(true, Ordering::Release); + } + for t in &self.threads { + t.unpark(); + } + } +} + +impl Drop for Latch { + fn drop(&mut self) { + // Invalidate the Arc *before* unparking so waiters see upgrade() == None and exit cleanly + // on the error path (latch dropped without being released). + drop(self.flag.take()); + for t in &self.threads { + t.unpark(); + } + } +} + +impl LatchWaiter { + /// Parks the calling thread until the latch is released or dropped without releasing. + /// + /// Returns `true` if the stream is ready, `false` if the [`Latch`] was dropped before release. + #[cfg(any( + target_os = "macos", + target_os = "windows", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + ))] + pub(crate) fn wait(&self) -> bool { + loop { + match self.0.upgrade() { + None => return false, + Some(flag) if flag.load(Ordering::Acquire) => return true, + Some(flag) => { + drop(flag); // release strong ref before parking + std::thread::park(); + } + } + } + } + + /// Returns `true` if the latch has already been released. + #[cfg(all(target_vendor = "apple", not(target_os = "macos")))] + pub(crate) fn is_released(&self) -> bool { + self.0.upgrade().is_some_and(|f| f.load(Ordering::Acquire)) + } +} diff --git a/crates/vendor/cpal/src/host/mod.rs b/crates/vendor/cpal/src/host/mod.rs new file mode 100644 index 000000000..effd77eba --- /dev/null +++ b/crates/vendor/cpal/src/host/mod.rs @@ -0,0 +1,298 @@ +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "windows", + target_vendor = "apple", + target_os = "android", +))] +pub(crate) mod equilibrium; + +#[cfg(windows)] +pub(crate) mod com; + +#[cfg(target_os = "android")] +pub(crate) mod aaudio; + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" +))] +pub(crate) mod alsa; + +#[cfg(all(windows, feature = "asio"))] +pub(crate) mod asio; + +#[cfg(all( + target_arch = "wasm32", + target_os = "unknown", + feature = "wasm-bindgen", + feature = "audioworklet", + target_feature = "atomics" +))] +pub(crate) mod audioworklet; + +#[cfg(target_vendor = "apple")] +pub(crate) mod coreaudio; + +#[cfg(all( + feature = "jack", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows", + ) +))] +pub(crate) mod jack; + +#[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + ), + feature = "pipewire" +))] +pub(crate) mod pipewire; + +#[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "pulseaudio" +))] +pub(crate) mod pulseaudio; + +#[cfg(windows)] +pub(crate) mod wasapi; + +#[cfg(all( + target_arch = "wasm32", + target_os = "unknown", + feature = "wasm-bindgen" +))] +pub(crate) mod webaudio; + +#[cfg(feature = "custom")] +pub(crate) mod custom; + +#[cfg(not(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_vendor = "apple", + target_os = "android", + all( + target_arch = "wasm32", + target_os = "unknown", + feature = "wasm-bindgen" + ), +)))] +pub(crate) mod null; + +#[cfg(any( + target_vendor = "apple", + target_os = "windows", + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + all( + feature = "jack", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows", + ) + ), +))] +pub(crate) mod latch; + +/// Shared error-callback type that hands the callback across thread boundaries. +#[allow(dead_code)] +pub(crate) type ErrorCallbackArc = std::sync::Arc>; + +/// Error-delivery helpers shared by backends that hold an `ErrorCallbackArc`. +#[cfg(any( + target_os = "android", + target_vendor = "apple", + target_os = "windows", + all( + feature = "jack", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows", + ) + ), + all( + feature = "pipewire", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + ) + ), + all( + feature = "pulseaudio", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + ) + ), +))] +pub(crate) mod error_emit; + +#[cfg(any( + target_os = "android", + target_vendor = "apple", + target_os = "windows", + all( + feature = "jack", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows", + ) + ), + all( + feature = "pipewire", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + ) + ), + all( + feature = "pulseaudio", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + ) + ), +))] +pub(crate) use error_emit::emit_error; + +// Unlike `emit_error`, PulseAudio has no RT callback path and never calls this. +#[cfg(any( + target_os = "android", + target_vendor = "apple", + target_os = "windows", + all( + feature = "jack", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "windows", + ) + ), + all( + feature = "pipewire", + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + ) + ), +))] +pub(crate) use error_emit::try_emit_error; + +/// Convert a frame count at a given sample rate to a [`std::time::Duration`]. +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "windows", + target_vendor = "apple", + feature = "audioworklet", +))] +#[inline] +pub(crate) fn frames_to_duration( + frames: crate::FrameCount, + rate: crate::SampleRate, +) -> std::time::Duration { + if rate == 0 { + return std::time::Duration::ZERO; + } + let rate = rate as u64; + let secs = frames as u64 / rate; + // rem_frames < rate <= u32::MAX, so rem_frames * 1_000_000_000 < u64::MAX + let rem_frames = frames as u64 % rate; + // Round to nearest so the duration isn't biased. + let nanos = (rem_frames * 1_000_000_000 + rate / 2) / rate; + std::time::Duration::new(secs, nanos as u32) +} + +/// Clamps a timestamp so it never precedes one we've already returned. +#[allow(dead_code)] +fn non_decreasing(floor: &mut u64, instant: crate::StreamInstant) -> crate::StreamInstant { + // u64 nanos covers ~585 years of runtime. + let nanos = instant.as_nanos().min(u64::MAX as u128) as u64; + *floor = (*floor).max(nanos); + crate::StreamInstant::from_nanos(*floor) +} + +/// Wraps an input data callback so the `capture` timestamp never regresses across callbacks. +#[allow(dead_code)] +pub(crate) fn monotonic_input_callback( + mut data_callback: D, +) -> impl FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static +where + D: FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static, +{ + // FnMut runs on one thread at a time, so the floor needs no synchronization. + let mut floor = 0u64; + move |data, info| { + let mut info = *info; + info.timestamp.capture = non_decreasing(&mut floor, info.timestamp.capture); + data_callback(data, &info); + } +} + +/// Wraps an output data callback so the `playback` timestamp never regresses across callbacks. +#[allow(dead_code)] +pub(crate) fn monotonic_output_callback( + mut data_callback: D, +) -> impl FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static +where + D: FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static, +{ + let mut floor = 0u64; + move |data, info| { + let mut info = *info; + info.timestamp.playback = non_decreasing(&mut floor, info.timestamp.playback); + data_callback(data, &info); + } +} diff --git a/crates/vendor/cpal/src/host/null/mod.rs b/crates/vendor/cpal/src/host/null/mod.rs new file mode 100644 index 000000000..e675339ec --- /dev/null +++ b/crates/vendor/cpal/src/host/null/mod.rs @@ -0,0 +1,170 @@ +//! Null backend implementation. +//! +//! Fallback no-op backend for unsupported platforms. + +use std::fmt; +use std::time::Duration; + +use crate::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + Data, DeviceDescription, DeviceDescriptionBuilder, DeviceId, Error, FrameCount, + InputCallbackInfo, OutputCallbackInfo, SampleFormat, StreamConfig, StreamInstant, + SupportedStreamConfig, SupportedStreamConfigRange, +}; + +pub struct Devices; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Device; + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let desc = self.description().map_err(|_| fmt::Error)?; + f.write_str(desc.name()) + } +} + +pub struct Host; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Stream; + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +#[derive(Clone)] +pub struct SupportedInputConfigs; +#[derive(Clone)] +pub struct SupportedOutputConfigs; + +impl Host { + #[allow(dead_code)] + pub fn new() -> Result { + Ok(Self) + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Null Device").build()) + } + + fn id(&self) -> Result { + Ok(DeviceId::new(crate::platform::HostId::Null, "")) + } + + fn supported_input_configs(&self) -> Result { + unimplemented!() + } + + fn supported_output_configs(&self) -> Result { + unimplemented!() + } + + fn default_input_config(&self) -> Result { + unimplemented!() + } + + fn default_output_config(&self) -> Result { + unimplemented!() + } + + fn build_input_stream_raw( + &self, + _config: StreamConfig, + _sample_format: SampleFormat, + _data_callback: D, + _error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + unimplemented!() + } + + /// Create an output stream. + fn build_output_stream_raw( + &self, + _config: StreamConfig, + _sample_format: SampleFormat, + _data_callback: D, + _error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + unimplemented!() + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + false + } + + fn devices(&self) -> Result { + Ok(Devices) + } + + fn default_input_device(&self) -> Option { + None + } + + fn default_output_device(&self) -> Option { + None + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), Error> { + unimplemented!() + } + + fn pause(&self) -> Result<(), Error> { + unimplemented!() + } + + fn now(&self) -> StreamInstant { + unimplemented!() + } + + fn buffer_size(&self) -> Result { + unimplemented!() + } +} + +impl Iterator for Devices { + type Item = Device; + + fn next(&mut self) -> Option { + None + } +} + +impl Iterator for SupportedInputConfigs { + type Item = SupportedStreamConfigRange; + + fn next(&mut self) -> Option { + None + } +} + +impl Iterator for SupportedOutputConfigs { + type Item = SupportedStreamConfigRange; + + fn next(&mut self) -> Option { + None + } +} diff --git a/crates/vendor/cpal/src/host/pipewire/device.rs b/crates/vendor/cpal/src/host/pipewire/device.rs new file mode 100644 index 000000000..5d30177ba --- /dev/null +++ b/crates/vendor/cpal/src/host/pipewire/device.rs @@ -0,0 +1,1155 @@ +use std::{ + cell::RefCell, + fmt, + hash::{Hash, Hasher}, + rc::Rc, + sync::{ + atomic::{AtomicBool, AtomicU32, Ordering}, + mpsc, Arc, + }, + thread, + time::Duration, +}; + +use pipewire::{ + self as pw, + context::ContextRc, + core::PW_ID_CORE, + main_loop::MainLoopRc, + metadata::{Metadata, MetadataListener}, + node::{Node, NodeListener}, + properties::PropertiesBox, + proxy::ProxyT, + spa::utils::result::AsyncSeq, + types::ObjectType, +}; + +use super::stream::Stream; +use crate::{ + host::{ + emit_error, + latch::Latch, + pipewire::{ + stream::{ + DefaultDeviceMonitor, PwInitGuard, StreamCommand, StreamData, SUPPORTED_FORMATS, + }, + utils::{audio, clock, default, node, DEVICE_ICON_NAME, METADATA_NAME}, + }, + }, + iter::{SupportedInputConfigs, SupportedOutputConfigs}, + traits::DeviceTrait, + BufferSize, ChannelCount, Data, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, + DeviceId, DeviceType, Error, ErrorKind, FrameCount, HostId, InputCallbackInfo, InterfaceType, + OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, +}; + +pub type Devices = std::vec::IntoIter; + +const INIT_TIMEOUT: Duration = Duration::from_secs(2); + +// This enum record whether it is created by human or just default device +#[derive(Clone, Debug, Default, Copy)] +pub(crate) enum Class { + #[default] + Node, + DefaultSink, + DefaultInput, + DefaultOutput, +} + +#[derive(Clone, Debug, Default, Copy)] +enum Role { + #[default] + Sink, + Source, + Duplex, + StreamOutput, + StreamInput, +} + +#[derive(Clone, Debug, Default)] +pub struct Device { + node_name: String, + description: String, + direction: DeviceDirection, + channels: ChannelCount, + rate: SampleRate, + allow_rates: Arc<[SampleRate]>, + quantum: FrameCount, + min_quantum: FrameCount, + max_quantum: FrameCount, + class: Class, + role: Role, + icon_name: String, + object_serial: u32, + interface_type: InterfaceType, + address: Option, + driver: Option, + connect_automatically: Arc, +} + +impl Device { + pub(crate) fn class(&self) -> Class { + self.class + } + + fn sink_default() -> Self { + Self { + node_name: "sink_default".to_owned(), + description: "default_sink".to_owned(), + direction: DeviceDirection::Duplex, + channels: 2, + class: Class::DefaultSink, + role: Role::Sink, + ..Default::default() + } + } + fn input_default() -> Self { + Self { + node_name: "input_default".to_owned(), + description: "default_input".to_owned(), + direction: DeviceDirection::Input, + channels: 2, + class: Class::DefaultInput, + role: Role::Source, + ..Default::default() + } + } + fn output_default() -> Self { + Self { + node_name: "output_default".to_owned(), + description: "default_output".to_owned(), + direction: DeviceDirection::Output, + channels: 2, + class: Class::DefaultOutput, + role: Role::Sink, + ..Default::default() + } + } + + fn device_type(&self) -> DeviceType { + match self.icon_name.as_str() { + "audio-headphones" => DeviceType::Headphones, + "audio-headset" => DeviceType::Headset, + "audio-input-microphone" => DeviceType::Microphone, + "audio-speakers" => DeviceType::Speaker, + _ => DeviceType::Unknown, + } + } + + /// Returns the WirePlumber metadata key to watch for default-device changes, + /// or `None` if this device is pinned to a specific node. + pub(crate) fn default_metadata_key(&self) -> Option<&'static str> { + match self.class { + Class::DefaultOutput | Class::DefaultSink => Some(default::SINK), + Class::DefaultInput => Some(default::SOURCE), + Class::Node => None, + } + } + + pub(crate) fn pw_properties( + &self, + direction: DeviceDirection, + config: &StreamConfig, + ) -> PropertiesBox { + let mut properties = match direction { + DeviceDirection::Output => pw::properties::properties! { + *pw::keys::MEDIA_TYPE => "Audio", + *pw::keys::MEDIA_CATEGORY => "Playback", + }, + DeviceDirection::Input => pw::properties::properties! { + *pw::keys::MEDIA_TYPE => "Audio", + *pw::keys::MEDIA_CATEGORY => "Capture", + }, + _ => unreachable!(), + }; + if matches!(self.role, Role::Sink) && matches!(direction, DeviceDirection::Input) { + properties.insert(*pw::keys::STREAM_CAPTURE_SINK, "true"); + } + if matches!(self.class, Class::Node) { + properties.insert(*pw::keys::TARGET_OBJECT, self.object_serial.to_string()); + } + + // Group input and output nodes so PipeWire schedules them in the same quantum, + // preventing phase drift between simultaneous input/output streams. + properties.insert("node.group", format!("cpal-{}", std::process::id())); + + if let BufferSize::Fixed(buffer_size) = config.buffer_size { + properties.insert( + *pw::keys::NODE_LATENCY, + format!("{buffer_size}/{rate}", rate = config.sample_rate), + ); + } + properties + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + self.node_name == other.node_name + } +} + +impl Eq for Device {} + +impl fmt::Display for Device { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let desc = self.description().map_err(|_| fmt::Error)?; + f.write_str(desc.name()) + } +} + +impl Hash for Device { + fn hash(&self, state: &mut H) { + self.node_name.hash(state); + } +} + +impl DeviceTrait for Device { + type Stream = Stream; + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + + fn id(&self) -> Result { + Ok(DeviceId::new(HostId::PipeWire, &self.node_name)) + } + + fn description(&self) -> Result { + let mut builder = DeviceDescriptionBuilder::new(&self.description) + .direction(self.direction) + .device_type(self.device_type()) + .interface_type(self.interface_type); + if let Some(address) = self.address.as_ref() { + builder = builder.address(address); + } + if let Some(driver) = self.driver.as_ref() { + builder = builder.driver(driver); + } + Ok(builder.build()) + } + + fn supports_input(&self) -> bool { + matches!( + self.direction, + DeviceDirection::Input | DeviceDirection::Duplex + ) + } + + fn supports_output(&self) -> bool { + matches!( + self.direction, + DeviceDirection::Output | DeviceDirection::Duplex + ) + } + + fn supported_input_configs(&self) -> Result { + if !self.supports_input() { + return Ok(vec![].into_iter()); + } + let rates: &[SampleRate] = if self.allow_rates.is_empty() { + &[self.rate] + } else { + &self.allow_rates + }; + Ok(rates + .iter() + .flat_map(|&rate| { + SUPPORTED_FORMATS + .iter() + .map(move |sample_format| SupportedStreamConfigRange { + channels: self.channels, + min_sample_rate: rate, + max_sample_rate: rate, + buffer_size: SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + sample_format: *sample_format, + }) + }) + .collect::>() + .into_iter()) + } + fn supported_output_configs(&self) -> Result { + if !self.supports_output() { + return Ok(vec![].into_iter()); + } + let rates: &[SampleRate] = if self.allow_rates.is_empty() { + &[self.rate] + } else { + &self.allow_rates + }; + Ok(rates + .iter() + .flat_map(|&rate| { + SUPPORTED_FORMATS + .iter() + .map(move |sample_format| SupportedStreamConfigRange { + channels: self.channels, + min_sample_rate: rate, + max_sample_rate: rate, + buffer_size: SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + sample_format: *sample_format, + }) + }) + .collect::>() + .into_iter()) + } + fn default_input_config(&self) -> Result { + if !self.supports_input() { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Device does not support input", + )); + } + Ok(SupportedStreamConfig { + channels: self.channels, + sample_format: SampleFormat::F32, + sample_rate: self.rate, + buffer_size: SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + }) + } + + fn default_output_config(&self) -> Result { + if !self.supports_output() { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + "Device does not support output", + )); + } + Ok(SupportedStreamConfig { + channels: self.channels, + sample_format: SampleFormat::F32, + sample_rate: self.rate, + buffer_size: SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + }) + } + + fn build_input_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + crate::validate_stream_config(&config)?; + if let BufferSize::Fixed(n) = config.buffer_size { + // When max_quantum is 0 the server clock metadata has not been received yet. + if self.max_quantum > 0 && !(self.min_quantum..=self.max_quantum).contains(&n) { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Buffer size {n} is not in the supported quantum range {min}..={max}", + min = self.min_quantum, + max = self.max_quantum + ), + )); + } + } + let (pw_play_tx, pw_play_rx) = pw::channel::channel::(); + + let (init_tx, init_rx) = mpsc::channel::>(); + let mut latch = Latch::new(); + let waiter = latch.waiter(); + let device = self.clone(); + let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); + let initial_quantum = match config.buffer_size { + BufferSize::Fixed(n) => n, + BufferSize::Default => self.quantum, + }; + let last_quantum = Arc::new(AtomicU32::new(initial_quantum)); + let last_quantum_clone = last_quantum.clone(); + // Keep `capture` monotonic: pw_time delay() grows when another client joins + // needing a larger buffer, which can pull `capture` backward. + let data_callback = crate::host::monotonic_input_callback(data_callback); + let start = std::time::Instant::now(); + let handle = thread::Builder::new() + .name("pw_in".to_owned()) + .spawn(move || { + let _pw = PwInitGuard::new(); + let properties = device.pw_properties(DeviceDirection::Input, &config); + + let stream_data = match super::stream::connect_input( + super::stream::ConnectParams { + config, + properties, + sample_format, + last_quantum: last_quantum_clone, + start, + connect_automatically: device.connect_automatically.load(Ordering::Relaxed), + is_default_device: matches!( + device.class(), + Class::DefaultSink | Class::DefaultInput | Class::DefaultOutput + ), + }, + data_callback, + error_callback, + ) { + Ok(d) => d, + Err(e) => { + let _ = init_tx.send(Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("PipeWire stream connection failed: {e}"), + ))); + return; + } + }; + + let StreamData { + mainloop, + listener, + stream, + context, + core, + core_monitor, + error_callback, + pending_device_changed, + invalidated, + } = stream_data; + + let default_monitor = if let Some(key) = device.default_metadata_key() { + match core.get_registry_rc() { + Ok(registry) => Some(DefaultDeviceMonitor::new( + registry, + key, + error_callback.clone(), + invalidated, + pending_device_changed, + )), + Err(e) => { + let _ = init_tx.send(Err(Error::with_message( + ErrorKind::BackendError, + format!("PipeWire: could not acquire registry: {e}"), + ))); + return; + } + } + } else { + None + }; + let stream_clone = stream.clone(); + let mainloop_rc1 = mainloop.clone(); + let error_callback_cmd = error_callback.clone(); + let _receiver = pw_play_rx.attach(mainloop.loop_(), move |play| match play { + StreamCommand::Toggle(state) => { + if let Err(e) = stream_clone.set_active(state) { + emit_error( + &error_callback_cmd, + Error::with_message( + ErrorKind::StreamInvalidated, + format!("PipeWire: set_active({state}) failed: {e}"), + ), + ); + } + } + StreamCommand::Stop => { + if let Err(e) = stream_clone.disconnect() { + emit_error( + &error_callback_cmd, + Error::with_message( + ErrorKind::StreamInvalidated, + format!("PipeWire: stream disconnect failed: {e}"), + ), + ); + } + mainloop_rc1.quit(); + } + }); + + if init_tx.send(Ok(())).is_err() { + return; + } + + // If the Latch is dropped without being released (error path), exit cleanly. + if !waiter.wait() { + return; + } + + mainloop.run(); + + drop(listener); + drop(default_monitor); + drop(core_monitor); + drop(core); + drop(context); + }) + .map_err(|e| { + Error::with_message( + ErrorKind::ResourceExhausted, + format!("Failed to create thread: {e}"), + ) + })?; + + let init_result = init_rx.recv_timeout(wait_timeout).unwrap_or_else(|_| { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "PipeWire timed out", + )) + }); + + if let Err(e) = init_result { + drop(latch); + return Err(e); + } + + latch.add_thread(handle.thread().clone()); + let stream = Stream::new(handle, pw_play_tx, last_quantum, start, latch); + stream.signal_ready(); + Ok(stream) + } + + fn build_output_stream_raw( + &self, + config: StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(Error) + Send + 'static, + { + crate::validate_stream_config(&config)?; + if let BufferSize::Fixed(n) = config.buffer_size { + // When max_quantum is 0 the server clock metadata has not been received yet. + if self.max_quantum > 0 && !(self.min_quantum..=self.max_quantum).contains(&n) { + return Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!( + "Buffer size {n} is not in the supported quantum range {min}..={max}", + min = self.min_quantum, + max = self.max_quantum + ), + )); + } + } + let (pw_play_tx, pw_play_rx) = pw::channel::channel::(); + + let (init_tx, init_rx) = mpsc::channel::>(); + let mut latch = Latch::new(); + let waiter = latch.waiter(); + let device = self.clone(); + let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); + let initial_quantum = match config.buffer_size { + BufferSize::Fixed(n) => n, + BufferSize::Default => self.quantum, + }; + let last_quantum = Arc::new(AtomicU32::new(initial_quantum)); + let last_quantum_clone = last_quantum.clone(); + // Keep `playback` monotonic: pw_time delay() shrinks when other clients that needed + // a larger buffer leave the graph, which can pull `playback` backward. + let data_callback = crate::host::monotonic_output_callback(data_callback); + let start = std::time::Instant::now(); + let handle = thread::Builder::new() + .name("pw_out".to_owned()) + .spawn(move || { + let _pw = PwInitGuard::new(); + let properties = device.pw_properties(DeviceDirection::Output, &config); + + let stream_data = match super::stream::connect_output( + super::stream::ConnectParams { + config, + properties, + sample_format, + last_quantum: last_quantum_clone, + start, + connect_automatically: device.connect_automatically.load(Ordering::Relaxed), + is_default_device: matches!( + device.class(), + Class::DefaultSink | Class::DefaultInput | Class::DefaultOutput + ), + }, + data_callback, + error_callback, + ) { + Ok(d) => d, + Err(e) => { + let _ = init_tx.send(Err(Error::with_message( + ErrorKind::UnsupportedConfig, + format!("PipeWire stream connection failed: {e}"), + ))); + return; + } + }; + + let StreamData { + mainloop, + listener, + stream, + context, + core, + core_monitor, + error_callback, + pending_device_changed, + invalidated, + } = stream_data; + + let default_monitor = if let Some(key) = device.default_metadata_key() { + match core.get_registry_rc() { + Ok(registry) => Some(DefaultDeviceMonitor::new( + registry, + key, + error_callback.clone(), + invalidated, + pending_device_changed, + )), + Err(e) => { + let _ = init_tx.send(Err(Error::with_message( + ErrorKind::BackendError, + format!("PipeWire: could not acquire registry: {e}"), + ))); + return; + } + } + } else { + None + }; + let stream_clone = stream.clone(); + let mainloop_rc1 = mainloop.clone(); + let error_callback_cmd = error_callback.clone(); + let _receiver = pw_play_rx.attach(mainloop.loop_(), move |play| match play { + StreamCommand::Toggle(state) => { + if let Err(e) = stream_clone.set_active(state) { + emit_error( + &error_callback_cmd, + Error::with_message( + ErrorKind::StreamInvalidated, + format!("PipeWire: set_active({state}) failed: {e}"), + ), + ); + } + } + StreamCommand::Stop => { + if let Err(e) = stream_clone.disconnect() { + emit_error( + &error_callback_cmd, + Error::with_message( + ErrorKind::StreamInvalidated, + format!("PipeWire: stream disconnect failed: {e}"), + ), + ); + } + mainloop_rc1.quit(); + } + }); + + if init_tx.send(Ok(())).is_err() { + return; + } + + // If the Latch is dropped without being released (error path), exit cleanly. + if !waiter.wait() { + return; + } + + mainloop.run(); + drop(listener); + drop(default_monitor); + drop(core_monitor); + drop(core); + drop(context); + }) + .map_err(|e| { + Error::with_message( + ErrorKind::ResourceExhausted, + format!("Failed to create thread: {e}"), + ) + })?; + + let init_result = init_rx.recv_timeout(wait_timeout).unwrap_or_else(|_| { + Err(Error::with_message( + ErrorKind::DeviceNotAvailable, + "PipeWire timed out", + )) + }); + + if let Err(e) = init_result { + drop(latch); + return Err(e); + } + + latch.add_thread(handle.thread().clone()); + let stream = Stream::new(handle, pw_play_tx, last_quantum, start, latch); + stream.signal_ready(); + Ok(stream) + } +} + +#[derive(Clone, Default)] +struct Settings { + rate: SampleRate, + allow_rates: Box<[SampleRate]>, + quantum: FrameCount, + min_quantum: FrameCount, + max_quantum: FrameCount, +} + +// NOTE: it is just used to keep the lifetime +#[allow(dead_code)] +enum Request { + Node(NodeListener), + Meta(MetadataListener), +} + +impl From for Request { + fn from(value: NodeListener) -> Self { + Self::Node(value) + } +} + +impl From for Request { + fn from(value: MetadataListener) -> Self { + Self::Meta(value) + } +} + +/// Per-node rate and quantum discovered during device enumeration. +struct NodeOverrides { + rate: Option, + quantum: Option, +} + +/// Parses a PipeWire fraction string like "1/48000" or "256/48000" into its parts. +fn parse_fraction(s: &str) -> Option<(u32, u32)> { + let mut it = s.splitn(2, '/'); + let num: u32 = it.next()?.parse().ok()?; + let den: u32 = it.next()?.parse().ok()?; + Some((num, den)) +} + +fn remote_props() -> Option { + let socket = super::utils::find_socket_path()?; + let mut props = PropertiesBox::new(); + props.insert(*pw::keys::REMOTE_NAME, socket.to_string_lossy().as_ref()); + Some(props) +} + +pub fn init_devices(connect_automatically: Arc) -> Option> { + let _pw = PwInitGuard::new(); + let mainloop = MainLoopRc::new(None).ok()?; + let context = ContextRc::new(&mainloop, None).ok()?; + let core = context.connect_rc(remote_props()).ok()?; + let registry = core.get_registry_rc().ok()?; + + // Discovered hardware nodes collected during enumeration. + let discovered: Rc>> = Rc::new(RefCell::new(vec![])); + let requests = Rc::new(RefCell::new(vec![])); + let settings = Rc::new(RefCell::new(Settings::default())); + let loop_clone = mainloop.clone(); + + // Trigger the sync event. The server's answer won't be processed until we start the main loop, + // so we can safely do this before setting up a callback. This lets us avoid using a Cell. + let pending_events: Rc>> = Rc::new(RefCell::new(vec![])); + let pending = core.sync(0).ok()?; + + pending_events.borrow_mut().push(pending); + + let _listener_core = core + .add_listener_local() + .done({ + let pending_events = pending_events.clone(); + move |id, seq| { + if id != PW_ID_CORE { + return; + } + let mut pendinglist = pending_events.borrow_mut(); + let Some(index) = pendinglist.iter().position(|o_seq| *o_seq == seq) else { + return; + }; + pendinglist.remove(index); + if !pendinglist.is_empty() { + return; + } + loop_clone.quit(); + } + }) + .register(); + let _listener_reg = registry + .add_listener_local() + .global({ + let discovered = discovered.clone(); + let registry = registry.clone(); + let requests = requests.clone(); + let settings = settings.clone(); + move |global| match global.type_ { + ObjectType::Metadata => { + if !global.props.is_some_and(|props| { + props + .get(METADATA_NAME) + .is_some_and(|name| name == "settings") + }) { + return; + } + let meta_settings: Metadata = match registry.bind(global) { + Ok(meta_settings) => meta_settings, + Err(_) => { + // TODO: do something about this error + // Though it is already checked, but maybe something happened with + // pipewire? + return; + } + }; + let settings = settings.clone(); + let listener = meta_settings + .add_listener_local() + .property(move |_, key, _, value| { + match (key, value) { + (Some(clock::RATE), Some(rate)) => { + let Ok(rate) = rate.parse() else { + return 0; + }; + settings.borrow_mut().rate = rate; + } + (Some(clock::ALLOWED_RATES), Some(list)) => { + let Some(allow_rates) = parse_allow_rates(list) else { + return 0; + }; + + settings.borrow_mut().allow_rates = + allow_rates.into_boxed_slice(); + } + (Some(clock::QUANTUM), Some(quantum)) => { + let Ok(quantum) = quantum.parse() else { + return 0; + }; + settings.borrow_mut().quantum = quantum; + } + (Some(clock::MIN_QUANTUM), Some(min_quantum)) => { + let Ok(min_quantum) = min_quantum.parse() else { + return 0; + }; + settings.borrow_mut().min_quantum = min_quantum; + } + (Some(clock::MAX_QUANTUM), Some(max_quantum)) => { + let Ok(max_quantum) = max_quantum.parse() else { + return 0; + }; + settings.borrow_mut().max_quantum = max_quantum; + } + _ => {} + } + 0 + }) + .register(); + let Ok(pending) = core.sync(0) else { + // TODO: maybe we should add a log? + return; + }; + pending_events.borrow_mut().push(pending); + requests + .borrow_mut() + .push((meta_settings.upcast(), Request::Meta(listener))); + } + ObjectType::Node => { + let Some(props) = global.props else { + return; + }; + let Some(media_class) = props.get(*pw::keys::MEDIA_CLASS) else { + return; + }; + if !matches!( + media_class, + audio::SINK + | audio::SOURCE + | audio::DUPLEX + | audio::SINK_INTERNAL + | audio::SOURCE_INTERNAL + | audio::DUPLEX_INTERNAL + | audio::STREAM_INPUT + | audio::STREAM_OUTPUT + ) { + return; + } + + let node: Node = match registry.bind(global) { + Ok(node) => node, + Err(_) => { + // TODO: do something about this error + // Though it is already checked, but maybe something happened with + // pipewire? + return; + } + }; + + let discovered = discovered.clone(); + let listener = node + .add_listener_local() + .info(move |info| { + let Some(props) = info.props() else { + return; + }; + let Some(media_class) = props.get(*pw::keys::MEDIA_CLASS) else { + return; + }; + let role = match media_class { + audio::SINK | audio::SINK_INTERNAL => Role::Sink, + audio::SOURCE | audio::SOURCE_INTERNAL => Role::Source, + audio::DUPLEX | audio::DUPLEX_INTERNAL => Role::Duplex, + audio::STREAM_OUTPUT => Role::StreamOutput, + audio::STREAM_INPUT => Role::StreamInput, + _ => { + return; + } + }; + // Discovered `Audio/Sink` nodes are exposed as + // `Duplex`, so they are treated as input-capable. + // When cpal later opens an input stream on such + // a device, it sets `STREAM_CAPTURE_SINK`, which + // makes that stream capture audio playing to the + // sink. + let direction = match role { + Role::Sink => DeviceDirection::Duplex, + Role::Source => DeviceDirection::Input, + Role::Duplex => DeviceDirection::Duplex, + Role::StreamOutput => DeviceDirection::Output, + Role::StreamInput => DeviceDirection::Input, + }; + let Some(object_serial) = props + .get(*pw::keys::OBJECT_SERIAL) + .and_then(|serial| serial.parse().ok()) + else { + return; + }; + let node_name = props + .get(*pw::keys::NODE_NAME) + .unwrap_or("unknown") + .to_owned(); + let description = props + .get(*pw::keys::NODE_DESCRIPTION) + .unwrap_or("unknown") + .to_owned(); + let channels = props + .get(*pw::keys::AUDIO_CHANNELS) + .and_then(|channels| channels.parse().ok()) + .unwrap_or(2); + + let icon_name = + props.get(DEVICE_ICON_NAME).unwrap_or("default").to_owned(); + + let interface_type = match props.get(*pw::keys::DEVICE_API) { + Some("bluez5") => InterfaceType::Bluetooth, + _ => match props.get("device.bus") { + Some("pci") => InterfaceType::Pci, + Some("usb") => InterfaceType::Usb, + Some("firewire") => InterfaceType::FireWire, + Some("thunderbolt") => InterfaceType::Thunderbolt, + _ => InterfaceType::Unknown, + }, + }; + + let address = props + .get("api.bluez5.address") + .or_else(|| props.get("api.alsa.path")) + .map(|s| s.to_owned()); + + let driver = props.get(*pw::keys::FACTORY_NAME).map(|s| s.to_owned()); + + // "node.rate" = "1/" — set by the driver, authoritative + // for the hardware clock rate. + let node_rate: Option = props + .get(node::RATE) + .and_then(parse_fraction) + .filter(|(_, den)| *den > 0) + .map(|(_, den)| den); + + // "node.latency" = "/" — preferred quantum; the rate + // denominator is a fallback when node.rate is absent. + let (node_quantum, latency_rate): ( + Option, + Option, + ) = props + .get(node::LATENCY) + .and_then(parse_fraction) + .filter(|(num, den)| *num > 0 && *den > 0) + .unzip(); + + // node.rate is authoritative; node.latency denominator is the + // fallback for devices that advertise latency but not a direct rate. + let rate_override = node_rate.or(latency_rate); + + let device = Device { + node_name, + description, + direction, + role, + channels, + icon_name, + object_serial, + interface_type, + address, + driver, + ..Default::default() + }; + discovered.borrow_mut().push(( + device, + NodeOverrides { + rate: rate_override, + quantum: node_quantum, + }, + )); + }) + .register(); + let Ok(pending) = core.sync(0) else { + // TODO: maybe we should add a log? + return; + }; + pending_events.borrow_mut().push(pending); + requests + .borrow_mut() + .push((node.upcast(), Request::Node(listener))); + } + _ => {} + } + }) + .register(); + + // Guard against PipeWire daemons that accept a connection but never send `done` events. + let (cancel_tx, cancel_rx) = mpsc::channel::<()>(); + let (timeout_tx, timeout_rx) = pw::channel::channel::<()>(); + let loop_quit = mainloop.clone(); + let _timeout_watcher = timeout_rx.attach(mainloop.loop_(), move |_| { + loop_quit.quit(); + }); + thread::spawn(move || { + if cancel_rx.recv_timeout(INIT_TIMEOUT).is_err() { + let _ = timeout_tx.send(()); + } + }); + + mainloop.run(); + let _ = cancel_tx.send(()); + + // If PipeWire connected but discovered no real audio nodes, it cannot route any streams. Treat + // this as unavailable so the caller can fall back to PulseAudio or ALSA. + if discovered.borrow().is_empty() { + return None; + } + + let settings = settings.take(); + + // Build the three synthetic default devices and apply global clock settings to them. + let mut devices = vec![ + Device::sink_default(), + Device::input_default(), + Device::output_default(), + ]; + let shared_rates: Arc<[SampleRate]> = Arc::from(settings.allow_rates.as_ref()); + for device in devices.iter_mut() { + device.rate = settings.rate; + device.allow_rates = Arc::clone(&shared_rates); + device.quantum = settings.quantum; + device.min_quantum = settings.min_quantum; + device.max_quantum = settings.max_quantum; + device.connect_automatically = connect_automatically.clone(); + } + + // Resolve each discovered hardware node: global settings apply unless the node + // advertised its own rate or quantum, in which case those take precedence. + devices.extend( + discovered + .take() + .into_iter() + .map(|(mut device, overrides)| { + device.rate = overrides.rate.unwrap_or(settings.rate); + device.allow_rates = Arc::clone(&shared_rates); + device.quantum = overrides.quantum.unwrap_or(settings.quantum); + device.min_quantum = settings.min_quantum; + device.max_quantum = settings.max_quantum; + device.connect_automatically = connect_automatically.clone(); + device + }), + ); + + Some(devices) +} + +fn parse_allow_rates(list: &str) -> Option> { + list.trim() + .strip_prefix("[")? + .strip_suffix("]")? + .split([' ', ',']) + .filter(|s| !s.is_empty()) + .map(|s| s.parse().ok()) + .collect() +} + +#[cfg(test)] +mod test { + use super::{parse_allow_rates, parse_fraction, Class, Device}; + use crate::host::pipewire::utils::default; + + #[test] + fn rate_parse() { + // In documents, the rates are separated by space + let rate_str = r#" [ 44100 48000 88200 96000 176400 192000 ] "#; + let rates = parse_allow_rates(rate_str).unwrap(); + assert_eq!(rates, vec![44100, 48000, 88200, 96000, 176400, 192000]); + // ',' is also allowed + let rate_str = r#" [ 44100, 48000, 88200, 96000 ,176400 ,192000 ] "#; + let rates = parse_allow_rates(rate_str).unwrap(); + assert_eq!(rates, vec![44100, 48000, 88200, 96000, 176400, 192000]); + assert_eq!(rates, vec![44100, 48000, 88200, 96000, 176400, 192000]); + // We only use [] to define the list + let rate_str = r#" { 44100, 48000, 88200, 96000 ,176400 ,192000 } "#; + let rates = parse_allow_rates(rate_str); + assert_eq!(rates, None); + } + + #[test] + fn fraction_parse() { + // node.rate format: "1/" + assert_eq!(parse_fraction("1/48000"), Some((1, 48000))); + + // node.latency format: "/" + assert_eq!(parse_fraction("256/48000"), Some((256, 48000))); + + // zero values are returned as-is; callers apply .filter() to reject them + assert_eq!(parse_fraction("0/48000"), Some((0, 48000))); + assert_eq!(parse_fraction("256/0"), Some((256, 0))); + + // invalid inputs + assert_eq!(parse_fraction(""), None); + assert_eq!(parse_fraction("48000"), None); + assert_eq!(parse_fraction("abc/def"), None); + assert_eq!(parse_fraction("/48000"), None); + assert_eq!(parse_fraction("256/"), None); + } + + #[test] + fn default_metadata_key_mapping() { + assert_eq!( + Device::output_default().default_metadata_key(), + Some(default::SINK) + ); + assert_eq!( + Device::sink_default().default_metadata_key(), + Some(default::SINK) + ); + assert_eq!( + Device::input_default().default_metadata_key(), + Some(default::SOURCE) + ); + + let node = Device { + class: Class::Node, + ..Default::default() + }; + assert_eq!(node.default_metadata_key(), None); + } +} diff --git a/crates/vendor/cpal/src/host/pipewire/mod.rs b/crates/vendor/cpal/src/host/pipewire/mod.rs new file mode 100644 index 000000000..c1b02a054 --- /dev/null +++ b/crates/vendor/cpal/src/host/pipewire/mod.rs @@ -0,0 +1,84 @@ +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +use device::{init_devices, Class, Device, Devices}; +use stream::PwInitGuard; + +use crate::{traits::HostTrait, Error, ErrorKind}; + +mod device; +#[cfg(all(target_os = "linux", feature = "realtime"))] +mod rt_promote; +mod stream; +mod utils; + +/// The PipeWire host, providing access to PipeWire audio devices. +/// +/// # PipeWire-Specific Configuration +/// +/// PipeWire provides a configuration option to control graph connection behavior: +/// - Port auto-connection via [`set_connect_automatically`](Host::set_connect_automatically) +pub struct Host { + // Keeps PipeWire initialized for the lifetime of the host, preventing + // pw_deinit() from running between device enumeration and stream creation. + _pw: PwInitGuard, + devices: Vec, + connect_automatically: Arc, +} + +impl Host { + pub fn new() -> Result { + let _pw = PwInitGuard::new(); + let connect_automatically = Arc::new(AtomicBool::new(true)); + let devices = init_devices(connect_automatically.clone()).ok_or_else(|| { + Error::with_message(ErrorKind::HostUnavailable, "PipeWire is not available") + })?; + Ok(Self { + _pw, + devices, + connect_automatically, + }) + } + + /// Configures whether created streams should automatically connect to system playback/capture + /// nodes via the session manager. + /// + /// When enabled (default), PipeWire's session manager links the stream to the appropriate sink + /// or source automatically. When disabled, the stream node is registered in the graph but left + /// unlinked; users must then manually connect ports using PipeWire tools or session manager + /// APIs. + /// + /// Default: `true` + pub fn set_connect_automatically(&mut self, connect: bool) { + self.connect_automatically.store(connect, Ordering::Relaxed); + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + utils::find_socket_path().is_some() + } + + fn devices(&self) -> Result { + Ok(self.devices.clone().into_iter()) + } + + fn default_input_device(&self) -> Option { + self.devices + .iter() + .find(|device| matches!(device.class(), Class::DefaultInput)) + .cloned() + } + + fn default_output_device(&self) -> Option { + self.devices + .iter() + .find(|device| matches!(device.class(), Class::DefaultOutput)) + .cloned() + } +} diff --git a/crates/vendor/cpal/src/host/pipewire/rt_promote.rs b/crates/vendor/cpal/src/host/pipewire/rt_promote.rs new file mode 100644 index 000000000..3fa0a522a --- /dev/null +++ b/crates/vendor/cpal/src/host/pipewire/rt_promote.rs @@ -0,0 +1,97 @@ +//! Background real-time thread promotion. + +use std::{ + sync::{ + atomic::{AtomicBool, AtomicU32, Ordering}, + Arc, + }, + thread::{self, JoinHandle, Thread}, +}; + +use crate::{ + host::{emit_error, ErrorCallbackArc}, + Error, ErrorKind, FrameCount, SampleRate, +}; + +pub(crate) struct RtPromoter { + pending_frames: Arc, + shutdown: Arc, + handle: Thread, + join: Option>, +} + +impl RtPromoter { + /// Must be called on the thread that will run the audio callbacks: `get_current_thread_info` + /// captures the calling thread's identity, to promote later from the background thread. + pub(crate) fn spawn(error_callback: ErrorCallbackArc, rate: SampleRate) -> Option { + let thread_info = match audio_thread_priority::get_current_thread_info() { + Ok(info) => info, + Err(e) => { + emit_error(&error_callback, Error::from(e)); + return None; + } + }; + + let pending_frames = Arc::new(AtomicU32::new(0)); + let shutdown = Arc::new(AtomicBool::new(false)); + let pending_frames_bg = pending_frames.clone(); + let shutdown_bg = shutdown.clone(); + let error_callback_bg = error_callback.clone(); + + let join = thread::Builder::new() + .name("cpal_rt_promote".to_owned()) + .spawn(move || loop { + if shutdown_bg.load(Ordering::Relaxed) { + return; + } + let frames = pending_frames_bg.swap(0, Ordering::Relaxed); + if frames != 0 { + if let Err(e) = audio_thread_priority::promote_thread_to_real_time( + thread_info, + frames, + rate, + ) { + emit_error(&error_callback_bg, Error::from(e)); + } + continue; + } + thread::park(); + }); + let join = match join { + Ok(j) => j, + Err(e) => { + emit_error( + &error_callback, + Error::with_message( + ErrorKind::ResourceExhausted, + format!("failed to create real-time promotion thread: {e}"), + ), + ); + return None; + } + }; + + Some(Self { + pending_frames, + shutdown, + handle: join.thread().clone(), + join: Some(join), + }) + } + + /// Hands a promotion request to the background thread; never blocks. + pub(crate) fn request(&self, frames: FrameCount) { + self.pending_frames.store(frames, Ordering::Relaxed); + self.handle.unpark(); + } +} + +impl Drop for RtPromoter { + fn drop(&mut self) { + self.shutdown.store(true, Ordering::Relaxed); + self.handle.unpark(); + if let Some(join) = self.join.take() { + let _ = join.join(); + } + } +} diff --git a/crates/vendor/cpal/src/host/pipewire/stream.rs b/crates/vendor/cpal/src/host/pipewire/stream.rs new file mode 100644 index 000000000..cea6e50fa --- /dev/null +++ b/crates/vendor/cpal/src/host/pipewire/stream.rs @@ -0,0 +1,1056 @@ +use std::{ + cell::RefCell, + rc::Rc, + sync::{ + atomic::{AtomicBool, AtomicU32, Ordering}, + Arc, Mutex, + }, + thread::JoinHandle, + time::Instant, +}; + +use pipewire::{ + self as pw, + context::ContextRc, + core::{CoreRc, Listener as CoreListener, PW_ID_CORE}, + main_loop::MainLoopRc, + metadata::{Metadata, MetadataListener}, + properties::PropertiesBox, + registry::{Listener as RegistryListener, RegistryRc}, + spa::{ + param::{ + audio::{AudioFormat, AudioInfoRaw}, + format::{MediaSubtype, MediaType}, + format_utils, ParamType, + }, + pod::{serialize::PodSerializer, Object, Pod, Value}, + sys::{spa_io_clock, SPA_IO_Clock}, + utils::{Direction, SpaTypes}, + }, + stream::{StreamFlags, StreamListener, StreamRc, StreamState, Time}, + types::ObjectType, +}; + +#[cfg(all(target_os = "linux", feature = "realtime"))] +use super::rt_promote::RtPromoter; +use crate::{ + host::{ + emit_error, equilibrium::fill_equilibrium, frames_to_duration, latch::Latch, + try_emit_error, ErrorCallbackArc, + }, + traits::StreamTrait, + Data, Error, ErrorKind, FrameCount, InputCallbackInfo, InputStreamTimestamp, + OutputCallbackInfo, OutputStreamTimestamp, SampleFormat, StreamConfig, StreamInstant, +}; + +/// Counts the number of live [`PwInitGuard`] instances across all threads. +static PW_INIT_COUNT: Mutex = Mutex::new(0); + +/// RAII guard that keeps the PipeWire library initialised for its lifetime. +pub(crate) struct PwInitGuard; + +impl PwInitGuard { + pub(crate) fn new() -> Self { + let mut count = PW_INIT_COUNT.lock().unwrap_or_else(|e| e.into_inner()); + if *count == 0 { + // pw::init() uses a OnceCell, preventing re-init after deinit. + unsafe { pw::sys::pw_init(std::ptr::null_mut(), std::ptr::null_mut()) } + } + *count += 1; + Self + } +} + +impl Drop for PwInitGuard { + fn drop(&mut self) { + let mut count = PW_INIT_COUNT.lock().unwrap_or_else(|e| e.into_inner()); + *count = count.saturating_sub(1); + if *count == 0 { + // Safety: the mutex ensures no other PwInitGuard exists at this + // point. Every scope that creates PipeWire objects holds a guard + // declared before those objects, so all PipeWire objects have + // already been dropped before this decrement reached zero. + unsafe { pw::deinit() } + } + } +} + +pub(super) enum StreamCommand { + Toggle(bool), + Stop, +} + +pub struct Stream { + handle: Option>, + controller: pw::channel::Sender, + last_quantum: Arc, + start: Instant, + latch: Latch, +} + +impl Stream { + pub(super) fn new( + handle: JoinHandle<()>, + controller: pw::channel::Sender, + last_quantum: Arc, + start: Instant, + latch: Latch, + ) -> Self { + Self { + handle: Some(handle), + controller, + last_quantum, + start, + latch, + } + } + + /// Releases the latch so the worker thread can begin processing audio callbacks. + pub fn signal_ready(&self) { + self.latch.release(); + } +} + +impl Drop for Stream { + fn drop(&mut self) { + // Unblock the worker in case the stream is dropped before signal_ready() was called. + self.signal_ready(); + let _ = self.controller.send(StreamCommand::Stop); + if let Some(handle) = self.handle.take() { + // Prevent self-join: Stop was sent; the handle detaches and the thread exits after + // the current callback returns. + if handle.thread().id() != std::thread::current().id() { + let _ = handle.join(); + } + } + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), Error> { + self.controller + .send(StreamCommand::Toggle(true)) + .map_err(|_| { + Error::with_message( + ErrorKind::StreamInvalidated, + "stream command channel closed", + ) + })?; + Ok(()) + } + fn pause(&self) -> Result<(), Error> { + self.controller + .send(StreamCommand::Toggle(false)) + .map_err(|_| { + Error::with_message( + ErrorKind::StreamInvalidated, + "stream command channel closed", + ) + })?; + Ok(()) + } + + fn now(&self) -> StreamInstant { + monotonic_stream_instant().unwrap_or_else(|| stream_instant_from_start(self.start)) + } + + fn buffer_size(&self) -> Result { + Ok(self.last_quantum.load(Ordering::Relaxed) as _) + } +} + +pub(crate) const SUPPORTED_FORMATS: &[SampleFormat] = &[ + SampleFormat::I8, + SampleFormat::U8, + SampleFormat::I16, + SampleFormat::U16, + SampleFormat::I24, + SampleFormat::U24, + SampleFormat::I32, + SampleFormat::U32, + // I64/U64 are excluded: libspa has no mapping for them yet. + // SampleFormat::I64, + // SampleFormat::U64, + SampleFormat::F32, + SampleFormat::F64, +]; + +impl From for AudioFormat { + fn from(value: SampleFormat) -> Self { + match value { + SampleFormat::I8 => Self::S8, + SampleFormat::U8 => Self::U8, + + #[cfg(target_endian = "little")] + SampleFormat::I16 => Self::S16LE, + #[cfg(target_endian = "big")] + SampleFormat::I16 => Self::S16BE, + #[cfg(target_endian = "little")] + SampleFormat::U16 => Self::U16LE, + #[cfg(target_endian = "big")] + SampleFormat::U16 => Self::U16BE, + + #[cfg(target_endian = "little")] + SampleFormat::I24 => Self::S24_32LE, + #[cfg(target_endian = "big")] + SampleFormat::I24 => Self::S24_32BE, + #[cfg(target_endian = "little")] + SampleFormat::U24 => Self::U24_32LE, + #[cfg(target_endian = "big")] + SampleFormat::U24 => Self::U24_32BE, + #[cfg(target_endian = "little")] + SampleFormat::I32 => Self::S32LE, + #[cfg(target_endian = "big")] + SampleFormat::I32 => Self::S32BE, + #[cfg(target_endian = "little")] + SampleFormat::U32 => Self::U32LE, + #[cfg(target_endian = "big")] + SampleFormat::U32 => Self::U32BE, + #[cfg(target_endian = "little")] + SampleFormat::F32 => Self::F32LE, + #[cfg(target_endian = "big")] + SampleFormat::F32 => Self::F32BE, + #[cfg(target_endian = "little")] + SampleFormat::F64 => Self::F64LE, + #[cfg(target_endian = "big")] + SampleFormat::F64 => Self::F64BE, + // NOTE: Seems PipeWire does support U64 and I64, but libspa doesn't yet. + // TODO: Maybe add the support in the future + _ => Self::Unknown, + } + } +} + +pub struct UserData { + data_callback: D, + error_callback: ErrorCallbackArc, + sample_format: SampleFormat, + format: AudioInfoRaw, + last_quantum: Arc, + start: Instant, + is_default_device: bool, + has_connected: bool, + invalidated: Arc, + pending_device_changed: Arc, + spa_io_clock: *const spa_io_clock, + xrun_recovering: bool, + #[cfg(feature = "realtime")] + rt_promoted_frames: FrameCount, + #[cfg(all(target_os = "linux", feature = "realtime"))] + rt_promoter: Option, +} + +impl UserData { + #[cfg(feature = "realtime")] + fn promote_realtime(&mut self, frames: FrameCount) { + // Set regardless of success, so a failed promotion isn't retried until the quantum grows. + self.rt_promoted_frames = frames; + + #[cfg(target_os = "linux")] + if let Some(promoter) = &self.rt_promoter { + promoter.request(frames); + } + + #[cfg(not(target_os = "linux"))] + // Currently a no-op outside Linux; kept in case audio_thread_priority adds a real backend. + if let Err(e) = + audio_thread_priority::promote_current_thread_to_real_time(frames, self.format.rate()) + { + let _ = try_emit_error(&self.error_callback, Error::from(e)); + } + } + + fn state_changed(&mut self, new: StreamState) { + match new { + StreamState::Streaming => self.has_connected = true, + StreamState::Unconnected => { + // Let the metadata monitor fire for default-device streams + if self.has_connected + && !self.is_default_device + && !self.invalidated.swap(true, Ordering::Relaxed) + { + emit_error( + &self.error_callback, + Error::with_message(ErrorKind::DeviceNotAvailable, "Device disconnected"), + ); + } + } + StreamState::Error(e) => { + if !self.invalidated.swap(true, Ordering::Relaxed) { + emit_error( + &self.error_callback, + Error::with_message(ErrorKind::StreamInvalidated, e), + ); + } + } + StreamState::Paused | StreamState::Connecting => {} + } + } + + fn check_xrun(&mut self) { + if self.spa_io_clock.is_null() { + return; + } + // spa/node/io.h; not present in bindgen output on all libspa versions. + const SPA_IO_CLOCK_FLAG_XRUN_RECOVER: u32 = 1 << 1; + // io_changed and process run on the same thread. + let flags = unsafe { (*self.spa_io_clock).flags }; + let recovering = flags & SPA_IO_CLOCK_FLAG_XRUN_RECOVER != 0; + if recovering && !self.xrun_recovering { + let _ = try_emit_error(&self.error_callback, ErrorKind::Xrun.into()); + } + self.xrun_recovering = recovering; + } +} + +/// Returns a hardware timestamp for the current graph cycle, or `None` if +/// the driver has not started yet or the rate is unavailable. +fn pw_stream_time(stream: &pw::stream::Stream) -> Option