diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c89fc..88e91e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and Termleaf uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [0.3.5] - 2026-08-02 + +### Fixed + +- Restored typewriter audio on i686 Linux by backporting CPAL's 32-bit-safe + ALSA timestamp conversion, letting ALSA choose a device-compatible buffer + size, and rebuilding streams after backend failures. + ## [0.3.4] - 2026-08-02 ### Changed @@ -116,7 +124,8 @@ and Termleaf uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Made forward Delete work for characters and line boundaries. - Made Save As reliable through `F12`, including a Markdown default extension. -[Unreleased]: https://github.com/andy5090/termleaf/compare/v0.3.4...HEAD +[Unreleased]: https://github.com/andy5090/termleaf/compare/v0.3.5...HEAD +[0.3.5]: https://github.com/andy5090/termleaf/compare/v0.3.4...v0.3.5 [0.3.4]: https://github.com/andy5090/termleaf/compare/v0.3.3...v0.3.4 [0.3.3]: https://github.com/andy5090/termleaf/compare/v0.3.2...v0.3.3 [0.3.2]: https://github.com/andy5090/termleaf/compare/v0.3.1...v0.3.2 diff --git a/Cargo.lock b/Cargo.lock index e459c92..d037ad4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,8 +96,6 @@ dependencies = [ [[package]] name = "cpal" version = "0.17.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7" dependencies = [ "alsa", "coreaudio-rs", @@ -729,7 +727,7 @@ dependencies = [ [[package]] name = "termleaf" -version = "0.3.4" +version = "0.3.5" dependencies = [ "crossterm", "rodio", diff --git a/Cargo.toml b/Cargo.toml index a49bf61..2fbc656 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "termleaf" -version = "0.3.4" +version = "0.3.5" edition = "2021" description = "A distraction-free terminal text editor built for focused writing." repository = "https://github.com/andy5090/termleaf" @@ -22,6 +22,12 @@ path = "src/bin/termleaf-update.rs" crossterm = "0.28" rodio = { version = "0.22.2", default-features = false, features = ["playback"] } +[patch.crates-io] +# CPAL 0.17.3 overflows when converting ALSA timestamps on 32-bit Linux, +# terminating the audio callback thread on its first buffer. The vendored copy +# backports upstream RustAudio/cpal#1137 until Rodio accepts CPAL 0.18. +cpal = { path = "vendor/cpal" } + [profile.release] opt-level = 3 lto = true diff --git a/src/audio.rs b/src/audio.rs index 64898ef..442b7e4 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -37,8 +37,6 @@ const BACKSPACE_WAV: &[u8] = include_bytes!("../assets/typewriter-backspace.wav" const RETURN_WAV: &[u8] = include_bytes!("../assets/typewriter-return.wav"); const BACKSPACE_MIN_INTERVAL: Duration = Duration::from_millis(55); const STREAM_RETRY_INTERVAL: Duration = Duration::from_secs(2); -#[cfg(all(target_os = "linux", target_arch = "x86"))] -const I686_STABILITY_BUFFER_FRAMES: u32 = 4_096; struct Clip { samples: Vec, @@ -159,13 +157,6 @@ fn open_stream(stream_healthy: Arc) -> Option { let callback_state = Arc::clone(&stream_healthy); let builder = DeviceSinkBuilder::from_default_device().ok()?; - // Older 32-bit x86 machines have less scheduling headroom. Rodio documents - // 2048-4096 frames as its stability-focused range, so use the upper bound - // there while retaining the lower-latency default on other targets. - #[cfg(all(target_os = "linux", target_arch = "x86"))] - let builder = - builder.with_buffer_size(rodio::cpal::BufferSize::Fixed(I686_STABILITY_BUFFER_FRAMES)); - builder .with_error_callback(move |error| { if stream_error_requires_rebuild(&error) { @@ -183,10 +174,7 @@ fn open_stream(stream_healthy: Arc) -> Option { } fn stream_error_requires_rebuild(error: &StreamError) -> bool { - matches!( - error, - StreamError::DeviceNotAvailable | StreamError::StreamInvalidated - ) + !matches!(error, StreamError::BufferUnderrun) } fn backspace_playback_allowed(last: Option, now: Instant) -> bool { @@ -256,6 +244,7 @@ fn decode_pcm_wave(wav: &[u8]) -> Option<(u16, u32, Vec)> { #[cfg(test)] mod tests { use super::*; + use std::thread; fn embedded_wavs() -> Vec<&'static [u8]> { TYPEWRITER_WAVS @@ -428,7 +417,7 @@ mod tests { } #[test] - fn only_device_loss_and_invalidated_streams_require_a_rebuild() { + fn backend_failures_require_a_rebuild_but_transient_underruns_do_not() { assert!(!stream_error_requires_rebuild(&StreamError::BufferUnderrun)); assert!(stream_error_requires_rebuild( &StreamError::DeviceNotAvailable @@ -436,7 +425,7 @@ mod tests { assert!(stream_error_requires_rebuild( &StreamError::StreamInvalidated )); - assert!(!stream_error_requires_rebuild( + assert!(stream_error_requires_rebuild( &StreamError::BackendSpecific { err: rodio::cpal::BackendSpecificError { description: "`alsa::poll()` returned POLLERR".into(), @@ -458,4 +447,16 @@ mod tests { start + STREAM_RETRY_INTERVAL )); } + + /// Exercise the real output callback when diagnosing a supported machine. + /// This stays ignored because CI and cross-build hosts may have no speaker. + #[test] + #[ignore = "requires a working default audio output device"] + fn hardware_audio_callback_stays_alive() { + let player = SoundPlayer::new(); + assert!(player.stream.borrow().is_some(), "audio stream should open"); + player.play_key("classic"); + thread::sleep(Duration::from_millis(500)); + assert!(player.stream_healthy.load(Ordering::Acquire)); + } } diff --git a/vendor/cpal/Cargo.toml b/vendor/cpal/Cargo.toml new file mode 100644 index 0000000..ce4482e --- /dev/null +++ b/vendor/cpal/Cargo.toml @@ -0,0 +1,288 @@ +# 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.78" +name = "cpal" +version = "0.17.3" +build = "build.rs" +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Low-level cross-platform audio I/O library in pure Rust." +documentation = "https://docs.rs/cpal" +readme = "README.md" +keywords = [ + "audio", + "sound", +] +license = "Apache-2.0" +repository = "https://github.com/RustAudio/cpal" + +[package.metadata.docs.rs] +all-features = true +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", + "wasm32-unknown-emscripten", + "aarch64-linux-android", + "x86_64-unknown-freebsd", + "x86_64-unknown-netbsd", + "x86_64-unknown-dragonfly", +] + +[features] +asio = [ + "dep:asio-sys", + "dep:num-traits", +] +audioworklet = [ + "wasm-bindgen", + "web-sys/Blob", + "web-sys/BlobPropertyBag", + "web-sys/Url", + "web-sys/AudioWorklet", + "web-sys/AudioWorkletNode", + "web-sys/AudioWorkletNodeOptions", +] +custom = [] +jack = ["dep:jack"] +wasm-bindgen = [ + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures", +] + +[lib] +name = "cpal" +path = "src/lib.rs" + +[[example]] +name = "beep" +path = "examples/beep.rs" + +[[example]] +name = "custom" +path = "examples/custom.rs" + +[[example]] +name = "enumerate" +path = "examples/enumerate.rs" + +[[example]] +name = "feedback" +path = "examples/feedback.rs" + +[[example]] +name = "record_wav" +path = "examples/record_wav.rs" + +[[example]] +name = "synth_tones" +path = "examples/synth_tones.rs" + +[dependencies.dasp_sample] +version = "0.11" + +[dev-dependencies.anyhow] +version = "1.0" + +[dev-dependencies.clap] +version = ">=4.0, <=4.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.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 = "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.audio_thread_priority] +version = "0.34" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies.jack] +version = "0.13" +optional = true + +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies.libc] +version = "0.2" + +[target.'cfg(target_os = "android")'.dependencies.jni] +version = "0.21" + +[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 = "emscripten")'.dependencies.js-sys] +version = "0.3" + +[target.'cfg(target_os = "emscripten")'.dependencies.wasm-bindgen] +version = "0.2" + +[target.'cfg(target_os = "emscripten")'.dependencies.wasm-bindgen-futures] +version = "0.4" + +[target.'cfg(target_os = "emscripten")'.dependencies.web-sys] +version = "0.3" +features = [ + "AudioContext", + "AudioContextOptions", + "AudioBuffer", + "AudioBufferSourceNode", + "AudioNode", + "AudioDestinationNode", + "Window", + "AudioContextState", +] + +[target.'cfg(target_os = "ios")'.dependencies.objc2-avf-audio] +version = "0.3" +features = [ + "std", + "AVAudioSession", +] +default-features = false + +[target.'cfg(target_os = "macos")'.dependencies.jack] +version = "0.13" +optional = true + +[target.'cfg(target_os = "windows")'.dependencies.asio-sys] +version = "0.2.6" +optional = true + +[target.'cfg(target_os = "windows")'.dependencies.audio_thread_priority] +version = "0.34" +optional = true + +[target.'cfg(target_os = "windows")'.dependencies.jack] +version = "0.13" +optional = true + +[target.'cfg(target_os = "windows")'.dependencies.num-traits] +version = "0.2" +optional = true + +[target.'cfg(target_os = "windows")'.dependencies.windows] +version = ">=0.59, <=0.62" +features = [ + "Win32_Media_Audio", + "Win32_Foundation", + "Win32_Devices_Properties", + "Win32_Media_KernelStreaming", + "Win32_System_Com_StructuredStorage", + "Win32_System_Threading", + "Win32_Security", + "Win32_System_SystemServices", + "Win32_System_Variant", + "Win32_Media_Multimedia", + "Win32_UI_Shell_PropertiesSystem", +] + +[target.'cfg(target_vendor = "apple")'.dependencies.coreaudio-rs] +version = "0.14" +features = [ + "core_audio", + "audio_toolbox", +] +default-features = false + +[target.'cfg(target_vendor = "apple")'.dependencies.mach2] +version = "0.5" + +[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" + +[target.'cfg(target_vendor = "apple")'.dependencies.objc2-foundation] +version = "0.3" diff --git a/vendor/cpal/LICENSE b/vendor/cpal/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/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/vendor/cpal/build.rs b/vendor/cpal/build.rs new file mode 100644 index 0000000..696db7b --- /dev/null +++ b/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/vendor/cpal/src/device_description.rs b/vendor/cpal/src/device_description.rs new file mode 100644 index 0000000..c99bb24 --- /dev/null +++ b/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: Vec, +} + +/// 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 + } + + /// 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) -> &[String] { + &self.extended + } +} + +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 Into) -> Self { + Self { + name: name.into(), + 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 Into) -> Self { + self.manufacturer = Some(manufacturer.into()); + self + } + + /// Sets the driver name. + pub fn driver(mut self, driver: impl Into) -> Self { + self.driver = Some(driver.into()); + 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 Into) -> Self { + self.address = Some(address.into()); + self + } + + /// Sets the description lines. + pub fn extended(mut self, lines: Vec) -> Self { + self.extended = lines; + self + } + + /// Adds a single description line. + pub fn add_extended_line(mut self, line: impl Into) -> Self { + self.extended.push(line.into()); + 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, + } + } +} + +/// 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/vendor/cpal/src/error.rs b/vendor/cpal/src/error.rs new file mode 100644 index 0000000..016d7cd --- /dev/null +++ b/vendor/cpal/src/error.rs @@ -0,0 +1,329 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; + +/// The requested host, although supported on this platform, is unavailable. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct HostUnavailable; + +impl Display for HostUnavailable { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("the requested host is unavailable") + } +} + +impl Error for HostUnavailable {} + +/// Some error has occurred that is specific to the backend from which it was produced. +/// +/// This error is often used as a catch-all in cases where: +/// +/// - It is unclear exactly what error might be produced by the backend API. +/// - It does not make sense to add a variant to the enclosing error type. +/// - No error was expected to occur at all, but we return an error to avoid the possibility of a +/// `panic!` caused by some unforeseen or unknown reason. +/// +/// **Note:** If you notice a `BackendSpecificError` that you believe could be better handled in a +/// cross-platform manner, please create an issue at +/// with details about your use case, the backend you're using, and the error message. Or submit +/// a pull request with a patch that adds the necessary error variant to the appropriate error enum. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct BackendSpecificError { + pub description: String, +} + +impl Display for BackendSpecificError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "A backend-specific error has occurred: {}", + self.description + ) + } +} + +impl Error for BackendSpecificError {} + +/// An error that might occur while attempting to enumerate the available devices on a system. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DevicesError { + /// See the [`BackendSpecificError`] docs for more information about this error variant. + BackendSpecific { err: BackendSpecificError }, +} + +impl Display for DevicesError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::BackendSpecific { err } => err.fmt(f), + } + } +} + +impl Error for DevicesError {} + +impl From for DevicesError { + fn from(err: BackendSpecificError) -> Self { + Self::BackendSpecific { err } + } +} + +/// An error that may occur while attempting to retrieve a device ID. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DeviceIdError { + /// See the [`BackendSpecificError`] docs for more information about this error variant. + BackendSpecific { + err: BackendSpecificError, + }, + UnsupportedPlatform, +} + +impl Display for DeviceIdError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::BackendSpecific { err } => err.fmt(f), + Self::UnsupportedPlatform => f.write_str("Device IDs are unsupported for this OS"), + } + } +} + +impl Error for DeviceIdError {} + +impl From for DeviceIdError { + fn from(err: BackendSpecificError) -> Self { + Self::BackendSpecific { err } + } +} + +/// An error that may occur while attempting to retrieve a device name. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DeviceNameError { + /// See the [`BackendSpecificError`] docs for more information about this error variant. + BackendSpecific { err: BackendSpecificError }, +} + +impl Display for DeviceNameError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::BackendSpecific { err } => err.fmt(f), + } + } +} + +impl Error for DeviceNameError {} + +impl From for DeviceNameError { + fn from(err: BackendSpecificError) -> Self { + Self::BackendSpecific { err } + } +} + +/// Error that can happen when enumerating the list of supported formats. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum SupportedStreamConfigsError { + /// The device no longer exists. This can happen if the device is disconnected while the + /// program is running. + DeviceNotAvailable, + /// We called something the C-Layer did not understand + InvalidArgument, + /// See the [`BackendSpecificError`] docs for more information about this error variant. + BackendSpecific { err: BackendSpecificError }, +} + +impl Display for SupportedStreamConfigsError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::BackendSpecific { err } => err.fmt(f), + Self::DeviceNotAvailable => f.write_str("The requested device is no longer available. For example, it has been unplugged."), + Self::InvalidArgument => f.write_str("Invalid argument passed to the backend. For example, this happens when trying to read capture capabilities when the device does not support it.") + } + } +} + +impl Error for SupportedStreamConfigsError {} + +impl From for SupportedStreamConfigsError { + fn from(err: BackendSpecificError) -> Self { + Self::BackendSpecific { err } + } +} + +/// May occur when attempting to request the default input or output stream format from a [`Device`](crate::Device). +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DefaultStreamConfigError { + /// The device no longer exists. This can happen if the device is disconnected while the + /// program is running. + DeviceNotAvailable, + /// Returned if e.g. the default input format was requested on an output-only audio device. + StreamTypeNotSupported, + /// See the [`BackendSpecificError`] docs for more information about this error variant. + BackendSpecific { err: BackendSpecificError }, +} + +impl Display for DefaultStreamConfigError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::BackendSpecific { err } => err.fmt(f), + Self::DeviceNotAvailable => f.write_str( + "The requested device is no longer available. For example, it has been unplugged.", + ), + Self::StreamTypeNotSupported => { + f.write_str("The requested stream type is not supported by the device.") + } + } + } +} + +impl Error for DefaultStreamConfigError {} + +impl From for DefaultStreamConfigError { + fn from(err: BackendSpecificError) -> Self { + Self::BackendSpecific { err } + } +} +/// Error that can happen when creating a [`Stream`](crate::Stream). +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum BuildStreamError { + /// The device no longer exists. This can happen if the device is disconnected while the + /// program is running. + DeviceNotAvailable, + /// The specified stream configuration is not supported. + StreamConfigNotSupported, + /// We called something the C-Layer did not understand + /// + /// On ALSA device functions called with a feature they do not support will yield this. E.g. + /// Trying to use capture capabilities on an output only format yields this. + InvalidArgument, + /// Occurs if adding a new Stream ID would cause an integer overflow. + StreamIdOverflow, + /// See the [`BackendSpecificError`] docs for more information about this error variant. + BackendSpecific { err: BackendSpecificError }, +} + +impl Display for BuildStreamError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::BackendSpecific { err } => err.fmt(f), + Self::DeviceNotAvailable => f.write_str( + "The requested device is no longer available. For example, it has been unplugged.", + ), + Self::StreamConfigNotSupported => { + f.write_str("The requested stream configuration is not supported by the device.") + } + Self::InvalidArgument => f.write_str( + "The requested device does not support this capability (invalid argument)", + ), + Self::StreamIdOverflow => f.write_str("Adding a new stream ID would cause an overflow"), + } + } +} + +impl Error for BuildStreamError {} + +impl From for BuildStreamError { + fn from(err: BackendSpecificError) -> Self { + Self::BackendSpecific { err } + } +} + +/// Errors that might occur when calling [`Stream::play()`](crate::traits::StreamTrait::play). +/// +/// As of writing this, only macOS may immediately return an error while calling this method. This +/// is because both the alsa and wasapi backends only enqueue these commands and do not process +/// them immediately. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum PlayStreamError { + /// The device associated with the stream is no longer available. + DeviceNotAvailable, + /// See the [`BackendSpecificError`] docs for more information about this error variant. + BackendSpecific { err: BackendSpecificError }, +} + +impl Display for PlayStreamError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::BackendSpecific { err } => err.fmt(f), + Self::DeviceNotAvailable => { + f.write_str("the device associated with the stream is no longer available") + } + } + } +} + +impl Error for PlayStreamError {} + +impl From for PlayStreamError { + fn from(err: BackendSpecificError) -> Self { + Self::BackendSpecific { err } + } +} + +/// Errors that might occur when calling [`Stream::pause()`](crate::traits::StreamTrait::pause). +/// +/// As of writing this, only macOS may immediately return an error while calling this method. This +/// is because both the alsa and wasapi backends only enqueue these commands and do not process +/// them immediately. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum PauseStreamError { + /// The device associated with the stream is no longer available. + DeviceNotAvailable, + /// See the [`BackendSpecificError`] docs for more information about this error variant. + BackendSpecific { err: BackendSpecificError }, +} + +impl Display for PauseStreamError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::BackendSpecific { err } => err.fmt(f), + Self::DeviceNotAvailable => { + f.write_str("the device associated with the stream is no longer available") + } + } + } +} + +impl Error for PauseStreamError {} + +impl From for PauseStreamError { + fn from(err: BackendSpecificError) -> Self { + Self::BackendSpecific { err } + } +} + +/// Errors that might occur while a stream is running. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum StreamError { + /// The device no longer exists. This can happen if the device is disconnected while the + /// program is running. + DeviceNotAvailable, + + /// The stream configuration is no longer valid and must be rebuilt. + StreamInvalidated, + + /// Buffer underrun or overrun occurred, causing a potential audio glitch. + BufferUnderrun, + + /// See the [`BackendSpecificError`] docs for more information about this error variant. + BackendSpecific { err: BackendSpecificError }, +} + +impl Display for StreamError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::BackendSpecific { err } => err.fmt(f), + Self::StreamInvalidated => { + f.write_str("The stream configuration is no longer valid and must be rebuilt.") + } + Self::BufferUnderrun => f.write_str("Buffer underrun/overrun occurred."), + Self::DeviceNotAvailable => f.write_str( + "The requested device is no longer available. For example, it has been unplugged.", + ), + } + } +} + +impl Error for StreamError {} + +impl From for StreamError { + fn from(err: BackendSpecificError) -> Self { + Self::BackendSpecific { err } + } +} diff --git a/vendor/cpal/src/host/aaudio/convert.rs b/vendor/cpal/src/host/aaudio/convert.rs new file mode 100644 index 0000000..7a085fa --- /dev/null +++ b/vendor/cpal/src/host/aaudio/convert.rs @@ -0,0 +1,81 @@ +use std::convert::TryInto; +use std::time::Duration; + +extern crate ndk; + +use crate::{ + BackendSpecificError, BuildStreamError, PauseStreamError, PlayStreamError, StreamError, + StreamInstant, +}; + +pub fn to_stream_instant(duration: Duration) -> StreamInstant { + StreamInstant::new( + duration.as_secs().try_into().unwrap(), + duration.subsec_nanos(), + ) +} + +pub fn stream_instant(stream: &ndk::audio::AudioStream) -> StreamInstant { + let ts = stream + .timestamp(ndk::audio::Clockid::Monotonic) + .unwrap_or(ndk::audio::Timestamp { + frame_position: 0, + time_nanoseconds: 0, + }); + to_stream_instant(Duration::from_nanos(ts.time_nanoseconds as u64)) +} + +impl From for StreamError { + fn from(error: ndk::audio::AudioError) -> Self { + use self::ndk::audio::AudioError::*; + match error { + Disconnected | Unavailable => Self::DeviceNotAvailable, + e => (BackendSpecificError { + description: e.to_string(), + }) + .into(), + } + } +} + +impl From for PlayStreamError { + fn from(error: ndk::audio::AudioError) -> Self { + use self::ndk::audio::AudioError::*; + match error { + Disconnected | Unavailable => Self::DeviceNotAvailable, + e => (BackendSpecificError { + description: e.to_string(), + }) + .into(), + } + } +} + +impl From for PauseStreamError { + fn from(error: ndk::audio::AudioError) -> Self { + use self::ndk::audio::AudioError::*; + match error { + Disconnected | Unavailable => Self::DeviceNotAvailable, + e => (BackendSpecificError { + description: e.to_string(), + }) + .into(), + } + } +} + +impl From for BuildStreamError { + fn from(error: ndk::audio::AudioError) -> Self { + use self::ndk::audio::AudioError::*; + match error { + Disconnected | Unavailable => Self::DeviceNotAvailable, + NoFreeHandles => Self::StreamIdOverflow, + InvalidFormat | InvalidRate => Self::StreamConfigNotSupported, + IllegalArgument => Self::InvalidArgument, + e => (BackendSpecificError { + description: e.to_string(), + }) + .into(), + } + } +} diff --git a/vendor/cpal/src/host/aaudio/java_interface.rs b/vendor/cpal/src/host/aaudio/java_interface.rs new file mode 100644 index 0000000..c12e540 --- /dev/null +++ b/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/vendor/cpal/src/host/aaudio/java_interface/audio_features.rs b/vendor/cpal/src/host/aaudio/java_interface/audio_features.rs new file mode 100644 index 0000000..bbd8809 --- /dev/null +++ b/vendor/cpal/src/host/aaudio/java_interface/audio_features.rs @@ -0,0 +1,56 @@ +use super::{ + utils::{ + get_context, get_package_manager, has_system_feature, with_attached, JNIEnv, JObject, + JResult, + }, + PackageManager, +}; + +/** + * The Android audio features + */ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +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 JNIEnv<'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/vendor/cpal/src/host/aaudio/java_interface/audio_manager.rs b/vendor/cpal/src/host/aaudio/java_interface/audio_manager.rs new file mode 100644 index 0000000..f96d8e2 --- /dev/null +++ b/vendor/cpal/src/host/aaudio/java_interface/audio_manager.rs @@ -0,0 +1,33 @@ +use super::{ + utils::{ + get_context, get_property, get_system_service, with_attached, JNIEnv, JObject, JResult, + }, + AudioManager, Context, +}; + +impl AudioManager { + /// Get the frames per buffer using Android Java API + pub fn get_frames_per_buffer() -> Result { + let context = get_context(); + + with_attached(context, |env, context| get_frames_per_buffer(env, &context)) + .map_err(|error| error.to_string()) + } +} + +fn get_frames_per_buffer<'j>(env: &mut JNIEnv<'j>, context: &JObject<'j>) -> JResult { + let audio_manager = get_system_service(env, context, Context::AUDIO_SERVICE)?; + + let frames_per_buffer = get_property( + env, + &audio_manager, + AudioManager::PROPERTY_OUTPUT_FRAMES_PER_BUFFER, + )?; + + let frames_per_buffer_string = String::from(env.get_string(&frames_per_buffer)?); + + // TODO: Use jni::errors::Error::ParseFailed instead of jni::errors::Error::JniCall once jni > v0.21.1 is released + frames_per_buffer_string + .parse::() + .map_err(|_| jni::errors::Error::JniCall(jni::errors::JniError::Unknown)) +} diff --git a/vendor/cpal/src/host/aaudio/java_interface/definitions.rs b/vendor/cpal/src/host/aaudio/java_interface/definitions.rs new file mode 100644 index 0000000..a10e643 --- /dev/null +++ b/vendor/cpal/src/host/aaudio/java_interface/definitions.rs @@ -0,0 +1,139 @@ +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 PROPERTY_OUTPUT_FRAMES_PER_BUFFER: &'static str = + "android.media.property.OUTPUT_FRAMES_PER_BUFFER"; + + 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: Vec, + + /** + * Supported sample rates + */ + pub sample_rates: Vec, + + /** + * Supported audio formats + */ + pub formats: Vec, +} + +/** + * The type of audio device + */ +#[derive(Debug, Clone, Copy, FromPrimitive)] +#[non_exhaustive] +#[repr(i32)] +pub enum AudioDeviceType { + 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/vendor/cpal/src/host/aaudio/java_interface/devices_info.rs b/vendor/cpal/src/host/aaudio/java_interface/devices_info.rs new file mode 100644 index 0000000..7a9c16b --- /dev/null +++ b/vendor/cpal/src/host/aaudio/java_interface/devices_info.rs @@ -0,0 +1,88 @@ +use num_traits::FromPrimitive; + +use crate::{DeviceDirection, SampleFormat}; + +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, JNIEnv, JObject, JResult, + }, + AudioDeviceInfo, AudioDeviceType, Context, +}; + +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("android/os/Build$VERSION", "SDK_INT", "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 JNIEnv<'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 = env.get_array_length(&devices)?; + + (0..length) + .map(|index| { + let device = env.get_object_array_element(&devices, 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(env.get_string(&address)?); + let product_name = + call_method_no_args_ret_char_sequence(env, &device, "getProductName")?; + let product_name = String::from(env.get_string(&product_name)?); + 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")?; + let sample_rates = call_method_no_args_ret_int_array(env, &device, "getSampleRates")?; + 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/vendor/cpal/src/host/aaudio/java_interface/utils.rs b/vendor/cpal/src/host/aaudio/java_interface/utils.rs new file mode 100644 index 0000000..5671ee6 --- /dev/null +++ b/vendor/cpal/src/host/aaudio/java_interface/utils.rs @@ -0,0 +1,181 @@ +use jni::sys::jobject; +use ndk_context::AndroidContext; +use std::sync::Arc; + +pub use jni::Executor; + +pub use jni::{ + errors::Result as JResult, + objects::{JIntArray, JObject, JObjectArray, JString}, + JNIEnv, JavaVM, +}; + +pub fn get_context() -> AndroidContext { + ndk_context::android_context() +} + +pub fn with_attached(context: AndroidContext, closure: F) -> JResult +where + for<'j> F: FnOnce(&mut JNIEnv<'j>, JObject<'j>) -> JResult, +{ + let vm = Arc::new(unsafe { JavaVM::from_raw(context.vm().cast())? }); + let context = context.context(); + let context = unsafe { JObject::from_raw(context as jobject) }; + Executor::new(vm).with_attached(|env| closure(env, context)) +} + +pub fn call_method_no_args_ret_int_array<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult> { + let array: JIntArray = env.call_method(subject, method, "()[I", &[])?.l()?.into(); + + let length = env.get_array_length(&array)?; + let mut values = Vec::with_capacity(length as usize); + + env.get_int_array_region(array, 0, values.as_mut())?; + + Ok(values) +} + +pub fn call_method_no_args_ret_int<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult { + env.call_method(subject, method, "()I", &[])?.i() +} + +pub fn call_method_no_args_ret_bool<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult { + env.call_method(subject, method, "()Z", &[])?.z() +} + +pub fn call_method_no_args_ret_string<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult> { + Ok(env + .call_method(subject, method, "()Ljava/lang/String;", &[])? + .l()? + .into()) +} + +pub fn call_method_no_args_ret_char_sequence<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, +) -> JResult> { + let cseq = env + .call_method(subject, method, "()Ljava/lang/CharSequence;", &[])? + .l()?; + + Ok(env + .call_method(&cseq, "toString", "()Ljava/lang/String;", &[])? + .l()? + .into()) +} + +pub fn call_method_string_arg_ret_bool<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + name: &str, + arg: impl AsRef, +) -> JResult { + env.call_method( + subject, + name, + "(Ljava/lang/String;)Z", + &[(&env.new_string(arg)?).into()], + )? + .z() +} + +pub fn call_method_string_arg_ret_string<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + name: &str, + arg: impl AsRef, +) -> JResult> { + Ok(env + .call_method( + subject, + name, + "(Ljava/lang/String;)Ljava/lang/String;", + &[(&env.new_string(arg)?).into()], + )? + .l()? + .into()) +} + +pub fn call_method_string_arg_ret_object<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + method: &str, + arg: &str, +) -> JResult> { + env.call_method( + subject, + method, + "(Ljava/lang/String;)Ljava/lang/Object;", + &[(&env.new_string(arg)?).into()], + )? + .l() +} + +pub fn get_package_manager<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, +) -> JResult> { + env.call_method( + subject, + "getPackageManager", + "()Landroid/content/pm/PackageManager;", + &[], + )? + .l() +} + +pub fn has_system_feature<'j>( + env: &mut JNIEnv<'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 JNIEnv<'j>, + subject: &JObject<'j>, + name: &str, +) -> JResult> { + call_method_string_arg_ret_object(env, subject, "getSystemService", name) +} + +pub fn get_property<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + name: &str, +) -> JResult> { + call_method_string_arg_ret_string(env, subject, "getProperty", name) +} + +pub fn get_devices<'j>( + env: &mut JNIEnv<'j>, + subject: &JObject<'j>, + flags: i32, +) -> JResult> { + env.call_method( + subject, + "getDevices", + "(I)[Landroid/media/AudioDeviceInfo;", + &[flags.into()], + )? + .l() + .map(From::from) +} diff --git a/vendor/cpal/src/host/aaudio/mod.rs b/vendor/cpal/src/host/aaudio/mod.rs new file mode 100644 index 0000000..c2afe3b --- /dev/null +++ b/vendor/cpal/src/host/aaudio/mod.rs @@ -0,0 +1,609 @@ +//! AAudio backend implementation. +//! +//! Default backend on Android. + +use std::cmp; +use std::convert::TryInto; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +use std::vec::IntoIter as VecIntoIter; + +extern crate ndk; + +use convert::{stream_instant, to_stream_instant}; +use java_interface::{AudioDeviceInfo, AudioManager}; + +use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crate::{ + BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, + DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, + DeviceNameError, DeviceType, DevicesError, InputCallbackInfo, InputStreamTimestamp, + InterfaceType, OutputCallbackInfo, OutputStreamTimestamp, PauseStreamError, PlayStreamError, + SampleFormat, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, +}; + +mod convert; +mod java_interface; + +use self::ndk::audio::AudioStream; +use java_interface::AudioDeviceType as AndroidDeviceType; + +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, + } + } +} + +// constants from android.media.AudioFormat +const CHANNEL_OUT_MONO: i32 = 4; +const CHANNEL_OUT_STEREO: i32 = 12; + +// Android Java API supports up to 8 channels +// TODO: more channels available in native AAudio +// Maps channel masks to their corresponding channel counts +const CHANNEL_CONFIGS: [(i32, u16); 2] = [(CHANNEL_OUT_MONO, 1), (CHANNEL_OUT_STEREO, 2)]; + +const SAMPLE_RATES: [i32; 15] = [ + 5512, 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000, + 176_400, 192_000, +]; + +pub struct Host; +#[derive(Clone)] +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 enum Stream { + Input(Arc>), + Output(Arc>), +} + +// 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); + +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 { + if let Ok(min_buffer_size) = AudioManager::get_frames_per_buffer() { + SupportedBufferSize::Range { + min: min_buffer_size as u32, + max: i32::MAX as u32, + } + } else { + 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() * CHANNEL_CONFIGS.len() * FORMATS.len()); + for sample_format in &FORMATS { + for (_channel_mask, channel_count) in &CHANNEL_CONFIGS { + for sample_rate in &SAMPLE_RATES { + output.push(SupportedStreamConfigRange { + channels: *channel_count, + min_sample_rate: *sample_rate as u32, + max_sample_rate: *sample_rate as u32, + buffer_size, + sample_format: *sample_format, + }); + } + } + } + + output.into_iter() +} + +fn device_supported_configs(device: &AudioDeviceInfo) -> VecIntoIter { + let sample_rates = if !device.sample_rates.is_empty() { + device.sample_rates.as_slice() + } else { + &SAMPLE_RATES + }; + + const ALL_CHANNELS: [i32; 2] = [1, 2]; + let channel_counts = if !device.channel_counts.is_empty() { + device.channel_counts.as_slice() + } else { + &ALL_CHANNELS + }; + + const ALL_FORMATS: [SampleFormat; 2] = [SampleFormat::I16, SampleFormat::F32]; + let formats = if !device.formats.is_empty() { + device.formats.as_slice() + } 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 { + assert!(*channel_count > 0); + if *channel_count > 2 { + // could be supported by the device + // TODO: more channels available in native AAudio + continue; + } + for format in formats { + output.push(SupportedStreamConfigRange { + channels: cmp::min(*channel_count as u16, 2u16), + min_sample_rate: *sample_rate as u32, + max_sample_rate: *sample_rate as u32, + 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.try_into().unwrap()); + + // Note: Buffer size validation is not needed - the native AAudio API validates buffer sizes + // when `open_stream()` is called. + match &config.buffer_size { + BufferSize::Default => builder, + BufferSize::Fixed(size) => builder + .frames_per_data_callback(*size as i32) + .buffer_capacity_in_frames((*size * 2) as i32), // Double-buffering + } +} + +fn build_input_stream( + device: &Device, + config: &StreamConfig, + mut data_callback: D, + mut error_callback: E, + builder: ndk::audio::AudioStreamBuilder, + sample_format: SampleFormat, +) -> Result +where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, +{ + let builder = configure_for_device(builder, device, config); + let created = Instant::now(); + let channel_count = config.channels as i32; + let stream = builder + .data_callback(Box::new(move |stream, data, num_frames| { + let cb_info = InputCallbackInfo { + timestamp: InputStreamTimestamp { + callback: to_stream_instant(created.elapsed()), + capture: stream_instant(stream), + }, + }; + (data_callback)( + &unsafe { + Data::from_parts( + data as *mut _, + (num_frames * channel_count).try_into().unwrap(), + sample_format, + ) + }, + &cb_info, + ); + ndk::audio::AudioCallbackResult::Continue + })) + .error_callback(Box::new(move |_stream, error| { + (error_callback)(StreamError::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::Input(Arc::new(Mutex::new(stream)))) +} + +fn build_output_stream( + device: &Device, + config: &StreamConfig, + mut data_callback: D, + mut error_callback: E, + builder: ndk::audio::AudioStreamBuilder, + sample_format: SampleFormat, +) -> Result +where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, +{ + let builder = configure_for_device(builder, device, config); + let created = Instant::now(); + let channel_count = config.channels as i32; + let stream = builder + .data_callback(Box::new(move |stream, data, num_frames| { + let cb_info = OutputCallbackInfo { + timestamp: OutputStreamTimestamp { + callback: to_stream_instant(created.elapsed()), + playback: stream_instant(stream), + }, + }; + (data_callback)( + &mut unsafe { + Data::from_parts( + data as *mut _, + (num_frames * channel_count).try_into().unwrap(), + sample_format, + ) + }, + &cb_info, + ); + ndk::audio::AudioCallbackResult::Continue + })) + .error_callback(Box::new(move |_stream, error| { + (error_callback)(StreamError::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::Output(Arc::new(Mutex::new(stream)))) +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn name(&self) -> Result { + match &self.0 { + None => Ok("default".to_string()), + Some(info) => { + let name = if info.address.is_empty() { + format!("{}:{:?}", info.product_name, info.device_type) + } else { + format!( + "{}:{:?}:{}", + info.product_name, info.device_type, info.address + ) + }; + Ok(name) + } + } + } + + fn description(&self) -> Result { + match &self.0 { + None => Ok(DeviceDescriptionBuilder::new("Default Device".to_string()).build()), + Some(info) => { + let mut builder = DeviceDescriptionBuilder::new(info.product_name.clone()) + .device_type(info.device_type.into()) + .interface_type(info.device_type.into()) + .direction(info.direction); + + // Add address if not empty + if !info.address.is_empty() { + builder = builder.address(info.address.clone()); + } + + 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(crate::platform::HostId::AAudio, device_str)) + } + + fn supported_input_configs( + &self, + ) -> Result { + let configs = if let Some(info) = &self.0 { + device_supported_configs(info) + } else { + default_supported_configs() + }; + Ok(configs) + } + + fn supported_output_configs( + &self, + ) -> Result { + let configs = if let Some(info) = &self.0 { + device_supported_configs(info) + } else { + default_supported_configs() + }; + Ok(configs) + } + + fn default_input_config(&self) -> Result { + let mut configs: Vec<_> = self.supported_input_configs().unwrap().collect(); + configs.sort_by(|a, b| b.cmp_default_heuristics(a)); + let config = configs + .into_iter() + .next() + .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)? + .with_max_sample_rate(); + Ok(config) + } + + fn default_output_config(&self) -> Result { + let mut configs: Vec<_> = self.supported_output_configs().unwrap().collect(); + configs.sort_by(|a, b| b.cmp_default_heuristics(a)); + let config = configs + .into_iter() + .next() + .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)? + .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(StreamError) + Send + 'static, + { + let format = match sample_format { + SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16, + SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float, + sample_format => { + return Err(BackendSpecificError { + description: format!("{} format is not supported on Android.", sample_format), + } + .into()) + } + }; + let channel_count = match config.channels { + 1 => 1, + 2 => 2, + channels => { + // TODO: more channels available in native AAudio + return Err(BackendSpecificError { + description: format!( + "{} channels are not supported yet (only 1 or 2).", + channels + ), + } + .into()); + } + }; + + let builder = ndk::audio::AudioStreamBuilder::new()? + .direction(ndk::audio::AudioDirection::Input) + .channel_count(channel_count) + .format(format); + + 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(StreamError) + Send + 'static, + { + let format = match sample_format { + SampleFormat::I16 => ndk::audio::AudioFormat::PCM_I16, + SampleFormat::F32 => ndk::audio::AudioFormat::PCM_Float, + sample_format => { + return Err(BackendSpecificError { + description: format!("{} format is not supported on Android.", sample_format), + } + .into()) + } + }; + let channel_count = match config.channels { + 1 => 1, + 2 => 2, + channels => { + // TODO: more channels available in native AAudio + return Err(BackendSpecificError { + description: format!( + "{} channels are not supported yet (only 1 or 2).", + channels + ), + } + .into()); + } + }; + + let builder = ndk::audio::AudioStreamBuilder::new()? + .direction(ndk::audio::AudioDirection::Output) + .channel_count(channel_count) + .format(format); + + build_output_stream( + self, + config, + data_callback, + error_callback, + builder, + sample_format, + ) + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + match self { + Self::Input(stream) => stream + .lock() + .unwrap() + .request_start() + .map_err(PlayStreamError::from), + Self::Output(stream) => stream + .lock() + .unwrap() + .request_start() + .map_err(PlayStreamError::from), + } + } + + fn pause(&self) -> Result<(), PauseStreamError> { + match self { + Self::Input(_) => Err(BackendSpecificError { + description: "Pause called on the input stream.".to_owned(), + } + .into()), + Self::Output(stream) => stream + .lock() + .unwrap() + .request_pause() + .map_err(PauseStreamError::from), + } + } +} diff --git a/vendor/cpal/src/host/alsa/enumerate.rs b/vendor/cpal/src/host/alsa/enumerate.rs new file mode 100644 index 0000000..0b880df --- /dev/null +++ b/vendor/cpal/src/host/alsa/enumerate.rs @@ -0,0 +1,164 @@ +use std::collections::HashSet; + +use super::{alsa, Device, Host}; +use crate::{BackendSpecificError, DeviceDirection, DevicesError}; + +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); + let device = Device { + pcm_id, + desc: hint.desc, + direction, + _context: self.inner.clone(), + }; + + seen_pcm_ids.insert(device.pcm_id.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_string())); + + 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_string()), + ), + (Some(p_info), None) => ( + DeviceDirection::Output, + p_info.get_name().ok().map(|s| s.to_string()), + ), + (None, Some(c_info)) => ( + DeviceDirection::Input, + c_info.get_name().ok().map(|s| s.to_string()), + ), + (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 DevicesError { + fn from(err: alsa::Error) -> Self { + let err: BackendSpecificError = err.into(); + err.into() + } +} + +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/vendor/cpal/src/host/alsa/mod.rs b/vendor/cpal/src/host/alsa/mod.rs new file mode 100644 index 0000000..849a178 --- /dev/null +++ b/vendor/cpal/src/host/alsa/mod.rs @@ -0,0 +1,1505 @@ +//! ALSA backend implementation. +//! +//! Default backend on Linux and BSD systems. + +extern crate alsa; +extern crate libc; + +use std::{ + cmp, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, + }, + thread::{self, JoinHandle}, + time::Duration, + vec::IntoIter as VecIntoIter, +}; + +use self::alsa::poll::Descriptors; +pub use self::enumerate::Devices; + +use crate::{ + iter::{SupportedInputConfigs, SupportedOutputConfigs}, + traits::{DeviceTrait, HostTrait, StreamTrait}, + BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, + DefaultStreamConfigError, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, + DeviceId, DeviceIdError, DeviceNameError, DevicesError, FrameCount, InputCallbackInfo, + OutputCallbackInfo, PauseStreamError, PlayStreamError, Sample, SampleFormat, SampleRate, + StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, I24, U24, +}; + +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"; + +// 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(|_| crate::HostUnavailable)?; + Ok(Host { + inner: Arc::new(inner), + }) + } +} + +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 default_input_device(&self) -> Option { + Some(Device::default()) + } + + fn default_output_device(&self) -> Option { + Some(Device::default()) + } +} + +/// Global count of active ALSA context instances. +static ALSA_CONTEXT_COUNT: AtomicUsize = AtomicUsize::new(0); + +/// ALSA backend context shared between `Host`, `Device`, and `Stream` via `Arc`. +#[derive(Debug)] +pub(super) struct AlsaContext; + +impl AlsaContext { + fn new() -> Result { + // Initialize global ALSA config cache on first context creation. + if ALSA_CONTEXT_COUNT.fetch_add(1, Ordering::SeqCst) == 0 { + alsa::config::update()?; + } + Ok(Self) + } +} + +impl Drop for AlsaContext { + fn drop(&mut self) { + // Free the global ALSA config cache when the last context is dropped. + if ALSA_CONTEXT_COUNT.fetch_sub(1, Ordering::SeqCst) == 1 { + let _ = alsa::config::update_free_global(); + } + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + // ALSA overrides name() to return pcm_id directly instead of from description + fn name(&self) -> Result { + Device::name(self) + } + + fn description(&self) -> Result { + Device::description(self) + } + + fn id(&self) -> Result { + Device::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 { + 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, + conf: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + 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(StreamError) + Send + 'static, + { + 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; + let ret = unsafe { libc::write(self.0, &buf as *const u64 as *const _, 8) }; + assert_eq!(ret, 8); + } +} + +impl TriggerReceiver { + fn clear_pipe(&self) { + let mut out = 0u64; + let ret = unsafe { libc::read(self.0, &mut out as *mut u64 as *mut _, 8) }; + assert_eq!(ret, 8); + } +} + +fn trigger() -> (TriggerSender, TriggerReceiver) { + let mut fds = [0, 0]; + match unsafe { libc::pipe(fds.as_mut_ptr()) } { + 0 => (TriggerSender(fds[1]), 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 PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + self.pcm_id == other.pcm_id + } +} + +impl Eq for Device {} + +impl std::hash::Hash for Device { + fn hash(&self, state: &mut H) { + self.pcm_id.hash(state); + } +} + +impl Device { + fn build_stream_inner( + &self, + conf: &StreamConfig, + sample_format: SampleFormat, + stream_type: alsa::Direction, + ) -> Result { + // Validate buffer size if Fixed is specified. This is necessary because + // `set_period_size_near()` with `ValueOr::Nearest` will accept ANY value and return the + // "nearest" supported value, which could be wildly different (e.g., requesting 4096 frames + // might return 512 frames if that's "nearest"). + if let BufferSize::Fixed(requested_size) = conf.buffer_size { + // Note: We use `default_input_config`/`default_output_config` to get the buffer size + // range. This queries the CURRENT device (`self.pcm_id`), not the default device. The + // buffer size range is the same across all format configurations for a given device + // (see `supported_configs()`). + let supported_config = match stream_type { + alsa::Direction::Capture => self.default_input_config(), + alsa::Direction::Playback => self.default_output_config(), + }; + if let Ok(config) = supported_config { + if let SupportedBufferSize::Range { min, max } = config.buffer_size { + if !(min..=max).contains(&requested_size) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + } + } + } + + let handle = match alsa::pcm::PCM::new(&self.pcm_id, stream_type, true) + .map_err(|e| (e, e.errno())) + { + Err((_, libc::ENOENT)) + | Err((_, libc::EPERM)) + | Err((_, libc::ENODEV)) + | Err((_, LIBC_ENOTSUPP)) + | Err((_, libc::EBUSY)) + | Err((_, libc::EAGAIN)) => return Err(BuildStreamError::DeviceNotAvailable), + Err((_, libc::EINVAL)) => return Err(BuildStreamError::InvalidArgument), + Err((e, _)) => return Err(e.into()), + Ok(handle) => handle, + }; + + let can_pause = set_hw_params_from_format(&handle, conf, sample_format)?; + let period_samples = set_sw_params_from_format(&handle, conf, stream_type)?; + + handle.prepare()?; + + let num_descriptors = handle.count(); + if num_descriptors == 0 { + let description = "poll descriptor count for stream was 0".to_string(); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + + // Check to see if we can retrieve valid timestamps from the device. + // Related: https://bugs.freedesktop.org/show_bug.cgi?id=88503 + let ts = handle.status()?.get_htstamp(); + let creation_instant = match (ts.tv_sec, ts.tv_nsec) { + (0, 0) => Some(std::time::Instant::now()), + _ => None, + }; + + if let alsa::Direction::Capture = stream_type { + handle.start()?; + } + + // Pre-compute a period-sized buffer filled with silence values. + let period_frames = period_samples / conf.channels as usize; + let period_bytes = period_samples * sample_format.sample_size(); + let mut silence_template = vec![0u8; period_bytes].into_boxed_slice(); + + // Only fill buffer for unsigned formats that don't have a zero value for silence. + if sample_format.is_uint() { + fill_with_equilibrium(&mut silence_template, sample_format); + } + + let stream_inner = StreamInner { + dropping: AtomicBool::new(false), + channel: handle, + sample_format, + num_descriptors, + conf: conf.clone(), + period_samples, + period_frames, + silence_template, + can_pause, + creation_instant, + _context: self._context.clone(), + }; + + Ok(stream_inner) + } + + fn name(&self) -> Result { + Ok(self.pcm_id.clone()) + } + + fn description(&self) -> Result { + let name = self + .desc + .as_ref() + .and_then(|desc| desc.lines().next()) + .unwrap_or(&self.pcm_id) + .to_string(); + + let mut builder = DeviceDescriptionBuilder::new(name) + .driver(self.pcm_id.clone()) + .direction(self.direction); + + if let Some(ref desc) = self.desc { + let lines = desc + .lines() + .map(|line| line.trim().to_string()) + .filter(|line| !line.is_empty()) + .collect(); + builder = builder.extended(lines); + } + + Ok(builder.build()) + } + + fn id(&self) -> Result { + Ok(DeviceId(crate::platform::HostId::Alsa, self.pcm_id.clone())) + } + + fn supported_configs( + &self, + stream_t: alsa::Direction, + ) -> Result, SupportedStreamConfigsError> { + let pcm = + match alsa::pcm::PCM::new(&self.pcm_id, stream_t, true).map_err(|e| (e, e.errno())) { + Err((_, libc::ENOENT)) + | Err((_, libc::EPERM)) + | Err((_, libc::ENODEV)) + | Err((_, LIBC_ENOTSUPP)) + | Err((_, libc::EBUSY)) + | Err((_, libc::EAGAIN)) => { + return Err(SupportedStreamConfigsError::DeviceNotAvailable) + } + Err((_, libc::EINVAL)) => return Err(SupportedStreamConfigsError::InvalidArgument), + Err((e, _)) => return Err(e.into()), + Ok(pcm) => pcm, + }; + + 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, + ]; + + // Collect supported formats, deduplicating since we test both LE and BE variants. + // If hardware supports both endiannesses (rare), we only report the format once. + let mut supported_formats = Vec::new(); + for &(sample_format, alsa_format) in FORMATS.iter() { + if hw_params.test_format(alsa_format).is_ok() + && !supported_formats.contains(&sample_format) + { + supported_formats.push(sample_format); + } + } + + 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() { + vec![(min_rate, max_rate)] + } else { + let mut rates = Vec::new(); + for &sample_rate in crate::COMMON_SAMPLE_RATES.iter() { + if hw_params.test_rate(sample_rate).is_ok() { + rates.push((sample_rate, sample_rate)); + } + } + + if rates.is_empty() { + vec![(min_rate, max_rate)] + } else { + rates + } + }; + + let min_channels = hw_params.get_channels_min()?; + let max_channels = hw_params.get_channels_max()?; + + let max_channels = cmp::min(max_channels, 32); // TODO: limiting to 32 channels or too much stuff is returned + let supported_channels = (min_channels..max_channels + 1) + .filter_map(|num| { + if hw_params.test_channels(num).is_ok() { + Some(num as ChannelCount) + } else { + None + } + }) + .collect::>(); + + let (min_buffer_size, max_buffer_size) = hw_params_buffer_size_min_max(&hw_params); + let buffer_size_range = SupportedBufferSize::Range { + min: min_buffer_size, + max: max_buffer_size, + }; + + let mut output = Vec::with_capacity( + supported_formats.len() * supported_channels.len() * sample_rates.len(), + ); + for &sample_format in supported_formats.iter() { + for &channels in supported_channels.iter() { + 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: buffer_size_range, + 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<_> = { + match self.supported_configs(stream_t) { + Err(SupportedStreamConfigsError::DeviceNotAvailable) => { + return Err(DefaultStreamConfigError::DeviceNotAvailable); + } + Err(SupportedStreamConfigsError::InvalidArgument) => { + // this happens sometimes when querying for input and output capabilities, but + // the device supports only one + return Err(DefaultStreamConfigError::StreamTypeNotSupported); + } + Err(SupportedStreamConfigsError::BackendSpecific { err }) => { + return Err(err.into()); + } + Ok(fmts) => fmts.collect(), + } + }; + + formats.sort_by(|a, b| a.cmp_default_heuristics(b)); + + match formats.into_iter().next_back() { + Some(f) => { + let min_r = f.min_sample_rate; + let max_r = f.max_sample_rate; + let mut format = f.with_max_sample_rate(); + const HZ_44100: SampleRate = 44_100; + if min_r <= HZ_44100 && HZ_44100 <= max_r { + format.sample_rate = HZ_44100; + } + Ok(format) + } + None => Err(DefaultStreamConfigError::StreamTypeNotSupported), + } + } + + 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 Default for Device { + fn default() -> Self { + // "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. + Self { + pcm_id: DEFAULT_DEVICE.to_owned(), + desc: Some("Default Audio Device".to_string()), + direction: DeviceDirection::Unknown, + _context: Arc::new( + AlsaContext::new().expect("Failed to initialize ALSA configuration"), + ), + } + } +} + +#[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, + + // The ALSA channel. + channel: alsa::pcm::PCM, + + // When converting between file descriptors and `snd_pcm_t`, this is the number of + // file descriptors that this `snd_pcm_t` uses. + num_descriptors: usize, + + // Format of the samples. + sample_format: SampleFormat, + + // The configuration used to open this stream. + conf: StreamConfig, + + // Cached values for performance in audio callback hot path + period_samples: usize, + period_frames: usize, + silence_template: Box<[u8]>, + + #[allow(dead_code)] + // Whether or not the hardware supports pausing the stream. + // TODO: We need an API to expose this. See #197, #284. + can_pause: bool, + + // In the case that the device does not return valid timestamps via `get_htstamp`, this field + // will be `Some` and will contain an `Instant` representing the moment the stream was created. + // + // If this field is `Some`, then the stream will use the duration since this instant as a + // source for timestamps. + // + // If this field is `None` then the elapsed duration between `get_trigger_htstamp` and + // `get_htstamp` is used. + creation_instant: Option, + + // 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, +} + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +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 { + d.as_millis().try_into().unwrap() + } else { + -1 // Don't timeout, wait forever. + }; + + // Pre-allocate buffer to exactly one period size with proper equilibrium values. + let transfer_buffer = stream.silence_template.clone(); + + // Pre-allocate and initialize descriptors vector: 1 for self-pipe + stream.num_descriptors + // for ALSA. 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 total_descriptors = 1 + stream.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 + .channel + .fill(&mut descriptors[1..]) + .expect("Failed to fill ALSA descriptors"); + debug_assert_eq!(filled, stream.num_descriptors); + + Self { + descriptors, + transfer_buffer, + poll_timeout, + } + } +} + +fn input_stream_worker( + rx: TriggerReceiver, + stream: &StreamInner, + data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static), + error_callback: &mut (dyn FnMut(StreamError) + Send + 'static), + timeout: Option, +) { + boost_current_thread_priority(stream.conf.buffer_size, stream.conf.sample_rate); + + let mut ctxt = StreamWorkerContext::new(&timeout, stream, &rx); + loop { + let flow = + poll_descriptors_and_prepare_buffer(&rx, stream, &mut ctxt).unwrap_or_else(|err| { + error_callback(err.into()); + PollDescriptorsFlow::Continue + }); + + match flow { + PollDescriptorsFlow::Continue => { + continue; + } + PollDescriptorsFlow::XRun => { + error_callback(StreamError::BufferUnderrun); + if let Err(err) = stream.channel.prepare() { + error_callback(err.into()); + } + continue; + } + PollDescriptorsFlow::Return => return, + PollDescriptorsFlow::Ready { + status, + delay_frames, + } => { + if let Err(err) = process_input( + stream, + &mut ctxt.transfer_buffer, + status, + delay_frames, + data_callback, + ) { + error_callback(err.into()); + } + } + } + } +} + +fn output_stream_worker( + rx: TriggerReceiver, + stream: &StreamInner, + data_callback: &mut (dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static), + error_callback: &mut (dyn FnMut(StreamError) + Send + 'static), + timeout: Option, +) { + boost_current_thread_priority(stream.conf.buffer_size, stream.conf.sample_rate); + + let mut ctxt = StreamWorkerContext::new(&timeout, stream, &rx); + + loop { + let flow = + poll_descriptors_and_prepare_buffer(&rx, stream, &mut ctxt).unwrap_or_else(|err| { + error_callback(err.into()); + PollDescriptorsFlow::Continue + }); + + match flow { + PollDescriptorsFlow::Continue => continue, + PollDescriptorsFlow::XRun => { + error_callback(StreamError::BufferUnderrun); + if let Err(err) = stream.channel.prepare() { + error_callback(err.into()); + } + continue; + } + PollDescriptorsFlow::Return => return, + PollDescriptorsFlow::Ready { + status, + delay_frames, + } => { + if let Err(err) = process_output( + stream, + &mut ctxt.transfer_buffer, + status, + delay_frames, + data_callback, + error_callback, + ) { + error_callback(err.into()); + } + } + } + } +} + +#[cfg(feature = "audio_thread_priority")] +fn boost_current_thread_priority(buffer_size: BufferSize, sample_rate: SampleRate) { + use audio_thread_priority::promote_current_thread_to_real_time; + + let buffer_size = if let BufferSize::Fixed(buffer_size) = buffer_size { + buffer_size + } else { + // if the buffer size isn't fixed, let audio_thread_priority choose a sensible default value + 0 + }; + + if let Err(err) = promote_current_thread_to_real_time(buffer_size, sample_rate) { + eprintln!("Failed to promote audio thread to real-time priority: {err}"); + } +} + +#[cfg(not(feature = "audio_thread_priority"))] +fn boost_current_thread_priority(_: BufferSize, _: SampleRate) {} + +enum PollDescriptorsFlow { + Continue, + Return, + Ready { + status: alsa::pcm::Status, + delay_frames: usize, + }, + XRun, +} + +// This block is shared between both input and output stream worker functions. +fn poll_descriptors_and_prepare_buffer( + rx: &TriggerReceiver, + stream: &StreamInner, + ctxt: &mut StreamWorkerContext, +) -> Result { + if stream.dropping.load(Ordering::Acquire) { + // The stream has been requested to be destroyed. + rx.clear_pipe(); + return Ok(PollDescriptorsFlow::Return); + } + + let StreamWorkerContext { + ref mut descriptors, + ref poll_timeout, + .. + } = *ctxt; + + let res = alsa::poll::poll(descriptors, *poll_timeout)?; + if res == 0 { + let description = String::from("`alsa::poll()` spuriously returned"); + return Err(BackendSpecificError { description }); + } + + if descriptors[0].revents != 0 { + // The stream has been requested to be destroyed. + rx.clear_pipe(); + return Ok(PollDescriptorsFlow::Return); + } + + let revents = stream.channel.revents(&descriptors[1..])?; + if revents.contains(alsa::poll::Flags::ERR) { + let description = String::from("`alsa::poll()` returned POLLERR"); + return Err(BackendSpecificError { description }); + } + + // Check if data is ready for processing (either input or output) + if !revents.contains(alsa::poll::Flags::IN) && !revents.contains(alsa::poll::Flags::OUT) { + // Nothing to process, poll again + return Ok(PollDescriptorsFlow::Continue); + } + + let status = stream.channel.status()?; + let avail_frames = match stream.channel.avail() { + Err(err) if err.errno() == libc::EPIPE => return Ok(PollDescriptorsFlow::XRun), + res => res, + }? as usize; + let delay_frames = match status.get_delay() { + // Buffer underrun detected, but notification happens in XRun handler + d if d < 0 => 0, + d => d as usize, + }; + let available_samples = avail_frames * stream.conf.channels as usize; + + // 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 + if available_samples < stream.period_samples { + return Ok(PollDescriptorsFlow::Continue); + } + + Ok(PollDescriptorsFlow::Ready { + status, + delay_frames, + }) +} + +// 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<(), BackendSpecificError> { + stream.channel.io_bytes().readi(buffer)?; + let data = buffer.as_mut_ptr() as *mut (); + let data = unsafe { Data::from_parts(data, stream.period_samples, stream.sample_format) }; + let callback = match stream.creation_instant { + None => stream_timestamp_hardware(&status)?, + Some(creation) => stream_timestamp_fallback(creation)?, + }; + let delay_duration = frames_to_duration(delay_frames, stream.conf.sample_rate); + let capture = callback + .sub(delay_duration) + .ok_or_else(|| BackendSpecificError { + description: "`capture` is earlier than representation supported by `StreamInstant`" + .to_string(), + })?; + let timestamp = crate::InputStreamTimestamp { callback, capture }; + let info = crate::InputCallbackInfo { timestamp }; + data_callback(&data, &info); + + Ok(()) +} + +// Request data from the user's function and write it via ALSA. +// +// Returns `true` +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), + error_callback: &mut dyn FnMut(StreamError), +) -> Result<(), BackendSpecificError> { + // Buffer is always pre-filled with equilibrium, user overwrites what they want + buffer.copy_from_slice(&stream.silence_template); + { + let data = buffer.as_mut_ptr() as *mut (); + let mut data = + unsafe { Data::from_parts(data, stream.period_samples, stream.sample_format) }; + let callback = match stream.creation_instant { + None => stream_timestamp_hardware(&status)?, + Some(creation) => stream_timestamp_fallback(creation)?, + }; + let delay_duration = frames_to_duration(delay_frames, stream.conf.sample_rate); + let playback = callback + .add(delay_duration) + .ok_or_else(|| BackendSpecificError { + description: "`playback` occurs beyond representation supported by `StreamInstant`" + .to_string(), + })?; + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + let info = crate::OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); + } + + loop { + match stream.channel.io_bytes().writei(buffer) { + Err(err) if err.errno() == libc::EPIPE => { + // ALSA underrun or overrun. + // See https://github.com/alsa-project/alsa-lib/blob/b154d9145f0e17b9650e4584ddfdf14580b4e0d7/src/pcm/pcm.c#L8767-L8770 + // Even if these recover successfully, they still may cause audible glitches. + + error_callback(StreamError::BufferUnderrun); + if let Err(recover_err) = stream.channel.try_recover(err, true) { + error_callback(recover_err.into()); + } + } + Err(err) => { + error_callback(err.into()); + continue; + } + Ok(result) if result != stream.period_frames => { + let description = format!( + "unexpected number of frames written: expected {}, \ + result {result} (this should never happen)", + stream.period_frames + ); + error_callback(BackendSpecificError { description }.into()); + continue; + } + _ => { + break; + } + } + } + Ok(()) +} + +// Use hardware timestamps from ALSA. +// +// This ensures accurate timestamps based on actual hardware timing. +#[inline] +fn stream_timestamp_hardware( + status: &alsa::pcm::Status, +) -> Result { + let trigger_ts = status.get_trigger_htstamp(); + let ts = status.get_htstamp(); + let nanos = timespec_diff_nanos(ts, trigger_ts); + if nanos < 0 { + let description = format!( + "get_htstamp `{}.{}` was earlier than get_trigger_htstamp `{}.{}`", + ts.tv_sec, ts.tv_nsec, trigger_ts.tv_sec, trigger_ts.tv_nsec + ); + return Err(BackendSpecificError { description }); + } + Ok(crate::StreamInstant::from_nanos(nanos)) +} + +// Use elapsed duration since stream creation as fallback when hardware timestamps are unavailable. +// +// This ensures positive values that are compatible with our `StreamInstant` representation. +#[inline] +fn stream_timestamp_fallback( + creation: std::time::Instant, +) -> Result { + let now = std::time::Instant::now(); + let duration = now.duration_since(creation); + crate::StreamInstant::from_nanos_i128(duration.as_nanos() as i128).ok_or(BackendSpecificError { + description: "stream duration has exceeded `StreamInstant` representation".to_string(), + }) +} + +// 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) +} + +// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +#[inline] +fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { + let secsf = frames as f64 / rate as f64; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} + +impl Stream { + 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(StreamError) + Send + 'static, + { + let (tx, rx) = trigger(); + // Clone the handle for passing into worker thread. + let stream = inner.clone(); + let thread = thread::Builder::new() + .name("cpal_alsa_in".to_owned()) + .spawn(move || { + input_stream_worker( + rx, + &stream, + &mut data_callback, + &mut error_callback, + timeout, + ); + }) + .unwrap(); + Self { + thread: Some(thread), + inner, + trigger: tx, + } + } + + 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(StreamError) + Send + 'static, + { + let (tx, rx) = trigger(); + // Clone the handle for passing into worker thread. + let stream = inner.clone(); + let thread = thread::Builder::new() + .name("cpal_alsa_out".to_owned()) + .spawn(move || { + output_stream_worker( + rx, + &stream, + &mut data_callback, + &mut error_callback, + timeout, + ); + }) + .unwrap(); + Self { + thread: Some(thread), + inner, + trigger: tx, + } + } +} + +impl Drop for Stream { + fn drop(&mut self) { + 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<(), PlayStreamError> { + self.inner.channel.pause(false).ok(); + Ok(()) + } + fn pause(&self) -> Result<(), PauseStreamError> { + self.inner.channel.pause(true).ok(); + Ok(()) + } +} + +// Convert ALSA frames to FrameCount, clamping to valid range. +// ALSA Frames are i64 (64-bit) or i32 (32-bit). +fn clamp_frame_count(buffer_size: alsa::pcm::Frames) -> FrameCount { + buffer_size.max(1).try_into().unwrap_or(FrameCount::MAX) +} + +fn hw_params_buffer_size_min_max(hw_params: &alsa::pcm::HwParams) -> (FrameCount, FrameCount) { + let min_buf = hw_params + .get_buffer_size_min() + .map(clamp_frame_count) + .unwrap_or(1); + let max_buf = hw_params + .get_buffer_size_max() + .map(clamp_frame_count) + .unwrap_or(FrameCount::MAX); + (min_buf, max_buf) +} + +// Fill a buffer with equilibrium values for any sample format. +// Works with any buffer size, even if not perfectly aligned to sample boundaries. +fn fill_with_equilibrium(buffer: &mut [u8], sample_format: SampleFormat) { + macro_rules! fill_typed { + ($sample_type:ty) => {{ + let sample_size = std::mem::size_of::<$sample_type>(); + + 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: We verified the buffer size is correctly aligned for the sample type + 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; + } + }}; + } + const DSD_SILENCE_BYTE: u8 = 0x69; + + match sample_format { + SampleFormat::I8 => fill_typed!(i8), + SampleFormat::I16 => fill_typed!(i16), + SampleFormat::I24 => fill_typed!(I24), + SampleFormat::I32 => fill_typed!(i32), + // SampleFormat::I48 => fill_typed!(I48), + SampleFormat::I64 => fill_typed!(i64), + SampleFormat::U8 => fill_typed!(u8), + SampleFormat::U16 => fill_typed!(u16), + SampleFormat::U24 => fill_typed!(U24), + SampleFormat::U32 => fill_typed!(u32), + // SampleFormat::U48 => fill_typed!(U48), + SampleFormat::U64 => fill_typed!(u64), + SampleFormat::F32 => fill_typed!(f32), + SampleFormat::F64 => fill_typed!(f64), + SampleFormat::DsdU8 | SampleFormat::DsdU16 | SampleFormat::DsdU32 => { + buffer.fill(DSD_SILENCE_BYTE) + } + } +} + +fn init_hw_params<'a>( + pcm_handle: &'a alsa::pcm::PCM, + config: &StreamConfig, + sample_format: SampleFormat, +) -> Result, BackendSpecificError> { + 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(BackendSpecificError { + description: 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(BackendSpecificError { + description: format!( + "Sample format '{sample_format}' is not supported by hardware in any endianness" + ), + }) +} + +fn set_hw_params_from_format( + pcm_handle: &alsa::pcm::PCM, + config: &StreamConfig, + sample_format: SampleFormat, +) -> Result { + 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(buffer_frames) = config.buffer_size { + hw_params.set_buffer_size_near((2 * buffer_frames) as alsa::pcm::Frames)?; + hw_params + .set_period_size_near(buffer_frames as alsa::pcm::Frames, 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) = 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, alsa::ValueOr::Nearest)?; + hw_params.set_buffer_size_near(2 * period)?; + + // Re-apply with new constraints + pcm_handle.hw_params(&hw_params)?; + } + } + + Ok(hw_params.can_pause()) +} + +fn set_sw_params_from_format( + pcm_handle: &alsa::pcm::PCM, + config: &StreamConfig, + stream_type: alsa::Direction, +) -> Result { + let sw_params = pcm_handle.sw_params_current()?; + + let period_samples = { + let (buffer, period) = pcm_handle.get_params()?; + if buffer == 0 { + return Err(BackendSpecificError { + description: "initialization resulted in a null buffer".to_string(), + }); + } + 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). + 2 * period + } + alsa::Direction::Capture => 1, + }; + sw_params.set_start_threshold(start_threshold as alsa::pcm::Frames)?; + sw_params.set_avail_min(period as alsa::pcm::Frames)?; + + period as usize * config.channels as usize + }; + + 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(period_samples) +} + +impl From for BackendSpecificError { + fn from(err: alsa::Error) -> Self { + Self { + description: err.to_string(), + } + } +} + +impl From for BuildStreamError { + fn from(err: alsa::Error) -> Self { + let err: BackendSpecificError = err.into(); + err.into() + } +} + +impl From for SupportedStreamConfigsError { + fn from(err: alsa::Error) -> Self { + let err: BackendSpecificError = err.into(); + err.into() + } +} + +impl From for PlayStreamError { + fn from(err: alsa::Error) -> Self { + let err: BackendSpecificError = err.into(); + err.into() + } +} + +impl From for PauseStreamError { + fn from(err: alsa::Error) -> Self { + let err: BackendSpecificError = err.into(); + err.into() + } +} + +impl From for StreamError { + fn from(err: alsa::Error) -> Self { + let err: BackendSpecificError = err.into(); + err.into() + } +} diff --git a/vendor/cpal/src/host/asio/device.rs b/vendor/cpal/src/host/asio/device.rs new file mode 100644 index 0000000..69f7fd1 --- /dev/null +++ b/vendor/cpal/src/host/asio/device.rs @@ -0,0 +1,258 @@ +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; + +use super::sys; +use crate::BackendSpecificError; +use crate::ChannelCount; +use crate::DefaultStreamConfigError; +use crate::DeviceDescription; +use crate::DeviceDescriptionBuilder; +use crate::DeviceId; +use crate::DeviceIdError; +use crate::DeviceNameError; +use crate::DevicesError; +use crate::SampleFormat; +use crate::SupportedBufferSize; +use crate::SupportedStreamConfig; +use crate::SupportedStreamConfigRange; +use crate::SupportedStreamConfigsError; + +use std::hash::{Hash, Hasher}; +use std::sync::atomic::AtomicU32; +use std::sync::{Arc, Mutex}; + +/// A ASIO Device +#[derive(Clone)] +pub struct Device { + /// The driver represented by this device. + pub driver: Arc, + + // Input and/or Output stream. + // A driver can only have one of each. + // They need to be created at the same time. + pub asio_streams: Arc>, + pub current_callback_flag: Arc, +} + +/// All available devices. +pub struct Devices { + asio: Arc, + drivers: std::vec::IntoIter, +} + +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + self.driver.name() == other.driver.name() + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash(&self, state: &mut H) { + self.driver.name().hash(state); + } +} + +impl Device { + pub fn description(&self) -> Result { + let driver_name = self.driver.name().to_string(); + + let direction = crate::device_description::direction_from_counts( + self.driver.channels().ok().map(|c| c.ins as ChannelCount), + self.driver.channels().ok().map(|c| c.outs as ChannelCount), + ); + + Ok(DeviceDescriptionBuilder::new(driver_name.clone()) + .driver(driver_name) + .direction(direction) + .build()) + } + + pub fn id(&self) -> Result { + Ok(DeviceId( + crate::platform::HostId::Asio, + self.driver.name().to_string(), + )) + } + + /// Gets the supported input configs. + /// TODO currently only supports the default. + /// Need to find all possible configs. + pub fn supported_input_configs( + &self, + ) -> Result { + // Retrieve the default config for the total supported channels and supported sample + // format. + let f = match self.default_input_config() { + Err(_) => return Err(SupportedStreamConfigsError::DeviceNotAvailable), + Ok(f) => f, + }; + + // Collect a config for every combination of supported sample rate and number of channels. + let mut supported_configs = vec![]; + for &rate in crate::COMMON_SAMPLE_RATES { + if !self + .driver + .can_sample_rate(rate.into()) + .ok() + .unwrap_or(false) + { + continue; + } + for channels in 1..f.channels + 1 { + supported_configs.push(SupportedStreamConfigRange { + channels, + min_sample_rate: rate, + max_sample_rate: rate, + buffer_size: f.buffer_size, + sample_format: f.sample_format, + }) + } + } + Ok(supported_configs.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 { + // Retrieve the default config for the total supported channels and supported sample + // format. + let f = match self.default_output_config() { + Err(_) => return Err(SupportedStreamConfigsError::DeviceNotAvailable), + Ok(f) => f, + }; + + // Collect a config for every combination of supported sample rate and number of channels. + let mut supported_configs = vec![]; + for &rate in crate::COMMON_SAMPLE_RATES { + if !self + .driver + .can_sample_rate(rate.into()) + .ok() + .unwrap_or(false) + { + continue; + } + for channels in 1..f.channels + 1 { + supported_configs.push(SupportedStreamConfigRange { + channels, + min_sample_rate: rate, + max_sample_rate: rate, + buffer_size: f.buffer_size, + sample_format: f.sample_format, + }) + } + } + Ok(supported_configs.into_iter()) + } + + /// Returns the default input config + pub fn default_input_config(&self) -> Result { + let channels = self.driver.channels().map_err(default_config_err)?.ins as u16; + let sample_rate = self.driver.sample_rate().map_err(default_config_err)? as u32; + let (min, max) = self.driver.buffersize_range().map_err(default_config_err)?; + let buffer_size = SupportedBufferSize::Range { + min: min as u32, + max: max as u32, + }; + // Map th ASIO sample type to a CPAL sample type + let data_type = self.driver.input_data_type().map_err(default_config_err)?; + let sample_format = convert_data_type(&data_type) + .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?; + Ok(SupportedStreamConfig { + channels, + sample_rate, + buffer_size, + sample_format, + }) + } + + /// Returns the default output config + pub fn default_output_config(&self) -> Result { + let channels = self.driver.channels().map_err(default_config_err)?.outs as u16; + let sample_rate = self.driver.sample_rate().map_err(default_config_err)? as u32; + let (min, max) = self.driver.buffersize_range().map_err(default_config_err)?; + let buffer_size = SupportedBufferSize::Range { + min: min as u32, + max: max as u32, + }; + let data_type = self.driver.output_data_type().map_err(default_config_err)?; + let sample_format = convert_data_type(&data_type) + .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?; + Ok(SupportedStreamConfig { + channels, + sample_rate, + buffer_size, + sample_format, + }) + } +} + +impl Devices { + pub fn new(asio: Arc) -> Result { + let drivers = asio.driver_names().into_iter(); + Ok(Devices { asio, drivers }) + } +} + +impl Iterator for Devices { + type Item = Device; + + /// Load drivers and return device + fn next(&mut self) -> Option { + loop { + match self.drivers.next() { + Some(name) => match self.asio.load_driver(&name) { + Ok(driver) => { + let driver = Arc::new(driver); + let asio_streams = Arc::new(Mutex::new(sys::AsioStreams { + input: None, + output: None, + })); + return Some(Device { + driver, + asio_streams, + // Initialize with sentinel value so it never matches global flag state (0 or 1). + current_callback_flag: Arc::new(AtomicU32::new(u32::MAX)), + }); + } + Err(_) => continue, + }, + None => return None, + } + } + } +} + +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) +} + +fn default_config_err(e: sys::AsioError) -> DefaultStreamConfigError { + match e { + sys::AsioError::NoDrivers | sys::AsioError::HardwareMalfunction => { + DefaultStreamConfigError::DeviceNotAvailable + } + sys::AsioError::NoRate => DefaultStreamConfigError::StreamTypeNotSupported, + err => { + let description = format!("{}", err); + BackendSpecificError { description }.into() + } + } +} diff --git a/vendor/cpal/src/host/asio/mod.rs b/vendor/cpal/src/host/asio/mod.rs new file mode 100644 index 0000000..87e3adf --- /dev/null +++ b/vendor/cpal/src/host/asio/mod.rs @@ -0,0 +1,156 @@ +//! 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 crate::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crate::{ + BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceId, DeviceIdError, + DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + PlayStreamError, SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, + SupportedStreamConfigsError, +}; + +pub use self::device::{Device, Devices, SupportedInputConfigs, SupportedOutputConfigs}; +pub use self::stream::Stream; +use std::sync::{Arc, OnceLock}; +use std::time::Duration; + +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 { + 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(StreamError) + 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(StreamError) + Send + 'static, + { + Device::build_output_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + Stream::play(self) + } + + fn pause(&self) -> Result<(), PauseStreamError> { + Stream::pause(self) + } +} diff --git a/vendor/cpal/src/host/asio/stream.rs b/vendor/cpal/src/host/asio/stream.rs new file mode 100644 index 0000000..958a52b --- /dev/null +++ b/vendor/cpal/src/host/asio/stream.rs @@ -0,0 +1,994 @@ +extern crate asio_sys as sys; +extern crate num_traits; + +use crate::I24; + +use self::num_traits::PrimInt; +use super::Device; +use crate::{ + BackendSpecificError, BufferSize, BuildStreamError, Data, InputCallbackInfo, + OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError, +}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +pub struct Stream { + playing: Arc, + // Ensure the `Driver` does not terminate until the last stream is dropped. + driver: Arc, + #[allow(dead_code)] + asio_streams: Arc>, + callback_id: sys::CallbackId, + message_callback_id: sys::MessageCallbackId, +} + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +impl Stream { + pub fn play(&self) -> Result<(), PlayStreamError> { + self.playing.store(true, Ordering::SeqCst); + Ok(()) + } + + pub fn pause(&self) -> Result<(), PauseStreamError> { + self.playing.store(false, Ordering::SeqCst); + Ok(()) + } +} + +impl Device { + pub 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(StreamError) + Send + 'static, + { + let stream_type = self.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(BuildStreamError::StreamConfigNotSupported)?; + if sample_format != expected_sample_format { + return Err(BuildStreamError::StreamConfigNotSupported); + } + + // Register the message callback with the driver + let message_callback_id = self.add_message_callback(error_callback); + + let num_channels = config.channels; + let buffer_size = self.get_or_create_input_stream(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]; + + let stream_playing = Arc::new(AtomicBool::new(false)); + let playing = Arc::clone(&stream_playing); + let asio_streams = self.asio_streams.clone(); + + // Set the input callback. + // This is most performance critical part of the ASIO bindings. + let config = config.clone(); + let callback_id = self.driver.add_callback(move |callback_info| unsafe { + // If not playing return early. + if !playing.load(Ordering::SeqCst) { + return; + } + + // 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, + }; + + /// 1. Write from the ASIO buffer to the interleaved CPAL buffer. + /// 2. Deliver the CPAL buffer to the user callback. + unsafe fn process_input_callback( + data_callback: &mut D, + interleaved: &mut [u8], + asio_stream: &sys::AsioStream, + asio_info: &sys::CallbackInfo, + sample_rate: crate::SampleRate, + format: SampleFormat, + from_endianness: F, + ) 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, + asio_stream, + asio_info, + sample_rate, + format, + ); + } + + 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, + ); + } + (&sys::AsioSampleType::ASIOSTInt16MSB, SampleFormat::I16) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::I16, + from_be, + ); + } + + (&sys::AsioSampleType::ASIOSTFloat32LSB, SampleFormat::F32) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F32, + from_le, + ); + } + (&sys::AsioSampleType::ASIOSTFloat32MSB, SampleFormat::F32) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F32, + from_be, + ); + } + + (&sys::AsioSampleType::ASIOSTInt32LSB, SampleFormat::I32) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::I32, + from_le, + ); + } + (&sys::AsioSampleType::ASIOSTInt32MSB, SampleFormat::I32) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::I32, + from_be, + ); + } + + (&sys::AsioSampleType::ASIOSTFloat64LSB, SampleFormat::F64) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F64, + from_le, + ); + } + (&sys::AsioSampleType::ASIOSTFloat64MSB, SampleFormat::F64) => { + process_input_callback::( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + SampleFormat::F64, + from_be, + ); + } + + (&sys::AsioSampleType::ASIOSTInt24LSB, SampleFormat::I24) => { + process_input_callback_i24( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + true, + ); + } + (&sys::AsioSampleType::ASIOSTInt24MSB, SampleFormat::I24) => { + process_input_callback_i24( + &mut data_callback, + &mut interleaved, + asio_stream, + callback_info, + config.sample_rate, + false, + ); + } + + unsupported_format_pair => unreachable!( + "`build_input_stream_raw` should have returned with unsupported \ + format {:?}", + unsupported_format_pair + ), + } + }); + + let driver = self.driver.clone(); + let asio_streams = self.asio_streams.clone(); + + // Immediately start the device? + self.driver.start().map_err(build_stream_err)?; + + Ok(Stream { + playing: stream_playing, + driver, + asio_streams, + callback_id, + message_callback_id, + }) + } + + pub fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + mut data_callback: D, + error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let stream_type = self.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(BuildStreamError::StreamConfigNotSupported)?; + if sample_format != expected_sample_format { + return Err(BuildStreamError::StreamConfigNotSupported); + } + + // Register the message callback with the driver + let message_callback_id = self.add_message_callback(error_callback); + + let num_channels = config.channels; + let buffer_size = self.get_or_create_output_stream(config, sample_format)?; + let cpal_num_samples = buffer_size * num_channels as usize; + + // Create buffers 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(); + + let stream_playing = Arc::new(AtomicBool::new(false)); + let playing = Arc::clone(&stream_playing); + let asio_streams = self.asio_streams.clone(); + + let config = config.clone(); + let callback_id = self.driver.add_callback(move |callback_info| unsafe { + // If not playing, return early. + if !playing.load(Ordering::SeqCst) { + return; + } + + // 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, + }; + + // 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: crate::SampleRate, + format: SampleFormat, + mix_samples: F, + ) where + A: Copy, + 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, + asio_stream, + asio_info, + sample_rate, + format, + ); + 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.align_to_mut::().1.fill(0); + } + for (frame, s_asio) in interleaved.chunks(n_channels).zip(asio_channel) { + *s_asio = mix_samples(*s_asio, frame[ch_ix]); + } + } + } + + 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() + }, + ); + } + (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() + }, + ); + } + (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() + }, + ); + } + + (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() + }, + ); + } + + (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() + }, + ); + } + (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() + }, + ); + } + + (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() + }, + ); + } + + (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() + }, + ); + } + + (SampleFormat::I24, &sys::AsioSampleType::ASIOSTInt24LSB) => { + process_output_callback_i24::<_>( + &mut data_callback, + &mut interleaved, + silence, + true, + asio_stream, + callback_info, + config.sample_rate, + ); + } + + (SampleFormat::I24, &sys::AsioSampleType::ASIOSTInt24MSB) => { + process_output_callback_i24::<_>( + &mut data_callback, + &mut interleaved, + silence, + false, + asio_stream, + callback_info, + config.sample_rate, + ); + } + + unsupported_format_pair => unreachable!( + "`build_output_stream_raw` should have returned with unsupported \ + format {:?}", + unsupported_format_pair + ), + } + }); + + let driver = self.driver.clone(); + let asio_streams = self.asio_streams.clone(); + + // Immediately start the device? + self.driver.start().map_err(build_stream_err)?; + + Ok(Stream { + playing: stream_playing, + driver, + asio_streams, + callback_id, + message_callback_id, + }) + } + + /// 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, + config: &StreamConfig, + sample_format: SampleFormat, + ) -> Result { + match self.default_input_config() { + Ok(f) => { + let num_asio_channels = f.channels; + check_config(&self.driver, config, sample_format, num_asio_channels) + } + Err(_) => Err(BuildStreamError::StreamConfigNotSupported), + }?; + let num_channels = config.channels as usize; + let mut streams = self.asio_streams.lock().unwrap(); + + 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(); + self.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(|ref e| { + println!("Error preparing stream: {}", e); + BuildStreamError::DeviceNotAvailable + }) + } + } + } + + /// 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, + config: &StreamConfig, + sample_format: SampleFormat, + ) -> Result { + match self.default_output_config() { + Ok(f) => { + let num_asio_channels = f.channels; + check_config(&self.driver, config, sample_format, num_asio_channels) + } + Err(_) => Err(BuildStreamError::StreamConfigNotSupported), + }?; + let num_channels = config.channels as usize; + let mut streams = self.asio_streams.lock().unwrap(); + + 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(); + self.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(|ref e| { + println!("Error preparing stream: {}", e); + BuildStreamError::DeviceNotAvailable + }) + } + } + } + + fn add_message_callback(&self, error_callback: E) -> sys::MessageCallbackId + where + E: FnMut(StreamError) + Send + 'static, + { + let error_callback_shared = Arc::new(Mutex::new(error_callback)); + + self.driver.add_message_callback(move |msg| { + // Check specifically for ResetRequest + if let sys::AsioMessageSelectors::kAsioResetRequest = msg { + if let Ok(mut cb) = error_callback_shared.lock() { + cb(StreamError::StreamInvalidated); + } + } + }) + } +} + +impl Drop for Stream { + fn drop(&mut self) { + self.driver.remove_callback(self.callback_id); + self.driver + .remove_message_callback(self.message_callback_id); + } +} + +fn asio_ns_to_double(val: sys::bindings::asio_import::ASIOTimeStamp) -> f64 { + let two_raised_to_32 = 4294967296.0; + val.lo as f64 + val.hi as f64 * two_raised_to_32 +} + +/// Asio retrieves system time via `timeGetTime` which returns the time in milliseconds. +fn system_time_to_stream_instant( + system_time: sys::bindings::asio_import::ASIOTimeStamp, +) -> crate::StreamInstant { + let systime_ns = asio_ns_to_double(system_time); + let secs = systime_ns as i64 / 1_000_000_000; + let nanos = (systime_ns as i64 - secs * 1_000_000_000) as u32; + crate::StreamInstant::new(secs, nanos) +} + +// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +#[inline] +fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { + let secsf = frames as f64 / rate as f64; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} + +/// 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<(), BuildStreamError> { + 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 (min, max) = driver.buffersize_range().map_err(build_stream_err)?; + let requested_size_i32 = *requested_size as i32; + if !(min..=max).contains(&requested_size_i32) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + } + + // 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(BuildStreamError::StreamConfigNotSupported); + } + } + // unsigned formats are not supported by asio + match sample_format { + SampleFormat::I16 | SampleFormat::I24 | SampleFormat::I32 | SampleFormat::F32 => (), + _ => return Err(BuildStreamError::StreamConfigNotSupported), + } + if *channels > num_asio_channels { + return Err(BuildStreamError::StreamConfigNotSupported); + } + 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. +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. +fn from_le(t: T) -> T { + T::from_le(t) +} + +/// Helper function to convert from little endianness. +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) +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) +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 build_stream_err(e: sys::AsioError) -> BuildStreamError { + match e { + sys::AsioError::NoDrivers | sys::AsioError::HardwareMalfunction => { + BuildStreamError::DeviceNotAvailable + } + sys::AsioError::InvalidInput | sys::AsioError::BadMode => BuildStreamError::InvalidArgument, + err => { + let description = format!("{}", err); + BackendSpecificError { description }.into() + } + } +} + +/// Convert i24 bytes to i32 +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 + } +} + +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: crate::SampleRate, +) 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, + asio_stream, + asio_info, + sample_rate, + format, + ); + + // 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 { + asio_channel.align_to_mut::().1.fill(0); + } + + // 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]; + } + } + } +} + +unsafe fn process_input_callback_i24( + data_callback: &mut D, + interleaved: &mut [u8], + asio_stream: &sys::AsioStream, + asio_info: &sys::CallbackInfo, + sample_rate: crate::SampleRate, + little_endian: bool, +) 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, + asio_stream, + asio_info, + sample_rate, + format, + ); +} + +/// Apply the output callback to the interleaved buffer. +unsafe fn apply_output_callback_to_data( + data_callback: &mut D, + interleaved: &mut [A], + asio_stream: &mut sys::AsioStream, + asio_info: &sys::CallbackInfo, + sample_rate: crate::SampleRate, + sample_format: SampleFormat, +) 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 callback = system_time_to_stream_instant(asio_info.system_time); + let delay = frames_to_duration(asio_stream.buffer_size as usize, sample_rate); + let playback = callback + .add(delay) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + let info = OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); +} + +/// Apply the input callback to the interleaved buffer. +unsafe fn apply_input_callback_to_data( + data_callback: &mut D, + interleaved: &mut [A], + asio_stream: &sys::AsioStream, + asio_info: &sys::CallbackInfo, + sample_rate: crate::SampleRate, + format: SampleFormat, +) where + A: Copy, + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, +{ + let data = Data::from_parts( + interleaved.as_mut_ptr() as *mut (), + interleaved.len(), + format, + ); + let callback = system_time_to_stream_instant(asio_info.system_time); + let delay = frames_to_duration(asio_stream.buffer_size as usize, sample_rate); + let capture = callback + .sub(delay) + .expect("`capture` occurs before origin of alsa `StreamInstant`"); + let timestamp = crate::InputStreamTimestamp { callback, capture }; + let info = InputCallbackInfo { timestamp }; + data_callback(&data, &info); +} diff --git a/vendor/cpal/src/host/audioworklet/dependent_module.rs b/vendor/cpal/src/host/audioworklet/dependent_module.rs new file mode 100644 index 0000000..75db7d1 --- /dev/null +++ b/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/vendor/cpal/src/host/audioworklet/mod.rs b/vendor/cpal/src/host/audioworklet/mod.rs new file mode 100644 index 0000000..547bd1d --- /dev/null +++ b/vendor/cpal/src/host/audioworklet/mod.rs @@ -0,0 +1,436 @@ +//! Audio Worklet backend implementation. +//! +//! Available on WebAssembly with the `audioworklet` feature. Requires atomics support. +//! See the `audioworklet-beep` example for setup instructions. + +mod dependent_module; +use js_sys::wasm_bindgen; + +use crate::dependent_module; +use wasm_bindgen::prelude::*; + +use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crate::{ + BackendSpecificError, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, + DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, + DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, + SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, +}; + +use std::time::Duration; + +/// Content is false if the iterator is empty. +pub struct Devices(bool); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Device; + +pub struct Host; + +pub struct Stream { + audio_context: web_sys::AudioContext, +} + +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; + +const MIN_CHANNELS: ChannelCount = 1; +const MAX_CHANNELS: ChannelCount = 32; +const MIN_SAMPLE_RATE: SampleRate = 8_000; +const MAX_SAMPLE_RATE: SampleRate = 96_000; +const DEFAULT_SAMPLE_RATE: SampleRate = 44_100; +const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; + +impl Host { + pub fn new() -> Result { + if Self::is_available() { + Ok(Host) + } else { + Err(crate::HostUnavailable) + } + } +} + +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 { + Some(Device) + } +} + +impl Devices { + fn new() -> Result { + Ok(Self::default()) + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + #[inline] + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) + .direction(crate::DeviceDirection::Output) + .build()) + } + + #[inline] + fn id(&self) -> Result { + Ok(DeviceId( + crate::platform::HostId::AudioWorklet, + "default".to_string(), + )) + } + + #[inline] + fn supported_input_configs( + &self, + ) -> Result { + // TODO + Ok(Vec::new().into_iter()) + } + + #[inline] + fn supported_output_configs( + &self, + ) -> Result { + let buffer_size = SupportedBufferSize::Unknown; + + // 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) + .map(|channels| SupportedStreamConfigRange { + channels, + min_sample_rate: MIN_SAMPLE_RATE, + max_sample_rate: MAX_SAMPLE_RATE, + buffer_size, + sample_format: SUPPORTED_SAMPLE_FORMAT, + }) + .collect(); + Ok(configs.into_iter()) + } + + #[inline] + fn default_input_config(&self) -> Result { + // TODO + Err(DefaultStreamConfigError::StreamTypeNotSupported) + } + + #[inline] + fn default_output_config(&self) -> Result { + const EXPECT: &str = "expected at least one valid webaudio stream config"; + let config = self + .supported_output_configs() + .expect(EXPECT) + .max_by(|a, b| a.cmp_default_heuristics(b)) + .unwrap() + .with_sample_rate(DEFAULT_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(StreamError) + Send + 'static, + { + // TODO + Err(BuildStreamError::StreamConfigNotSupported) + } + + /// Create an output stream. + fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + mut data_callback: D, + mut error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + if !valid_config(config, sample_format) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + + let config = config.clone(); + + let stream_opts = web_sys::AudioContextOptions::new(); + stream_opts.set_sample_rate(config.sample_rate as f32); + + let audio_context = web_sys::AudioContext::new_with_context_options(&stream_opts).map_err( + |err| -> BuildStreamError { + let description = format!("{err:?}"); + let err = BackendSpecificError { description }; + err.into() + }, + )?; + + let destination = audio_context.destination(); + + // If possible, set the destination's channel_count to the given config.channel. + // If not, fallback on the default destination channel_count to keep previous behavior + // and do not return an error. + if config.channels as u32 <= destination.max_channel_count() { + destination.set_channel_count(config.channels as u32); + } + + 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| { + let data = interleaved_data.as_mut_ptr() as *mut (); + let mut data = unsafe { + Data::from_parts(data, interleaved_data.len(), sample_format) + }; + + let callback = crate::StreamInstant::from_secs_f64(now); + + let buffer_duration = frames_to_duration(frame_size as _, sample_rate); + let playback = callback.add(buffer_duration).expect( + "`playback` occurs beyond representation supported by `StreamInstant`", + ); + let timestamp = crate::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 description = if let Some(string_value) = err.as_string() { + string_value + } else { + format!("Browser error initializing stream: {err:?}") + }; + + error_callback(StreamError::BackendSpecific { + err: BackendSpecificError { description }, + }) + } + }); + + Ok(Stream { audio_context }) + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + match self.audio_context.resume() { + Ok(_) => Ok(()), + Err(err) => { + let description = format!("{err:?}"); + let err = BackendSpecificError { description }; + Err(err.into()) + } + } + } + + fn pause(&self) -> Result<(), PauseStreamError> { + match self.audio_context.suspend() { + Ok(_) => Ok(()), + Err(err) => { + let description = format!("{err:?}"); + let err = BackendSpecificError { description }; + Err(err.into()) + } + } + } +} + +impl Drop for Stream { + fn drop(&mut self) { + let _ = self.audio_context.close(); + } +} + +impl Default for Devices { + fn default() -> Devices { + Devices(true) + } +} + +impl Iterator for Devices { + type Item = Device; + #[inline] + fn next(&mut self) -> Option { + if self.0 { + self.0 = false; + Some(Device) + } else { + None + } + } +} + +// Whether or not the given stream configuration is valid for building a stream. +fn valid_config(conf: &StreamConfig, sample_format: SampleFormat) -> bool { + conf.channels <= MAX_CHANNELS + && conf.channels >= MIN_CHANNELS + && conf.sample_rate <= MAX_SAMPLE_RATE + && conf.sample_rate >= MIN_SAMPLE_RATE + && sample_format == SUPPORTED_SAMPLE_FORMAT +} + +// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { + let secsf = frames as f64 / rate as f64; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} + +type AudioProcessorCallback = Box; + +/// WasmAudioProcessor provides an interface for the Javascript code +/// running in the AudioWorklet to interact with Rust. +#[wasm_bindgen] +pub struct WasmAudioProcessor { + #[wasm_bindgen(skip)] + interleaved_buffer: Vec, + #[wasm_bindgen(skip)] + // 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()), + 0.0, + ); + + (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. + /// + /// # Purpose + /// This function is intended to transfer ownership of the processor instance to the caller, + /// typically for passing between Rust and JavaScript via WebAssembly. + /// + /// # Relationship with [`unpack`] + /// The returned pointer must be passed to [`unpack`] exactly once to recover the original + /// `WasmAudioProcessor` instance. Failing to do so will result in a memory leak. Calling + /// [`unpack`] more than once or using the pointer after it has been unpacked will result in + /// undefined behavior. + /// + /// # Safety and Lifetime + /// After calling `pack`, the caller is responsible for ensuring that `unpack` is called + /// exactly once, and that the pointer is not used after being unpacked. This function + /// should be used with care, as improper use can lead to memory safety issues. + /// + /// [`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 _) + } +} diff --git a/vendor/cpal/src/host/audioworklet/worklet.js b/vendor/cpal/src/host/audioworklet/worklet.js new file mode 100644 index 0000000..f395dd1 --- /dev/null +++ b/vendor/cpal/src/host/audioworklet/worklet.js @@ -0,0 +1,49 @@ +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; + } +}); \ No newline at end of file diff --git a/vendor/cpal/src/host/coreaudio/ios/enumerate.rs b/vendor/cpal/src/host/coreaudio/ios/enumerate.rs new file mode 100644 index 0000000..fb9efb8 --- /dev/null +++ b/vendor/cpal/src/host/coreaudio/ios/enumerate.rs @@ -0,0 +1,36 @@ +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(VecIntoIter); + +impl Devices { + pub fn new() -> Self { + Self::default() + } +} + +impl Default for Devices { + fn default() -> Devices { + Devices(vec![Device].into_iter()) + } +} + +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/vendor/cpal/src/host/coreaudio/ios/mod.rs b/vendor/cpal/src/host/coreaudio/ios/mod.rs new file mode 100644 index 0000000..4fbc9c1 --- /dev/null +++ b/vendor/cpal/src/host/coreaudio/ios/mod.rs @@ -0,0 +1,592 @@ +//! CoreAudio implementation for iOS using AVAudioSession and RemoteIO Audio Units. + +use std::sync::Mutex; + +use coreaudio::audio_unit::render_callback::data; +use coreaudio::audio_unit::{render_callback, AudioUnit, Element, Scope}; +use objc2_audio_toolbox::{kAudioOutputUnitProperty_EnableIO, kAudioUnitProperty_StreamFormat}; +use objc2_core_audio_types::AudioBuffer; + +use objc2_avf_audio::AVAudioSession; + +use super::{asbd_from_config, frames_to_duration, host_time_to_stream_instant}; +use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; + +use crate::{ + BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, + DefaultStreamConfigError, DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError, + DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, +}; + +use self::enumerate::{ + default_input_device, default_output_device, Devices, SupportedInputConfigs, + SupportedOutputConfigs, +}; +use std::ptr::NonNull; +use std::time::Duration; + +pub mod enumerate; + +// 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; + +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::new()) + } + + 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".to_string()) + .direction(direction) + .build()) + } + + fn id(&self) -> Result { + Ok(DeviceId( + crate::platform::HostId::CoreAudio, + "default".to_string(), + )) + } + + 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 + get_supported_stream_configs(true) + .next() + .map(|range| range.with_max_sample_rate()) + .ok_or(DefaultStreamConfigError::StreamTypeNotSupported) + } + + fn default_output_config(&self) -> Result { + // Get the maximum channel count config from supported configs + get_supported_stream_configs(false) + .last() + .map(|range| range.with_max_sample_rate()) + .ok_or(DefaultStreamConfigError::StreamTypeNotSupported) + } +} + +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(StreamError) + Send + 'static, + { + // Configure buffer size and create audio unit + let mut audio_unit = setup_stream_audio_unit(config, sample_format, true)?; + + // Query device buffer size for latency calculation + let device_buffer_frames = Some(get_device_buffer_frames()); + + // Set up input callback + setup_input_callback( + &mut audio_unit, + sample_format, + config.sample_rate, + device_buffer_frames, + data_callback, + error_callback, + )?; + + audio_unit.start()?; + + Ok(Stream::new(StreamInner { + playing: true, + audio_unit, + })) + } + + /// 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(StreamError) + Send + 'static, + { + // Configure buffer size and create audio unit + let mut audio_unit = setup_stream_audio_unit(config, sample_format, false)?; + + // Query device buffer size for latency calculation + let device_buffer_frames = Some(get_device_buffer_frames()); + + // Set up output callback + setup_output_callback( + &mut audio_unit, + sample_format, + config.sample_rate, + device_buffer_frames, + data_callback, + error_callback, + )?; + + audio_unit.start()?; + + Ok(Stream::new(StreamInner { + playing: true, + audio_unit, + })) + } +} + +pub struct Stream { + inner: Mutex, +} + +impl Stream { + fn new(inner: StreamInner) -> Self { + Self { + inner: Mutex::new(inner), + } + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + let mut stream = self + .inner + .lock() + .map_err(|_| PlayStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })?; + + if !stream.playing { + if let Err(e) = stream.audio_unit.start() { + let description = format!("{}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + stream.playing = true; + } + Ok(()) + } + + fn pause(&self) -> Result<(), PauseStreamError> { + let mut stream = self + .inner + .lock() + .map_err(|_| PauseStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })?; + + if stream.playing { + if let Err(e) = stream.audio_unit.stop() { + let description = format!("{}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + + stream.playing = false; + } + Ok(()) + } +} + +struct StreamInner { + playing: bool, + audio_unit: AudioUnit, +} + +fn create_audio_unit() -> Result { + AudioUnit::new(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: u32, + sample_rate: crate::SampleRate, +) -> Result<(), BuildStreamError> { + // 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(|_| BuildStreamError::StreamConfigNotSupported)?; + } + + 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(); + (buffer_duration * sample_rate) as usize + } +} + +/// 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 u32; + let max_channels = if is_input { + audio_session.inputNumberOfChannels() as u16 + } else { + audio_session.outputNumberOfChannels() as u16 + }; + (sample_rate, max_channels) + }; + + // Typical iOS hardware buffer frame limits according to Apple Technical Q&A QA1631. + let buffer_size = SupportedBufferSize::Range { + min: 256, + max: 4096, + }; + + // 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 { + // Configure buffer size via AVAudioSession + if let BufferSize::Fixed(buffer_size) = config.buffer_size { + set_audio_session_buffer_size(buffer_size, config.sample_rate)?; + } + + let mut audio_unit = create_audio_unit()?; + + if is_input { + audio_unit.uninitialize()?; + configure_for_recording(&mut audio_unit)?; + audio_unit.initialize()?; + } + + // 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))?; + + 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, + device_buffer_frames: Option, + mut data_callback: D, + mut error_callback: E, +) -> Result<(), BuildStreamError> +where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + 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.into()); + return Err(()); + } + Ok(cb) => cb, + }; + + let latency_frames = device_buffer_frames.unwrap_or_else(|| { + let channels = buffer.mNumberChannels as usize; + if channels > 0 { + data.len() / channels + } else { + 0 + } + }); + let delay = frames_to_duration(latency_frames, sample_rate); + let capture = callback + .sub(delay) + .expect("`capture` occurs before origin of alsa `StreamInstant`"); + let timestamp = crate::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, + device_buffer_frames: Option, + mut data_callback: D, + mut error_callback: E, +) -> Result<(), BuildStreamError> +where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + 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) }; + + let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { + Err(err) => { + error_callback(err.into()); + return Err(()); + } + Ok(cb) => cb, + }; + + let latency_frames = device_buffer_frames.unwrap_or_else(|| { + let channels = buffer.mNumberChannels as usize; + if channels > 0 { + data.len() / channels + } else { + 0 + } + }); + let delay = frames_to_duration(latency_frames, sample_rate); + let playback = callback + .add(delay) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + let timestamp = crate::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: &crate::OutputCallbackInfo| {}, + |_err| {}, + None, + ); + + assert!( + result.is_ok(), + "BufferSize::Fixed should be supported on iOS via AVAudioSession" + ); + } +} diff --git a/vendor/cpal/src/host/coreaudio/macos/device.rs b/vendor/cpal/src/host/coreaudio/macos/device.rs new file mode 100644 index 0000000..1302299 --- /dev/null +++ b/vendor/cpal/src/host/coreaudio/macos/device.rs @@ -0,0 +1,1019 @@ +use super::OSStatus; +use super::Stream; +use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_stream_instant}; +use crate::host::coreaudio::macos::loopback::LoopbackDevice; +use crate::host::coreaudio::macos::StreamInner; +use crate::traits::DeviceTrait; +use crate::{ + BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, + DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, InputCallbackInfo, + OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, +}; +use coreaudio::audio_unit::render_callback::{self, data}; +use coreaudio::audio_unit::{AudioUnit, Element, Scope}; +use objc2_audio_toolbox::{ + kAudioOutputUnitProperty_CurrentDevice, kAudioOutputUnitProperty_EnableIO, + kAudioUnitProperty_StreamFormat, +}; +use objc2_core_audio::kAudioDevicePropertyDeviceUID; +use objc2_core_audio::kAudioObjectPropertyElementMain; +use objc2_core_audio::{ + kAudioAggregateDeviceClassID, kAudioDevicePropertyAvailableNominalSampleRates, + kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange, + kAudioDevicePropertyNominalSampleRate, kAudioDevicePropertyStreamConfiguration, + kAudioDevicePropertyStreamFormat, kAudioObjectPropertyClass, kAudioObjectPropertyElementMaster, + kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput, + kAudioObjectPropertyScopeOutput, AudioClassID, AudioDeviceID, AudioObjectGetPropertyData, + AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress, + AudioObjectPropertyScope, AudioObjectSetPropertyData, +}; +use objc2_core_audio_types::{ + AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, +}; +use objc2_core_foundation::CFString; +use objc2_core_foundation::Type; + +pub use super::enumerate::{ + default_input_device, default_output_device, SupportedInputConfigs, SupportedOutputConfigs, +}; +use std::fmt; +use std::mem::{self, size_of}; +use std::ptr::{null, NonNull}; +use std::sync::mpsc::{channel, RecvTimeoutError}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use super::invoke_error_callback; +use super::property_listener::AudioObjectPropertyListener; +use coreaudio::audio_unit::macos_helpers::get_device_name; + +/// Attempt to set the device sample rate to the provided rate. +/// Return an error if the requested sample rate is not supported by the device. +fn set_sample_rate( + audio_device_id: AudioObjectID, + target_sample_rate: SampleRate, +) -> Result<(), BuildStreamError> { + // Get the current sample rate. + let mut property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyNominalSampleRate, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + 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 as u32 != target_sample_rate { + // 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(BuildStreamError::StreamConfigNotSupported); + } + + let (send, recv) = channel::>(); + let sample_rate_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyNominalSampleRate, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + // Send sample rate updates back on a channel. + let sample_rate_handler = move || { + let mut rate: f64 = 0.0; + let mut data_size = mem::size_of::() as u32; + + let result = unsafe { + AudioObjectGetPropertyData( + audio_device_id, + NonNull::from(&sample_rate_address), + 0, + null(), + NonNull::from(&mut data_size), + NonNull::from(&mut rate).cast(), + ) + }; + send.send(coreaudio::Error::from_os_status(result).map(|_| rate)) + .ok(); + }; + + let listener = AudioObjectPropertyListener::new( + audio_device_id, + sample_rate_address, + sample_rate_handler, + )?; + + // Finally, set the sample rate. + property_address.mSelector = kAudioDevicePropertyNominalSampleRate; + // Set the nominal sample rate using a single f64 as required by CoreAudio. + 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, but we timeout after 1 sec just in case. + // We loop over potentially several events from the channel to ensure + // that we catch the expected change in sample rate. + let mut timeout = Duration::from_secs(1); + let start = Instant::now(); + + loop { + match recv.recv_timeout(timeout) { + Err(err) => { + let description = match err { + RecvTimeoutError::Disconnected => { + "sample rate listener channel disconnected unexpectedly" + } + RecvTimeoutError::Timeout => { + "timeout waiting for sample rate update for device" + } + } + .to_string(); + return Err(BackendSpecificError { description }.into()); + } + Ok(Ok(reported_sample_rate)) => { + if reported_sample_rate == target_sample_rate as f64 { + break; + } + } + Ok(Err(_)) => { + // TODO: should we consider collecting this error? + } + }; + timeout = timeout + .checked_sub(start.elapsed()) + .unwrap_or(Duration::ZERO); + } + listener.remove()?; + } + Ok(()) +} + +fn audio_unit_from_device(device: &Device, input: bool) -> Result { + let output_type = if !input && is_default_output_device(device) { + coreaudio::audio_unit::IOType::DefaultOutput + } else { + coreaudio::audio_unit::IOType::HalOutput + }; + let mut audio_unit = AudioUnit::new(output_type)?; + + if input { + // Enable input processing. + let enable_input = 1u32; + audio_unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Input, + Element::Input, + Some(&enable_input), + )?; + + // Disable output processing. + let disable_output = 0u32; + audio_unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Output, + Element::Output, + Some(&disable_output), + )?; + } + + // 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( + audio_unit: &AudioUnit, +) -> Result { + // Device-level property: always use Scope::Global + Element::Output + // regardless of whether this audio unit is configured for input or output + let buffer_size_range: AudioValueRange = audio_unit.get_property( + kAudioDevicePropertyBufferFrameSizeRange, + Scope::Global, + Element::Output, + )?; + + Ok(SupportedBufferSize::Range { + min: buffer_size_range.mMinimum as u32, + max: buffer_size_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(StreamError) + 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(StreamError) + Send + 'static, + { + Device::build_output_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + } +} + +#[derive(Clone, Eq, Hash, PartialEq)] +pub struct Device { + pub(crate) audio_device_id: AudioDeviceID, +} + +fn is_default_input_device(device: &Device) -> bool { + default_input_device().is_some_and(|d| d.audio_device_id == device.audio_device_id) +} + +fn is_default_output_device(device: &Device) -> bool { + default_output_device().is_some_and(|d| d.audio_device_id == device.audio_device_id) +} + +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 } + } + + /// 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).map_err(|err| { + DeviceNameError::BackendSpecific { + err: BackendSpecificError { + description: err.to_string(), + }, + } + })?; + + 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 = crate::DeviceDescriptionBuilder::new(name).direction(direction); + + // Check if this is an aggregate device + if self.is_aggregate_device() { + builder = builder.interface_type(crate::InterfaceType::Aggregate); + } + + Ok(builder.build()) + } + + fn id(&self) -> Result { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyDeviceUID, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + // CFString is copied from the audio object, use wrap_under_create_rule + 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 { CFString::wrap_under_create_rule(uid).to_string() }; + Ok(DeviceId(crate::platform::HostId::CoreAudio, uid_string)) + } else { + Err(DeviceIdError::BackendSpecific { + err: BackendSpecificError { + description: "Device UID is null".to_string(), + }, + }) + } + } + + // 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: kAudioObjectPropertyElementMaster, + }; + + 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)] + let input = match scope { + kAudioObjectPropertyScopeInput => Ok(true), + kAudioObjectPropertyScopeOutput => Ok(false), + _ => Err(BackendSpecificError { + description: format!("unexpected scope (neither input nor output): {scope:?}"), + }), + }?; + let audio_unit = audio_unit_from_device(self, input)?; + let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; + + // Collect the supported formats for the device. + + let contains_different_sample_rates = ranges.iter().any(|r| r.mMinimum != r.mMaximum); + if ranges.is_empty() { + Ok(vec![].into_iter()) + } else if contains_different_sample_rates { + let res = 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, + }); + Ok(res.collect::>().into_iter()) + } else { + let fmt = SupportedStreamConfigRange { + channels: n_channels as ChannelCount, + min_sample_rate: ranges + .iter() + .map(|v| v.mMinimum as u32) + .min() + .expect("the list must not be empty"), + max_sample_rate: ranges + .iter() + .map(|v| v.mMaximum as u32) + .max() + .expect("the list must not be empty"), + buffer_size, + sample_format, + }; + + Ok(vec![fmt].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 { + fn default_config_error_from_os_status( + status: OSStatus, + ) -> Result<(), DefaultStreamConfigError> { + let err = match coreaudio::Error::from_os_status(status) { + Err(err) => err, + Ok(_) => return Ok(()), + }; + match err { + coreaudio::Error::AudioUnit( + coreaudio::error::AudioUnitError::FormatNotSupported, + ) + | coreaudio::Error::AudioCodec(_) + | coreaudio::Error::AudioFormat(_) => { + Err(DefaultStreamConfigError::StreamTypeNotSupported) + } + coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::NoConnection) => { + Err(DefaultStreamConfigError::DeviceNotAvailable) + } + err => { + let description = format!("{err}"); + let err = BackendSpecificError { description }; + Err(err.into()) + } + } + } + + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyStreamFormat, + mScope: scope, + mElement: kAudioObjectPropertyElementMaster, + }; + + 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(), + ); + default_config_error_from_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(DefaultStreamConfigError::StreamTypeNotSupported), + }; + 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(DefaultStreamConfigError::StreamTypeNotSupported), + } + }; + + #[allow(non_upper_case_globals)] + let input = match scope { + kAudioObjectPropertyScopeInput => Ok(true), + kAudioObjectPropertyScopeOutput => Ok(false), + _ => Err(BackendSpecificError { + description: format!("unexpected scope (neither input nor output): {scope:?}"), + }), + }?; + let audio_unit = audio_unit_from_device(self, input)?; + let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; + + 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 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.name()) + .finish() + } +} + +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(StreamError) + Send + 'static, + { + // The scope and element for working with a device's input stream. + let scope = Scope::Output; + let element = Element::Input; + + // Potentially change the device sample rate to match the config. + set_sample_rate(self.audio_device_id, config.sample_rate)?; + + let mut loopback_aggregate: Option = None; + let mut audio_unit = if self.supports_input() { + audio_unit_from_device(self, true)? + } else { + loopback_aggregate.replace(LoopbackDevice::from_device(self)?); + audio_unit_from_device(&loopback_aggregate.as_ref().unwrap().aggregate_device, true)? + }; + + // Configure stream format and buffer size for predictable callback behavior. + configure_stream_format_and_buffer(&mut audio_unit, config, sample_format, scope, element)?; + + let error_callback = 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) = + setup_callback_vars(&audio_unit, config, sample_format); + + 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) => { + invoke_error_callback(&error_callback, err.into()); + return Err(()); + } + Ok(cb) => cb, + }; + let buffer_frames = len / channels as usize; + // Use device buffer size for latency calculation if available + let latency_frames = device_buffer_frames.unwrap_or( + // Fallback to callback buffer size if device buffer size is unknown + // (may overestimate latency for BufferSize::Default) + buffer_frames, + ); + let delay = frames_to_duration(latency_frames, sample_rate); + let capture = callback + .sub(delay) + .expect("`capture` occurs before origin of alsa `StreamInstant`"); + let timestamp = crate::InputStreamTimestamp { callback, capture }; + + let info = InputCallbackInfo { timestamp }; + data_callback(&data, &info); + Ok(()) + })?; + + // Create error callback for stream - either dummy or real based on device type + let error_callback_for_stream: super::ErrorCallback = if is_default_input_device(self) { + Box::new(|_: StreamError| {}) + } else { + let error_callback_clone = error_callback_disconnect.clone(); + Box::new(move |err: StreamError| { + invoke_error_callback(&error_callback_clone, err); + }) + }; + + let stream = Stream::new( + StreamInner { + playing: true, + audio_unit, + device_id: self.audio_device_id, + _loopback_device: loopback_aggregate, + }, + error_callback_for_stream, + )?; + + stream + .inner + .lock() + .map_err(|_| BuildStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })? + .audio_unit + .start()?; + + Ok(stream) + } + + fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + mut data_callback: D, + error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let mut audio_unit = audio_unit_from_device(self, false)?; + + // 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)?; + + let error_callback = 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) = + setup_callback_vars(&audio_unit, config, sample_format); + + 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 mut data = Data::from_parts(data, len, sample_format); + + let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { + Err(err) => { + invoke_error_callback(&error_callback, err.into()); + return Err(()); + } + Ok(cb) => cb, + }; + let buffer_frames = len / channels as usize; + // Use device buffer size for latency calculation if available + let latency_frames = device_buffer_frames.unwrap_or( + // Fallback to callback buffer size if device buffer size is unknown + // (may overestimate latency for BufferSize::Default) + buffer_frames, + ); + let delay = frames_to_duration(latency_frames, sample_rate); + let playback = callback + .add(delay) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + + let info = OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); + Ok(()) + })?; + + // Create error callback for stream - either dummy or real based on device type + let error_callback_for_stream: super::ErrorCallback = if is_default_output_device(self) { + Box::new(|_: StreamError| {}) + } else { + let error_callback_clone = error_callback_disconnect.clone(); + Box::new(move |err: StreamError| { + invoke_error_callback(&error_callback_clone, err); + }) + }; + + let stream = Stream::new( + StreamInner { + playing: true, + audio_unit, + device_id: self.audio_device_id, + _loopback_device: None, + }, + error_callback_for_stream, + )?; + + stream + .inner + .lock() + .map_err(|_| BuildStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })? + .audio_unit + .start()?; + + Ok(stream) + } +} + +/// 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, +) -> Result<(), BuildStreamError> { + // 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 { + // 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(()) +} + +/// Setup common callback variables and query device buffer size. +/// +/// Returns (bytes_per_channel, sample_rate, device_buffer_frames) +fn setup_callback_vars( + audio_unit: &AudioUnit, + config: &StreamConfig, + sample_format: SampleFormat, +) -> (usize, crate::SampleRate, Option) { + let bytes_per_channel = sample_format.sample_size(); + let sample_rate = config.sample_rate; + + // Query device buffer size for latency calculation + let device_buffer_frames = get_device_buffer_frame_size(audio_unit).ok(); + + (bytes_per_channel, sample_rate, device_buffer_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. +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/vendor/cpal/src/host/coreaudio/macos/enumerate.rs b/vendor/cpal/src/host/coreaudio/macos/enumerate.rs new file mode 100644 index 0000000..01dc573 --- /dev/null +++ b/vendor/cpal/src/host/coreaudio/macos/enumerate.rs @@ -0,0 +1,140 @@ +use super::{Device, OSStatus}; +use crate::{BackendSpecificError, DevicesError}; +use objc2_core_audio::{ + kAudioHardwareNoError, kAudioHardwarePropertyDefaultInputDevice, + kAudioHardwarePropertyDefaultOutputDevice, kAudioHardwarePropertyDevices, + kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeGlobal, kAudioObjectSystemObject, + AudioDeviceID, AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize, AudioObjectID, + AudioObjectPropertyAddress, +}; +use std::mem; +use std::ptr::{null, NonNull}; +use std::vec::IntoIter as VecIntoIter; + +unsafe fn audio_devices() -> Result, OSStatus> { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyDevices, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + macro_rules! try_status_or_return { + ($status:expr) => { + if $status != kAudioHardwareNoError as i32 { + return Err($status); + } + }; + } + + 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 { + match audio_devices() { + Ok(devices) => devices, + Err(os_status) => { + let description = format!("{os_status}"); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + } + }; + Ok(Devices(devices.into_iter())) + } +} + +impl Iterator for Devices { + type Item = Device; + + fn next(&mut self) -> Option { + self.0.next().map(|id| Device { + audio_device_id: id, + }) + } +} + +pub fn default_input_device() -> Option { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyDefaultInputDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + 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 }; + Some(device) +} + +pub fn default_output_device() -> Option { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyDefaultOutputDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + 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 }; + Some(device) +} + +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; diff --git a/vendor/cpal/src/host/coreaudio/macos/loopback.rs b/vendor/cpal/src/host/coreaudio/macos/loopback.rs new file mode 100644 index 0000000..11b8152 --- /dev/null +++ b/vendor/cpal/src/host/coreaudio/macos/loopback.rs @@ -0,0 +1,248 @@ +//! Manages loopback recording (recording system audio output) + +use super::device::Device; +use crate::{host::coreaudio::check_os_status, BackendSpecificError, BuildStreamError}; +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, CFStringCreateWithCString, +}; +use objc2_foundation::{ns_string, NSArray, NSNumber, NSString}; +use std::{ + ffi::{c_void, CStr}, + mem::MaybeUninit, + ptr::NonNull, +}; +type CFStringRef = *mut std::os::raw::c_void; + +impl Device { + fn uid(&self) -> Result, BackendSpecificError> { + 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(BackendSpecificError { + description: "Device uid is null".to_string(), + }); + } + + 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 + + // 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(ns_string!("cpal output recorder")); + 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 { + AudioHardwareCreateProcessTap(Some(tap_desc.as_ref()), tap_obj_id.as_mut_ptr()); + 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); + let aggregate_device_id: AudioObjectID = 0; + let status = unsafe { + AudioHardwareCreateAggregateDevice( + aggregate_device_properties.as_ref(), + NonNull::from(&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 { + CFStringCreateWithCString( + 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 : @NO, +/// // If we set this to NO then I believe we need to make the Tap public as +/// // well. +/// @kAudioAggregateDeviceIsPrivateKey : @YES, +/// }; +/// ``` +pub fn create_audio_aggregate_device_properties( + tap_uid: Retained, +) -> 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("Cpal loopback record aggregate device") 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("com.cpal.LoopbackRecordAggregateDevice") 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(), false) 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/vendor/cpal/src/host/coreaudio/macos/mod.rs b/vendor/cpal/src/host/coreaudio/macos/mod.rs new file mode 100644 index 0000000..f34041e --- /dev/null +++ b/vendor/cpal/src/host/coreaudio/macos/mod.rs @@ -0,0 +1,369 @@ +#![allow(deprecated)] +use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_stream_instant}; + +use super::OSStatus; +use crate::host::coreaudio::macos::loopback::LoopbackDevice; +use crate::traits::{HostTrait, StreamTrait}; +use crate::{BackendSpecificError, DevicesError, PauseStreamError, PlayStreamError}; +use coreaudio::audio_unit::AudioUnit; +use objc2_core_audio::AudioDeviceID; +use std::sync::{mpsc, Arc, Mutex, Weak}; + +pub use self::enumerate::{default_input_device, default_output_device, Devices}; + +use objc2_core_audio::{ + kAudioDevicePropertyDeviceIsAlive, kAudioObjectPropertyElementMain, + kAudioObjectPropertyScopeGlobal, AudioObjectPropertyAddress, +}; +use property_listener::AudioObjectPropertyListener; + +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 = Box; + +/// Invoke error callback, recovering from poisoned mutex if needed. +/// Returns true if callback was invoked, false if skipped due to WouldBlock. +#[inline] +fn invoke_error_callback(error_callback: &Arc>, err: crate::StreamError) -> bool +where + E: FnMut(crate::StreamError) + Send, +{ + match error_callback.try_lock() { + Ok(mut cb) => { + cb(err); + true + } + Err(std::sync::TryLockError::Poisoned(guard)) => { + // Recover from poisoned lock to still report this error + guard.into_inner()(err); + true + } + Err(std::sync::TryLockError::WouldBlock) => { + // Skip if callback is busy + false + } + } +} + +/// 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 `StreamError::DeviceNotAvailable` +/// +/// The dedicated thread architecture ensures `Stream` can implement `Send`. +struct DisconnectManager { + _shutdown_tx: mpsc::Sender<()>, +} + +impl DisconnectManager { + /// Create a new DisconnectManager that monitors device disconnection on a dedicated thread + 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 dedicated thread to own the AudioObjectPropertyListener + let disconnect_tx_clone = disconnect_tx.clone(); + std::thread::spawn(move || { + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyDeviceIsAlive, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + // Create the listener on this dedicated thread + let disconnect_fn = move || { + let _ = disconnect_tx_clone.send(()); + }; + match AudioObjectPropertyListener::new(device_id, property_address, disconnect_fn) { + Ok(_listener) => { + let _ = ready_tx.send(Ok(())); + // Drop the listener on this thread after receiving a shutdown signal + let _ = shutdown_rx.recv(); + } + Err(e) => { + let _ = ready_tx.send(Err(e)); + } + } + }); + + // Wait for listener creation to complete or fail + ready_rx + .recv() + .map_err(|_| crate::BuildStreamError::BackendSpecific { + err: BackendSpecificError { + description: "Disconnect listener thread terminated unexpectedly".to_string(), + }, + })??; + + // Handle disconnect events on the main thread pool + let stream_weak_clone = stream_weak.clone(); + let error_callback_clone = error_callback.clone(); + std::thread::spawn(move || { + while disconnect_rx.recv().is_ok() { + // Check if stream still exists + if let Some(stream_arc) = stream_weak_clone.upgrade() { + // First, try to pause the stream to stop playback + if let Ok(mut stream_inner) = stream_arc.try_lock() { + let _ = stream_inner.pause(); + } + + // Always try to notify about device disconnection + invoke_error_callback( + &error_callback_clone, + crate::StreamError::DeviceNotAvailable, + ); + } else { + // Stream is gone, exit the handler thread + break; + } + } + }); + + Ok(DisconnectManager { + _shutdown_tx: shutdown_tx, + }) + } +} + +struct StreamInner { + playing: bool, + audio_unit: AudioUnit, + // Track the device with which the audio unit was spawned. + // + // We must do this so that we can avoid changing the device sample rate if there is already + // a stream associated with the device. + #[allow(dead_code)] + device_id: AudioDeviceID, + /// Manage the lifetime of the aggregate device used + /// for loopback recording + _loopback_device: Option, +} + +impl StreamInner { + fn play(&mut self) -> Result<(), PlayStreamError> { + if !self.playing { + if let Err(e) = self.audio_unit.start() { + let description = format!("{e}"); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + self.playing = true; + } + Ok(()) + } + + fn pause(&mut self) -> Result<(), PauseStreamError> { + if self.playing { + if let Err(e) = self.audio_unit.stop() { + let description = format!("{e}"); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + self.playing = false; + } + Ok(()) + } +} + +pub struct Stream { + inner: Arc>, + // Manages the device disconnection listener separately to allow Stream to be Send. + // The DisconnectManager contains the non-Send AudioObjectPropertyListener. + _disconnect_manager: DisconnectManager, +} + +impl Stream { + fn new( + inner: StreamInner, + error_callback: ErrorCallback, + ) -> Result { + let device_id = inner.device_id; + let inner_arc = Arc::new(Mutex::new(inner)); + let weak_inner = Arc::downgrade(&inner_arc); + + let error_callback = Arc::new(Mutex::new(error_callback)); + let disconnect_manager = DisconnectManager::new(device_id, weak_inner, error_callback)?; + + Ok(Self { + inner: inner_arc, + _disconnect_manager: disconnect_manager, + }) + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + let mut stream = self + .inner + .lock() + .map_err(|_| PlayStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })?; + + stream.play() + } + + fn pause(&self) -> Result<(), PauseStreamError> { + let mut stream = self + .inner + .lock() + .map_err(|_| PauseStreamError::BackendSpecific { + err: BackendSpecificError { + description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), + }, + })?; + + stream.pause() + } +} + +#[cfg(test)] +mod test { + use crate::{ + default_host, + traits::{DeviceTrait, HostTrait, StreamTrait}, + 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.name()); + + 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], _: &crate::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], _: &crate::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], _: &crate::OutputCallbackInfo) { + for sample in data.iter_mut() { + *sample = Sample::EQUILIBRIUM; + } + } +} diff --git a/vendor/cpal/src/host/coreaudio/macos/property_listener.rs b/vendor/cpal/src/host/coreaudio/macos/property_listener.rs new file mode 100644 index 0000000..f65f513 --- /dev/null +++ b/vendor/cpal/src/host/coreaudio/macos/property_listener.rs @@ -0,0 +1,88 @@ +//! 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::BuildStreamError; + +/// 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. + pub fn remove(mut self) -> Result<(), BuildStreamError> { + self.remove_inner() + } + + fn remove_inner(&mut self) -> Result<(), BuildStreamError> { + 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/vendor/cpal/src/host/coreaudio/mod.rs b/vendor/cpal/src/host/coreaudio/mod.rs new file mode 100644 index 0000000..dd9b85f --- /dev/null +++ b/vendor/cpal/src/host/coreaudio/mod.rs @@ -0,0 +1,134 @@ +//! CoreAudio backend implementation. +//! +//! Default backend on macOS and iOS. + +use objc2_core_audio_types::{ + kAudioFormatFlagIsFloat, kAudioFormatFlagIsPacked, kAudioFormatFlagIsSignedInteger, + kAudioFormatLinearPCM, AudioStreamBasicDescription, +}; + +use crate::DefaultStreamConfigError; +use crate::{BuildStreamError, SupportedStreamConfigsError}; + +use crate::{BackendSpecificError, SampleFormat, StreamConfig}; + +#[cfg(target_os = "ios")] +mod ios; +#[cfg(target_os = "macos")] +mod macos; + +#[cfg(target_os = "ios")] +#[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<(), BackendSpecificError> { + match coreaudio::Error::from_os_status(os_status) { + Ok(()) => Ok(()), + Err(err) => { + let description = err.to_string(); + Err(BackendSpecificError { description }) + } + } +} + +// 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 * info.numer as u64 / info.denom as u64; + let secs = nanos / 1_000_000_000; + let subsec_nanos = nanos - secs * 1_000_000_000; + Ok(crate::StreamInstant::new(secs as i64, subsec_nanos as u32)) +} + +// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +#[inline] +fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { + let secsf = frames as f64 / rate as f64; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} + +// TODO need stronger error identification +impl From for BuildStreamError { + fn from(err: coreaudio::Error) -> BuildStreamError { + match err { + coreaudio::Error::RenderCallbackBufferFormatDoesNotMatchAudioUnitStreamFormat + | coreaudio::Error::NoKnownSubtype + | coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::FormatNotSupported) + | coreaudio::Error::AudioCodec(_) + | coreaudio::Error::AudioFormat(_) => BuildStreamError::StreamConfigNotSupported, + _ => BuildStreamError::DeviceNotAvailable, + } + } +} + +impl From for SupportedStreamConfigsError { + fn from(err: coreaudio::Error) -> SupportedStreamConfigsError { + let description = format!("{err}"); + let err = BackendSpecificError { description }; + // Check for possible DeviceNotAvailable variant + SupportedStreamConfigsError::BackendSpecific { err } + } +} + +impl From for DefaultStreamConfigError { + fn from(err: coreaudio::Error) -> DefaultStreamConfigError { + let description = format!("{err}"); + let err = BackendSpecificError { description }; + // Check for possible DeviceNotAvailable variant + DefaultStreamConfigError::BackendSpecific { err } + } +} + +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/vendor/cpal/src/host/custom/mod.rs b/vendor/cpal/src/host/custom/mod.rs new file mode 100644 index 0000000..68fdea9 --- /dev/null +++ b/vendor/cpal/src/host/custom/mod.rs @@ -0,0 +1,438 @@ +//! Custom host backend. +//! +//! Allows user-defined host implementations with the `custom` feature. +//! See `examples/custom.rs` for usage. + +use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crate::{ + BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceId, DeviceIdError, + DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + PlayStreamError, SampleFormat, StreamConfig, StreamError, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, +}; +use core::time::Duration; + +/// 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(crate::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() + } +} + +/// 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 name(&self) -> Result; + 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<(), PlayStreamError>; + fn pause(&self) -> Result<(), PauseStreamError>; +} + +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, +{ + #[allow(deprecated)] + fn name(&self) -> Result { + ::name(self) + } + + 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<(), PlayStreamError> { + ::play(self) + } + + fn pause(&self) -> Result<(), PauseStreamError> { + ::pause(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 name(&self) -> Result { + self.0.name() + } + + 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(StreamError) + 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(StreamError) + 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<(), PlayStreamError> { + self.0.play() + } + + fn pause(&self) -> Result<(), PauseStreamError> { + self.0.pause() + } +} diff --git a/vendor/cpal/src/host/emscripten/mod.rs b/vendor/cpal/src/host/emscripten/mod.rs new file mode 100644 index 0000000..81f73ec --- /dev/null +++ b/vendor/cpal/src/host/emscripten/mod.rs @@ -0,0 +1,437 @@ +//! Emscripten backend implementation. +//! +//! Default backend on Emscripten. + +use js_sys::Float32Array; +use std::time::Duration; +use wasm_bindgen::prelude::*; +use wasm_bindgen::JsCast; +use wasm_bindgen_futures::{spawn_local, JsFuture}; +use web_sys::AudioContext; + +use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crate::{ + BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, + DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, DevicesError, + InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, + SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, +}; + +// The emscripten backend currently works by instantiating an `AudioContext` object per `Stream`. +// Creating a stream creates a new `AudioContext`. Destroying a stream destroys it. Creation of a +// `Host` instance initializes the `stdweb` context. + +/// The default emscripten host type. +#[derive(Debug)] +pub struct Host; + +/// Content is false if the iterator is empty. +pub struct Devices(bool); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Device; + +#[wasm_bindgen] +#[derive(Clone)] +pub struct Stream { + // A reference to an `AudioContext` object. + audio_ctxt: AudioContext, +} + +// WASM runs in a single-threaded environment, so Send and Sync are safe by design. +unsafe impl Send for Stream {} +unsafe impl Sync for Stream {} + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; + +const MIN_CHANNELS: u16 = 1; +const MAX_CHANNELS: u16 = 32; +const MIN_SAMPLE_RATE: SampleRate = 8_000; +const MAX_SAMPLE_RATE: SampleRate = 96_000; +const DEFAULT_SAMPLE_RATE: SampleRate = 44_100; +const MIN_BUFFER_SIZE: u32 = 1; +const MAX_BUFFER_SIZE: u32 = u32::MAX; +const DEFAULT_BUFFER_SIZE: usize = 2048; +const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; + +impl Host { + pub fn new() -> Result { + Ok(Host) + } +} + +impl Devices { + fn new() -> Result { + Ok(Self::default()) + } +} + +impl Device { + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) + .direction(crate::DeviceDirection::Output) + .build()) + } + + fn id(&self) -> Result { + Ok(DeviceId( + crate::platform::HostId::Emscripten, + "default".to_string(), + )) + } + + fn supported_input_configs( + &self, + ) -> Result { + unimplemented!(); + } + + fn supported_output_configs( + &self, + ) -> Result { + let buffer_size = SupportedBufferSize::Range { + min: MIN_BUFFER_SIZE, + max: MAX_BUFFER_SIZE, + }; + let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS) + .map(|channels| SupportedStreamConfigRange { + channels, + min_sample_rate: MIN_SAMPLE_RATE, + max_sample_rate: MAX_SAMPLE_RATE, + buffer_size, + sample_format: SUPPORTED_SAMPLE_FORMAT, + }) + .collect(); + Ok(configs.into_iter()) + } + + fn default_input_config(&self) -> Result { + unimplemented!(); + } + + fn default_output_config(&self) -> Result { + const EXPECT: &str = "expected at least one valid webaudio stream config"; + let config = self + .supported_output_configs() + .expect(EXPECT) + .max_by(|a, b| a.cmp_default_heuristics(b)) + .unwrap() + .with_sample_rate(DEFAULT_SAMPLE_RATE); + + Ok(config) + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + // Assume this host is always available on emscripten. + 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() + } +} + +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(StreamError) + Send + 'static, + { + unimplemented!() + } + + 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(StreamError) + Send + 'static, + { + if !valid_config(config, sample_format) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + + let buffer_size_frames = match config.buffer_size { + BufferSize::Fixed(v) => { + if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + v as usize + } + BufferSize::Default => DEFAULT_BUFFER_SIZE, + }; + + // Create the stream. + let audio_ctxt = AudioContext::new().expect("webaudio is not present on this system"); + let stream = Stream { audio_ctxt }; + + // Use `set_timeout` to invoke a Rust callback repeatedly. + // + // The job of this callback is to fill the content of the audio buffers. + // + // See also: The call to `set_timeout` at the end of the `audio_callback_fn` which creates + // the loop. + set_timeout( + 10, + stream.clone(), + data_callback, + config, + sample_format, + buffer_size_frames as u32, + ); + + Ok(stream) + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + let future = JsFuture::from( + self.audio_ctxt + .resume() + .expect("Could not resume the stream"), + ); + spawn_local(async { + match future.await { + Ok(value) => assert!(value.is_undefined()), + Err(value) => panic!("AudioContext.resume() promise was rejected: {:?}", value), + } + }); + Ok(()) + } + + fn pause(&self) -> Result<(), PauseStreamError> { + let future = JsFuture::from( + self.audio_ctxt + .suspend() + .expect("Could not suspend the stream"), + ); + spawn_local(async { + match future.await { + Ok(value) => assert!(value.is_undefined()), + Err(value) => panic!("AudioContext.suspend() promise was rejected: {:?}", value), + } + }); + Ok(()) + } +} + +fn audio_callback_fn( + mut data_callback: D, +) -> impl FnOnce(Stream, StreamConfig, SampleFormat, u32) +where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, +{ + |stream, config, sample_format, buffer_size_frames| { + let sample_rate = config.sample_rate; + let buffer_size_samples = buffer_size_frames * config.channels as u32; + let audio_ctxt = &stream.audio_ctxt; + + // TODO: We should be re-using a buffer. + let mut temporary_buffer = vec![0f32; buffer_size_samples as usize]; + + { + let len = temporary_buffer.len(); + let data = temporary_buffer.as_mut_ptr() as *mut (); + let mut data = unsafe { Data::from_parts(data, len, sample_format) }; + let now_secs: f64 = audio_ctxt.current_time(); + let callback = crate::StreamInstant::from_secs_f64(now_secs); + // TODO: Use proper latency instead. Currently, unsupported on most browsers though, so + // we estimate based on buffer size instead. Probably should use this, but it's only + // supported by firefox (2020-04-28). + // let latency_secs: f64 = audio_ctxt.outputLatency.try_into().unwrap(); + let buffer_duration = frames_to_duration(len, sample_rate as usize); + let playback = callback + .add(buffer_duration) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + let info = OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); + } + + let typed_array: Float32Array = temporary_buffer.as_slice().into(); + + debug_assert_eq!(temporary_buffer.len() % config.channels as usize, 0); + + let src_buffer = Float32Array::new(typed_array.buffer().as_ref()); + let context = audio_ctxt; + let buffer = context + .create_buffer( + config.channels as u32, + buffer_size_frames, + sample_rate as f32, + ) + .expect("Buffer could not be created"); + for channel in 0..config.channels { + let mut buffer_content = buffer + .get_channel_data(channel as u32) + .expect("Should be impossible"); + for (i, buffer_content_item) in buffer_content.iter_mut().enumerate() { + *buffer_content_item = + src_buffer.get_index(i as u32 * config.channels as u32 + channel as u32); + } + } + + let node = context + .create_buffer_source() + .expect("The buffer source node could not be created"); + node.set_buffer(Some(&buffer)); + context + .destination() + .connect_with_audio_node(&node) + .expect("Could not connect the audio node to the destination"); + node.start().expect("Could not start the audio node"); + + // TODO: handle latency better ; right now we just use setInterval with the amount of sound + // data that is in each buffer ; this is obviously bad, and also the schedule is too tight + // and there may be underflows + set_timeout( + 1000 * buffer_size_frames as i32 / sample_rate as i32, + stream.clone().clone(), + data_callback, + &config, + sample_format, + buffer_size_frames, + ); + } +} + +fn set_timeout( + time: i32, + stream: Stream, + data_callback: D, + config: &StreamConfig, + sample_format: SampleFormat, + buffer_size_frames: u32, +) where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, +{ + let window = web_sys::window().expect("Not in a window somehow?"); + window + .set_timeout_with_callback_and_timeout_and_arguments_4( + Closure::once_into_js(audio_callback_fn(data_callback)) + .dyn_ref::() + .expect("The function was somehow not a function"), + time, + &stream.into(), + &((*config).clone()).into(), + &Closure::once_into_js(move || sample_format), + &buffer_size_frames.into(), + ) + .expect("The timeout could not be set"); +} + +impl Default for Devices { + fn default() -> Devices { + // We produce an empty iterator if the WebAudio API isn't available. + Devices(is_webaudio_available()) + } +} +impl Iterator for Devices { + type Item = Device; + + fn next(&mut self) -> Option { + if self.0 { + self.0 = false; + Some(Device) + } else { + None + } + } +} + +fn default_input_device() -> Option { + unimplemented!(); +} + +fn default_output_device() -> Option { + if is_webaudio_available() { + Some(Device) + } else { + None + } +} + +// Detects whether the `AudioContext` global variable is available. +fn is_webaudio_available() -> bool { + AudioContext::new().is_ok() +} + +// Whether or not the given stream configuration is valid for building a stream. +fn valid_config(conf: &StreamConfig, sample_format: SampleFormat) -> bool { + conf.channels <= MAX_CHANNELS + && conf.channels >= MIN_CHANNELS + && conf.sample_rate <= MAX_SAMPLE_RATE + && conf.sample_rate >= MIN_SAMPLE_RATE + && sample_format == SUPPORTED_SAMPLE_FORMAT +} + +// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +fn frames_to_duration(frames: usize, rate: usize) -> std::time::Duration { + let secsf = frames as f64 / rate as f64; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} diff --git a/vendor/cpal/src/host/jack/device.rs b/vendor/cpal/src/host/jack/device.rs new file mode 100644 index 0000000..3c264f7 --- /dev/null +++ b/vendor/cpal/src/host/jack/device.rs @@ -0,0 +1,289 @@ +use crate::traits::DeviceTrait; +use crate::{ + BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, + DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, DeviceNameError, + InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, + SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + SupportedStreamConfigsError, +}; +use std::hash::{Hash, Hasher}; +use std::time::Duration; + +use super::stream::Stream; +use super::JACK_SAMPLE_FORMAT; + +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; + +const DEFAULT_NUM_CHANNELS: u16 = 2; +const DEFAULT_SUPPORTED_CHANNELS: [u16; 10] = [1, 2, 4, 6, 8, 16, 24, 32, 48, 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 { + // ClientOptions are bit flags that you can set with the constants provided + 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. + match super::get_client(&name, client_options) { + Ok(client) => Ok(Device { + // 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_string(), + sample_rate: client.sample_rate(), + buffer_size: SupportedBufferSize::Range { + min: client.buffer_size(), + max: client.buffer_size(), + }, + direction, + start_server_automatically, + connect_ports_automatically, + }), + Err(e) => Err(e), + } + } + + fn id(&self) -> Result { + Ok(DeviceId(crate::platform::HostId::Jack, self.name.clone())) + } + + 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, + }; + + let mut supported_configs = vec![]; + + for &channels in DEFAULT_SUPPORTED_CHANNELS.iter() { + supported_configs.push(SupportedStreamConfigRange { + channels, + min_sample_rate: f.sample_rate, + max_sample_rate: f.sample_rate, + buffer_size: f.buffer_size, + sample_format: f.sample_format, + }); + } + supported_configs + } + + pub fn is_input(&self) -> bool { + matches!(self.direction, DeviceDirection::Input) + } + + pub fn is_output(&self) -> bool { + matches!(self.direction, DeviceDirection::Output) + } + + /// Validate buffer size if Fixed is specified. This is necessary because JACK buffer size + /// is controlled by the JACK server and cannot be changed by clients. Without validation, + /// cpal would silently use the server's buffer size even if a different value was requested. + fn validate_buffer_size(&self, conf: &StreamConfig) -> Result<(), BuildStreamError> { + if let crate::BufferSize::Fixed(requested_size) = conf.buffer_size { + if let SupportedBufferSize::Range { min, max } = self.buffer_size { + if !(min..=max).contains(&requested_size) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + } + } + Ok(()) + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new(self.name.clone()) + .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(StreamError) + Send + 'static, + { + if self.is_output() { + // Trying to create an input stream from an output device + return Err(BuildStreamError::StreamConfigNotSupported); + } + if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT { + return Err(BuildStreamError::StreamConfigNotSupported); + } + self.validate_buffer_size(conf)?; + + // The settings should be fine, create a Client + let client_options = super::get_client_options(self.start_server_automatically); + let client; + match super::get_client(&self.name, client_options) { + Ok(c) => client = c, + Err(e) => { + return Err(BuildStreamError::BackendSpecific { + err: BackendSpecificError { description: e }, + }) + } + }; + let mut stream = Stream::new_input(client, conf.channels, data_callback, error_callback); + + if self.connect_ports_automatically { + stream.connect_to_system_inputs(); + } + + 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(StreamError) + Send + 'static, + { + if self.is_input() { + // Trying to create an output stream from an input device + return Err(BuildStreamError::StreamConfigNotSupported); + } + if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT { + return Err(BuildStreamError::StreamConfigNotSupported); + } + self.validate_buffer_size(conf)?; + + // The settings should be fine, create a Client + let client_options = super::get_client_options(self.start_server_automatically); + let client; + match super::get_client(&self.name, client_options) { + Ok(c) => client = c, + Err(e) => { + return Err(BuildStreamError::BackendSpecific { + err: BackendSpecificError { description: e }, + }) + } + }; + let mut stream = Stream::new_output(client, conf.channels, data_callback, error_callback); + + if self.connect_ports_automatically { + stream.connect_to_system_outputs(); + } + + Ok(stream) + } +} + +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 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/vendor/cpal/src/host/jack/mod.rs b/vendor/cpal/src/host/jack/mod.rs new file mode 100644 index 0000000..07795b2 --- /dev/null +++ b/vendor/cpal/src/host/jack/mod.rs @@ -0,0 +1,199 @@ +//! JACK backend implementation. +//! +//! Available on all platforms with the `jack` feature. Requires JACK server and client libraries. + +extern crate jack; + +use crate::traits::HostTrait; +use crate::{DevicesError, 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 +/// +/// Unlike other backends, 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: "cpal_client".to_owned(), + 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, applications 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, + ); + + match in_device_res { + Ok(device) => self.devices_created.push(device), + Err(err) => { + println!("{}", err); + } + } + + let out_device_res = Device::default_output_device( + &self.name, + self.connect_ports_automatically, + self.start_server_automatically, + ); + match out_device_res { + Ok(device) => self.devices_created.push(device), + Err(err) => { + println!("{}", err); + } + } + } +} + +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 +} + +fn get_client(name: &str, client_options: jack::ClientOptions) -> Result { + let c_res = jack::Client::new(name, client_options); + match c_res { + Ok((client, status)) => { + // The ClientStatus can tell us many things + if status.intersects(jack::ClientStatus::SERVER_ERROR) { + return Err(String::from( + "There was an error communicating with the JACK server!", + )); + } else if status.intersects(jack::ClientStatus::SERVER_FAILED) { + return Err(String::from("Could not connect to the JACK server!")); + } else if status.intersects(jack::ClientStatus::VERSION_ERROR) { + return Err(String::from( + "Error connecting to JACK server: Client's protocol version does not match!", + )); + } else if status.intersects(jack::ClientStatus::INIT_FAILURE) { + return Err(String::from( + "Error connecting to JACK server: Unable to initialize client!", + )); + } else if status.intersects(jack::ClientStatus::SHM_FAILURE) { + return Err(String::from( + "Error connecting to JACK server: Unable to access shared memory!", + )); + } else if status.intersects(jack::ClientStatus::NO_SUCH_CLIENT) { + return Err(String::from( + "Error connecting to JACK server: Requested client does not exist!", + )); + } else if status.intersects(jack::ClientStatus::INVALID_OPTION) { + return Err(String::from("Error connecting to JACK server: The operation contained an invalid or unsupported option!")); + } + Ok(client) + } + Err(e) => Err(format!("Failed to open client because of error: {:?}", e)), + } +} diff --git a/vendor/cpal/src/host/jack/stream.rs b/vendor/cpal/src/host/jack/stream.rs new file mode 100644 index 0000000..b90cd4c --- /dev/null +++ b/vendor/cpal/src/host/jack/stream.rs @@ -0,0 +1,472 @@ +use crate::traits::StreamTrait; +use crate::ChannelCount; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +use crate::{ + BackendSpecificError, Data, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + PlayStreamError, SampleRate, StreamError, +}; + +use super::JACK_SAMPLE_FORMAT; + +type ErrorCallbackPtr = Arc>; + +pub struct Stream { + // TODO: It might be faster to send a message when playing/pausing than to check this every iteration + playing: Arc, + async_client: jack::AsyncClient, + // Port names are stored in order to connect them to other ports in jack automatically + input_port_names: Vec, + output_port_names: Vec, +} + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +impl Stream { + // TODO: Return error messages + pub fn new_input( + client: jack::Client, + channels: ChannelCount, + data_callback: D, + mut error_callback: E, + ) -> Stream + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let mut ports = vec![]; + let mut port_names: Vec = vec![]; + // Create ports + for i in 0..channels { + let port_try = client.register_port(&format!("in_{}", i), jack::AudioIn::default()); + match port_try { + Ok(port) => { + // Get the port name in order to later connect it automatically + if let Ok(port_name) = port.name() { + port_names.push(port_name); + } + // Store the port into a Vec to move to the ProcessHandler + ports.push(port); + } + Err(e) => { + // If port creation failed, send the error back via the error_callback + error_callback( + BackendSpecificError { + description: e.to_string(), + } + .into(), + ); + } + } + } + + let playing = Arc::new(AtomicBool::new(true)); + + let error_callback_ptr = Arc::new(Mutex::new(error_callback)) as ErrorCallbackPtr; + + let input_process_handler = LocalProcessHandler::new( + vec![], + ports, + client.sample_rate(), + client.buffer_size() as usize, + Some(Box::new(data_callback)), + None, + playing.clone(), + Arc::clone(&error_callback_ptr), + ); + + let notification_handler = JackNotificationHandler::new(error_callback_ptr); + + let async_client = client + .activate_async(notification_handler, input_process_handler) + .unwrap(); + + Stream { + playing, + async_client, + input_port_names: port_names, + output_port_names: vec![], + } + } + + pub fn new_output( + client: jack::Client, + channels: ChannelCount, + data_callback: D, + mut error_callback: E, + ) -> Stream + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let mut ports = vec![]; + let mut port_names: Vec = vec![]; + // Create ports + for i in 0..channels { + let port_try = client.register_port(&format!("out_{}", i), jack::AudioOut::default()); + match port_try { + Ok(port) => { + // Get the port name in order to later connect it automatically + if let Ok(port_name) = port.name() { + port_names.push(port_name); + } + // Store the port into a Vec to move to the ProcessHandler + ports.push(port); + } + Err(e) => { + // If port creation failed, send the error back via the error_callback + error_callback( + BackendSpecificError { + description: e.to_string(), + } + .into(), + ); + } + } + } + + let playing = Arc::new(AtomicBool::new(true)); + + let error_callback_ptr = Arc::new(Mutex::new(error_callback)) as ErrorCallbackPtr; + + let output_process_handler = LocalProcessHandler::new( + ports, + vec![], + client.sample_rate(), + client.buffer_size() as usize, + None, + Some(Box::new(data_callback)), + playing.clone(), + Arc::clone(&error_callback_ptr), + ); + + let notification_handler = JackNotificationHandler::new(error_callback_ptr); + + let async_client = client + .activate_async(notification_handler, output_process_handler) + .unwrap(); + + Stream { + playing, + async_client, + input_port_names: vec![], + output_port_names: port_names, + } + } + + /// Connect to the standard system outputs in jack, system:playback_1 and system:playback_2 + /// This has to be done after the client is activated, doing it just after creating the ports doesn't work. + pub fn connect_to_system_outputs(&mut self) { + // Get the system ports + let system_ports = self.async_client.as_client().ports( + Some("system:playback_.*"), + None, + jack::PortFlags::empty(), + ); + + // Connect outputs from this client to the system playback inputs + for i in 0..self.output_port_names.len() { + if i >= system_ports.len() { + break; + } + match self + .async_client + .as_client() + .connect_ports_by_name(&self.output_port_names[i], &system_ports[i]) + { + Ok(_) => (), + Err(e) => println!("Unable to connect to port with error {}", e), + } + } + } + + /// Connect to the standard system outputs in jack, system:capture_1 and system:capture_2 + /// This has to be done after the client is activated, doing it just after creating the ports doesn't work. + pub fn connect_to_system_inputs(&mut self) { + // Get the system ports + let system_ports = self.async_client.as_client().ports( + Some("system:capture_.*"), + None, + jack::PortFlags::empty(), + ); + + // Connect outputs from this client to the system playback inputs + for i in 0..self.input_port_names.len() { + if i >= system_ports.len() { + break; + } + match self + .async_client + .as_client() + .connect_ports_by_name(&system_ports[i], &self.input_port_names[i]) + { + Ok(_) => (), + Err(e) => println!("Unable to connect to port with error {}", e), + } + } + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + self.playing.store(true, Ordering::SeqCst); + Ok(()) + } + + fn pause(&self) -> Result<(), PauseStreamError> { + self.playing.store(false, Ordering::SeqCst); + Ok(()) + } +} + +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, + playing: Arc, + creation_timestamp: std::time::Instant, + /// This should not be called on `process`, only on `buffer_size` because it can block. + error_callback_ptr: ErrorCallbackPtr, +} + +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, + playing: Arc, + error_callback_ptr: ErrorCallbackPtr, + ) -> Self { + // These may be reallocated in the `buffer_size` callback. + let temp_input_buffer = vec![0.0; in_ports.len() * buffer_size]; + let temp_output_buffer = vec![0.0; out_ports.len() * buffer_size]; + + LocalProcessHandler { + out_ports, + in_ports, + sample_rate, + buffer_size, + input_data_callback, + output_data_callback, + temp_input_buffer, + temp_output_buffer, + playing, + creation_timestamp: std::time::Instant::now(), + error_callback_ptr, + } + } +} + +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, _: &jack::Client, process_scope: &jack::ProcessScope) -> jack::Control { + if !self.playing.load(Ordering::SeqCst) { + return jack::Control::Continue; + } + + // 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 cycle_times = process_scope.cycle_times(); + let current_start_usecs = match cycle_times { + Ok(times) => times.current_usecs, + Err(_) => { + // jack was unable to get the current time information + // Fall back to using Instants + let now = std::time::Instant::now(); + let duration = now.duration_since(self.creation_timestamp); + duration.as_micros() as u64 + } + }; + let start_cycle_instant = micros_to_stream_instant(current_start_usecs); + let start_callback_instant = start_cycle_instant + .add(frames_to_duration( + process_scope.frames_since_cycle_start() as usize, + self.sample_rate, + )) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + + 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 frames_since_cycle_start = process_scope.frames_since_cycle_start() as usize; + let duration_since_cycle_start = + frames_to_duration(frames_since_cycle_start, self.sample_rate); + let callback = start_callback_instant + .add(duration_since_cycle_start) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + let capture = start_callback_instant; + let timestamp = crate::InputStreamTimestamp { callback, capture }; + let info = crate::InputCallbackInfo { timestamp }; + input_callback(&data, &info); + } + + if let Some(output_callback) = &mut self.output_data_callback { + let num_out_channels = self.out_ports.len(); + + // Create a slice of exactly current_frame_count frames + let mut data = temp_buffer_to_data( + &mut self.temp_output_buffer, + current_frame_count * num_out_channels, + ); + // Create timestamp + let frames_since_cycle_start = process_scope.frames_since_cycle_start() as usize; + let duration_since_cycle_start = + frames_to_duration(frames_since_cycle_start, self.sample_rate); + let callback = start_callback_instant + .add(duration_since_cycle_start) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + let buffer_duration = frames_to_duration(current_frame_count, self.sample_rate); + let playback = start_cycle_instant + .add(buffer_duration) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + let info = crate::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. It is also fine to call the error callback. + // 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![0.0; self.in_ports.len() * new_size]; + self.temp_output_buffer = vec![0.0; self.out_ports.len() * new_size]; + let description = format!("buffer size changed to: {}", new_size); + if let Ok(mut mutex_guard) = self.error_callback_ptr.lock() { + let err = &mut *mutex_guard; + err(BackendSpecificError { description }.into()); + } + } + + jack::Control::Continue + } +} + +fn micros_to_stream_instant(micros: u64) -> crate::StreamInstant { + let nanos = micros * 1000; + let secs = micros / 1_000_000; + let subsec_nanos = nanos - secs * 1_000_000_000; + crate::StreamInstant::new(secs as i64, subsec_nanos as u32) +} + +// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { + let secsf = frames as f64 / rate as f64; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} + +/// Receives notifications from the JACK server. It is unclear if this may be run concurrent with itself under JACK2 specs +/// so it needs to be Sync. +struct JackNotificationHandler { + error_callback_ptr: ErrorCallbackPtr, + init_sample_rate_flag: Arc, +} + +impl JackNotificationHandler { + pub fn new(error_callback_ptr: ErrorCallbackPtr) -> Self { + JackNotificationHandler { + error_callback_ptr, + init_sample_rate_flag: Arc::new(AtomicBool::new(false)), + } + } + + fn send_error(&mut self, description: String) { + // This thread isn't the audio thread, it's fine to block + if let Ok(mut mutex_guard) = self.error_callback_ptr.lock() { + let err = &mut *mutex_guard; + err(BackendSpecificError { description }.into()); + } + } +} + +impl jack::NotificationHandler for JackNotificationHandler { + unsafe fn shutdown(&mut self, _status: jack::ClientStatus, reason: &str) { + self.send_error(format!("JACK was shut down for reason: {}", reason)); + } + + fn sample_rate(&mut self, _: &jack::Client, _srate: jack::Frames) -> jack::Control { + match self.init_sample_rate_flag.load(Ordering::SeqCst) { + false => { + // One of these notifications is sent every time a client is started. + self.init_sample_rate_flag.store(true, Ordering::SeqCst); + jack::Control::Continue + } + true => { + // The JACK server has changed the sample rate, invalidating this stream. + // The stream configuration must be rebuilt with the new sample rate. + if let Ok(mut cb) = self.error_callback_ptr.lock() { + cb(StreamError::StreamInvalidated); + } + jack::Control::Quit + } + } + } + + fn xrun(&mut self, _: &jack::Client) -> jack::Control { + if let Ok(mut cb) = self.error_callback_ptr.lock() { + cb(StreamError::BufferUnderrun); + } + jack::Control::Continue + } +} diff --git a/vendor/cpal/src/host/mod.rs b/vendor/cpal/src/host/mod.rs new file mode 100644 index 0000000..58b79bb --- /dev/null +++ b/vendor/cpal/src/host/mod.rs @@ -0,0 +1,53 @@ +#[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( + feature = "wasm-bindgen", + feature = "audioworklet", + target_feature = "atomics" +))] +pub(crate) mod audioworklet; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub(crate) mod coreaudio; +#[cfg(target_os = "emscripten")] +pub(crate) mod emscripten; +#[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(windows)] +pub(crate) mod wasapi; +#[cfg(all(target_arch = "wasm32", 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_os = "macos", + target_os = "ios", + target_os = "emscripten", + target_os = "android", + all(target_arch = "wasm32", feature = "wasm-bindgen"), +)))] +pub(crate) mod null; diff --git a/vendor/cpal/src/host/null/mod.rs b/vendor/cpal/src/host/null/mod.rs new file mode 100644 index 0000000..f1bce59 --- /dev/null +++ b/vendor/cpal/src/host/null/mod.rs @@ -0,0 +1,170 @@ +//! Null backend implementation. +//! +//! Fallback no-op backend for unsupported platforms. + +use std::time::Duration; + +use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crate::{ + BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceDescriptionBuilder, + DeviceId, DeviceIdError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, + PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, +}; + +#[derive(Default)] +pub struct Devices; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Device; + +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(Host) + } +} + +impl Devices { + pub fn new() -> Result { + Ok(Devices) + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn name(&self) -> Result { + Ok("null".to_string()) + } + + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Null Device".to_string()).build()) + } + + fn id(&self) -> Result { + Ok(DeviceId(crate::platform::HostId::Null, String::new())) + } + + 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(StreamError) + 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(StreamError) + Send + 'static, + { + unimplemented!() + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + false + } + + fn devices(&self) -> Result { + Devices::new() + } + + fn default_input_device(&self) -> Option { + None + } + + fn default_output_device(&self) -> Option { + None + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + unimplemented!() + } + + fn pause(&self) -> Result<(), PauseStreamError> { + 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/vendor/cpal/src/host/wasapi/com.rs b/vendor/cpal/src/host/wasapi/com.rs new file mode 100644 index 0000000..973d8f4 --- /dev/null +++ b/vendor/cpal/src/host/wasapi/com.rs @@ -0,0 +1,56 @@ +//! Handles COM initialization and cleanup. + +use super::IoError; +use std::marker::PhantomData; + +use windows::Win32::Foundation::RPC_E_CHANGED_MODE; +use windows::Win32::System::Com::{CoInitializeEx, 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() }; + } + } +} + +/// Ensures that COM is initialized in this thread. +#[inline] +pub fn com_initialized() { + COM_INITIALIZED.with(|_| {}); +} diff --git a/vendor/cpal/src/host/wasapi/device.rs b/vendor/cpal/src/host/wasapi/device.rs new file mode 100644 index 0000000..9c46099 --- /dev/null +++ b/vendor/cpal/src/host/wasapi/device.rs @@ -0,0 +1,1239 @@ +use crate::{ + BackendSpecificError, BufferSize, Data, DefaultStreamConfigError, DeviceDescription, + DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, DeviceNameError, + DeviceType, DevicesError, FrameCount, InputCallbackInfo, InterfaceType, OutputCallbackInfo, + SampleFormat, SampleRate, StreamConfig, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, COMMON_SAMPLE_RATES, +}; + +impl From for DeviceDirection { + fn from(data_flow: Audio::EDataFlow) -> Self { + if data_flow == Audio::eCapture { + DeviceDirection::Input + } else if data_flow == Audio::eRender { + DeviceDirection::Output + } else { + DeviceDirection::Unknown + } + } +} +use std::ffi::OsString; +use std::fmt; +use std::mem; +use std::os::windows::ffi::OsStringExt; +use std::ptr; +use std::slice; +use std::sync::OnceLock; +use std::sync::{Arc, Mutex, MutexGuard}; +use std::time::Duration; + +use super::com; +use super::{windows_err_to_cpal_err, windows_err_to_cpal_err_message}; +use windows::core::Interface; +use windows::core::GUID; +use windows::Win32::Devices::Properties; +use windows::Win32::Foundation::PROPERTYKEY; +use windows::Win32::Media::Audio::IAudioRenderClient; +use windows::Win32::Media::{Audio, KernelStreaming, Multimedia}; +use windows::Win32::System::Com; +use windows::Win32::System::Com::{StructuredStorage, STGM_READ}; +use windows::Win32::System::Threading; +use windows::Win32::System::Variant::{VT_LPWSTR, VT_UI4}; +use windows::Win32::UI::Shell::PropertiesSystem::IPropertyStore; + +use super::stream::{AudioClientFlow, Stream, StreamInner}; +use crate::{traits::DeviceTrait, BuildStreamError, StreamError}; + +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; + +// PKEY_AudioEndpoint properties not yet in windows-rs + +/// PKEY_AudioEndpoint_FormFactor (PID 0) - VT_UI4 containing EndpointFormFactor enum +const PKEY_AUDIOENDPOINT_FORMFACTOR: PROPERTYKEY = PROPERTYKEY { + fmtid: GUID::from_u128(0x1da5d803_d492_4edd_8c23_e0c0ffee7f0e), + pid: 0, +}; + +/// PKEY_AudioEndpoint_JackSubType (PID 8) - VT_LPWSTR containing KS node type GUID +const PKEY_AUDIOENDPOINT_JACKSUBTYPE: PROPERTYKEY = PROPERTYKEY { + fmtid: GUID::from_u128(0x1da5d803_d492_4edd_8c23_e0c0ffee7f0e), + pid: 8, +}; + +const DEFAULT_FLAGS: u32 = Audio::AUDCLNT_STREAMFLAGS_EVENTCALLBACK + | Audio::AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY + | Audio::AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM; + +/// Wrapper because of that stupid decision to remove `Send` and `Sync` from raw pointers. +#[derive(Clone)] +struct IAudioClientWrapper(Audio::IAudioClient); +unsafe impl Send for IAudioClientWrapper {} +unsafe impl Sync for IAudioClientWrapper {} + +/// An opaque type that identifies an end point. +#[derive(Clone)] +pub struct Device { + device: Audio::IMMDevice, + /// We cache an uninitialized `IAudioClient` so that we can call functions from it without + /// having to create/destroy audio clients all the time. + future_audio_client: Arc>>, // TODO: add NonZero around the ptr +} + +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 supports_input(&self) -> bool { + self.data_flow() == Audio::eCapture + } + + fn supports_output(&self) -> bool { + self.data_flow() == Audio::eRender + } + + 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(StreamError) + Send + 'static, + { + let stream_inner = self.build_input_stream_raw_inner(config, sample_format)?; + Ok(Stream::new_input( + stream_inner, + data_callback, + error_callback, + )) + } + + 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(StreamError) + Send + 'static, + { + let stream_inner = self.build_output_stream_raw_inner(config, sample_format)?; + Ok(Stream::new_output( + stream_inner, + data_callback, + error_callback, + )) + } +} + +struct Endpoint { + endpoint: Audio::IMMEndpoint, +} + +// Use RAII to make sure CoTaskMemFree is called when we are responsible for freeing. +struct WaveFormatExPtr(*mut Audio::WAVEFORMATEX); + +impl Drop for WaveFormatExPtr { + fn drop(&mut self) { + unsafe { + Com::CoTaskMemFree(Some(self.0 as *mut _)); + } + } +} + +unsafe fn immendpoint_from_immdevice(device: Audio::IMMDevice) -> Audio::IMMEndpoint { + device + .cast::() + .expect("could not query IMMDevice interface for IMMEndpoint") +} + +unsafe fn data_flow_from_immendpoint(endpoint: &Audio::IMMEndpoint) -> Audio::EDataFlow { + endpoint + .GetDataFlow() + .expect("could not get endpoint data_flow") +} + +// Given the audio client and format, returns whether or not the format is supported. +pub unsafe fn is_format_supported( + _client: &Audio::IAudioClient, + _waveformatex_ptr: *const Audio::WAVEFORMATEX, +) -> Result { + // Checking formats is not needed for shared mode with auto-conversion, therefore this check has been removed until someone implements WASAPI exclusive mode support + // I used an NAudio issue as reference: https://github.com/naudio/NAudio/issues/819 + + Ok(true) +} + +// Get a cpal Format from a WAVEFORMATEX. +unsafe fn format_from_waveformatex_ptr( + waveformatex_ptr: *const Audio::WAVEFORMATEX, + audio_client: &Audio::IAudioClient, +) -> Option { + fn cmp_guid(a: &GUID, b: &GUID) -> bool { + (a.data1, a.data2, a.data3, a.data4) == (b.data1, b.data2, b.data3, b.data4) + } + let sample_format = match ( + (*waveformatex_ptr).wBitsPerSample, + (*waveformatex_ptr).wFormatTag as u32, + ) { + (8, Audio::WAVE_FORMAT_PCM) => SampleFormat::U8, + (16, Audio::WAVE_FORMAT_PCM) => SampleFormat::I16, + (32, Multimedia::WAVE_FORMAT_IEEE_FLOAT) => SampleFormat::F32, + (n_bits, KernelStreaming::WAVE_FORMAT_EXTENSIBLE) => { + let waveformatextensible_ptr = waveformatex_ptr as *const Audio::WAVEFORMATEXTENSIBLE; + let sub = (*waveformatextensible_ptr).SubFormat; + + if cmp_guid(&sub, &KernelStreaming::KSDATAFORMAT_SUBTYPE_PCM) { + match n_bits { + 8 => SampleFormat::U8, + 16 => SampleFormat::I16, + 24 => SampleFormat::I24, + 32 => SampleFormat::I32, + 64 => SampleFormat::I64, + _ => return None, + } + } else if n_bits == 32 && cmp_guid(&sub, &Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) { + SampleFormat::F32 + } else { + return None; + } + } + // Unknown data format returned by GetMixFormat. + _ => return None, + }; + + let sample_rate = (*waveformatex_ptr).nSamplesPerSec; + + // GetBufferSizeLimits is only used for Hardware-Offloaded Audio + // Processing, which was added in Windows 8, which places hardware + // limits on the size of the audio buffer. If the sound system + // *isn't* using offloaded audio, we're using a software audio + // processing stack and have pretty much free rein to set buffer + // size. + // + // In software audio stacks GetBufferSizeLimits returns + // AUDCLNT_E_OFFLOAD_MODE_ONLY. + // + // https://docs.microsoft.com/en-us/windows-hardware/drivers/audio/hardware-offloaded-audio-processing + let (mut min_buffer_duration, mut max_buffer_duration) = (0, 0); + let buffer_size_is_limited = audio_client + .cast::() + .and_then(|audio_client| { + audio_client.GetBufferSizeLimits( + waveformatex_ptr, + true, + &mut min_buffer_duration, + &mut max_buffer_duration, + ) + }) + .is_ok(); + let buffer_size = if buffer_size_is_limited { + SupportedBufferSize::Range { + min: buffer_duration_to_frames(min_buffer_duration, sample_rate), + max: buffer_duration_to_frames(max_buffer_duration, sample_rate), + } + } else { + SupportedBufferSize::Range { + min: 0, + max: u32::MAX, + } + }; + + let format = SupportedStreamConfig { + channels: (*waveformatex_ptr).nChannels as _, + sample_rate, + buffer_size, + sample_format, + }; + Some(format) +} + +unsafe impl Send for Device {} +unsafe impl Sync for Device {} + +/// Maps PKEY_AudioEndpoint_JackSubType GUID to InterfaceType. +/// +/// The JackSubType property contains a KS node type GUID string from Ksmedia.h +/// that specifies the physical connector type. +fn jacksubtype_to_interface_type(guid_str: &str) -> Option { + let guid_upper = guid_str.to_uppercase(); + let typ = match guid_upper.as_str() { + "{D9E55EA0-0C89-4692-84FF-EB3C4B0D172F}" => InterfaceType::Hdmi, + "{E47E4031-3EA6-418D-8F9B-B73843CCB2AD}" => InterfaceType::DisplayPort, + "{DFF21CE1-F70F-11D0-B917-00A0C9223196}" => InterfaceType::Spdif, + _ => return None, + }; + + Some(typ) +} + +/// Maps WASAPI FormFactor values to DeviceType and optionally InterfaceType. +fn form_factor_to_types(form_factor: u32) -> (crate::DeviceType, Option) { + match form_factor { + 0 => (DeviceType::Unknown, Some(InterfaceType::Network)), // RemoteNetworkDevice + 1 => (DeviceType::Speaker, None), // Speakers + 2 => (DeviceType::Unknown, Some(InterfaceType::Line)), // LineLevel + 3 => (DeviceType::Headphones, None), // Headphones + 4 => (DeviceType::Microphone, None), // Microphone + 5 => (DeviceType::Headset, None), // Headset + 6 => (DeviceType::Handset, None), // Handset + 7 => (DeviceType::Unknown, None), // UnknownDigitalPassthrough + 8 => (DeviceType::Unknown, Some(InterfaceType::Spdif)), // SPDIF + 9 => (DeviceType::Unknown, Some(InterfaceType::Hdmi)), // DigitalAudioDisplayDevice + _ => (DeviceType::Unknown, None), // UnknownFormFactor or future values + } +} + +/// Maps WASAPI EnumeratorName to InterfaceType. +fn enumerator_to_interface_type(enumerator: &str) -> Option { + let typ = match enumerator.to_uppercase().as_str() { + "HDAUDIO" => InterfaceType::BuiltIn, + "USB" => InterfaceType::Usb, + "BTHENUM" => InterfaceType::Bluetooth, + "MMDEVAPI" | "SW" => InterfaceType::Virtual, + _ => return None, + }; + Some(typ) +} + +impl Device { + pub fn description(&self) -> Result { + unsafe { + // Open the device's property store. + let property_store = self + .device + .OpenPropertyStore(STGM_READ) + .expect("could not open property store"); + + // Query all available properties + let friendly_name = get_property_string( + &property_store, + &Properties::DEVPKEY_Device_FriendlyName as *const _ as *const _, + ); + + let device_desc = get_property_string( + &property_store, + &Properties::DEVPKEY_Device_DeviceDesc as *const _ as *const _, + ); + + let interface_name = get_property_string( + &property_store, + &Properties::DEVPKEY_DeviceInterface_FriendlyName as *const _ as *const _, + ); + + let enumerator_name = get_property_string( + &property_store, + &Properties::DEVPKEY_Device_EnumeratorName as *const _ as *const _, + ); + + let form_factor = get_property_u32( + &property_store, + &PKEY_AUDIOENDPOINT_FORMFACTOR as *const _ as *const _, + ); + + let jack_subtype = get_property_string( + &property_store, + &PKEY_AUDIOENDPOINT_JACKSUBTYPE as *const _ as *const _, + ); + + // Prefer DeviceDesc for name, fall back to FriendlyName + let name = device_desc + .clone() + .or(friendly_name.clone()) + .ok_or_else(|| DeviceNameError::BackendSpecific { + err: BackendSpecificError { + description: "failed to retrieve device name".to_string(), + }, + })?; + + // Get direction from data flow (eCapture = Input, eRender = Output) + let direction = self.data_flow().into(); + + // Determine device_type and initial interface_type from FormFactor + let (device_type, mut interface_type) = form_factor + .map(form_factor_to_types) + .unwrap_or((crate::DeviceType::Unknown, None)); + + // Override interface_type from EnumeratorName if available + if let Some(ref enumerator) = enumerator_name { + if let Some(itype) = enumerator_to_interface_type(enumerator) { + interface_type = Some(itype); + } + } + + // JackSubType has highest priority for interface_type + if let Some(ref jack_guid) = jack_subtype { + if let Some(itype) = jacksubtype_to_interface_type(jack_guid) { + interface_type = Some(itype); + } + } + + let mut builder = DeviceDescriptionBuilder::new(name) + .direction(direction) + .device_type(device_type); + + if let Some(itype) = interface_type { + builder = builder.interface_type(itype); + } + + // Add interface name to driver field if available + if let Some(iface_name) = interface_name { + builder = builder.driver(iface_name); + } + + // Add FriendlyName to extended if different from the name we used + if let Some(fname) = friendly_name { + if device_desc.is_some() && Some(&fname) != device_desc.as_ref() { + builder = builder.add_extended_line(fname); + } + } + + Ok(builder.build()) + } + } + + fn id(&self) -> Result { + unsafe { + match self.device.GetId() { + Ok(pwstr) => match pwstr.to_string() { + Ok(id_str) => Ok(DeviceId(crate::platform::HostId::Wasapi, id_str)), + Err(e) => Err(DeviceIdError::BackendSpecific { + err: BackendSpecificError { + description: format!("Failed to convert device ID to string: {}", e), + }, + }), + }, + Err(e) => Err(DeviceIdError::BackendSpecific { err: e.into() }), + } + } + } + + fn from_immdevice(device: Audio::IMMDevice) -> Self { + Device { + device, + future_audio_client: Arc::new(Mutex::new(None)), + } + } + + pub fn immdevice(&self) -> &Audio::IMMDevice { + &self.device + } + + /// Ensures that `future_audio_client` contains a `Some` and returns a locked mutex to it. + fn ensure_future_audio_client( + &self, + ) -> Result>, windows::core::Error> { + let mut lock = self.future_audio_client.lock().unwrap(); + if lock.is_some() { + return Ok(lock); + } + + let audio_client: Audio::IAudioClient = unsafe { + // can fail if the device has been disconnected since we enumerated it, or if + // the device doesn't support playback for some reason + self.device.Activate(Com::CLSCTX_ALL, None)? + }; + + *lock = Some(IAudioClientWrapper(audio_client)); + Ok(lock) + } + + /// Returns an uninitialized `IAudioClient`. + pub(crate) fn build_audioclient(&self) -> Result { + let mut lock = self.ensure_future_audio_client()?; + Ok(lock.take().unwrap().0) + } + + // There is no way to query the list of all formats that are supported by the + // audio processor, so instead we just trial some commonly supported formats. + // + // Common formats are trialed by first getting the default format (returned via + // `GetMixFormat`) and then mutating that format with common sample rates and + // querying them via `IsFormatSupported`. + // + // When calling `IsFormatSupported` with the shared-mode audio engine, only the default + // number of channels seems to be supported. Any, more or less returns an invalid + // parameter error. Thus, we just assume that the default number of channels is the only + // number supported. + fn supported_formats(&self) -> Result { + // initializing COM because we call `CoTaskMemFree` to release the format. + com::com_initialized(); + + // Retrieve the `IAudioClient`. + let lock = match self.ensure_future_audio_client() { + Ok(lock) => lock, + Err(ref e) if e.code() == Audio::AUDCLNT_E_DEVICE_INVALIDATED => { + return Err(SupportedStreamConfigsError::DeviceNotAvailable) + } + Err(e) => { + let description = format!("{}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + }; + let client = &lock.as_ref().unwrap().0; + + unsafe { + // Retrieve the pointer to the default WAVEFORMATEX. + let default_waveformatex_ptr = client + .GetMixFormat() + .map(WaveFormatExPtr) + .map_err(windows_err_to_cpal_err::)?; + + // If the default format can't succeed we have no hope of finding other formats. + if !is_format_supported(client, default_waveformatex_ptr.0)? { + let description = + "Could not determine support for default `WAVEFORMATEX`".to_string(); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + + let format = match format_from_waveformatex_ptr(default_waveformatex_ptr.0, client) { + Some(fmt) => fmt, + None => { + let description = + "could not create a `cpal::SupportedStreamConfig` from a `WAVEFORMATEX`" + .to_string(); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + }; + + let mut sample_rates: Vec = COMMON_SAMPLE_RATES.to_vec(); + + if !sample_rates.contains(&format.sample_rate) { + sample_rates.push(format.sample_rate) + } + + let mut supported_formats = Vec::new(); + + for sample_rate in sample_rates { + for sample_format in [ + SampleFormat::U8, + SampleFormat::I16, + SampleFormat::I24, + SampleFormat::U24, + SampleFormat::I32, + SampleFormat::I64, + SampleFormat::F32, + ] { + if let Some(waveformat) = config_to_waveformatextensible( + &StreamConfig { + channels: format.channels, + sample_rate, + buffer_size: BufferSize::Default, + }, + sample_format, + ) { + if is_format_supported( + client, + &waveformat.Format as *const Audio::WAVEFORMATEX, + )? { + supported_formats.push(SupportedStreamConfigRange { + channels: format.channels, + min_sample_rate: sample_rate, + max_sample_rate: sample_rate, + buffer_size: format.buffer_size, + sample_format, + }) + } + } + } + } + Ok(supported_formats.into_iter()) + } + } + + pub fn supported_input_configs( + &self, + ) -> Result { + if self.data_flow() == Audio::eCapture { + self.supported_formats() + // If it's an output device, assume no input formats. + } else { + Ok(vec![].into_iter()) + } + } + + pub fn supported_output_configs( + &self, + ) -> Result { + if self.data_flow() == Audio::eRender { + self.supported_formats() + // If it's an input device, assume no output formats. + } else { + Ok(vec![].into_iter()) + } + } + + // We always create voices in shared mode, therefore all samples go through an audio + // processor to mix them together. + // + // One format is guaranteed to be supported, the one returned by `GetMixFormat`. + fn default_format(&self) -> Result { + // initializing COM because we call `CoTaskMemFree` + com::com_initialized(); + + let lock = match self.ensure_future_audio_client() { + Ok(lock) => lock, + Err(ref e) if e.code() == Audio::AUDCLNT_E_DEVICE_INVALIDATED => { + return Err(DefaultStreamConfigError::DeviceNotAvailable) + } + Err(e) => { + let description = format!("{}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + }; + let client = &lock.as_ref().unwrap().0; + + unsafe { + let format_ptr = client + .GetMixFormat() + .map(WaveFormatExPtr) + .map_err(windows_err_to_cpal_err::)?; + + format_from_waveformatex_ptr(format_ptr.0, client) + .ok_or(DefaultStreamConfigError::StreamTypeNotSupported) + } + } + + pub(crate) fn data_flow(&self) -> Audio::EDataFlow { + let endpoint = Endpoint::from(self.device.clone()); + endpoint.data_flow() + } + + pub fn default_input_config(&self) -> Result { + if self.data_flow() == Audio::eCapture { + self.default_format() + } else { + Err(DefaultStreamConfigError::StreamTypeNotSupported) + } + } + + pub fn default_output_config(&self) -> Result { + let data_flow = self.data_flow(); + if data_flow == Audio::eRender { + self.default_format() + } else { + Err(DefaultStreamConfigError::StreamTypeNotSupported) + } + } + + pub(crate) fn build_input_stream_raw_inner( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + ) -> Result { + unsafe { + // Making sure that COM is initialized. + // It's not actually sure that this is required, but when in doubt do it. + com::com_initialized(); + + // Obtaining a `IAudioClient`. + let audio_client = match self.build_audioclient() { + Ok(client) => client, + Err(ref e) if e.code() == Audio::AUDCLNT_E_DEVICE_INVALIDATED => { + return Err(BuildStreamError::DeviceNotAvailable) + } + Err(e) => { + let description = format!("{}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + }; + + // Note: Buffer size validation is not needed here - `IAudioClient::Initialize` + // will return `AUDCLNT_E_BUFFER_SIZE_ERROR` if the buffer size is not supported. + let buffer_duration = buffer_size_to_duration(&config.buffer_size, config.sample_rate); + + let mut stream_flags = DEFAULT_FLAGS; + + if self.data_flow() == Audio::eRender { + stream_flags |= Audio::AUDCLNT_STREAMFLAGS_LOOPBACK; + } + + // Computing the format and initializing the device. + let waveformatex = { + let format_attempt = config_to_waveformatextensible(config, sample_format) + .ok_or(BuildStreamError::StreamConfigNotSupported)?; + let share_mode = Audio::AUDCLNT_SHAREMODE_SHARED; + + // Ensure the format is supported. + match super::device::is_format_supported(&audio_client, &format_attempt.Format) { + Ok(false) => return Err(BuildStreamError::StreamConfigNotSupported), + Err(_) => return Err(BuildStreamError::DeviceNotAvailable), + _ => (), + } + + // Finally, initializing the audio client + let hresult = audio_client.Initialize( + share_mode, + stream_flags, + buffer_duration, + 0, + &format_attempt.Format, + None, + ); + match hresult { + Err(ref e) if e.code() == Audio::AUDCLNT_E_DEVICE_INVALIDATED => { + return Err(BuildStreamError::DeviceNotAvailable); + } + Err(e) => { + let description = format!("{}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + Ok(()) => (), + }; + + format_attempt.Format + }; + + // obtaining the size of the samples buffer in number of frames + let max_frames_in_buffer = audio_client + .GetBufferSize() + .map_err(windows_err_to_cpal_err::)?; + + // Creating the event that will be signalled whenever we need to submit some samples. + let event = { + let event = + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null())) + .map_err(|e| { + let description = format!("failed to create event: {}", e); + let err = BackendSpecificError { description }; + BuildStreamError::from(err) + })?; + + if let Err(e) = audio_client.SetEventHandle(event) { + let description = format!("failed to call SetEventHandle: {}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + + event + }; + + // Building a `IAudioCaptureClient` that will be used to read captured samples. + let capture_client = audio_client + .GetService::() + .map_err(|e| { + windows_err_to_cpal_err_message::( + e, + "failed to build capture client: ", + ) + })?; + + // Once we built the `StreamInner`, we add a command that will be picked up by the + // `run()` method and added to the `RunContext`. + let client_flow = AudioClientFlow::Capture { capture_client }; + + let audio_clock = get_audio_clock(&audio_client)?; + + Ok(StreamInner { + audio_client, + audio_clock, + client_flow, + event, + playing: false, + max_frames_in_buffer, + bytes_per_frame: waveformatex.nBlockAlign, + config: config.clone(), + sample_format, + }) + } + } + + pub(crate) fn build_output_stream_raw_inner( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + ) -> Result { + unsafe { + // Making sure that COM is initialized. + // It's not actually sure that this is required, but when in doubt do it. + com::com_initialized(); + + // Obtaining a `IAudioClient`. + let audio_client = self + .build_audioclient() + .map_err(windows_err_to_cpal_err::)?; + + // Note: Buffer size validation is not needed here - `IAudioClient::Initialize` + // will return `AUDCLNT_E_BUFFER_SIZE_ERROR` if the buffer size is not supported. + let buffer_duration = buffer_size_to_duration(&config.buffer_size, config.sample_rate); + + // Computing the format and initializing the device. + let waveformatex = { + let format_attempt = config_to_waveformatextensible(config, sample_format) + .ok_or(BuildStreamError::StreamConfigNotSupported)?; + let share_mode = Audio::AUDCLNT_SHAREMODE_SHARED; + + // Ensure the format is supported. + match super::device::is_format_supported(&audio_client, &format_attempt.Format) { + Ok(false) => return Err(BuildStreamError::StreamConfigNotSupported), + Err(_) => return Err(BuildStreamError::DeviceNotAvailable), + _ => (), + } + + // Finally, initializing the audio client + audio_client + .Initialize( + share_mode, + DEFAULT_FLAGS, + buffer_duration, + 0, + &format_attempt.Format, + None, + ) + .map_err(windows_err_to_cpal_err::)?; + + format_attempt.Format + }; + + // Creating the event that will be signalled whenever we need to submit some samples. + let event = { + let event = + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null())) + .map_err(|e| { + let description = format!("failed to create event: {}", e); + let err = BackendSpecificError { description }; + BuildStreamError::from(err) + })?; + + if let Err(e) = audio_client.SetEventHandle(event) { + let description = format!("failed to call SetEventHandle: {}", e); + let err = BackendSpecificError { description }; + return Err(err.into()); + } + + event + }; + + // obtaining the size of the samples buffer in number of frames + let max_frames_in_buffer = audio_client.GetBufferSize().map_err(|e| { + windows_err_to_cpal_err_message::( + e, + "failed to obtain buffer size: ", + ) + })?; + + // Building a `IAudioRenderClient` that will be used to fill the samples buffer. + let render_client = audio_client + .GetService::() + .map_err(|e| { + windows_err_to_cpal_err_message::( + e, + "failed to build render client: ", + ) + })?; + + // Once we built the `StreamInner`, we add a command that will be picked up by the + // `run()` method and added to the `RunContext`. + let client_flow = AudioClientFlow::Render { render_client }; + + let audio_clock = get_audio_clock(&audio_client)?; + + Ok(StreamInner { + audio_client, + audio_clock, + client_flow, + event, + playing: false, + max_frames_in_buffer, + bytes_per_frame: waveformatex.nBlockAlign, + config: config.clone(), + sample_format, + }) + } + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Device) -> bool { + // Use case: In order to check whether the default device has changed + // the client code might need to compare the previous default device with the current one. + // The pointer comparison (`self.device == other.device`) don't work there, + // because the pointers are different even when the default device stays the same. + // + // In this code section we're trying to use the GetId method for the device comparison, cf. + // https://docs.microsoft.com/en-us/windows/desktop/api/mmdeviceapi/nf-mmdeviceapi-immdevice-getid + unsafe { + struct IdRAII(windows::core::PWSTR); + /// RAII for device IDs. + impl Drop for IdRAII { + fn drop(&mut self) { + unsafe { Com::CoTaskMemFree(Some(self.0 .0 as *mut _)) } + } + } + // GetId only fails with E_OUTOFMEMORY and if it does, we're probably dead already. + // Plus it won't do to change the device comparison logic unexpectedly. + let id1 = self.device.GetId().expect("cpal: GetId failure"); + let id1 = IdRAII(id1); + let id2 = other.device.GetId().expect("cpal: GetId failure"); + let id2 = IdRAII(id2); + // 16-bit null-terminated comparison. + let mut offset = 0; + loop { + let w1: u16 = *(id1.0).0.offset(offset); + let w2: u16 = *(id2.0).0.offset(offset); + if w1 == 0 && w2 == 0 { + return true; + } + if w1 != w2 { + return false; + } + offset += 1; + } + } + } +} + +impl Eq for Device {} + +impl std::hash::Hash for Device { + fn hash(&self, state: &mut H) { + // Hash the device ID for consistency with PartialEq + // SAFETY: GetId only fails with E_OUTOFMEMORY, which is unrecoverable. + // We need consistent hash/eq behavior. + unsafe { + use windows::Win32::System::Com; + + struct IdRAII(windows::core::PWSTR); + impl Drop for IdRAII { + fn drop(&mut self) { + unsafe { Com::CoTaskMemFree(Some(self.0 .0 as *mut _)) } + } + } + + let id = self.device.GetId().expect("cpal: GetId failure"); + let id = IdRAII(id); + + // Hash the 16-bit null-terminated string + let mut offset = 0; + loop { + let w: u16 = *(id.0).0.offset(offset); + if w == 0 { + break; + } + w.hash(state); + offset += 1; + } + } + } +} + +impl fmt::Debug for Device { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Device") + .field("device", &self.device) + .field("description", &self.description()) + .finish() + } +} + +impl From for Endpoint { + fn from(device: Audio::IMMDevice) -> Self { + unsafe { + let endpoint = immendpoint_from_immdevice(device); + Endpoint { endpoint } + } + } +} + +impl Endpoint { + fn data_flow(&self) -> Audio::EDataFlow { + unsafe { data_flow_from_immendpoint(&self.endpoint) } + } +} + +static ENUMERATOR: OnceLock = OnceLock::new(); + +fn get_enumerator() -> &'static Enumerator { + ENUMERATOR.get_or_init(|| { + // COM initialization is thread local, but we only need to have COM initialized in the + // thread we create the objects in + com::com_initialized(); + + // building the devices enumerator object + unsafe { + let enumerator = Com::CoCreateInstance::<_, Audio::IMMDeviceEnumerator>( + &Audio::MMDeviceEnumerator, + None, + Com::CLSCTX_ALL, + ) + .unwrap(); + + Enumerator(enumerator) + } + }) +} + +// Helper function to query a DWORD property from a WASAPI device property store +unsafe fn get_property_u32( + property_store: &IPropertyStore, + property_key: *const PROPERTYKEY, +) -> Option { + let mut property_value = property_store.GetValue(property_key).ok()?; + let prop_variant = &property_value.Anonymous.Anonymous; + + // Check if it's a UI4 (unsigned 32-bit integer) + if prop_variant.vt != VT_UI4 { + return None; + } + + let value = *(&prop_variant.Anonymous as *const _ as *const u32); + + // Clean up the property + StructuredStorage::PropVariantClear(&mut property_value).ok(); + + Some(value) +} + +// Helper function to query a string property from a WASAPI device property store +unsafe fn get_property_string( + property_store: &IPropertyStore, + property_key: *const PROPERTYKEY, +) -> Option { + let mut property_value = property_store.GetValue(property_key).ok()?; + let prop_variant = &property_value.Anonymous.Anonymous; + + // Read the string from the union data field, expecting a *const u16. + if prop_variant.vt != VT_LPWSTR { + return None; + } + let ptr_utf16 = *(&prop_variant.Anonymous as *const _ as *const *const u16); + + // Find the length of the null-terminated string with a safety limit + const MAX_STRING_LEN: usize = 32768; // 32K characters should be more than enough + let mut len = 0; + while len < MAX_STRING_LEN && *ptr_utf16.add(len) != 0 { + len += 1; + } + + // If we hit the limit, the string is likely malformed (not null-terminated) + if len >= MAX_STRING_LEN { + return None; + } + + // Create the utf16 slice and convert it into a string. + let string_slice = slice::from_raw_parts(ptr_utf16, len); + let os_string: OsString = OsStringExt::from_wide(string_slice); + let result = match os_string.into_string() { + Ok(string) => Some(string), + Err(os_string) => Some(os_string.to_string_lossy().into()), + }; + + // Clean up the property. + StructuredStorage::PropVariantClear(&mut property_value).ok(); + + result +} + +/// Send/Sync wrapper around `IMMDeviceEnumerator`. +struct Enumerator(Audio::IMMDeviceEnumerator); + +unsafe impl Send for Enumerator {} +unsafe impl Sync for Enumerator {} + +/// WASAPI implementation for `Devices`. +pub struct Devices { + collection: Audio::IMMDeviceCollection, + total_count: u32, + next_item: u32, +} + +impl Devices { + pub fn new() -> Result { + unsafe { + // can fail because of wrong parameters (should never happen) or out of memory + let collection = get_enumerator() + .0 + .EnumAudioEndpoints(Audio::eAll, Audio::DEVICE_STATE_ACTIVE) + .map_err(BackendSpecificError::from)?; + + let count = collection.GetCount().map_err(BackendSpecificError::from)?; + + Ok(Devices { + collection, + total_count: count, + next_item: 0, + }) + } + } +} + +unsafe impl Send for Devices {} +unsafe impl Sync for Devices {} + +impl Iterator for Devices { + type Item = Device; + + fn next(&mut self) -> Option { + if self.next_item >= self.total_count { + return None; + } + + unsafe { + let device = self.collection.Item(self.next_item).unwrap(); + self.next_item += 1; + Some(Device::from_immdevice(device)) + } + } + + fn size_hint(&self) -> (usize, Option) { + let num = self.total_count - self.next_item; + let num = num as usize; + (num, Some(num)) + } +} + +fn default_device(data_flow: Audio::EDataFlow) -> Option { + unsafe { + let device = get_enumerator() + .0 + .GetDefaultAudioEndpoint(data_flow, Audio::eConsole) + .ok()?; + // TODO: check specifically for `E_NOTFOUND`, and panic otherwise + Some(Device::from_immdevice(device)) + } +} + +pub fn default_input_device() -> Option { + default_device(Audio::eCapture) +} + +pub fn default_output_device() -> Option { + default_device(Audio::eRender) +} + +/// Get the audio clock used to produce `StreamInstant`s. +unsafe fn get_audio_clock( + audio_client: &Audio::IAudioClient, +) -> Result { + audio_client + .GetService::() + .map_err(|e| { + windows_err_to_cpal_err_message::(e, "failed to build audio clock: ") + }) +} + +// Turns a `Format` into a `WAVEFORMATEXTENSIBLE`. +// +// Returns `None` if the WAVEFORMATEXTENSIBLE does not support the given format. +fn config_to_waveformatextensible( + config: &StreamConfig, + sample_format: SampleFormat, +) -> Option { + let format_tag = match sample_format { + SampleFormat::U8 | SampleFormat::I16 => Audio::WAVE_FORMAT_PCM, + + SampleFormat::I24 + | SampleFormat::U24 + | SampleFormat::I32 + | SampleFormat::I64 + | SampleFormat::F32 => KernelStreaming::WAVE_FORMAT_EXTENSIBLE, + + _ => return None, + }; + let channels = config.channels; + let sample_rate = config.sample_rate; + let sample_bytes = sample_format.sample_size() as u16; + let avg_bytes_per_sec = u32::from(channels) * sample_rate * u32::from(sample_bytes); + let block_align = channels * sample_bytes; + let bits_per_sample = match sample_format { + // 24-bit formats use 32-bit storage but only 24 valid bits + SampleFormat::I24 | SampleFormat::U24 => 24, + _ => 8 * sample_bytes, + }; + + let cb_size = if format_tag == Audio::WAVE_FORMAT_PCM { + 0 + } else { + let extensible_size = mem::size_of::(); + let ex_size = mem::size_of::(); + (extensible_size - ex_size) as u16 + }; + + let waveformatex = Audio::WAVEFORMATEX { + wFormatTag: format_tag as u16, + nChannels: channels, + nSamplesPerSec: sample_rate, + nAvgBytesPerSec: avg_bytes_per_sec, + nBlockAlign: block_align, + wBitsPerSample: bits_per_sample, + cbSize: cb_size, + }; + + // CPAL does not care about speaker positions, so pass audio right through. + let channel_mask = KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT; + + let sub_format = match sample_format { + SampleFormat::U8 + | SampleFormat::I16 + | SampleFormat::I24 + | SampleFormat::U24 + | SampleFormat::I32 + | SampleFormat::I64 => KernelStreaming::KSDATAFORMAT_SUBTYPE_PCM, + + SampleFormat::F32 => Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, + _ => return None, + }; + + let waveformatextensible = Audio::WAVEFORMATEXTENSIBLE { + Format: waveformatex, + Samples: Audio::WAVEFORMATEXTENSIBLE_0 { + wSamplesPerBlock: bits_per_sample, + }, + dwChannelMask: channel_mask, + SubFormat: sub_format, + }; + + Some(waveformatextensible) +} + +fn buffer_size_to_duration(buffer_size: &BufferSize, sample_rate: u32) -> i64 { + match buffer_size { + BufferSize::Fixed(frames) => *frames as i64 * (1_000_000_000 / 100) / sample_rate as i64, + BufferSize::Default => 0, + } +} + +fn buffer_duration_to_frames(buffer_duration: i64, sample_rate: u32) -> FrameCount { + (buffer_duration * sample_rate as i64 * 100 / 1_000_000_000) as FrameCount +} diff --git a/vendor/cpal/src/host/wasapi/mod.rs b/vendor/cpal/src/host/wasapi/mod.rs new file mode 100644 index 0000000..2becafa --- /dev/null +++ b/vendor/cpal/src/host/wasapi/mod.rs @@ -0,0 +1,112 @@ +//! WASAPI backend implementation. +//! +//! Default backend on Windows. + +#[allow(unused_imports)] +pub use self::device::{ + default_input_device, default_output_device, Device, Devices, SupportedInputConfigs, + SupportedOutputConfigs, +}; +#[allow(unused_imports)] +pub use self::stream::Stream; +use crate::traits::HostTrait; +use crate::BackendSpecificError; +use crate::DevicesError; +use std::io::Error as IoError; +use windows::Win32::Media::Audio; + +mod com; +mod device; +mod stream; + +/// The WASAPI host, the default windows host type. +/// +/// Note: If you use a WASAPI output device as an input device it will +/// transparently enable loopback mode (see +/// https://docs.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording). +#[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 WASAPI is always available on Windows. + 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() + } +} + +impl From for BackendSpecificError { + fn from(error: windows::core::Error) -> Self { + BackendSpecificError { + description: format!("{}", IoError::from(error)), + } + } +} + +trait ErrDeviceNotAvailable: From { + fn device_not_available() -> Self; +} + +impl ErrDeviceNotAvailable for crate::BuildStreamError { + fn device_not_available() -> Self { + Self::DeviceNotAvailable + } +} + +impl ErrDeviceNotAvailable for crate::SupportedStreamConfigsError { + fn device_not_available() -> Self { + Self::DeviceNotAvailable + } +} + +impl ErrDeviceNotAvailable for crate::DefaultStreamConfigError { + fn device_not_available() -> Self { + Self::DeviceNotAvailable + } +} + +impl ErrDeviceNotAvailable for crate::StreamError { + fn device_not_available() -> Self { + Self::DeviceNotAvailable + } +} + +fn windows_err_to_cpal_err(e: windows::core::Error) -> E { + windows_err_to_cpal_err_message::(e, "") +} + +fn windows_err_to_cpal_err_message( + e: windows::core::Error, + message: &str, +) -> E { + match e.code() { + Audio::AUDCLNT_E_DEVICE_INVALIDATED | Audio::AUDCLNT_E_DEVICE_IN_USE => { + E::device_not_available() + } + _ => { + let description = format!("{}{}", message, e); + let err = BackendSpecificError { description }; + err.into() + } + } +} diff --git a/vendor/cpal/src/host/wasapi/stream.rs b/vendor/cpal/src/host/wasapi/stream.rs new file mode 100644 index 0000000..330b6d1 --- /dev/null +++ b/vendor/cpal/src/host/wasapi/stream.rs @@ -0,0 +1,600 @@ +use super::windows_err_to_cpal_err; +use crate::traits::StreamTrait; +use crate::{ + BackendSpecificError, BufferSize, Data, InputCallbackInfo, OutputCallbackInfo, + PauseStreamError, PlayStreamError, SampleFormat, StreamError, +}; +use std::mem; +use std::ptr; +use std::sync::mpsc::{channel, Receiver, SendError, Sender}; +use std::thread::{self, JoinHandle}; +use windows::Win32::Foundation; +use windows::Win32::Foundation::WAIT_OBJECT_0; +use windows::Win32::Media::Audio; +use windows::Win32::System::SystemServices; +use windows::Win32::System::Threading; + +pub struct Stream { + /// The high-priority audio processing thread calling callbacks. + /// Option used for moving out in destructor. + /// + /// TODO: Actually set the thread priority. + thread: Option>, + + // Commands processed by the `run()` method that is currently running. + // `pending_scheduled_event` must be signalled whenever a command is added here, so that it + // will get picked up. + commands: Sender, + + // This event is signalled after a new entry is added to `commands`, so that the `run()` + // method can be notified. + pending_scheduled_event: Foundation::HANDLE, +} + +// SAFETY: Windows Event HANDLEs are safe to send between threads - they are designed for +// synchronization. All fields of Stream are Send: +// - JoinHandle<()> is Send +// - Sender is Send +// - Foundation::HANDLE is Send (Windows synchronization primitive) +// See: https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventa +unsafe impl Send for Stream {} + +// SAFETY: Windows Event HANDLEs are safe to access from multiple threads simultaneously. +// All synchronization operations (SetEvent, WaitForSingleObject) are thread-safe. +// All fields of Stream are Sync: +// - JoinHandle<()> is Sync +// - Sender is Sync (uses internal synchronization) +// - Foundation::HANDLE for event objects supports concurrent access +// The audio thread owns all COM objects, so no cross-thread COM access occurs. +unsafe impl Sync for Stream {} + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +struct RunContext { + // Streams that have been created in this event loop. + stream: StreamInner, + + // Handles corresponding to the `event` field of each element of `voices`. Must always be in + // sync with `voices`, except that the first element is always `pending_scheduled_event`. + handles: Vec, + + commands: Receiver, +} + +// Once we start running the eventloop, the RunContext will not be moved. +unsafe impl Send for RunContext {} + +pub enum Command { + PlayStream, + PauseStream, + Terminate, +} + +pub enum AudioClientFlow { + Render { + render_client: Audio::IAudioRenderClient, + }, + Capture { + capture_client: Audio::IAudioCaptureClient, + }, +} + +pub struct StreamInner { + pub audio_client: Audio::IAudioClient, + pub audio_clock: Audio::IAudioClock, + pub client_flow: AudioClientFlow, + // Event that is signalled by WASAPI whenever audio data must be written. + pub event: Foundation::HANDLE, + // True if the stream is currently playing. False if paused. + pub playing: bool, + // Number of frames of audio data in the underlying buffer allocated by WASAPI. + pub max_frames_in_buffer: u32, + // Number of bytes that each frame occupies. + pub bytes_per_frame: u16, + // The configuration with which the stream was created. + pub config: crate::StreamConfig, + // The sample format with which the stream was created. + pub sample_format: SampleFormat, +} + +impl Stream { + pub(crate) fn new_input( + stream_inner: StreamInner, + mut data_callback: D, + mut error_callback: E, + ) -> Stream + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let pending_scheduled_event = unsafe { + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null())) + } + .expect("cpal: could not create input stream event"); + let (tx, rx) = channel(); + + let run_context = RunContext { + handles: vec![pending_scheduled_event, stream_inner.event], + stream: stream_inner, + commands: rx, + }; + + let thread = thread::Builder::new() + .name("cpal_wasapi_in".to_owned()) + .spawn(move || run_input(run_context, &mut data_callback, &mut error_callback)) + .unwrap(); + + Stream { + thread: Some(thread), + commands: tx, + pending_scheduled_event, + } + } + + pub(crate) fn new_output( + stream_inner: StreamInner, + mut data_callback: D, + mut error_callback: E, + ) -> Stream + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let pending_scheduled_event = unsafe { + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null())) + } + .expect("cpal: could not create output stream event"); + let (tx, rx) = channel(); + + let run_context = RunContext { + handles: vec![pending_scheduled_event, stream_inner.event], + stream: stream_inner, + commands: rx, + }; + + let thread = thread::Builder::new() + .name("cpal_wasapi_out".to_owned()) + .spawn(move || run_output(run_context, &mut data_callback, &mut error_callback)) + .unwrap(); + + Stream { + thread: Some(thread), + commands: tx, + pending_scheduled_event, + } + } + + fn push_command(&self, command: Command) -> Result<(), SendError> { + self.commands.send(command)?; + unsafe { + Threading::SetEvent(self.pending_scheduled_event).unwrap(); + } + Ok(()) + } +} + +impl Drop for Stream { + fn drop(&mut self) { + if self.push_command(Command::Terminate).is_ok() { + if let Some(handle) = self.thread.take() { + let _ = handle.join(); + } + unsafe { + let _ = Foundation::CloseHandle(self.pending_scheduled_event); + } + } + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + self.push_command(Command::PlayStream) + .map_err(|_| crate::error::PlayStreamError::DeviceNotAvailable)?; + Ok(()) + } + + fn pause(&self) -> Result<(), PauseStreamError> { + self.push_command(Command::PauseStream) + .map_err(|_| crate::error::PauseStreamError::DeviceNotAvailable)?; + Ok(()) + } +} + +impl Drop for StreamInner { + fn drop(&mut self) { + unsafe { + let _ = Foundation::CloseHandle(self.event); + } + } +} + +// Process any pending commands that are queued within the `RunContext`. +// Returns `true` if the loop should continue running, `false` if it should terminate. +fn process_commands(run_context: &mut RunContext) -> Result { + // Process the pending commands. + for command in run_context.commands.try_iter() { + match command { + Command::PlayStream => unsafe { + if !run_context.stream.playing { + run_context + .stream + .audio_client + .Start() + .map_err(windows_err_to_cpal_err::)?; + run_context.stream.playing = true; + } + }, + Command::PauseStream => unsafe { + if run_context.stream.playing { + run_context + .stream + .audio_client + .Stop() + .map_err(windows_err_to_cpal_err::)?; + run_context.stream.playing = false; + } + }, + Command::Terminate => { + return Ok(false); + } + } + } + + Ok(true) +} +// Wait for any of the given handles to be signalled. +// +// Returns the index of the `handle` that was signalled, or an `Err` if +// `WaitForMultipleObjectsEx` fails. +// +// This is called when the `run` thread is ready to wait for the next event. The +// next event might be some command submitted by the user (the first handle) or +// might indicate that one of the streams is ready to deliver or receive audio. +fn wait_for_handle_signal(handles: &[Foundation::HANDLE]) -> Result { + debug_assert!(handles.len() <= SystemServices::MAXIMUM_WAIT_OBJECTS as usize); + let result = unsafe { + Threading::WaitForMultipleObjectsEx( + handles, + false, // Don't wait for all, just wait for the first + Threading::INFINITE, // TODO: allow setting a timeout + false, // irrelevant parameter here + ) + }; + if result == Foundation::WAIT_FAILED { + let err = unsafe { Foundation::GetLastError() }; + let description = format!("`WaitForMultipleObjectsEx failed: {:?}", err); + let err = BackendSpecificError { description }; + return Err(err); + } + // Notifying the corresponding task handler. + let handle_idx = (result.0 - WAIT_OBJECT_0.0) as usize; + Ok(handle_idx) +} + +// Get the number of available frames that are available for writing/reading. +fn get_available_frames(stream: &StreamInner) -> Result { + unsafe { + let padding = stream + .audio_client + .GetCurrentPadding() + .map_err(windows_err_to_cpal_err::)?; + Ok(stream.max_frames_in_buffer - padding) + } +} + +fn run_input( + mut run_ctxt: RunContext, + data_callback: &mut dyn FnMut(&Data, &InputCallbackInfo), + error_callback: &mut dyn FnMut(StreamError), +) { + boost_current_thread_priority( + run_ctxt.stream.config.buffer_size, + run_ctxt.stream.config.sample_rate, + ); + + loop { + match process_commands_and_await_signal(&mut run_ctxt, error_callback) { + Some(ControlFlow::Break) => break, + Some(ControlFlow::Continue) => continue, + None => (), + } + let capture_client = match run_ctxt.stream.client_flow { + AudioClientFlow::Capture { ref capture_client } => capture_client.clone(), + _ => unreachable!(), + }; + match process_input( + &run_ctxt.stream, + capture_client, + data_callback, + error_callback, + ) { + ControlFlow::Break => break, + ControlFlow::Continue => continue, + } + } +} + +fn run_output( + mut run_ctxt: RunContext, + data_callback: &mut dyn FnMut(&mut Data, &OutputCallbackInfo), + error_callback: &mut dyn FnMut(StreamError), +) { + boost_current_thread_priority( + run_ctxt.stream.config.buffer_size, + run_ctxt.stream.config.sample_rate, + ); + + loop { + match process_commands_and_await_signal(&mut run_ctxt, error_callback) { + Some(ControlFlow::Break) => break, + Some(ControlFlow::Continue) => continue, + None => (), + } + let render_client = match run_ctxt.stream.client_flow { + AudioClientFlow::Render { ref render_client } => render_client.clone(), + _ => unreachable!(), + }; + match process_output( + &run_ctxt.stream, + render_client, + data_callback, + error_callback, + ) { + ControlFlow::Break => break, + ControlFlow::Continue => continue, + } + } +} + +#[cfg(feature = "audio_thread_priority")] +fn boost_current_thread_priority(buffer_size: BufferSize, sample_rate: crate::SampleRate) { + use audio_thread_priority::promote_current_thread_to_real_time; + + let buffer_size = if let BufferSize::Fixed(buffer_size) = buffer_size { + buffer_size + } else { + // if the buffer size isn't fixed, let audio_thread_priority choose a sensible default value + 0 + }; + + if let Err(err) = promote_current_thread_to_real_time(buffer_size, sample_rate) { + eprintln!("Failed to promote audio thread to real-time priority: {err}"); + } +} + +#[cfg(not(feature = "audio_thread_priority"))] +fn boost_current_thread_priority(_: BufferSize, _: crate::SampleRate) { + unsafe { + let thread_handle = Threading::GetCurrentThread(); + + let _ = + Threading::SetThreadPriority(thread_handle, Threading::THREAD_PRIORITY_TIME_CRITICAL); + } +} + +enum ControlFlow { + Break, + Continue, +} + +fn process_commands_and_await_signal( + run_context: &mut RunContext, + error_callback: &mut dyn FnMut(StreamError), +) -> Option { + // Process queued commands. + match process_commands(run_context) { + Ok(true) => (), + Ok(false) => return Some(ControlFlow::Break), + Err(err) => { + error_callback(err); + return Some(ControlFlow::Break); + } + }; + + // Wait for any of the handles to be signalled. + let handle_idx = match wait_for_handle_signal(&run_context.handles) { + Ok(idx) => idx, + Err(err) => { + error_callback(err.into()); + return Some(ControlFlow::Break); + } + }; + + // If `handle_idx` is 0, then it's `pending_scheduled_event` that was signalled in + // order for us to pick up the pending commands. Otherwise, a stream needs data. + if handle_idx == 0 { + return Some(ControlFlow::Continue); + } + + None +} + +// The loop for processing pending input data. +fn process_input( + stream: &StreamInner, + capture_client: Audio::IAudioCaptureClient, + data_callback: &mut dyn FnMut(&Data, &InputCallbackInfo), + error_callback: &mut dyn FnMut(StreamError), +) -> ControlFlow { + unsafe { + // Get the available data in the shared buffer. + let mut buffer: *mut u8 = ptr::null_mut(); + let mut flags = mem::MaybeUninit::uninit(); + loop { + let mut frames_available = match capture_client.GetNextPacketSize() { + Ok(0) => return ControlFlow::Continue, + Ok(f) => f, + Err(err) => { + error_callback(windows_err_to_cpal_err(err)); + return ControlFlow::Break; + } + }; + let mut qpc_position: u64 = 0; + let result = capture_client.GetBuffer( + &mut buffer, + &mut frames_available, + flags.as_mut_ptr(), + None, + Some(&mut qpc_position), + ); + + match result { + // TODO: Can this happen? + Err(e) if e.code() == Audio::AUDCLNT_S_BUFFER_EMPTY => continue, + Err(e) => { + error_callback(windows_err_to_cpal_err(e)); + return ControlFlow::Break; + } + Ok(_) => (), + } + + debug_assert!(!buffer.is_null()); + + let data = buffer as *mut (); + let len = frames_available as usize * stream.bytes_per_frame as usize + / stream.sample_format.sample_size(); + let data = Data::from_parts(data, len, stream.sample_format); + + // The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds. + let timestamp = match input_timestamp(stream, qpc_position) { + Ok(ts) => ts, + Err(err) => { + error_callback(err); + return ControlFlow::Break; + } + }; + let info = InputCallbackInfo { timestamp }; + data_callback(&data, &info); + + // Release the buffer. + let result = capture_client + .ReleaseBuffer(frames_available) + .map_err(windows_err_to_cpal_err); + if let Err(err) = result { + error_callback(err); + return ControlFlow::Break; + } + } + } +} + +// The loop for writing output data. +fn process_output( + stream: &StreamInner, + render_client: Audio::IAudioRenderClient, + data_callback: &mut dyn FnMut(&mut Data, &OutputCallbackInfo), + error_callback: &mut dyn FnMut(StreamError), +) -> ControlFlow { + // The number of frames available for writing. + let frames_available = match get_available_frames(stream) { + Ok(0) => return ControlFlow::Continue, // TODO: Can this happen? + Ok(n) => n, + Err(err) => { + error_callback(err); + return ControlFlow::Break; + } + }; + + unsafe { + let buffer = match render_client.GetBuffer(frames_available) { + Ok(b) => b, + Err(e) => { + error_callback(windows_err_to_cpal_err(e)); + return ControlFlow::Break; + } + }; + + debug_assert!(!buffer.is_null()); + + let data = buffer as *mut (); + let len = frames_available as usize * stream.bytes_per_frame as usize + / stream.sample_format.sample_size(); + let mut data = Data::from_parts(data, len, stream.sample_format); + let sample_rate = stream.config.sample_rate; + let timestamp = match output_timestamp(stream, frames_available, sample_rate) { + Ok(ts) => ts, + Err(err) => { + error_callback(err); + return ControlFlow::Break; + } + }; + let info = OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); + + if let Err(err) = render_client.ReleaseBuffer(frames_available, 0) { + error_callback(windows_err_to_cpal_err(err)); + return ControlFlow::Break; + } + } + + ControlFlow::Continue +} + +/// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +fn frames_to_duration(frames: u32, rate: crate::SampleRate) -> std::time::Duration { + let secsf = frames as f64 / rate as f64; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} + +/// Use the stream's `IAudioClock` to produce the current stream instant. +/// +/// Uses the QPC position produced via the `GetPosition` method. +fn stream_instant(stream: &StreamInner) -> Result { + let mut position: u64 = 0; + let mut qpc_position: u64 = 0; + unsafe { + stream + .audio_clock + .GetPosition(&mut position, Some(&mut qpc_position)) + .map_err(windows_err_to_cpal_err::)?; + }; + // The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds. + let qpc_nanos = qpc_position as i128 * 100; + let instant = crate::StreamInstant::from_nanos_i128(qpc_nanos) + .expect("performance counter out of range of `StreamInstant` representation"); + Ok(instant) +} + +/// Produce the input stream timestamp. +/// +/// `buffer_qpc_position` is the `qpc_position` returned via the `GetBuffer` call on the capture +/// client. It represents the instant at which the first sample of the retrieved buffer was +/// captured. +fn input_timestamp( + stream: &StreamInner, + buffer_qpc_position: u64, +) -> Result { + // The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds. + let qpc_nanos = buffer_qpc_position as i128 * 100; + let capture = crate::StreamInstant::from_nanos_i128(qpc_nanos) + .expect("performance counter out of range of `StreamInstant` representation"); + let callback = stream_instant(stream)?; + Ok(crate::InputStreamTimestamp { capture, callback }) +} + +/// Produce the output stream timestamp. +/// +/// `frames_available` is the number of frames available for writing as reported by subtracting the +/// result of `GetCurrentPadding` from the maximum buffer size. +/// +/// `sample_rate` is the rate at which audio frames are processed by the device. +/// +/// TODO: The returned `playback` is an estimate that assumes audio is delivered immediately after +/// `frames_available` are consumed. The reality is that there is likely a tiny amount of latency +/// after this, but not sure how to determine this. +fn output_timestamp( + stream: &StreamInner, + frames_available: u32, + sample_rate: crate::SampleRate, +) -> Result { + let callback = stream_instant(stream)?; + let buffer_duration = frames_to_duration(frames_available, sample_rate); + let playback = callback + .add(buffer_duration) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + Ok(crate::OutputStreamTimestamp { callback, playback }) +} diff --git a/vendor/cpal/src/host/webaudio/mod.rs b/vendor/cpal/src/host/webaudio/mod.rs new file mode 100644 index 0000000..4ca7a29 --- /dev/null +++ b/vendor/cpal/src/host/webaudio/mod.rs @@ -0,0 +1,547 @@ +//! Web Audio backend implementation. +//! +//! Default backend on WebAssembly. + +extern crate js_sys; +extern crate wasm_bindgen; +extern crate web_sys; + +use self::wasm_bindgen::prelude::*; +use self::wasm_bindgen::JsCast; +use self::web_sys::{AudioContext, AudioContextOptions}; +use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crate::{ + BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError, + DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError, DeviceNameError, + DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, + SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, +}; +use std::ops::DerefMut; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::Duration; + +/// Type alias for shared closure handles used in audio callbacks +type ClosureHandle = Arc>>>; + +/// Content is false if the iterator is empty. +pub struct Devices(bool); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Device; + +pub struct Host; + +pub struct Stream { + ctx: Arc, + on_ended_closures: Vec, + config: StreamConfig, + buffer_size_frames: usize, +} + +// WASM runs in a single-threaded environment, so Send and Sync are safe by design. +unsafe impl Send for Stream {} +unsafe impl Sync for Stream {} + +// Compile-time assertion that Stream is Send and Sync +crate::assert_stream_send!(Stream); +crate::assert_stream_sync!(Stream); + +pub use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; + +const MIN_CHANNELS: u16 = 1; +const MAX_CHANNELS: u16 = 32; +const MIN_SAMPLE_RATE: SampleRate = 8_000; +const MAX_SAMPLE_RATE: SampleRate = 96_000; +const DEFAULT_SAMPLE_RATE: SampleRate = 44_100; +const MIN_BUFFER_SIZE: u32 = 1; +const MAX_BUFFER_SIZE: u32 = u32::MAX; +const DEFAULT_BUFFER_SIZE: usize = 2048; +const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; + +impl Host { + pub fn new() -> Result { + Ok(Host) + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + // Assume this host is always available on webaudio. + 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() + } +} + +impl Devices { + fn new() -> Result { + Ok(Self::default()) + } +} + +impl Device { + fn description(&self) -> Result { + Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) + .direction(crate::DeviceDirection::Output) + .build()) + } + + fn id(&self) -> Result { + Ok(DeviceId( + crate::platform::HostId::WebAudio, + "default".to_string(), + )) + } + + fn supported_input_configs( + &self, + ) -> Result { + // TODO + Ok(Vec::new().into_iter()) + } + + fn supported_output_configs( + &self, + ) -> Result { + let buffer_size = SupportedBufferSize::Range { + min: MIN_BUFFER_SIZE, + max: MAX_BUFFER_SIZE, + }; + let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS) + .map(|channels| SupportedStreamConfigRange { + channels, + min_sample_rate: MIN_SAMPLE_RATE, + max_sample_rate: MAX_SAMPLE_RATE, + buffer_size, + sample_format: SUPPORTED_SAMPLE_FORMAT, + }) + .collect(); + Ok(configs.into_iter()) + } + + fn default_input_config(&self) -> Result { + // TODO + Err(DefaultStreamConfigError::StreamTypeNotSupported) + } + + fn default_output_config(&self) -> Result { + const EXPECT: &str = "expected at least one valid webaudio stream config"; + let config = self + .supported_output_configs() + .expect(EXPECT) + .max_by(|a, b| a.cmp_default_heuristics(b)) + .unwrap() + .with_sample_rate(DEFAULT_SAMPLE_RATE); + + Ok(config) + } +} + +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(StreamError) + Send + 'static, + { + // TODO + Err(BuildStreamError::StreamConfigNotSupported) + } + + /// 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(StreamError) + Send + 'static, + { + if !valid_config(config, sample_format) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + + let n_channels = config.channels as usize; + + let buffer_size_frames = match config.buffer_size { + BufferSize::Fixed(v) => { + if !(MIN_BUFFER_SIZE..=MAX_BUFFER_SIZE).contains(&v) { + return Err(BuildStreamError::StreamConfigNotSupported); + } + v as usize + } + BufferSize::Default => DEFAULT_BUFFER_SIZE, + }; + let buffer_size_samples = buffer_size_frames * n_channels; + let buffer_time_step_secs = buffer_time_step_secs(buffer_size_frames, config.sample_rate); + + let data_callback = Arc::new(Mutex::new(Box::new(data_callback))); + + // Create the WebAudio stream. + let stream_opts = AudioContextOptions::new(); + stream_opts.set_sample_rate(config.sample_rate as f32); + let ctx = AudioContext::new_with_context_options(&stream_opts).map_err( + |err| -> BuildStreamError { + let description = format!("{:?}", err); + let err = BackendSpecificError { description }; + err.into() + }, + )?; + + let destination = ctx.destination(); + + // If possible, set the destination's channel_count to the given config.channel. + // If not, fallback on the default destination channel_count to keep previous behavior + // and do not return an error. + if config.channels as u32 <= destination.max_channel_count() { + destination.set_channel_count(config.channels as u32); + } + + // SAFETY: WASM is single-threaded, so Arc is safe even though AudioContext is not Send/Sync + #[allow(clippy::arc_with_non_send_sync)] + let ctx = Arc::new(ctx); + + // A container for managing the lifecycle of the audio callbacks. + let mut on_ended_closures: Vec = Vec::new(); + + // A cursor keeping track of the current time at which new frames should be scheduled. + let time = Arc::new(RwLock::new(0f64)); + + // Create a set of closures / callbacks which will continuously fetch and schedule sample + // playback. Starting with two workers, e.g. a front and back buffer so that audio frames + // can be fetched in the background. + for _i in 0..2 { + let data_callback_handle = data_callback.clone(); + let ctx_handle = ctx.clone(); + let time_handle = time.clone(); + + // A set of temporary buffers to be used for intermediate sample transformation steps. + let mut temporary_buffer = vec![0f32; buffer_size_samples]; + let mut temporary_channel_buffer = vec![0f32; buffer_size_frames]; + + #[cfg(target_feature = "atomics")] + let temporary_channel_array_view: js_sys::Float32Array; + #[cfg(target_feature = "atomics")] + { + let temporary_channel_array = js_sys::ArrayBuffer::new( + (std::mem::size_of::() * buffer_size_frames) as u32, + ); + temporary_channel_array_view = js_sys::Float32Array::new(&temporary_channel_array); + } + + // Create a webaudio buffer which will be reused to avoid allocations. + let ctx_buffer = ctx + .create_buffer( + config.channels as u32, + buffer_size_frames as u32, + config.sample_rate as f32, + ) + .map_err(|err| -> BuildStreamError { + let description = format!("{:?}", err); + let err = BackendSpecificError { description }; + err.into() + })?; + + // A self reference to this closure for passing to future audio event calls. + // SAFETY: WASM is single-threaded, so Arc is safe even though Closure is not Send/Sync + #[allow(clippy::arc_with_non_send_sync)] + let on_ended_closure: ClosureHandle = Arc::new(RwLock::new(None)); + let on_ended_closure_handle = on_ended_closure.clone(); + + on_ended_closure + .write() + .unwrap() + .replace(Closure::wrap(Box::new(move || { + let now = ctx_handle.current_time(); + let time_at_start_of_buffer = { + let time_at_start_of_buffer = time_handle + .read() + .expect("Unable to get a read lock on the time cursor"); + // Synchronise first buffer as necessary (eg. keep the time value + // referenced to the context clock). + if *time_at_start_of_buffer > 0.001 { + *time_at_start_of_buffer + } else { + // 25ms of time to fetch the first sample data, increase to avoid + // initial underruns. + now + 0.025 + } + }; + + // Populate the sample data into an interleaved temporary buffer. + { + let len = temporary_buffer.len(); + let data = temporary_buffer.as_mut_ptr() as *mut (); + let mut data = unsafe { Data::from_parts(data, len, sample_format) }; + let mut data_callback = data_callback_handle.lock().unwrap(); + let callback = crate::StreamInstant::from_secs_f64(now); + let playback = crate::StreamInstant::from_secs_f64(time_at_start_of_buffer); + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + let info = OutputCallbackInfo { timestamp }; + (data_callback.deref_mut())(&mut data, &info); + } + + // Deinterleave the sample data and copy into the audio context buffer. + // We do not reference the audio context buffer directly e.g. getChannelData. + // As wasm-bindgen only gives us a copy, not a direct reference. + for channel in 0..n_channels { + for i in 0..buffer_size_frames { + temporary_channel_buffer[i] = + temporary_buffer[n_channels * i + channel]; + } + + #[cfg(not(target_feature = "atomics"))] + { + ctx_buffer + .copy_to_channel(&temporary_channel_buffer, channel as i32) + .expect( + "Unable to write sample data into the audio context buffer", + ); + } + + // copyToChannel cannot be directly copied into from a SharedArrayBuffer, + // which WASM memory is backed by if the 'atomics' flag is enabled. + // This workaround copies the data into an intermediary buffer first. + // There's a chance browsers may eventually relax that requirement. + // See this issue: https://github.com/WebAudio/web-audio-api/issues/2565 + #[cfg(target_feature = "atomics")] + { + temporary_channel_array_view.copy_from(&temporary_channel_buffer); + ctx_buffer + .unchecked_ref::() + .copy_to_channel(&temporary_channel_array_view, channel as i32) + .expect( + "Unable to write sample data into the audio context buffer", + ); + } + } + + // Create an AudioBufferSourceNode, schedule it to playback the reused buffer + // in the future. + let source = ctx_handle + .create_buffer_source() + .expect("Unable to create a webaudio buffer source"); + source.set_buffer(Some(&ctx_buffer)); + source + .connect_with_audio_node(&ctx_handle.destination()) + .expect( + "Unable to connect the web audio buffer source to the context destination", + ); + source + .add_event_listener_with_callback( + "ended", + on_ended_closure_handle + .read() + .unwrap() + .as_ref() + .unwrap() + .as_ref() + .unchecked_ref(), + ) + .expect("Failed to add ended event listener"); + + source + .start_with_when(time_at_start_of_buffer) + .expect("Unable to start the webaudio buffer source"); + + // Keep track of when the next buffer worth of samples should be played. + *time_handle.write().unwrap() = time_at_start_of_buffer + buffer_time_step_secs; + }) as Box)); + + on_ended_closures.push(on_ended_closure); + } + + Ok(Stream { + ctx, + on_ended_closures, + config: config.clone(), + buffer_size_frames, + }) + } +} + +impl Stream { + /// Return the [`AudioContext`](https://developer.mozilla.org/docs/Web/API/AudioContext) used + /// by this stream. + pub fn audio_context(&self) -> &AudioContext { + &self.ctx + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + let window = web_sys::window().unwrap(); + match self.ctx.resume() { + Ok(_) => { + // Begin webaudio playback, initially scheduling the closures to fire on a timeout + // event. + let mut offset_ms = 10; + let time_step_secs = + buffer_time_step_secs(self.buffer_size_frames, self.config.sample_rate); + let time_step_ms = (time_step_secs * 1_000.0) as i32; + for on_ended_closure in self.on_ended_closures.iter() { + window + .set_timeout_with_callback_and_timeout_and_arguments_0( + on_ended_closure + .read() + .unwrap() + .as_ref() + .unwrap() + .as_ref() + .unchecked_ref(), + offset_ms, + ) + .unwrap(); + offset_ms += time_step_ms; + } + Ok(()) + } + Err(err) => { + let description = format!("{:?}", err); + let err = BackendSpecificError { description }; + Err(err.into()) + } + } + } + + fn pause(&self) -> Result<(), PauseStreamError> { + match self.ctx.suspend() { + Ok(_) => Ok(()), + Err(err) => { + let description = format!("{:?}", err); + let err = BackendSpecificError { description }; + Err(err.into()) + } + } + } +} + +impl Drop for Stream { + fn drop(&mut self) { + let _ = self.ctx.close(); + } +} + +impl Default for Devices { + fn default() -> Devices { + // We produce an empty iterator if the WebAudio API isn't available. + Devices(is_webaudio_available()) + } +} + +impl Iterator for Devices { + type Item = Device; + + #[inline] + fn next(&mut self) -> Option { + if self.0 { + self.0 = false; + Some(Device) + } else { + None + } + } +} + +fn default_input_device() -> Option { + // TODO + None +} + +fn default_output_device() -> Option { + if is_webaudio_available() { + Some(Device) + } else { + None + } +} + +// Detects whether the `AudioContext` global variable is available. +fn is_webaudio_available() -> bool { + js_sys::Reflect::get(&js_sys::global(), &JsValue::from("AudioContext")) + .unwrap() + .is_truthy() +} + +// Whether or not the given stream configuration is valid for building a stream. +fn valid_config(conf: &StreamConfig, sample_format: SampleFormat) -> bool { + conf.channels <= MAX_CHANNELS + && conf.channels >= MIN_CHANNELS + && conf.sample_rate <= MAX_SAMPLE_RATE + && conf.sample_rate >= MIN_SAMPLE_RATE + && sample_format == SUPPORTED_SAMPLE_FORMAT +} + +fn buffer_time_step_secs(buffer_size_frames: usize, sample_rate: SampleRate) -> f64 { + buffer_size_frames as f64 / sample_rate as f64 +} + +#[cfg(target_feature = "atomics")] +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = AudioBuffer)] + type ExternalArrayAudioBuffer; + + # [wasm_bindgen(catch, method, structural, js_class = "AudioBuffer", js_name = copyToChannel)] + pub fn copy_to_channel( + this: &ExternalArrayAudioBuffer, + source: &js_sys::Float32Array, + channel_number: i32, + ) -> Result<(), JsValue>; +} diff --git a/vendor/cpal/src/lib.rs b/vendor/cpal/src/lib.rs new file mode 100644 index 0000000..5252671 --- /dev/null +++ b/vendor/cpal/src/lib.rs @@ -0,0 +1,1040 @@ +//! # How to use cpal +//! +//! Here are some concepts cpal exposes: +//! +//! - A [`Host`] provides access to the available audio devices on the system. +//! Some platforms have more than one host available, but every platform supported by CPAL has at +//! least one [default_host] that is guaranteed to be available. +//! - A [`Device`] is an audio device that may have any number of input and +//! output streams. +//! - A [`Stream`] is an open flow of audio data. Input streams allow you to +//! receive audio data, output streams allow you to play audio data. You must choose which +//! [Device] will run your stream before you can create one. Often, a default device can be +//! retrieved via the [Host]. +//! +//! The first step is to initialise the [`Host`]: +//! +//! ``` +//! use cpal::traits::HostTrait; +//! let host = cpal::default_host(); +//! ``` +//! +//! Then choose an available [`Device`]. The easiest way is to use the default input or output +//! `Device` via the [`default_input_device()`] or [`default_output_device()`] methods on `host`. +//! +//! Alternatively, you can enumerate all the available devices with the [`devices()`] method. +//! Beware that the `default_*_device()` functions return an `Option` in case no device +//! is available for that stream type on the system. +//! +//! ```no_run +//! # use cpal::traits::HostTrait; +//! # let host = cpal::default_host(); +//! let device = host.default_output_device().expect("no output device available"); +//! ``` +//! +//! Before we can create a stream, we must decide what the configuration of the audio stream is +//! going to be. +//! You can query all the supported configurations with the +//! [`supported_input_configs()`] and [`supported_output_configs()`] methods. +//! These produce a list of [`SupportedStreamConfigRange`] structs which can later be turned into +//! actual [`SupportedStreamConfig`] structs. +//! +//! If you don't want to query the list of configs, +//! you can also build your own [`StreamConfig`] manually, but doing so could lead to an error when +//! building the stream if the config is not supported by the device. +//! +//! > **Note**: the `supported_input/output_configs()` methods +//! > could return an error for example if the device has been disconnected. +//! +//! ```no_run +//! use cpal::traits::{DeviceTrait, HostTrait}; +//! # let host = cpal::default_host(); +//! # let device = host.default_output_device().unwrap(); +//! let mut supported_configs_range = device.supported_output_configs() +//! .expect("error while querying configs"); +//! let supported_config = supported_configs_range.next() +//! .expect("no supported config?!") +//! .with_max_sample_rate(); +//! ``` +//! +//! Now that we have everything for the stream, we are ready to create it from our selected device: +//! +//! ```no_run +//! use cpal::Data; +//! use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +//! # let host = cpal::default_host(); +//! # let device = host.default_output_device().unwrap(); +//! # let config = device.default_output_config().unwrap().into(); +//! let stream = device.build_output_stream( +//! &config, +//! move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { +//! // react to stream events and read or write stream data here. +//! }, +//! move |err| { +//! // react to errors here. +//! }, +//! None // None=blocking, Some(Duration)=timeout +//! ); +//! ``` +//! +//! While the stream is running, the selected audio device will periodically call the data callback +//! that was passed to the function. For input streams, the callback receives `&`[`Data`] containing +//! captured audio samples. For output streams, the callback receives `&mut`[`Data`] to be filled +//! with audio samples for playback. +//! +//! > **Note**: Creating and running a stream will *not* block the thread. On modern platforms, the +//! > given callback is called by a dedicated, high-priority thread responsible for delivering +//! > audio data to the system's audio device in a timely manner. On older platforms that only +//! > provide a blocking API (e.g. ALSA), CPAL will create a thread in order to consistently +//! > provide non-blocking behaviour (currently this is a thread per stream, but this may change to +//! > use a single thread for all streams). *If this is an issue for your platform or design, +//! > please share your issue and use-case with the CPAL team on the GitHub issue tracker for +//! > consideration.* +//! +//! In this example, we simply fill the given output buffer with silence. +//! +//! ```no_run +//! use cpal::{Data, Sample, SampleFormat, FromSample}; +//! use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +//! # let host = cpal::default_host(); +//! # let device = host.default_output_device().unwrap(); +//! # let supported_config = device.default_output_config().unwrap(); +//! let err_fn = |err| eprintln!("an error occurred on the output audio stream: {}", err); +//! let sample_format = supported_config.sample_format(); +//! let config = supported_config.into(); +//! let stream = match sample_format { +//! SampleFormat::F32 => device.build_output_stream(&config, write_silence::, err_fn, None), +//! SampleFormat::I16 => device.build_output_stream(&config, write_silence::, err_fn, None), +//! SampleFormat::U16 => device.build_output_stream(&config, write_silence::, err_fn, None), +//! sample_format => panic!("Unsupported sample format '{sample_format}'") +//! }.unwrap(); +//! +//! fn write_silence(data: &mut [T], _: &cpal::OutputCallbackInfo) { +//! for sample in data.iter_mut() { +//! *sample = Sample::EQUILIBRIUM; +//! } +//! } +//! ``` +//! +//! Not all platforms automatically run the stream upon creation. To ensure the stream has started, +//! we can use [`Stream::play`](traits::StreamTrait::play). +//! +//! ```no_run +//! # use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +//! # let host = cpal::default_host(); +//! # let device = host.default_output_device().unwrap(); +//! # let supported_config = device.default_output_config().unwrap(); +//! # let sample_format = supported_config.sample_format(); +//! # let config = supported_config.into(); +//! # let data_fn = move |_data: &mut cpal::Data, _: &cpal::OutputCallbackInfo| {}; +//! # let err_fn = move |_err| {}; +//! # let stream = device.build_output_stream_raw(&config, sample_format, data_fn, err_fn, None).unwrap(); +//! stream.play().unwrap(); +//! ``` +//! +//! Some devices support pausing the audio stream. This can be useful for saving energy in moments +//! of silence. +//! +//! ```no_run +//! # use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +//! # let host = cpal::default_host(); +//! # let device = host.default_output_device().unwrap(); +//! # let supported_config = device.default_output_config().unwrap(); +//! # let sample_format = supported_config.sample_format(); +//! # let config = supported_config.into(); +//! # let data_fn = move |_data: &mut cpal::Data, _: &cpal::OutputCallbackInfo| {}; +//! # let err_fn = move |_err| {}; +//! # let stream = device.build_output_stream_raw(&config, sample_format, data_fn, err_fn, None).unwrap(); +//! stream.pause().unwrap(); +//! ``` +//! +//! [`default_input_device()`]: traits::HostTrait::default_input_device +//! [`default_output_device()`]: traits::HostTrait::default_output_device +//! [`devices()`]: traits::HostTrait::devices +//! [`supported_input_configs()`]: traits::DeviceTrait::supported_input_configs +//! [`supported_output_configs()`]: traits::DeviceTrait::supported_output_configs + +#![cfg_attr(docsrs, feature(doc_cfg))] + +// Extern crate declarations with `#[macro_use]` must unfortunately be at crate root. +#[cfg(all( + target_arch = "wasm32", + any(target_os = "emscripten", feature = "wasm-bindgen") +))] +extern crate js_sys; +#[cfg(all( + target_arch = "wasm32", + any(target_os = "emscripten", feature = "wasm-bindgen") +))] +extern crate wasm_bindgen; +#[cfg(all( + target_arch = "wasm32", + any(target_os = "emscripten", feature = "wasm-bindgen") +))] +extern crate web_sys; + +#[cfg(all( + target_arch = "wasm32", + any(target_os = "emscripten", feature = "wasm-bindgen") +))] +use wasm_bindgen::prelude::*; + +pub use device_description::{ + DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, DeviceType, InterfaceType, +}; +pub use error::*; +pub use platform::{ + available_hosts, default_host, host_from_id, Device, Devices, Host, HostId, Stream, + SupportedInputConfigs, SupportedOutputConfigs, ALL_HOSTS, +}; +pub use samples_formats::{FromSample, Sample, SampleFormat, SizedSample, I24, U24}; +use std::convert::TryInto; +use std::time::Duration; + +pub mod device_description; +mod error; +mod host; +pub mod platform; +mod samples_formats; +pub mod traits; + +/// Iterator of devices wrapped in a filter to only include certain device types +pub type DevicesFiltered = std::iter::Filter::Item) -> bool>; + +/// A host's device iterator yielding only *input* devices. +pub type InputDevices = DevicesFiltered; + +/// A host's device iterator yielding only *output* devices. +pub type OutputDevices = DevicesFiltered; + +/// Number of channels. +pub type ChannelCount = u16; + +/// The number of samples processed per second for a single channel of audio. +pub type SampleRate = u32; + +/// A frame represents one sample for each channel. For example, with stereo audio, +/// one frame contains two samples (left and right channels). +pub type FrameCount = u32; + +/// A stable identifier for an audio device across all supported platforms. +/// +/// Device IDs should remain stable across application restarts and can be serialized using `Display`/`FromStr`. +/// +/// A device ID consists of a [`HostId`] identifying the audio backend and a device-specific identifier string. +/// +/// # Example +/// +/// ```no_run +/// use cpal::traits::{HostTrait, DeviceTrait}; +/// use cpal::DeviceId; +/// use std::str::FromStr; +/// +/// let host = cpal::default_host(); +/// let device = host.default_output_device().unwrap(); +/// let device_id = device.id().unwrap(); +/// +/// // Serialize to string (e.g., for storage in config file) +/// let id_string = device_id.to_string(); +/// println!("Device ID: {}", id_string); // e.g., "wasapi:device_identifier" +/// +/// // Deserialize from string +/// match DeviceId::from_str(&id_string) { +/// Ok(parsed_id) => { +/// // Retrieve the device by its ID +/// if let Some(device) = host.device_by_id(&parsed_id) { +/// println!("Found device: {:?}", device.id()); +/// } +/// } +/// Err(e) => eprintln!("Failed to parse device ID: {}", e), +/// } +/// ``` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct DeviceId(pub crate::platform::HostId, pub String); + +impl std::fmt::Display for DeviceId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.0, self.1) + } +} + +impl std::str::FromStr for DeviceId { + type Err = DeviceIdError; + + fn from_str(s: &str) -> Result { + let (host_str, device_str) = s.split_once(':').ok_or(DeviceIdError::BackendSpecific { + err: BackendSpecificError { + description: format!( + "Failed to parse device ID from: {s}\nCheck if format matches \"host:device_id\"" + ), + }, + })?; + + let host_id = crate::platform::HostId::from_str(host_str) + .map_err(|_| DeviceIdError::UnsupportedPlatform)?; + + Ok(DeviceId(host_id, device_str.to_string())) + } +} + +/// The buffer size requests the callback size for audio streams. +/// +/// This controls the approximate size of the audio buffer passed to your callback. +/// The actual callback size depends on the host/platform implementation and hardware +/// constraints, and may differ from or vary around the requested size. +/// +/// ## Callback Size Expectations +/// +/// When you specify [`BufferSize::Fixed(x)`], you are **requesting** that callbacks +/// receive approximately `x` frames of audio data. However, **no guarantees can be +/// made** about the actual callback size: +/// +/// - The host may round to hardware-supported values +/// - Different devices have different constraints +/// - The callback size may vary between calls (especially on mobile platforms) +/// - The actual size might be larger or smaller than requested +/// +/// ## Latency Considerations +/// +/// [`BufferSize::Default`] uses the host's default buffer size, which may be +/// surprisingly large, leading to higher latency. If low latency is desired, +/// [`BufferSize::Fixed`] should be used with a small value in accordance with +/// the [`SupportedBufferSize`] range from [`SupportedStreamConfig`]. +/// +/// Smaller buffer sizes reduce latency but may increase CPU usage and risk audio +/// dropouts if the callback cannot process audio quickly enough. +/// +/// # Example +/// +/// ```no_run +/// use cpal::traits::{DeviceTrait, HostTrait}; +/// use cpal::{BufferSize, SupportedBufferSize}; +/// +/// let host = cpal::default_host(); +/// let device = host.default_output_device().unwrap(); +/// let config = device.default_output_config().unwrap(); +/// +/// // Check supported buffer size range +/// match config.buffer_size() { +/// SupportedBufferSize::Range { min, max } => { +/// println!("Buffer size range: {} - {}", min, max); +/// // Request a small buffer for low latency +/// let mut stream_config = config.config(); +/// stream_config.buffer_size = BufferSize::Fixed(256); +/// } +/// SupportedBufferSize::Unknown => { +/// // Platform doesn't expose buffer size control +/// println!("Buffer size cannot be queried on this platform"); +/// } +/// } +/// ``` +/// +/// [`BufferSize::Default`]: BufferSize::Default +/// [`BufferSize::Fixed`]: BufferSize::Fixed +/// [`BufferSize::Fixed(x)`]: BufferSize::Fixed +/// [`SupportedBufferSize`]: SupportedStreamConfig::buffer_size +/// [`SupportedStreamConfig`]: SupportedStreamConfig +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum BufferSize { + Default, + Fixed(FrameCount), +} + +#[cfg(all( + target_arch = "wasm32", + any(target_os = "emscripten", feature = "wasm-bindgen") +))] +impl wasm_bindgen::describe::WasmDescribe for BufferSize { + fn describe() { + as wasm_bindgen::describe::WasmDescribe>::describe(); + } +} + +#[cfg(all( + target_arch = "wasm32", + any(target_os = "emscripten", feature = "wasm-bindgen") +))] +impl wasm_bindgen::convert::IntoWasmAbi for BufferSize { + type Abi = as wasm_bindgen::convert::IntoWasmAbi>::Abi; + + fn into_abi(self) -> Self::Abi { + match self { + Self::Default => None, + Self::Fixed(fc) => Some(fc), + } + .into_abi() + } +} + +#[cfg(all( + target_arch = "wasm32", + any(target_os = "emscripten", feature = "wasm-bindgen") +))] +impl wasm_bindgen::convert::FromWasmAbi for BufferSize { + type Abi = as wasm_bindgen::convert::FromWasmAbi>::Abi; + + unsafe fn from_abi(js: Self::Abi) -> Self { + match Option::::from_abi(js) { + None => Self::Default, + Some(fc) => Self::Fixed(fc), + } + } +} + +/// The set of parameters used to describe how to open a stream. +/// +/// The sample format is omitted in favour of using a sample type. +/// +/// See also [`BufferSize`] for details on buffer size behavior and latency considerations. +#[cfg_attr( + all( + target_arch = "wasm32", + any(target_os = "emscripten", feature = "wasm-bindgen") + ), + wasm_bindgen +)] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StreamConfig { + pub channels: ChannelCount, + pub sample_rate: SampleRate, + pub buffer_size: BufferSize, +} + +/// Describes the minimum and maximum supported buffer size for the device +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum SupportedBufferSize { + Range { + min: FrameCount, + max: FrameCount, + }, + /// In the case that the platform provides no way of getting the default + /// buffer size before starting a stream. + Unknown, +} + +/// Describes a range of supported stream configurations, retrieved via the +/// [`Device::supported_input/output_configs`](traits::DeviceTrait#required-methods) method. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SupportedStreamConfigRange { + pub(crate) channels: ChannelCount, + /// Minimum value for the sample rate of the supported formats. + pub(crate) min_sample_rate: SampleRate, + /// Maximum value for the sample rate of the supported formats. + pub(crate) max_sample_rate: SampleRate, + /// Buffer size ranges supported by the device + pub(crate) buffer_size: SupportedBufferSize, + /// Type of data expected by the device. + pub(crate) sample_format: SampleFormat, +} + +/// Common iterator types used by backend implementations. +/// +/// All backends use these same concrete iterator types for supported stream configurations. +#[allow(dead_code)] +pub(crate) mod iter { + use super::SupportedStreamConfigRange; + + /// Iterator type for supported input stream configurations. + /// + /// This is the iterator type returned by all backend implementations of + /// [`DeviceTrait::supported_input_configs`](crate::traits::DeviceTrait::supported_input_configs). + pub type SupportedInputConfigs = std::vec::IntoIter; + + /// Iterator type for supported output stream configurations. + /// + /// This is the iterator type returned by all backend implementations of + /// [`DeviceTrait::supported_output_configs`](crate::traits::DeviceTrait::supported_output_configs). + pub type SupportedOutputConfigs = std::vec::IntoIter; +} + +/// Describes a single supported stream configuration, retrieved via either a +/// [`SupportedStreamConfigRange`] instance or one of the +/// [`Device::default_input/output_config`](traits::DeviceTrait#required-methods) methods. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SupportedStreamConfig { + channels: ChannelCount, + sample_rate: SampleRate, + buffer_size: SupportedBufferSize, + sample_format: SampleFormat, +} + +/// A buffer of dynamically typed audio data, passed to raw stream callbacks. +/// +/// Raw input stream callbacks receive `&Data`, while raw output stream callbacks expect `&mut Data`. +#[cfg_attr(target_os = "emscripten", wasm_bindgen)] +#[derive(Debug)] +pub struct Data { + data: *mut (), + len: usize, + sample_format: SampleFormat, +} + +/// A monotonic time instance associated with a stream, retrieved from either: +/// +/// 1. A timestamp provided to the stream's underlying audio data callback or +/// 2. The same time source used to generate timestamps for a stream's underlying audio data +/// callback. +/// +/// `StreamInstant` represents a duration since an unspecified origin point. The origin +/// is guaranteed to occur at or before the stream starts, and remains consistent for the +/// lifetime of that stream. Different streams may have different origins. +/// +/// ## Host `StreamInstant` Sources +/// +/// | Host | Source | +/// | ---- | ------ | +/// | alsa | `snd_pcm_status_get_htstamp` | +/// | coreaudio | `mach_absolute_time` | +/// | wasapi | `QueryPerformanceCounter` | +/// | asio | `timeGetTime` | +/// | emscripten | `AudioContext.getOutputTimestamp` | +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct StreamInstant { + secs: i64, + nanos: u32, +} + +/// A timestamp associated with a call to an input stream's data callback. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub struct InputStreamTimestamp { + /// The instant the stream's data callback was invoked. + pub callback: StreamInstant, + /// The instant that data was captured from the device. + /// + /// E.g. The instant data was read from an ADC. + pub capture: StreamInstant, +} + +/// A timestamp associated with a call to an output stream's data callback. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub struct OutputStreamTimestamp { + /// The instant the stream's data callback was invoked. + pub callback: StreamInstant, + /// The predicted instant that data written will be delivered to the device for playback. + /// + /// E.g. The instant data will be played by a DAC. + pub playback: StreamInstant, +} + +/// Information relevant to a single call to the user's input stream data callback. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub struct InputCallbackInfo { + timestamp: InputStreamTimestamp, +} + +/// Information relevant to a single call to the user's output stream data callback. +#[cfg_attr(target_os = "emscripten", wasm_bindgen)] +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub struct OutputCallbackInfo { + timestamp: OutputStreamTimestamp, +} + +impl SupportedStreamConfig { + pub fn new( + channels: ChannelCount, + sample_rate: SampleRate, + buffer_size: SupportedBufferSize, + sample_format: SampleFormat, + ) -> Self { + Self { + channels, + sample_rate, + buffer_size, + sample_format, + } + } + + pub fn channels(&self) -> ChannelCount { + self.channels + } + + pub fn sample_rate(&self) -> SampleRate { + self.sample_rate + } + + pub fn buffer_size(&self) -> &SupportedBufferSize { + &self.buffer_size + } + + pub fn sample_format(&self) -> SampleFormat { + self.sample_format + } + + pub fn config(&self) -> StreamConfig { + StreamConfig { + channels: self.channels, + sample_rate: self.sample_rate, + buffer_size: BufferSize::Default, + } + } +} + +impl StreamInstant { + /// The amount of time elapsed from another instant to this one. + /// + /// Returns `None` if `earlier` is later than self. + pub fn duration_since(&self, earlier: &Self) -> Option { + if self < earlier { + None + } else { + (self.as_nanos() - earlier.as_nanos()) + .try_into() + .ok() + .map(Duration::from_nanos) + } + } + + /// Returns the instant in time after the given duration has passed. + /// + /// Returns `None` if the resulting instant would exceed the bounds of the underlying data + /// structure. + pub fn add(&self, duration: Duration) -> Option { + self.as_nanos() + .checked_add(duration.as_nanos() as i128) + .and_then(Self::from_nanos_i128) + } + + /// Returns the instant in time one `duration` ago. + /// + /// Returns `None` if the resulting instant would underflow. As a result, it is important to + /// consider that on some platforms the [`StreamInstant`] may begin at `0` from the moment the + /// source stream is created. + pub fn sub(&self, duration: Duration) -> Option { + self.as_nanos() + .checked_sub(duration.as_nanos() as i128) + .and_then(Self::from_nanos_i128) + } + + fn as_nanos(&self) -> i128 { + (self.secs as i128 * 1_000_000_000) + self.nanos as i128 + } + + #[allow(dead_code)] + fn from_nanos(nanos: i64) -> Self { + let secs = nanos / 1_000_000_000; + let subsec_nanos = nanos - secs * 1_000_000_000; + Self::new(secs, subsec_nanos as u32) + } + + #[allow(dead_code)] + fn from_nanos_i128(nanos: i128) -> Option { + let secs = nanos / 1_000_000_000; + if secs > i64::MAX as i128 || secs < i64::MIN as i128 { + None + } else { + let subsec_nanos = nanos - secs * 1_000_000_000; + debug_assert!(subsec_nanos < u32::MAX as i128); + Some(Self::new(secs as i64, subsec_nanos as u32)) + } + } + + #[allow(dead_code)] + fn from_secs_f64(secs: f64) -> crate::StreamInstant { + let s = secs.floor() as i64; + let ns = ((secs - s as f64) * 1_000_000_000.0) as u32; + Self::new(s, ns) + } + + pub fn new(secs: i64, nanos: u32) -> Self { + StreamInstant { secs, nanos } + } +} + +impl InputCallbackInfo { + pub fn new(timestamp: InputStreamTimestamp) -> Self { + Self { timestamp } + } + + /// The timestamp associated with the call to an input stream's data callback. + pub fn timestamp(&self) -> InputStreamTimestamp { + self.timestamp + } +} + +impl OutputCallbackInfo { + pub fn new(timestamp: OutputStreamTimestamp) -> Self { + Self { timestamp } + } + + /// The timestamp associated with the call to an output stream's data callback. + pub fn timestamp(&self) -> OutputStreamTimestamp { + self.timestamp + } +} + +// Note: Data does not implement `is_empty()` because it always contains a valid audio buffer +// by design. The buffer may contain silence, but it is never structurally empty. +#[allow(clippy::len_without_is_empty)] +impl Data { + /// Constructor for host implementations to use. + /// + /// # Safety + /// The following requirements must be met in order for the safety of `Data`'s API. + /// - The `data` pointer must point to the first sample in the slice containing all samples. + /// - The `len` must describe the length of the buffer as a number of samples in the expected + /// format specified via the `sample_format` argument. + /// - The `sample_format` must correctly represent the underlying sample data delivered/expected + /// by the stream. + pub unsafe fn from_parts(data: *mut (), len: usize, sample_format: SampleFormat) -> Self { + Data { + data, + len, + sample_format, + } + } + + /// The sample format of the internal audio data. + pub fn sample_format(&self) -> SampleFormat { + self.sample_format + } + + /// The full length of the buffer in samples. + /// + /// The returned length is the same length as the slice of type `T` that would be returned via + /// [`as_slice`](Self::as_slice) given a sample type that matches the inner sample format. + pub fn len(&self) -> usize { + self.len + } + + /// The raw slice of memory representing the underlying audio data as a slice of bytes. + /// + /// It is up to the user to interpret the slice of memory based on [`Data::sample_format`]. + pub fn bytes(&self) -> &[u8] { + let len = self.len * self.sample_format.sample_size(); + // The safety of this block relies on correct construction of the `Data` instance. + // See the unsafe `from_parts` constructor for these requirements. + unsafe { std::slice::from_raw_parts(self.data as *const u8, len) } + } + + /// The raw slice of memory representing the underlying audio data as a slice of bytes. + /// + /// It is up to the user to interpret the slice of memory based on [`Data::sample_format`]. + pub fn bytes_mut(&mut self) -> &mut [u8] { + let len = self.len * self.sample_format.sample_size(); + // The safety of this block relies on correct construction of the `Data` instance. See + // the unsafe `from_parts` constructor for these requirements. + unsafe { std::slice::from_raw_parts_mut(self.data as *mut u8, len) } + } + + /// Access the data as a slice of sample type `T`. + /// + /// Returns `None` if the sample type does not match the expected sample format. + pub fn as_slice(&self) -> Option<&[T]> + where + T: SizedSample, + { + if T::FORMAT == self.sample_format { + // The safety of this block relies on correct construction of the `Data` instance. See + // the unsafe `from_parts` constructor for these requirements. + unsafe { Some(std::slice::from_raw_parts(self.data as *const T, self.len)) } + } else { + None + } + } + + /// Access the data as a slice of sample type `T`. + /// + /// Returns `None` if the sample type does not match the expected sample format. + pub fn as_slice_mut(&mut self) -> Option<&mut [T]> + where + T: SizedSample, + { + if T::FORMAT == self.sample_format { + // The safety of this block relies on correct construction of the `Data` instance. See + // the unsafe `from_parts` constructor for these requirements. + unsafe { + Some(std::slice::from_raw_parts_mut( + self.data as *mut T, + self.len, + )) + } + } else { + None + } + } +} + +impl SupportedStreamConfigRange { + pub fn new( + channels: ChannelCount, + min_sample_rate: SampleRate, + max_sample_rate: SampleRate, + buffer_size: SupportedBufferSize, + sample_format: SampleFormat, + ) -> Self { + Self { + channels, + min_sample_rate, + max_sample_rate, + buffer_size, + sample_format, + } + } + + pub fn channels(&self) -> ChannelCount { + self.channels + } + + pub fn min_sample_rate(&self) -> SampleRate { + self.min_sample_rate + } + + pub fn max_sample_rate(&self) -> SampleRate { + self.max_sample_rate + } + + pub fn buffer_size(&self) -> &SupportedBufferSize { + &self.buffer_size + } + + pub fn sample_format(&self) -> SampleFormat { + self.sample_format + } + + /// Retrieve a [`SupportedStreamConfig`] with the given sample rate and buffer size. + /// + /// # Panics + /// + /// Panics if the given `sample_rate` is outside the range specified within + /// this [`SupportedStreamConfigRange`] instance. For a non-panicking + /// variant, use [`try_with_sample_rate`](#method.try_with_sample_rate). + pub fn with_sample_rate(self, sample_rate: SampleRate) -> SupportedStreamConfig { + self.try_with_sample_rate(sample_rate) + .expect("sample rate out of range") + } + + /// Retrieve a [`SupportedStreamConfig`] with the given sample rate and buffer size. + /// + /// Returns `None` if the given sample rate is outside the range specified + /// within this [`SupportedStreamConfigRange`] instance. + pub fn try_with_sample_rate(self, sample_rate: SampleRate) -> Option { + if self.min_sample_rate <= sample_rate && sample_rate <= self.max_sample_rate { + Some(SupportedStreamConfig { + channels: self.channels, + sample_rate, + sample_format: self.sample_format, + buffer_size: self.buffer_size, + }) + } else { + None + } + } + + /// Turns this [`SupportedStreamConfigRange`] into a [`SupportedStreamConfig`] corresponding to the maximum sample rate. + #[inline] + pub fn with_max_sample_rate(self) -> SupportedStreamConfig { + SupportedStreamConfig { + channels: self.channels, + sample_rate: self.max_sample_rate, + sample_format: self.sample_format, + buffer_size: self.buffer_size, + } + } + + /// A comparison function which compares two [`SupportedStreamConfigRange`]s in terms of their priority of + /// use as a default stream format. + /// + /// Some backends do not provide a default stream format for their audio devices. In these + /// cases, CPAL attempts to decide on a reasonable default format for the user. To do this we + /// use the "greatest" of all supported stream formats when compared with this method. + /// + /// SupportedStreamConfigs are prioritised by the following heuristics: + /// + /// **Channels**: + /// + /// - Stereo + /// - Mono + /// - Max available channels + /// + /// **Sample format**: + /// - f32 + /// - i16 + /// - u16 + /// + /// **Sample rate**: + /// + /// - 44100 (cd quality) + /// - Max sample rate + pub fn cmp_default_heuristics(&self, other: &Self) -> std::cmp::Ordering { + use std::cmp::Ordering::Equal; + use SampleFormat::{F32, I16, I24, I32, U16, U24, U32}; + + let cmp_stereo = (self.channels == 2).cmp(&(other.channels == 2)); + if cmp_stereo != Equal { + return cmp_stereo; + } + + let cmp_mono = (self.channels == 1).cmp(&(other.channels == 1)); + if cmp_mono != Equal { + return cmp_mono; + } + + let cmp_channels = self.channels.cmp(&other.channels); + if cmp_channels != Equal { + return cmp_channels; + } + + let cmp_f32 = (self.sample_format == F32).cmp(&(other.sample_format == F32)); + if cmp_f32 != Equal { + return cmp_f32; + } + + let cmp_i32 = (self.sample_format == I32).cmp(&(other.sample_format == I32)); + if cmp_i32 != Equal { + return cmp_i32; + } + + let cmp_u32 = (self.sample_format == U32).cmp(&(other.sample_format == U32)); + if cmp_u32 != Equal { + return cmp_u32; + } + + let cmp_i24 = (self.sample_format == I24).cmp(&(other.sample_format == I24)); + if cmp_i24 != Equal { + return cmp_i24; + } + + let cmp_u24 = (self.sample_format == U24).cmp(&(other.sample_format == U24)); + if cmp_u24 != Equal { + return cmp_u24; + } + + let cmp_i16 = (self.sample_format == I16).cmp(&(other.sample_format == I16)); + if cmp_i16 != Equal { + return cmp_i16; + } + + let cmp_u16 = (self.sample_format == U16).cmp(&(other.sample_format == U16)); + if cmp_u16 != Equal { + return cmp_u16; + } + + const HZ_44100: SampleRate = 44_100; + let r44100_in_self = self.min_sample_rate <= HZ_44100 && HZ_44100 <= self.max_sample_rate; + let r44100_in_other = + other.min_sample_rate <= HZ_44100 && HZ_44100 <= other.max_sample_rate; + let cmp_r44100 = r44100_in_self.cmp(&r44100_in_other); + if cmp_r44100 != Equal { + return cmp_r44100; + } + + self.max_sample_rate.cmp(&other.max_sample_rate) + } +} + +#[test] +fn test_cmp_default_heuristics() { + let mut formats = [ + SupportedStreamConfigRange { + buffer_size: SupportedBufferSize::Range { min: 256, max: 512 }, + channels: 2, + min_sample_rate: 1, + max_sample_rate: 96000, + sample_format: SampleFormat::F32, + }, + SupportedStreamConfigRange { + buffer_size: SupportedBufferSize::Range { min: 256, max: 512 }, + channels: 1, + min_sample_rate: 1, + max_sample_rate: 96000, + sample_format: SampleFormat::F32, + }, + SupportedStreamConfigRange { + buffer_size: SupportedBufferSize::Range { min: 256, max: 512 }, + channels: 2, + min_sample_rate: 1, + max_sample_rate: 96000, + sample_format: SampleFormat::I16, + }, + SupportedStreamConfigRange { + buffer_size: SupportedBufferSize::Range { min: 256, max: 512 }, + channels: 2, + min_sample_rate: 1, + max_sample_rate: 96000, + sample_format: SampleFormat::U16, + }, + SupportedStreamConfigRange { + buffer_size: SupportedBufferSize::Range { min: 256, max: 512 }, + channels: 2, + min_sample_rate: 1, + max_sample_rate: 22050, + sample_format: SampleFormat::F32, + }, + ]; + + formats.sort_by(|a, b| a.cmp_default_heuristics(b)); + + // lowest-priority first: + assert_eq!(formats[0].sample_format(), SampleFormat::F32); + assert_eq!(formats[0].min_sample_rate(), 1); + assert_eq!(formats[0].max_sample_rate(), 96000); + assert_eq!(formats[0].channels(), 1); + + assert_eq!(formats[1].sample_format(), SampleFormat::U16); + assert_eq!(formats[1].min_sample_rate(), 1); + assert_eq!(formats[1].max_sample_rate(), 96000); + assert_eq!(formats[1].channels(), 2); + + assert_eq!(formats[2].sample_format(), SampleFormat::I16); + assert_eq!(formats[2].min_sample_rate(), 1); + assert_eq!(formats[2].max_sample_rate(), 96000); + assert_eq!(formats[2].channels(), 2); + + assert_eq!(formats[3].sample_format(), SampleFormat::F32); + assert_eq!(formats[3].min_sample_rate(), 1); + assert_eq!(formats[3].max_sample_rate(), 22050); + assert_eq!(formats[3].channels(), 2); + + assert_eq!(formats[4].sample_format(), SampleFormat::F32); + assert_eq!(formats[4].min_sample_rate(), 1); + assert_eq!(formats[4].max_sample_rate(), 96000); + assert_eq!(formats[4].channels(), 2); +} + +impl From for StreamConfig { + fn from(conf: SupportedStreamConfig) -> Self { + conf.config() + } +} + +// If a backend does not provide an API for retrieving supported formats, we query it with a bunch +// of commonly used rates. This is always the case for WASAPI and is sometimes the case for ALSA. +#[allow(dead_code)] +pub(crate) const COMMON_SAMPLE_RATES: &[SampleRate] = &[ + 5512, 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000, + 176400, 192000, 352800, 384000, 705600, 768000, 1411200, 1536000, +]; + +#[test] +fn test_stream_instant() { + let a = StreamInstant::new(2, 0); + let b = StreamInstant::new(-2, 0); + let min = StreamInstant::new(i64::MIN, 0); + let max = StreamInstant::new(i64::MAX, 0); + assert_eq!( + a.sub(Duration::from_secs(1)), + Some(StreamInstant::new(1, 0)) + ); + assert_eq!( + a.sub(Duration::from_secs(2)), + Some(StreamInstant::new(0, 0)) + ); + assert_eq!( + a.sub(Duration::from_secs(3)), + Some(StreamInstant::new(-1, 0)) + ); + assert_eq!(min.sub(Duration::from_secs(1)), None); + assert_eq!( + b.add(Duration::from_secs(1)), + Some(StreamInstant::new(-1, 0)) + ); + assert_eq!( + b.add(Duration::from_secs(2)), + Some(StreamInstant::new(0, 0)) + ); + assert_eq!( + b.add(Duration::from_secs(3)), + Some(StreamInstant::new(1, 0)) + ); + assert_eq!(max.add(Duration::from_secs(1)), None); +} diff --git a/vendor/cpal/src/platform/mod.rs b/vendor/cpal/src/platform/mod.rs new file mode 100644 index 0000000..0f62026 --- /dev/null +++ b/vendor/cpal/src/platform/mod.rs @@ -0,0 +1,887 @@ +//! Platform-specific items. +//! +//! This module also contains the implementation of the platform's dynamically dispatched [`Host`] +//! type and its associated [`Device`], [`Stream`] and other associated types. These +//! types are useful in the case that users require switching between audio host APIs at runtime. + +#[doc(inline)] +pub use self::platform_impl::*; + +#[cfg(feature = "custom")] +pub use crate::host::custom::{Device as CustomDevice, Host as CustomHost, Stream as CustomStream}; + +/// A macro to assist with implementing a platform's dynamically dispatched [`Host`] type. +/// +/// These dynamically dispatched types are necessary to allow for users to switch between hosts at +/// runtime. +/// +/// For example the invocation `impl_platform_host(Wasapi wasapi "WASAPI", Asio asio "ASIO")`, +/// this macro should expand to: +/// +// This sample code block is marked as text because it's not a valid test, +// it's just illustrative. (see rust issue #96573) +/// ```text +/// pub enum HostId { +/// Wasapi, +/// Asio, +/// } +/// +/// pub enum Host { +/// Wasapi(crate::host::wasapi::Host), +/// Asio(crate::host::asio::Host), +/// } +/// ``` +/// +/// And so on for Device, Devices, Host, Stream, SupportedInputConfigs, +/// SupportedOutputConfigs and all their necessary trait implementations. +/// +macro_rules! impl_platform_host { + ($($(#[cfg($feat: meta)])? $HostVariant:ident => $Host:ty),* $(,)?) => { + /// All hosts supported by CPAL on this platform. + pub const ALL_HOSTS: &'static [HostId] = &[ + $( + $(#[cfg($feat)])? + HostId::$HostVariant, + )* + ]; + + /// The platform's dynamically dispatched `Host` type. + /// + /// An instance of this `Host` type may represent one of the `Host`s available + /// on the platform. + /// + /// Use this type if you require switching between available hosts at runtime. + /// + /// This type may be constructed via the [`host_from_id`] function. [`HostId`]s may + /// be acquired via the [`ALL_HOSTS`] const, and the [`available_hosts`] function. + pub struct Host(HostInner); + + /// The `Device` implementation associated with the platform's dynamically dispatched + /// [`Host`] type. + #[derive(Clone)] + pub struct Device(DeviceInner); + + /// The `Devices` iterator associated with the platform's dynamically dispatched [`Host`] + /// type. + pub struct Devices(DevicesInner); + + /// The `Stream` implementation associated with the platform's dynamically dispatched + /// [`Host`] type. + #[must_use = "If the stream is not stored it will not play."] + pub struct Stream(StreamInner); + + /// The `SupportedInputConfigs` iterator associated with the platform's dynamically + /// dispatched [`Host`] type. + #[derive(Clone)] + pub struct SupportedInputConfigs(SupportedInputConfigsInner); + + /// The `SupportedOutputConfigs` iterator associated with the platform's dynamically + /// dispatched [`Host`] type. + #[derive(Clone)] + pub struct SupportedOutputConfigs(SupportedOutputConfigsInner); + + /// Unique identifier for available hosts on the platform. + /// + /// Only the hosts supported by the current platform are available as enum variants. + /// For cross-platform code that needs to handle hosts from other platforms, + /// use the string representation via [`std::fmt::Display`]/[`std::str::FromStr`]. + /// + /// # Available Host Strings + /// + /// For cross-platform matching, these host strings are available: + /// + /// - `"aaudio"` - Android Audio + /// - `"alsa"` - Advanced Linux Sound Architecture + /// - `"asio"` - ASIO + /// - `"coreaudio"` - CoreAudio + /// - `"custom"` - Custom host (requires `custom` feature) + /// - `"emscripten"` - Emscripten + /// - `"jack"` - JACK Audio Connection Kit + /// - `"null"` - Null host + /// - `"wasapi"` - Windows Audio Session API + /// - `"webaudio"` - Web Audio API + /// - `"audioworklet"` - Audio Worklet + /// + /// # Cross-Platform Example + /// + /// ``` + /// use cpal::HostId; + /// use std::str::FromStr; + /// + /// fn handle_host_string(host_string: &str) { + /// // String matching works on all platforms + /// match host_string { + /// "alsa" => println!("ALSA host"), + /// "coreaudio" => println!("CoreAudio host"), + /// "jack" => println!("JACK host"), + /// "wasapi" => println!("WASAPI host"), + /// "asio" => println!("ASIO host"), + /// "aaudio" => println!("AAudio host"), + /// _ => println!("Other host"), + /// } + /// + /// // Parse host string (may fail if host is not available on this platform) + /// if let Ok(host_id) = HostId::from_str(host_string) { + /// println!("Successfully parsed: {}", host_id); + /// } + /// } + /// ``` + #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] + pub enum HostId { + $( + $(#[cfg($feat)])? + $(#[cfg_attr(docsrs, doc(cfg($feat)))])? + $HostVariant, + )* + } + + /// Contains a platform specific [`Device`] implementation. + #[derive(Clone)] + pub enum DeviceInner { + $( + $(#[cfg($feat)])? + $HostVariant(<$Host as crate::traits::HostTrait>::Device), + )* + } + + /// Contains a platform specific [`Devices`] implementation. + pub enum DevicesInner { + $( + $(#[cfg($feat)])? + $HostVariant(<$Host as crate::traits::HostTrait>::Devices), + )* + } + + /// Contains a platform specific [`Host`] implementation. + pub enum HostInner { + $( + $(#[cfg($feat)])? + $HostVariant($Host), + )* + } + + /// Contains a platform specific [`Stream`] implementation. + pub enum StreamInner { + $( + $(#[cfg($feat)])? + $HostVariant(<<$Host as crate::traits::HostTrait>::Device as crate::traits::DeviceTrait>::Stream), + )* + } + + #[derive(Clone)] + enum SupportedInputConfigsInner { + $( + $(#[cfg($feat)])? + $HostVariant(<<$Host as crate::traits::HostTrait>::Device as crate::traits::DeviceTrait>::SupportedInputConfigs), + )* + } + + #[derive(Clone)] + enum SupportedOutputConfigsInner { + $( + $(#[cfg($feat)])? + $HostVariant(<<$Host as crate::traits::HostTrait>::Device as crate::traits::DeviceTrait>::SupportedOutputConfigs), + )* + } + + impl HostId { + pub fn name(&self) -> &'static str { + match self { + $( + $(#[cfg($feat)])? + HostId::$HostVariant => stringify!($HostVariant), + )* + } + } + } + + impl std::fmt::Display for HostId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name().to_lowercase()) + } + } + + impl std::str::FromStr for HostId { + type Err = crate::HostUnavailable; + + fn from_str(s: &str) -> Result { + $( + $(#[cfg($feat)])? + if stringify!($HostVariant).eq_ignore_ascii_case(s) { + return Ok(HostId::$HostVariant); + } + )* + Err(crate::HostUnavailable) + } + } + + impl Devices { + /// Returns a reference to the underlying platform specific implementation of this + /// `Devices`. + pub fn as_inner(&self) -> &DevicesInner { + &self.0 + } + + /// Returns a mutable reference to the underlying platform specific implementation of + /// this `Devices`. + pub fn as_inner_mut(&mut self) -> &mut DevicesInner { + &mut self.0 + } + + /// Returns the underlying platform specific implementation of this `Devices`. + pub fn into_inner(self) -> DevicesInner { + self.0 + } + } + + impl Device { + /// Returns a reference to the underlying platform specific implementation of this + /// `Device`. + pub fn as_inner(&self) -> &DeviceInner { + &self.0 + } + + /// Returns a mutable reference to the underlying platform specific implementation of + /// this `Device`. + pub fn as_inner_mut(&mut self) -> &mut DeviceInner { + &mut self.0 + } + + /// Returns the underlying platform specific implementation of this `Device`. + pub fn into_inner(self) -> DeviceInner { + self.0 + } + } + + impl Host { + /// The unique identifier associated with this `Host`. + pub fn id(&self) -> HostId { + match self.0 { + $( + $(#[cfg($feat)])? + HostInner::$HostVariant(_) => HostId::$HostVariant, + )* + } + } + + /// Returns a reference to the underlying platform specific implementation of this + /// `Host`. + pub fn as_inner(&self) -> &HostInner { + &self.0 + } + + /// Returns a mutable reference to the underlying platform specific implementation of + /// this `Host`. + pub fn as_inner_mut(&mut self) -> &mut HostInner { + &mut self.0 + } + + /// Returns the underlying platform specific implementation of this `Host`. + pub fn into_inner(self) -> HostInner { + self.0 + } + } + + impl Stream { + /// Returns a reference to the underlying platform specific implementation of this + /// `Stream`. + pub fn as_inner(&self) -> &StreamInner { + &self.0 + } + + /// Returns a mutable reference to the underlying platform specific implementation of + /// this `Stream`. + pub fn as_inner_mut(&mut self) -> &mut StreamInner { + &mut self.0 + } + + /// Returns the underlying platform specific implementation of this `Stream`. + pub fn into_inner(self) -> StreamInner { + self.0 + } + } + + impl Iterator for Devices { + type Item = Device; + + fn next(&mut self) -> Option { + match self.0 { + $( + $(#[cfg($feat)])? + DevicesInner::$HostVariant(ref mut d) => { + d.next().map(DeviceInner::$HostVariant).map(Device::from) + } + )* + } + } + + fn size_hint(&self) -> (usize, Option) { + match self.0 { + $( + $(#[cfg($feat)])? + DevicesInner::$HostVariant(ref d) => d.size_hint(), + )* + } + } + } + + impl Iterator for SupportedInputConfigs { + type Item = crate::SupportedStreamConfigRange; + + fn next(&mut self) -> Option { + match self.0 { + $( + $(#[cfg($feat)])? + SupportedInputConfigsInner::$HostVariant(ref mut s) => s.next(), + )* + } + } + + fn size_hint(&self) -> (usize, Option) { + match self.0 { + $( + $(#[cfg($feat)])? + SupportedInputConfigsInner::$HostVariant(ref d) => d.size_hint(), + )* + } + } + } + + impl Iterator for SupportedOutputConfigs { + type Item = crate::SupportedStreamConfigRange; + + fn next(&mut self) -> Option { + match self.0 { + $( + $(#[cfg($feat)])? + SupportedOutputConfigsInner::$HostVariant(ref mut s) => s.next(), + )* + } + } + + fn size_hint(&self) -> (usize, Option) { + match self.0 { + $( + $(#[cfg($feat)])? + SupportedOutputConfigsInner::$HostVariant(ref d) => d.size_hint(), + )* + } + } + } + + impl crate::traits::DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + #[allow(deprecated)] + fn name(&self) -> Result { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d.name(), + )* + } + } + + fn description(&self) -> Result { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d.description(), + )* + } + } + + fn id(&self) -> Result { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d.id(), + )* + } + } + + fn supports_input(&self) -> bool { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d.supports_input(), + )* + } + } + + fn supports_output(&self) -> bool { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d.supports_output(), + )* + } + } + + fn supported_input_configs(&self) -> Result { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => { + d.supported_input_configs() + .map(SupportedInputConfigsInner::$HostVariant) + .map(SupportedInputConfigs) + } + )* + } + } + + fn supported_output_configs(&self) -> Result { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => { + d.supported_output_configs() + .map(SupportedOutputConfigsInner::$HostVariant) + .map(SupportedOutputConfigs) + } + )* + } + } + + fn default_input_config(&self) -> Result { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d.default_input_config(), + )* + } + } + + fn default_output_config(&self) -> Result { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d.default_output_config(), + )* + } + } + + fn build_input_stream_raw( + &self, + config: &crate::StreamConfig, + sample_format: crate::SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static, + E: FnMut(crate::StreamError) + Send + 'static, + { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d + .build_input_stream_raw( + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + .map(StreamInner::$HostVariant) + .map(Stream::from), + )* + } + } + + fn build_output_stream_raw( + &self, + config: &crate::StreamConfig, + sample_format: crate::SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static, + E: FnMut(crate::StreamError) + Send + 'static, + { + match self.0 { + $( + $(#[cfg($feat)])? + DeviceInner::$HostVariant(ref d) => d + .build_output_stream_raw( + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + .map(StreamInner::$HostVariant) + .map(Stream::from), + )* + } + } + } + + impl crate::traits::HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + $( + $(#[cfg($feat)])? + if <$Host>::is_available() { return true; } + )* + false + } + + fn devices(&self) -> Result { + match self.0 { + $( + $(#[cfg($feat)])? + HostInner::$HostVariant(ref h) => { + h.devices().map(DevicesInner::$HostVariant).map(Devices::from) + } + )* + } + } + + fn default_input_device(&self) -> Option { + match self.0 { + $( + $(#[cfg($feat)])? + HostInner::$HostVariant(ref h) => { + h.default_input_device().map(DeviceInner::$HostVariant).map(Device::from) + } + )* + } + } + + fn default_output_device(&self) -> Option { + match self.0 { + $( + $(#[cfg($feat)])? + HostInner::$HostVariant(ref h) => { + h.default_output_device().map(DeviceInner::$HostVariant).map(Device::from) + } + )* + } + } + } + + impl crate::traits::StreamTrait for Stream { + fn play(&self) -> Result<(), crate::PlayStreamError> { + match self.0 { + $( + $(#[cfg($feat)])? + StreamInner::$HostVariant(ref s) => { + s.play() + } + )* + } + } + + fn pause(&self) -> Result<(), crate::PauseStreamError> { + match self.0 { + $( + $(#[cfg($feat)])? + StreamInner::$HostVariant(ref s) => { + s.pause() + } + )* + } + } + } + + impl From for Device { + fn from(d: DeviceInner) -> Self { + Device(d) + } + } + + impl From for Devices { + fn from(d: DevicesInner) -> Self { + Devices(d) + } + } + + impl From for Host { + fn from(h: HostInner) -> Self { + Host(h) + } + } + + impl From for Stream { + fn from(s: StreamInner) -> Self { + Stream(s) + } + } + + $( + $(#[cfg($feat)])? + impl From<<$Host as crate::traits::HostTrait>::Device> for Device { + fn from(h: <$Host as crate::traits::HostTrait>::Device) -> Self { + DeviceInner::$HostVariant(h).into() + } + } + + $(#[cfg($feat)])? + impl From<<$Host as crate::traits::HostTrait>::Devices> for Devices { + fn from(h: <$Host as crate::traits::HostTrait>::Devices) -> Self { + DevicesInner::$HostVariant(h).into() + } + } + + $(#[cfg($feat)])? + impl From<$Host> for Host { + fn from(h: $Host) -> Self { + HostInner::$HostVariant(h).into() + } + } + + $(#[cfg($feat)])? + impl From<<<$Host as crate::traits::HostTrait>::Device as crate::traits::DeviceTrait>::Stream> for Stream { + fn from(h: <<$Host as crate::traits::HostTrait>::Device as crate::traits::DeviceTrait>::Stream) -> Self { + StreamInner::$HostVariant(h).into() + } + } + )* + + /// Produces a list of hosts that are currently available on the system. + pub fn available_hosts() -> Vec { + let mut host_ids = vec![]; + $( + $(#[cfg($feat)])? + if <$Host as crate::traits::HostTrait>::is_available() { + host_ids.push(HostId::$HostVariant); + } + )* + host_ids + } + + /// Given a unique host identifier, initialise and produce the host if it is available. + pub fn host_from_id(id: HostId) -> Result { + match id { + $( + $(#[cfg($feat)])? + HostId::$HostVariant => { + <$Host>::new() + .map(HostInner::$HostVariant) + .map(Host::from) + } + )* + } + } + + impl Default for Host { + fn default() -> Host { + default_host() + } + } + }; +} + +// TODO: Add pulseaudio here eventually. +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" +))] +mod platform_impl { + #[cfg_attr( + docsrs, + doc(cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ))) + )] + pub use crate::host::alsa::Host as AlsaHost; + #[cfg(feature = "jack")] + #[cfg_attr( + docsrs, + doc(cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "jack" + ))) + )] + pub use crate::host::jack::Host as JackHost; + + impl_platform_host!( + #[cfg(feature = "jack")] Jack => JackHost, + Alsa => AlsaHost, + #[cfg(feature = "custom")] Custom => super::CustomHost + ); + + /// The default host for the current compilation target platform. + pub fn default_host() -> Host { + AlsaHost::new() + .expect("the default host should always be available") + .into() + } +} + +#[cfg(any(target_os = "macos", target_os = "ios"))] +mod platform_impl { + #[cfg_attr(docsrs, doc(cfg(any(target_os = "macos", target_os = "ios"))))] + pub use crate::host::coreaudio::Host as CoreAudioHost; + #[cfg(all(feature = "jack", target_os = "macos"))] + #[cfg_attr(docsrs, doc(cfg(all(feature = "jack", target_os = "macos"))))] + pub use crate::host::jack::Host as JackHost; + + impl_platform_host!( + CoreAudio => CoreAudioHost, + #[cfg(all(feature = "jack", target_os = "macos"))] Jack => JackHost, + #[cfg(feature = "custom")] Custom => super::CustomHost + ); + + /// The default host for the current compilation target platform. + pub fn default_host() -> Host { + CoreAudioHost::new() + .expect("the default host should always be available") + .into() + } +} + +#[cfg(target_os = "emscripten")] +mod platform_impl { + #[cfg_attr(docsrs, doc(cfg(target_os = "emscripten")))] + pub use crate::host::emscripten::Host as EmscriptenHost; + impl_platform_host!( + Emscripten => EmscriptenHost, + #[cfg(feature = "custom")] Custom => super::CustomHost + ); + + /// The default host for the current compilation target platform. + pub fn default_host() -> Host { + EmscriptenHost::new() + .expect("the default host should always be available") + .into() + } +} + +#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] +mod platform_impl { + #[cfg_attr( + docsrs, + doc(cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))) + )] + pub use crate::host::webaudio::Host as WebAudioHost; + + #[cfg(feature = "audioworklet")] + #[cfg_attr( + docsrs, + doc(cfg(all( + target_arch = "wasm32", + feature = "wasm-bindgen", + feature = "audioworklet" + ))) + )] + pub use crate::host::audioworklet::Host as AudioWorkletHost; + + impl_platform_host!( + WebAudio => WebAudioHost, + #[cfg(feature = "audioworklet")] AudioWorklet => AudioWorkletHost, + #[cfg(feature = "custom")] Custom => super::CustomHost + ); + + /// The default host for the current compilation target platform. + pub fn default_host() -> Host { + WebAudioHost::new() + .expect("the default host should always be available") + .into() + } +} + +#[cfg(windows)] +mod platform_impl { + #[cfg(feature = "asio")] + #[cfg_attr(docsrs, doc(cfg(all(windows, feature = "asio"))))] + pub use crate::host::asio::Host as AsioHost; + #[cfg(feature = "jack")] + #[cfg_attr(docsrs, doc(cfg(all(windows, feature = "jack"))))] + pub use crate::host::jack::Host as JackHost; + #[cfg_attr(docsrs, doc(cfg(windows)))] + pub use crate::host::wasapi::Host as WasapiHost; + + impl_platform_host!( + #[cfg(feature = "asio")] Asio => AsioHost, + Wasapi => WasapiHost, + #[cfg(feature = "jack")] Jack => JackHost, + #[cfg(feature = "custom")] Custom => super::CustomHost, + ); + + /// The default host for the current compilation target platform. + pub fn default_host() -> Host { + WasapiHost::new() + .expect("the default host should always be available") + .into() + } +} + +#[cfg(target_os = "android")] +mod platform_impl { + #[cfg_attr(docsrs, doc(cfg(target_os = "android")))] + pub use crate::host::aaudio::Host as AAudioHost; + impl_platform_host!( + AAudio => AAudioHost, + #[cfg(feature = "custom")] Custom => super::CustomHost + ); + + /// The default host for the current compilation target platform. + pub fn default_host() -> Host { + AAudioHost::new() + .expect("the default host should always be available") + .into() + } +} + +#[cfg(not(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "ios", + target_os = "emscripten", + target_os = "android", + all(target_arch = "wasm32", feature = "wasm-bindgen"), +)))] +mod platform_impl { + #[cfg_attr( + docsrs, + doc(cfg(not(any( + windows, + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "macos", + target_os = "ios", + target_os = "emscripten", + target_os = "android", + all(target_arch = "wasm32", feature = "wasm-bindgen") + )))) + )] + pub use crate::host::null::Host as NullHost; + + impl_platform_host!( + Null => NullHost, + #[cfg(feature = "custom")] Custom => super::CustomHost, + ); + + /// The default host for the current compilation target platform. + pub fn default_host() -> Host { + NullHost::new() + .expect("the default host should always be available") + .into() + } +} diff --git a/vendor/cpal/src/samples_formats.rs b/vendor/cpal/src/samples_formats.rs new file mode 100644 index 0000000..24d6ffd --- /dev/null +++ b/vendor/cpal/src/samples_formats.rs @@ -0,0 +1,313 @@ +//! Audio sample format types and conversions. +//! +//! # Byte Order +//! +//! All multi-byte sample formats use the native endianness of the target platform. +//! CPAL handles any necessary conversions when interfacing with hardware that uses +//! a different byte order. + +use std::{fmt::Display, mem}; +#[cfg(all( + target_arch = "wasm32", + any(target_os = "emscripten", feature = "wasm-bindgen") +))] +use wasm_bindgen::prelude::*; + +pub use dasp_sample::{FromSample, Sample}; + +/// 24-bit signed integer sample type. +/// +/// Represents 24-bit audio with range `-(1 << 23)..=((1 << 23) - 1)`. +/// +/// **Note:** While representing 24-bit audio, this format uses 4 bytes (i32) of storage +/// with the most significant byte unused. Use [`SampleFormat::bits_per_sample`] to get +/// the actual bit depth (24) vs [`SampleFormat::sample_size`] for storage size (4 bytes). +pub use dasp_sample::I24; + +/// 24-bit unsigned integer sample type. +/// +/// Represents 24-bit audio with range `0..=((1 << 24) - 1)`, with origin at `1 << 23 == 8388608`. +/// +/// **Note:** While representing 24-bit audio, this format uses 4 bytes (u32) of storage +/// with the most significant byte unused. Use [`SampleFormat::bits_per_sample`] to get +/// the actual bit depth (24) vs [`SampleFormat::sample_size`] for storage size (4 bytes). +pub use dasp_sample::U24; + +// I48 and U48 are not currently supported by cpal but available in dasp_sample: +// pub use dasp_sample::{I48, U48}; + +/// Format that each sample has. Usually, this corresponds to the sampling +/// depth of the audio source. For example, 16 bit quantized samples can be +/// encoded in `i16` or `u16`. Note that the quantized sampling depth is not +/// directly visible for formats where [`is_float`] is true. +/// +/// Also note that the backend must support the encoding of the quantized +/// samples in the given format, as there is no generic transformation from one +/// format into the other done inside the frontend-library code. You can query +/// the supported formats by using [`supported_input_configs`]. +/// +/// A good rule of thumb is to use [`SampleFormat::I16`] as this covers typical +/// music (WAV, MP3) as well as typical audio input devices on most platforms, +/// +/// [`is_float`]: SampleFormat::is_float +/// [`supported_input_configs`]: crate::traits::DeviceTrait::supported_input_configs +#[cfg_attr( + all( + target_arch = "wasm32", + any(target_os = "emscripten", feature = "wasm-bindgen") + ), + wasm_bindgen +)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum SampleFormat { + /// `i8` with a valid range of `i8::MIN..=i8::MAX` with `0` being the origin. + I8, + + /// `i16` with a valid range of `i16::MIN..=i16::MAX` with `0` being the origin. + I16, + + /// `I24` with a valid range of `-(1 << 23)..=((1 << 23) - 1)` with `0` being the origin. + /// + /// This format uses 4 bytes of storage but only 24 bits are significant. + I24, + + /// `i32` with a valid range of `i32::MIN..=i32::MAX` with `0` being the origin. + I32, + + // /// `I48` with a valid range of '-(1 << 47)..(1 << 47)' with `0` being the origin + // I48, + /// `i64` with a valid range of `i64::MIN..=i64::MAX` with `0` being the origin. + I64, + + /// `u8` with a valid range of `u8::MIN..=u8::MAX` with `1 << 7 == 128` being the origin. + U8, + + /// `u16` with a valid range of `u16::MIN..=u16::MAX` with `1 << 15 == 32768` being the origin. + U16, + + /// `U24` with a valid range of `0..=((1 << 24) - 1)` with `1 << 23 == 8388608` being the origin. + /// + /// This format uses 4 bytes of storage but only 24 bits are significant. + U24, + + /// `u32` with a valid range of `u32::MIN..=u32::MAX` with `1 << 31` being the origin. + U32, + + /// `U48` with a valid range of '0..(1 << 48)' with `1 << 47` being the origin + // U48, + + /// `u64` with a valid range of `u64::MIN..=u64::MAX` with `1 << 63` being the origin. + U64, + + /// `f32` with a valid range of `-1.0..=1.0` with `0.0` being the origin. + F32, + + /// `f64` with a valid range of `-1.0..=1.0` with `0.0` being the origin. + F64, + + /// DSD 1-bit stream in u8 container (8 bits = 8 DSD samples) with 0x69 being the silence byte pattern. + DsdU8, + + /// DSD 1-bit stream in u16 container (16 bits = 16 DSD samples) with 0x69 being the silence byte pattern. + DsdU16, + + /// DSD 1-bit stream in u32 container (32 bits = 32 DSD samples) with 0x69 being the silence byte pattern. + DsdU32, +} + +impl SampleFormat { + /// Returns the size in bytes of a sample of this format. This corresponds to + /// the internal size of the rust primitives that are used to represent this + /// sample format (e.g., i24 has size of i32). + #[inline] + #[must_use] + pub fn sample_size(&self) -> usize { + match *self { + SampleFormat::I8 => mem::size_of::(), + SampleFormat::U8 => mem::size_of::(), + SampleFormat::I16 => mem::size_of::(), + SampleFormat::U16 => mem::size_of::(), + SampleFormat::I24 => mem::size_of::(), + SampleFormat::U24 => mem::size_of::(), + SampleFormat::I32 => mem::size_of::(), + SampleFormat::U32 => mem::size_of::(), + // SampleFormat::I48 => mem::size_of::(), + // SampleFormat::U48 => mem::size_of::(), + SampleFormat::I64 => mem::size_of::(), + SampleFormat::U64 => mem::size_of::(), + SampleFormat::F32 => mem::size_of::(), + SampleFormat::F64 => mem::size_of::(), + SampleFormat::DsdU8 => mem::size_of::(), + SampleFormat::DsdU16 => mem::size_of::(), + SampleFormat::DsdU32 => mem::size_of::(), + } + } + + /// Returns the number of bits of a sample of this format. Note that this is + /// not necessarily the same as the size of the primitive used to represent + /// this sample format (e.g., I24 has size of i32 but 24 bits per sample). + #[inline] + #[must_use] + pub fn bits_per_sample(&self) -> u32 { + match *self { + SampleFormat::I8 => i8::BITS, + SampleFormat::U8 => u8::BITS, + SampleFormat::I16 => i16::BITS, + SampleFormat::U16 => u16::BITS, + SampleFormat::I24 => 24, + SampleFormat::U24 => 24, + SampleFormat::I32 => i32::BITS, + SampleFormat::U32 => u32::BITS, + // SampleFormat::I48 => 48, + // SampleFormat::U48 => 48, + SampleFormat::I64 => i64::BITS, + SampleFormat::U64 => u64::BITS, + SampleFormat::F32 => 32, + SampleFormat::F64 => 64, + SampleFormat::DsdU8 | SampleFormat::DsdU16 | SampleFormat::DsdU32 => 1, + } + } + + #[inline] + #[must_use] + pub fn is_int(&self) -> bool { + matches!( + *self, + SampleFormat::I8 + | SampleFormat::I16 + | SampleFormat::I24 + | SampleFormat::I32 + // | SampleFormat::I48 + | SampleFormat::I64 + ) + } + + #[inline] + #[must_use] + pub fn is_uint(&self) -> bool { + matches!( + *self, + SampleFormat::U8 + | SampleFormat::U16 + | SampleFormat::U24 + | SampleFormat::U32 + // | SampleFormat::U48 + | SampleFormat::U64 + ) + } + + #[inline] + #[must_use] + pub fn is_float(&self) -> bool { + matches!(*self, SampleFormat::F32 | SampleFormat::F64) + } + + #[inline] + #[must_use] + pub fn is_dsd(&self) -> bool { + matches!( + *self, + SampleFormat::DsdU8 | SampleFormat::DsdU16 | SampleFormat::DsdU32 + ) + } +} + +impl Display for SampleFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match *self { + SampleFormat::I8 => "i8", + SampleFormat::I16 => "i16", + SampleFormat::I24 => "i24", + SampleFormat::I32 => "i32", + // SampleFormat::I48 => "i48", + SampleFormat::I64 => "i64", + SampleFormat::U8 => "u8", + SampleFormat::U16 => "u16", + SampleFormat::U24 => "u24", + SampleFormat::U32 => "u32", + // SampleFormat::U48 => "u48", + SampleFormat::U64 => "u64", + SampleFormat::F32 => "f32", + SampleFormat::F64 => "f64", + SampleFormat::DsdU8 => "dsdu8", + SampleFormat::DsdU16 => "dsdu16", + SampleFormat::DsdU32 => "dsdu32", + } + .fmt(f) + } +} + +/// A [`Sample`] type with a known corresponding [`SampleFormat`]. +/// +/// This trait is automatically implemented for all primitive sample types and provides +/// a way to determine the [`SampleFormat`] at compile time. +/// +/// # Example +/// +/// ``` +/// use cpal::SizedSample; +/// +/// assert_eq!(i16::FORMAT, cpal::SampleFormat::I16); +/// assert_eq!(f32::FORMAT, cpal::SampleFormat::F32); +/// ``` +pub trait SizedSample: Sample { + /// The corresponding [`SampleFormat`] for this sample type. + const FORMAT: SampleFormat; +} + +impl SizedSample for i8 { + const FORMAT: SampleFormat = SampleFormat::I8; +} + +impl SizedSample for i16 { + const FORMAT: SampleFormat = SampleFormat::I16; +} + +impl SizedSample for I24 { + const FORMAT: SampleFormat = SampleFormat::I24; +} + +impl SizedSample for i32 { + const FORMAT: SampleFormat = SampleFormat::I32; +} + +// impl SizedSample for I48 { +// const FORMAT: SampleFormat = SampleFormat::I48; +// } + +impl SizedSample for i64 { + const FORMAT: SampleFormat = SampleFormat::I64; +} + +impl SizedSample for u8 { + const FORMAT: SampleFormat = SampleFormat::U8; +} + +impl SizedSample for u16 { + const FORMAT: SampleFormat = SampleFormat::U16; +} + +impl SizedSample for U24 { + const FORMAT: SampleFormat = SampleFormat::U24; +} + +impl SizedSample for u32 { + const FORMAT: SampleFormat = SampleFormat::U32; +} + +// impl SizedSample for U48 { +// const FORMAT: SampleFormat = SampleFormat::U48; +// } + +impl SizedSample for u64 { + const FORMAT: SampleFormat = SampleFormat::U64; +} + +impl SizedSample for f32 { + const FORMAT: SampleFormat = SampleFormat::F32; +} + +impl SizedSample for f64 { + const FORMAT: SampleFormat = SampleFormat::F64; +} diff --git a/vendor/cpal/src/traits.rs b/vendor/cpal/src/traits.rs new file mode 100644 index 0000000..2c3bccc --- /dev/null +++ b/vendor/cpal/src/traits.rs @@ -0,0 +1,345 @@ +//! The suite of traits allowing CPAL to abstract over hosts, devices, event loops and stream IDs. +//! +//! # Custom Host Implementations +//! +//! When implementing custom hosts with the `custom` feature, use the [`assert_stream_send!`](crate::assert_stream_send) +//! and [`assert_stream_sync!`](crate::assert_stream_sync) macros to verify your `Stream` type meets CPAL's requirements. + +use std::time::Duration; + +use crate::{ + BuildStreamError, Data, DefaultStreamConfigError, DeviceDescription, DeviceId, DeviceIdError, + DeviceNameError, DevicesError, InputCallbackInfo, InputDevices, OutputCallbackInfo, + OutputDevices, PauseStreamError, PlayStreamError, SampleFormat, SizedSample, StreamConfig, + StreamError, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, +}; + +/// A [`Host`] provides access to the available audio devices on the system. +/// +/// Each platform may have a number of available hosts depending on the system, each with their own +/// pros and cons. +/// +/// For example, WASAPI is the standard audio host API that ships with the Windows operating +/// system. However, due to historical limitations with respect to performance and flexibility, +/// Steinberg created the ASIO API providing better audio device support for pro audio and +/// low-latency applications. As a result, it is common for some devices and device capabilities to +/// only be available via ASIO, while others are only available via WASAPI. +/// +/// Another great example is the Linux platform. While the ALSA host API is the lowest-level API +/// available to almost all distributions of Linux, its flexibility is limited as it requires that +/// each process have exclusive access to the devices with which they establish streams. PulseAudio +/// is another popular host API that aims to solve this issue by providing user-space mixing, +/// however it has its own limitations w.r.t. low-latency and high-performance audio applications. +/// JACK is yet another host API that is more suitable to pro-audio applications, however it is +/// less readily available by default in many Linux distributions and is known to be tricky to +/// set up. +/// +/// [`Host`]: crate::Host +pub trait HostTrait { + /// The type used for enumerating available devices by the host. + type Devices: Iterator; + /// The `Device` type yielded by the host. + type Device: DeviceTrait; + + /// Whether or not the host is available on the system. + fn is_available() -> bool; + + /// An iterator yielding all [`Device`](DeviceTrait)s currently available to the host on the system. + /// + /// Can be empty if the system does not support audio in general. + fn devices(&self) -> Result; + + /// Fetches a [`Device`](DeviceTrait) based on a [`DeviceId`] if available + /// + /// Returns `None` if no device matching the id is found + fn device_by_id(&self, id: &DeviceId) -> Option { + self.devices() + .ok()? + .find(|device| device.id().ok().as_ref() == Some(id)) + } + + /// The default input audio device on the system. + /// + /// Returns `None` if no input device is available. + fn default_input_device(&self) -> Option; + + /// The default output audio device on the system. + /// + /// Returns `None` if no output device is available. + fn default_output_device(&self) -> Option; + + /// An iterator yielding all `Device`s currently available to the system that support one or more + /// input stream formats. + /// + /// Can be empty if the system does not support audio input. + fn input_devices(&self) -> Result, DevicesError> { + Ok(self.devices()?.filter(DeviceTrait::supports_input)) + } + + /// An iterator yielding all `Device`s currently available to the system that support one or more + /// output stream formats. + /// + /// Can be empty if the system does not support audio output. + fn output_devices(&self) -> Result, DevicesError> { + Ok(self.devices()?.filter(DeviceTrait::supports_output)) + } +} + +/// A device that is capable of audio input and/or output. +/// +/// Please note that `Device`s may become invalid if they get disconnected. Therefore, all the +/// methods that involve a device return a `Result` allowing the user to handle this case. +pub trait DeviceTrait { + /// The iterator type yielding supported input stream formats. + type SupportedInputConfigs: Iterator; + /// The iterator type yielding supported output stream formats. + type SupportedOutputConfigs: Iterator; + /// The stream type created by [`build_input_stream_raw`] and [`build_output_stream_raw`]. + /// + /// [`build_input_stream_raw`]: Self::build_input_stream_raw + /// [`build_output_stream_raw`]: Self::build_output_stream_raw + type Stream: StreamTrait; + + /// The human-readable name of the device. + #[deprecated( + since = "0.17.0", + note = "Use `description()` for comprehensive device information including name, \ + manufacturer, and device type. Use `id()` for a unique, stable device identifier \ + that persists across reboots and reconnections." + )] + fn name(&self) -> Result { + self.description().map(|desc| desc.name().to_string()) + } + + /// Structured description of the device with metadata. + /// + /// This returns a [`DeviceDescription`] containing structured information about the device, + /// including name, manufacturer (if available), device type, bus type, and other + /// platform-specific metadata. + /// + /// For simple string representation, use `device.description().to_string()` or + /// `device.description().name()`. + fn description(&self) -> Result; + + /// The ID of the device. + /// + /// This ID uniquely identifies the device on the host. It should be stable across program + /// runs, device disconnections, and system reboots where possible. + fn id(&self) -> Result; + + /// True if the device supports audio input, otherwise false + fn supports_input(&self) -> bool { + self.supported_input_configs() + .is_ok_and(|mut iter| iter.next().is_some()) + } + + /// True if the device supports audio output, otherwise false + fn supports_output(&self) -> bool { + self.supported_output_configs() + .is_ok_and(|mut iter| iter.next().is_some()) + } + + /// An iterator yielding formats that are supported by the backend. + /// + /// Can return an error if the device is no longer valid (e.g. it has been disconnected). + fn supported_input_configs( + &self, + ) -> Result; + + /// An iterator yielding output stream formats that are supported by the device. + /// + /// Can return an error if the device is no longer valid (e.g. it has been disconnected). + fn supported_output_configs( + &self, + ) -> Result; + + /// The default input stream format for the device. + fn default_input_config(&self) -> Result; + + /// The default output stream format for the device. + fn default_output_config(&self) -> Result; + + /// Create an input stream. + /// + /// # Parameters + /// + /// * `config` - The stream configuration including sample rate, channels, and buffer size. + /// * `data_callback` - Called periodically with captured audio data. The callback receives + /// a slice of samples in the format `T` and timing information. + /// * `error_callback` - Called when a stream error occurs (e.g., device disconnected). + /// * `timeout` - Optional timeout for backend operations. `None` indicates blocking behavior, + /// `Some(duration)` sets a maximum wait time. Not all backends support timeouts. + fn build_input_stream( + &self, + config: &StreamConfig, + mut data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + T: SizedSample, + D: FnMut(&[T], &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + self.build_input_stream_raw( + config, + T::FORMAT, + move |data, info| { + data_callback( + data.as_slice() + .expect("host supplied incorrect sample type"), + info, + ) + }, + error_callback, + timeout, + ) + } + + /// Create an output stream. + /// + /// # Parameters + /// + /// * `config` - The stream configuration including sample rate, channels, and buffer size. + /// * `data_callback` - Called periodically to fill the output buffer. The callback receives + /// a mutable slice of samples in the format `T` to be filled with audio data, along with + /// timing information. + /// * `error_callback` - Called when a stream error occurs (e.g., device disconnected). + /// * `timeout` - Optional timeout for backend operations. `None` indicates blocking behavior, + /// `Some(duration)` sets a maximum wait time. Not all backends support timeouts. + fn build_output_stream( + &self, + config: &StreamConfig, + mut data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + T: SizedSample, + D: FnMut(&mut [T], &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + self.build_output_stream_raw( + config, + T::FORMAT, + move |data, info| { + data_callback( + data.as_slice_mut() + .expect("host supplied incorrect sample type"), + info, + ) + }, + error_callback, + timeout, + ) + } + + /// Create a dynamically typed input stream. + /// + /// This method allows working with sample data as raw bytes, useful when the sample + /// format is determined at runtime. For compile-time known formats, prefer + /// [`build_input_stream`](Self::build_input_stream). + /// + /// # Parameters + /// + /// * `config` - The stream configuration including sample rate, channels, and buffer size. + /// * `sample_format` - The sample format of the audio data. + /// * `data_callback` - Called periodically with captured audio data as a [`Data`] buffer. + /// * `error_callback` - Called when a stream error occurs (e.g., device disconnected). + /// * `timeout` - Optional timeout for backend operations. `None` indicates blocking behavior, + /// `Some(duration)` sets a maximum wait time. Not all backends support timeouts. + 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(StreamError) + Send + 'static; + + /// Create a dynamically typed output stream. + /// + /// This method allows working with sample data as raw bytes, useful when the sample + /// format is determined at runtime. For compile-time known formats, prefer + /// [`build_output_stream`](Self::build_output_stream). + /// + /// # Parameters + /// + /// * `config` - The stream configuration including sample rate, channels, and buffer size. + /// * `sample_format` - The sample format of the audio data. + /// * `data_callback` - Called periodically to fill the output buffer with audio data as + /// a mutable [`Data`] buffer. + /// * `error_callback` - Called when a stream error occurs (e.g., device disconnected). + /// * `timeout` - Optional timeout for backend operations. `None` indicates blocking behavior, + /// `Some(duration)` sets a maximum wait time. Not all backends support timeouts. + 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(StreamError) + Send + 'static; +} + +/// A stream created from [`Device`](DeviceTrait), with methods to control playback. +pub trait StreamTrait { + /// Run the stream. + /// + /// Note: Not all platforms automatically run the stream upon creation, so it is important to + /// call `play` after creation if it is expected that the stream should run immediately. + fn play(&self) -> Result<(), PlayStreamError>; + + /// Some devices support pausing the audio stream. This can be useful for saving energy in + /// moments of silence. + /// + /// Note: Not all devices support suspending the stream at the hardware level. This method may + /// fail in these cases. + fn pause(&self) -> Result<(), PauseStreamError>; +} + +/// Compile-time assertion that a stream type implements [`Send`]. +/// +/// Custom host implementations should use this macro to verify their `Stream` type +/// can be safely transferred between threads, as required by CPAL's API. +/// +/// # Example +/// +/// ``` +/// use cpal::assert_stream_send; +/// struct MyStream { /* ... */ } +/// assert_stream_send!(MyStream); +/// ``` +#[macro_export] +macro_rules! assert_stream_send { + ($t:ty) => { + const fn _assert_stream_send() {} + const _: () = _assert_stream_send::<$t>(); + }; +} + +/// Compile-time assertion that a stream type implements [`Sync`]. +/// +/// Custom host implementations should use this macro to verify their `Stream` type +/// can be safely shared between threads, as required by CPAL's API. +/// +/// # Example +/// +/// ``` +/// use cpal::assert_stream_sync; +/// struct MyStream { /* ... */ } +/// assert_stream_sync!(MyStream); +/// ``` +#[macro_export] +macro_rules! assert_stream_sync { + ($t:ty) => { + const fn _assert_stream_sync() {} + const _: () = _assert_stream_sync::<$t>(); + }; +}