├── .github └── workflows │ └── cpal.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── Cross.toml ├── Dockerfile ├── LICENSE ├── README.md ├── asio-sys ├── .gitignore ├── Cargo.toml ├── asio-link │ ├── helpers.cpp │ └── helpers.hpp ├── build.rs ├── examples │ ├── enumerate.rs │ └── test.rs └── src │ ├── bindings │ ├── asio_import.rs │ ├── errors.rs │ └── mod.rs │ └── lib.rs ├── build.rs ├── examples ├── android │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── lib.rs ├── beep.rs ├── enumerate.rs ├── feedback.rs ├── ios-feedback │ ├── Cargo.toml │ ├── README.md │ ├── build_rust_deps.sh │ ├── cpal-ios-example.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── mikeh.xcuserdatad │ │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ ├── ios-src │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── ViewController.h │ │ ├── ViewController.m │ │ └── main.m │ └── src │ │ ├── feedback.rs │ │ └── lib.rs ├── record_wav.rs ├── synth_tones.rs └── wasm-beep │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ └── lib.rs └── src ├── error.rs ├── host ├── aaudio │ ├── android_media.rs │ ├── convert.rs │ ├── java_interface.rs │ ├── java_interface │ │ ├── audio_features.rs │ │ ├── definitions.rs │ │ ├── devices_info.rs │ │ └── utils.rs │ └── mod.rs ├── alsa │ ├── enumerate.rs │ └── mod.rs ├── asio │ ├── device.rs │ ├── mod.rs │ └── stream.rs ├── coreaudio │ ├── ios │ │ ├── enumerate.rs │ │ └── mod.rs │ ├── macos │ │ ├── enumerate.rs │ │ ├── mod.rs │ │ └── property_listener.rs │ └── mod.rs ├── emscripten │ └── mod.rs ├── jack │ ├── device.rs │ ├── mod.rs │ └── stream.rs ├── mod.rs ├── null │ └── mod.rs ├── wasapi │ ├── com.rs │ ├── device.rs │ ├── mod.rs │ └── stream.rs └── webaudio │ └── mod.rs ├── lib.rs ├── platform └── mod.rs ├── samples_formats.rs └── traits.rs /.github/workflows/cpal.yml: -------------------------------------------------------------------------------- 1 | name: cpal 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | clippy-test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Update apt 12 | run: sudo apt update 13 | - name: Install alsa 14 | run: sudo apt-get install libasound2-dev 15 | - name: Install libjack 16 | run: sudo apt-get install libjack-jackd2-dev libjack-jackd2-0 17 | - name: Install dbus 18 | run: sudo apt-get install libdbus-1-dev 19 | - name: Install stable 20 | uses: dtolnay/rust-toolchain@stable 21 | with: 22 | components: clippy 23 | target: armv7-linux-androideabi 24 | - name: Run clippy 25 | run: cargo clippy --all --all-features 26 | - name: Run clippy for Android target 27 | run: cargo clippy --all --features asio --target armv7-linux-androideabi 28 | 29 | rustfmt-check: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Install stable 34 | uses: dtolnay/rust-toolchain@stable 35 | with: 36 | components: rustfmt 37 | - name: Run rustfmt 38 | run: cargo fmt --all -- --check 39 | 40 | cargo-publish: 41 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v4 45 | - name: Install rust 46 | uses: dtolnay/rust-toolchain@stable 47 | - name: Update apt 48 | run: sudo apt update 49 | - name: Install alsa 50 | run: sudo apt-get install libasound2-dev 51 | - name: Verify publish crate 52 | uses: katyo/publish-crates@v2 53 | with: 54 | dry-run: true 55 | ignore-unpublished-changes: true 56 | - name: Publish crate 57 | uses: katyo/publish-crates@v2 58 | with: 59 | ignore-unpublished-changes: true 60 | registry-token: ${{ secrets.CRATESIO_TOKEN }} 61 | 62 | ubuntu-test: 63 | runs-on: ubuntu-latest 64 | steps: 65 | - uses: actions/checkout@v4 66 | - name: Update apt 67 | run: sudo apt update 68 | - name: Install alsa 69 | run: sudo apt-get install libasound2-dev 70 | - name: Install libjack 71 | run: sudo apt-get install libjack-jackd2-dev libjack-jackd2-0 72 | - name: Install dbus 73 | run: sudo apt-get install libdbus-1-dev 74 | - name: Install stable 75 | uses: dtolnay/rust-toolchain@stable 76 | - name: Run without features 77 | run: cargo test --all --no-default-features --verbose 78 | - name: Run all features 79 | run: cargo test --all --all-features --verbose 80 | 81 | linux-check-and-test-armv7: 82 | runs-on: ubuntu-latest 83 | steps: 84 | - name: Checkout sources 85 | uses: actions/checkout@v4 86 | 87 | - name: Install stable toolchain 88 | uses: dtolnay/rust-toolchain@stable 89 | with: 90 | target: armv7-unknown-linux-gnueabihf 91 | 92 | - name: Install cross 93 | run: cargo install cross 94 | 95 | - name: Check without features for armv7 96 | run: cross check --target armv7-unknown-linux-gnueabihf --workspace --no-default-features --verbose 97 | 98 | - name: Test without features for armv7 99 | run: cross test --target armv7-unknown-linux-gnueabihf --workspace --no-default-features --verbose 100 | 101 | - name: Check all features for armv7 102 | run: cross check --target armv7-unknown-linux-gnueabihf --workspace --all-features --verbose 103 | 104 | - name: Test all features for armv7 105 | run: cross test --target armv7-unknown-linux-gnueabihf --workspace --all-features --verbose 106 | 107 | asmjs-wasm32-test: 108 | strategy: 109 | matrix: 110 | target: [wasm32-unknown-emscripten] 111 | runs-on: ubuntu-latest 112 | steps: 113 | - uses: actions/checkout@v4 114 | - name: Setup Emscripten toolchain 115 | uses: mymindstorm/setup-emsdk@v14 116 | - name: Install stable 117 | uses: dtolnay/rust-toolchain@stable 118 | with: 119 | target: ${{ matrix.target }} 120 | - name: Build beep example 121 | run: cargo build --example beep --release --target ${{ matrix.target }} 122 | 123 | wasm32-bindgen-test: 124 | 125 | strategy: 126 | matrix: 127 | target: [wasm32-unknown-unknown] 128 | 129 | runs-on: ubuntu-latest 130 | 131 | steps: 132 | - uses: actions/checkout@v4 133 | - name: Install stable 134 | uses: dtolnay/rust-toolchain@stable 135 | with: 136 | target: ${{ matrix.target }} 137 | - name: Build beep example 138 | run: cargo build --example beep --target ${{ matrix.target }} --features=wasm-bindgen 139 | 140 | wasm32-wasi-test: 141 | 142 | strategy: 143 | matrix: 144 | target: [wasm32-wasip1] 145 | 146 | runs-on: ubuntu-latest 147 | 148 | steps: 149 | - uses: actions/checkout@v4 150 | - name: Install stable 151 | uses: dtolnay/rust-toolchain@stable 152 | with: 153 | target: ${{ matrix.target }} 154 | - name: Build beep example 155 | run: cargo build --example beep --target ${{ matrix.target }} 156 | 157 | windows-test: 158 | strategy: 159 | matrix: 160 | version: [x86_64, i686] 161 | runs-on: windows-latest 162 | steps: 163 | - uses: actions/checkout@v4 164 | - name: Install ASIO SDK 165 | env: 166 | LINK: https://www.steinberg.net/asiosdk 167 | run: | 168 | curl -L -o asio.zip $env:LINK 169 | 7z x -oasio asio.zip 170 | move asio\*\* asio\ 171 | - name: Install ASIO4ALL 172 | run: choco install asio4all 173 | - name: Install llvm and clang 174 | run: choco install llvm 175 | - name: Install stable 176 | uses: dtolnay/rust-toolchain@stable 177 | with: 178 | target: ${{ matrix.version }}-pc-windows-msvc 179 | - name: Run without features 180 | run: cargo test --all --no-default-features --verbose 181 | - name: Run all features 182 | run: | 183 | $Env:CPAL_ASIO_DIR = "$Env:GITHUB_WORKSPACE\asio" 184 | cargo test --all --all-features --verbose 185 | 186 | macos-test: 187 | runs-on: macOS-latest 188 | steps: 189 | - uses: actions/checkout@v4 190 | - name: Install llvm and clang 191 | run: brew install llvm 192 | - name: Install stable 193 | uses: dtolnay/rust-toolchain@stable 194 | - name: Build beep example 195 | run: cargo build --example beep 196 | - name: Run without features 197 | run: cargo test --all --no-default-features --verbose 198 | - name: Run all features 199 | run: cargo test --all --all-features --verbose 200 | 201 | android-check: 202 | runs-on: ubuntu-latest 203 | steps: 204 | - uses: actions/checkout@v4 205 | - name: Install stable (Android target) 206 | uses: dtolnay/rust-toolchain@stable 207 | with: 208 | target: armv7-linux-androideabi 209 | - name: Check android 210 | working-directory: examples/android 211 | run: cargo check --target armv7-linux-androideabi --verbose 212 | - name: Check beep 213 | run: cargo check --example beep --target armv7-linux-androideabi --verbose 214 | - name: Check enumerate 215 | run: cargo check --example enumerate --target armv7-linux-androideabi --verbose 216 | - name: Check feedback 217 | run: cargo check --example feedback --target armv7-linux-androideabi --verbose 218 | - name: Check record_wav 219 | run: cargo check --example record_wav --target armv7-linux-androideabi --verbose 220 | 221 | android-apk-build: 222 | runs-on: ubuntu-latest 223 | steps: 224 | - uses: actions/checkout@v4 225 | - name: Install stable (Android targets) 226 | uses: dtolnay/rust-toolchain@stable 227 | with: 228 | targets: armv7-linux-androideabi,aarch64-linux-android,i686-linux-android,x86_64-linux-android 229 | - name: Set Up Android tools 230 | run: | 231 | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "build-tools;30.0.2" "platforms;android-30" 232 | - name: Install Cargo APK 233 | run: cargo install cargo-apk 234 | - name: Build APK 235 | working-directory: examples/android 236 | run: cargo apk build 237 | 238 | ios-build: 239 | runs-on: macOS-latest 240 | steps: 241 | - uses: actions/checkout@v4 242 | - name: Install llvm and clang 243 | run: brew install llvm 244 | - name: Install stable (iOS targets) 245 | uses: dtolnay/rust-toolchain@stable 246 | with: 247 | targets: aarch64-apple-ios,x86_64-apple-ios 248 | - name: Install cargo lipo 249 | run: cargo install cargo-lipo 250 | - name: Build iphonesimulator feedback example 251 | run: cd examples/ios-feedback && xcodebuild -scheme cpal-ios-example -configuration Debug -derivedDataPath build -sdk iphonesimulator 252 | 253 | wasm-beep-build: 254 | # this only confirms that the Rust source builds 255 | # and checks to prevent regressions like #721. 256 | # 257 | # It does not test the javascript/web integration 258 | runs-on: ubuntu-latest 259 | steps: 260 | - uses: actions/checkout@v4 261 | - name: Install stable (wasm32 target) 262 | uses: dtolnay/rust-toolchain@stable 263 | with: 264 | targets: wasm32-unknown-unknown 265 | - name: Cargo Build 266 | working-directory: ./examples/wasm-beep 267 | run: cargo build --target wasm32-unknown-unknown 268 | 269 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .cargo/ 4 | .DS_Store 5 | recorded.wav 6 | rls*.log 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | - All error enums are now `Clone`. 4 | 5 | # Version 0.15.3 (2024-03-04) 6 | 7 | - Add `try_with_sample_rate`, a non-panicking variant of `with_sample_rate`. 8 | - struct `platform::Stream` is now #[must_use]. 9 | - enum `SupportedBufferSize` and struct `SupportedStreamConfigRange` are now `Copy`. 10 | - `platform::Device` is now `Clone`. 11 | - Remove `parking_lot` dependency in favor of the std library. 12 | - Fix crash on web/wasm when `atomics` flag is enabled. 13 | - Improve Examples: Migrate wasm example to `trunk`, Improve syth-thones example. 14 | - Improve CI: Update actions, Use Android 30 API level in CI, Remove `asmjs-unknown-emscripten` target. 15 | - Update `windows` dependency to v0.54 16 | - Update `jni` dependency to 0.21 17 | - Update `alsa` dependency to 0.9 18 | - Update `oboe` dependency to 0.6 19 | - Update `ndk` dependency to 0.8 and disable `default-features`. 20 | - Update `wasm-bindgen` to 0.2.89 21 | 22 | # Version 0.15.2 (2023-03-30) 23 | 24 | - webaudio: support multichannel output streams 25 | - Update `windows` dependency 26 | - wasapi: fix some thread panics 27 | 28 | # Version 0.15.1 (2023-03-14) 29 | 30 | - Add feature `oboe-shared-stdcxx` to enable `shared-stdcxx` on `oboe` for Android support 31 | - Remove `thiserror` dependency 32 | - Swith `mach` dependency to `mach2` 33 | 34 | # Version 0.15.0 (2023-01-29) 35 | 36 | - Update `windows-rs`, `jack`, `coreaudio-sys`, `oboe`, `alsa` dependencies 37 | - Switch to the `dasp_sample` crate for the sample trait 38 | - Switch to `web-sys` on the emscripten target 39 | - Adopt edition 2021 40 | - Add disconnection detection on Mac OS 41 | 42 | # Version 0.14.1 (2022-10-23) 43 | 44 | - Support the 0.6.1 release of `alsa-rs` 45 | - Fix `asio` feature broken in 0.14.0 46 | - `NetBSD` support 47 | - CI improvements 48 | 49 | # Version 0.14.0 (2022-08-22) 50 | 51 | - Switch to `windows-rs` crate 52 | - Turn `ndk-glue` into a dev-dependency and use `ndk-context` instead 53 | - Update dependencies (ndk, ndk-glue, parking_lot, once_cell, jack) 54 | 55 | # Version 0.13.5 (2022-01-28) 56 | 57 | - Faster sample format conversion 58 | - Update dependencies (ndk, oboe, ndk-glue, jack, alsa, nix) 59 | 60 | # Version 0.13.4 (2021-08-08) 61 | 62 | - wasapi: Allow both threading models and switch the default to STA 63 | - Update dependencies (core-foundation-sys, jni, rust-jack) 64 | - Alsa: improve stream setup parameters 65 | 66 | # Version 0.13.3 (2021-03-29) 67 | 68 | - Give each thread a unique name 69 | - Fix distortion regression on some alsa configs 70 | 71 | # Version 0.13.2 (2021-03-16) 72 | 73 | - Update dependencies (ndk, nix, oboe, jni, etc) 74 | 75 | # Version 0.13.1 (2020-11-08) 76 | 77 | - Don't panic when device is plugged out on Windows 78 | - Update `parking_lot` dependency 79 | 80 | # Version 0.13.0 (2020-10-28) 81 | 82 | - Add Android support via `oboe-rs`. 83 | - Add Android APK build an CI job. 84 | 85 | # Version 0.12.1 (2020-07-23) 86 | 87 | - Bugfix release to get the asio feature working again. 88 | 89 | # Version 0.12.0 (2020-07-09) 90 | 91 | - Large refactor removing the blocking EventLoop API. 92 | - Rename many `Format` types to `StreamConfig`: 93 | - `Format` type's `data_type` field renamed to `sample_format`. 94 | - `Shape` -> `StreamConfig` - The configuration input required to build a stream. 95 | - `Format` -> `SupportedStreamConfig` - Describes a single supported stream configuration. 96 | - `SupportedFormat` -> `SupportedStreamConfigRange` - Describes a range of supported configurations. 97 | - `Device::default_input/output_format` -> `Device::default_input/output_config`. 98 | - `Device::supported_input/output_formats` -> `Device::supported_input/output_configs`. 99 | - `Device::SupportedInput/OutputFormats` -> `Device::SupportedInput/OutputConfigs`. 100 | - `SupportedFormatsError` -> `SupportedStreamConfigsError` 101 | - `DefaultFormatError` -> `DefaultStreamConfigError` 102 | - `BuildStreamError::FormatNotSupported` -> `BuildStreamError::StreamConfigNotSupported` 103 | - Address deprecated use of `mem::uninitialized` in WASAPI. 104 | - Removed `UnknownTypeBuffer` in favour of specifying sample type. 105 | - Added `build_input/output_stream_raw` methods allowing for dynamically 106 | handling sample format type. 107 | - Added support for DragonFly platform. 108 | - Add `InputCallbackInfo` and `OutputCallbackInfo` types and update expected 109 | user data callback function signature to provide these. 110 | 111 | # Version 0.11.0 (2019-12-11) 112 | 113 | - Fix some underruns that could occur in ALSA. 114 | - Add name to `HostId`. 115 | - Use `snd_pcm_hw_params_set_buffer_time_near` rather than `set_buffer_time_max` 116 | in ALSA backend. 117 | - Remove many uses of `std::mem::uninitialized`. 118 | - Fix WASAPI capture logic. 119 | - Panic on stream ID overflow rather than returning an error. 120 | - Use `ringbuffer` crate in feedback example. 121 | - Move errors into a separate module. 122 | - Switch from `failure` to `thiserror` for error handling. 123 | - Add `winbase` winapi feature to solve windows compile error issues. 124 | - Lots of CI improvements. 125 | 126 | # Version 0.10.0 (2019-07-05) 127 | 128 | - core-foundation-sys and coreaudio-rs version bumps. 129 | - Add an ASIO host, available under Windows. 130 | - Introduce a new Host API, adding support for alternative audio APIs. 131 | - Remove sleep loop on macOS in favour of using a `Condvar`. 132 | - Allow users to handle stream callback errors with a new `StreamEvent` type. 133 | - Overhaul error handling throughout the crate. 134 | - Remove unnecessary Mutex from ALSA and WASAPI backends in favour of channels. 135 | - Remove `panic!` from OutputBuffer Deref impl as it is no longer necessary. 136 | 137 | # Version 0.9.0 (2019-06-06) 138 | 139 | - Better buffer handling 140 | - Fix logic error in frame/sample size 141 | - Added error handling for unknown ALSA device errors 142 | - Fix resuming a paused stream on Windows (wasapi). 143 | - Implement `default_output_format` for emscripten backend. 144 | 145 | # Version 0.8.1 (2018-03-18) 146 | 147 | - Fix the handling of non-default sample rates for coreaudio input streams. 148 | 149 | # Version 0.8.0 (2018-02-15) 150 | 151 | - Add `record_wav.rs` example. Records 3 seconds to 152 | `$CARGO_MANIFEST_DIR/recorded.wav` using default input device. 153 | - Update `enumerate.rs` example to display default input/output devices and 154 | formats. 155 | - Add input stream support to coreaudio, alsa and windows backends. 156 | - Introduce `StreamData` type for handling either input or output streams in 157 | `EventLoop::run` callback. 158 | - Add `Device::supported_{input/output}_formats` methods. 159 | - Add `Device::default_{input/output}_format` methods. 160 | - Add `default_{input/output}_device` functions. 161 | - Replace usage of `Voice` with `Stream` throughout the crate. 162 | - Remove `Endpoint` in favour of `Device` for supporting both input and output 163 | streams. 164 | 165 | # Version 0.7.0 (2018-02-04) 166 | 167 | - Rename `ChannelsCount` to `ChannelCount`. 168 | - Rename `SamplesRate` to `SampleRate`. 169 | - Rename the `min_samples_rate` field of `SupportedFormat` to `min_sample_rate` 170 | - Rename the `with_max_samples_rate()` method of`SupportedFormat` to `with_max_sample_rate()` 171 | - Rename the `samples_rate` field of `Format` to `sample_rate` 172 | - Changed the type of the `channels` field of the `SupportedFormat` struct from `Vec` to `ChannelCount` (an alias to `u16`) 173 | - Remove unused ChannelPosition API. 174 | - Implement `Endpoint` and `Format` Enumeration for macOS. 175 | - Implement format handling for macos `build_voice` method. 176 | 177 | # Version 0.6.0 (2017-12-11) 178 | 179 | - Changed the emscripten backend to consume less CPU. 180 | - Added improvements to the crate documentation. 181 | - Implement `pause` and `play` for ALSA backend. 182 | - Reduced the number of allocations in the CoreAudio backend. 183 | - Fixes for macOS build (#186, #189). 184 | 185 | # Version 0.5.1 (2017-10-21) 186 | 187 | - Added `Sample::to_i16()`, `Sample::to_u16()` and `Sample::from`. 188 | 189 | # Version 0.5.0 (2017-10-21) 190 | 191 | - Removed the dependency on the `futures` library. 192 | - Removed the `Voice` and `SamplesStream` types. 193 | - Added `EventLoop::build_voice`, `EventLoop::destroy_voice`, `EventLoop::play`, 194 | and `EventLoop::pause` that can be used to create, destroy, play and pause voices. 195 | - Added a `VoiceId` struct that is now used to identify a voice owned by an `EventLoop`. 196 | - Changed `EventLoop::run()` to take a callback that is called whenever a voice requires sound data. 197 | - Changed `supported_formats()` to produce a list of `SupportedFormat` instead of `Format`. A 198 | `SupportedFormat` must then be turned into a `Format` in order to build a voice. 199 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cpal" 3 | version = "0.15.3" 4 | description = "Low-level cross-platform audio I/O library in pure Rust." 5 | repository = "https://github.com/rustaudio/cpal" 6 | documentation = "https://docs.rs/cpal" 7 | license = "Apache-2.0" 8 | keywords = ["audio", "sound"] 9 | edition = "2021" 10 | rust-version = "1.70" 11 | 12 | [features] 13 | asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions. 14 | 15 | [dependencies] 16 | dasp_sample = "0.11" 17 | 18 | [dev-dependencies] 19 | anyhow = "1.0" 20 | hound = "3.5" 21 | ringbuf = "0.4.1" 22 | clap = { version = "4.0", features = ["derive"] } 23 | 24 | [target.'cfg(target_os = "windows")'.dependencies] 25 | windows = { version = "0.54.0", features = [ 26 | "Win32_Media_Audio", 27 | "Win32_Foundation", 28 | "Win32_Devices_Properties", 29 | "Win32_Media_KernelStreaming", 30 | "Win32_System_Com_StructuredStorage", 31 | "Win32_System_Threading", 32 | "Win32_Security", 33 | "Win32_System_SystemServices", 34 | "Win32_System_Variant", 35 | "Win32_Media_Multimedia", 36 | "Win32_UI_Shell_PropertiesSystem" 37 | ]} 38 | audio_thread_priority = { version = "0.33.0", optional = true } 39 | asio-sys = { version = "0.2", path = "asio-sys", optional = true } 40 | num-traits = { version = "0.2.6", optional = true } 41 | 42 | [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] 43 | alsa = "0.9" 44 | libc = "0.2" 45 | audio_thread_priority = { version = "0.33.0", optional = true } 46 | jack = { version = "0.13.0", optional = true } 47 | 48 | [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] 49 | core-foundation-sys = "0.8.2" # For linking to CoreFoundation.framework and handling device name `CFString`s. 50 | mach2 = "0.4" # For access to mach_timebase type. 51 | 52 | [target.'cfg(target_os = "macos")'.dependencies] 53 | coreaudio-rs = { version = "0.11", default-features = false, features = ["audio_unit", "core_audio"] } 54 | 55 | [target.'cfg(target_os = "ios")'.dependencies] 56 | coreaudio-rs = { version = "0.11", default-features = false, features = ["audio_unit", "core_audio", "audio_toolbox"] } 57 | 58 | [target.'cfg(target_os = "emscripten")'.dependencies] 59 | wasm-bindgen = { version = "0.2.89" } 60 | wasm-bindgen-futures = "0.4.33" 61 | js-sys = { version = "0.3.35" } 62 | web-sys = { version = "0.3.35", features = [ "AudioContext", "AudioContextOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioNode", "AudioDestinationNode", "Window", "AudioContextState"] } 63 | 64 | [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] 65 | wasm-bindgen = { version = "0.2.58", optional = true } 66 | js-sys = { version = "0.3.35" } 67 | web-sys = { version = "0.3.35", features = [ "AudioContext", "AudioContextOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioNode", "AudioDestinationNode", "Window", "AudioContextState"] } 68 | 69 | [target.'cfg(target_os = "android")'.dependencies] 70 | ndk = { version = "0.9", default-features = false, features = ["audio", "api-level-26"]} 71 | ndk-context = "0.1" 72 | jni = "0.21" 73 | num-derive = "0.4" 74 | num-traits = "0.2" 75 | 76 | [[example]] 77 | name = "beep" 78 | 79 | [[example]] 80 | name = "enumerate" 81 | 82 | [[example]] 83 | name = "feedback" 84 | 85 | [[example]] 86 | name = "record_wav" 87 | 88 | [[example]] 89 | name = "synth_tones" 90 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.armv7-unknown-linux-gnueabihf] 2 | dockerfile = "Dockerfile" 3 | 4 | [target.armv7-unknown-linux-gnueabihf.env] 5 | passthrough = [ 6 | "RUSTFLAGS", 7 | ] 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG CROSS_BASE_IMAGE 2 | FROM $CROSS_BASE_IMAGE 3 | 4 | ENV PKG_CONFIG_ALLOW_CROSS 1 5 | ENV PKG_CONFIG_PATH /usr/lib/arm-linux-gnueabihf/pkgconfig/ 6 | 7 | RUN dpkg --add-architecture armhf && \ 8 | apt-get update && \ 9 | apt-get install libasound2-dev:armhf -y && \ 10 | apt-get install libjack-jackd2-dev:armhf libjack-jackd2-0:armhf -y \ 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CPAL - Cross-Platform Audio Library 2 | 3 | [![Actions Status](https://github.com/RustAudio/cpal/workflows/cpal/badge.svg)](https://github.com/RustAudio/cpal/actions) 4 | [![Crates.io](https://img.shields.io/crates/v/cpal.svg)](https://crates.io/crates/cpal) [![docs.rs](https://docs.rs/cpal/badge.svg)](https://docs.rs/cpal/) 5 | 6 | Low-level library for audio input and output in pure Rust. 7 | 8 | This library currently supports the following: 9 | 10 | - Enumerate supported audio hosts. 11 | - Enumerate all available audio devices. 12 | - Get the current default input and output devices. 13 | - Enumerate known supported input and output stream formats for a device. 14 | - Get the current default input and output stream formats for a device. 15 | - Build and run input and output PCM streams on a chosen device with a given stream format. 16 | 17 | Currently, supported hosts include: 18 | 19 | - Linux (via ALSA or JACK) 20 | - Windows (via WASAPI by default, see ASIO instructions below) 21 | - macOS (via CoreAudio) 22 | - iOS (via CoreAudio) 23 | - Android (via AAudio) 24 | - Emscripten 25 | 26 | Note that on Linux, the ALSA development files are required. These are provided 27 | as part of the `libasound2-dev` package on Debian and Ubuntu distributions and 28 | `alsa-lib-devel` on Fedora. 29 | 30 | ## Compiling for Web Assembly 31 | 32 | If you are interested in using CPAL with WASM, please see [this guide](https://github.com/RustAudio/cpal/wiki/Setting-up-a-new-CPAL-WASM-project) in our Wiki which walks through setting up a new project from scratch. 33 | 34 | ## Feature flags for audio backends 35 | 36 | Some audio backends are optional and will only be compiled with a [feature flag](https://doc.rust-lang.org/cargo/reference/features.html). 37 | 38 | - JACK (on Linux): `jack` 39 | - ASIO (on Windows): `asio` 40 | 41 | ## ASIO on Windows 42 | 43 | [ASIO](https://en.wikipedia.org/wiki/Audio_Stream_Input/Output) is an audio 44 | driver protocol by Steinberg. While it is available on multiple operating 45 | systems, it is most commonly used on Windows to work around limitations of 46 | WASAPI including access to large numbers of channels and lower-latency audio 47 | processing. 48 | 49 | CPAL allows for using the ASIO SDK as the audio host on Windows instead of 50 | WASAPI. 51 | 52 | ### Locating the ASIO SDK 53 | 54 | The location of ASIO SDK is exposed to CPAL by setting the `CPAL_ASIO_DIR` environment variable. 55 | 56 | The build script will try to find the ASIO SDK by following these steps in order: 57 | 58 | 1. Check if `CPAL_ASIO_DIR` is set and if so use the path to point to the SDK. 59 | 2. Check if the ASIO SDK is already installed in the temporary directory, if so use that and set the path of `CPAL_ASIO_DIR` to the output of `std::env::temp_dir().join("asio_sdk")`. 60 | 3. If the ASIO SDK is not already installed, download it from and install it in the temporary directory. The path of `CPAL_ASIO_DIR` will be set to the output of `std::env::temp_dir().join("asio_sdk")`. 61 | 62 | In an ideal situation you don't need to worry about this step. 63 | 64 | ### Preparing the build environment 65 | 66 | 1. `bindgen`, the library used to generate bindings to the C++ SDK, requires 67 | clang. **Download and install LLVM** from 68 | [here](http://releases.llvm.org/download.html) under the "Pre-Built Binaries" 69 | section. The version as of writing this is 17.0.1. 70 | 2. Add the LLVM `bin` directory to a `LIBCLANG_PATH` environment variable. If 71 | you installed LLVM to the default directory, this should work in the command 72 | prompt: 73 | ``` 74 | setx LIBCLANG_PATH "C:\Program Files\LLVM\bin" 75 | ``` 76 | 3. If you don't have any ASIO devices or drivers available, you can [**download 77 | and install ASIO4ALL**](http://www.asio4all.org/). Be sure to enable the 78 | "offline" feature during installation despite what the installer says about 79 | it being useless. 80 | 4. Our build script assumes that Microsoft Visual Studio is installed if the host OS for compilation is Windows. The script will try to find `vcvarsall.bat` 81 | and execute it with the right host and target machine architecture regardless of the Microsoft Visual Studio version. 82 | If there are any errors encountered in this process which is unlikely, 83 | you may find the `vcvarsall.bat` manually and execute it with your machine architecture as an argument. 84 | The script will detect this and skip the step. 85 | 86 | A manually executed command example for 64 bit machines: 87 | 88 | ``` 89 | "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64 90 | ``` 91 | 92 | For more information please refer to the documentation of [`vcvarsall.bat``](https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=msvc-160#vcvarsall-syntax). 93 | 94 | 5. Select the ASIO host at the start of our program with the following code: 95 | 96 | ```rust 97 | let host; 98 | #[cfg(target_os = "windows")] 99 | { 100 | host = cpal::host_from_id(cpal::HostId::Asio).expect("failed to initialise ASIO host"); 101 | } 102 | ``` 103 | 104 | If you run into compilations errors produced by `asio-sys` or `bindgen`, make 105 | sure that `CPAL_ASIO_DIR` is set correctly and try `cargo clean`. 106 | 107 | 6. Make sure to enable the `asio` feature when building CPAL: 108 | 109 | ``` 110 | cargo build --features "asio" 111 | ``` 112 | 113 | or if you are using CPAL as a dependency in a downstream project, enable the 114 | feature like this: 115 | 116 | ```toml 117 | cpal = { version = "*", features = ["asio"] } 118 | ``` 119 | 120 | _Updated as of ASIO version 2.3.3._ 121 | 122 | ### Cross compilation 123 | 124 | When Windows is the host and the target OS, the build script of `asio-sys` supports all cross compilation targets 125 | which are supported by the MSVC compiler. An exhaustive list of combinations could be found [here](https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=msvc-160#vcvarsall-syntax) with the addition of undocumented `arm64`, `arm64_x86`, `arm64_amd64` and `arm64_arm` targets. (5.11.2023) 126 | 127 | It is also possible to compile Windows applications with ASIO support on Linux and macOS. 128 | 129 | For both platforms the common way to do this is to use the [MinGW-w64](https://www.mingw-w64.org/) toolchain. 130 | 131 | Make sure that you have included the `MinGW-w64` include directory in your `CPLUS_INCLUDE_PATH` environment variable. 132 | Make sure that LLVM is installed and include directory is also included in your `CPLUS_INCLUDE_PATH` environment variable. 133 | 134 | Example for macOS for the target of `x86_64-pc-windows-gnu` where `mingw-w64` is installed via brew: 135 | 136 | ``` 137 | export CPLUS_INCLUDE_PATH="$CPLUS_INCLUDE_PATH:/opt/homebrew/Cellar/mingw-w64/11.0.1/toolchain-x86_64/x86_64-w64-mingw32/include" 138 | ``` 139 | -------------------------------------------------------------------------------- /asio-sys/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /asio-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "asio-sys" 3 | version = "0.2.2" 4 | authors = ["Tom Gowan "] 5 | description = "Low-level interface and binding generation for the steinberg ASIO SDK." 6 | repository = "https://github.com/RustAudio/cpal/" 7 | documentation = "https://docs.rs/asio-sys" 8 | license = "Apache-2.0" 9 | keywords = ["audio", "sound", "asio", "steinberg"] 10 | build = "build.rs" 11 | 12 | [build-dependencies] 13 | bindgen = "0.69" 14 | walkdir = "2" 15 | cc = "1.0.83" 16 | parse_cfg = "4.1.1" 17 | 18 | [dependencies] 19 | num-derive = "0.4" 20 | num-traits = "0.2" 21 | -------------------------------------------------------------------------------- /asio-sys/asio-link/helpers.cpp: -------------------------------------------------------------------------------- 1 | #include "helpers.hpp" 2 | #include 3 | 4 | extern "C" ASIOError get_sample_rate(double * rate){ 5 | return ASIOGetSampleRate(reinterpret_cast(rate)); 6 | } 7 | 8 | extern "C" ASIOError set_sample_rate(double rate){ 9 | return ASIOSetSampleRate(rate); 10 | } 11 | 12 | extern "C" ASIOError can_sample_rate(double rate){ 13 | return ASIOCanSampleRate(rate); 14 | } 15 | 16 | extern AsioDrivers* asioDrivers; 17 | bool loadAsioDriver(char *name); 18 | 19 | extern "C" bool load_asio_driver(char * name){ 20 | return loadAsioDriver(name); 21 | } 22 | 23 | extern "C" void remove_current_driver() { 24 | asioDrivers->removeCurrentDriver(); 25 | } 26 | extern "C" long get_driver_names(char **names, long maxDrivers) { 27 | AsioDrivers ad; 28 | return ad.getDriverNames(names, maxDrivers); 29 | } -------------------------------------------------------------------------------- /asio-sys/asio-link/helpers.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "asiodrivers.h" 3 | #include "asio.h" 4 | 5 | // Helper function to wrap confusing preprocessor 6 | extern "C" ASIOError get_sample_rate(double * rate); 7 | 8 | // Helper function to wrap confusing preprocessor 9 | extern "C" ASIOError set_sample_rate(double rate); 10 | 11 | // Helper function to wrap confusing preprocessor 12 | extern "C" ASIOError can_sample_rate(double rate); 13 | 14 | extern "C" bool load_asio_driver(char * name); 15 | extern "C" void remove_current_driver(); 16 | extern "C" long get_driver_names(char **names, long maxDrivers); -------------------------------------------------------------------------------- /asio-sys/examples/enumerate.rs: -------------------------------------------------------------------------------- 1 | /* This example aims to produce the same behaviour 2 | * as the enumerate example in cpal 3 | * by Tom Gowan 4 | */ 5 | 6 | extern crate asio_sys as sys; 7 | 8 | // This is the same data that enumerate 9 | // is trying to find 10 | // Basically these are stubbed versions 11 | // 12 | // Format that each sample has. 13 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 14 | pub enum SampleFormat { 15 | // The value 0 corresponds to 0. 16 | I16, 17 | // The value 0 corresponds to 32768. 18 | U16, 19 | // The boundaries are (-1.0, 1.0). 20 | F32, 21 | } 22 | // Number of channels. 23 | pub type ChannelCount = u16; 24 | 25 | // The number of samples processed per second for a single channel of audio. 26 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 27 | pub struct SampleRate(pub u32); 28 | 29 | #[derive(Debug, Clone, PartialEq, Eq)] 30 | pub struct Format { 31 | pub channels: ChannelCount, 32 | pub sample_rate: SampleRate, 33 | pub data_type: SampleFormat, 34 | } 35 | 36 | fn main() { 37 | let asio = sys::Asio::new(); 38 | for name in asio.driver_names() { 39 | println!("Driver: {:?}", name); 40 | let driver = asio.load_driver(&name).expect("failed to load driver"); 41 | let channels = driver 42 | .channels() 43 | .expect("failed to retrieve channel counts"); 44 | let sample_rate = driver 45 | .sample_rate() 46 | .expect("failed to retrieve sample rate"); 47 | let in_fmt = Format { 48 | channels: channels.ins as _, 49 | sample_rate: SampleRate(sample_rate as _), 50 | data_type: SampleFormat::F32, 51 | }; 52 | let out_fmt = Format { 53 | channels: channels.outs as _, 54 | sample_rate: SampleRate(sample_rate as _), 55 | data_type: SampleFormat::F32, 56 | }; 57 | println!(" Input {:?}", in_fmt); 58 | println!(" Output {:?}", out_fmt); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /asio-sys/examples/test.rs: -------------------------------------------------------------------------------- 1 | extern crate asio_sys as sys; 2 | 3 | fn main() { 4 | let asio = sys::Asio::new(); 5 | for driver in asio.driver_names() { 6 | println!("Driver: {}", driver); 7 | let driver = asio.load_driver(&driver).expect("failed to load drivers"); 8 | println!( 9 | " Channels: {:?}", 10 | driver.channels().expect("failed to get channels") 11 | ); 12 | println!( 13 | " Sample rate: {:?}", 14 | driver.sample_rate().expect("failed to get sample rate") 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /asio-sys/src/bindings/asio_import.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(dead_code)] 5 | #![allow(clippy::missing_safety_doc)] 6 | 7 | include!(concat!(env!("OUT_DIR"), "/asio_bindings.rs")); 8 | -------------------------------------------------------------------------------- /asio-sys/src/bindings/errors.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | /// Errors that might occur during `Asio::load_driver`. 5 | #[derive(Clone, Debug)] 6 | pub enum LoadDriverError { 7 | LoadDriverFailed, 8 | DriverAlreadyExists, 9 | InitializationFailed(AsioError), 10 | } 11 | 12 | /// General errors returned by ASIO. 13 | #[derive(Clone, Debug)] 14 | pub enum AsioError { 15 | NoDrivers, 16 | HardwareMalfunction, 17 | InvalidInput, 18 | BadMode, 19 | HardwareStuck, 20 | NoRate, 21 | ASE_NoMemory, 22 | InvalidBufferSize, 23 | UnknownError, 24 | } 25 | 26 | #[derive(Debug)] 27 | pub enum AsioErrorWrapper { 28 | ASE_OK = 0, // This value will be returned whenever the call succeeded 29 | ASE_SUCCESS = 0x3f4847a0, // unique success return value for ASIOFuture calls 30 | ASE_NotPresent = -1000, // hardware input or output is not present or available 31 | ASE_HWMalfunction, // hardware is malfunctioning (can be returned by any ASIO function) 32 | ASE_InvalidParameter, // input parameter invalid 33 | ASE_InvalidMode, // hardware is in a bad mode or used in a bad mode 34 | ASE_SPNotAdvancing, // hardware is not running when sample position is inquired 35 | ASE_NoClock, // sample clock or rate cannot be determined or is not present 36 | ASE_NoMemory, // not enough memory for completing the request 37 | Invalid, 38 | } 39 | 40 | impl fmt::Display for LoadDriverError { 41 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 42 | match *self { 43 | LoadDriverError::LoadDriverFailed => { 44 | write!( 45 | f, 46 | "ASIO `loadDriver` function returned `false` indicating failure" 47 | ) 48 | } 49 | LoadDriverError::InitializationFailed(ref err) => { 50 | write!(f, "{err}") 51 | } 52 | LoadDriverError::DriverAlreadyExists => { 53 | write!(f, "ASIO only supports loading one driver at a time") 54 | } 55 | } 56 | } 57 | } 58 | 59 | impl fmt::Display for AsioError { 60 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 61 | match *self { 62 | AsioError::NoDrivers => { 63 | write!(f, "hardware input or output is not present or available") 64 | } 65 | AsioError::HardwareMalfunction => write!( 66 | f, 67 | "hardware is malfunctioning (can be returned by any ASIO function)" 68 | ), 69 | AsioError::InvalidInput => write!(f, "input parameter invalid"), 70 | AsioError::BadMode => write!(f, "hardware is in a bad mode or used in a bad mode"), 71 | AsioError::HardwareStuck => write!( 72 | f, 73 | "hardware is not running when sample position is inquired" 74 | ), 75 | AsioError::NoRate => write!( 76 | f, 77 | "sample clock or rate cannot be determined or is not present" 78 | ), 79 | AsioError::ASE_NoMemory => write!(f, "not enough memory for completing the request"), 80 | AsioError::InvalidBufferSize => write!(f, "buffersize out of range for device"), 81 | AsioError::UnknownError => write!(f, "Error not in SDK"), 82 | } 83 | } 84 | } 85 | 86 | impl Error for LoadDriverError {} 87 | impl Error for AsioError {} 88 | 89 | impl From for LoadDriverError { 90 | fn from(err: AsioError) -> Self { 91 | LoadDriverError::InitializationFailed(err) 92 | } 93 | } 94 | 95 | macro_rules! asio_result { 96 | ($e:expr) => {{ 97 | let res = { $e }; 98 | match res { 99 | r if r == AsioErrorWrapper::ASE_OK as i32 => Ok(()), 100 | r if r == AsioErrorWrapper::ASE_SUCCESS as i32 => Ok(()), 101 | r if r == AsioErrorWrapper::ASE_NotPresent as i32 => Err(AsioError::NoDrivers), 102 | r if r == AsioErrorWrapper::ASE_HWMalfunction as i32 => { 103 | Err(AsioError::HardwareMalfunction) 104 | } 105 | r if r == AsioErrorWrapper::ASE_InvalidParameter as i32 => Err(AsioError::InvalidInput), 106 | r if r == AsioErrorWrapper::ASE_InvalidMode as i32 => Err(AsioError::BadMode), 107 | r if r == AsioErrorWrapper::ASE_SPNotAdvancing as i32 => Err(AsioError::HardwareStuck), 108 | r if r == AsioErrorWrapper::ASE_NoClock as i32 => Err(AsioError::NoRate), 109 | r if r == AsioErrorWrapper::ASE_NoMemory as i32 => Err(AsioError::ASE_NoMemory), 110 | _ => Err(AsioError::UnknownError), 111 | } 112 | }}; 113 | } 114 | -------------------------------------------------------------------------------- /asio-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | 3 | #[macro_use] 4 | extern crate num_derive; 5 | extern crate num_traits; 6 | 7 | #[cfg(asio)] 8 | pub mod bindings; 9 | #[cfg(asio)] 10 | pub use bindings::errors::{AsioError, LoadDriverError}; 11 | #[cfg(asio)] 12 | pub use bindings::*; 13 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | const CPAL_ASIO_DIR: &str = "CPAL_ASIO_DIR"; 4 | 5 | fn main() { 6 | println!("cargo:rerun-if-env-changed={}", CPAL_ASIO_DIR); 7 | 8 | // If ASIO directory isn't set silently return early 9 | // otherwise set the asio config flag 10 | match env::var(CPAL_ASIO_DIR) { 11 | Err(_) => {} 12 | Ok(_) => println!("cargo:rustc-cfg=asio"), 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /examples/android/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .cargo/ 4 | .DS_Store 5 | recorded.wav 6 | rls*.log 7 | -------------------------------------------------------------------------------- /examples/android/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "android" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | cpal = { path = "../../" } 8 | anyhow = "1.0" 9 | ndk-glue = "0.7" 10 | 11 | [lib] 12 | name = "android" 13 | path = "src/lib.rs" 14 | crate-type = ["cdylib"] 15 | 16 | [package.metadata.android] 17 | # Specifies the package property of the manifest. 18 | package = "com.foo.bar" 19 | 20 | # Specifies the array of targets to build for. 21 | build_targets = [ "armv7-linux-androideabi", "aarch64-linux-android", "i686-linux-android", "x86_64-linux-android" ] 22 | 23 | # Name for final APK file. 24 | # Defaults to package name. 25 | apk_name = "myapp" 26 | 27 | [package.metadata.android.sdk] 28 | min_sdk_version = 26 29 | target_sdk_version = 30 30 | max_sdk_version = 29 31 | -------------------------------------------------------------------------------- /examples/android/README.md: -------------------------------------------------------------------------------- 1 | ## How to install 2 | 3 | ```sh 4 | rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android 5 | ``` 6 | 7 | ## How to build apk 8 | 9 | ```sh 10 | # Builds the project in release mode and places it into a `apk` file. 11 | cargo apk build --release 12 | ``` 13 | 14 | more information at: https://github.com/rust-mobile/cargo-apk -------------------------------------------------------------------------------- /examples/android/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | extern crate anyhow; 4 | extern crate cpal; 5 | 6 | use cpal::{ 7 | traits::{DeviceTrait, HostTrait, StreamTrait}, 8 | SizedSample, I24, 9 | }; 10 | use cpal::{FromSample, Sample}; 11 | 12 | #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "full"))] 13 | fn main() { 14 | let host = cpal::default_host(); 15 | 16 | let device = host 17 | .default_output_device() 18 | .expect("failed to find output device"); 19 | 20 | let config = device.default_output_config().unwrap(); 21 | 22 | match config.sample_format() { 23 | cpal::SampleFormat::I8 => run::(&device, &config.into()).unwrap(), 24 | cpal::SampleFormat::I16 => run::(&device, &config.into()).unwrap(), 25 | cpal::SampleFormat::I24 => run::(&device, &config.into()).unwrap(), 26 | cpal::SampleFormat::I32 => run::(&device, &config.into()).unwrap(), 27 | // cpal::SampleFormat::I48 => run::(&device, &config.into()).unwrap(), 28 | cpal::SampleFormat::I64 => run::(&device, &config.into()).unwrap(), 29 | cpal::SampleFormat::U8 => run::(&device, &config.into()).unwrap(), 30 | cpal::SampleFormat::U16 => run::(&device, &config.into()).unwrap(), 31 | // cpal::SampleFormat::U24 => run::(&device, &config.into()).unwrap(), 32 | cpal::SampleFormat::U32 => run::(&device, &config.into()).unwrap(), 33 | // cpal::SampleFormat::U48 => run::(&device, &config.into()).unwrap(), 34 | cpal::SampleFormat::U64 => run::(&device, &config.into()).unwrap(), 35 | cpal::SampleFormat::F32 => run::(&device, &config.into()).unwrap(), 36 | cpal::SampleFormat::F64 => run::(&device, &config.into()).unwrap(), 37 | sample_format => panic!("Unsupported sample format '{sample_format}'"), 38 | } 39 | } 40 | 41 | fn run(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error> 42 | where 43 | T: SizedSample + FromSample, 44 | { 45 | let sample_rate = config.sample_rate.0 as f32; 46 | let channels = config.channels as usize; 47 | 48 | // Produce a sinusoid of maximum amplitude. 49 | let mut sample_clock = 0f32; 50 | let mut next_value = move || { 51 | sample_clock = (sample_clock + 1.0) % sample_rate; 52 | (sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin() 53 | }; 54 | 55 | let err_fn = |err| eprintln!("an error occurred on stream: {}", err); 56 | 57 | let stream = device.build_output_stream( 58 | config, 59 | move |data: &mut [T], _: &cpal::OutputCallbackInfo| { 60 | write_data(data, channels, &mut next_value) 61 | }, 62 | err_fn, 63 | None, 64 | )?; 65 | stream.play()?; 66 | 67 | std::thread::sleep(std::time::Duration::from_millis(1000)); 68 | 69 | Ok(()) 70 | } 71 | 72 | fn write_data(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32) 73 | where 74 | T: Sample + FromSample, 75 | { 76 | for frame in output.chunks_mut(channels) { 77 | let value: T = T::from_sample(next_sample()); 78 | for sample in frame.iter_mut() { 79 | *sample = value; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/beep.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use cpal::{ 3 | traits::{DeviceTrait, HostTrait, StreamTrait}, 4 | FromSample, Sample, SizedSample, I24, 5 | }; 6 | 7 | #[derive(Parser, Debug)] 8 | #[command(version, about = "CPAL beep example", long_about = None)] 9 | struct Opt { 10 | /// The audio device to use 11 | #[arg(short, long, default_value_t = String::from("default"))] 12 | device: String, 13 | 14 | /// Use the JACK host 15 | #[cfg(all( 16 | any( 17 | target_os = "linux", 18 | target_os = "dragonfly", 19 | target_os = "freebsd", 20 | target_os = "netbsd" 21 | ), 22 | feature = "jack" 23 | ))] 24 | #[arg(short, long)] 25 | #[allow(dead_code)] 26 | jack: bool, 27 | } 28 | 29 | fn main() -> anyhow::Result<()> { 30 | let opt = Opt::parse(); 31 | 32 | // Conditionally compile with jack if the feature is specified. 33 | #[cfg(all( 34 | any( 35 | target_os = "linux", 36 | target_os = "dragonfly", 37 | target_os = "freebsd", 38 | target_os = "netbsd" 39 | ), 40 | feature = "jack" 41 | ))] 42 | // Manually check for flags. Can be passed through cargo with -- e.g. 43 | // cargo run --release --example beep --features jack -- --jack 44 | let host = if opt.jack { 45 | cpal::host_from_id(cpal::available_hosts() 46 | .into_iter() 47 | .find(|id| *id == cpal::HostId::Jack) 48 | .expect( 49 | "make sure --features jack is specified. only works on OSes where jack is available", 50 | )).expect("jack host unavailable") 51 | } else { 52 | cpal::default_host() 53 | }; 54 | 55 | #[cfg(any( 56 | not(any( 57 | target_os = "linux", 58 | target_os = "dragonfly", 59 | target_os = "freebsd", 60 | target_os = "netbsd" 61 | )), 62 | not(feature = "jack") 63 | ))] 64 | let host = cpal::default_host(); 65 | 66 | let device = if opt.device == "default" { 67 | host.default_output_device() 68 | } else { 69 | host.output_devices()? 70 | .find(|x| x.name().map(|y| y == opt.device).unwrap_or(false)) 71 | } 72 | .expect("failed to find output device"); 73 | println!("Output device: {}", device.name()?); 74 | 75 | let config = device.default_output_config().unwrap(); 76 | println!("Default output config: {:?}", config); 77 | 78 | match config.sample_format() { 79 | cpal::SampleFormat::I8 => run::(&device, &config.into()), 80 | cpal::SampleFormat::I16 => run::(&device, &config.into()), 81 | cpal::SampleFormat::I24 => run::(&device, &config.into()), 82 | cpal::SampleFormat::I32 => run::(&device, &config.into()), 83 | // cpal::SampleFormat::I48 => run::(&device, &config.into()), 84 | cpal::SampleFormat::I64 => run::(&device, &config.into()), 85 | cpal::SampleFormat::U8 => run::(&device, &config.into()), 86 | cpal::SampleFormat::U16 => run::(&device, &config.into()), 87 | // cpal::SampleFormat::U24 => run::(&device, &config.into()), 88 | cpal::SampleFormat::U32 => run::(&device, &config.into()), 89 | // cpal::SampleFormat::U48 => run::(&device, &config.into()), 90 | cpal::SampleFormat::U64 => run::(&device, &config.into()), 91 | cpal::SampleFormat::F32 => run::(&device, &config.into()), 92 | cpal::SampleFormat::F64 => run::(&device, &config.into()), 93 | sample_format => panic!("Unsupported sample format '{sample_format}'"), 94 | } 95 | } 96 | 97 | pub fn run(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error> 98 | where 99 | T: SizedSample + FromSample, 100 | { 101 | let sample_rate = config.sample_rate.0 as f32; 102 | let channels = config.channels as usize; 103 | 104 | // Produce a sinusoid of maximum amplitude. 105 | let mut sample_clock = 0f32; 106 | let mut next_value = move || { 107 | sample_clock = (sample_clock + 1.0) % sample_rate; 108 | (sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin() 109 | }; 110 | 111 | let err_fn = |err| eprintln!("an error occurred on stream: {}", err); 112 | 113 | let stream = device.build_output_stream( 114 | config, 115 | move |data: &mut [T], _: &cpal::OutputCallbackInfo| { 116 | write_data(data, channels, &mut next_value) 117 | }, 118 | err_fn, 119 | None, 120 | )?; 121 | stream.play()?; 122 | 123 | std::thread::sleep(std::time::Duration::from_millis(1000)); 124 | 125 | Ok(()) 126 | } 127 | 128 | fn write_data(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32) 129 | where 130 | T: Sample + FromSample, 131 | { 132 | for frame in output.chunks_mut(channels) { 133 | let value: T = T::from_sample(next_sample()); 134 | for sample in frame.iter_mut() { 135 | *sample = value; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /examples/enumerate.rs: -------------------------------------------------------------------------------- 1 | extern crate anyhow; 2 | extern crate cpal; 3 | 4 | use cpal::traits::{DeviceTrait, HostTrait}; 5 | 6 | fn main() -> Result<(), anyhow::Error> { 7 | println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS); 8 | let available_hosts = cpal::available_hosts(); 9 | println!("Available hosts:\n {:?}", available_hosts); 10 | 11 | for host_id in available_hosts { 12 | println!("{}", host_id.name()); 13 | let host = cpal::host_from_id(host_id)?; 14 | 15 | let default_in = host.default_input_device().map(|e| e.name().unwrap()); 16 | let default_out = host.default_output_device().map(|e| e.name().unwrap()); 17 | println!(" Default Input Device:\n {:?}", default_in); 18 | println!(" Default Output Device:\n {:?}", default_out); 19 | 20 | let devices = host.devices()?; 21 | println!(" Devices: "); 22 | for (device_index, device) in devices.enumerate() { 23 | println!(" {}. \"{}\"", device_index + 1, device.name()?); 24 | 25 | // Input configs 26 | if let Ok(conf) = device.default_input_config() { 27 | println!(" Default input stream config:\n {:?}", conf); 28 | } 29 | let input_configs = match device.supported_input_configs() { 30 | Ok(f) => f.collect(), 31 | Err(e) => { 32 | println!(" Error getting supported input configs: {:?}", e); 33 | Vec::new() 34 | } 35 | }; 36 | if !input_configs.is_empty() { 37 | println!(" All supported input stream configs:"); 38 | for (config_index, config) in input_configs.into_iter().enumerate() { 39 | println!( 40 | " {}.{}. {:?}", 41 | device_index + 1, 42 | config_index + 1, 43 | config 44 | ); 45 | } 46 | } 47 | 48 | // Output configs 49 | if let Ok(conf) = device.default_output_config() { 50 | println!(" Default output stream config:\n {:?}", conf); 51 | } 52 | let output_configs = match device.supported_output_configs() { 53 | Ok(f) => f.collect(), 54 | Err(e) => { 55 | println!(" Error getting supported output configs: {:?}", e); 56 | Vec::new() 57 | } 58 | }; 59 | if !output_configs.is_empty() { 60 | println!(" All supported output stream configs:"); 61 | for (config_index, config) in output_configs.into_iter().enumerate() { 62 | println!( 63 | " {}.{}. {:?}", 64 | device_index + 1, 65 | config_index + 1, 66 | config 67 | ); 68 | } 69 | } 70 | } 71 | } 72 | 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /examples/feedback.rs: -------------------------------------------------------------------------------- 1 | //! Feeds back the input stream directly into the output stream. 2 | //! 3 | //! Assumes that the input and output devices can use the same stream configuration and that they 4 | //! support the f32 sample format. 5 | //! 6 | //! Uses a delay of `LATENCY_MS` milliseconds in case the default input and output streams are not 7 | //! precisely synchronised. 8 | 9 | use clap::Parser; 10 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 11 | use ringbuf::{ 12 | traits::{Consumer, Producer, Split}, 13 | HeapRb, 14 | }; 15 | 16 | #[derive(Parser, Debug)] 17 | #[command(version, about = "CPAL feedback example", long_about = None)] 18 | struct Opt { 19 | /// The input audio device to use 20 | #[arg(short, long, value_name = "IN", default_value_t = String::from("default"))] 21 | input_device: String, 22 | 23 | /// The output audio device to use 24 | #[arg(short, long, value_name = "OUT", default_value_t = String::from("default"))] 25 | output_device: String, 26 | 27 | /// Specify the delay between input and output 28 | #[arg(short, long, value_name = "DELAY_MS", default_value_t = 150.0)] 29 | latency: f32, 30 | 31 | /// Use the JACK host 32 | #[cfg(all( 33 | any( 34 | target_os = "linux", 35 | target_os = "dragonfly", 36 | target_os = "freebsd", 37 | target_os = "netbsd" 38 | ), 39 | feature = "jack" 40 | ))] 41 | #[arg(short, long)] 42 | #[allow(dead_code)] 43 | jack: bool, 44 | } 45 | 46 | fn main() -> anyhow::Result<()> { 47 | let opt = Opt::parse(); 48 | 49 | // Conditionally compile with jack if the feature is specified. 50 | #[cfg(all( 51 | any( 52 | target_os = "linux", 53 | target_os = "dragonfly", 54 | target_os = "freebsd", 55 | target_os = "netbsd" 56 | ), 57 | feature = "jack" 58 | ))] 59 | // Manually check for flags. Can be passed through cargo with -- e.g. 60 | // cargo run --release --example beep --features jack -- --jack 61 | let host = if opt.jack { 62 | cpal::host_from_id(cpal::available_hosts() 63 | .into_iter() 64 | .find(|id| *id == cpal::HostId::Jack) 65 | .expect( 66 | "make sure --features jack is specified. only works on OSes where jack is available", 67 | )).expect("jack host unavailable") 68 | } else { 69 | cpal::default_host() 70 | }; 71 | 72 | #[cfg(any( 73 | not(any( 74 | target_os = "linux", 75 | target_os = "dragonfly", 76 | target_os = "freebsd", 77 | target_os = "netbsd" 78 | )), 79 | not(feature = "jack") 80 | ))] 81 | let host = cpal::default_host(); 82 | 83 | // Find devices. 84 | let input_device = if opt.input_device == "default" { 85 | host.default_input_device() 86 | } else { 87 | host.input_devices()? 88 | .find(|x| x.name().map(|y| y == opt.input_device).unwrap_or(false)) 89 | } 90 | .expect("failed to find input device"); 91 | 92 | let output_device = if opt.output_device == "default" { 93 | host.default_output_device() 94 | } else { 95 | host.output_devices()? 96 | .find(|x| x.name().map(|y| y == opt.output_device).unwrap_or(false)) 97 | } 98 | .expect("failed to find output device"); 99 | 100 | println!("Using input device: \"{}\"", input_device.name()?); 101 | println!("Using output device: \"{}\"", output_device.name()?); 102 | 103 | // We'll try and use the same configuration between streams to keep it simple. 104 | let config: cpal::StreamConfig = input_device.default_input_config()?.into(); 105 | 106 | // Create a delay in case the input and output devices aren't synced. 107 | let latency_frames = (opt.latency / 1_000.0) * config.sample_rate.0 as f32; 108 | let latency_samples = latency_frames as usize * config.channels as usize; 109 | 110 | // The buffer to share samples 111 | let ring = HeapRb::::new(latency_samples * 2); 112 | let (mut producer, mut consumer) = ring.split(); 113 | 114 | // Fill the samples with 0.0 equal to the length of the delay. 115 | for _ in 0..latency_samples { 116 | // The ring buffer has twice as much space as necessary to add latency here, 117 | // so this should never fail 118 | producer.try_push(0.0).unwrap(); 119 | } 120 | 121 | let input_data_fn = move |data: &[f32], _: &cpal::InputCallbackInfo| { 122 | let mut output_fell_behind = false; 123 | for &sample in data { 124 | if producer.try_push(sample).is_err() { 125 | output_fell_behind = true; 126 | } 127 | } 128 | if output_fell_behind { 129 | eprintln!("output stream fell behind: try increasing latency"); 130 | } 131 | }; 132 | 133 | let output_data_fn = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { 134 | let mut input_fell_behind = false; 135 | for sample in data { 136 | *sample = match consumer.try_pop() { 137 | Some(s) => s, 138 | None => { 139 | input_fell_behind = true; 140 | 0.0 141 | } 142 | }; 143 | } 144 | if input_fell_behind { 145 | eprintln!("input stream fell behind: try increasing latency"); 146 | } 147 | }; 148 | 149 | // Build streams. 150 | println!( 151 | "Attempting to build both streams with f32 samples and `{:?}`.", 152 | config 153 | ); 154 | let input_stream = input_device.build_input_stream(&config, input_data_fn, err_fn, None)?; 155 | let output_stream = output_device.build_output_stream(&config, output_data_fn, err_fn, None)?; 156 | println!("Successfully built streams."); 157 | 158 | // Play the streams. 159 | println!( 160 | "Starting the input and output streams with `{}` milliseconds of latency.", 161 | opt.latency 162 | ); 163 | input_stream.play()?; 164 | output_stream.play()?; 165 | 166 | // Run for 3 seconds before closing. 167 | println!("Playing for 3 seconds... "); 168 | std::thread::sleep(std::time::Duration::from_secs(3)); 169 | drop(input_stream); 170 | drop(output_stream); 171 | println!("Done!"); 172 | Ok(()) 173 | } 174 | 175 | fn err_fn(err: cpal::StreamError) { 176 | eprintln!("an error occurred on stream: {}", err); 177 | } 178 | -------------------------------------------------------------------------------- /examples/ios-feedback/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cpal-ios-example" 3 | version = "0.1.0" 4 | authors = ["Michael Hills "] 5 | edition = "2018" 6 | 7 | [lib] 8 | name = "cpal_ios_example" 9 | crate-type = ["staticlib"] 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | cpal = { path = "../.." } 15 | anyhow = "1.0.12" 16 | ringbuf = "0.1.6" 17 | 18 | -------------------------------------------------------------------------------- /examples/ios-feedback/README.md: -------------------------------------------------------------------------------- 1 | # iOS Feedback Example 2 | 3 | This example is an Xcode project that exercises both input and output 4 | audio streams. Audio samples are read in from your microphone and then 5 | routed to your audio output device with a small but noticeable delay, 6 | so you can verify it is working correctly. 7 | 8 | To build the example you will need to install `cargo-lipo`. While not 9 | necessary for building iOS binaries, it is used to build a universal 10 | binary (x86 for simulator and aarch64 for device.) 11 | 12 | ``` 13 | cargo install cargo-lipo 14 | ``` 15 | 16 | Then open the XCode project and click run. A hook in the iOS application 17 | lifecycle calls into the Rust code to start the input/output feedback 18 | loop and immediately returns back control. 19 | 20 | Before calling into Rust, the AVAudioSession category is configured. 21 | This is important for controlling how audio is shared with the rest 22 | of the system when your app is in the foreground. One example is 23 | controlling whether other apps can play music in the background. 24 | More information [here](https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionCategoriesandModes/AudioSessionCategoriesandModes.html#//apple_ref/doc/uid/TP40007875-CH10). 25 | 26 | -------------------------------------------------------------------------------- /examples/ios-feedback/build_rust_deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | PATH=$PATH:$HOME/.cargo/bin 6 | if [[ -n "${DEVELOPER_SDK_DIR:-}" ]]; then 7 | # Assume we're in Xcode, which means we're probably cross-compiling. 8 | # In this case, we need to add an extra library search path for build scripts and proc-macros, 9 | # which run on the host instead of the target. 10 | # (macOS Big Sur does not have linkable libraries in /usr/lib/.) 11 | export LIBRARY_PATH="${DEVELOPER_SDK_DIR}/MacOSX.sdk/usr/lib:${LIBRARY_PATH:-}" 12 | fi 13 | 14 | # If you want your build to run faster, add a "--targets x86_64-apple-ios" for just using the ios simulator. 15 | if [ -n "${IOS_TARGETS}" ]; then 16 | cargo lipo --targets "${IOS_TARGETS}" 17 | else 18 | cargo lipo 19 | fi 20 | -------------------------------------------------------------------------------- /examples/ios-feedback/cpal-ios-example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/ios-feedback/cpal-ios-example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/ios-feedback/cpal-ios-example.xcodeproj/xcuserdata/mikeh.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | cargo_ios.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | cpal-ios-example.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/ios-feedback/ios-src/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // cpal-ios-example 4 | // 5 | // Created by Michael Hills on 2/10/20. 6 | // 7 | 8 | #import 9 | 10 | @interface AppDelegate : UIResponder 11 | 12 | @property (strong, nonatomic) UIWindow *window; 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /examples/ios-feedback/ios-src/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // cpal-ios-example 4 | // 5 | // Created by Michael Hills on 2/10/20. 6 | // 7 | 8 | #import "AppDelegate.h" 9 | @import AVFoundation; 10 | 11 | void rust_ios_main(void); 12 | 13 | 14 | @interface AppDelegate () 15 | 16 | @end 17 | 18 | @implementation AppDelegate 19 | 20 | 21 | 22 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 23 | // Override point for customization after application launch. 24 | 25 | NSError *error; 26 | BOOL success; 27 | 28 | // It is necessary to access the sharedInstance so that calls to AudioSessionGetProperty 29 | // will work. 30 | AVAudioSession *session = AVAudioSession.sharedInstance; 31 | // Setting up the category is not necessary, but generally advised. 32 | // Since this demo records and plays, lets use AVAudioSessionCategoryPlayAndRecord. 33 | // Also default to speaker as defaulting to the phone earpiece would be unusual. 34 | // Allowing bluetooth should direct audio to your bluetooth headset. 35 | success = [session setCategory:AVAudioSessionCategoryPlayAndRecord 36 | withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetooth 37 | error:&error]; 38 | 39 | if (success) { 40 | NSLog(@"Calling rust_ios_main()"); 41 | rust_ios_main(); 42 | } else { 43 | NSLog(@"Failed to configure audio session category"); 44 | } 45 | 46 | return YES; 47 | } 48 | 49 | @end 50 | -------------------------------------------------------------------------------- /examples/ios-feedback/ios-src/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/ios-feedback/ios-src/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/ios-feedback/ios-src/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSupportsIndirectInputEvents 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | NSMicrophoneUsageDescription 40 | cpal feedback demo 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/ios-feedback/ios-src/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // cpal-ios-example 4 | // 5 | // Created by Michael Hills on 2/10/20. 6 | // 7 | 8 | #import 9 | 10 | @interface ViewController : UIViewController 11 | 12 | 13 | @end 14 | 15 | -------------------------------------------------------------------------------- /examples/ios-feedback/ios-src/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // cpal-ios-example 4 | // 5 | // Created by Michael Hills on 2/10/20. 6 | // 7 | 8 | #import "ViewController.h" 9 | 10 | @interface ViewController () 11 | 12 | @end 13 | 14 | @implementation ViewController 15 | 16 | - (void)viewDidLoad { 17 | [super viewDidLoad]; 18 | // Do any additional setup after loading the view. 19 | } 20 | 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /examples/ios-feedback/ios-src/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // cpal-ios-example 4 | // 5 | // Created by Michael Hills on 2/10/20. 6 | // 7 | 8 | #import 9 | #import "AppDelegate.h" 10 | 11 | int main(int argc, char * argv[]) { 12 | NSString * appDelegateClassName; 13 | @autoreleasepool { 14 | // Setup code that might create autoreleased objects goes here. 15 | appDelegateClassName = NSStringFromClass([AppDelegate class]); 16 | } 17 | return UIApplicationMain(argc, argv, nil, appDelegateClassName); 18 | } 19 | -------------------------------------------------------------------------------- /examples/ios-feedback/src/feedback.rs: -------------------------------------------------------------------------------- 1 | //! Feeds back the input stream directly into the output stream. 2 | //! 3 | //! Assumes that the input and output devices can use the same stream configuration and that they 4 | //! support the f32 sample format. 5 | //! 6 | //! Uses a delay of `LATENCY_MS` milliseconds in case the default input and output streams are not 7 | //! precisely synchronised. 8 | 9 | extern crate anyhow; 10 | extern crate cpal; 11 | extern crate ringbuf; 12 | 13 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 14 | use ringbuf::RingBuffer; 15 | 16 | const LATENCY_MS: f32 = 1000.0; 17 | 18 | pub fn run_example() -> Result<(), anyhow::Error> { 19 | let host = cpal::default_host(); 20 | 21 | // Default devices. 22 | let input_device = host 23 | .default_input_device() 24 | .expect("failed to get default input device"); 25 | let output_device = host 26 | .default_output_device() 27 | .expect("failed to get default output device"); 28 | println!("Using default input device: \"{}\"", input_device.name()?); 29 | println!("Using default output device: \"{}\"", output_device.name()?); 30 | 31 | // We'll try and use the same configuration between streams to keep it simple. 32 | let config: cpal::StreamConfig = input_device.default_input_config()?.into(); 33 | 34 | // Create a delay in case the input and output devices aren't synced. 35 | let latency_frames = (LATENCY_MS / 1_000.0) * config.sample_rate.0 as f32; 36 | let latency_samples = latency_frames as usize * config.channels as usize; 37 | 38 | // The buffer to share samples 39 | let ring = RingBuffer::new(latency_samples * 2); 40 | let (mut producer, mut consumer) = ring.split(); 41 | 42 | // Fill the samples with 0.0 equal to the length of the delay. 43 | for _ in 0..latency_samples { 44 | // The ring buffer has twice as much space as necessary to add latency here, 45 | // so this should never fail 46 | producer.push(0.0).unwrap(); 47 | } 48 | 49 | let input_data_fn = move |data: &[f32], _: &cpal::InputCallbackInfo| { 50 | let mut output_fell_behind = false; 51 | for &sample in data { 52 | if producer.push(sample).is_err() { 53 | output_fell_behind = true; 54 | } 55 | } 56 | if output_fell_behind { 57 | eprintln!("output stream fell behind: try increasing latency"); 58 | } 59 | }; 60 | 61 | let output_data_fn = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { 62 | let mut input_fell_behind = None; 63 | for sample in data { 64 | *sample = match consumer.pop() { 65 | Ok(s) => s, 66 | Err(err) => { 67 | input_fell_behind = Some(err); 68 | 0.0 69 | } 70 | }; 71 | } 72 | if let Some(err) = input_fell_behind { 73 | eprintln!( 74 | "input stream fell behind: {:?}: try increasing latency", 75 | err 76 | ); 77 | } 78 | }; 79 | 80 | // Build streams. 81 | println!( 82 | "Attempting to build both streams with f32 samples and `{:?}`.", 83 | config 84 | ); 85 | println!("Setup input stream"); 86 | let input_stream = input_device.build_input_stream(&config, input_data_fn, err_fn, None)?; 87 | println!("Setup output stream"); 88 | let output_stream = output_device.build_output_stream(&config, output_data_fn, err_fn, None)?; 89 | println!("Successfully built streams."); 90 | 91 | // Play the streams. 92 | println!( 93 | "Starting the input and output streams with `{}` milliseconds of latency.", 94 | LATENCY_MS 95 | ); 96 | input_stream.play()?; 97 | output_stream.play()?; 98 | 99 | // for the purposes of this demo, leak these so that after returning the audio units will 100 | // keep running 101 | std::mem::forget(input_stream); 102 | std::mem::forget(output_stream); 103 | Ok(()) 104 | } 105 | 106 | fn err_fn(err: cpal::StreamError) { 107 | eprintln!("an error occurred on stream: {}", err); 108 | } 109 | -------------------------------------------------------------------------------- /examples/ios-feedback/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod feedback; 2 | 3 | #[no_mangle] 4 | pub extern "C" fn rust_ios_main() { 5 | feedback::run_example().unwrap(); 6 | } 7 | -------------------------------------------------------------------------------- /examples/record_wav.rs: -------------------------------------------------------------------------------- 1 | //! Records a WAV file (roughly 3 seconds long) using the default input device and config. 2 | //! 3 | //! The input data is recorded to "$CARGO_MANIFEST_DIR/recorded.wav". 4 | 5 | use clap::Parser; 6 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 7 | use cpal::{FromSample, Sample}; 8 | use std::fs::File; 9 | use std::io::BufWriter; 10 | use std::sync::{Arc, Mutex}; 11 | 12 | #[derive(Parser, Debug)] 13 | #[command(version, about = "CPAL record_wav example", long_about = None)] 14 | struct Opt { 15 | /// The audio device to use 16 | #[arg(short, long, default_value_t = String::from("default"))] 17 | device: String, 18 | 19 | /// Use the JACK host 20 | #[cfg(all( 21 | any( 22 | target_os = "linux", 23 | target_os = "dragonfly", 24 | target_os = "freebsd", 25 | target_os = "netbsd" 26 | ), 27 | feature = "jack" 28 | ))] 29 | #[arg(short, long)] 30 | #[allow(dead_code)] 31 | jack: bool, 32 | } 33 | 34 | fn main() -> Result<(), anyhow::Error> { 35 | let opt = Opt::parse(); 36 | 37 | // Conditionally compile with jack if the feature is specified. 38 | #[cfg(all( 39 | any( 40 | target_os = "linux", 41 | target_os = "dragonfly", 42 | target_os = "freebsd", 43 | target_os = "netbsd" 44 | ), 45 | feature = "jack" 46 | ))] 47 | // Manually check for flags. Can be passed through cargo with -- e.g. 48 | // cargo run --release --example beep --features jack -- --jack 49 | let host = if opt.jack { 50 | cpal::host_from_id(cpal::available_hosts() 51 | .into_iter() 52 | .find(|id| *id == cpal::HostId::Jack) 53 | .expect( 54 | "make sure --features jack is specified. only works on OSes where jack is available", 55 | )).expect("jack host unavailable") 56 | } else { 57 | cpal::default_host() 58 | }; 59 | 60 | #[cfg(any( 61 | not(any( 62 | target_os = "linux", 63 | target_os = "dragonfly", 64 | target_os = "freebsd", 65 | target_os = "netbsd" 66 | )), 67 | not(feature = "jack") 68 | ))] 69 | let host = cpal::default_host(); 70 | 71 | // Set up the input device and stream with the default input config. 72 | let device = if opt.device == "default" { 73 | host.default_input_device() 74 | } else { 75 | host.input_devices()? 76 | .find(|x| x.name().map(|y| y == opt.device).unwrap_or(false)) 77 | } 78 | .expect("failed to find input device"); 79 | 80 | println!("Input device: {}", device.name()?); 81 | 82 | let config = device 83 | .default_input_config() 84 | .expect("Failed to get default input config"); 85 | println!("Default input config: {:?}", config); 86 | 87 | // The WAV file we're recording to. 88 | const PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/recorded.wav"); 89 | let spec = wav_spec_from_config(&config); 90 | let writer = hound::WavWriter::create(PATH, spec)?; 91 | let writer = Arc::new(Mutex::new(Some(writer))); 92 | 93 | // A flag to indicate that recording is in progress. 94 | println!("Begin recording..."); 95 | 96 | // Run the input stream on a separate thread. 97 | let writer_2 = writer.clone(); 98 | 99 | let err_fn = move |err| { 100 | eprintln!("an error occurred on stream: {}", err); 101 | }; 102 | 103 | let stream = match config.sample_format() { 104 | cpal::SampleFormat::I8 => device.build_input_stream( 105 | &config.into(), 106 | move |data, _: &_| write_input_data::(data, &writer_2), 107 | err_fn, 108 | None, 109 | )?, 110 | cpal::SampleFormat::I16 => device.build_input_stream( 111 | &config.into(), 112 | move |data, _: &_| write_input_data::(data, &writer_2), 113 | err_fn, 114 | None, 115 | )?, 116 | cpal::SampleFormat::I32 => device.build_input_stream( 117 | &config.into(), 118 | move |data, _: &_| write_input_data::(data, &writer_2), 119 | err_fn, 120 | None, 121 | )?, 122 | cpal::SampleFormat::F32 => device.build_input_stream( 123 | &config.into(), 124 | move |data, _: &_| write_input_data::(data, &writer_2), 125 | err_fn, 126 | None, 127 | )?, 128 | sample_format => { 129 | return Err(anyhow::Error::msg(format!( 130 | "Unsupported sample format '{sample_format}'" 131 | ))) 132 | } 133 | }; 134 | 135 | stream.play()?; 136 | 137 | // Let recording go for roughly three seconds. 138 | std::thread::sleep(std::time::Duration::from_secs(3)); 139 | drop(stream); 140 | writer.lock().unwrap().take().unwrap().finalize()?; 141 | println!("Recording {} complete!", PATH); 142 | Ok(()) 143 | } 144 | 145 | fn sample_format(format: cpal::SampleFormat) -> hound::SampleFormat { 146 | if format.is_float() { 147 | hound::SampleFormat::Float 148 | } else { 149 | hound::SampleFormat::Int 150 | } 151 | } 152 | 153 | fn wav_spec_from_config(config: &cpal::SupportedStreamConfig) -> hound::WavSpec { 154 | hound::WavSpec { 155 | channels: config.channels() as _, 156 | sample_rate: config.sample_rate().0 as _, 157 | bits_per_sample: (config.sample_format().sample_size() * 8) as _, 158 | sample_format: sample_format(config.sample_format()), 159 | } 160 | } 161 | 162 | type WavWriterHandle = Arc>>>>; 163 | 164 | fn write_input_data(input: &[T], writer: &WavWriterHandle) 165 | where 166 | T: Sample, 167 | U: Sample + hound::Sample + FromSample, 168 | { 169 | if let Ok(mut guard) = writer.try_lock() { 170 | if let Some(writer) = guard.as_mut() { 171 | for &sample in input.iter() { 172 | let sample: U = U::from_sample(sample); 173 | writer.write_sample(sample).ok(); 174 | } 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /examples/synth_tones.rs: -------------------------------------------------------------------------------- 1 | /* This example expose parameter to pass generator of sample. 2 | Good starting point for integration of cpal into your application. 3 | */ 4 | 5 | extern crate anyhow; 6 | extern crate clap; 7 | extern crate cpal; 8 | 9 | use cpal::{ 10 | traits::{DeviceTrait, HostTrait, StreamTrait}, 11 | SizedSample, I24, 12 | }; 13 | use cpal::{FromSample, Sample}; 14 | 15 | fn main() -> anyhow::Result<()> { 16 | let stream = stream_setup_for()?; 17 | stream.play()?; 18 | std::thread::sleep(std::time::Duration::from_millis(4000)); 19 | Ok(()) 20 | } 21 | 22 | pub enum Waveform { 23 | Sine, 24 | Square, 25 | Saw, 26 | Triangle, 27 | } 28 | 29 | pub struct Oscillator { 30 | pub sample_rate: f32, 31 | pub waveform: Waveform, 32 | pub current_sample_index: f32, 33 | pub frequency_hz: f32, 34 | } 35 | 36 | impl Oscillator { 37 | fn advance_sample(&mut self) { 38 | self.current_sample_index = (self.current_sample_index + 1.0) % self.sample_rate; 39 | } 40 | 41 | fn set_waveform(&mut self, waveform: Waveform) { 42 | self.waveform = waveform; 43 | } 44 | 45 | fn calculate_sine_output_from_freq(&self, freq: f32) -> f32 { 46 | let two_pi = 2.0 * std::f32::consts::PI; 47 | (self.current_sample_index * freq * two_pi / self.sample_rate).sin() 48 | } 49 | 50 | fn is_multiple_of_freq_above_nyquist(&self, multiple: f32) -> bool { 51 | self.frequency_hz * multiple > self.sample_rate / 2.0 52 | } 53 | 54 | fn sine_wave(&mut self) -> f32 { 55 | self.advance_sample(); 56 | self.calculate_sine_output_from_freq(self.frequency_hz) 57 | } 58 | 59 | fn generative_waveform(&mut self, harmonic_index_increment: i32, gain_exponent: f32) -> f32 { 60 | self.advance_sample(); 61 | let mut output = 0.0; 62 | let mut i = 1; 63 | while !self.is_multiple_of_freq_above_nyquist(i as f32) { 64 | let gain = 1.0 / (i as f32).powf(gain_exponent); 65 | output += gain * self.calculate_sine_output_from_freq(self.frequency_hz * i as f32); 66 | i += harmonic_index_increment; 67 | } 68 | output 69 | } 70 | 71 | fn square_wave(&mut self) -> f32 { 72 | self.generative_waveform(2, 1.0) 73 | } 74 | 75 | fn saw_wave(&mut self) -> f32 { 76 | self.generative_waveform(1, 1.0) 77 | } 78 | 79 | fn triangle_wave(&mut self) -> f32 { 80 | self.generative_waveform(2, 2.0) 81 | } 82 | 83 | fn tick(&mut self) -> f32 { 84 | match self.waveform { 85 | Waveform::Sine => self.sine_wave(), 86 | Waveform::Square => self.square_wave(), 87 | Waveform::Saw => self.saw_wave(), 88 | Waveform::Triangle => self.triangle_wave(), 89 | } 90 | } 91 | } 92 | 93 | pub fn stream_setup_for() -> Result 94 | where 95 | { 96 | let (_host, device, config) = host_device_setup()?; 97 | 98 | match config.sample_format() { 99 | cpal::SampleFormat::I8 => make_stream::(&device, &config.into()), 100 | cpal::SampleFormat::I16 => make_stream::(&device, &config.into()), 101 | cpal::SampleFormat::I24 => make_stream::(&device, &config.into()), 102 | cpal::SampleFormat::I32 => make_stream::(&device, &config.into()), 103 | cpal::SampleFormat::I64 => make_stream::(&device, &config.into()), 104 | cpal::SampleFormat::U8 => make_stream::(&device, &config.into()), 105 | cpal::SampleFormat::U16 => make_stream::(&device, &config.into()), 106 | cpal::SampleFormat::U32 => make_stream::(&device, &config.into()), 107 | cpal::SampleFormat::U64 => make_stream::(&device, &config.into()), 108 | cpal::SampleFormat::F32 => make_stream::(&device, &config.into()), 109 | cpal::SampleFormat::F64 => make_stream::(&device, &config.into()), 110 | sample_format => Err(anyhow::Error::msg(format!( 111 | "Unsupported sample format '{sample_format}'" 112 | ))), 113 | } 114 | } 115 | 116 | pub fn host_device_setup( 117 | ) -> Result<(cpal::Host, cpal::Device, cpal::SupportedStreamConfig), anyhow::Error> { 118 | let host = cpal::default_host(); 119 | 120 | let device = host 121 | .default_output_device() 122 | .ok_or_else(|| anyhow::Error::msg("Default output device is not available"))?; 123 | println!("Output device : {}", device.name()?); 124 | 125 | let config = device.default_output_config()?; 126 | println!("Default output config : {:?}", config); 127 | 128 | Ok((host, device, config)) 129 | } 130 | 131 | pub fn make_stream( 132 | device: &cpal::Device, 133 | config: &cpal::StreamConfig, 134 | ) -> Result 135 | where 136 | T: SizedSample + FromSample, 137 | { 138 | let num_channels = config.channels as usize; 139 | let mut oscillator = Oscillator { 140 | waveform: Waveform::Sine, 141 | sample_rate: config.sample_rate.0 as f32, 142 | current_sample_index: 0.0, 143 | frequency_hz: 440.0, 144 | }; 145 | let err_fn = |err| eprintln!("Error building output sound stream: {}", err); 146 | 147 | let time_at_start = std::time::Instant::now(); 148 | println!("Time at start: {:?}", time_at_start); 149 | 150 | let stream = device.build_output_stream( 151 | config, 152 | move |output: &mut [T], _: &cpal::OutputCallbackInfo| { 153 | // for 0-1s play sine, 1-2s play square, 2-3s play saw, 3-4s play triangle_wave 154 | let time_since_start = std::time::Instant::now() 155 | .duration_since(time_at_start) 156 | .as_secs_f32(); 157 | if time_since_start < 1.0 { 158 | oscillator.set_waveform(Waveform::Sine); 159 | } else if time_since_start < 2.0 { 160 | oscillator.set_waveform(Waveform::Triangle); 161 | } else if time_since_start < 3.0 { 162 | oscillator.set_waveform(Waveform::Square); 163 | } else if time_since_start < 4.0 { 164 | oscillator.set_waveform(Waveform::Saw); 165 | } else { 166 | oscillator.set_waveform(Waveform::Sine); 167 | } 168 | process_frame(output, &mut oscillator, num_channels) 169 | }, 170 | err_fn, 171 | None, 172 | )?; 173 | 174 | Ok(stream) 175 | } 176 | 177 | fn process_frame( 178 | output: &mut [SampleType], 179 | oscillator: &mut Oscillator, 180 | num_channels: usize, 181 | ) where 182 | SampleType: Sample + FromSample, 183 | { 184 | for frame in output.chunks_mut(num_channels) { 185 | let value: SampleType = SampleType::from_sample(oscillator.tick()); 186 | 187 | // copy the same value to all channels 188 | for sample in frame.iter_mut() { 189 | *sample = value; 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /examples/wasm-beep/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | /dist 3 | /target 4 | -------------------------------------------------------------------------------- /examples/wasm-beep/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasm-beep" 3 | description = "cpal beep example for WebAssembly" 4 | version = "0.1.0" 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [profile.release] 11 | # This makes the compiled code faster and smaller, but it makes compiling slower, 12 | # so it's only enabled in release mode. 13 | lto = true 14 | 15 | [features] 16 | # If you uncomment this line, it will enable `wee_alloc`: 17 | #default = ["wee_alloc"] 18 | 19 | [dependencies] 20 | cpal = { path = "../..", features = ["wasm-bindgen"] } 21 | # `gloo` is a utility crate which improves ergonomics over direct `web-sys` usage. 22 | gloo = "0.11.0" 23 | # The `wasm-bindgen` crate provides the bare minimum functionality needed 24 | # to interact with JavaScript. 25 | wasm-bindgen = "0.2.45" 26 | 27 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 28 | # compared to the default allocator's ~10K. However, it is slower than the default 29 | # allocator, so it's not enabled by default. 30 | wee_alloc = { version = "0.4.2", optional = true } 31 | 32 | # The `console_error_panic_hook` crate provides better debugging of panics by 33 | # logging them with `console.error`. 34 | console_error_panic_hook = "0.1.5" 35 | 36 | # The `web-sys` crate allows you to interact with the various browser APIs, 37 | # like the DOM. 38 | [dependencies.web-sys] 39 | version = "0.3.22" 40 | features = ["console", "MouseEvent"] 41 | -------------------------------------------------------------------------------- /examples/wasm-beep/README.md: -------------------------------------------------------------------------------- 1 | ## How to install 2 | 3 | [trunk](https://trunkrs.dev/) is used to build and serve the example. 4 | ```sh 5 | cargo install --locked trunk 6 | # -- or -- 7 | cargo binstall trunk 8 | ``` 9 | 10 | ## How to run in debug mode 11 | 12 | ```sh 13 | # Builds the project and opens it in a new browser tab. Auto-reloads when the project changes. 14 | trunk serve --open 15 | ``` 16 | 17 | ## How to build in release mode 18 | 19 | ```sh 20 | # Builds the project in release mode and places it into the `dist` folder. 21 | trunk build --release 22 | ``` 23 | 24 | ## What does each file do? 25 | 26 | * `Cargo.toml` contains the standard Rust metadata. You put your Rust dependencies in here. You must change this file with your details (name, description, version, authors, categories) 27 | 28 | * The `src` folder contains your Rust code. 29 | -------------------------------------------------------------------------------- /examples/wasm-beep/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | cpal beep example 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/wasm-beep/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::Cell, rc::Rc}; 2 | 3 | use cpal::{ 4 | traits::{DeviceTrait, HostTrait, StreamTrait}, 5 | Stream, 6 | }; 7 | use wasm_bindgen::prelude::*; 8 | use web_sys::console; 9 | 10 | // When the `wee_alloc` feature is enabled, this uses `wee_alloc` as the global 11 | // allocator. 12 | // 13 | // If you don't want to use `wee_alloc`, you can safely delete this. 14 | #[cfg(feature = "wee_alloc")] 15 | #[global_allocator] 16 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 17 | 18 | // This is like the `main` function, except for JavaScript. 19 | #[wasm_bindgen(start)] 20 | pub fn main_js() -> Result<(), JsValue> { 21 | // This provides better error messages in debug mode. 22 | // It's disabled in release mode, so it doesn't bloat up the file size. 23 | #[cfg(debug_assertions)] 24 | console_error_panic_hook::set_once(); 25 | 26 | let document = gloo::utils::document(); 27 | let play_button = document.get_element_by_id("play").unwrap(); 28 | let stop_button = document.get_element_by_id("stop").unwrap(); 29 | 30 | // stream needs to be referenced from the "play" and "stop" closures 31 | let stream = Rc::new(Cell::new(None)); 32 | 33 | // set up play button 34 | { 35 | let stream = stream.clone(); 36 | let closure = Closure::::new(move |_event: web_sys::MouseEvent| { 37 | stream.set(Some(beep())); 38 | }); 39 | play_button 40 | .add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?; 41 | closure.forget(); 42 | } 43 | 44 | // set up stop button 45 | { 46 | let closure = Closure::::new(move |_event: web_sys::MouseEvent| { 47 | // stop the stream by dropping it 48 | stream.take(); 49 | }); 50 | stop_button 51 | .add_event_listener_with_callback("mousedown", closure.as_ref().unchecked_ref())?; 52 | closure.forget(); 53 | } 54 | 55 | Ok(()) 56 | } 57 | 58 | fn beep() -> Stream { 59 | let host = cpal::default_host(); 60 | let device = host 61 | .default_output_device() 62 | .expect("failed to find a default output device"); 63 | let config = device.default_output_config().unwrap(); 64 | 65 | match config.sample_format() { 66 | cpal::SampleFormat::F32 => run::(&device, &config.into()), 67 | cpal::SampleFormat::I16 => run::(&device, &config.into()), 68 | cpal::SampleFormat::U16 => run::(&device, &config.into()), 69 | _ => panic!("unsupported sample format"), 70 | } 71 | } 72 | 73 | fn run(device: &cpal::Device, config: &cpal::StreamConfig) -> Stream 74 | where 75 | T: cpal::Sample + cpal::SizedSample + cpal::FromSample, 76 | { 77 | let sample_rate = config.sample_rate.0 as f32; 78 | let channels = config.channels as usize; 79 | 80 | // Produce a sinusoid of maximum amplitude. 81 | let mut sample_clock = 0f32; 82 | let mut next_value = move || { 83 | sample_clock = (sample_clock + 1.0) % sample_rate; 84 | (sample_clock * 440.0 * 2.0 * 3.141592 / sample_rate).sin() 85 | }; 86 | 87 | let err_fn = |err| console::error_1(&format!("an error occurred on stream: {}", err).into()); 88 | 89 | let stream = device 90 | .build_output_stream( 91 | config, 92 | move |data: &mut [T], _| write_data(data, channels, &mut next_value), 93 | err_fn, 94 | None, 95 | ) 96 | .unwrap(); 97 | stream.play().unwrap(); 98 | stream 99 | } 100 | 101 | fn write_data(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32) 102 | where 103 | T: cpal::Sample + cpal::FromSample, 104 | { 105 | for frame in output.chunks_mut(channels) { 106 | let sample = next_sample(); 107 | let value = T::from_sample::(sample); 108 | for sample in frame.iter_mut() { 109 | *sample = value; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt::{Display, Formatter}; 3 | 4 | /// The requested host, although supported on this platform, is unavailable. 5 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 6 | pub struct HostUnavailable; 7 | 8 | impl Display for HostUnavailable { 9 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 10 | f.write_str("the requested host is unavailable") 11 | } 12 | } 13 | 14 | impl Error for HostUnavailable {} 15 | 16 | /// Some error has occurred that is specific to the backend from which it was produced. 17 | /// 18 | /// This error is often used as a catch-all in cases where: 19 | /// 20 | /// - It is unclear exactly what error might be produced by the backend API. 21 | /// - It does not make sense to add a variant to the enclosing error type. 22 | /// - No error was expected to occur at all, but we return an error to avoid the possibility of a 23 | /// `panic!` caused by some unforeseen or unknown reason. 24 | /// 25 | /// **Note:** If you notice a `BackendSpecificError` that you believe could be better handled in a 26 | /// cross-platform manner, please create an issue or submit a pull request with a patch that adds 27 | /// the necessary error variant to the appropriate error enum. 28 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 29 | pub struct BackendSpecificError { 30 | pub description: String, 31 | } 32 | 33 | impl Display for BackendSpecificError { 34 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 35 | write!( 36 | f, 37 | "A backend-specific error has occurred: {}", 38 | self.description 39 | ) 40 | } 41 | } 42 | 43 | impl Error for BackendSpecificError {} 44 | 45 | /// An error that might occur while attempting to enumerate the available devices on a system. 46 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 47 | pub enum DevicesError { 48 | /// See the [`BackendSpecificError`] docs for more information about this error variant. 49 | BackendSpecific { err: BackendSpecificError }, 50 | } 51 | 52 | impl Display for DevicesError { 53 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 54 | match self { 55 | Self::BackendSpecific { err } => err.fmt(f), 56 | } 57 | } 58 | } 59 | 60 | impl Error for DevicesError {} 61 | 62 | impl From for DevicesError { 63 | fn from(err: BackendSpecificError) -> Self { 64 | Self::BackendSpecific { err } 65 | } 66 | } 67 | 68 | /// An error that may occur while attempting to retrieve a device name. 69 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 70 | pub enum DeviceNameError { 71 | /// See the [`BackendSpecificError`] docs for more information about this error variant. 72 | BackendSpecific { err: BackendSpecificError }, 73 | } 74 | 75 | impl Display for DeviceNameError { 76 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 77 | match self { 78 | Self::BackendSpecific { err } => err.fmt(f), 79 | } 80 | } 81 | } 82 | 83 | impl Error for DeviceNameError {} 84 | 85 | impl From for DeviceNameError { 86 | fn from(err: BackendSpecificError) -> Self { 87 | Self::BackendSpecific { err } 88 | } 89 | } 90 | 91 | /// Error that can happen when enumerating the list of supported formats. 92 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 93 | pub enum SupportedStreamConfigsError { 94 | /// The device no longer exists. This can happen if the device is disconnected while the 95 | /// program is running. 96 | DeviceNotAvailable, 97 | /// We called something the C-Layer did not understand 98 | InvalidArgument, 99 | /// See the [`BackendSpecificError`] docs for more information about this error variant. 100 | BackendSpecific { err: BackendSpecificError }, 101 | } 102 | 103 | impl Display for SupportedStreamConfigsError { 104 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 105 | match self { 106 | Self::BackendSpecific { err } => err.fmt(f), 107 | Self::DeviceNotAvailable => f.write_str("The requested device is no longer available. For example, it has been unplugged."), 108 | 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.") 109 | } 110 | } 111 | } 112 | 113 | impl Error for SupportedStreamConfigsError {} 114 | 115 | impl From for SupportedStreamConfigsError { 116 | fn from(err: BackendSpecificError) -> Self { 117 | Self::BackendSpecific { err } 118 | } 119 | } 120 | 121 | /// May occur when attempting to request the default input or output stream format from a [`Device`](crate::Device). 122 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 123 | pub enum DefaultStreamConfigError { 124 | /// The device no longer exists. This can happen if the device is disconnected while the 125 | /// program is running. 126 | DeviceNotAvailable, 127 | /// Returned if e.g. the default input format was requested on an output-only audio device. 128 | StreamTypeNotSupported, 129 | /// See the [`BackendSpecificError`] docs for more information about this error variant. 130 | BackendSpecific { err: BackendSpecificError }, 131 | } 132 | 133 | impl Display for DefaultStreamConfigError { 134 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 135 | match self { 136 | Self::BackendSpecific { err } => err.fmt(f), 137 | Self::DeviceNotAvailable => f.write_str( 138 | "The requested device is no longer available. For example, it has been unplugged.", 139 | ), 140 | Self::StreamTypeNotSupported => { 141 | f.write_str("The requested stream type is not supported by the device.") 142 | } 143 | } 144 | } 145 | } 146 | 147 | impl Error for DefaultStreamConfigError {} 148 | 149 | impl From for DefaultStreamConfigError { 150 | fn from(err: BackendSpecificError) -> Self { 151 | Self::BackendSpecific { err } 152 | } 153 | } 154 | /// Error that can happen when creating a [`Stream`](crate::Stream). 155 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 156 | pub enum BuildStreamError { 157 | /// The device no longer exists. This can happen if the device is disconnected while the 158 | /// program is running. 159 | DeviceNotAvailable, 160 | /// The specified stream configuration is not supported. 161 | StreamConfigNotSupported, 162 | /// We called something the C-Layer did not understand 163 | /// 164 | /// On ALSA device functions called with a feature they do not support will yield this. E.g. 165 | /// Trying to use capture capabilities on an output only format yields this. 166 | InvalidArgument, 167 | /// Occurs if adding a new Stream ID would cause an integer overflow. 168 | StreamIdOverflow, 169 | /// See the [`BackendSpecificError`] docs for more information about this error variant. 170 | BackendSpecific { err: BackendSpecificError }, 171 | } 172 | 173 | impl Display for BuildStreamError { 174 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 175 | match self { 176 | Self::BackendSpecific { err } => err.fmt(f), 177 | Self::DeviceNotAvailable => f.write_str( 178 | "The requested device is no longer available. For example, it has been unplugged.", 179 | ), 180 | Self::StreamConfigNotSupported => { 181 | f.write_str("The requested stream configuration is not supported by the device.") 182 | } 183 | Self::InvalidArgument => f.write_str( 184 | "The requested device does not support this capability (invalid argument)", 185 | ), 186 | Self::StreamIdOverflow => f.write_str("Adding a new stream ID would cause an overflow"), 187 | } 188 | } 189 | } 190 | 191 | impl Error for BuildStreamError {} 192 | 193 | impl From for BuildStreamError { 194 | fn from(err: BackendSpecificError) -> Self { 195 | Self::BackendSpecific { err } 196 | } 197 | } 198 | 199 | /// Errors that might occur when calling [`Stream::play()`](crate::traits::StreamTrait::play). 200 | /// 201 | /// As of writing this, only macOS may immediately return an error while calling this method. This 202 | /// is because both the alsa and wasapi backends only enqueue these commands and do not process 203 | /// them immediately. 204 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 205 | pub enum PlayStreamError { 206 | /// The device associated with the stream is no longer available. 207 | DeviceNotAvailable, 208 | /// See the [`BackendSpecificError`] docs for more information about this error variant. 209 | BackendSpecific { err: BackendSpecificError }, 210 | } 211 | 212 | impl Display for PlayStreamError { 213 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 214 | match self { 215 | Self::BackendSpecific { err } => err.fmt(f), 216 | Self::DeviceNotAvailable => { 217 | f.write_str("the device associated with the stream is no longer available") 218 | } 219 | } 220 | } 221 | } 222 | 223 | impl Error for PlayStreamError {} 224 | 225 | impl From for PlayStreamError { 226 | fn from(err: BackendSpecificError) -> Self { 227 | Self::BackendSpecific { err } 228 | } 229 | } 230 | 231 | /// Errors that might occur when calling [`Stream::pause()`](crate::traits::StreamTrait::pause). 232 | /// 233 | /// As of writing this, only macOS may immediately return an error while calling this method. This 234 | /// is because both the alsa and wasapi backends only enqueue these commands and do not process 235 | /// them immediately. 236 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 237 | pub enum PauseStreamError { 238 | /// The device associated with the stream is no longer available. 239 | DeviceNotAvailable, 240 | /// See the [`BackendSpecificError`] docs for more information about this error variant. 241 | BackendSpecific { err: BackendSpecificError }, 242 | } 243 | 244 | impl Display for PauseStreamError { 245 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 246 | match self { 247 | Self::BackendSpecific { err } => err.fmt(f), 248 | Self::DeviceNotAvailable => { 249 | f.write_str("the device associated with the stream is no longer available") 250 | } 251 | } 252 | } 253 | } 254 | 255 | impl Error for PauseStreamError {} 256 | 257 | impl From for PauseStreamError { 258 | fn from(err: BackendSpecificError) -> Self { 259 | Self::BackendSpecific { err } 260 | } 261 | } 262 | 263 | /// Errors that might occur while a stream is running. 264 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 265 | pub enum StreamError { 266 | /// The device no longer exists. This can happen if the device is disconnected while the 267 | /// program is running. 268 | DeviceNotAvailable, 269 | /// See the [`BackendSpecificError`] docs for more information about this error variant. 270 | BackendSpecific { err: BackendSpecificError }, 271 | } 272 | 273 | impl Display for StreamError { 274 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 275 | match self { 276 | Self::BackendSpecific { err } => err.fmt(f), 277 | Self::DeviceNotAvailable => f.write_str( 278 | "The requested device is no longer available. For example, it has been unplugged.", 279 | ), 280 | } 281 | } 282 | } 283 | 284 | impl Error for StreamError {} 285 | 286 | impl From for StreamError { 287 | fn from(err: BackendSpecificError) -> Self { 288 | Self::BackendSpecific { err } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /src/host/aaudio/android_media.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | extern crate jni; 4 | 5 | use self::jni::Executor; 6 | use self::jni::{errors::Result as JResult, JNIEnv, JavaVM}; 7 | 8 | // constants from android.media.AudioFormat 9 | pub const ENCODING_PCM_16BIT: i32 = 2; 10 | pub const ENCODING_PCM_FLOAT: i32 = 4; 11 | pub const CHANNEL_OUT_MONO: i32 = 4; 12 | pub const CHANNEL_OUT_STEREO: i32 = 12; 13 | 14 | fn with_attached(closure: F) -> JResult 15 | where 16 | F: FnOnce(&mut JNIEnv) -> JResult, 17 | { 18 | let android_context = ndk_context::android_context(); 19 | let vm = Arc::new(unsafe { JavaVM::from_raw(android_context.vm().cast())? }); 20 | Executor::new(vm).with_attached(|env| closure(env)) 21 | } 22 | 23 | fn get_min_buffer_size( 24 | class: &'static str, 25 | sample_rate: i32, 26 | channel_mask: i32, 27 | format: i32, 28 | ) -> i32 { 29 | // Unwrapping everything because these operations are not expected to fail 30 | // or throw exceptions. Android returns negative values for invalid parameters, 31 | // which is what we expect. 32 | with_attached(|env| { 33 | let class = env.find_class(class).unwrap(); 34 | env.call_static_method( 35 | class, 36 | "getMinBufferSize", 37 | "(III)I", 38 | &[sample_rate.into(), channel_mask.into(), format.into()], 39 | ) 40 | .unwrap() 41 | .i() 42 | }) 43 | .unwrap() 44 | } 45 | 46 | pub fn get_audio_track_min_buffer_size(sample_rate: i32, channel_mask: i32, format: i32) -> i32 { 47 | get_min_buffer_size( 48 | "android/media/AudioTrack", 49 | sample_rate, 50 | channel_mask, 51 | format, 52 | ) 53 | } 54 | 55 | pub fn get_audio_record_min_buffer_size(sample_rate: i32, channel_mask: i32, format: i32) -> i32 { 56 | get_min_buffer_size( 57 | "android/media/AudioRecord", 58 | sample_rate, 59 | channel_mask, 60 | format, 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/host/aaudio/convert.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | use std::time::Duration; 3 | 4 | extern crate ndk; 5 | 6 | use crate::{ 7 | BackendSpecificError, BuildStreamError, PauseStreamError, PlayStreamError, StreamError, 8 | StreamInstant, 9 | }; 10 | 11 | pub fn to_stream_instant(duration: Duration) -> StreamInstant { 12 | StreamInstant::new( 13 | duration.as_secs().try_into().unwrap(), 14 | duration.subsec_nanos(), 15 | ) 16 | } 17 | 18 | pub fn stream_instant(stream: &ndk::audio::AudioStream) -> StreamInstant { 19 | let ts = stream 20 | .timestamp(ndk::audio::Clockid::Monotonic) 21 | .unwrap_or(ndk::audio::Timestamp { 22 | frame_position: 0, 23 | time_nanoseconds: 0, 24 | }); 25 | to_stream_instant(Duration::from_nanos(ts.time_nanoseconds as u64)) 26 | } 27 | 28 | impl From for StreamError { 29 | fn from(error: ndk::audio::AudioError) -> Self { 30 | use self::ndk::audio::AudioError::*; 31 | match error { 32 | Disconnected | Unavailable => Self::DeviceNotAvailable, 33 | e => (BackendSpecificError { 34 | description: e.to_string(), 35 | }) 36 | .into(), 37 | } 38 | } 39 | } 40 | 41 | impl From for PlayStreamError { 42 | fn from(error: ndk::audio::AudioError) -> Self { 43 | use self::ndk::audio::AudioError::*; 44 | match error { 45 | Disconnected | Unavailable => Self::DeviceNotAvailable, 46 | e => (BackendSpecificError { 47 | description: e.to_string(), 48 | }) 49 | .into(), 50 | } 51 | } 52 | } 53 | 54 | impl From for PauseStreamError { 55 | fn from(error: ndk::audio::AudioError) -> Self { 56 | use self::ndk::audio::AudioError::*; 57 | match error { 58 | Disconnected | Unavailable => Self::DeviceNotAvailable, 59 | e => (BackendSpecificError { 60 | description: e.to_string(), 61 | }) 62 | .into(), 63 | } 64 | } 65 | } 66 | 67 | impl From for BuildStreamError { 68 | fn from(error: ndk::audio::AudioError) -> Self { 69 | use self::ndk::audio::AudioError::*; 70 | match error { 71 | Disconnected | Unavailable => Self::DeviceNotAvailable, 72 | NoFreeHandles => Self::StreamIdOverflow, 73 | InvalidFormat | InvalidRate => Self::StreamConfigNotSupported, 74 | IllegalArgument => Self::InvalidArgument, 75 | e => (BackendSpecificError { 76 | description: e.to_string(), 77 | }) 78 | .into(), 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/host/aaudio/java_interface.rs: -------------------------------------------------------------------------------- 1 | mod audio_features; 2 | mod definitions; 3 | mod devices_info; 4 | mod utils; 5 | 6 | pub use self::audio_features::*; 7 | pub use self::definitions::*; 8 | -------------------------------------------------------------------------------- /src/host/aaudio/java_interface/audio_features.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | utils::{ 3 | get_context, get_package_manager, has_system_feature, with_attached, JNIEnv, JObject, 4 | JResult, 5 | }, 6 | PackageManager, 7 | }; 8 | 9 | /** 10 | * The Android audio features 11 | */ 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 13 | pub enum AudioFeature { 14 | LowLatency, 15 | Output, 16 | Pro, 17 | Microphone, 18 | Midi, 19 | } 20 | 21 | impl From for &'static str { 22 | fn from(feature: AudioFeature) -> Self { 23 | use AudioFeature::*; 24 | match feature { 25 | LowLatency => PackageManager::FEATURE_AUDIO_LOW_LATENCY, 26 | Output => PackageManager::FEATURE_AUDIO_OUTPUT, 27 | Pro => PackageManager::FEATURE_AUDIO_PRO, 28 | Microphone => PackageManager::FEATURE_MICROPHONE, 29 | Midi => PackageManager::FEATURE_MIDI, 30 | } 31 | } 32 | } 33 | 34 | impl AudioFeature { 35 | /** 36 | * Check availability of an audio feature using Android Java API 37 | */ 38 | pub fn has(&self) -> Result { 39 | let context = get_context(); 40 | 41 | with_attached(context, |env, activity| { 42 | try_check_system_feature(env, &activity, (*self).into()) 43 | }) 44 | .map_err(|error| error.to_string()) 45 | } 46 | } 47 | 48 | fn try_check_system_feature<'j>( 49 | env: &mut JNIEnv<'j>, 50 | activity: &JObject<'j>, 51 | feature: &str, 52 | ) -> JResult { 53 | let package_manager = get_package_manager(env, activity)?; 54 | 55 | has_system_feature(env, &package_manager, feature) 56 | } 57 | -------------------------------------------------------------------------------- /src/host/aaudio/java_interface/definitions.rs: -------------------------------------------------------------------------------- 1 | use num_derive::FromPrimitive; 2 | 3 | use crate::SampleFormat; 4 | 5 | pub(crate) struct Context; 6 | 7 | impl Context { 8 | pub const AUDIO_SERVICE: &'static str = "audio"; 9 | } 10 | 11 | pub(crate) struct PackageManager; 12 | 13 | impl PackageManager { 14 | pub const FEATURE_AUDIO_LOW_LATENCY: &'static str = "android.hardware.audio.low_latency"; 15 | pub const FEATURE_AUDIO_OUTPUT: &'static str = "android.hardware.audio.output"; 16 | pub const FEATURE_AUDIO_PRO: &'static str = "android.hardware.audio.pro"; 17 | pub const FEATURE_MICROPHONE: &'static str = "android.hardware.microphone"; 18 | pub const FEATURE_MIDI: &'static str = "android.software.midi"; 19 | } 20 | 21 | pub(crate) struct AudioManager; 22 | 23 | impl AudioManager { 24 | pub const PROPERTY_OUTPUT_SAMPLE_RATE: &'static str = 25 | "android.media.property.OUTPUT_SAMPLE_RATE"; 26 | pub const PROPERTY_OUTPUT_FRAMES_PER_BUFFER: &'static str = 27 | "android.media.property.OUTPUT_FRAMES_PER_BUFFER"; 28 | 29 | pub const GET_DEVICES_INPUTS: i32 = 1 << 0; 30 | pub const GET_DEVICES_OUTPUTS: i32 = 1 << 1; 31 | pub const GET_DEVICES_ALL: i32 = Self::GET_DEVICES_INPUTS | Self::GET_DEVICES_OUTPUTS; 32 | } 33 | 34 | /** 35 | * The Android audio device info 36 | */ 37 | #[derive(Debug, Clone)] 38 | pub struct AudioDeviceInfo { 39 | /** 40 | * Device identifier 41 | */ 42 | pub id: i32, 43 | 44 | /** 45 | * The type of device 46 | */ 47 | pub device_type: AudioDeviceType, 48 | 49 | /** 50 | * The device can be used for playback and/or capture 51 | */ 52 | pub direction: AudioDeviceDirection, 53 | 54 | /** 55 | * Device address 56 | */ 57 | pub address: String, 58 | 59 | /** 60 | * Device product name 61 | */ 62 | pub product_name: String, 63 | 64 | /** 65 | * Available channel configurations 66 | */ 67 | pub channel_counts: Vec, 68 | 69 | /** 70 | * Supported sample rates 71 | */ 72 | pub sample_rates: Vec, 73 | 74 | /** 75 | * Supported audio formats 76 | */ 77 | pub formats: Vec, 78 | } 79 | 80 | /** 81 | * The type of audio device 82 | */ 83 | #[derive(Debug, Clone, Copy, FromPrimitive)] 84 | #[non_exhaustive] 85 | #[repr(i32)] 86 | pub enum AudioDeviceType { 87 | Unknown = 0, 88 | AuxLine = 19, 89 | BleBroadcast = 30, 90 | BleHeadset = 26, 91 | BleSpeaker = 27, 92 | BluetoothA2DP = 8, 93 | BluetoothSCO = 7, 94 | BuiltinEarpiece = 1, 95 | BuiltinMic = 15, 96 | BuiltinSpeaker = 2, 97 | BuiltinSpeakerSafe = 24, 98 | Bus = 21, 99 | Dock = 13, 100 | Fm = 14, 101 | FmTuner = 16, 102 | Hdmi = 9, 103 | HdmiArc = 10, 104 | HdmiEarc = 29, 105 | HearingAid = 23, 106 | Ip = 20, 107 | LineAnalog = 5, 108 | LineDigital = 6, 109 | RemoteSubmix = 25, 110 | Telephony = 18, 111 | TvTuner = 17, 112 | UsbAccessory = 12, 113 | UsbDevice = 11, 114 | UsbHeadset = 22, 115 | WiredHeadphones = 4, 116 | WiredHeadset = 3, 117 | Unsupported = -1, 118 | } 119 | 120 | /** 121 | * The direction of audio device 122 | */ 123 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 124 | #[repr(i32)] 125 | pub enum AudioDeviceDirection { 126 | Dumb = 0, 127 | Input = AudioManager::GET_DEVICES_INPUTS, 128 | Output = AudioManager::GET_DEVICES_OUTPUTS, 129 | InputOutput = AudioManager::GET_DEVICES_ALL, 130 | } 131 | 132 | impl AudioDeviceDirection { 133 | pub fn new(is_input: bool, is_output: bool) -> Self { 134 | use self::AudioDeviceDirection::*; 135 | match (is_input, is_output) { 136 | (true, true) => InputOutput, 137 | (false, true) => Output, 138 | (true, false) => Input, 139 | _ => Dumb, 140 | } 141 | } 142 | 143 | pub fn is_input(&self) -> bool { 144 | 0 < *self as i32 & AudioDeviceDirection::Input as i32 145 | } 146 | 147 | pub fn is_output(&self) -> bool { 148 | 0 < *self as i32 & AudioDeviceDirection::Output as i32 149 | } 150 | } 151 | 152 | impl SampleFormat { 153 | pub(crate) const ENCODING_PCM_16BIT: i32 = 2; 154 | //pub(crate) const ENCODING_PCM_8BIT: i32 = 3; 155 | pub(crate) const ENCODING_PCM_FLOAT: i32 = 4; 156 | 157 | pub(crate) fn from_encoding(encoding: i32) -> Option { 158 | match encoding { 159 | SampleFormat::ENCODING_PCM_16BIT => Some(SampleFormat::I16), 160 | SampleFormat::ENCODING_PCM_FLOAT => Some(SampleFormat::F32), 161 | _ => None, 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/host/aaudio/java_interface/devices_info.rs: -------------------------------------------------------------------------------- 1 | use num_traits::FromPrimitive; 2 | 3 | use crate::SampleFormat; 4 | 5 | use super::{ 6 | utils::{ 7 | call_method_no_args_ret_bool, call_method_no_args_ret_char_sequence, 8 | call_method_no_args_ret_int, call_method_no_args_ret_int_array, 9 | call_method_no_args_ret_string, get_context, get_devices, get_system_service, 10 | with_attached, JNIEnv, JObject, JResult, 11 | }, 12 | AudioDeviceDirection, AudioDeviceInfo, AudioDeviceType, Context, 13 | }; 14 | 15 | impl AudioDeviceInfo { 16 | /** 17 | * Request audio devices using Android Java API 18 | */ 19 | pub fn request(direction: AudioDeviceDirection) -> Result, String> { 20 | let context = get_context(); 21 | 22 | with_attached(context, |env, context| { 23 | let sdk_version = env 24 | .get_static_field("android/os/Build$VERSION", "SDK_INT", "I")? 25 | .i()?; 26 | 27 | if sdk_version >= 23 { 28 | try_request_devices_info(env, &context, direction) 29 | } else { 30 | Err(jni::errors::Error::MethodNotFound { 31 | name: "".into(), 32 | sig: "".into(), 33 | }) 34 | } 35 | }) 36 | .map_err(|error| error.to_string()) 37 | } 38 | } 39 | 40 | fn try_request_devices_info<'j>( 41 | env: &mut JNIEnv<'j>, 42 | context: &JObject<'j>, 43 | direction: AudioDeviceDirection, 44 | ) -> JResult> { 45 | let audio_manager = get_system_service(env, context, Context::AUDIO_SERVICE)?; 46 | 47 | let devices = get_devices(env, &audio_manager, direction as i32)?; 48 | 49 | let length = env.get_array_length(&devices)?; 50 | 51 | (0..length) 52 | .map(|index| { 53 | let device = env.get_object_array_element(&devices, index)?; 54 | let id = call_method_no_args_ret_int(env, &device, "getId")?; 55 | let address = call_method_no_args_ret_string(env, &device, "getAddress")?; 56 | let address = String::from(env.get_string(&address)?); 57 | let product_name = 58 | call_method_no_args_ret_char_sequence(env, &device, "getProductName")?; 59 | let product_name = String::from(env.get_string(&product_name)?); 60 | let device_type = 61 | FromPrimitive::from_i32(call_method_no_args_ret_int(env, &device, "getType")?) 62 | .unwrap_or(AudioDeviceType::Unsupported); 63 | let direction = AudioDeviceDirection::new( 64 | call_method_no_args_ret_bool(env, &device, "isSource")?, 65 | call_method_no_args_ret_bool(env, &device, "isSink")?, 66 | ); 67 | let channel_counts = 68 | call_method_no_args_ret_int_array(env, &device, "getChannelCounts")?; 69 | let sample_rates = call_method_no_args_ret_int_array(env, &device, "getSampleRates")?; 70 | let formats = call_method_no_args_ret_int_array(env, &device, "getEncodings")? 71 | .into_iter() 72 | .filter_map(SampleFormat::from_encoding) 73 | .collect::>(); 74 | 75 | Ok(AudioDeviceInfo { 76 | id, 77 | address, 78 | product_name, 79 | device_type, 80 | direction, 81 | channel_counts, 82 | sample_rates, 83 | formats, 84 | }) 85 | }) 86 | .collect::, _>>() 87 | } 88 | -------------------------------------------------------------------------------- /src/host/aaudio/java_interface/utils.rs: -------------------------------------------------------------------------------- 1 | use jni::sys::jobject; 2 | use ndk_context::AndroidContext; 3 | use std::sync::Arc; 4 | 5 | pub use jni::Executor; 6 | 7 | pub use jni::{ 8 | errors::Result as JResult, 9 | objects::{JIntArray, JObject, JObjectArray, JString}, 10 | JNIEnv, JavaVM, 11 | }; 12 | 13 | pub fn get_context() -> AndroidContext { 14 | ndk_context::android_context() 15 | } 16 | 17 | pub fn with_attached(context: AndroidContext, closure: F) -> JResult 18 | where 19 | for<'j> F: FnOnce(&mut JNIEnv<'j>, JObject<'j>) -> JResult, 20 | { 21 | let vm = Arc::new(unsafe { JavaVM::from_raw(context.vm().cast())? }); 22 | let context = context.context(); 23 | let context = unsafe { JObject::from_raw(context as jobject) }; 24 | Executor::new(vm).with_attached(|env| closure(env, context)) 25 | } 26 | 27 | pub fn call_method_no_args_ret_int_array<'j>( 28 | env: &mut JNIEnv<'j>, 29 | subject: &JObject<'j>, 30 | method: &str, 31 | ) -> JResult> { 32 | let array: JIntArray = env.call_method(subject, method, "()[I", &[])?.l()?.into(); 33 | 34 | let length = env.get_array_length(&array)?; 35 | let mut values = Vec::with_capacity(length as usize); 36 | 37 | env.get_int_array_region(array, 0, values.as_mut())?; 38 | 39 | Ok(values) 40 | } 41 | 42 | pub fn call_method_no_args_ret_int<'j>( 43 | env: &mut JNIEnv<'j>, 44 | subject: &JObject<'j>, 45 | method: &str, 46 | ) -> JResult { 47 | env.call_method(subject, method, "()I", &[])?.i() 48 | } 49 | 50 | pub fn call_method_no_args_ret_bool<'j>( 51 | env: &mut JNIEnv<'j>, 52 | subject: &JObject<'j>, 53 | method: &str, 54 | ) -> JResult { 55 | env.call_method(subject, method, "()Z", &[])?.z() 56 | } 57 | 58 | pub fn call_method_no_args_ret_string<'j>( 59 | env: &mut JNIEnv<'j>, 60 | subject: &JObject<'j>, 61 | method: &str, 62 | ) -> JResult> { 63 | Ok(env 64 | .call_method(subject, method, "()Ljava/lang/String;", &[])? 65 | .l()? 66 | .into()) 67 | } 68 | 69 | pub fn call_method_no_args_ret_char_sequence<'j>( 70 | env: &mut JNIEnv<'j>, 71 | subject: &JObject<'j>, 72 | method: &str, 73 | ) -> JResult> { 74 | let cseq = env 75 | .call_method(subject, method, "()Ljava/lang/CharSequence;", &[])? 76 | .l()?; 77 | 78 | Ok(env 79 | .call_method(&cseq, "toString", "()Ljava/lang/String;", &[])? 80 | .l()? 81 | .into()) 82 | } 83 | 84 | pub fn call_method_string_arg_ret_bool<'j>( 85 | env: &mut JNIEnv<'j>, 86 | subject: &JObject<'j>, 87 | name: &str, 88 | arg: impl AsRef, 89 | ) -> JResult { 90 | env.call_method( 91 | subject, 92 | name, 93 | "(Ljava/lang/String;)Z", 94 | &[(&env.new_string(arg)?).into()], 95 | )? 96 | .z() 97 | } 98 | 99 | pub fn call_method_string_arg_ret_string<'j>( 100 | env: &mut JNIEnv<'j>, 101 | subject: &JObject<'j>, 102 | name: &str, 103 | arg: impl AsRef, 104 | ) -> JResult> { 105 | Ok(env 106 | .call_method( 107 | subject, 108 | name, 109 | "(Ljava/lang/String;)Ljava/lang/String;", 110 | &[(&env.new_string(arg)?).into()], 111 | )? 112 | .l()? 113 | .into()) 114 | } 115 | 116 | pub fn call_method_string_arg_ret_object<'j>( 117 | env: &mut JNIEnv<'j>, 118 | subject: &JObject<'j>, 119 | method: &str, 120 | arg: &str, 121 | ) -> JResult> { 122 | env.call_method( 123 | subject, 124 | method, 125 | "(Ljava/lang/String;)Ljava/lang/Object;", 126 | &[(&env.new_string(arg)?).into()], 127 | )? 128 | .l() 129 | } 130 | 131 | pub fn get_package_manager<'j>( 132 | env: &mut JNIEnv<'j>, 133 | subject: &JObject<'j>, 134 | ) -> JResult> { 135 | env.call_method( 136 | subject, 137 | "getPackageManager", 138 | "()Landroid/content/pm/PackageManager;", 139 | &[], 140 | )? 141 | .l() 142 | } 143 | 144 | pub fn has_system_feature<'j>( 145 | env: &mut JNIEnv<'j>, 146 | subject: &JObject<'j>, 147 | name: &str, 148 | ) -> JResult { 149 | call_method_string_arg_ret_bool(env, subject, "hasSystemFeature", name) 150 | } 151 | 152 | pub fn get_system_service<'j>( 153 | env: &mut JNIEnv<'j>, 154 | subject: &JObject<'j>, 155 | name: &str, 156 | ) -> JResult> { 157 | call_method_string_arg_ret_object(env, subject, "getSystemService", name) 158 | } 159 | 160 | pub fn get_property<'j>( 161 | env: &mut JNIEnv<'j>, 162 | subject: &JObject<'j>, 163 | name: &str, 164 | ) -> JResult> { 165 | call_method_string_arg_ret_string(env, subject, "getProperty", name) 166 | } 167 | 168 | pub fn get_devices<'j>( 169 | env: &mut JNIEnv<'j>, 170 | subject: &JObject<'j>, 171 | flags: i32, 172 | ) -> JResult> { 173 | env.call_method( 174 | subject, 175 | "getDevices", 176 | "(I)[Landroid/media/AudioDeviceInfo;", 177 | &[flags.into()], 178 | )? 179 | .l() 180 | .map(From::from) 181 | } 182 | -------------------------------------------------------------------------------- /src/host/alsa/enumerate.rs: -------------------------------------------------------------------------------- 1 | use super::alsa; 2 | use super::{Device, DeviceHandles}; 3 | use crate::{BackendSpecificError, DevicesError}; 4 | use std::sync::{Arc, Mutex}; 5 | 6 | /// ALSA's implementation for `Devices`. 7 | pub struct Devices { 8 | builtin_pos: usize, 9 | card_iter: alsa::card::Iter, 10 | } 11 | 12 | impl Devices { 13 | pub fn new() -> Result { 14 | Ok(Devices { 15 | builtin_pos: 0, 16 | card_iter: alsa::card::Iter::new(), 17 | }) 18 | } 19 | } 20 | 21 | unsafe impl Send for Devices {} 22 | unsafe impl Sync for Devices {} 23 | 24 | const BUILTINS: [&str; 5] = ["default", "pipewire", "pulse", "jack", "oss"]; 25 | 26 | impl Iterator for Devices { 27 | type Item = Device; 28 | 29 | fn next(&mut self) -> Option { 30 | while self.builtin_pos < BUILTINS.len() { 31 | let pos = self.builtin_pos; 32 | self.builtin_pos += 1; 33 | let name = BUILTINS[pos]; 34 | 35 | if let Ok(handles) = DeviceHandles::open(name) { 36 | return Some(Device { 37 | name: name.to_string(), 38 | pcm_id: name.to_string(), 39 | handles: Arc::new(Mutex::new(handles)), 40 | }); 41 | } 42 | } 43 | 44 | loop { 45 | let res = self.card_iter.next()?; 46 | let Ok(card) = res else { continue }; 47 | 48 | let ctl_id = format!("hw:{}", card.get_index()); 49 | let Ok(ctl) = alsa::Ctl::new(&ctl_id, false) else { 50 | continue; 51 | }; 52 | let Ok(cardinfo) = ctl.card_info() else { 53 | continue; 54 | }; 55 | let Ok(card_name) = cardinfo.get_name() else { 56 | continue; 57 | }; 58 | 59 | // Using plughw adds the ALSA plug layer, which can do sample type conversion, 60 | // sample rate convertion, ... 61 | // It is convenient, but at the same time not suitable for pro-audio as it hides 62 | // the actual device capabilities and perform audio manipulation under your feet, 63 | // for example sample rate conversion, sample format conversion, adds dummy channels, 64 | // ... 65 | // For now, many hardware only support 24bit / 3 bytes, which isn't yet supported by 66 | // cpal. So we have to enable plughw (unfortunately) for maximum compatibility. 67 | const USE_PLUGHW: bool = true; 68 | let pcm_id = if USE_PLUGHW { 69 | format!("plughw:{}", card.get_index()) 70 | } else { 71 | ctl_id 72 | }; 73 | if let Ok(handles) = DeviceHandles::open(&pcm_id) { 74 | return Some(Device { 75 | name: card_name.to_string(), 76 | pcm_id: pcm_id.to_string(), 77 | handles: Arc::new(Mutex::new(handles)), 78 | }); 79 | } 80 | } 81 | } 82 | } 83 | 84 | #[inline] 85 | pub fn default_input_device() -> Option { 86 | Some(Device { 87 | name: "default".to_owned(), 88 | pcm_id: "default".to_owned(), 89 | handles: Arc::new(Mutex::new(Default::default())), 90 | }) 91 | } 92 | 93 | #[inline] 94 | pub fn default_output_device() -> Option { 95 | Some(Device { 96 | name: "default".to_owned(), 97 | pcm_id: "default".to_owned(), 98 | handles: Arc::new(Mutex::new(Default::default())), 99 | }) 100 | } 101 | 102 | impl From for DevicesError { 103 | fn from(err: alsa::Error) -> Self { 104 | let err: BackendSpecificError = err.into(); 105 | err.into() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/host/asio/device.rs: -------------------------------------------------------------------------------- 1 | pub type SupportedInputConfigs = std::vec::IntoIter; 2 | pub type SupportedOutputConfigs = std::vec::IntoIter; 3 | 4 | use super::sys; 5 | use crate::BackendSpecificError; 6 | use crate::DefaultStreamConfigError; 7 | use crate::DeviceNameError; 8 | use crate::DevicesError; 9 | use crate::SampleFormat; 10 | use crate::SampleRate; 11 | use crate::SupportedBufferSize; 12 | use crate::SupportedStreamConfig; 13 | use crate::SupportedStreamConfigRange; 14 | use crate::SupportedStreamConfigsError; 15 | use std::hash::{Hash, Hasher}; 16 | use std::sync::atomic::AtomicI32; 17 | use std::sync::{Arc, Mutex}; 18 | 19 | /// A ASIO Device 20 | #[derive(Clone)] 21 | pub struct Device { 22 | /// The driver represented by this device. 23 | pub driver: Arc, 24 | 25 | // Input and/or Output stream. 26 | // A driver can only have one of each. 27 | // They need to be created at the same time. 28 | pub asio_streams: Arc>, 29 | pub current_buffer_index: Arc, 30 | } 31 | 32 | /// All available devices. 33 | pub struct Devices { 34 | asio: Arc, 35 | drivers: std::vec::IntoIter, 36 | } 37 | 38 | impl PartialEq for Device { 39 | fn eq(&self, other: &Self) -> bool { 40 | self.driver.name() == other.driver.name() 41 | } 42 | } 43 | 44 | impl Eq for Device {} 45 | 46 | impl Hash for Device { 47 | fn hash(&self, state: &mut H) { 48 | self.driver.name().hash(state); 49 | } 50 | } 51 | 52 | impl Device { 53 | pub fn name(&self) -> Result { 54 | Ok(self.driver.name().to_string()) 55 | } 56 | 57 | /// Gets the supported input configs. 58 | /// TODO currently only supports the default. 59 | /// Need to find all possible configs. 60 | pub fn supported_input_configs( 61 | &self, 62 | ) -> Result { 63 | // Retrieve the default config for the total supported channels and supported sample 64 | // format. 65 | let f = match self.default_input_config() { 66 | Err(_) => return Err(SupportedStreamConfigsError::DeviceNotAvailable), 67 | Ok(f) => f, 68 | }; 69 | 70 | // Collect a config for every combination of supported sample rate and number of channels. 71 | let mut supported_configs = vec![]; 72 | for &rate in crate::COMMON_SAMPLE_RATES { 73 | if !self 74 | .driver 75 | .can_sample_rate(rate.0.into()) 76 | .ok() 77 | .unwrap_or(false) 78 | { 79 | continue; 80 | } 81 | for channels in 1..f.channels + 1 { 82 | supported_configs.push(SupportedStreamConfigRange { 83 | channels, 84 | min_sample_rate: rate, 85 | max_sample_rate: rate, 86 | buffer_size: f.buffer_size, 87 | sample_format: f.sample_format, 88 | }) 89 | } 90 | } 91 | Ok(supported_configs.into_iter()) 92 | } 93 | 94 | /// Gets the supported output configs. 95 | /// TODO currently only supports the default. 96 | /// Need to find all possible configs. 97 | pub fn supported_output_configs( 98 | &self, 99 | ) -> Result { 100 | // Retrieve the default config for the total supported channels and supported sample 101 | // format. 102 | let f = match self.default_output_config() { 103 | Err(_) => return Err(SupportedStreamConfigsError::DeviceNotAvailable), 104 | Ok(f) => f, 105 | }; 106 | 107 | // Collect a config for every combination of supported sample rate and number of channels. 108 | let mut supported_configs = vec![]; 109 | for &rate in crate::COMMON_SAMPLE_RATES { 110 | if !self 111 | .driver 112 | .can_sample_rate(rate.0.into()) 113 | .ok() 114 | .unwrap_or(false) 115 | { 116 | continue; 117 | } 118 | for channels in 1..f.channels + 1 { 119 | supported_configs.push(SupportedStreamConfigRange { 120 | channels, 121 | min_sample_rate: rate, 122 | max_sample_rate: rate, 123 | buffer_size: f.buffer_size, 124 | sample_format: f.sample_format, 125 | }) 126 | } 127 | } 128 | Ok(supported_configs.into_iter()) 129 | } 130 | 131 | /// Returns the default input config 132 | pub fn default_input_config(&self) -> Result { 133 | let channels = self.driver.channels().map_err(default_config_err)?.ins as u16; 134 | let sample_rate = SampleRate(self.driver.sample_rate().map_err(default_config_err)? as _); 135 | let (min, max) = self.driver.buffersize_range().map_err(default_config_err)?; 136 | let buffer_size = SupportedBufferSize::Range { 137 | min: min as u32, 138 | max: max as u32, 139 | }; 140 | // Map th ASIO sample type to a CPAL sample type 141 | let data_type = self.driver.input_data_type().map_err(default_config_err)?; 142 | let sample_format = convert_data_type(&data_type) 143 | .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?; 144 | Ok(SupportedStreamConfig { 145 | channels, 146 | sample_rate, 147 | buffer_size, 148 | sample_format, 149 | }) 150 | } 151 | 152 | /// Returns the default output config 153 | pub fn default_output_config(&self) -> Result { 154 | let channels = self.driver.channels().map_err(default_config_err)?.outs as u16; 155 | let sample_rate = SampleRate(self.driver.sample_rate().map_err(default_config_err)? as _); 156 | let (min, max) = self.driver.buffersize_range().map_err(default_config_err)?; 157 | let buffer_size = SupportedBufferSize::Range { 158 | min: min as u32, 159 | max: max as u32, 160 | }; 161 | let data_type = self.driver.output_data_type().map_err(default_config_err)?; 162 | let sample_format = convert_data_type(&data_type) 163 | .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?; 164 | Ok(SupportedStreamConfig { 165 | channels, 166 | sample_rate, 167 | buffer_size, 168 | sample_format, 169 | }) 170 | } 171 | } 172 | 173 | impl Devices { 174 | pub fn new(asio: Arc) -> Result { 175 | let drivers = asio.driver_names().into_iter(); 176 | Ok(Devices { asio, drivers }) 177 | } 178 | } 179 | 180 | impl Iterator for Devices { 181 | type Item = Device; 182 | 183 | /// Load drivers and return device 184 | fn next(&mut self) -> Option { 185 | loop { 186 | match self.drivers.next() { 187 | Some(name) => match self.asio.load_driver(&name) { 188 | Ok(driver) => { 189 | let driver = Arc::new(driver); 190 | let asio_streams = Arc::new(Mutex::new(sys::AsioStreams { 191 | input: None, 192 | output: None, 193 | })); 194 | return Some(Device { 195 | driver, 196 | asio_streams, 197 | current_buffer_index: Arc::new(AtomicI32::new(-1)), 198 | }); 199 | } 200 | Err(_) => continue, 201 | }, 202 | None => return None, 203 | } 204 | } 205 | } 206 | } 207 | 208 | pub(crate) fn convert_data_type(ty: &sys::AsioSampleType) -> Option { 209 | let fmt = match *ty { 210 | sys::AsioSampleType::ASIOSTInt16MSB => SampleFormat::I16, 211 | sys::AsioSampleType::ASIOSTInt16LSB => SampleFormat::I16, 212 | sys::AsioSampleType::ASIOSTInt24MSB => SampleFormat::I24, 213 | sys::AsioSampleType::ASIOSTInt24LSB => SampleFormat::I24, 214 | sys::AsioSampleType::ASIOSTInt32MSB => SampleFormat::I32, 215 | sys::AsioSampleType::ASIOSTInt32LSB => SampleFormat::I32, 216 | sys::AsioSampleType::ASIOSTFloat32MSB => SampleFormat::F32, 217 | sys::AsioSampleType::ASIOSTFloat32LSB => SampleFormat::F32, 218 | sys::AsioSampleType::ASIOSTFloat64MSB => SampleFormat::F64, 219 | sys::AsioSampleType::ASIOSTFloat64LSB => SampleFormat::F64, 220 | _ => return None, 221 | }; 222 | Some(fmt) 223 | } 224 | 225 | fn default_config_err(e: sys::AsioError) -> DefaultStreamConfigError { 226 | match e { 227 | sys::AsioError::NoDrivers | sys::AsioError::HardwareMalfunction => { 228 | DefaultStreamConfigError::DeviceNotAvailable 229 | } 230 | sys::AsioError::NoRate => DefaultStreamConfigError::StreamTypeNotSupported, 231 | err => { 232 | let description = format!("{}", err); 233 | BackendSpecificError { description }.into() 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/host/asio/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate asio_sys as sys; 2 | 3 | use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; 4 | use crate::{ 5 | BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, 6 | InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, 7 | StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigsError, 8 | }; 9 | 10 | pub use self::device::{Device, Devices, SupportedInputConfigs, SupportedOutputConfigs}; 11 | pub use self::stream::Stream; 12 | use std::sync::Arc; 13 | use std::time::Duration; 14 | 15 | mod device; 16 | mod stream; 17 | 18 | /// The host for ASIO. 19 | #[derive(Debug)] 20 | pub struct Host { 21 | asio: Arc, 22 | } 23 | 24 | impl Host { 25 | pub fn new() -> Result { 26 | let asio = Arc::new(sys::Asio::new()); 27 | let host = Host { asio }; 28 | Ok(host) 29 | } 30 | } 31 | 32 | impl HostTrait for Host { 33 | type Devices = Devices; 34 | type Device = Device; 35 | 36 | fn is_available() -> bool { 37 | true 38 | //unimplemented!("check how to do this using asio-sys") 39 | } 40 | 41 | fn devices(&self) -> Result { 42 | Devices::new(self.asio.clone()) 43 | } 44 | 45 | fn default_input_device(&self) -> Option { 46 | // ASIO has no concept of a default device, so just use the first. 47 | self.input_devices().ok().and_then(|mut ds| ds.next()) 48 | } 49 | 50 | fn default_output_device(&self) -> Option { 51 | // ASIO has no concept of a default device, so just use the first. 52 | self.output_devices().ok().and_then(|mut ds| ds.next()) 53 | } 54 | } 55 | 56 | impl DeviceTrait for Device { 57 | type SupportedInputConfigs = SupportedInputConfigs; 58 | type SupportedOutputConfigs = SupportedOutputConfigs; 59 | type Stream = Stream; 60 | 61 | fn name(&self) -> Result { 62 | Device::name(self) 63 | } 64 | 65 | fn supported_input_configs( 66 | &self, 67 | ) -> Result { 68 | Device::supported_input_configs(self) 69 | } 70 | 71 | fn supported_output_configs( 72 | &self, 73 | ) -> Result { 74 | Device::supported_output_configs(self) 75 | } 76 | 77 | fn default_input_config(&self) -> Result { 78 | Device::default_input_config(self) 79 | } 80 | 81 | fn default_output_config(&self) -> Result { 82 | Device::default_output_config(self) 83 | } 84 | 85 | fn build_input_stream_raw( 86 | &self, 87 | config: &StreamConfig, 88 | sample_format: SampleFormat, 89 | data_callback: D, 90 | error_callback: E, 91 | timeout: Option, 92 | ) -> Result 93 | where 94 | D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, 95 | E: FnMut(StreamError) + Send + 'static, 96 | { 97 | Device::build_input_stream_raw( 98 | self, 99 | config, 100 | sample_format, 101 | data_callback, 102 | error_callback, 103 | timeout, 104 | ) 105 | } 106 | 107 | fn build_output_stream_raw( 108 | &self, 109 | config: &StreamConfig, 110 | sample_format: SampleFormat, 111 | data_callback: D, 112 | error_callback: E, 113 | timeout: Option, 114 | ) -> Result 115 | where 116 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, 117 | E: FnMut(StreamError) + Send + 'static, 118 | { 119 | Device::build_output_stream_raw( 120 | self, 121 | config, 122 | sample_format, 123 | data_callback, 124 | error_callback, 125 | timeout, 126 | ) 127 | } 128 | } 129 | 130 | impl StreamTrait for Stream { 131 | fn play(&self) -> Result<(), PlayStreamError> { 132 | Stream::play(self) 133 | } 134 | 135 | fn pause(&self) -> Result<(), PauseStreamError> { 136 | Stream::pause(self) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/host/coreaudio/ios/enumerate.rs: -------------------------------------------------------------------------------- 1 | use std::vec::IntoIter as VecIntoIter; 2 | 3 | use crate::DevicesError; 4 | use crate::SupportedStreamConfigRange; 5 | 6 | use super::Device; 7 | 8 | pub type SupportedInputConfigs = ::std::vec::IntoIter; 9 | pub type SupportedOutputConfigs = ::std::vec::IntoIter; 10 | 11 | // TODO: Support enumerating earpiece vs headset vs speaker etc? 12 | pub struct Devices(VecIntoIter); 13 | 14 | impl Devices { 15 | pub fn new() -> Result { 16 | Ok(Self::default()) 17 | } 18 | } 19 | 20 | impl Default for Devices { 21 | fn default() -> Devices { 22 | Devices(vec![Device].into_iter()) 23 | } 24 | } 25 | 26 | impl Iterator for Devices { 27 | type Item = Device; 28 | 29 | #[inline] 30 | fn next(&mut self) -> Option { 31 | self.0.next() 32 | } 33 | } 34 | 35 | #[inline] 36 | pub fn default_input_device() -> Option { 37 | Some(Device) 38 | } 39 | 40 | #[inline] 41 | pub fn default_output_device() -> Option { 42 | Some(Device) 43 | } 44 | -------------------------------------------------------------------------------- /src/host/coreaudio/macos/enumerate.rs: -------------------------------------------------------------------------------- 1 | extern crate coreaudio; 2 | 3 | use self::coreaudio::sys::{ 4 | kAudioHardwareNoError, kAudioHardwarePropertyDefaultInputDevice, 5 | kAudioHardwarePropertyDefaultOutputDevice, kAudioHardwarePropertyDevices, 6 | kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeGlobal, kAudioObjectSystemObject, 7 | AudioDeviceID, AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize, 8 | AudioObjectPropertyAddress, OSStatus, 9 | }; 10 | use super::Device; 11 | use crate::{BackendSpecificError, DevicesError, SupportedStreamConfigRange}; 12 | use std::mem; 13 | use std::ptr::null; 14 | use std::vec::IntoIter as VecIntoIter; 15 | 16 | unsafe fn audio_devices() -> Result, OSStatus> { 17 | let property_address = AudioObjectPropertyAddress { 18 | mSelector: kAudioHardwarePropertyDevices, 19 | mScope: kAudioObjectPropertyScopeGlobal, 20 | mElement: kAudioObjectPropertyElementMaster, 21 | }; 22 | 23 | macro_rules! try_status_or_return { 24 | ($status:expr) => { 25 | if $status != kAudioHardwareNoError as i32 { 26 | return Err($status); 27 | } 28 | }; 29 | } 30 | 31 | let data_size = 0u32; 32 | let status = AudioObjectGetPropertyDataSize( 33 | kAudioObjectSystemObject, 34 | &property_address as *const _, 35 | 0, 36 | null(), 37 | &data_size as *const _ as *mut _, 38 | ); 39 | try_status_or_return!(status); 40 | 41 | let device_count = data_size / mem::size_of::() as u32; 42 | let mut audio_devices = vec![]; 43 | audio_devices.reserve_exact(device_count as usize); 44 | 45 | let status = AudioObjectGetPropertyData( 46 | kAudioObjectSystemObject, 47 | &property_address as *const _, 48 | 0, 49 | null(), 50 | &data_size as *const _ as *mut _, 51 | audio_devices.as_mut_ptr() as *mut _, 52 | ); 53 | try_status_or_return!(status); 54 | 55 | audio_devices.set_len(device_count as usize); 56 | 57 | Ok(audio_devices) 58 | } 59 | 60 | pub struct Devices(VecIntoIter); 61 | 62 | impl Devices { 63 | pub fn new() -> Result { 64 | let devices = unsafe { 65 | match audio_devices() { 66 | Ok(devices) => devices, 67 | Err(os_status) => { 68 | let description = format!("{}", os_status); 69 | let err = BackendSpecificError { description }; 70 | return Err(err.into()); 71 | } 72 | } 73 | }; 74 | Ok(Devices(devices.into_iter())) 75 | } 76 | } 77 | 78 | unsafe impl Send for Devices {} 79 | unsafe impl Sync for Devices {} 80 | 81 | impl Iterator for Devices { 82 | type Item = Device; 83 | fn next(&mut self) -> Option { 84 | self.0.next().map(|id| Device { 85 | audio_device_id: id, 86 | is_default: false, 87 | }) 88 | } 89 | } 90 | 91 | pub fn default_input_device() -> Option { 92 | let property_address = AudioObjectPropertyAddress { 93 | mSelector: kAudioHardwarePropertyDefaultInputDevice, 94 | mScope: kAudioObjectPropertyScopeGlobal, 95 | mElement: kAudioObjectPropertyElementMaster, 96 | }; 97 | 98 | let audio_device_id: AudioDeviceID = 0; 99 | let data_size = mem::size_of::(); 100 | let status = unsafe { 101 | AudioObjectGetPropertyData( 102 | kAudioObjectSystemObject, 103 | &property_address as *const _, 104 | 0, 105 | null(), 106 | &data_size as *const _ as *mut _, 107 | &audio_device_id as *const _ as *mut _, 108 | ) 109 | }; 110 | if status != kAudioHardwareNoError as i32 { 111 | return None; 112 | } 113 | 114 | let device = Device { 115 | audio_device_id, 116 | is_default: true, 117 | }; 118 | Some(device) 119 | } 120 | 121 | pub fn default_output_device() -> Option { 122 | let property_address = AudioObjectPropertyAddress { 123 | mSelector: kAudioHardwarePropertyDefaultOutputDevice, 124 | mScope: kAudioObjectPropertyScopeGlobal, 125 | mElement: kAudioObjectPropertyElementMaster, 126 | }; 127 | 128 | let audio_device_id: AudioDeviceID = 0; 129 | let data_size = mem::size_of::(); 130 | let status = unsafe { 131 | AudioObjectGetPropertyData( 132 | kAudioObjectSystemObject, 133 | &property_address as *const _, 134 | 0, 135 | null(), 136 | &data_size as *const _ as *mut _, 137 | &audio_device_id as *const _ as *mut _, 138 | ) 139 | }; 140 | if status != kAudioHardwareNoError as i32 { 141 | return None; 142 | } 143 | 144 | let device = Device { 145 | audio_device_id, 146 | is_default: true, 147 | }; 148 | Some(device) 149 | } 150 | 151 | pub type SupportedInputConfigs = VecIntoIter; 152 | pub type SupportedOutputConfigs = VecIntoIter; 153 | -------------------------------------------------------------------------------- /src/host/coreaudio/macos/property_listener.rs: -------------------------------------------------------------------------------- 1 | //! Helper code for registering audio object property listeners. 2 | use super::coreaudio::sys::{ 3 | AudioObjectAddPropertyListener, AudioObjectID, AudioObjectPropertyAddress, 4 | AudioObjectRemovePropertyListener, OSStatus, 5 | }; 6 | 7 | use crate::BuildStreamError; 8 | 9 | /// A double-indirection to be able to pass a closure (a fat pointer) 10 | /// via a single c_void. 11 | struct PropertyListenerCallbackWrapper(Box); 12 | 13 | /// Maintain an audio object property listener. 14 | /// The listener will be removed when this type is dropped. 15 | pub struct AudioObjectPropertyListener { 16 | callback: Box, 17 | property_address: AudioObjectPropertyAddress, 18 | audio_object_id: AudioObjectID, 19 | removed: bool, 20 | } 21 | 22 | impl AudioObjectPropertyListener { 23 | /// Attach the provided callback as a audio object property listener. 24 | pub fn new( 25 | audio_object_id: AudioObjectID, 26 | property_address: AudioObjectPropertyAddress, 27 | callback: F, 28 | ) -> Result { 29 | let callback = Box::new(PropertyListenerCallbackWrapper(Box::new(callback))); 30 | unsafe { 31 | coreaudio::Error::from_os_status(AudioObjectAddPropertyListener( 32 | audio_object_id, 33 | &property_address as *const _, 34 | Some(property_listener_handler_shim), 35 | &*callback as *const _ as *mut _, 36 | ))?; 37 | }; 38 | Ok(Self { 39 | callback, 40 | audio_object_id, 41 | property_address, 42 | removed: false, 43 | }) 44 | } 45 | 46 | /// Explicitly remove the property listener. 47 | /// Use this method if you need to explicitly handle failure to remove 48 | /// the property listener. 49 | pub fn remove(mut self) -> Result<(), BuildStreamError> { 50 | self.remove_inner() 51 | } 52 | 53 | fn remove_inner(&mut self) -> Result<(), BuildStreamError> { 54 | unsafe { 55 | coreaudio::Error::from_os_status(AudioObjectRemovePropertyListener( 56 | self.audio_object_id, 57 | &self.property_address as *const _, 58 | Some(property_listener_handler_shim), 59 | &*self.callback as *const _ as *mut _, 60 | ))?; 61 | } 62 | self.removed = true; 63 | Ok(()) 64 | } 65 | } 66 | 67 | impl Drop for AudioObjectPropertyListener { 68 | fn drop(&mut self) { 69 | if !self.removed { 70 | let _ = self.remove_inner(); 71 | } 72 | } 73 | } 74 | 75 | /// Callback used to call user-provided closure as a property listener. 76 | unsafe extern "C" fn property_listener_handler_shim( 77 | _: AudioObjectID, 78 | _: u32, 79 | _: *const AudioObjectPropertyAddress, 80 | callback: *mut ::std::os::raw::c_void, 81 | ) -> OSStatus { 82 | let wrapper = callback as *mut PropertyListenerCallbackWrapper; 83 | (*wrapper).0(); 84 | 0 85 | } 86 | -------------------------------------------------------------------------------- /src/host/coreaudio/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate coreaudio; 2 | 3 | use self::coreaudio::sys::{ 4 | kAudioFormatFlagIsFloat, kAudioFormatFlagIsPacked, kAudioFormatFlagIsSignedInteger, 5 | kAudioFormatLinearPCM, AudioStreamBasicDescription, OSStatus, 6 | }; 7 | 8 | use crate::DefaultStreamConfigError; 9 | use crate::{BuildStreamError, SupportedStreamConfigsError}; 10 | 11 | use crate::{BackendSpecificError, SampleFormat, StreamConfig}; 12 | 13 | #[cfg(target_os = "ios")] 14 | mod ios; 15 | #[cfg(target_os = "macos")] 16 | mod macos; 17 | 18 | #[cfg(target_os = "ios")] 19 | pub use self::ios::{ 20 | enumerate::{Devices, SupportedInputConfigs, SupportedOutputConfigs}, 21 | Device, Host, Stream, 22 | }; 23 | 24 | #[cfg(target_os = "macos")] 25 | pub use self::macos::{ 26 | enumerate::{Devices, SupportedInputConfigs, SupportedOutputConfigs}, 27 | Device, Host, Stream, 28 | }; 29 | 30 | /// Common helper methods used by both macOS and iOS 31 | 32 | fn check_os_status(os_status: OSStatus) -> Result<(), BackendSpecificError> { 33 | match coreaudio::Error::from_os_status(os_status) { 34 | Ok(()) => Ok(()), 35 | Err(err) => { 36 | let description = err.to_string(); 37 | Err(BackendSpecificError { description }) 38 | } 39 | } 40 | } 41 | 42 | // Create a coreaudio AudioStreamBasicDescription from a CPAL Format. 43 | fn asbd_from_config( 44 | config: &StreamConfig, 45 | sample_format: SampleFormat, 46 | ) -> AudioStreamBasicDescription { 47 | let n_channels = config.channels as usize; 48 | let sample_rate = config.sample_rate.0; 49 | let bytes_per_channel = sample_format.sample_size(); 50 | let bits_per_channel = bytes_per_channel * 8; 51 | let bytes_per_frame = n_channels * bytes_per_channel; 52 | let frames_per_packet = 1; 53 | let bytes_per_packet = frames_per_packet * bytes_per_frame; 54 | let format_flags = match sample_format { 55 | SampleFormat::F32 | SampleFormat::F64 => kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked, 56 | SampleFormat::I16 | SampleFormat::I32 | SampleFormat::I64 => { 57 | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked 58 | } 59 | _ => kAudioFormatFlagIsPacked, 60 | }; 61 | AudioStreamBasicDescription { 62 | mBitsPerChannel: bits_per_channel as _, 63 | mBytesPerFrame: bytes_per_frame as _, 64 | mChannelsPerFrame: n_channels as _, 65 | mBytesPerPacket: bytes_per_packet as _, 66 | mFramesPerPacket: frames_per_packet as _, 67 | mFormatFlags: format_flags, 68 | mFormatID: kAudioFormatLinearPCM, 69 | mSampleRate: sample_rate as _, 70 | ..Default::default() 71 | } 72 | } 73 | 74 | fn host_time_to_stream_instant( 75 | m_host_time: u64, 76 | ) -> Result { 77 | let mut info: mach2::mach_time::mach_timebase_info = Default::default(); 78 | let res = unsafe { mach2::mach_time::mach_timebase_info(&mut info) }; 79 | check_os_status(res)?; 80 | let nanos = m_host_time * info.numer as u64 / info.denom as u64; 81 | let secs = nanos / 1_000_000_000; 82 | let subsec_nanos = nanos - secs * 1_000_000_000; 83 | Ok(crate::StreamInstant::new(secs as i64, subsec_nanos as u32)) 84 | } 85 | 86 | // Convert the given duration in frames at the given sample rate to a `std::time::Duration`. 87 | fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { 88 | let secsf = frames as f64 / rate.0 as f64; 89 | let secs = secsf as u64; 90 | let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; 91 | std::time::Duration::new(secs, nanos) 92 | } 93 | 94 | // TODO need stronger error identification 95 | impl From for BuildStreamError { 96 | fn from(err: coreaudio::Error) -> BuildStreamError { 97 | match err { 98 | coreaudio::Error::RenderCallbackBufferFormatDoesNotMatchAudioUnitStreamFormat 99 | | coreaudio::Error::NoKnownSubtype 100 | | coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::FormatNotSupported) 101 | | coreaudio::Error::AudioCodec(_) 102 | | coreaudio::Error::AudioFormat(_) => BuildStreamError::StreamConfigNotSupported, 103 | _ => BuildStreamError::DeviceNotAvailable, 104 | } 105 | } 106 | } 107 | 108 | impl From for SupportedStreamConfigsError { 109 | fn from(err: coreaudio::Error) -> SupportedStreamConfigsError { 110 | let description = format!("{}", err); 111 | let err = BackendSpecificError { description }; 112 | // Check for possible DeviceNotAvailable variant 113 | SupportedStreamConfigsError::BackendSpecific { err } 114 | } 115 | } 116 | 117 | impl From for DefaultStreamConfigError { 118 | fn from(err: coreaudio::Error) -> DefaultStreamConfigError { 119 | let description = format!("{}", err); 120 | let err = BackendSpecificError { description }; 121 | // Check for possible DeviceNotAvailable variant 122 | DefaultStreamConfigError::BackendSpecific { err } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/host/jack/device.rs: -------------------------------------------------------------------------------- 1 | use crate::traits::DeviceTrait; 2 | use crate::{ 3 | BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, 4 | InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, 5 | SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, 6 | SupportedStreamConfigsError, 7 | }; 8 | use std::hash::{Hash, Hasher}; 9 | use std::time::Duration; 10 | 11 | use super::stream::Stream; 12 | use super::JACK_SAMPLE_FORMAT; 13 | 14 | pub type SupportedInputConfigs = std::vec::IntoIter; 15 | pub type SupportedOutputConfigs = std::vec::IntoIter; 16 | 17 | const DEFAULT_NUM_CHANNELS: u16 = 2; 18 | const DEFAULT_SUPPORTED_CHANNELS: [u16; 10] = [1, 2, 4, 6, 8, 16, 24, 32, 48, 64]; 19 | 20 | /// If a device is for input or output. 21 | /// Until we have duplex stream support JACK clients and CPAL devices for JACK will be either input or output. 22 | #[derive(Clone, Debug)] 23 | pub enum DeviceType { 24 | InputDevice, 25 | OutputDevice, 26 | } 27 | #[derive(Clone, Debug)] 28 | pub struct Device { 29 | name: String, 30 | sample_rate: SampleRate, 31 | buffer_size: SupportedBufferSize, 32 | device_type: DeviceType, 33 | start_server_automatically: bool, 34 | connect_ports_automatically: bool, 35 | } 36 | 37 | impl Device { 38 | fn new_device( 39 | name: String, 40 | connect_ports_automatically: bool, 41 | start_server_automatically: bool, 42 | device_type: DeviceType, 43 | ) -> Result { 44 | // ClientOptions are bit flags that you can set with the constants provided 45 | let client_options = super::get_client_options(start_server_automatically); 46 | 47 | // Create a dummy client to find out the sample rate of the server to be able to provide it as a possible config. 48 | // This client will be dropped, and a new one will be created when making the stream. 49 | // This is a hack due to the fact that the Client must be moved to create the AsyncClient. 50 | match super::get_client(&name, client_options) { 51 | Ok(client) => Ok(Device { 52 | // The name given to the client by JACK, could potentially be different from the name supplied e.g.if there is a name collision 53 | name: client.name().to_string(), 54 | sample_rate: SampleRate(client.sample_rate() as u32), 55 | buffer_size: SupportedBufferSize::Range { 56 | min: client.buffer_size(), 57 | max: client.buffer_size(), 58 | }, 59 | device_type, 60 | start_server_automatically, 61 | connect_ports_automatically, 62 | }), 63 | Err(e) => Err(e), 64 | } 65 | } 66 | 67 | pub fn default_output_device( 68 | name: &str, 69 | connect_ports_automatically: bool, 70 | start_server_automatically: bool, 71 | ) -> Result { 72 | let output_client_name = format!("{}_out", name); 73 | Device::new_device( 74 | output_client_name, 75 | connect_ports_automatically, 76 | start_server_automatically, 77 | DeviceType::OutputDevice, 78 | ) 79 | } 80 | 81 | pub fn default_input_device( 82 | name: &str, 83 | connect_ports_automatically: bool, 84 | start_server_automatically: bool, 85 | ) -> Result { 86 | let input_client_name = format!("{}_in", name); 87 | Device::new_device( 88 | input_client_name, 89 | connect_ports_automatically, 90 | start_server_automatically, 91 | DeviceType::InputDevice, 92 | ) 93 | } 94 | 95 | pub fn default_config(&self) -> Result { 96 | let channels = DEFAULT_NUM_CHANNELS; 97 | let sample_rate = self.sample_rate; 98 | let buffer_size = self.buffer_size.clone(); 99 | // The sample format for JACK audio ports is always "32-bit float mono audio" in the current implementation. 100 | // Custom formats are allowed within JACK, but this is of niche interest. 101 | // The format can be found programmatically by calling jack::PortSpec::port_type() on a created port. 102 | let sample_format = JACK_SAMPLE_FORMAT; 103 | Ok(SupportedStreamConfig { 104 | channels, 105 | sample_rate, 106 | buffer_size, 107 | sample_format, 108 | }) 109 | } 110 | 111 | pub fn supported_configs(&self) -> Vec { 112 | let f = match self.default_config() { 113 | Err(_) => return vec![], 114 | Ok(f) => f, 115 | }; 116 | 117 | let mut supported_configs = vec![]; 118 | 119 | for &channels in DEFAULT_SUPPORTED_CHANNELS.iter() { 120 | supported_configs.push(SupportedStreamConfigRange { 121 | channels, 122 | min_sample_rate: f.sample_rate, 123 | max_sample_rate: f.sample_rate, 124 | buffer_size: f.buffer_size.clone(), 125 | sample_format: f.sample_format, 126 | }); 127 | } 128 | supported_configs 129 | } 130 | 131 | pub fn is_input(&self) -> bool { 132 | matches!(self.device_type, DeviceType::InputDevice) 133 | } 134 | 135 | pub fn is_output(&self) -> bool { 136 | matches!(self.device_type, DeviceType::OutputDevice) 137 | } 138 | } 139 | 140 | impl DeviceTrait for Device { 141 | type SupportedInputConfigs = SupportedInputConfigs; 142 | type SupportedOutputConfigs = SupportedOutputConfigs; 143 | type Stream = Stream; 144 | 145 | fn name(&self) -> Result { 146 | Ok(self.name.clone()) 147 | } 148 | 149 | fn supported_input_configs( 150 | &self, 151 | ) -> Result { 152 | Ok(self.supported_configs().into_iter()) 153 | } 154 | 155 | fn supported_output_configs( 156 | &self, 157 | ) -> Result { 158 | Ok(self.supported_configs().into_iter()) 159 | } 160 | 161 | /// Returns the default input config 162 | /// The sample format for JACK audio ports is always "32-bit float mono audio" unless using a custom type. 163 | /// The sample rate is set by the JACK server. 164 | fn default_input_config(&self) -> Result { 165 | self.default_config() 166 | } 167 | 168 | /// Returns the default output config 169 | /// The sample format for JACK audio ports is always "32-bit float mono audio" unless using a custom type. 170 | /// The sample rate is set by the JACK server. 171 | fn default_output_config(&self) -> Result { 172 | self.default_config() 173 | } 174 | 175 | fn build_input_stream_raw( 176 | &self, 177 | conf: &StreamConfig, 178 | sample_format: SampleFormat, 179 | data_callback: D, 180 | error_callback: E, 181 | _timeout: Option, 182 | ) -> Result 183 | where 184 | D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, 185 | E: FnMut(StreamError) + Send + 'static, 186 | { 187 | if let DeviceType::OutputDevice = &self.device_type { 188 | // Trying to create an input stream from an output device 189 | return Err(BuildStreamError::StreamConfigNotSupported); 190 | } 191 | if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT { 192 | return Err(BuildStreamError::StreamConfigNotSupported); 193 | } 194 | // The settings should be fine, create a Client 195 | let client_options = super::get_client_options(self.start_server_automatically); 196 | let client; 197 | match super::get_client(&self.name, client_options) { 198 | Ok(c) => client = c, 199 | Err(e) => { 200 | return Err(BuildStreamError::BackendSpecific { 201 | err: BackendSpecificError { description: e }, 202 | }) 203 | } 204 | }; 205 | let mut stream = Stream::new_input(client, conf.channels, data_callback, error_callback); 206 | 207 | if self.connect_ports_automatically { 208 | stream.connect_to_system_inputs(); 209 | } 210 | 211 | Ok(stream) 212 | } 213 | 214 | fn build_output_stream_raw( 215 | &self, 216 | conf: &StreamConfig, 217 | sample_format: SampleFormat, 218 | data_callback: D, 219 | error_callback: E, 220 | _timeout: Option, 221 | ) -> Result 222 | where 223 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, 224 | E: FnMut(StreamError) + Send + 'static, 225 | { 226 | if let DeviceType::InputDevice = &self.device_type { 227 | // Trying to create an output stream from an input device 228 | return Err(BuildStreamError::StreamConfigNotSupported); 229 | } 230 | if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT { 231 | return Err(BuildStreamError::StreamConfigNotSupported); 232 | } 233 | 234 | // The settings should be fine, create a Client 235 | let client_options = super::get_client_options(self.start_server_automatically); 236 | let client; 237 | match super::get_client(&self.name, client_options) { 238 | Ok(c) => client = c, 239 | Err(e) => { 240 | return Err(BuildStreamError::BackendSpecific { 241 | err: BackendSpecificError { description: e }, 242 | }) 243 | } 244 | }; 245 | let mut stream = Stream::new_output(client, conf.channels, data_callback, error_callback); 246 | 247 | if self.connect_ports_automatically { 248 | stream.connect_to_system_outputs(); 249 | } 250 | 251 | Ok(stream) 252 | } 253 | } 254 | 255 | impl PartialEq for Device { 256 | fn eq(&self, other: &Self) -> bool { 257 | // Device::name() can never fail in this implementation 258 | self.name().unwrap() == other.name().unwrap() 259 | } 260 | } 261 | 262 | impl Eq for Device {} 263 | 264 | impl Hash for Device { 265 | fn hash(&self, state: &mut H) { 266 | self.name().unwrap().hash(state); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/host/jack/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate jack; 2 | 3 | use crate::traits::HostTrait; 4 | use crate::{DevicesError, SampleFormat, SupportedStreamConfigRange}; 5 | 6 | mod device; 7 | pub use self::device::Device; 8 | pub use self::stream::Stream; 9 | mod stream; 10 | 11 | const JACK_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; 12 | 13 | pub type SupportedInputConfigs = std::vec::IntoIter; 14 | pub type SupportedOutputConfigs = std::vec::IntoIter; 15 | pub type Devices = std::vec::IntoIter; 16 | 17 | /// The JACK Host type 18 | #[derive(Debug)] 19 | pub struct Host { 20 | /// The name that the client will have in JACK. 21 | /// Until we have duplex streams two clients will be created adding "out" or "in" to the name 22 | /// since names have to be unique. 23 | name: String, 24 | /// If ports are to be connected to the system (soundcard) ports automatically (default is true). 25 | connect_ports_automatically: bool, 26 | /// If the JACK server should be started automatically if it isn't already when creating a Client (default is false). 27 | start_server_automatically: bool, 28 | /// A list of the devices that have been created from this Host. 29 | devices_created: Vec, 30 | } 31 | 32 | impl Host { 33 | pub fn new() -> Result { 34 | let mut host = Host { 35 | name: "cpal_client".to_owned(), 36 | connect_ports_automatically: true, 37 | start_server_automatically: false, 38 | devices_created: vec![], 39 | }; 40 | // Devices don't exist for JACK, they have to be created 41 | host.initialize_default_devices(); 42 | Ok(host) 43 | } 44 | /// Set whether the ports should automatically be connected to system 45 | /// (default is true) 46 | pub fn set_connect_automatically(&mut self, do_connect: bool) { 47 | self.connect_ports_automatically = do_connect; 48 | } 49 | /// Set whether a JACK server should be automatically started if it isn't already. 50 | /// (default is false) 51 | pub fn set_start_server_automatically(&mut self, do_start_server: bool) { 52 | self.start_server_automatically = do_start_server; 53 | } 54 | 55 | pub fn input_device_with_name(&mut self, name: &str) -> Option { 56 | self.name = name.to_owned(); 57 | self.default_input_device() 58 | } 59 | 60 | pub fn output_device_with_name(&mut self, name: &str) -> Option { 61 | self.name = name.to_owned(); 62 | self.default_output_device() 63 | } 64 | 65 | fn initialize_default_devices(&mut self) { 66 | let in_device_res = Device::default_input_device( 67 | &self.name, 68 | self.connect_ports_automatically, 69 | self.start_server_automatically, 70 | ); 71 | 72 | match in_device_res { 73 | Ok(device) => self.devices_created.push(device), 74 | Err(err) => { 75 | println!("{}", err); 76 | } 77 | } 78 | 79 | let out_device_res = Device::default_output_device( 80 | &self.name, 81 | self.connect_ports_automatically, 82 | self.start_server_automatically, 83 | ); 84 | match out_device_res { 85 | Ok(device) => self.devices_created.push(device), 86 | Err(err) => { 87 | println!("{}", err); 88 | } 89 | } 90 | } 91 | } 92 | 93 | impl HostTrait for Host { 94 | type Devices = Devices; 95 | type Device = Device; 96 | 97 | /// JACK is available if 98 | /// - the jack feature flag is set 99 | /// - libjack is installed (wouldn't compile without it) 100 | /// - the JACK server can be started 101 | /// 102 | /// If the code compiles the necessary jack libraries are installed. 103 | /// There is no way to know if the user has set up a correct JACK configuration e.g. with qjackctl. 104 | /// Users can choose to automatically start the server if it isn't already started when creating a client 105 | /// so checking if the server is running could give a false negative in some use cases. 106 | /// For these reasons this function should always return true. 107 | fn is_available() -> bool { 108 | true 109 | } 110 | 111 | fn devices(&self) -> Result { 112 | Ok(self.devices_created.clone().into_iter()) 113 | } 114 | 115 | fn default_input_device(&self) -> Option { 116 | for device in &self.devices_created { 117 | if device.is_input() { 118 | return Some(device.clone()); 119 | } 120 | } 121 | None 122 | } 123 | 124 | fn default_output_device(&self) -> Option { 125 | for device in &self.devices_created { 126 | if device.is_output() { 127 | return Some(device.clone()); 128 | } 129 | } 130 | None 131 | } 132 | } 133 | 134 | fn get_client_options(start_server_automatically: bool) -> jack::ClientOptions { 135 | let mut client_options = jack::ClientOptions::empty(); 136 | client_options.set( 137 | jack::ClientOptions::NO_START_SERVER, 138 | !start_server_automatically, 139 | ); 140 | client_options 141 | } 142 | 143 | fn get_client(name: &str, client_options: jack::ClientOptions) -> Result { 144 | let c_res = jack::Client::new(name, client_options); 145 | match c_res { 146 | Ok((client, status)) => { 147 | // The ClientStatus can tell us many things 148 | if status.intersects(jack::ClientStatus::SERVER_ERROR) { 149 | return Err(String::from( 150 | "There was an error communicating with the JACK server!", 151 | )); 152 | } else if status.intersects(jack::ClientStatus::SERVER_FAILED) { 153 | return Err(String::from("Could not connect to the JACK server!")); 154 | } else if status.intersects(jack::ClientStatus::VERSION_ERROR) { 155 | return Err(String::from( 156 | "Error connecting to JACK server: Client's protocol version does not match!", 157 | )); 158 | } else if status.intersects(jack::ClientStatus::INIT_FAILURE) { 159 | return Err(String::from( 160 | "Error connecting to JACK server: Unable to initialize client!", 161 | )); 162 | } else if status.intersects(jack::ClientStatus::SHM_FAILURE) { 163 | return Err(String::from( 164 | "Error connecting to JACK server: Unable to access shared memory!", 165 | )); 166 | } else if status.intersects(jack::ClientStatus::NO_SUCH_CLIENT) { 167 | return Err(String::from( 168 | "Error connecting to JACK server: Requested client does not exist!", 169 | )); 170 | } else if status.intersects(jack::ClientStatus::INVALID_OPTION) { 171 | return Err(String::from("Error connecting to JACK server: The operation contained an invalid or unsupported option!")); 172 | } 173 | Ok(client) 174 | } 175 | Err(e) => Err(format!("Failed to open client because of error: {:?}", e)), 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/host/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "android")] 2 | pub(crate) mod aaudio; 3 | #[cfg(any( 4 | target_os = "linux", 5 | target_os = "dragonfly", 6 | target_os = "freebsd", 7 | target_os = "netbsd" 8 | ))] 9 | pub(crate) mod alsa; 10 | #[cfg(all(windows, feature = "asio"))] 11 | pub(crate) mod asio; 12 | #[cfg(any(target_os = "macos", target_os = "ios"))] 13 | pub(crate) mod coreaudio; 14 | #[cfg(target_os = "emscripten")] 15 | pub(crate) mod emscripten; 16 | #[cfg(all( 17 | any( 18 | target_os = "linux", 19 | target_os = "dragonfly", 20 | target_os = "freebsd", 21 | target_os = "netbsd" 22 | ), 23 | feature = "jack" 24 | ))] 25 | pub(crate) mod jack; 26 | pub(crate) mod null; 27 | #[cfg(windows)] 28 | pub(crate) mod wasapi; 29 | #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] 30 | pub(crate) mod webaudio; 31 | -------------------------------------------------------------------------------- /src/host/null/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; 4 | use crate::{ 5 | BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, 6 | InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, 7 | StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, 8 | SupportedStreamConfigsError, 9 | }; 10 | 11 | #[derive(Default)] 12 | pub struct Devices; 13 | 14 | #[derive(Clone, Debug, PartialEq, Eq)] 15 | pub struct Device; 16 | 17 | pub struct Host; 18 | 19 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 20 | pub struct Stream; 21 | 22 | pub struct SupportedInputConfigs; 23 | pub struct SupportedOutputConfigs; 24 | 25 | impl Host { 26 | #[allow(dead_code)] 27 | pub fn new() -> Result { 28 | Ok(Host) 29 | } 30 | } 31 | 32 | impl Devices { 33 | pub fn new() -> Result { 34 | Ok(Devices) 35 | } 36 | } 37 | 38 | impl DeviceTrait for Device { 39 | type SupportedInputConfigs = SupportedInputConfigs; 40 | type SupportedOutputConfigs = SupportedOutputConfigs; 41 | type Stream = Stream; 42 | 43 | #[inline] 44 | fn name(&self) -> Result { 45 | Ok("null".to_owned()) 46 | } 47 | 48 | #[inline] 49 | fn supported_input_configs( 50 | &self, 51 | ) -> Result { 52 | unimplemented!() 53 | } 54 | 55 | #[inline] 56 | fn supported_output_configs( 57 | &self, 58 | ) -> Result { 59 | unimplemented!() 60 | } 61 | 62 | #[inline] 63 | fn default_input_config(&self) -> Result { 64 | unimplemented!() 65 | } 66 | 67 | #[inline] 68 | fn default_output_config(&self) -> Result { 69 | unimplemented!() 70 | } 71 | 72 | fn build_input_stream_raw( 73 | &self, 74 | _config: &StreamConfig, 75 | _sample_format: SampleFormat, 76 | _data_callback: D, 77 | _error_callback: E, 78 | _timeout: Option, 79 | ) -> Result 80 | where 81 | D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, 82 | E: FnMut(StreamError) + Send + 'static, 83 | { 84 | unimplemented!() 85 | } 86 | 87 | /// Create an output stream. 88 | fn build_output_stream_raw( 89 | &self, 90 | _config: &StreamConfig, 91 | _sample_format: SampleFormat, 92 | _data_callback: D, 93 | _error_callback: E, 94 | _timeout: Option, 95 | ) -> Result 96 | where 97 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, 98 | E: FnMut(StreamError) + Send + 'static, 99 | { 100 | unimplemented!() 101 | } 102 | } 103 | 104 | impl HostTrait for Host { 105 | type Devices = Devices; 106 | type Device = Device; 107 | 108 | fn is_available() -> bool { 109 | false 110 | } 111 | 112 | fn devices(&self) -> Result { 113 | Devices::new() 114 | } 115 | 116 | fn default_input_device(&self) -> Option { 117 | None 118 | } 119 | 120 | fn default_output_device(&self) -> Option { 121 | None 122 | } 123 | } 124 | 125 | impl StreamTrait for Stream { 126 | fn play(&self) -> Result<(), PlayStreamError> { 127 | unimplemented!() 128 | } 129 | 130 | fn pause(&self) -> Result<(), PauseStreamError> { 131 | unimplemented!() 132 | } 133 | } 134 | 135 | impl Iterator for Devices { 136 | type Item = Device; 137 | 138 | #[inline] 139 | fn next(&mut self) -> Option { 140 | None 141 | } 142 | } 143 | 144 | impl Iterator for SupportedInputConfigs { 145 | type Item = SupportedStreamConfigRange; 146 | 147 | #[inline] 148 | fn next(&mut self) -> Option { 149 | None 150 | } 151 | } 152 | 153 | impl Iterator for SupportedOutputConfigs { 154 | type Item = SupportedStreamConfigRange; 155 | 156 | #[inline] 157 | fn next(&mut self) -> Option { 158 | None 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/host/wasapi/com.rs: -------------------------------------------------------------------------------- 1 | //! Handles COM initialization and cleanup. 2 | 3 | use super::IoError; 4 | use std::marker::PhantomData; 5 | 6 | use windows::Win32::Foundation::RPC_E_CHANGED_MODE; 7 | use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED}; 8 | 9 | thread_local!(static COM_INITIALIZED: ComInitialized = { 10 | unsafe { 11 | // Try to initialize COM with STA by default to avoid compatibility issues with the ASIO 12 | // backend (where CoInitialize() is called by the ASIO SDK) or winit (where drag and drop 13 | // requires STA). 14 | // This call can fail with RPC_E_CHANGED_MODE if another library initialized COM with MTA. 15 | // That's OK though since COM ensures thread-safety/compatibility through marshalling when 16 | // necessary. 17 | let result = CoInitializeEx(None, COINIT_APARTMENTTHREADED); 18 | if result.is_ok() || result == RPC_E_CHANGED_MODE { 19 | ComInitialized { 20 | result, 21 | _ptr: PhantomData, 22 | } 23 | } else { 24 | // COM initialization failed in another way, something is really wrong. 25 | panic!( 26 | "Failed to initialize COM: {}", 27 | IoError::from_raw_os_error(result.0) 28 | ); 29 | } 30 | } 31 | }); 32 | 33 | /// RAII object that guards the fact that COM is initialized. 34 | /// 35 | // We store a raw pointer because it's the only way at the moment to remove `Send`/`Sync` from the 36 | // object. 37 | struct ComInitialized { 38 | result: windows::core::HRESULT, 39 | _ptr: PhantomData<*mut ()>, 40 | } 41 | 42 | impl Drop for ComInitialized { 43 | #[inline] 44 | fn drop(&mut self) { 45 | // Need to avoid calling CoUninitialize() if CoInitializeEx failed since it may have 46 | // returned RPC_E_MODE_CHANGED - which is OK, see above. 47 | if self.result.is_ok() { 48 | unsafe { CoUninitialize() }; 49 | } 50 | } 51 | } 52 | 53 | /// Ensures that COM is initialized in this thread. 54 | #[inline] 55 | pub fn com_initialized() { 56 | COM_INITIALIZED.with(|_| {}); 57 | } 58 | -------------------------------------------------------------------------------- /src/host/wasapi/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::device::{ 2 | default_input_device, default_output_device, Device, Devices, SupportedInputConfigs, 3 | SupportedOutputConfigs, 4 | }; 5 | pub use self::stream::Stream; 6 | use crate::traits::HostTrait; 7 | use crate::BackendSpecificError; 8 | use crate::DevicesError; 9 | use std::io::Error as IoError; 10 | use windows::Win32::Media::Audio; 11 | 12 | mod com; 13 | mod device; 14 | mod stream; 15 | 16 | /// The WASAPI host, the default windows host type. 17 | /// 18 | /// Note: If you use a WASAPI output device as an input device it will 19 | /// transparently enable loopback mode (see 20 | /// https://docs.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording). 21 | #[derive(Debug)] 22 | pub struct Host; 23 | 24 | impl Host { 25 | pub fn new() -> Result { 26 | Ok(Host) 27 | } 28 | } 29 | 30 | impl HostTrait for Host { 31 | type Devices = Devices; 32 | type Device = Device; 33 | 34 | fn is_available() -> bool { 35 | // Assume WASAPI is always available on Windows. 36 | true 37 | } 38 | 39 | fn devices(&self) -> Result { 40 | Devices::new() 41 | } 42 | 43 | fn default_input_device(&self) -> Option { 44 | default_input_device() 45 | } 46 | 47 | fn default_output_device(&self) -> Option { 48 | default_output_device() 49 | } 50 | } 51 | 52 | impl From for BackendSpecificError { 53 | fn from(error: windows::core::Error) -> Self { 54 | BackendSpecificError { 55 | description: format!("{}", IoError::from(error)), 56 | } 57 | } 58 | } 59 | 60 | trait ErrDeviceNotAvailable: From { 61 | fn device_not_available() -> Self; 62 | } 63 | 64 | impl ErrDeviceNotAvailable for crate::BuildStreamError { 65 | fn device_not_available() -> Self { 66 | Self::DeviceNotAvailable 67 | } 68 | } 69 | 70 | impl ErrDeviceNotAvailable for crate::SupportedStreamConfigsError { 71 | fn device_not_available() -> Self { 72 | Self::DeviceNotAvailable 73 | } 74 | } 75 | 76 | impl ErrDeviceNotAvailable for crate::DefaultStreamConfigError { 77 | fn device_not_available() -> Self { 78 | Self::DeviceNotAvailable 79 | } 80 | } 81 | 82 | impl ErrDeviceNotAvailable for crate::StreamError { 83 | fn device_not_available() -> Self { 84 | Self::DeviceNotAvailable 85 | } 86 | } 87 | 88 | fn windows_err_to_cpal_err(e: windows::core::Error) -> E { 89 | windows_err_to_cpal_err_message::(e, "") 90 | } 91 | 92 | fn windows_err_to_cpal_err_message( 93 | e: windows::core::Error, 94 | message: &str, 95 | ) -> E { 96 | match e.code() { 97 | Audio::AUDCLNT_E_DEVICE_INVALIDATED => E::device_not_available(), 98 | _ => { 99 | let description = format!("{}{}", message, e); 100 | let err = BackendSpecificError { description }; 101 | err.into() 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/samples_formats.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt::Display, mem}; 2 | #[cfg(target_os = "emscripten")] 3 | use wasm_bindgen::prelude::*; 4 | 5 | pub use dasp_sample::{FromSample, Sample, I24, I48, U24, U48}; 6 | 7 | /// Format that each sample has. Usually, this corresponds to the sampling 8 | /// depth of the audio source. For example, 16 bit quantized samples can be 9 | /// encoded in `i16` or `u16`. Note that the sampling depth is not directly 10 | /// visible for formats where [`is_float`] is true. 11 | /// 12 | /// Also note that the backend must support the encoding of the quantized 13 | /// samples in the given format, as there is no generic transformation from one 14 | /// format into the other done inside the frontend-library code. You can query 15 | /// the supported formats by using [`supported_input_configs`]. 16 | /// 17 | /// A good rule of thumb is to use [`SampleFormat::I16`] as this covers typical 18 | /// music (WAV, MP3) as well as typical audio input devices on most platforms, 19 | /// 20 | /// [`is_float`]: SampleFormat::is_float 21 | /// [`supported_input_configs`]: crate::Device::supported_input_configs 22 | #[cfg_attr(target_os = "emscripten", wasm_bindgen)] 23 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 24 | #[non_exhaustive] 25 | pub enum SampleFormat { 26 | /// `i8` with a valid range of `i8::MIN..=i8::MAX` with `0` being the origin. 27 | I8, 28 | 29 | /// `i16` with a valid range of `i16::MIN..=i16::MAX` with `0` being the origin. 30 | I16, 31 | 32 | /// `I24` with a valid range of '-(1 << 23)..(1 << 23)' with `0` being the origin 33 | I24, 34 | 35 | /// `i32` with a valid range of `i32::MIN..=i32::MAX` with `0` being the origin. 36 | I32, 37 | 38 | // /// `I48` with a valid range of '-(1 << 47)..(1 << 47)' with `0` being the origin 39 | // I48, 40 | /// `i64` with a valid range of `i64::MIN..=i64::MAX` with `0` being the origin. 41 | I64, 42 | 43 | /// `u8` with a valid range of `u8::MIN..=u8::MAX` with `1 << 7 == 128` being the origin. 44 | U8, 45 | 46 | /// `u16` with a valid range of `u16::MIN..=u16::MAX` with `1 << 15 == 32768` being the origin. 47 | U16, 48 | 49 | /// `U24` with a valid range of '0..16777216' with `1 << 23 == 8388608` being the origin 50 | // U24, 51 | 52 | /// `u32` with a valid range of `u32::MIN..=u32::MAX` with `1 << 31` being the origin. 53 | U32, 54 | 55 | /// `U48` with a valid range of '0..(1 << 48)' with `1 << 47` being the origin 56 | // U48, 57 | 58 | /// `u64` with a valid range of `u64::MIN..=u64::MAX` with `1 << 63` being the origin. 59 | U64, 60 | 61 | /// `f32` with a valid range of `-1.0..1.0` with `0.0` being the origin. 62 | F32, 63 | 64 | /// `f64` with a valid range of `-1.0..1.0` with `0.0` being the origin. 65 | F64, 66 | } 67 | 68 | impl SampleFormat { 69 | /// Returns the size in bytes of a sample of this format. This corresponds to 70 | /// the internal size of the rust primitives that are used to represent this 71 | /// sample format (e.g., i24 has size of i32). 72 | #[inline] 73 | #[must_use] 74 | pub fn sample_size(&self) -> usize { 75 | match *self { 76 | SampleFormat::I8 | SampleFormat::U8 => mem::size_of::(), 77 | SampleFormat::I16 | SampleFormat::U16 => mem::size_of::(), 78 | SampleFormat::I24 => mem::size_of::(), 79 | // SampleFormat::U24 => mem::size_of::(), 80 | SampleFormat::I32 | SampleFormat::U32 => mem::size_of::(), 81 | 82 | // SampleFormat::I48 => mem::size_of::(), 83 | // SampleFormat::U48 => mem::size_of::(), 84 | SampleFormat::I64 | SampleFormat::U64 => mem::size_of::(), 85 | SampleFormat::F32 => mem::size_of::(), 86 | SampleFormat::F64 => mem::size_of::(), 87 | } 88 | } 89 | 90 | #[inline] 91 | #[must_use] 92 | pub fn is_int(&self) -> bool { 93 | matches!( 94 | *self, 95 | SampleFormat::I8 96 | | SampleFormat::I16 97 | | SampleFormat::I24 98 | | SampleFormat::I32 99 | // | SampleFormat::I48 100 | | SampleFormat::I64 101 | ) 102 | } 103 | 104 | #[inline] 105 | #[must_use] 106 | pub fn is_uint(&self) -> bool { 107 | matches!( 108 | *self, 109 | SampleFormat::U8 110 | | SampleFormat::U16 111 | // | SampleFormat::U24 112 | | SampleFormat::U32 113 | // | SampleFormat::U48 114 | | SampleFormat::U64 115 | ) 116 | } 117 | 118 | #[inline] 119 | #[must_use] 120 | pub fn is_float(&self) -> bool { 121 | matches!(*self, SampleFormat::F32 | SampleFormat::F64) 122 | } 123 | } 124 | 125 | impl Display for SampleFormat { 126 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 127 | match *self { 128 | SampleFormat::I8 => "i8", 129 | SampleFormat::I16 => "i16", 130 | SampleFormat::I24 => "i24", 131 | SampleFormat::I32 => "i32", 132 | // SampleFormat::I48 => "i48", 133 | SampleFormat::I64 => "i64", 134 | SampleFormat::U8 => "u8", 135 | SampleFormat::U16 => "u16", 136 | // SampleFormat::U24 => "u24", 137 | SampleFormat::U32 => "u32", 138 | // SampleFormat::U48 => "u48", 139 | SampleFormat::U64 => "u64", 140 | SampleFormat::F32 => "f32", 141 | SampleFormat::F64 => "f64", 142 | } 143 | .fmt(f) 144 | } 145 | } 146 | 147 | pub trait SizedSample: Sample { 148 | const FORMAT: SampleFormat; 149 | } 150 | 151 | impl SizedSample for i8 { 152 | const FORMAT: SampleFormat = SampleFormat::I8; 153 | } 154 | 155 | impl SizedSample for i16 { 156 | const FORMAT: SampleFormat = SampleFormat::I16; 157 | } 158 | 159 | impl SizedSample for I24 { 160 | const FORMAT: SampleFormat = SampleFormat::I24; 161 | } 162 | 163 | impl SizedSample for i32 { 164 | const FORMAT: SampleFormat = SampleFormat::I32; 165 | } 166 | 167 | // impl SizedSample for I48 { 168 | // const FORMAT: SampleFormat = SampleFormat::I48; 169 | // } 170 | 171 | impl SizedSample for i64 { 172 | const FORMAT: SampleFormat = SampleFormat::I64; 173 | } 174 | 175 | impl SizedSample for u8 { 176 | const FORMAT: SampleFormat = SampleFormat::U8; 177 | } 178 | 179 | impl SizedSample for u16 { 180 | const FORMAT: SampleFormat = SampleFormat::U16; 181 | } 182 | 183 | // impl SizedSample for U24 { 184 | // const FORMAT: SampleFormat = SampleFormat::U24; 185 | // } 186 | 187 | impl SizedSample for u32 { 188 | const FORMAT: SampleFormat = SampleFormat::U32; 189 | } 190 | 191 | // impl SizedSample for U48 { 192 | // const FORMAT: SampleFormat = SampleFormat::U48; 193 | // } 194 | 195 | impl SizedSample for u64 { 196 | const FORMAT: SampleFormat = SampleFormat::U64; 197 | } 198 | 199 | impl SizedSample for f32 { 200 | const FORMAT: SampleFormat = SampleFormat::F32; 201 | } 202 | 203 | impl SizedSample for f64 { 204 | const FORMAT: SampleFormat = SampleFormat::F64; 205 | } 206 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | //! The suite of traits allowing CPAL to abstract over hosts, devices, event loops and stream IDs. 2 | 3 | use std::time::Duration; 4 | 5 | use crate::{ 6 | BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, 7 | InputCallbackInfo, InputDevices, OutputCallbackInfo, OutputDevices, PauseStreamError, 8 | PlayStreamError, SampleFormat, SizedSample, StreamConfig, StreamError, SupportedStreamConfig, 9 | SupportedStreamConfigRange, SupportedStreamConfigsError, 10 | }; 11 | 12 | /// A [`Host`] provides access to the available audio devices on the system. 13 | /// 14 | /// Each platform may have a number of available hosts depending on the system, each with their own 15 | /// pros and cons. 16 | /// 17 | /// For example, WASAPI is the standard audio host API that ships with the Windows operating 18 | /// system. However, due to historical limitations with respect to performance and flexibility, 19 | /// Steinberg created the ASIO API providing better audio device support for pro audio and 20 | /// low-latency applications. As a result, it is common for some devices and device capabilities to 21 | /// only be available via ASIO, while others are only available via WASAPI. 22 | /// 23 | /// Another great example is the Linux platform. While the ALSA host API is the lowest-level API 24 | /// available to almost all distributions of Linux, its flexibility is limited as it requires that 25 | /// each process have exclusive access to the devices with which they establish streams. PulseAudio 26 | /// is another popular host API that aims to solve this issue by providing user-space mixing, 27 | /// however it has its own limitations w.r.t. low-latency and high-performance audio applications. 28 | /// JACK is yet another host API that is more suitable to pro-audio applications, however it is 29 | /// less readily available by default in many Linux distributions and is known to be tricky to 30 | /// set up. 31 | /// 32 | /// [`Host`]: crate::Host 33 | pub trait HostTrait { 34 | /// The type used for enumerating available devices by the host. 35 | type Devices: Iterator; 36 | /// The `Device` type yielded by the host. 37 | type Device: DeviceTrait; 38 | 39 | /// Whether or not the host is available on the system. 40 | fn is_available() -> bool; 41 | 42 | /// An iterator yielding all [`Device`](DeviceTrait)s currently available to the host on the system. 43 | /// 44 | /// Can be empty if the system does not support audio in general. 45 | fn devices(&self) -> Result; 46 | 47 | /// The default input audio device on the system. 48 | /// 49 | /// Returns `None` if no input device is available. 50 | fn default_input_device(&self) -> Option; 51 | 52 | /// The default output audio device on the system. 53 | /// 54 | /// Returns `None` if no output device is available. 55 | fn default_output_device(&self) -> Option; 56 | 57 | /// An iterator yielding all `Device`s currently available to the system that support one or more 58 | /// input stream formats. 59 | /// 60 | /// Can be empty if the system does not support audio input. 61 | fn input_devices(&self) -> Result, DevicesError> { 62 | Ok(self.devices()?.filter(DeviceTrait::supports_input)) 63 | } 64 | 65 | /// An iterator yielding all `Device`s currently available to the system that support one or more 66 | /// output stream formats. 67 | /// 68 | /// Can be empty if the system does not support audio output. 69 | fn output_devices(&self) -> Result, DevicesError> { 70 | Ok(self.devices()?.filter(DeviceTrait::supports_output)) 71 | } 72 | } 73 | 74 | /// A device that is capable of audio input and/or output. 75 | /// 76 | /// Please note that `Device`s may become invalid if they get disconnected. Therefore, all the 77 | /// methods that involve a device return a `Result` allowing the user to handle this case. 78 | pub trait DeviceTrait { 79 | /// The iterator type yielding supported input stream formats. 80 | type SupportedInputConfigs: Iterator; 81 | /// The iterator type yielding supported output stream formats. 82 | type SupportedOutputConfigs: Iterator; 83 | /// The stream type created by [`build_input_stream_raw`] and [`build_output_stream_raw`]. 84 | /// 85 | /// [`build_input_stream_raw`]: Self::build_input_stream_raw 86 | /// [`build_output_stream_raw`]: Self::build_output_stream_raw 87 | type Stream: StreamTrait; 88 | 89 | /// The human-readable name of the device. 90 | fn name(&self) -> Result; 91 | 92 | /// True if the device supports audio input, otherwise false 93 | fn supports_input(&self) -> bool { 94 | self.supported_input_configs() 95 | .map(|mut iter| iter.next().is_some()) 96 | .unwrap_or(false) 97 | } 98 | 99 | /// True if the device supports audio output, otherwise false 100 | fn supports_output(&self) -> bool { 101 | self.supported_output_configs() 102 | .map(|mut iter| iter.next().is_some()) 103 | .unwrap_or(false) 104 | } 105 | 106 | /// An iterator yielding formats that are supported by the backend. 107 | /// 108 | /// Can return an error if the device is no longer valid (e.g. it has been disconnected). 109 | fn supported_input_configs( 110 | &self, 111 | ) -> Result; 112 | 113 | /// An iterator yielding output stream formats that are supported by the device. 114 | /// 115 | /// Can return an error if the device is no longer valid (e.g. it has been disconnected). 116 | fn supported_output_configs( 117 | &self, 118 | ) -> Result; 119 | 120 | /// The default input stream format for the device. 121 | fn default_input_config(&self) -> Result; 122 | 123 | /// The default output stream format for the device. 124 | fn default_output_config(&self) -> Result; 125 | 126 | /// Create an input stream. 127 | fn build_input_stream( 128 | &self, 129 | config: &StreamConfig, 130 | mut data_callback: D, 131 | error_callback: E, 132 | timeout: Option, 133 | ) -> Result 134 | where 135 | T: SizedSample, 136 | D: FnMut(&[T], &InputCallbackInfo) + Send + 'static, 137 | E: FnMut(StreamError) + Send + 'static, 138 | { 139 | self.build_input_stream_raw( 140 | config, 141 | T::FORMAT, 142 | move |data, info| { 143 | data_callback( 144 | data.as_slice() 145 | .expect("host supplied incorrect sample type"), 146 | info, 147 | ) 148 | }, 149 | error_callback, 150 | timeout, 151 | ) 152 | } 153 | 154 | /// Create an output stream. 155 | fn build_output_stream( 156 | &self, 157 | config: &StreamConfig, 158 | mut data_callback: D, 159 | error_callback: E, 160 | timeout: Option, 161 | ) -> Result 162 | where 163 | T: SizedSample, 164 | D: FnMut(&mut [T], &OutputCallbackInfo) + Send + 'static, 165 | E: FnMut(StreamError) + Send + 'static, 166 | { 167 | self.build_output_stream_raw( 168 | config, 169 | T::FORMAT, 170 | move |data, info| { 171 | data_callback( 172 | data.as_slice_mut() 173 | .expect("host supplied incorrect sample type"), 174 | info, 175 | ) 176 | }, 177 | error_callback, 178 | timeout, 179 | ) 180 | } 181 | 182 | /// Create a dynamically typed input stream. 183 | fn build_input_stream_raw( 184 | &self, 185 | config: &StreamConfig, 186 | sample_format: SampleFormat, 187 | data_callback: D, 188 | error_callback: E, 189 | timeout: Option, 190 | ) -> Result 191 | where 192 | D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, 193 | E: FnMut(StreamError) + Send + 'static; 194 | 195 | /// Create a dynamically typed output stream. 196 | fn build_output_stream_raw( 197 | &self, 198 | config: &StreamConfig, 199 | sample_format: SampleFormat, 200 | data_callback: D, 201 | error_callback: E, 202 | timeout: Option, 203 | ) -> Result 204 | where 205 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, 206 | E: FnMut(StreamError) + Send + 'static; 207 | } 208 | 209 | /// A stream created from [`Device`](DeviceTrait), with methods to control playback. 210 | pub trait StreamTrait { 211 | /// Run the stream. 212 | /// 213 | /// Note: Not all platforms automatically run the stream upon creation, so it is important to 214 | /// call `play` after creation if it is expected that the stream should run immediately. 215 | fn play(&self) -> Result<(), PlayStreamError>; 216 | 217 | /// Some devices support pausing the audio stream. This can be useful for saving energy in 218 | /// moments of silence. 219 | /// 220 | /// Note: Not all devices support suspending the stream at the hardware level. This method may 221 | /// fail in these cases. 222 | fn pause(&self) -> Result<(), PauseStreamError>; 223 | } 224 | --------------------------------------------------------------------------------