chore: upgrade rodio 0.19 to 0.22, remove vendored rodio/cpal
Remove vendored rodio and cpal from src-tauri/vendor/ since rodio 0.22 now includes upstream f32 decoder support (our vendored modification). Update all Cargo.toml files to use rodio 0.22 from crates.io and remove the [patch.crates-io] section.
This commit is contained in:
122 files changed
+5
-22358
No files matched your search
@@ -22,10 +22,8 @@ serde_json = { workspace = true }
|
||||
# Error handling
|
||||
thiserror = { workspace = true }
|
||||
|
||||
# Audio - Vendored rodio for bit-perfect playback
|
||||
# This is the same vendored version used by qbz-nix
|
||||
# CRITICAL: Must use symphonia-all feature for f32 pipeline
|
||||
rodio = { path = "../../src-tauri/vendor/rodio", features = ["symphonia-all"] }
|
||||
# Audio playback (f32 pipeline is now upstream in rodio 0.22)
|
||||
rodio = { version = "0.22", features = ["symphonia-all"] }
|
||||
|
||||
# Audio analysis
|
||||
symphonia = { workspace = true }
|
||||
|
||||
@@ -31,8 +31,8 @@ rand = "0.8"
|
||||
# Audio decoding
|
||||
symphonia = { workspace = true }
|
||||
|
||||
# Vendored audio libraries (same as qbz-audio)
|
||||
rodio = { path = "../../src-tauri/vendor/rodio", features = ["symphonia-all"] }
|
||||
# Audio playback
|
||||
rodio = { version = "0.22", features = ["symphonia-all"] }
|
||||
|
||||
# HTTP client for streaming
|
||||
reqwest = { workspace = true }
|
||||
@@ -78,7 +78,7 @@ env_logger = "0.11"
|
||||
rand = "0.8"
|
||||
|
||||
# Audio playback
|
||||
rodio = { version = "0.19", features = ["symphonia-all"] }
|
||||
rodio = { version = "0.22", features = ["symphonia-all"] }
|
||||
symphonia = { version = "0.5", features = ["all"] }
|
||||
|
||||
# Audio visualization (FFT)
|
||||
@@ -156,6 +156,3 @@ tauri-plugin-dialog = { version = "2.6.0" }
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[patch.crates-io]
|
||||
cpal = { path = "vendor/cpal" }
|
||||
rodio = { path = "vendor/rodio" }
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"v":1}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "ac6cbb2ba55e61665a35ab88ae136a83380d1354"
|
||||
},
|
||||
"path_in_vcs": ""
|
||||
}
|
||||
-265
@@ -1,265 +0,0 @@
|
||||
name: cpal
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
|
||||
clippy-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Update apt
|
||||
run: sudo apt update
|
||||
- name: Install alsa
|
||||
run: sudo apt-get install libasound2-dev
|
||||
- name: Install libjack
|
||||
run: sudo apt-get install libjack-jackd2-dev libjack-jackd2-0
|
||||
- name: Install stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
target: armv7-linux-androideabi
|
||||
- name: Run clippy
|
||||
run: cargo clippy --all --all-features
|
||||
- name: Run clippy for Android target
|
||||
run: cargo clippy --all --features asio --features oboe/fetch-prebuilt --target armv7-linux-androideabi
|
||||
|
||||
rustfmt-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: Run rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
cargo-publish:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Update apt
|
||||
run: sudo apt update
|
||||
- name: Install alsa
|
||||
run: sudo apt-get install libasound2-dev
|
||||
- name: Verify publish crate
|
||||
uses: katyo/publish-crates@v2
|
||||
with:
|
||||
dry-run: true
|
||||
ignore-unpublished-changes: true
|
||||
- name: Publish crate
|
||||
uses: katyo/publish-crates@v2
|
||||
with:
|
||||
ignore-unpublished-changes: true
|
||||
registry-token: ${{ secrets.CRATESIO_TOKEN }}
|
||||
|
||||
ubuntu-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Update apt
|
||||
run: sudo apt update
|
||||
- name: Install alsa
|
||||
run: sudo apt-get install libasound2-dev
|
||||
- name: Install libjack
|
||||
run: sudo apt-get install libjack-jackd2-dev libjack-jackd2-0
|
||||
- name: Install stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Run without features
|
||||
run: cargo test --all --no-default-features --verbose
|
||||
- name: Run all features
|
||||
run: cargo test --all --all-features --verbose
|
||||
|
||||
linux-check-and-test-armv7:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
|
||||
- name: Build image
|
||||
run: docker build -t cross/cpal_armv7:v1 ./
|
||||
|
||||
- name: Install cross
|
||||
run: cargo install cross
|
||||
|
||||
- name: Check without features for armv7
|
||||
run: cross check --target armv7-unknown-linux-gnueabihf --workspace --no-default-features --verbose
|
||||
|
||||
- name: Test without features for armv7
|
||||
run: cross test --target armv7-unknown-linux-gnueabihf --workspace --no-default-features --verbose
|
||||
|
||||
- name: Check all features for armv7
|
||||
run: cross check --target armv7-unknown-linux-gnueabihf --workspace --all-features --verbose
|
||||
|
||||
- name: Test all features for armv7
|
||||
run: cross test --target armv7-unknown-linux-gnueabihf --workspace --all-features --verbose
|
||||
|
||||
asmjs-wasm32-test:
|
||||
strategy:
|
||||
matrix:
|
||||
target: [wasm32-unknown-emscripten]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Emscripten toolchain
|
||||
uses: mymindstorm/setup-emsdk@v14
|
||||
- name: Install stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
- name: Build beep example
|
||||
run: cargo build --example beep --release --target ${{ matrix.target }}
|
||||
|
||||
wasm32-bindgen-test:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
target: [wasm32-unknown-unknown]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
- name: Build beep example
|
||||
run: cargo build --example beep --target ${{ matrix.target }} --features=wasm-bindgen
|
||||
|
||||
wasm32-wasi-test:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
target: [wasm32-wasi]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
- name: Build beep example
|
||||
run: cargo build --example beep --target ${{ matrix.target }}
|
||||
|
||||
windows-test:
|
||||
strategy:
|
||||
matrix:
|
||||
version: [x86_64, i686]
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install ASIO SDK
|
||||
env:
|
||||
LINK: https://www.steinberg.net/asiosdk
|
||||
run: |
|
||||
curl -L -o asio.zip $env:LINK
|
||||
7z x -oasio asio.zip
|
||||
move asio\*\* asio\
|
||||
- name: Install ASIO4ALL
|
||||
run: choco install asio4all
|
||||
- name: Install llvm and clang
|
||||
run: choco install llvm
|
||||
- name: Install stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: ${{ matrix.version }}-pc-windows-msvc
|
||||
- name: Run without features
|
||||
run: cargo test --all --no-default-features --verbose
|
||||
- name: Run all features
|
||||
run: |
|
||||
$Env:CPAL_ASIO_DIR = "$Env:GITHUB_WORKSPACE\asio"
|
||||
cargo test --all --all-features --verbose
|
||||
|
||||
macos-test:
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install llvm and clang
|
||||
run: brew install llvm
|
||||
- name: Install stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Build beep example
|
||||
run: cargo build --example beep
|
||||
- name: Run without features
|
||||
run: cargo test --all --no-default-features --verbose
|
||||
- name: Run all features
|
||||
run: cargo test --all --all-features --verbose
|
||||
|
||||
android-check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install stable (Android target)
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: armv7-linux-androideabi
|
||||
- name: Check android
|
||||
run: cargo check --example android --target armv7-linux-androideabi --features oboe/fetch-prebuilt --verbose
|
||||
- name: Check beep
|
||||
run: cargo check --example beep --target armv7-linux-androideabi --features oboe/fetch-prebuilt --verbose
|
||||
- name: Check enumerate
|
||||
run: cargo check --example enumerate --target armv7-linux-androideabi --features oboe/fetch-prebuilt --verbose
|
||||
- name: Check feedback
|
||||
run: cargo check --example feedback --target armv7-linux-androideabi --features oboe/fetch-prebuilt --verbose
|
||||
- name: Check record_wav
|
||||
run: cargo check --example record_wav --target armv7-linux-androideabi --features oboe/fetch-prebuilt --verbose
|
||||
|
||||
android-apk-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install stable (Android targets)
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: armv7-linux-androideabi,aarch64-linux-android,i686-linux-android,x86_64-linux-android
|
||||
- name: Set Up Android tools
|
||||
run: |
|
||||
${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "platforms;android-30"
|
||||
- name: Install Cargo APK
|
||||
run: cargo install cargo-apk
|
||||
- name: Build APK
|
||||
run: cargo apk build --example android
|
||||
|
||||
ios-build:
|
||||
runs-on: macOS-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install llvm and clang
|
||||
run: brew install llvm
|
||||
- name: Install stable (iOS targets)
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-apple-ios,x86_64-apple-ios
|
||||
- name: Install cargo lipo
|
||||
run: cargo install cargo-lipo
|
||||
- name: Build iphonesimulator feedback example
|
||||
run: cd examples/ios-feedback && xcodebuild -scheme cpal-ios-example -configuration Debug -derivedDataPath build -sdk iphonesimulator
|
||||
|
||||
wasm-beep-build:
|
||||
# this only confirms that the Rust source builds
|
||||
# and checks to prevent regressions like #721.
|
||||
#
|
||||
# It does not test the javascript/web integration
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install stable (wasm32 target)
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
- name: Cargo Build
|
||||
working-directory: ./examples/wasm-beep
|
||||
run: cargo build --target wasm32-unknown-unknown
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
/target
|
||||
/Cargo.lock
|
||||
.cargo/
|
||||
.DS_Store
|
||||
recorded.wav
|
||||
rls*.log
|
||||
Vendored
-196
@@ -1,196 +0,0 @@
|
||||
# Unreleased
|
||||
|
||||
# Version 0.15.3 (2024-03-04)
|
||||
|
||||
- Add `try_with_sample_rate`, a non-panicking variant of `with_sample_rate`.
|
||||
- struct `platform::Stream` is now #[must_use].
|
||||
- enum `SupportedBufferSize` and struct `SupportedStreamConfigRange` are now `Copy`.
|
||||
- `platform::Device` is now `Clone`.
|
||||
- Remove `parking_lot` dependency in favor of the std library.
|
||||
- Fix crash on web/wasm when `atomics` flag is enabled.
|
||||
- Improve Examples: Migrate wasm example to `trunk`, Improve syth-thones example.
|
||||
- Improve CI: Update actions, Use Android 30 API level in CI, Remove `asmjs-unknown-emscripten` target.
|
||||
- Update `windows` dependency to v0.54
|
||||
- Update `jni` dependency to 0.21
|
||||
- Update `alsa` dependency to 0.9
|
||||
- Update `oboe` dependency to 0.6
|
||||
- Update `ndk` dependency to 0.8 and disable `default-features`.
|
||||
- Update `wasm-bindgen` to 0.2.89
|
||||
|
||||
# Version 0.15.2 (2023-03-30)
|
||||
|
||||
- webaudio: support multichannel output streams
|
||||
- Update `windows` dependency
|
||||
- wasapi: fix some thread panics
|
||||
|
||||
# Version 0.15.1 (2023-03-14)
|
||||
|
||||
- Add feature `oboe-shared-stdcxx` to enable `shared-stdcxx` on `oboe` for Android support
|
||||
- Remove `thiserror` dependency
|
||||
- Swith `mach` dependency to `mach2`
|
||||
|
||||
# Version 0.15.0 (2023-01-29)
|
||||
|
||||
- Update `windows-rs`, `jack`, `coreaudio-sys`, `oboe`, `alsa` dependencies
|
||||
- Switch to the `dasp_sample` crate for the sample trait
|
||||
- Switch to `web-sys` on the emscripten target
|
||||
- Adopt edition 2021
|
||||
- Add disconnection detection on Mac OS
|
||||
|
||||
# Version 0.14.1 (2022-10-23)
|
||||
|
||||
- Support the 0.6.1 release of `alsa-rs`
|
||||
- Fix `asio` feature broken in 0.14.0
|
||||
- `NetBSD` support
|
||||
- CI improvements
|
||||
|
||||
# Version 0.14.0 (2022-08-22)
|
||||
|
||||
- Switch to `windows-rs` crate
|
||||
- Turn `ndk-glue` into a dev-dependency and use `ndk-context` instead
|
||||
- Update dependencies (ndk, ndk-glue, parking_lot, once_cell, jack)
|
||||
|
||||
# Version 0.13.5 (2022-01-28)
|
||||
|
||||
- Faster sample format conversion
|
||||
- Update dependencies (ndk, oboe, ndk-glue, jack, alsa, nix)
|
||||
|
||||
# Version 0.13.4 (2021-08-08)
|
||||
|
||||
- wasapi: Allow both threading models and switch the default to STA
|
||||
- Update dependencies (core-foundation-sys, jni, rust-jack)
|
||||
- Alsa: improve stream setup parameters
|
||||
|
||||
# Version 0.13.3 (2021-03-29)
|
||||
|
||||
- Give each thread a unique name
|
||||
- Fix distortion regression on some alsa configs
|
||||
|
||||
# Version 0.13.2 (2021-03-16)
|
||||
|
||||
- Update dependencies (ndk, nix, oboe, jni, etc)
|
||||
|
||||
# Version 0.13.1 (2020-11-08)
|
||||
|
||||
- Don't panic when device is plugged out on Windows
|
||||
- Update `parking_lot` dependency
|
||||
|
||||
# Version 0.13.0 (2020-10-28)
|
||||
|
||||
- Add Android support via `oboe-rs`.
|
||||
- Add Android APK build an CI job.
|
||||
|
||||
# Version 0.12.1 (2020-07-23)
|
||||
|
||||
- Bugfix release to get the asio feature working again.
|
||||
|
||||
# Version 0.12.0 (2020-07-09)
|
||||
|
||||
- Large refactor removing the blocking EventLoop API.
|
||||
- Rename many `Format` types to `StreamConfig`:
|
||||
- `Format` type's `data_type` field renamed to `sample_format`.
|
||||
- `Shape` -> `StreamConfig` - The configuration input required to build a stream.
|
||||
- `Format` -> `SupportedStreamConfig` - Describes a single supported stream configuration.
|
||||
- `SupportedFormat` -> `SupportedStreamConfigRange` - Describes a range of supported configurations.
|
||||
- `Device::default_input/output_format` -> `Device::default_input/output_config`.
|
||||
- `Device::supported_input/output_formats` -> `Device::supported_input/output_configs`.
|
||||
- `Device::SupportedInput/OutputFormats` -> `Device::SupportedInput/OutputConfigs`.
|
||||
- `SupportedFormatsError` -> `SupportedStreamConfigsError`
|
||||
- `DefaultFormatError` -> `DefaultStreamConfigError`
|
||||
- `BuildStreamError::FormatNotSupported` -> `BuildStreamError::StreamConfigNotSupported`
|
||||
- Address deprecated use of `mem::uninitialized` in WASAPI.
|
||||
- Removed `UnknownTypeBuffer` in favour of specifying sample type.
|
||||
- Added `build_input/output_stream_raw` methods allowing for dynamically
|
||||
handling sample format type.
|
||||
- Added support for DragonFly platform.
|
||||
- Add `InputCallbackInfo` and `OutputCallbackInfo` types and update expected
|
||||
user data callback function signature to provide these.
|
||||
|
||||
# Version 0.11.0 (2019-12-11)
|
||||
|
||||
- Fix some underruns that could occur in ALSA.
|
||||
- Add name to `HostId`.
|
||||
- Use `snd_pcm_hw_params_set_buffer_time_near` rather than `set_buffer_time_max`
|
||||
in ALSA backend.
|
||||
- Remove many uses of `std::mem::uninitialized`.
|
||||
- Fix WASAPI capture logic.
|
||||
- Panic on stream ID overflow rather than returning an error.
|
||||
- Use `ringbuffer` crate in feedback example.
|
||||
- Move errors into a separate module.
|
||||
- Switch from `failure` to `thiserror` for error handling.
|
||||
- Add `winbase` winapi feature to solve windows compile error issues.
|
||||
- Lots of CI improvements.
|
||||
|
||||
# Version 0.10.0 (2019-07-05)
|
||||
|
||||
- core-foundation-sys and coreaudio-rs version bumps.
|
||||
- Add an ASIO host, available under Windows.
|
||||
- Introduce a new Host API, adding support for alternative audio APIs.
|
||||
- Remove sleep loop on macOS in favour of using a `Condvar`.
|
||||
- Allow users to handle stream callback errors with a new `StreamEvent` type.
|
||||
- Overhaul error handling throughout the crate.
|
||||
- Remove unnecessary Mutex from ALSA and WASAPI backends in favour of channels.
|
||||
- Remove `panic!` from OutputBuffer Deref impl as it is no longer necessary.
|
||||
|
||||
# Version 0.9.0 (2019-06-06)
|
||||
|
||||
- Better buffer handling
|
||||
- Fix logic error in frame/sample size
|
||||
- Added error handling for unknown ALSA device errors
|
||||
- Fix resuming a paused stream on Windows (wasapi).
|
||||
- Implement `default_output_format` for emscripten backend.
|
||||
|
||||
# Version 0.8.1 (2018-03-18)
|
||||
|
||||
- Fix the handling of non-default sample rates for coreaudio input streams.
|
||||
|
||||
# Version 0.8.0 (2018-02-15)
|
||||
|
||||
- Add `record_wav.rs` example. Records 3 seconds to
|
||||
`$CARGO_MANIFEST_DIR/recorded.wav` using default input device.
|
||||
- Update `enumerate.rs` example to display default input/output devices and
|
||||
formats.
|
||||
- Add input stream support to coreaudio, alsa and windows backends.
|
||||
- Introduce `StreamData` type for handling either input or output streams in
|
||||
`EventLoop::run` callback.
|
||||
- Add `Device::supported_{input/output}_formats` methods.
|
||||
- Add `Device::default_{input/output}_format` methods.
|
||||
- Add `default_{input/output}_device` functions.
|
||||
- Replace usage of `Voice` with `Stream` throughout the crate.
|
||||
- Remove `Endpoint` in favour of `Device` for supporting both input and output
|
||||
streams.
|
||||
|
||||
# Version 0.7.0 (2018-02-04)
|
||||
|
||||
- Rename `ChannelsCount` to `ChannelCount`.
|
||||
- Rename `SamplesRate` to `SampleRate`.
|
||||
- Rename the `min_samples_rate` field of `SupportedFormat` to `min_sample_rate`
|
||||
- Rename the `with_max_samples_rate()` method of`SupportedFormat` to `with_max_sample_rate()`
|
||||
- Rename the `samples_rate` field of `Format` to `sample_rate`
|
||||
- Changed the type of the `channels` field of the `SupportedFormat` struct from `Vec<ChannelPosition>` to `ChannelCount` (an alias to `u16`)
|
||||
- Remove unused ChannelPosition API.
|
||||
- Implement `Endpoint` and `Format` Enumeration for macOS.
|
||||
- Implement format handling for macos `build_voice` method.
|
||||
|
||||
# Version 0.6.0 (2017-12-11)
|
||||
|
||||
- Changed the emscripten backend to consume less CPU.
|
||||
- Added improvements to the crate documentation.
|
||||
- Implement `pause` and `play` for ALSA backend.
|
||||
- Reduced the number of allocations in the CoreAudio backend.
|
||||
- Fixes for macOS build (#186, #189).
|
||||
|
||||
# Version 0.5.1 (2017-10-21)
|
||||
|
||||
- Added `Sample::to_i16()`, `Sample::to_u16()` and `Sample::from`.
|
||||
|
||||
# Version 0.5.0 (2017-10-21)
|
||||
|
||||
- Removed the dependency on the `futures` library.
|
||||
- Removed the `Voice` and `SamplesStream` types.
|
||||
- Added `EventLoop::build_voice`, `EventLoop::destroy_voice`, `EventLoop::play`,
|
||||
and `EventLoop::pause` that can be used to create, destroy, play and pause voices.
|
||||
- Added a `VoiceId` struct that is now used to identify a voice owned by an `EventLoop`.
|
||||
- Changed `EventLoop::run()` to take a callback that is called whenever a voice requires sound data.
|
||||
- Changed `supported_formats()` to produce a list of `SupportedFormat` instead of `Format`. A
|
||||
`SupportedFormat` must then be turned into a `Format` in order to build a voice.
|
||||
Vendored
-184
@@ -1,184 +0,0 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
name = "cpal"
|
||||
version = "0.15.3"
|
||||
description = "Low-level cross-platform audio I/O library in pure Rust."
|
||||
documentation = "https://docs.rs/cpal"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"audio",
|
||||
"sound",
|
||||
]
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/rustaudio/cpal"
|
||||
|
||||
[[example]]
|
||||
name = "android"
|
||||
crate-type = ["cdylib"]
|
||||
path = "examples/android.rs"
|
||||
|
||||
[[example]]
|
||||
name = "beep"
|
||||
|
||||
[[example]]
|
||||
name = "enumerate"
|
||||
|
||||
[[example]]
|
||||
name = "feedback"
|
||||
|
||||
[[example]]
|
||||
name = "record_wav"
|
||||
|
||||
[[example]]
|
||||
name = "synth_tones"
|
||||
|
||||
[dependencies.dasp_sample]
|
||||
version = "0.11"
|
||||
|
||||
[dev-dependencies.anyhow]
|
||||
version = "1.0"
|
||||
|
||||
[dev-dependencies.clap]
|
||||
version = "4.0"
|
||||
features = ["derive"]
|
||||
|
||||
[dev-dependencies.hound]
|
||||
version = "3.5"
|
||||
|
||||
[dev-dependencies.ringbuf]
|
||||
version = "0.3"
|
||||
|
||||
[features]
|
||||
asio = [
|
||||
"asio-sys",
|
||||
"num-traits",
|
||||
]
|
||||
oboe-shared-stdcxx = ["oboe/shared-stdcxx"]
|
||||
|
||||
[target."cfg(all(target_arch = \"wasm32\", target_os = \"unknown\"))".dependencies.js-sys]
|
||||
version = "0.3.35"
|
||||
|
||||
[target."cfg(all(target_arch = \"wasm32\", target_os = \"unknown\"))".dependencies.wasm-bindgen]
|
||||
version = "0.2.58"
|
||||
optional = true
|
||||
|
||||
[target."cfg(all(target_arch = \"wasm32\", target_os = \"unknown\"))".dependencies.web-sys]
|
||||
version = "0.3.35"
|
||||
features = [
|
||||
"AudioContext",
|
||||
"AudioContextOptions",
|
||||
"AudioBuffer",
|
||||
"AudioBufferSourceNode",
|
||||
"AudioNode",
|
||||
"AudioDestinationNode",
|
||||
"Window",
|
||||
"AudioContextState",
|
||||
]
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\"))".dependencies.alsa]
|
||||
version = "0.9"
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\"))".dependencies.jack]
|
||||
version = "0.11"
|
||||
optional = true
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\"))".dependencies.libc]
|
||||
version = "0.2"
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", target_os = \"ios\"))".dependencies.core-foundation-sys]
|
||||
version = "0.8.2"
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", target_os = \"ios\"))".dependencies.mach2]
|
||||
version = "0.4"
|
||||
|
||||
[target."cfg(target_os = \"android\")".dependencies.jni]
|
||||
version = "0.21"
|
||||
|
||||
[target."cfg(target_os = \"android\")".dependencies.ndk]
|
||||
version = "0.8"
|
||||
default-features = false
|
||||
|
||||
[target."cfg(target_os = \"android\")".dependencies.ndk-context]
|
||||
version = "0.1"
|
||||
|
||||
[target."cfg(target_os = \"android\")".dependencies.oboe]
|
||||
version = "0.6"
|
||||
features = ["java-interface"]
|
||||
|
||||
[target."cfg(target_os = \"android\")".dev-dependencies.ndk-glue]
|
||||
version = "0.7"
|
||||
|
||||
[target."cfg(target_os = \"emscripten\")".dependencies.js-sys]
|
||||
version = "0.3.35"
|
||||
|
||||
[target."cfg(target_os = \"emscripten\")".dependencies.wasm-bindgen]
|
||||
version = "0.2.89"
|
||||
|
||||
[target."cfg(target_os = \"emscripten\")".dependencies.wasm-bindgen-futures]
|
||||
version = "0.4.33"
|
||||
|
||||
[target."cfg(target_os = \"emscripten\")".dependencies.web-sys]
|
||||
version = "0.3.35"
|
||||
features = [
|
||||
"AudioContext",
|
||||
"AudioContextOptions",
|
||||
"AudioBuffer",
|
||||
"AudioBufferSourceNode",
|
||||
"AudioNode",
|
||||
"AudioDestinationNode",
|
||||
"Window",
|
||||
"AudioContextState",
|
||||
]
|
||||
|
||||
[target."cfg(target_os = \"ios\")".dependencies.coreaudio-rs]
|
||||
version = "0.11"
|
||||
features = [
|
||||
"audio_unit",
|
||||
"core_audio",
|
||||
"audio_toolbox",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies.coreaudio-rs]
|
||||
version = "0.11"
|
||||
features = [
|
||||
"audio_unit",
|
||||
"core_audio",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies.asio-sys]
|
||||
version = "0.2"
|
||||
optional = true
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies.num-traits]
|
||||
version = "0.2.6"
|
||||
optional = true
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies.windows]
|
||||
version = "0.54.0"
|
||||
features = [
|
||||
"Win32_Media_Audio",
|
||||
"Win32_Foundation",
|
||||
"Win32_Devices_Properties",
|
||||
"Win32_Media_KernelStreaming",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Threading",
|
||||
"Win32_Security",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Variant",
|
||||
"Win32_Media_Multimedia",
|
||||
"Win32_UI_Shell_PropertiesSystem",
|
||||
]
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
[package]
|
||||
name = "cpal"
|
||||
version = "0.15.3"
|
||||
description = "Low-level cross-platform audio I/O library in pure Rust."
|
||||
repository = "https://github.com/rustaudio/cpal"
|
||||
documentation = "https://docs.rs/cpal"
|
||||
license = "Apache-2.0"
|
||||
keywords = ["audio", "sound"]
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
|
||||
[features]
|
||||
asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions.
|
||||
oboe-shared-stdcxx = ["oboe/shared-stdcxx"] # Only available on Android. See README for what it does.
|
||||
|
||||
[dependencies]
|
||||
dasp_sample = "0.11"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0"
|
||||
hound = "3.5"
|
||||
ringbuf = "0.3"
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dev-dependencies]
|
||||
ndk-glue = "0.7"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { version = "0.54.0", features = [
|
||||
"Win32_Media_Audio",
|
||||
"Win32_Foundation",
|
||||
"Win32_Devices_Properties",
|
||||
"Win32_Media_KernelStreaming",
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_Threading",
|
||||
"Win32_Security",
|
||||
"Win32_System_SystemServices",
|
||||
"Win32_System_Variant",
|
||||
"Win32_Media_Multimedia",
|
||||
"Win32_UI_Shell_PropertiesSystem"
|
||||
]}
|
||||
asio-sys = { version = "0.2", path = "asio-sys", optional = true }
|
||||
num-traits = { version = "0.2.6", optional = true }
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies]
|
||||
alsa = "0.9"
|
||||
libc = "0.2"
|
||||
jack = { version = "0.11", optional = true }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
||||
core-foundation-sys = "0.8.2" # For linking to CoreFoundation.framework and handling device name `CFString`s.
|
||||
mach2 = "0.4" # For access to mach_timebase type.
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
coreaudio-rs = { version = "0.11", default-features = false, features = ["audio_unit", "core_audio"] }
|
||||
|
||||
[target.'cfg(target_os = "ios")'.dependencies]
|
||||
coreaudio-rs = { version = "0.11", default-features = false, features = ["audio_unit", "core_audio", "audio_toolbox"] }
|
||||
|
||||
[target.'cfg(target_os = "emscripten")'.dependencies]
|
||||
wasm-bindgen = { version = "0.2.89" }
|
||||
wasm-bindgen-futures = "0.4.33"
|
||||
js-sys = { version = "0.3.35" }
|
||||
web-sys = { version = "0.3.35", features = [ "AudioContext", "AudioContextOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioNode", "AudioDestinationNode", "Window", "AudioContextState"] }
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
wasm-bindgen = { version = "0.2.58", optional = true }
|
||||
js-sys = { version = "0.3.35" }
|
||||
web-sys = { version = "0.3.35", features = [ "AudioContext", "AudioContextOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioNode", "AudioDestinationNode", "Window", "AudioContextState"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
oboe = { version = "0.6", features = [ "java-interface" ] }
|
||||
ndk = { version = "0.8", default-features = false }
|
||||
ndk-context = "0.1"
|
||||
jni = "0.21"
|
||||
|
||||
[[example]]
|
||||
name = "android"
|
||||
path = "examples/android.rs"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[[example]]
|
||||
name = "beep"
|
||||
|
||||
[[example]]
|
||||
name = "enumerate"
|
||||
|
||||
[[example]]
|
||||
name = "feedback"
|
||||
|
||||
[[example]]
|
||||
name = "record_wav"
|
||||
|
||||
[[example]]
|
||||
name = "synth_tones"
|
||||
Vendored
-7
@@ -1,7 +0,0 @@
|
||||
[target.armv7-unknown-linux-gnueabihf]
|
||||
image = "cross/cpal_armv7:v1"
|
||||
|
||||
[target.armv7-unknown-linux-gnueabihf.env]
|
||||
passthrough = [
|
||||
"RUSTFLAGS",
|
||||
]
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
FROM rustembedded/cross:armv7-unknown-linux-gnueabihf
|
||||
|
||||
ENV PKG_CONFIG_ALLOW_CROSS 1
|
||||
ENV PKG_CONFIG_PATH /usr/lib/arm-linux-gnueabihf/pkgconfig/
|
||||
|
||||
RUN dpkg --add-architecture armhf && \
|
||||
apt-get update && \
|
||||
apt-get install libasound2-dev:armhf -y && \
|
||||
apt-get install libjack-jackd2-dev:armhf libjack-jackd2-0:armhf -y \
|
||||
Vendored
-201
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Vendored
-142
@@ -1,142 +0,0 @@
|
||||
# CPAL - Cross-Platform Audio Library
|
||||
|
||||
[](https://github.com/RustAudio/cpal/actions)
|
||||
[](https://crates.io/crates/cpal) [](https://docs.rs/cpal/)
|
||||
|
||||
Low-level library for audio input and output in pure Rust.
|
||||
|
||||
This library currently supports the following:
|
||||
|
||||
- Enumerate supported audio hosts.
|
||||
- Enumerate all available audio devices.
|
||||
- Get the current default input and output devices.
|
||||
- Enumerate known supported input and output stream formats for a device.
|
||||
- Get the current default input and output stream formats for a device.
|
||||
- Build and run input and output PCM streams on a chosen device with a given stream format.
|
||||
|
||||
Currently, supported hosts include:
|
||||
|
||||
- Linux (via ALSA or JACK)
|
||||
- Windows (via WASAPI by default, see ASIO instructions below)
|
||||
- macOS (via CoreAudio)
|
||||
- iOS (via CoreAudio)
|
||||
- Android (via Oboe)
|
||||
- Emscripten
|
||||
|
||||
Note that on Linux, the ALSA development files are required. These are provided
|
||||
as part of the `libasound2-dev` package on Debian and Ubuntu distributions and
|
||||
`alsa-lib-devel` on Fedora.
|
||||
|
||||
## Compiling for Web Assembly
|
||||
|
||||
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.
|
||||
|
||||
## Feature flags for audio backends
|
||||
|
||||
Some audio backends are optional and will only be compiled with a [feature flag](https://doc.rust-lang.org/cargo/reference/features.html).
|
||||
|
||||
- JACK (on Linux): `jack`
|
||||
- ASIO (on Windows): `asio`
|
||||
|
||||
Oboe can either use a shared or static runtime. The static runtime is used by default, but activating the
|
||||
`oboe-shared-stdcxx` feature makes it use the shared runtime, which requires `libc++_shared.so` from the Android NDK to
|
||||
be present during execution.
|
||||
|
||||
## ASIO on Windows
|
||||
|
||||
[ASIO](https://en.wikipedia.org/wiki/Audio_Stream_Input/Output) is an audio
|
||||
driver protocol by Steinberg. While it is available on multiple operating
|
||||
systems, it is most commonly used on Windows to work around limitations of
|
||||
WASAPI including access to large numbers of channels and lower-latency audio
|
||||
processing.
|
||||
|
||||
CPAL allows for using the ASIO SDK as the audio host on Windows instead of
|
||||
WASAPI.
|
||||
|
||||
### Locating the ASIO SDK
|
||||
|
||||
The location of ASIO SDK is exposed to CPAL by setting the `CPAL_ASIO_DIR` environment variable.
|
||||
|
||||
The build script will try to find the ASIO SDK by following these steps in order:
|
||||
|
||||
1. Check if `CPAL_ASIO_DIR` is set and if so use the path to point to the SDK.
|
||||
2. Check if the ASIO SDK is already installed in the temporary directory, if so use that and set the path of `CPAL_ASIO_DIR` to the output of `std::env::temp_dir().join("asio_sdk")`.
|
||||
3. If the ASIO SDK is not already installed, download it from <https://www.steinberg.net/asiosdk> and install it in the temporary directory. The path of `CPAL_ASIO_DIR` will be set to the output of `std::env::temp_dir().join("asio_sdk")`.
|
||||
|
||||
In an ideal situation you don't need to worry about this step.
|
||||
|
||||
### Preparing the build environment
|
||||
|
||||
1. `bindgen`, the library used to generate bindings to the C++ SDK, requires
|
||||
clang. **Download and install LLVM** from
|
||||
[here](http://releases.llvm.org/download.html) under the "Pre-Built Binaries"
|
||||
section. The version as of writing this is 17.0.1.
|
||||
2. Add the LLVM `bin` directory to a `LIBCLANG_PATH` environment variable. If
|
||||
you installed LLVM to the default directory, this should work in the command
|
||||
prompt:
|
||||
```
|
||||
setx LIBCLANG_PATH "C:\Program Files\LLVM\bin"
|
||||
```
|
||||
3. If you don't have any ASIO devices or drivers available, you can [**download
|
||||
and install ASIO4ALL**](http://www.asio4all.org/). Be sure to enable the
|
||||
"offline" feature during installation despite what the installer says about
|
||||
it being useless.
|
||||
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`
|
||||
and execute it with the right host and target machine architecture regardless of the Microsoft Visual Studio version.
|
||||
If there are any errors encountered in this process which is unlikely,
|
||||
you may find the `vcvarsall.bat` manually and execute it with your machine architecture as an argument.
|
||||
The script will detect this and skip the step.
|
||||
|
||||
A manually executed command example for 64 bit machines:
|
||||
|
||||
```
|
||||
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" amd64
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
5. Select the ASIO host at the start of our program with the following code:
|
||||
|
||||
```rust
|
||||
let host;
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
host = cpal::host_from_id(cpal::HostId::Asio).expect("failed to initialise ASIO host");
|
||||
}
|
||||
```
|
||||
|
||||
If you run into compilations errors produced by `asio-sys` or `bindgen`, make
|
||||
sure that `CPAL_ASIO_DIR` is set correctly and try `cargo clean`.
|
||||
|
||||
6. Make sure to enable the `asio` feature when building CPAL:
|
||||
|
||||
```
|
||||
cargo build --features "asio"
|
||||
```
|
||||
|
||||
or if you are using CPAL as a dependency in a downstream project, enable the
|
||||
feature like this:
|
||||
|
||||
```toml
|
||||
cpal = { version = "*", features = ["asio"] }
|
||||
```
|
||||
|
||||
_Updated as of ASIO version 2.3.3._
|
||||
|
||||
### Cross compilation
|
||||
|
||||
When Windows is the host and the target OS, the build script of `asio-sys` supports all cross compilation targets
|
||||
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)
|
||||
|
||||
It is also possible to compile Windows applications with ASIO support on Linux and macOS.
|
||||
|
||||
For both platforms the common way to do this is to use the [MinGW-w64](https://www.mingw-w64.org/) toolchain.
|
||||
|
||||
Make sure that you have included the `MinGW-w64` include directory in your `CPLUS_INCLUDE_PATH` environment variable.
|
||||
Make sure that LLVM is installed and include directory is also included in your `CPLUS_INCLUDE_PATH` environment variable.
|
||||
|
||||
Example for macOS for the target of `x86_64-pc-windows-gnu` where `mingw-w64` is installed via brew:
|
||||
|
||||
```
|
||||
export CPLUS_INCLUDE_PATH="$CPLUS_INCLUDE_PATH:/opt/homebrew/Cellar/mingw-w64/11.0.1/toolchain-x86_64/x86_64-w64-mingw32/include"
|
||||
```
|
||||
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
const CPAL_ASIO_DIR: &str = "CPAL_ASIO_DIR";
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-env-changed={}", CPAL_ASIO_DIR);
|
||||
|
||||
// If ASIO directory isn't set silently return early
|
||||
// otherwise set the asio config flag
|
||||
match env::var(CPAL_ASIO_DIR) {
|
||||
Err(_) => {}
|
||||
Ok(_) => println!("cargo:rustc-cfg=asio"),
|
||||
};
|
||||
}
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
extern crate anyhow;
|
||||
extern crate cpal;
|
||||
|
||||
use cpal::{
|
||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
SizedSample,
|
||||
};
|
||||
use cpal::{FromSample, Sample};
|
||||
|
||||
#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "full"))]
|
||||
fn main() {
|
||||
let host = cpal::default_host();
|
||||
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.expect("failed to find output device");
|
||||
|
||||
let config = device.default_output_config().unwrap();
|
||||
|
||||
match config.sample_format() {
|
||||
cpal::SampleFormat::I8 => run::<i8>(&device, &config.into()).unwrap(),
|
||||
cpal::SampleFormat::I16 => run::<i16>(&device, &config.into()).unwrap(),
|
||||
// cpal::SampleFormat::I24 => run::<I24>(&device, &config.into()).unwrap(),
|
||||
cpal::SampleFormat::I32 => run::<i32>(&device, &config.into()).unwrap(),
|
||||
// cpal::SampleFormat::I48 => run::<I48>(&device, &config.into()).unwrap(),
|
||||
cpal::SampleFormat::I64 => run::<i64>(&device, &config.into()).unwrap(),
|
||||
cpal::SampleFormat::U8 => run::<u8>(&device, &config.into()).unwrap(),
|
||||
cpal::SampleFormat::U16 => run::<u16>(&device, &config.into()).unwrap(),
|
||||
// cpal::SampleFormat::U24 => run::<U24>(&device, &config.into()).unwrap(),
|
||||
cpal::SampleFormat::U32 => run::<u32>(&device, &config.into()).unwrap(),
|
||||
// cpal::SampleFormat::U48 => run::<U48>(&device, &config.into()).unwrap(),
|
||||
cpal::SampleFormat::U64 => run::<u64>(&device, &config.into()).unwrap(),
|
||||
cpal::SampleFormat::F32 => run::<f32>(&device, &config.into()).unwrap(),
|
||||
cpal::SampleFormat::F64 => run::<f64>(&device, &config.into()).unwrap(),
|
||||
sample_format => panic!("Unsupported sample format '{sample_format}'"),
|
||||
}
|
||||
}
|
||||
|
||||
fn run<T>(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error>
|
||||
where
|
||||
T: SizedSample + FromSample<f32>,
|
||||
{
|
||||
let sample_rate = config.sample_rate.0 as f32;
|
||||
let channels = config.channels as usize;
|
||||
|
||||
// Produce a sinusoid of maximum amplitude.
|
||||
let mut sample_clock = 0f32;
|
||||
let mut next_value = move || {
|
||||
sample_clock = (sample_clock + 1.0) % sample_rate;
|
||||
(sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin()
|
||||
};
|
||||
|
||||
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
|
||||
|
||||
let stream = device.build_output_stream(
|
||||
config,
|
||||
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
|
||||
write_data(data, channels, &mut next_value)
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
)?;
|
||||
stream.play()?;
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32)
|
||||
where
|
||||
T: Sample + FromSample<f32>,
|
||||
{
|
||||
for frame in output.chunks_mut(channels) {
|
||||
let value: T = T::from_sample(next_sample());
|
||||
for sample in frame.iter_mut() {
|
||||
*sample = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
use clap::Parser;
|
||||
use cpal::{
|
||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
FromSample, Sample, SizedSample,
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about = "CPAL beep example", long_about = None)]
|
||||
struct Opt {
|
||||
/// The audio device to use
|
||||
#[arg(short, long, default_value_t = String::from("default"))]
|
||||
device: String,
|
||||
|
||||
/// Use the JACK host
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
),
|
||||
feature = "jack"
|
||||
))]
|
||||
#[arg(short, long)]
|
||||
#[allow(dead_code)]
|
||||
jack: bool,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let opt = Opt::parse();
|
||||
|
||||
// Conditionally compile with jack if the feature is specified.
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
),
|
||||
feature = "jack"
|
||||
))]
|
||||
// Manually check for flags. Can be passed through cargo with -- e.g.
|
||||
// cargo run --release --example beep --features jack -- --jack
|
||||
let host = if opt.jack {
|
||||
cpal::host_from_id(cpal::available_hosts()
|
||||
.into_iter()
|
||||
.find(|id| *id == cpal::HostId::Jack)
|
||||
.expect(
|
||||
"make sure --features jack is specified. only works on OSes where jack is available",
|
||||
)).expect("jack host unavailable")
|
||||
} else {
|
||||
cpal::default_host()
|
||||
};
|
||||
|
||||
#[cfg(any(
|
||||
not(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
)),
|
||||
not(feature = "jack")
|
||||
))]
|
||||
let host = cpal::default_host();
|
||||
|
||||
let device = if opt.device == "default" {
|
||||
host.default_output_device()
|
||||
} else {
|
||||
host.output_devices()?
|
||||
.find(|x| x.name().map(|y| y == opt.device).unwrap_or(false))
|
||||
}
|
||||
.expect("failed to find output device");
|
||||
println!("Output device: {}", device.name()?);
|
||||
|
||||
let config = device.default_output_config().unwrap();
|
||||
println!("Default output config: {:?}", config);
|
||||
|
||||
match config.sample_format() {
|
||||
cpal::SampleFormat::I8 => run::<i8>(&device, &config.into()),
|
||||
cpal::SampleFormat::I16 => run::<i16>(&device, &config.into()),
|
||||
// cpal::SampleFormat::I24 => run::<I24>(&device, &config.into()),
|
||||
cpal::SampleFormat::I32 => run::<i32>(&device, &config.into()),
|
||||
// cpal::SampleFormat::I48 => run::<I48>(&device, &config.into()),
|
||||
cpal::SampleFormat::I64 => run::<i64>(&device, &config.into()),
|
||||
cpal::SampleFormat::U8 => run::<u8>(&device, &config.into()),
|
||||
cpal::SampleFormat::U16 => run::<u16>(&device, &config.into()),
|
||||
// cpal::SampleFormat::U24 => run::<U24>(&device, &config.into()),
|
||||
cpal::SampleFormat::U32 => run::<u32>(&device, &config.into()),
|
||||
// cpal::SampleFormat::U48 => run::<U48>(&device, &config.into()),
|
||||
cpal::SampleFormat::U64 => run::<u64>(&device, &config.into()),
|
||||
cpal::SampleFormat::F32 => run::<f32>(&device, &config.into()),
|
||||
cpal::SampleFormat::F64 => run::<f64>(&device, &config.into()),
|
||||
sample_format => panic!("Unsupported sample format '{sample_format}'"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run<T>(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error>
|
||||
where
|
||||
T: SizedSample + FromSample<f32>,
|
||||
{
|
||||
let sample_rate = config.sample_rate.0 as f32;
|
||||
let channels = config.channels as usize;
|
||||
|
||||
// Produce a sinusoid of maximum amplitude.
|
||||
let mut sample_clock = 0f32;
|
||||
let mut next_value = move || {
|
||||
sample_clock = (sample_clock + 1.0) % sample_rate;
|
||||
(sample_clock * 440.0 * 2.0 * std::f32::consts::PI / sample_rate).sin()
|
||||
};
|
||||
|
||||
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
|
||||
|
||||
let stream = device.build_output_stream(
|
||||
config,
|
||||
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
|
||||
write_data(data, channels, &mut next_value)
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
)?;
|
||||
stream.play()?;
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32)
|
||||
where
|
||||
T: Sample + FromSample<f32>,
|
||||
{
|
||||
for frame in output.chunks_mut(channels) {
|
||||
let value: T = T::from_sample(next_sample());
|
||||
for sample in frame.iter_mut() {
|
||||
*sample = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
extern crate anyhow;
|
||||
extern crate cpal;
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS);
|
||||
let available_hosts = cpal::available_hosts();
|
||||
println!("Available hosts:\n {:?}", available_hosts);
|
||||
|
||||
for host_id in available_hosts {
|
||||
println!("{}", host_id.name());
|
||||
let host = cpal::host_from_id(host_id)?;
|
||||
|
||||
let default_in = host.default_input_device().map(|e| e.name().unwrap());
|
||||
let default_out = host.default_output_device().map(|e| e.name().unwrap());
|
||||
println!(" Default Input Device:\n {:?}", default_in);
|
||||
println!(" Default Output Device:\n {:?}", default_out);
|
||||
|
||||
let devices = host.devices()?;
|
||||
println!(" Devices: ");
|
||||
for (device_index, device) in devices.enumerate() {
|
||||
println!(" {}. \"{}\"", device_index + 1, device.name()?);
|
||||
|
||||
// Input configs
|
||||
if let Ok(conf) = device.default_input_config() {
|
||||
println!(" Default input stream config:\n {:?}", conf);
|
||||
}
|
||||
let input_configs = match device.supported_input_configs() {
|
||||
Ok(f) => f.collect(),
|
||||
Err(e) => {
|
||||
println!(" Error getting supported input configs: {:?}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
if !input_configs.is_empty() {
|
||||
println!(" All supported input stream configs:");
|
||||
for (config_index, config) in input_configs.into_iter().enumerate() {
|
||||
println!(
|
||||
" {}.{}. {:?}",
|
||||
device_index + 1,
|
||||
config_index + 1,
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Output configs
|
||||
if let Ok(conf) = device.default_output_config() {
|
||||
println!(" Default output stream config:\n {:?}", conf);
|
||||
}
|
||||
let output_configs = match device.supported_output_configs() {
|
||||
Ok(f) => f.collect(),
|
||||
Err(e) => {
|
||||
println!(" Error getting supported output configs: {:?}", e);
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
if !output_configs.is_empty() {
|
||||
println!(" All supported output stream configs:");
|
||||
for (config_index, config) in output_configs.into_iter().enumerate() {
|
||||
println!(
|
||||
" {}.{}. {:?}",
|
||||
device_index + 1,
|
||||
config_index + 1,
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
-174
@@ -1,174 +0,0 @@
|
||||
//! Feeds back the input stream directly into the output stream.
|
||||
//!
|
||||
//! Assumes that the input and output devices can use the same stream configuration and that they
|
||||
//! support the f32 sample format.
|
||||
//!
|
||||
//! Uses a delay of `LATENCY_MS` milliseconds in case the default input and output streams are not
|
||||
//! precisely synchronised.
|
||||
|
||||
use clap::Parser;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use ringbuf::HeapRb;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about = "CPAL feedback example", long_about = None)]
|
||||
struct Opt {
|
||||
/// The input audio device to use
|
||||
#[arg(short, long, value_name = "IN", default_value_t = String::from("default"))]
|
||||
input_device: String,
|
||||
|
||||
/// The output audio device to use
|
||||
#[arg(short, long, value_name = "OUT", default_value_t = String::from("default"))]
|
||||
output_device: String,
|
||||
|
||||
/// Specify the delay between input and output
|
||||
#[arg(short, long, value_name = "DELAY_MS", default_value_t = 150.0)]
|
||||
latency: f32,
|
||||
|
||||
/// Use the JACK host
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
),
|
||||
feature = "jack"
|
||||
))]
|
||||
#[arg(short, long)]
|
||||
#[allow(dead_code)]
|
||||
jack: bool,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let opt = Opt::parse();
|
||||
|
||||
// Conditionally compile with jack if the feature is specified.
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
),
|
||||
feature = "jack"
|
||||
))]
|
||||
// Manually check for flags. Can be passed through cargo with -- e.g.
|
||||
// cargo run --release --example beep --features jack -- --jack
|
||||
let host = if opt.jack {
|
||||
cpal::host_from_id(cpal::available_hosts()
|
||||
.into_iter()
|
||||
.find(|id| *id == cpal::HostId::Jack)
|
||||
.expect(
|
||||
"make sure --features jack is specified. only works on OSes where jack is available",
|
||||
)).expect("jack host unavailable")
|
||||
} else {
|
||||
cpal::default_host()
|
||||
};
|
||||
|
||||
#[cfg(any(
|
||||
not(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
)),
|
||||
not(feature = "jack")
|
||||
))]
|
||||
let host = cpal::default_host();
|
||||
|
||||
// Find devices.
|
||||
let input_device = if opt.input_device == "default" {
|
||||
host.default_input_device()
|
||||
} else {
|
||||
host.input_devices()?
|
||||
.find(|x| x.name().map(|y| y == opt.input_device).unwrap_or(false))
|
||||
}
|
||||
.expect("failed to find input device");
|
||||
|
||||
let output_device = if opt.output_device == "default" {
|
||||
host.default_output_device()
|
||||
} else {
|
||||
host.output_devices()?
|
||||
.find(|x| x.name().map(|y| y == opt.output_device).unwrap_or(false))
|
||||
}
|
||||
.expect("failed to find output device");
|
||||
|
||||
println!("Using input device: \"{}\"", input_device.name()?);
|
||||
println!("Using output device: \"{}\"", output_device.name()?);
|
||||
|
||||
// We'll try and use the same configuration between streams to keep it simple.
|
||||
let config: cpal::StreamConfig = input_device.default_input_config()?.into();
|
||||
|
||||
// Create a delay in case the input and output devices aren't synced.
|
||||
let latency_frames = (opt.latency / 1_000.0) * config.sample_rate.0 as f32;
|
||||
let latency_samples = latency_frames as usize * config.channels as usize;
|
||||
|
||||
// The buffer to share samples
|
||||
let ring = HeapRb::<f32>::new(latency_samples * 2);
|
||||
let (mut producer, mut consumer) = ring.split();
|
||||
|
||||
// Fill the samples with 0.0 equal to the length of the delay.
|
||||
for _ in 0..latency_samples {
|
||||
// The ring buffer has twice as much space as necessary to add latency here,
|
||||
// so this should never fail
|
||||
producer.push(0.0).unwrap();
|
||||
}
|
||||
|
||||
let input_data_fn = move |data: &[f32], _: &cpal::InputCallbackInfo| {
|
||||
let mut output_fell_behind = false;
|
||||
for &sample in data {
|
||||
if producer.push(sample).is_err() {
|
||||
output_fell_behind = true;
|
||||
}
|
||||
}
|
||||
if output_fell_behind {
|
||||
eprintln!("output stream fell behind: try increasing latency");
|
||||
}
|
||||
};
|
||||
|
||||
let output_data_fn = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||
let mut input_fell_behind = false;
|
||||
for sample in data {
|
||||
*sample = match consumer.pop() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
input_fell_behind = true;
|
||||
0.0
|
||||
}
|
||||
};
|
||||
}
|
||||
if input_fell_behind {
|
||||
eprintln!("input stream fell behind: try increasing latency");
|
||||
}
|
||||
};
|
||||
|
||||
// Build streams.
|
||||
println!(
|
||||
"Attempting to build both streams with f32 samples and `{:?}`.",
|
||||
config
|
||||
);
|
||||
let input_stream = input_device.build_input_stream(&config, input_data_fn, err_fn, None)?;
|
||||
let output_stream = output_device.build_output_stream(&config, output_data_fn, err_fn, None)?;
|
||||
println!("Successfully built streams.");
|
||||
|
||||
// Play the streams.
|
||||
println!(
|
||||
"Starting the input and output streams with `{}` milliseconds of latency.",
|
||||
opt.latency
|
||||
);
|
||||
input_stream.play()?;
|
||||
output_stream.play()?;
|
||||
|
||||
// Run for 3 seconds before closing.
|
||||
println!("Playing for 3 seconds... ");
|
||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||
drop(input_stream);
|
||||
drop(output_stream);
|
||||
println!("Done!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn err_fn(err: cpal::StreamError) {
|
||||
eprintln!("an error occurred on stream: {}", err);
|
||||
}
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
//! Records a WAV file (roughly 3 seconds long) using the default input device and config.
|
||||
//!
|
||||
//! The input data is recorded to "$CARGO_MANIFEST_DIR/recorded.wav".
|
||||
|
||||
use clap::Parser;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::{FromSample, Sample};
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about = "CPAL record_wav example", long_about = None)]
|
||||
struct Opt {
|
||||
/// The audio device to use
|
||||
#[arg(short, long, default_value_t = String::from("default"))]
|
||||
device: String,
|
||||
|
||||
/// Use the JACK host
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
),
|
||||
feature = "jack"
|
||||
))]
|
||||
#[arg(short, long)]
|
||||
#[allow(dead_code)]
|
||||
jack: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let opt = Opt::parse();
|
||||
|
||||
// Conditionally compile with jack if the feature is specified.
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
),
|
||||
feature = "jack"
|
||||
))]
|
||||
// Manually check for flags. Can be passed through cargo with -- e.g.
|
||||
// cargo run --release --example beep --features jack -- --jack
|
||||
let host = if opt.jack {
|
||||
cpal::host_from_id(cpal::available_hosts()
|
||||
.into_iter()
|
||||
.find(|id| *id == cpal::HostId::Jack)
|
||||
.expect(
|
||||
"make sure --features jack is specified. only works on OSes where jack is available",
|
||||
)).expect("jack host unavailable")
|
||||
} else {
|
||||
cpal::default_host()
|
||||
};
|
||||
|
||||
#[cfg(any(
|
||||
not(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
)),
|
||||
not(feature = "jack")
|
||||
))]
|
||||
let host = cpal::default_host();
|
||||
|
||||
// Set up the input device and stream with the default input config.
|
||||
let device = if opt.device == "default" {
|
||||
host.default_input_device()
|
||||
} else {
|
||||
host.input_devices()?
|
||||
.find(|x| x.name().map(|y| y == opt.device).unwrap_or(false))
|
||||
}
|
||||
.expect("failed to find input device");
|
||||
|
||||
println!("Input device: {}", device.name()?);
|
||||
|
||||
let config = device
|
||||
.default_input_config()
|
||||
.expect("Failed to get default input config");
|
||||
println!("Default input config: {:?}", config);
|
||||
|
||||
// The WAV file we're recording to.
|
||||
const PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/recorded.wav");
|
||||
let spec = wav_spec_from_config(&config);
|
||||
let writer = hound::WavWriter::create(PATH, spec)?;
|
||||
let writer = Arc::new(Mutex::new(Some(writer)));
|
||||
|
||||
// A flag to indicate that recording is in progress.
|
||||
println!("Begin recording...");
|
||||
|
||||
// Run the input stream on a separate thread.
|
||||
let writer_2 = writer.clone();
|
||||
|
||||
let err_fn = move |err| {
|
||||
eprintln!("an error occurred on stream: {}", err);
|
||||
};
|
||||
|
||||
let stream = match config.sample_format() {
|
||||
cpal::SampleFormat::I8 => device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data, _: &_| write_input_data::<i8, i8>(data, &writer_2),
|
||||
err_fn,
|
||||
None,
|
||||
)?,
|
||||
cpal::SampleFormat::I16 => device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data, _: &_| write_input_data::<i16, i16>(data, &writer_2),
|
||||
err_fn,
|
||||
None,
|
||||
)?,
|
||||
cpal::SampleFormat::I32 => device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data, _: &_| write_input_data::<i32, i32>(data, &writer_2),
|
||||
err_fn,
|
||||
None,
|
||||
)?,
|
||||
cpal::SampleFormat::F32 => device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data, _: &_| write_input_data::<f32, f32>(data, &writer_2),
|
||||
err_fn,
|
||||
None,
|
||||
)?,
|
||||
sample_format => {
|
||||
return Err(anyhow::Error::msg(format!(
|
||||
"Unsupported sample format '{sample_format}'"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
stream.play()?;
|
||||
|
||||
// Let recording go for roughly three seconds.
|
||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||
drop(stream);
|
||||
writer.lock().unwrap().take().unwrap().finalize()?;
|
||||
println!("Recording {} complete!", PATH);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sample_format(format: cpal::SampleFormat) -> hound::SampleFormat {
|
||||
if format.is_float() {
|
||||
hound::SampleFormat::Float
|
||||
} else {
|
||||
hound::SampleFormat::Int
|
||||
}
|
||||
}
|
||||
|
||||
fn wav_spec_from_config(config: &cpal::SupportedStreamConfig) -> hound::WavSpec {
|
||||
hound::WavSpec {
|
||||
channels: config.channels() as _,
|
||||
sample_rate: config.sample_rate().0 as _,
|
||||
bits_per_sample: (config.sample_format().sample_size() * 8) as _,
|
||||
sample_format: sample_format(config.sample_format()),
|
||||
}
|
||||
}
|
||||
|
||||
type WavWriterHandle = Arc<Mutex<Option<hound::WavWriter<BufWriter<File>>>>>;
|
||||
|
||||
fn write_input_data<T, U>(input: &[T], writer: &WavWriterHandle)
|
||||
where
|
||||
T: Sample,
|
||||
U: Sample + hound::Sample + FromSample<T>,
|
||||
{
|
||||
if let Ok(mut guard) = writer.try_lock() {
|
||||
if let Some(writer) = guard.as_mut() {
|
||||
for &sample in input.iter() {
|
||||
let sample: U = U::from_sample(sample);
|
||||
writer.write_sample(sample).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-191
@@ -1,191 +0,0 @@
|
||||
/* This example expose parameter to pass generator of sample.
|
||||
Good starting point for integration of cpal into your application.
|
||||
*/
|
||||
|
||||
extern crate anyhow;
|
||||
extern crate clap;
|
||||
extern crate cpal;
|
||||
|
||||
use cpal::{
|
||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
SizedSample,
|
||||
};
|
||||
use cpal::{FromSample, Sample};
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let stream = stream_setup_for()?;
|
||||
stream.play()?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(4000));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub enum Waveform {
|
||||
Sine,
|
||||
Square,
|
||||
Saw,
|
||||
Triangle,
|
||||
}
|
||||
|
||||
pub struct Oscillator {
|
||||
pub sample_rate: f32,
|
||||
pub waveform: Waveform,
|
||||
pub current_sample_index: f32,
|
||||
pub frequency_hz: f32,
|
||||
}
|
||||
|
||||
impl Oscillator {
|
||||
fn advance_sample(&mut self) {
|
||||
self.current_sample_index = (self.current_sample_index + 1.0) % self.sample_rate;
|
||||
}
|
||||
|
||||
fn set_waveform(&mut self, waveform: Waveform) {
|
||||
self.waveform = waveform;
|
||||
}
|
||||
|
||||
fn calculate_sine_output_from_freq(&self, freq: f32) -> f32 {
|
||||
let two_pi = 2.0 * std::f32::consts::PI;
|
||||
(self.current_sample_index * freq * two_pi / self.sample_rate).sin()
|
||||
}
|
||||
|
||||
fn is_multiple_of_freq_above_nyquist(&self, multiple: f32) -> bool {
|
||||
self.frequency_hz * multiple > self.sample_rate / 2.0
|
||||
}
|
||||
|
||||
fn sine_wave(&mut self) -> f32 {
|
||||
self.advance_sample();
|
||||
self.calculate_sine_output_from_freq(self.frequency_hz)
|
||||
}
|
||||
|
||||
fn generative_waveform(&mut self, harmonic_index_increment: i32, gain_exponent: f32) -> f32 {
|
||||
self.advance_sample();
|
||||
let mut output = 0.0;
|
||||
let mut i = 1;
|
||||
while !self.is_multiple_of_freq_above_nyquist(i as f32) {
|
||||
let gain = 1.0 / (i as f32).powf(gain_exponent);
|
||||
output += gain * self.calculate_sine_output_from_freq(self.frequency_hz * i as f32);
|
||||
i += harmonic_index_increment;
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn square_wave(&mut self) -> f32 {
|
||||
self.generative_waveform(2, 1.0)
|
||||
}
|
||||
|
||||
fn saw_wave(&mut self) -> f32 {
|
||||
self.generative_waveform(1, 1.0)
|
||||
}
|
||||
|
||||
fn triangle_wave(&mut self) -> f32 {
|
||||
self.generative_waveform(2, 2.0)
|
||||
}
|
||||
|
||||
fn tick(&mut self) -> f32 {
|
||||
match self.waveform {
|
||||
Waveform::Sine => self.sine_wave(),
|
||||
Waveform::Square => self.square_wave(),
|
||||
Waveform::Saw => self.saw_wave(),
|
||||
Waveform::Triangle => self.triangle_wave(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stream_setup_for() -> Result<cpal::Stream, anyhow::Error>
|
||||
where
|
||||
{
|
||||
let (_host, device, config) = host_device_setup()?;
|
||||
|
||||
match config.sample_format() {
|
||||
cpal::SampleFormat::I8 => make_stream::<i8>(&device, &config.into()),
|
||||
cpal::SampleFormat::I16 => make_stream::<i16>(&device, &config.into()),
|
||||
cpal::SampleFormat::I32 => make_stream::<i32>(&device, &config.into()),
|
||||
cpal::SampleFormat::I64 => make_stream::<i64>(&device, &config.into()),
|
||||
cpal::SampleFormat::U8 => make_stream::<u8>(&device, &config.into()),
|
||||
cpal::SampleFormat::U16 => make_stream::<u16>(&device, &config.into()),
|
||||
cpal::SampleFormat::U32 => make_stream::<u32>(&device, &config.into()),
|
||||
cpal::SampleFormat::U64 => make_stream::<u64>(&device, &config.into()),
|
||||
cpal::SampleFormat::F32 => make_stream::<f32>(&device, &config.into()),
|
||||
cpal::SampleFormat::F64 => make_stream::<f64>(&device, &config.into()),
|
||||
sample_format => Err(anyhow::Error::msg(format!(
|
||||
"Unsupported sample format '{sample_format}'"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host_device_setup(
|
||||
) -> Result<(cpal::Host, cpal::Device, cpal::SupportedStreamConfig), anyhow::Error> {
|
||||
let host = cpal::default_host();
|
||||
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.ok_or_else(|| anyhow::Error::msg("Default output device is not available"))?;
|
||||
println!("Output device : {}", device.name()?);
|
||||
|
||||
let config = device.default_output_config()?;
|
||||
println!("Default output config : {:?}", config);
|
||||
|
||||
Ok((host, device, config))
|
||||
}
|
||||
|
||||
pub fn make_stream<T>(
|
||||
device: &cpal::Device,
|
||||
config: &cpal::StreamConfig,
|
||||
) -> Result<cpal::Stream, anyhow::Error>
|
||||
where
|
||||
T: SizedSample + FromSample<f32>,
|
||||
{
|
||||
let num_channels = config.channels as usize;
|
||||
let mut oscillator = Oscillator {
|
||||
waveform: Waveform::Sine,
|
||||
sample_rate: config.sample_rate.0 as f32,
|
||||
current_sample_index: 0.0,
|
||||
frequency_hz: 440.0,
|
||||
};
|
||||
let err_fn = |err| eprintln!("Error building output sound stream: {}", err);
|
||||
|
||||
let time_at_start = std::time::Instant::now();
|
||||
println!("Time at start: {:?}", time_at_start);
|
||||
|
||||
let stream = device.build_output_stream(
|
||||
config,
|
||||
move |output: &mut [T], _: &cpal::OutputCallbackInfo| {
|
||||
// for 0-1s play sine, 1-2s play square, 2-3s play saw, 3-4s play triangle_wave
|
||||
let time_since_start = std::time::Instant::now()
|
||||
.duration_since(time_at_start)
|
||||
.as_secs_f32();
|
||||
if time_since_start < 1.0 {
|
||||
oscillator.set_waveform(Waveform::Sine);
|
||||
} else if time_since_start < 2.0 {
|
||||
oscillator.set_waveform(Waveform::Triangle);
|
||||
} else if time_since_start < 3.0 {
|
||||
oscillator.set_waveform(Waveform::Square);
|
||||
} else if time_since_start < 4.0 {
|
||||
oscillator.set_waveform(Waveform::Saw);
|
||||
} else {
|
||||
oscillator.set_waveform(Waveform::Sine);
|
||||
}
|
||||
process_frame(output, &mut oscillator, num_channels)
|
||||
},
|
||||
err_fn,
|
||||
None,
|
||||
)?;
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn process_frame<SampleType>(
|
||||
output: &mut [SampleType],
|
||||
oscillator: &mut Oscillator,
|
||||
num_channels: usize,
|
||||
) where
|
||||
SampleType: Sample + FromSample<f32>,
|
||||
{
|
||||
for frame in output.chunks_mut(num_channels) {
|
||||
let value: SampleType = SampleType::from_sample(oscillator.tick());
|
||||
|
||||
// copy the same value to all channels
|
||||
for sample in frame.iter_mut() {
|
||||
*sample = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
-292
@@ -1,292 +0,0 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
/// The requested host, although supported on this platform, is unavailable.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct HostUnavailable;
|
||||
|
||||
impl Display for HostUnavailable {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("the requested host is unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for HostUnavailable {}
|
||||
|
||||
/// Some error has occurred that is specific to the backend from which it was produced.
|
||||
///
|
||||
/// This error is often used as a catch-all in cases where:
|
||||
///
|
||||
/// - It is unclear exactly what error might be produced by the backend API.
|
||||
/// - It does not make sense to add a variant to the enclosing error type.
|
||||
/// - No error was expected to occur at all, but we return an error to avoid the possibility of a
|
||||
/// `panic!` caused by some unforeseen or unknown reason.
|
||||
///
|
||||
/// **Note:** If you notice a `BackendSpecificError` that you believe could be better handled in a
|
||||
/// cross-platform manner, please create an issue or submit a pull request with a patch that adds
|
||||
/// the necessary error variant to the appropriate error enum.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BackendSpecificError {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl Display for BackendSpecificError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"A backend-specific error has occurred: {}",
|
||||
self.description
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for BackendSpecificError {}
|
||||
|
||||
/// An error that might occur while attempting to enumerate the available devices on a system.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DevicesError {
|
||||
/// See the [`BackendSpecificError`] docs for more information about this error variant.
|
||||
BackendSpecific { err: BackendSpecificError },
|
||||
}
|
||||
|
||||
impl Display for DevicesError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BackendSpecific { err } => err.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for DevicesError {}
|
||||
|
||||
impl From<BackendSpecificError> for DevicesError {
|
||||
fn from(err: BackendSpecificError) -> Self {
|
||||
Self::BackendSpecific { err }
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that may occur while attempting to retrieve a device name.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DeviceNameError {
|
||||
/// See the [`BackendSpecificError`] docs for more information about this error variant.
|
||||
BackendSpecific { err: BackendSpecificError },
|
||||
}
|
||||
|
||||
impl Display for DeviceNameError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BackendSpecific { err } => err.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for DeviceNameError {}
|
||||
|
||||
impl From<BackendSpecificError> for DeviceNameError {
|
||||
fn from(err: BackendSpecificError) -> Self {
|
||||
Self::BackendSpecific { err }
|
||||
}
|
||||
}
|
||||
|
||||
/// Error that can happen when enumerating the list of supported formats.
|
||||
#[derive(Debug)]
|
||||
pub enum SupportedStreamConfigsError {
|
||||
/// The device no longer exists. This can happen if the device is disconnected while the
|
||||
/// program is running.
|
||||
DeviceNotAvailable,
|
||||
/// We called something the C-Layer did not understand
|
||||
InvalidArgument,
|
||||
/// See the [`BackendSpecificError`] docs for more information about this error variant.
|
||||
BackendSpecific { err: BackendSpecificError },
|
||||
}
|
||||
|
||||
impl Display for SupportedStreamConfigsError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BackendSpecific { err } => err.fmt(f),
|
||||
Self::DeviceNotAvailable => f.write_str("The requested device is no longer available. For example, it has been unplugged."),
|
||||
Self::InvalidArgument => f.write_str("Invalid argument passed to the backend. For example, this happens when trying to read capture capabilities when the device does not support it.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for SupportedStreamConfigsError {}
|
||||
|
||||
impl From<BackendSpecificError> for SupportedStreamConfigsError {
|
||||
fn from(err: BackendSpecificError) -> Self {
|
||||
Self::BackendSpecific { err }
|
||||
}
|
||||
}
|
||||
|
||||
/// May occur when attempting to request the default input or output stream format from a [`Device`](crate::Device).
|
||||
#[derive(Debug)]
|
||||
pub enum DefaultStreamConfigError {
|
||||
/// The device no longer exists. This can happen if the device is disconnected while the
|
||||
/// program is running.
|
||||
DeviceNotAvailable,
|
||||
/// Returned if e.g. the default input format was requested on an output-only audio device.
|
||||
StreamTypeNotSupported,
|
||||
/// See the [`BackendSpecificError`] docs for more information about this error variant.
|
||||
BackendSpecific { err: BackendSpecificError },
|
||||
}
|
||||
|
||||
impl Display for DefaultStreamConfigError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BackendSpecific { err } => err.fmt(f),
|
||||
DefaultStreamConfigError::DeviceNotAvailable => f.write_str(
|
||||
"The requested device is no longer available. For example, it has been unplugged.",
|
||||
),
|
||||
DefaultStreamConfigError::StreamTypeNotSupported => {
|
||||
f.write_str("The requested stream type is not supported by the device.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for DefaultStreamConfigError {}
|
||||
|
||||
impl From<BackendSpecificError> for DefaultStreamConfigError {
|
||||
fn from(err: BackendSpecificError) -> Self {
|
||||
Self::BackendSpecific { err }
|
||||
}
|
||||
}
|
||||
/// Error that can happen when creating a [`Stream`](crate::Stream).
|
||||
#[derive(Debug)]
|
||||
pub enum BuildStreamError {
|
||||
/// The device no longer exists. This can happen if the device is disconnected while the
|
||||
/// program is running.
|
||||
DeviceNotAvailable,
|
||||
/// The specified stream configuration is not supported.
|
||||
StreamConfigNotSupported,
|
||||
/// We called something the C-Layer did not understand
|
||||
///
|
||||
/// On ALSA device functions called with a feature they do not support will yield this. E.g.
|
||||
/// Trying to use capture capabilities on an output only format yields this.
|
||||
InvalidArgument,
|
||||
/// Occurs if adding a new Stream ID would cause an integer overflow.
|
||||
StreamIdOverflow,
|
||||
/// See the [`BackendSpecificError`] docs for more information about this error variant.
|
||||
BackendSpecific { err: BackendSpecificError },
|
||||
}
|
||||
|
||||
impl Display for BuildStreamError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BackendSpecific { err } => err.fmt(f),
|
||||
BuildStreamError::DeviceNotAvailable => f.write_str(
|
||||
"The requested device is no longer available. For example, it has been unplugged.",
|
||||
),
|
||||
BuildStreamError::StreamConfigNotSupported => {
|
||||
f.write_str("The requested stream configuration is not supported by the device.")
|
||||
}
|
||||
BuildStreamError::InvalidArgument => f.write_str(
|
||||
"The requested device does not support this capability (invalid argument)",
|
||||
),
|
||||
BuildStreamError::StreamIdOverflow => {
|
||||
f.write_str("Adding a new stream ID would cause an overflow")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for BuildStreamError {}
|
||||
|
||||
impl From<BackendSpecificError> for BuildStreamError {
|
||||
fn from(err: BackendSpecificError) -> Self {
|
||||
Self::BackendSpecific { err }
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that might occur when calling [`Stream::play()`](crate::traits::StreamTrait::play).
|
||||
///
|
||||
/// As of writing this, only macOS may immediately return an error while calling this method. This
|
||||
/// is because both the alsa and wasapi backends only enqueue these commands and do not process
|
||||
/// them immediately.
|
||||
#[derive(Debug)]
|
||||
pub enum PlayStreamError {
|
||||
/// The device associated with the stream is no longer available.
|
||||
DeviceNotAvailable,
|
||||
/// See the [`BackendSpecificError`] docs for more information about this error variant.
|
||||
BackendSpecific { err: BackendSpecificError },
|
||||
}
|
||||
|
||||
impl Display for PlayStreamError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BackendSpecific { err } => err.fmt(f),
|
||||
PlayStreamError::DeviceNotAvailable => {
|
||||
f.write_str("the device associated with the stream is no longer available")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for PlayStreamError {}
|
||||
|
||||
impl From<BackendSpecificError> for PlayStreamError {
|
||||
fn from(err: BackendSpecificError) -> Self {
|
||||
Self::BackendSpecific { err }
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that might occur when calling [`Stream::pause()`](crate::traits::StreamTrait::pause).
|
||||
///
|
||||
/// As of writing this, only macOS may immediately return an error while calling this method. This
|
||||
/// is because both the alsa and wasapi backends only enqueue these commands and do not process
|
||||
/// them immediately.
|
||||
#[derive(Debug)]
|
||||
pub enum PauseStreamError {
|
||||
/// The device associated with the stream is no longer available.
|
||||
DeviceNotAvailable,
|
||||
/// See the [`BackendSpecificError`] docs for more information about this error variant.
|
||||
BackendSpecific { err: BackendSpecificError },
|
||||
}
|
||||
|
||||
impl Display for PauseStreamError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BackendSpecific { err } => err.fmt(f),
|
||||
PauseStreamError::DeviceNotAvailable => {
|
||||
f.write_str("the device associated with the stream is no longer available")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for PauseStreamError {}
|
||||
|
||||
impl From<BackendSpecificError> for PauseStreamError {
|
||||
fn from(err: BackendSpecificError) -> Self {
|
||||
Self::BackendSpecific { err }
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that might occur while a stream is running.
|
||||
#[derive(Debug)]
|
||||
pub enum StreamError {
|
||||
/// The device no longer exists. This can happen if the device is disconnected while the
|
||||
/// program is running.
|
||||
DeviceNotAvailable,
|
||||
/// See the [`BackendSpecificError`] docs for more information about this error variant.
|
||||
BackendSpecific { err: BackendSpecificError },
|
||||
}
|
||||
|
||||
impl Display for StreamError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::BackendSpecific { err } => err.fmt(f),
|
||||
StreamError::DeviceNotAvailable => f.write_str(
|
||||
"The requested device is no longer available. For example, it has been unplugged.",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for StreamError {}
|
||||
|
||||
impl From<BackendSpecificError> for StreamError {
|
||||
fn from(err: BackendSpecificError) -> Self {
|
||||
Self::BackendSpecific { err }
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
use super::alsa;
|
||||
use super::{Device, DeviceHandles};
|
||||
use crate::{BackendSpecificError, DevicesError};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// ALSA's implementation for `Devices`.
|
||||
pub struct Devices {
|
||||
hint_iter: alsa::device_name::HintIter,
|
||||
}
|
||||
|
||||
impl Devices {
|
||||
pub fn new() -> Result<Self, DevicesError> {
|
||||
Ok(Devices {
|
||||
hint_iter: alsa::device_name::HintIter::new_str(None, "pcm")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for Devices {}
|
||||
unsafe impl Sync for Devices {}
|
||||
|
||||
impl Iterator for Devices {
|
||||
type Item = Device;
|
||||
|
||||
fn next(&mut self) -> Option<Device> {
|
||||
loop {
|
||||
match self.hint_iter.next() {
|
||||
None => return None,
|
||||
Some(hint) => {
|
||||
let name = match hint.name {
|
||||
None => continue,
|
||||
// Ignoring the `null` device.
|
||||
Some(name) if name == "null" => continue,
|
||||
Some(name) => name,
|
||||
};
|
||||
|
||||
if let Ok(handles) = DeviceHandles::open(&name) {
|
||||
return Some(Device {
|
||||
name,
|
||||
handles: Arc::new(Mutex::new(handles)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn default_input_device() -> Option<Device> {
|
||||
Some(Device {
|
||||
name: "default".to_owned(),
|
||||
handles: Arc::new(Mutex::new(Default::default())),
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn default_output_device() -> Option<Device> {
|
||||
Some(Device {
|
||||
name: "default".to_owned(),
|
||||
handles: Arc::new(Mutex::new(Default::default())),
|
||||
})
|
||||
}
|
||||
|
||||
impl From<alsa::Error> for DevicesError {
|
||||
fn from(err: alsa::Error) -> Self {
|
||||
let err: BackendSpecificError = err.into();
|
||||
err.into()
|
||||
}
|
||||
}
|
||||
-1166
File diff suppressed because it is too large.
Load diff
-232
@@ -1,232 +0,0 @@
|
||||
pub type SupportedInputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
pub type SupportedOutputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
|
||||
use super::sys;
|
||||
use crate::BackendSpecificError;
|
||||
use crate::DefaultStreamConfigError;
|
||||
use crate::DeviceNameError;
|
||||
use crate::DevicesError;
|
||||
use crate::SampleFormat;
|
||||
use crate::SampleRate;
|
||||
use crate::SupportedBufferSize;
|
||||
use crate::SupportedStreamConfig;
|
||||
use crate::SupportedStreamConfigRange;
|
||||
use crate::SupportedStreamConfigsError;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::atomic::AtomicI32;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// A ASIO Device
|
||||
#[derive(Clone)]
|
||||
pub struct Device {
|
||||
/// The driver represented by this device.
|
||||
pub driver: Arc<sys::Driver>,
|
||||
|
||||
// Input and/or Output stream.
|
||||
// A driver can only have one of each.
|
||||
// They need to be created at the same time.
|
||||
pub asio_streams: Arc<Mutex<sys::AsioStreams>>,
|
||||
pub current_buffer_index: Arc<AtomicI32>,
|
||||
}
|
||||
|
||||
/// All available devices.
|
||||
pub struct Devices {
|
||||
asio: Arc<sys::Asio>,
|
||||
drivers: std::vec::IntoIter<String>,
|
||||
}
|
||||
|
||||
impl PartialEq for Device {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.driver.name() == other.driver.name()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Device {}
|
||||
|
||||
impl Hash for Device {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.driver.name().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn name(&self) -> Result<String, DeviceNameError> {
|
||||
Ok(self.driver.name().to_string())
|
||||
}
|
||||
|
||||
/// Gets the supported input configs.
|
||||
/// TODO currently only supports the default.
|
||||
/// Need to find all possible configs.
|
||||
pub fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
// Retrieve the default config for the total supported channels and supported sample
|
||||
// format.
|
||||
let f = match self.default_input_config() {
|
||||
Err(_) => return Err(SupportedStreamConfigsError::DeviceNotAvailable),
|
||||
Ok(f) => f,
|
||||
};
|
||||
|
||||
// Collect a config for every combination of supported sample rate and number of channels.
|
||||
let mut supported_configs = vec![];
|
||||
for &rate in crate::COMMON_SAMPLE_RATES {
|
||||
if !self
|
||||
.driver
|
||||
.can_sample_rate(rate.0.into())
|
||||
.ok()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for channels in 1..f.channels + 1 {
|
||||
supported_configs.push(SupportedStreamConfigRange {
|
||||
channels,
|
||||
min_sample_rate: rate,
|
||||
max_sample_rate: rate,
|
||||
buffer_size: f.buffer_size,
|
||||
sample_format: f.sample_format,
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(supported_configs.into_iter())
|
||||
}
|
||||
|
||||
/// Gets the supported output configs.
|
||||
/// TODO currently only supports the default.
|
||||
/// Need to find all possible configs.
|
||||
pub fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
// Retrieve the default config for the total supported channels and supported sample
|
||||
// format.
|
||||
let f = match self.default_output_config() {
|
||||
Err(_) => return Err(SupportedStreamConfigsError::DeviceNotAvailable),
|
||||
Ok(f) => f,
|
||||
};
|
||||
|
||||
// Collect a config for every combination of supported sample rate and number of channels.
|
||||
let mut supported_configs = vec![];
|
||||
for &rate in crate::COMMON_SAMPLE_RATES {
|
||||
if !self
|
||||
.driver
|
||||
.can_sample_rate(rate.0.into())
|
||||
.ok()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for channels in 1..f.channels + 1 {
|
||||
supported_configs.push(SupportedStreamConfigRange {
|
||||
channels,
|
||||
min_sample_rate: rate,
|
||||
max_sample_rate: rate,
|
||||
buffer_size: f.buffer_size,
|
||||
sample_format: f.sample_format,
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(supported_configs.into_iter())
|
||||
}
|
||||
|
||||
/// Returns the default input config
|
||||
pub fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
let channels = self.driver.channels().map_err(default_config_err)?.ins as u16;
|
||||
let sample_rate = SampleRate(self.driver.sample_rate().map_err(default_config_err)? as _);
|
||||
let (min, max) = self.driver.buffersize_range().map_err(default_config_err)?;
|
||||
let buffer_size = SupportedBufferSize::Range {
|
||||
min: min as u32,
|
||||
max: max as u32,
|
||||
};
|
||||
// Map th ASIO sample type to a CPAL sample type
|
||||
let data_type = self.driver.input_data_type().map_err(default_config_err)?;
|
||||
let sample_format = convert_data_type(&data_type)
|
||||
.ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?;
|
||||
Ok(SupportedStreamConfig {
|
||||
channels,
|
||||
sample_rate,
|
||||
buffer_size,
|
||||
sample_format,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the default output config
|
||||
pub fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
let channels = self.driver.channels().map_err(default_config_err)?.outs as u16;
|
||||
let sample_rate = SampleRate(self.driver.sample_rate().map_err(default_config_err)? as _);
|
||||
let (min, max) = self.driver.buffersize_range().map_err(default_config_err)?;
|
||||
let buffer_size = SupportedBufferSize::Range {
|
||||
min: min as u32,
|
||||
max: max as u32,
|
||||
};
|
||||
let data_type = self.driver.output_data_type().map_err(default_config_err)?;
|
||||
let sample_format = convert_data_type(&data_type)
|
||||
.ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?;
|
||||
Ok(SupportedStreamConfig {
|
||||
channels,
|
||||
sample_rate,
|
||||
buffer_size,
|
||||
sample_format,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Devices {
|
||||
pub fn new(asio: Arc<sys::Asio>) -> Result<Self, DevicesError> {
|
||||
let drivers = asio.driver_names().into_iter();
|
||||
Ok(Devices { asio, drivers })
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Devices {
|
||||
type Item = Device;
|
||||
|
||||
/// Load drivers and return device
|
||||
fn next(&mut self) -> Option<Device> {
|
||||
loop {
|
||||
match self.drivers.next() {
|
||||
Some(name) => match self.asio.load_driver(&name) {
|
||||
Ok(driver) => {
|
||||
let driver = Arc::new(driver);
|
||||
let asio_streams = Arc::new(Mutex::new(sys::AsioStreams {
|
||||
input: None,
|
||||
output: None,
|
||||
}));
|
||||
return Some(Device {
|
||||
driver,
|
||||
asio_streams,
|
||||
current_buffer_index: Arc::new(AtomicI32::new(-1)),
|
||||
});
|
||||
}
|
||||
Err(_) => continue,
|
||||
},
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn convert_data_type(ty: &sys::AsioSampleType) -> Option<SampleFormat> {
|
||||
let fmt = match *ty {
|
||||
sys::AsioSampleType::ASIOSTInt16MSB => SampleFormat::I16,
|
||||
sys::AsioSampleType::ASIOSTInt16LSB => SampleFormat::I16,
|
||||
sys::AsioSampleType::ASIOSTFloat32MSB => SampleFormat::F32,
|
||||
sys::AsioSampleType::ASIOSTFloat32LSB => SampleFormat::F32,
|
||||
sys::AsioSampleType::ASIOSTInt32MSB => SampleFormat::I32,
|
||||
sys::AsioSampleType::ASIOSTInt32LSB => SampleFormat::I32,
|
||||
_ => return None,
|
||||
};
|
||||
Some(fmt)
|
||||
}
|
||||
|
||||
fn default_config_err(e: sys::AsioError) -> DefaultStreamConfigError {
|
||||
match e {
|
||||
sys::AsioError::NoDrivers | sys::AsioError::HardwareMalfunction => {
|
||||
DefaultStreamConfigError::DeviceNotAvailable
|
||||
}
|
||||
sys::AsioError::NoRate => DefaultStreamConfigError::StreamTypeNotSupported,
|
||||
err => {
|
||||
let description = format!("{}", err);
|
||||
BackendSpecificError { description }.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
extern crate asio_sys as sys;
|
||||
|
||||
use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use crate::{
|
||||
BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError,
|
||||
InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat,
|
||||
StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigsError,
|
||||
};
|
||||
|
||||
pub use self::device::{Device, Devices, SupportedInputConfigs, SupportedOutputConfigs};
|
||||
pub use self::stream::Stream;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
mod device;
|
||||
mod stream;
|
||||
|
||||
/// The host for ASIO.
|
||||
#[derive(Debug)]
|
||||
pub struct Host {
|
||||
asio: Arc<sys::Asio>,
|
||||
}
|
||||
|
||||
impl Host {
|
||||
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
||||
let asio = Arc::new(sys::Asio::new());
|
||||
let host = Host { asio };
|
||||
Ok(host)
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTrait for Host {
|
||||
type Devices = Devices;
|
||||
type Device = Device;
|
||||
|
||||
fn is_available() -> bool {
|
||||
true
|
||||
//unimplemented!("check how to do this using asio-sys")
|
||||
}
|
||||
|
||||
fn devices(&self) -> Result<Self::Devices, DevicesError> {
|
||||
Devices::new(self.asio.clone())
|
||||
}
|
||||
|
||||
fn default_input_device(&self) -> Option<Self::Device> {
|
||||
// ASIO has no concept of a default device, so just use the first.
|
||||
self.input_devices().ok().and_then(|mut ds| ds.next())
|
||||
}
|
||||
|
||||
fn default_output_device(&self) -> Option<Self::Device> {
|
||||
// ASIO has no concept of a default device, so just use the first.
|
||||
self.output_devices().ok().and_then(|mut ds| ds.next())
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceTrait for Device {
|
||||
type SupportedInputConfigs = SupportedInputConfigs;
|
||||
type SupportedOutputConfigs = SupportedOutputConfigs;
|
||||
type Stream = Stream;
|
||||
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
Device::name(self)
|
||||
}
|
||||
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
Device::supported_input_configs(self)
|
||||
}
|
||||
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
Device::supported_output_configs(self)
|
||||
}
|
||||
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
Device::default_input_config(self)
|
||||
}
|
||||
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
Device::default_output_config(self)
|
||||
}
|
||||
|
||||
fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
Device::build_input_stream_raw(
|
||||
self,
|
||||
config,
|
||||
sample_format,
|
||||
data_callback,
|
||||
error_callback,
|
||||
timeout,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
Device::build_output_stream_raw(
|
||||
self,
|
||||
config,
|
||||
sample_format,
|
||||
data_callback,
|
||||
error_callback,
|
||||
timeout,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTrait for Stream {
|
||||
fn play(&self) -> Result<(), PlayStreamError> {
|
||||
Stream::play(self)
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<(), PauseStreamError> {
|
||||
Stream::pause(self)
|
||||
}
|
||||
}
|
||||
-718
@@ -1,718 +0,0 @@
|
||||
extern crate asio_sys as sys;
|
||||
extern crate num_traits;
|
||||
|
||||
use self::num_traits::PrimInt;
|
||||
use super::Device;
|
||||
use crate::{
|
||||
BackendSpecificError, BufferSize, BuildStreamError, Data, InputCallbackInfo,
|
||||
OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct Stream {
|
||||
playing: Arc<AtomicBool>,
|
||||
// Ensure the `Driver` does not terminate until the last stream is dropped.
|
||||
driver: Arc<sys::Driver>,
|
||||
#[allow(dead_code)]
|
||||
asio_streams: Arc<Mutex<sys::AsioStreams>>,
|
||||
callback_id: sys::CallbackId,
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
pub fn play(&self) -> Result<(), PlayStreamError> {
|
||||
self.playing.store(true, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pause(&self) -> Result<(), PauseStreamError> {
|
||||
self.playing.store(false, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
mut data_callback: D,
|
||||
_error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
let stream_type = self.driver.input_data_type().map_err(build_stream_err)?;
|
||||
|
||||
// Ensure that the desired sample type is supported.
|
||||
let expected_sample_format = super::device::convert_data_type(&stream_type)
|
||||
.ok_or(BuildStreamError::StreamConfigNotSupported)?;
|
||||
if sample_format != expected_sample_format {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
|
||||
let num_channels = config.channels;
|
||||
let buffer_size = self.get_or_create_input_stream(config, sample_format)?;
|
||||
let cpal_num_samples = buffer_size * num_channels as usize;
|
||||
|
||||
// Create the buffer depending on the size of the data type.
|
||||
let len_bytes = cpal_num_samples * sample_format.sample_size();
|
||||
let mut interleaved = vec![0u8; len_bytes];
|
||||
|
||||
let stream_playing = Arc::new(AtomicBool::new(false));
|
||||
let playing = Arc::clone(&stream_playing);
|
||||
let asio_streams = self.asio_streams.clone();
|
||||
|
||||
// Set the input callback.
|
||||
// This is most performance critical part of the ASIO bindings.
|
||||
let config = config.clone();
|
||||
let callback_id = self.driver.add_callback(move |callback_info| unsafe {
|
||||
// If not playing return early.
|
||||
if !playing.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
// There is 0% chance of lock contention the host only locks when recreating streams.
|
||||
let stream_lock = asio_streams.lock().unwrap();
|
||||
let asio_stream = match stream_lock.input {
|
||||
Some(ref asio_stream) => asio_stream,
|
||||
None => return,
|
||||
};
|
||||
|
||||
/// 1. Write from the ASIO buffer to the interleaved CPAL buffer.
|
||||
/// 2. Deliver the CPAL buffer to the user callback.
|
||||
unsafe fn process_input_callback<A, D, F>(
|
||||
data_callback: &mut D,
|
||||
interleaved: &mut [u8],
|
||||
asio_stream: &sys::AsioStream,
|
||||
asio_info: &sys::CallbackInfo,
|
||||
sample_rate: crate::SampleRate,
|
||||
format: SampleFormat,
|
||||
from_endianness: F,
|
||||
) where
|
||||
A: Copy,
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
F: Fn(A) -> A,
|
||||
{
|
||||
// 1. Write the ASIO channels to the CPAL buffer.
|
||||
let interleaved: &mut [A] = cast_slice_mut(interleaved);
|
||||
let n_frames = asio_stream.buffer_size as usize;
|
||||
let n_channels = interleaved.len() / n_frames;
|
||||
let buffer_index = asio_info.buffer_index as usize;
|
||||
for ch_ix in 0..n_channels {
|
||||
let asio_channel = asio_channel_slice::<A>(asio_stream, buffer_index, ch_ix);
|
||||
for (frame, s_asio) in interleaved.chunks_mut(n_channels).zip(asio_channel) {
|
||||
frame[ch_ix] = from_endianness(*s_asio);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Deliver the interleaved buffer to the callback.
|
||||
let data = interleaved.as_mut_ptr() as *mut ();
|
||||
let len = interleaved.len();
|
||||
let data = Data::from_parts(data, len, format);
|
||||
let callback = system_time_to_stream_instant(asio_info.system_time);
|
||||
let delay = frames_to_duration(n_frames, sample_rate);
|
||||
let capture = callback
|
||||
.sub(delay)
|
||||
.expect("`capture` occurs before origin of alsa `StreamInstant`");
|
||||
let timestamp = crate::InputStreamTimestamp { callback, capture };
|
||||
let info = InputCallbackInfo { timestamp };
|
||||
data_callback(&data, &info);
|
||||
}
|
||||
|
||||
match (&stream_type, sample_format) {
|
||||
(&sys::AsioSampleType::ASIOSTInt16LSB, SampleFormat::I16) => {
|
||||
process_input_callback::<i16, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::I16,
|
||||
from_le,
|
||||
);
|
||||
}
|
||||
(&sys::AsioSampleType::ASIOSTInt16MSB, SampleFormat::I16) => {
|
||||
process_input_callback::<i16, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::I16,
|
||||
from_be,
|
||||
);
|
||||
}
|
||||
|
||||
(&sys::AsioSampleType::ASIOSTFloat32LSB, SampleFormat::F32) => {
|
||||
process_input_callback::<u32, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::F32,
|
||||
from_le,
|
||||
);
|
||||
}
|
||||
(&sys::AsioSampleType::ASIOSTFloat32MSB, SampleFormat::F32) => {
|
||||
process_input_callback::<u32, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::F32,
|
||||
from_be,
|
||||
);
|
||||
}
|
||||
|
||||
(&sys::AsioSampleType::ASIOSTInt32LSB, SampleFormat::I32) => {
|
||||
process_input_callback::<i32, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::I32,
|
||||
from_le,
|
||||
);
|
||||
}
|
||||
(&sys::AsioSampleType::ASIOSTInt32MSB, SampleFormat::I32) => {
|
||||
process_input_callback::<i32, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::I32,
|
||||
from_be,
|
||||
);
|
||||
}
|
||||
|
||||
(&sys::AsioSampleType::ASIOSTFloat64LSB, SampleFormat::F64) => {
|
||||
process_input_callback::<u64, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::F64,
|
||||
from_le,
|
||||
);
|
||||
}
|
||||
(&sys::AsioSampleType::ASIOSTFloat64MSB, SampleFormat::F64) => {
|
||||
process_input_callback::<u64, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::F64,
|
||||
from_be,
|
||||
);
|
||||
}
|
||||
|
||||
unsupported_format_pair => unreachable!(
|
||||
"`build_input_stream_raw` should have returned with unsupported \
|
||||
format {:?}",
|
||||
unsupported_format_pair
|
||||
),
|
||||
}
|
||||
});
|
||||
|
||||
let driver = self.driver.clone();
|
||||
let asio_streams = self.asio_streams.clone();
|
||||
|
||||
// Immediately start the device?
|
||||
self.driver.start().map_err(build_stream_err)?;
|
||||
|
||||
Ok(Stream {
|
||||
playing: stream_playing,
|
||||
driver,
|
||||
asio_streams,
|
||||
callback_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
mut data_callback: D,
|
||||
_error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
let stream_type = self.driver.output_data_type().map_err(build_stream_err)?;
|
||||
|
||||
// Ensure that the desired sample type is supported.
|
||||
let expected_sample_format = super::device::convert_data_type(&stream_type)
|
||||
.ok_or(BuildStreamError::StreamConfigNotSupported)?;
|
||||
if sample_format != expected_sample_format {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
|
||||
let num_channels = config.channels;
|
||||
let buffer_size = self.get_or_create_output_stream(config, sample_format)?;
|
||||
let cpal_num_samples = buffer_size * num_channels as usize;
|
||||
|
||||
// Create buffers depending on data type.
|
||||
let len_bytes = cpal_num_samples * sample_format.sample_size();
|
||||
let mut interleaved = vec![0u8; len_bytes];
|
||||
let current_buffer_index = self.current_buffer_index.clone();
|
||||
|
||||
let stream_playing = Arc::new(AtomicBool::new(false));
|
||||
let playing = Arc::clone(&stream_playing);
|
||||
let asio_streams = self.asio_streams.clone();
|
||||
|
||||
let config = config.clone();
|
||||
let callback_id = self.driver.add_callback(move |callback_info| unsafe {
|
||||
// If not playing, return early.
|
||||
if !playing.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
// There is 0% chance of lock contention the host only locks when recreating streams.
|
||||
let mut stream_lock = asio_streams.lock().unwrap();
|
||||
let asio_stream = match stream_lock.output {
|
||||
Some(ref mut asio_stream) => asio_stream,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Silence the ASIO buffer that is about to be used.
|
||||
//
|
||||
// This checks if any other callbacks have already silenced the buffer associated with
|
||||
// the current `buffer_index`.
|
||||
let silence =
|
||||
current_buffer_index.load(Ordering::Acquire) != callback_info.buffer_index;
|
||||
|
||||
if silence {
|
||||
current_buffer_index.store(callback_info.buffer_index, Ordering::Release);
|
||||
}
|
||||
|
||||
/// 1. Render the given callback to the given buffer of interleaved samples.
|
||||
/// 2. If required, silence the ASIO buffer.
|
||||
/// 3. Finally, write the interleaved data to the non-interleaved ASIO buffer,
|
||||
/// performing endianness conversions as necessary.
|
||||
unsafe fn process_output_callback<A, D, F>(
|
||||
data_callback: &mut D,
|
||||
interleaved: &mut [u8],
|
||||
silence_asio_buffer: bool,
|
||||
asio_stream: &mut sys::AsioStream,
|
||||
asio_info: &sys::CallbackInfo,
|
||||
sample_rate: crate::SampleRate,
|
||||
format: SampleFormat,
|
||||
mix_samples: F,
|
||||
) where
|
||||
A: Copy,
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
F: Fn(A, A) -> A,
|
||||
{
|
||||
// 1. Render interleaved buffer from callback.
|
||||
let interleaved: &mut [A] = cast_slice_mut(interleaved);
|
||||
let data = interleaved.as_mut_ptr() as *mut ();
|
||||
let len = interleaved.len();
|
||||
let mut data = Data::from_parts(data, len, format);
|
||||
let callback = system_time_to_stream_instant(asio_info.system_time);
|
||||
let n_frames = asio_stream.buffer_size as usize;
|
||||
let delay = frames_to_duration(n_frames, sample_rate);
|
||||
let playback = callback
|
||||
.add(delay)
|
||||
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
|
||||
let timestamp = crate::OutputStreamTimestamp { callback, playback };
|
||||
let info = OutputCallbackInfo { timestamp };
|
||||
data_callback(&mut data, &info);
|
||||
|
||||
// 2. Silence ASIO channels if necessary.
|
||||
let n_channels = interleaved.len() / n_frames;
|
||||
let buffer_index = asio_info.buffer_index as usize;
|
||||
if silence_asio_buffer {
|
||||
for ch_ix in 0..n_channels {
|
||||
let asio_channel =
|
||||
asio_channel_slice_mut::<A>(asio_stream, buffer_index, ch_ix);
|
||||
asio_channel.align_to_mut::<u8>().1.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Write interleaved samples to ASIO channels, one channel at a time.
|
||||
for ch_ix in 0..n_channels {
|
||||
let asio_channel =
|
||||
asio_channel_slice_mut::<A>(asio_stream, buffer_index, ch_ix);
|
||||
for (frame, s_asio) in interleaved.chunks(n_channels).zip(asio_channel) {
|
||||
*s_asio = mix_samples(*s_asio, frame[ch_ix]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (sample_format, &stream_type) {
|
||||
(SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt16LSB) => {
|
||||
process_output_callback::<i16, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
silence,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::I16,
|
||||
|old_sample, new_sample| {
|
||||
from_le(old_sample).saturating_add(new_sample).to_le()
|
||||
},
|
||||
);
|
||||
}
|
||||
(SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt16MSB) => {
|
||||
process_output_callback::<i16, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
silence,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::I16,
|
||||
|old_sample, new_sample| {
|
||||
from_be(old_sample).saturating_add(new_sample).to_be()
|
||||
},
|
||||
);
|
||||
}
|
||||
(SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat32LSB) => {
|
||||
process_output_callback::<u32, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
silence,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::F32,
|
||||
|old_sample, new_sample| {
|
||||
(f32::from_bits(from_le(old_sample)) + f32::from_bits(new_sample))
|
||||
.to_bits()
|
||||
.to_le()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
(SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat32MSB) => {
|
||||
process_output_callback::<u32, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
silence,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::F32,
|
||||
|old_sample, new_sample| {
|
||||
(f32::from_bits(from_be(old_sample)) + f32::from_bits(new_sample))
|
||||
.to_bits()
|
||||
.to_be()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
(SampleFormat::I32, &sys::AsioSampleType::ASIOSTInt32LSB) => {
|
||||
process_output_callback::<i32, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
silence,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::I32,
|
||||
|old_sample, new_sample| {
|
||||
from_le(old_sample).saturating_add(new_sample).to_le()
|
||||
},
|
||||
);
|
||||
}
|
||||
(SampleFormat::I32, &sys::AsioSampleType::ASIOSTInt32MSB) => {
|
||||
process_output_callback::<i32, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
silence,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::I32,
|
||||
|old_sample, new_sample| {
|
||||
from_be(old_sample).saturating_add(new_sample).to_be()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
(SampleFormat::F64, &sys::AsioSampleType::ASIOSTFloat64LSB) => {
|
||||
process_output_callback::<u64, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
silence,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::F64,
|
||||
|old_sample, new_sample| {
|
||||
(f64::from_bits(from_le(old_sample)) + f64::from_bits(new_sample))
|
||||
.to_bits()
|
||||
.to_le()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
(SampleFormat::F64, &sys::AsioSampleType::ASIOSTFloat64MSB) => {
|
||||
process_output_callback::<u64, _, _>(
|
||||
&mut data_callback,
|
||||
&mut interleaved,
|
||||
silence,
|
||||
asio_stream,
|
||||
callback_info,
|
||||
config.sample_rate,
|
||||
SampleFormat::F64,
|
||||
|old_sample, new_sample| {
|
||||
(f64::from_bits(from_be(old_sample)) + f64::from_bits(new_sample))
|
||||
.to_bits()
|
||||
.to_be()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
unsupported_format_pair => unreachable!(
|
||||
"`build_output_stream_raw` should have returned with unsupported \
|
||||
format {:?}",
|
||||
unsupported_format_pair
|
||||
),
|
||||
}
|
||||
});
|
||||
|
||||
let driver = self.driver.clone();
|
||||
let asio_streams = self.asio_streams.clone();
|
||||
|
||||
// Immediately start the device?
|
||||
self.driver.start().map_err(build_stream_err)?;
|
||||
|
||||
Ok(Stream {
|
||||
playing: stream_playing,
|
||||
driver,
|
||||
asio_streams,
|
||||
callback_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new CPAL Input Stream.
|
||||
///
|
||||
/// If there is no existing ASIO Input Stream it will be created.
|
||||
///
|
||||
/// On success, the buffer size of the stream is returned.
|
||||
fn get_or_create_input_stream(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
) -> Result<usize, BuildStreamError> {
|
||||
match self.default_input_config() {
|
||||
Ok(f) => {
|
||||
let num_asio_channels = f.channels;
|
||||
check_config(&self.driver, config, sample_format, num_asio_channels)
|
||||
}
|
||||
Err(_) => Err(BuildStreamError::StreamConfigNotSupported),
|
||||
}?;
|
||||
let num_channels = config.channels as usize;
|
||||
let mut streams = self.asio_streams.lock().unwrap();
|
||||
|
||||
let buffer_size = match config.buffer_size {
|
||||
BufferSize::Fixed(v) => Some(v as i32),
|
||||
BufferSize::Default => None,
|
||||
};
|
||||
|
||||
// Either create a stream if thers none or had back the
|
||||
// size of the current one.
|
||||
match streams.input {
|
||||
Some(ref input) => Ok(input.buffer_size as usize),
|
||||
None => {
|
||||
let output = streams.output.take();
|
||||
self.driver
|
||||
.prepare_input_stream(output, num_channels, buffer_size)
|
||||
.map(|new_streams| {
|
||||
let bs = match new_streams.input {
|
||||
Some(ref inp) => inp.buffer_size as usize,
|
||||
None => unreachable!(),
|
||||
};
|
||||
*streams = new_streams;
|
||||
bs
|
||||
})
|
||||
.map_err(|ref e| {
|
||||
println!("Error preparing stream: {}", e);
|
||||
BuildStreamError::DeviceNotAvailable
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new CPAL Output Stream.
|
||||
///
|
||||
/// If there is no existing ASIO Output Stream it will be created.
|
||||
fn get_or_create_output_stream(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
) -> Result<usize, BuildStreamError> {
|
||||
match self.default_output_config() {
|
||||
Ok(f) => {
|
||||
let num_asio_channels = f.channels;
|
||||
check_config(&self.driver, config, sample_format, num_asio_channels)
|
||||
}
|
||||
Err(_) => Err(BuildStreamError::StreamConfigNotSupported),
|
||||
}?;
|
||||
let num_channels = config.channels as usize;
|
||||
let mut streams = self.asio_streams.lock().unwrap();
|
||||
|
||||
let buffer_size = match config.buffer_size {
|
||||
BufferSize::Fixed(v) => Some(v as i32),
|
||||
BufferSize::Default => None,
|
||||
};
|
||||
|
||||
// Either create a stream if thers none or had back the
|
||||
// size of the current one.
|
||||
match streams.output {
|
||||
Some(ref output) => Ok(output.buffer_size as usize),
|
||||
None => {
|
||||
let input = streams.input.take();
|
||||
self.driver
|
||||
.prepare_output_stream(input, num_channels, buffer_size)
|
||||
.map(|new_streams| {
|
||||
let bs = match new_streams.output {
|
||||
Some(ref out) => out.buffer_size as usize,
|
||||
None => unreachable!(),
|
||||
};
|
||||
*streams = new_streams;
|
||||
bs
|
||||
})
|
||||
.map_err(|ref e| {
|
||||
println!("Error preparing stream: {}", e);
|
||||
BuildStreamError::DeviceNotAvailable
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Stream {
|
||||
fn drop(&mut self) {
|
||||
self.driver.remove_callback(self.callback_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn asio_ns_to_double(val: sys::bindings::asio_import::ASIOTimeStamp) -> f64 {
|
||||
let two_raised_to_32 = 4294967296.0;
|
||||
val.lo as f64 + val.hi as f64 * two_raised_to_32
|
||||
}
|
||||
|
||||
/// Asio retrieves system time via `timeGetTime` which returns the time in milliseconds.
|
||||
fn system_time_to_stream_instant(
|
||||
system_time: sys::bindings::asio_import::ASIOTimeStamp,
|
||||
) -> crate::StreamInstant {
|
||||
let systime_ns = asio_ns_to_double(system_time);
|
||||
let secs = systime_ns as i64 / 1_000_000_000;
|
||||
let nanos = (systime_ns as i64 - secs * 1_000_000_000) as u32;
|
||||
crate::StreamInstant::new(secs, nanos)
|
||||
}
|
||||
|
||||
/// Convert the given duration in frames at the given sample rate to a `std::time::Duration`.
|
||||
fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration {
|
||||
let secsf = frames as f64 / rate.0 as f64;
|
||||
let secs = secsf as u64;
|
||||
let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32;
|
||||
std::time::Duration::new(secs, nanos)
|
||||
}
|
||||
|
||||
/// Check whether or not the desired config is supported by the stream.
|
||||
///
|
||||
/// Checks sample rate, data type and then finally the number of channels.
|
||||
fn check_config(
|
||||
driver: &sys::Driver,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
num_asio_channels: u16,
|
||||
) -> Result<(), BuildStreamError> {
|
||||
let StreamConfig {
|
||||
channels,
|
||||
sample_rate,
|
||||
buffer_size: _,
|
||||
} = config;
|
||||
// Try and set the sample rate to what the user selected.
|
||||
let sample_rate = sample_rate.0.into();
|
||||
if sample_rate != driver.sample_rate().map_err(build_stream_err)? {
|
||||
if driver
|
||||
.can_sample_rate(sample_rate)
|
||||
.map_err(build_stream_err)?
|
||||
{
|
||||
driver
|
||||
.set_sample_rate(sample_rate)
|
||||
.map_err(build_stream_err)?;
|
||||
} else {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
}
|
||||
// unsigned formats are not supported by asio
|
||||
match sample_format {
|
||||
SampleFormat::I16 | SampleFormat::I32 | SampleFormat::F32 => (),
|
||||
_ => return Err(BuildStreamError::StreamConfigNotSupported),
|
||||
}
|
||||
if *channels > num_asio_channels {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cast a byte slice into a mutable slice of desired type.
|
||||
///
|
||||
/// Safety: it's up to the caller to ensure that the input slice has valid bit representations.
|
||||
unsafe fn cast_slice_mut<T>(v: &mut [u8]) -> &mut [T] {
|
||||
debug_assert!(v.len() % std::mem::size_of::<T>() == 0);
|
||||
std::slice::from_raw_parts_mut(v.as_mut_ptr() as *mut T, v.len() / std::mem::size_of::<T>())
|
||||
}
|
||||
|
||||
/// Helper function to convert from little endianness.
|
||||
fn from_le<T: PrimInt>(t: T) -> T {
|
||||
T::from_le(t)
|
||||
}
|
||||
|
||||
/// Helper function to convert from little endianness.
|
||||
fn from_be<T: PrimInt>(t: T) -> T {
|
||||
T::from_be(t)
|
||||
}
|
||||
|
||||
/// Shorthand for retrieving the asio buffer slice associated with a channel.
|
||||
unsafe fn asio_channel_slice<T>(
|
||||
asio_stream: &sys::AsioStream,
|
||||
buffer_index: usize,
|
||||
channel_index: usize,
|
||||
) -> &[T] {
|
||||
let buff_ptr: *const T =
|
||||
asio_stream.buffer_infos[channel_index].buffers[buffer_index as usize] as *const _;
|
||||
std::slice::from_raw_parts(buff_ptr, asio_stream.buffer_size as usize)
|
||||
}
|
||||
|
||||
/// Shorthand for retrieving the asio buffer slice associated with a channel.
|
||||
unsafe fn asio_channel_slice_mut<T>(
|
||||
asio_stream: &mut sys::AsioStream,
|
||||
buffer_index: usize,
|
||||
channel_index: usize,
|
||||
) -> &mut [T] {
|
||||
let buff_ptr: *mut T =
|
||||
asio_stream.buffer_infos[channel_index].buffers[buffer_index as usize] as *mut _;
|
||||
std::slice::from_raw_parts_mut(buff_ptr, asio_stream.buffer_size as usize)
|
||||
}
|
||||
|
||||
fn build_stream_err(e: sys::AsioError) -> BuildStreamError {
|
||||
match e {
|
||||
sys::AsioError::NoDrivers | sys::AsioError::HardwareMalfunction => {
|
||||
BuildStreamError::DeviceNotAvailable
|
||||
}
|
||||
sys::AsioError::InvalidInput | sys::AsioError::BadMode => BuildStreamError::InvalidArgument,
|
||||
err => {
|
||||
let description = format!("{}", err);
|
||||
BackendSpecificError { description }.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
use std::vec::IntoIter as VecIntoIter;
|
||||
|
||||
use crate::DevicesError;
|
||||
use crate::SupportedStreamConfigRange;
|
||||
|
||||
use super::Device;
|
||||
|
||||
pub type SupportedInputConfigs = ::std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
pub type SupportedOutputConfigs = ::std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
|
||||
// TODO: Support enumerating earpiece vs headset vs speaker etc?
|
||||
pub struct Devices(VecIntoIter<Device>);
|
||||
|
||||
impl Devices {
|
||||
pub fn new() -> Result<Self, DevicesError> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Devices {
|
||||
fn default() -> Devices {
|
||||
Devices(vec![Device].into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Devices {
|
||||
type Item = Device;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Device> {
|
||||
self.0.next()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn default_input_device() -> Option<Device> {
|
||||
Some(Device)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn default_output_device() -> Option<Device> {
|
||||
Some(Device)
|
||||
}
|
||||
-434
@@ -1,434 +0,0 @@
|
||||
//!
|
||||
//! coreaudio on iOS looks a bit different from macOS. A lot of configuration needs to use
|
||||
//! the AVAudioSession objc API which doesn't exist on macOS.
|
||||
//!
|
||||
//! TODO:
|
||||
//! - Use AVAudioSession to enumerate buffer size / sample rate / number of channels and set
|
||||
//! buffer size.
|
||||
//!
|
||||
|
||||
extern crate core_foundation_sys;
|
||||
extern crate coreaudio;
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use self::coreaudio::audio_unit::render_callback::data;
|
||||
use self::coreaudio::audio_unit::{render_callback, AudioUnit, Element, Scope};
|
||||
use self::coreaudio::sys::{
|
||||
kAudioOutputUnitProperty_EnableIO, kAudioUnitProperty_StreamFormat, AudioBuffer,
|
||||
AudioStreamBasicDescription,
|
||||
};
|
||||
|
||||
use super::{asbd_from_config, frames_to_duration, host_time_to_stream_instant};
|
||||
use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
|
||||
use crate::{
|
||||
BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError,
|
||||
DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError,
|
||||
PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize,
|
||||
SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError,
|
||||
};
|
||||
|
||||
use self::enumerate::{
|
||||
default_input_device, default_output_device, Devices, SupportedInputConfigs,
|
||||
SupportedOutputConfigs,
|
||||
};
|
||||
use std::slice;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod enumerate;
|
||||
|
||||
// These days the default of iOS is now F32 and no longer I16
|
||||
const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Device;
|
||||
|
||||
pub struct Host;
|
||||
|
||||
impl Host {
|
||||
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
||||
Ok(Host)
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTrait for Host {
|
||||
type Devices = Devices;
|
||||
type Device = Device;
|
||||
|
||||
fn is_available() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn devices(&self) -> Result<Self::Devices, DevicesError> {
|
||||
Devices::new()
|
||||
}
|
||||
|
||||
fn default_input_device(&self) -> Option<Self::Device> {
|
||||
default_input_device()
|
||||
}
|
||||
|
||||
fn default_output_device(&self) -> Option<Self::Device> {
|
||||
default_output_device()
|
||||
}
|
||||
}
|
||||
|
||||
impl Device {
|
||||
#[inline]
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
Ok("Default Device".to_owned())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
// TODO: query AVAudioSession for parameters, some values like sample rate and buffer size
|
||||
// probably need to actually be set to see if it works, but channels can be enumerated.
|
||||
|
||||
let asbd: AudioStreamBasicDescription = default_input_asbd()?;
|
||||
let stream_config = stream_config_from_asbd(asbd);
|
||||
Ok(vec![SupportedStreamConfigRange {
|
||||
channels: stream_config.channels,
|
||||
min_sample_rate: stream_config.sample_rate,
|
||||
max_sample_rate: stream_config.sample_rate,
|
||||
buffer_size: stream_config.buffer_size.clone(),
|
||||
sample_format: SUPPORTED_SAMPLE_FORMAT,
|
||||
}]
|
||||
.into_iter())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
// TODO: query AVAudioSession for parameters, some values like sample rate and buffer size
|
||||
// probably need to actually be set to see if it works, but channels can be enumerated.
|
||||
|
||||
let asbd: AudioStreamBasicDescription = default_output_asbd()?;
|
||||
let stream_config = stream_config_from_asbd(asbd);
|
||||
|
||||
let configs: Vec<_> = (1..=asbd.mChannelsPerFrame as u16)
|
||||
.map(|channels| SupportedStreamConfigRange {
|
||||
channels,
|
||||
min_sample_rate: stream_config.sample_rate,
|
||||
max_sample_rate: stream_config.sample_rate,
|
||||
buffer_size: stream_config.buffer_size.clone(),
|
||||
sample_format: SUPPORTED_SAMPLE_FORMAT,
|
||||
})
|
||||
.collect();
|
||||
Ok(configs.into_iter())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
let asbd: AudioStreamBasicDescription = default_input_asbd()?;
|
||||
let stream_config = stream_config_from_asbd(asbd);
|
||||
Ok(stream_config)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
let asbd: AudioStreamBasicDescription = default_output_asbd()?;
|
||||
let stream_config = stream_config_from_asbd(asbd);
|
||||
Ok(stream_config)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceTrait for Device {
|
||||
type SupportedInputConfigs = SupportedInputConfigs;
|
||||
type SupportedOutputConfigs = SupportedOutputConfigs;
|
||||
type Stream = Stream;
|
||||
|
||||
#[inline]
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
Device::name(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
Device::supported_input_configs(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
Device::supported_output_configs(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
Device::default_input_config(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
Device::default_output_config(self)
|
||||
}
|
||||
|
||||
fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
mut data_callback: D,
|
||||
mut error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
// The scope and element for working with a device's input stream.
|
||||
let scope = Scope::Output;
|
||||
let element = Element::Input;
|
||||
|
||||
let mut audio_unit = create_audio_unit()?;
|
||||
audio_unit.uninitialize()?;
|
||||
configure_for_recording(&mut audio_unit)?;
|
||||
audio_unit.initialize()?;
|
||||
|
||||
// Set the stream in interleaved mode.
|
||||
let asbd = asbd_from_config(config, sample_format);
|
||||
audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?;
|
||||
|
||||
// Set the buffersize
|
||||
match config.buffer_size {
|
||||
BufferSize::Fixed(_) => {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
BufferSize::Default => (),
|
||||
}
|
||||
|
||||
// Register the callback that is being called by coreaudio whenever it needs data to be
|
||||
// fed to the audio buffer.
|
||||
let bytes_per_channel = sample_format.sample_size();
|
||||
let sample_rate = config.sample_rate;
|
||||
type Args = render_callback::Args<data::Raw>;
|
||||
audio_unit.set_input_callback(move |args: Args| unsafe {
|
||||
let ptr = (*args.data.data).mBuffers.as_ptr() as *const AudioBuffer;
|
||||
let len = (*args.data.data).mNumberBuffers as usize;
|
||||
let buffers: &[AudioBuffer] = slice::from_raw_parts(ptr, len);
|
||||
|
||||
// There is only 1 buffer when using interleaved channels
|
||||
let AudioBuffer {
|
||||
mNumberChannels: channels,
|
||||
mDataByteSize: data_byte_size,
|
||||
mData: data,
|
||||
} = buffers[0];
|
||||
|
||||
let data = data as *mut ();
|
||||
let len = (data_byte_size as usize / bytes_per_channel) as usize;
|
||||
let data = Data::from_parts(data, len, sample_format);
|
||||
|
||||
// TODO: Need a better way to get delay, for now we assume a double-buffer offset.
|
||||
let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) {
|
||||
Err(err) => {
|
||||
error_callback(err.into());
|
||||
return Err(());
|
||||
}
|
||||
Ok(cb) => cb,
|
||||
};
|
||||
let buffer_frames = len / channels as usize;
|
||||
let delay = frames_to_duration(buffer_frames, sample_rate);
|
||||
let capture = callback
|
||||
.sub(delay)
|
||||
.expect("`capture` occurs before origin of alsa `StreamInstant`");
|
||||
let timestamp = crate::InputStreamTimestamp { callback, capture };
|
||||
|
||||
let info = InputCallbackInfo { timestamp };
|
||||
data_callback(&data, &info);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
audio_unit.start()?;
|
||||
|
||||
Ok(Stream::new(StreamInner {
|
||||
playing: true,
|
||||
audio_unit,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create an output stream.
|
||||
fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
mut data_callback: D,
|
||||
mut error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
match config.buffer_size {
|
||||
BufferSize::Fixed(_) => {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
BufferSize::Default => (),
|
||||
};
|
||||
|
||||
let mut audio_unit = create_audio_unit()?;
|
||||
|
||||
// The scope and element for working with a device's output stream.
|
||||
let scope = Scope::Input;
|
||||
let element = Element::Output;
|
||||
|
||||
// Set the stream in interleaved mode.
|
||||
let asbd = asbd_from_config(config, sample_format);
|
||||
audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?;
|
||||
|
||||
// Register the callback that is being called by coreaudio whenever it needs data to be
|
||||
// fed to the audio buffer.
|
||||
let bytes_per_channel = sample_format.sample_size();
|
||||
let sample_rate = config.sample_rate;
|
||||
type Args = render_callback::Args<data::Raw>;
|
||||
audio_unit.set_render_callback(move |args: Args| unsafe {
|
||||
// If `run()` is currently running, then a callback will be available from this list.
|
||||
// Otherwise, we just fill the buffer with zeroes and return.
|
||||
|
||||
let AudioBuffer {
|
||||
mNumberChannels: channels,
|
||||
mDataByteSize: data_byte_size,
|
||||
mData: data,
|
||||
} = (*args.data.data).mBuffers[0];
|
||||
|
||||
let data = data as *mut ();
|
||||
let len = (data_byte_size as usize / bytes_per_channel) as usize;
|
||||
let mut data = Data::from_parts(data, len, sample_format);
|
||||
|
||||
let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) {
|
||||
Err(err) => {
|
||||
error_callback(err.into());
|
||||
return Err(());
|
||||
}
|
||||
Ok(cb) => cb,
|
||||
};
|
||||
// TODO: Need a better way to get delay, for now we assume a double-buffer offset.
|
||||
let buffer_frames = len / channels as usize;
|
||||
let delay = frames_to_duration(buffer_frames, sample_rate);
|
||||
let playback = callback
|
||||
.add(delay)
|
||||
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
|
||||
let timestamp = crate::OutputStreamTimestamp { callback, playback };
|
||||
|
||||
let info = OutputCallbackInfo { timestamp };
|
||||
data_callback(&mut data, &info);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
audio_unit.start()?;
|
||||
|
||||
Ok(Stream::new(StreamInner {
|
||||
playing: true,
|
||||
audio_unit,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Stream {
|
||||
inner: RefCell<StreamInner>,
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
fn new(inner: StreamInner) -> Self {
|
||||
Self {
|
||||
inner: RefCell::new(inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTrait for Stream {
|
||||
fn play(&self) -> Result<(), PlayStreamError> {
|
||||
let mut stream = self.inner.borrow_mut();
|
||||
|
||||
if !stream.playing {
|
||||
if let Err(e) = stream.audio_unit.start() {
|
||||
let description = format!("{}", e);
|
||||
let err = BackendSpecificError { description };
|
||||
return Err(err.into());
|
||||
}
|
||||
stream.playing = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<(), PauseStreamError> {
|
||||
let mut stream = self.inner.borrow_mut();
|
||||
|
||||
if stream.playing {
|
||||
if let Err(e) = stream.audio_unit.stop() {
|
||||
let description = format!("{}", e);
|
||||
let err = BackendSpecificError { description };
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
stream.playing = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct StreamInner {
|
||||
playing: bool,
|
||||
audio_unit: AudioUnit,
|
||||
}
|
||||
|
||||
fn create_audio_unit() -> Result<AudioUnit, coreaudio::Error> {
|
||||
AudioUnit::new(coreaudio::audio_unit::IOType::RemoteIO)
|
||||
}
|
||||
|
||||
fn configure_for_recording(audio_unit: &mut AudioUnit) -> Result<(), coreaudio::Error> {
|
||||
// Enable mic recording
|
||||
let enable_input = 1u32;
|
||||
audio_unit.set_property(
|
||||
kAudioOutputUnitProperty_EnableIO,
|
||||
Scope::Input,
|
||||
Element::Input,
|
||||
Some(&enable_input),
|
||||
)?;
|
||||
|
||||
// Disable output
|
||||
let disable_output = 0u32;
|
||||
audio_unit.set_property(
|
||||
kAudioOutputUnitProperty_EnableIO,
|
||||
Scope::Output,
|
||||
Element::Output,
|
||||
Some(&disable_output),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn default_output_asbd() -> Result<AudioStreamBasicDescription, coreaudio::Error> {
|
||||
let audio_unit = create_audio_unit()?;
|
||||
let id = kAudioUnitProperty_StreamFormat;
|
||||
let asbd: AudioStreamBasicDescription =
|
||||
audio_unit.get_property(id, Scope::Output, Element::Output)?;
|
||||
Ok(asbd)
|
||||
}
|
||||
|
||||
fn default_input_asbd() -> Result<AudioStreamBasicDescription, coreaudio::Error> {
|
||||
let mut audio_unit = create_audio_unit()?;
|
||||
audio_unit.uninitialize()?;
|
||||
configure_for_recording(&mut audio_unit)?;
|
||||
audio_unit.initialize()?;
|
||||
|
||||
let id = kAudioUnitProperty_StreamFormat;
|
||||
let asbd: AudioStreamBasicDescription =
|
||||
audio_unit.get_property(id, Scope::Input, Element::Input)?;
|
||||
Ok(asbd)
|
||||
}
|
||||
|
||||
fn stream_config_from_asbd(asbd: AudioStreamBasicDescription) -> SupportedStreamConfig {
|
||||
let buffer_size = SupportedBufferSize::Range { min: 0, max: 0 };
|
||||
SupportedStreamConfig {
|
||||
channels: asbd.mChannelsPerFrame as u16,
|
||||
sample_rate: SampleRate(asbd.mSampleRate as u32),
|
||||
buffer_size: buffer_size.clone(),
|
||||
sample_format: SUPPORTED_SAMPLE_FORMAT,
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
extern crate coreaudio;
|
||||
|
||||
use self::coreaudio::sys::{
|
||||
kAudioHardwareNoError, kAudioHardwarePropertyDefaultInputDevice,
|
||||
kAudioHardwarePropertyDefaultOutputDevice, kAudioHardwarePropertyDevices,
|
||||
kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeGlobal, kAudioObjectSystemObject,
|
||||
AudioDeviceID, AudioObjectGetPropertyData, AudioObjectGetPropertyDataSize,
|
||||
AudioObjectPropertyAddress, OSStatus,
|
||||
};
|
||||
use super::Device;
|
||||
use crate::{BackendSpecificError, DevicesError, SupportedStreamConfigRange};
|
||||
use std::mem;
|
||||
use std::ptr::null;
|
||||
use std::vec::IntoIter as VecIntoIter;
|
||||
|
||||
unsafe fn audio_devices() -> Result<Vec<AudioDeviceID>, OSStatus> {
|
||||
let property_address = AudioObjectPropertyAddress {
|
||||
mSelector: kAudioHardwarePropertyDevices,
|
||||
mScope: kAudioObjectPropertyScopeGlobal,
|
||||
mElement: kAudioObjectPropertyElementMaster,
|
||||
};
|
||||
|
||||
macro_rules! try_status_or_return {
|
||||
($status:expr) => {
|
||||
if $status != kAudioHardwareNoError as i32 {
|
||||
return Err($status);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let data_size = 0u32;
|
||||
let status = AudioObjectGetPropertyDataSize(
|
||||
kAudioObjectSystemObject,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
);
|
||||
try_status_or_return!(status);
|
||||
|
||||
let device_count = data_size / mem::size_of::<AudioDeviceID>() as u32;
|
||||
let mut audio_devices = vec![];
|
||||
audio_devices.reserve_exact(device_count as usize);
|
||||
|
||||
let status = AudioObjectGetPropertyData(
|
||||
kAudioObjectSystemObject,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
audio_devices.as_mut_ptr() as *mut _,
|
||||
);
|
||||
try_status_or_return!(status);
|
||||
|
||||
audio_devices.set_len(device_count as usize);
|
||||
|
||||
Ok(audio_devices)
|
||||
}
|
||||
|
||||
pub struct Devices(VecIntoIter<AudioDeviceID>);
|
||||
|
||||
impl Devices {
|
||||
pub fn new() -> Result<Self, DevicesError> {
|
||||
let devices = unsafe {
|
||||
match audio_devices() {
|
||||
Ok(devices) => devices,
|
||||
Err(os_status) => {
|
||||
let description = format!("{}", os_status);
|
||||
let err = BackendSpecificError { description };
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Devices(devices.into_iter()))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for Devices {}
|
||||
unsafe impl Sync for Devices {}
|
||||
|
||||
impl Iterator for Devices {
|
||||
type Item = Device;
|
||||
fn next(&mut self) -> Option<Device> {
|
||||
self.0.next().map(|id| Device {
|
||||
audio_device_id: id,
|
||||
is_default: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_input_device() -> Option<Device> {
|
||||
let property_address = AudioObjectPropertyAddress {
|
||||
mSelector: kAudioHardwarePropertyDefaultInputDevice,
|
||||
mScope: kAudioObjectPropertyScopeGlobal,
|
||||
mElement: kAudioObjectPropertyElementMaster,
|
||||
};
|
||||
|
||||
let audio_device_id: AudioDeviceID = 0;
|
||||
let data_size = mem::size_of::<AudioDeviceID>();
|
||||
let status = unsafe {
|
||||
AudioObjectGetPropertyData(
|
||||
kAudioObjectSystemObject,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
&audio_device_id as *const _ as *mut _,
|
||||
)
|
||||
};
|
||||
if status != kAudioHardwareNoError as i32 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let device = Device {
|
||||
audio_device_id,
|
||||
is_default: true,
|
||||
};
|
||||
Some(device)
|
||||
}
|
||||
|
||||
pub fn default_output_device() -> Option<Device> {
|
||||
let property_address = AudioObjectPropertyAddress {
|
||||
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
|
||||
mScope: kAudioObjectPropertyScopeGlobal,
|
||||
mElement: kAudioObjectPropertyElementMaster,
|
||||
};
|
||||
|
||||
let audio_device_id: AudioDeviceID = 0;
|
||||
let data_size = mem::size_of::<AudioDeviceID>();
|
||||
let status = unsafe {
|
||||
AudioObjectGetPropertyData(
|
||||
kAudioObjectSystemObject,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
&audio_device_id as *const _ as *mut _,
|
||||
)
|
||||
};
|
||||
if status != kAudioHardwareNoError as i32 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let device = Device {
|
||||
audio_device_id,
|
||||
is_default: true,
|
||||
};
|
||||
Some(device)
|
||||
}
|
||||
|
||||
pub type SupportedInputConfigs = VecIntoIter<SupportedStreamConfigRange>;
|
||||
pub type SupportedOutputConfigs = VecIntoIter<SupportedStreamConfigRange>;
|
||||
@@ -1,941 +0,0 @@
|
||||
extern crate core_foundation_sys;
|
||||
extern crate coreaudio;
|
||||
|
||||
use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_stream_instant};
|
||||
|
||||
use self::core_foundation_sys::string::{CFStringGetCString, CFStringGetCStringPtr, CFStringRef};
|
||||
use self::coreaudio::audio_unit::render_callback::{self, data};
|
||||
use self::coreaudio::audio_unit::{AudioUnit, Element, Scope};
|
||||
use self::coreaudio::sys::{
|
||||
kAudioDevicePropertyAvailableNominalSampleRates, kAudioDevicePropertyBufferFrameSize,
|
||||
kAudioDevicePropertyBufferFrameSizeRange, kAudioDevicePropertyDeviceIsAlive,
|
||||
kAudioDevicePropertyDeviceNameCFString, kAudioDevicePropertyNominalSampleRate,
|
||||
kAudioDevicePropertyScopeOutput, kAudioDevicePropertyStreamConfiguration,
|
||||
kAudioDevicePropertyStreamFormat, kAudioObjectPropertyElementMaster,
|
||||
kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput,
|
||||
kAudioObjectPropertyScopeOutput, kAudioOutputUnitProperty_CurrentDevice,
|
||||
kAudioOutputUnitProperty_EnableIO, kAudioUnitProperty_StreamFormat, kCFStringEncodingUTF8,
|
||||
AudioBuffer, AudioBufferList, AudioDeviceID, AudioObjectGetPropertyData,
|
||||
AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress,
|
||||
AudioObjectPropertyScope, AudioObjectSetPropertyData, AudioStreamBasicDescription,
|
||||
AudioValueRange, OSStatus,
|
||||
};
|
||||
use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use crate::{
|
||||
BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data,
|
||||
DefaultStreamConfigError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo,
|
||||
PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError,
|
||||
SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange,
|
||||
SupportedStreamConfigsError,
|
||||
};
|
||||
use std::ffi::CStr;
|
||||
use std::fmt;
|
||||
use std::mem;
|
||||
use std::os::raw::c_char;
|
||||
use std::ptr::null;
|
||||
use std::rc::Rc;
|
||||
use std::slice;
|
||||
use std::sync::mpsc::{channel, RecvTimeoutError};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub use self::enumerate::{
|
||||
default_input_device, default_output_device, Devices, SupportedInputConfigs,
|
||||
SupportedOutputConfigs,
|
||||
};
|
||||
|
||||
use property_listener::AudioObjectPropertyListener;
|
||||
|
||||
pub mod enumerate;
|
||||
mod property_listener;
|
||||
|
||||
/// Coreaudio host, the default host on macOS.
|
||||
#[derive(Debug)]
|
||||
pub struct Host;
|
||||
|
||||
impl Host {
|
||||
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
||||
Ok(Host)
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTrait for Host {
|
||||
type Devices = Devices;
|
||||
type Device = Device;
|
||||
|
||||
fn is_available() -> bool {
|
||||
// Assume coreaudio is always available
|
||||
true
|
||||
}
|
||||
|
||||
fn devices(&self) -> Result<Self::Devices, DevicesError> {
|
||||
Devices::new()
|
||||
}
|
||||
|
||||
fn default_input_device(&self) -> Option<Self::Device> {
|
||||
default_input_device()
|
||||
}
|
||||
|
||||
fn default_output_device(&self) -> Option<Self::Device> {
|
||||
default_output_device()
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceTrait for Device {
|
||||
type SupportedInputConfigs = SupportedInputConfigs;
|
||||
type SupportedOutputConfigs = SupportedOutputConfigs;
|
||||
type Stream = Stream;
|
||||
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
Device::name(self)
|
||||
}
|
||||
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
Device::supported_input_configs(self)
|
||||
}
|
||||
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
Device::supported_output_configs(self)
|
||||
}
|
||||
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
Device::default_input_config(self)
|
||||
}
|
||||
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
Device::default_output_config(self)
|
||||
}
|
||||
|
||||
fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
Device::build_input_stream_raw(
|
||||
self,
|
||||
config,
|
||||
sample_format,
|
||||
data_callback,
|
||||
error_callback,
|
||||
timeout,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
Device::build_output_stream_raw(
|
||||
self,
|
||||
config,
|
||||
sample_format,
|
||||
data_callback,
|
||||
error_callback,
|
||||
timeout,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Device {
|
||||
pub(crate) audio_device_id: AudioDeviceID,
|
||||
is_default: bool,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
let property_address = AudioObjectPropertyAddress {
|
||||
mSelector: kAudioDevicePropertyDeviceNameCFString,
|
||||
mScope: kAudioDevicePropertyScopeOutput,
|
||||
mElement: kAudioObjectPropertyElementMaster,
|
||||
};
|
||||
let device_name: CFStringRef = null();
|
||||
let data_size = mem::size_of::<CFStringRef>();
|
||||
let c_str = unsafe {
|
||||
let status = AudioObjectGetPropertyData(
|
||||
self.audio_device_id,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
&device_name as *const _ as *mut _,
|
||||
);
|
||||
check_os_status(status)?;
|
||||
|
||||
let c_string: *const c_char = CFStringGetCStringPtr(device_name, kCFStringEncodingUTF8);
|
||||
if c_string.is_null() {
|
||||
let status = AudioObjectGetPropertyData(
|
||||
self.audio_device_id,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
&device_name as *const _ as *mut _,
|
||||
);
|
||||
check_os_status(status)?;
|
||||
let mut buf: [i8; 255] = [0; 255];
|
||||
let result = CFStringGetCString(
|
||||
device_name,
|
||||
buf.as_mut_ptr(),
|
||||
buf.len() as _,
|
||||
kCFStringEncodingUTF8,
|
||||
);
|
||||
if result == 0 {
|
||||
let description =
|
||||
"core foundation failed to return device name string".to_string();
|
||||
let err = BackendSpecificError { description };
|
||||
return Err(err.into());
|
||||
}
|
||||
let name: &CStr = CStr::from_ptr(buf.as_ptr());
|
||||
return Ok(name.to_str().unwrap().to_owned());
|
||||
}
|
||||
CStr::from_ptr(c_string as *mut _)
|
||||
};
|
||||
Ok(c_str.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
// Logic re-used between `supported_input_configs` and `supported_output_configs`.
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
fn supported_configs(
|
||||
&self,
|
||||
scope: AudioObjectPropertyScope,
|
||||
) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
let mut property_address = AudioObjectPropertyAddress {
|
||||
mSelector: kAudioDevicePropertyStreamConfiguration,
|
||||
mScope: scope,
|
||||
mElement: kAudioObjectPropertyElementMaster,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
// Retrieve the devices audio buffer list.
|
||||
let data_size = 0u32;
|
||||
let status = AudioObjectGetPropertyDataSize(
|
||||
self.audio_device_id,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
);
|
||||
check_os_status(status)?;
|
||||
|
||||
let mut audio_buffer_list: Vec<u8> = vec![];
|
||||
audio_buffer_list.reserve_exact(data_size as usize);
|
||||
let status = AudioObjectGetPropertyData(
|
||||
self.audio_device_id,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
audio_buffer_list.as_mut_ptr() as *mut _,
|
||||
);
|
||||
check_os_status(status)?;
|
||||
|
||||
let audio_buffer_list = audio_buffer_list.as_mut_ptr() as *mut AudioBufferList;
|
||||
|
||||
// If there's no buffers, skip.
|
||||
if (*audio_buffer_list).mNumberBuffers == 0 {
|
||||
return Ok(vec![].into_iter());
|
||||
}
|
||||
|
||||
// Count the number of channels as the sum of all channels in all output buffers.
|
||||
let n_buffers = (*audio_buffer_list).mNumberBuffers as usize;
|
||||
let first: *const AudioBuffer = (*audio_buffer_list).mBuffers.as_ptr();
|
||||
let buffers: &'static [AudioBuffer] = slice::from_raw_parts(first, n_buffers);
|
||||
let mut n_channels = 0;
|
||||
for buffer in buffers {
|
||||
n_channels += buffer.mNumberChannels as usize;
|
||||
}
|
||||
|
||||
// TODO: macOS should support U8, I16, I32, F32 and F64. This should allow for using
|
||||
// I16 but just use F32 for now as it's the default anyway.
|
||||
let sample_format = SampleFormat::F32;
|
||||
|
||||
// Get available sample rate ranges.
|
||||
property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates;
|
||||
let data_size = 0u32;
|
||||
let status = AudioObjectGetPropertyDataSize(
|
||||
self.audio_device_id,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
);
|
||||
check_os_status(status)?;
|
||||
|
||||
let n_ranges = data_size as usize / mem::size_of::<AudioValueRange>();
|
||||
let mut ranges: Vec<u8> = vec![];
|
||||
ranges.reserve_exact(data_size as usize);
|
||||
let status = AudioObjectGetPropertyData(
|
||||
self.audio_device_id,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
ranges.as_mut_ptr() as *mut _,
|
||||
);
|
||||
check_os_status(status)?;
|
||||
|
||||
let ranges: *mut AudioValueRange = ranges.as_mut_ptr() as *mut _;
|
||||
let ranges: &'static [AudioValueRange] = slice::from_raw_parts(ranges, n_ranges);
|
||||
|
||||
let audio_unit = audio_unit_from_device(self, true)?;
|
||||
let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?;
|
||||
|
||||
// Collect the supported formats for the device.
|
||||
let mut fmts = vec![];
|
||||
for range in ranges {
|
||||
let fmt = SupportedStreamConfigRange {
|
||||
channels: n_channels as ChannelCount,
|
||||
min_sample_rate: SampleRate(range.mMinimum as _),
|
||||
max_sample_rate: SampleRate(range.mMaximum as _),
|
||||
buffer_size,
|
||||
sample_format,
|
||||
};
|
||||
fmts.push(fmt);
|
||||
}
|
||||
|
||||
Ok(fmts.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
self.supported_configs(kAudioObjectPropertyScopeInput)
|
||||
}
|
||||
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
self.supported_configs(kAudioObjectPropertyScopeOutput)
|
||||
}
|
||||
|
||||
fn default_config(
|
||||
&self,
|
||||
scope: AudioObjectPropertyScope,
|
||||
) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
fn default_config_error_from_os_status(
|
||||
status: OSStatus,
|
||||
) -> Result<(), DefaultStreamConfigError> {
|
||||
let err = match coreaudio::Error::from_os_status(status) {
|
||||
Err(err) => err,
|
||||
Ok(_) => return Ok(()),
|
||||
};
|
||||
match err {
|
||||
coreaudio::Error::AudioUnit(
|
||||
coreaudio::error::AudioUnitError::FormatNotSupported,
|
||||
)
|
||||
| coreaudio::Error::AudioCodec(_)
|
||||
| coreaudio::Error::AudioFormat(_) => {
|
||||
Err(DefaultStreamConfigError::StreamTypeNotSupported)
|
||||
}
|
||||
coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::NoConnection) => {
|
||||
Err(DefaultStreamConfigError::DeviceNotAvailable)
|
||||
}
|
||||
err => {
|
||||
let description = format!("{}", err);
|
||||
let err = BackendSpecificError { description };
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let property_address = AudioObjectPropertyAddress {
|
||||
mSelector: kAudioDevicePropertyStreamFormat,
|
||||
mScope: scope,
|
||||
mElement: kAudioObjectPropertyElementMaster,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let asbd: AudioStreamBasicDescription = mem::zeroed();
|
||||
let data_size = mem::size_of::<AudioStreamBasicDescription>() as u32;
|
||||
let status = AudioObjectGetPropertyData(
|
||||
self.audio_device_id,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
&asbd as *const _ as *mut _,
|
||||
);
|
||||
default_config_error_from_os_status(status)?;
|
||||
|
||||
let sample_format = {
|
||||
let audio_format = coreaudio::audio_unit::AudioFormat::from_format_and_flag(
|
||||
asbd.mFormatID,
|
||||
Some(asbd.mFormatFlags),
|
||||
);
|
||||
let flags = match audio_format {
|
||||
Some(coreaudio::audio_unit::AudioFormat::LinearPCM(flags)) => flags,
|
||||
_ => return Err(DefaultStreamConfigError::StreamTypeNotSupported),
|
||||
};
|
||||
let maybe_sample_format =
|
||||
coreaudio::audio_unit::SampleFormat::from_flags_and_bits_per_sample(
|
||||
flags,
|
||||
asbd.mBitsPerChannel,
|
||||
);
|
||||
match maybe_sample_format {
|
||||
Some(coreaudio::audio_unit::SampleFormat::F32) => SampleFormat::F32,
|
||||
Some(coreaudio::audio_unit::SampleFormat::I16) => SampleFormat::I16,
|
||||
_ => return Err(DefaultStreamConfigError::StreamTypeNotSupported),
|
||||
}
|
||||
};
|
||||
|
||||
let audio_unit = audio_unit_from_device(self, true)?;
|
||||
let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?;
|
||||
|
||||
let config = SupportedStreamConfig {
|
||||
sample_rate: SampleRate(asbd.mSampleRate as _),
|
||||
channels: asbd.mChannelsPerFrame as _,
|
||||
buffer_size,
|
||||
sample_format,
|
||||
};
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
self.default_config(kAudioObjectPropertyScopeInput)
|
||||
}
|
||||
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
self.default_config(kAudioObjectPropertyScopeOutput)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Device {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.debug_struct("Device")
|
||||
.field("audio_device_id", &self.audio_device_id)
|
||||
.field("name", &self.name())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
struct StreamInner {
|
||||
playing: bool,
|
||||
audio_unit: AudioUnit,
|
||||
/// Manage the lifetime of the closure that handles device disconnection.
|
||||
_disconnect_listener: Option<AudioObjectPropertyListener>,
|
||||
// Track the device with which the audio unit was spawned.
|
||||
//
|
||||
// We must do this so that we can avoid changing the device sample rate if there is already
|
||||
// a stream associated with the device.
|
||||
#[allow(dead_code)]
|
||||
device_id: AudioDeviceID,
|
||||
}
|
||||
|
||||
/// Register the on-disconnect callback.
|
||||
/// This will both stop the stream and call the error callback with DeviceNotAvailable.
|
||||
/// This function should only be called once per stream.
|
||||
fn add_disconnect_listener<E>(
|
||||
stream: &Stream,
|
||||
error_callback: Arc<Mutex<E>>,
|
||||
) -> Result<(), BuildStreamError>
|
||||
where
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
let stream_copy = stream.clone();
|
||||
let mut stream_inner = stream.inner.lock().unwrap();
|
||||
stream_inner._disconnect_listener = Some(AudioObjectPropertyListener::new(
|
||||
stream_inner.device_id,
|
||||
AudioObjectPropertyAddress {
|
||||
mSelector: kAudioDevicePropertyDeviceIsAlive,
|
||||
mScope: kAudioObjectPropertyScopeGlobal,
|
||||
mElement: kAudioObjectPropertyElementMaster,
|
||||
},
|
||||
move || {
|
||||
let _ = stream_copy.pause();
|
||||
(error_callback.lock().unwrap())(StreamError::DeviceNotAvailable);
|
||||
},
|
||||
)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn audio_unit_from_device(device: &Device, input: bool) -> Result<AudioUnit, coreaudio::Error> {
|
||||
let output_type = if device.is_default && !input {
|
||||
coreaudio::audio_unit::IOType::DefaultOutput
|
||||
} else {
|
||||
coreaudio::audio_unit::IOType::HalOutput
|
||||
};
|
||||
let mut audio_unit = AudioUnit::new(output_type)?;
|
||||
|
||||
if input {
|
||||
// Enable input processing.
|
||||
let enable_input = 1u32;
|
||||
audio_unit.set_property(
|
||||
kAudioOutputUnitProperty_EnableIO,
|
||||
Scope::Input,
|
||||
Element::Input,
|
||||
Some(&enable_input),
|
||||
)?;
|
||||
|
||||
// Disable output processing.
|
||||
let disable_output = 0u32;
|
||||
audio_unit.set_property(
|
||||
kAudioOutputUnitProperty_EnableIO,
|
||||
Scope::Output,
|
||||
Element::Output,
|
||||
Some(&disable_output),
|
||||
)?;
|
||||
}
|
||||
|
||||
audio_unit.set_property(
|
||||
kAudioOutputUnitProperty_CurrentDevice,
|
||||
Scope::Global,
|
||||
Element::Output,
|
||||
Some(&device.audio_device_id),
|
||||
)?;
|
||||
|
||||
Ok(audio_unit)
|
||||
}
|
||||
|
||||
impl Device {
|
||||
#[allow(clippy::cast_ptr_alignment)]
|
||||
#[allow(clippy::while_immutable_condition)]
|
||||
#[allow(clippy::float_cmp)]
|
||||
fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
mut data_callback: D,
|
||||
error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
// The scope and element for working with a device's input stream.
|
||||
let scope = Scope::Output;
|
||||
let element = Element::Input;
|
||||
|
||||
// Potentially change the device sample rate to match the config.
|
||||
set_sample_rate(self.audio_device_id, config.sample_rate)?;
|
||||
|
||||
let mut audio_unit = audio_unit_from_device(self, true)?;
|
||||
|
||||
// Set the stream in interleaved mode.
|
||||
let asbd = asbd_from_config(config, sample_format);
|
||||
audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?;
|
||||
|
||||
// Set the buffersize
|
||||
match config.buffer_size {
|
||||
BufferSize::Fixed(v) => {
|
||||
let buffer_size_range = get_io_buffer_frame_size_range(&audio_unit)?;
|
||||
match buffer_size_range {
|
||||
SupportedBufferSize::Range { min, max } => {
|
||||
if v >= min && v <= max {
|
||||
audio_unit.set_property(
|
||||
kAudioDevicePropertyBufferFrameSize,
|
||||
scope,
|
||||
element,
|
||||
Some(&v),
|
||||
)?
|
||||
} else {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
}
|
||||
SupportedBufferSize::Unknown => (),
|
||||
}
|
||||
}
|
||||
BufferSize::Default => (),
|
||||
}
|
||||
|
||||
let error_callback = Arc::new(Mutex::new(error_callback));
|
||||
let error_callback_disconnect = error_callback.clone();
|
||||
|
||||
// Register the callback that is being called by coreaudio whenever it needs data to be
|
||||
// fed to the audio buffer.
|
||||
let bytes_per_channel = sample_format.sample_size();
|
||||
let sample_rate = config.sample_rate;
|
||||
type Args = render_callback::Args<data::Raw>;
|
||||
audio_unit.set_input_callback(move |args: Args| unsafe {
|
||||
let ptr = (*args.data.data).mBuffers.as_ptr();
|
||||
let len = (*args.data.data).mNumberBuffers as usize;
|
||||
let buffers: &[AudioBuffer] = slice::from_raw_parts(ptr, len);
|
||||
|
||||
// TODO: Perhaps loop over all buffers instead?
|
||||
let AudioBuffer {
|
||||
mNumberChannels: channels,
|
||||
mDataByteSize: data_byte_size,
|
||||
mData: data,
|
||||
} = buffers[0];
|
||||
|
||||
let data = data as *mut ();
|
||||
let len = data_byte_size as usize / bytes_per_channel;
|
||||
let data = Data::from_parts(data, len, sample_format);
|
||||
|
||||
// TODO: Need a better way to get delay, for now we assume a double-buffer offset.
|
||||
let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) {
|
||||
Err(err) => {
|
||||
(error_callback.lock().unwrap())(err.into());
|
||||
return Err(());
|
||||
}
|
||||
Ok(cb) => cb,
|
||||
};
|
||||
let buffer_frames = len / channels as usize;
|
||||
let delay = frames_to_duration(buffer_frames, sample_rate);
|
||||
let capture = callback
|
||||
.sub(delay)
|
||||
.expect("`capture` occurs before origin of alsa `StreamInstant`");
|
||||
let timestamp = crate::InputStreamTimestamp { callback, capture };
|
||||
|
||||
let info = InputCallbackInfo { timestamp };
|
||||
data_callback(&data, &info);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let stream = Stream::new(StreamInner {
|
||||
playing: true,
|
||||
_disconnect_listener: None,
|
||||
audio_unit,
|
||||
device_id: self.audio_device_id,
|
||||
});
|
||||
|
||||
// If we didn't request the default device, stop the stream if the
|
||||
// device disconnects.
|
||||
if !self.is_default {
|
||||
add_disconnect_listener(&stream, error_callback_disconnect)?;
|
||||
}
|
||||
|
||||
stream.inner.lock().unwrap().audio_unit.start()?;
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
mut data_callback: D,
|
||||
error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
let mut audio_unit = audio_unit_from_device(self, false)?;
|
||||
|
||||
// The scope and element for working with a device's output stream.
|
||||
let scope = Scope::Input;
|
||||
let element = Element::Output;
|
||||
|
||||
// Set the stream in interleaved mode.
|
||||
let asbd = asbd_from_config(config, sample_format);
|
||||
audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?;
|
||||
|
||||
// Set the buffersize
|
||||
match config.buffer_size {
|
||||
BufferSize::Fixed(v) => {
|
||||
let buffer_size_range = get_io_buffer_frame_size_range(&audio_unit)?;
|
||||
match buffer_size_range {
|
||||
SupportedBufferSize::Range { min, max } => {
|
||||
if v >= min && v <= max {
|
||||
audio_unit.set_property(
|
||||
kAudioDevicePropertyBufferFrameSize,
|
||||
scope,
|
||||
element,
|
||||
Some(&v),
|
||||
)?
|
||||
} else {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
}
|
||||
SupportedBufferSize::Unknown => (),
|
||||
}
|
||||
}
|
||||
BufferSize::Default => (),
|
||||
}
|
||||
|
||||
let error_callback = Arc::new(Mutex::new(error_callback));
|
||||
let error_callback_disconnect = error_callback.clone();
|
||||
|
||||
// Register the callback that is being called by coreaudio whenever it needs data to be
|
||||
// fed to the audio buffer.
|
||||
let bytes_per_channel = sample_format.sample_size();
|
||||
let sample_rate = config.sample_rate;
|
||||
type Args = render_callback::Args<data::Raw>;
|
||||
audio_unit.set_render_callback(move |args: Args| unsafe {
|
||||
// If `run()` is currently running, then a callback will be available from this list.
|
||||
// Otherwise, we just fill the buffer with zeroes and return.
|
||||
|
||||
let AudioBuffer {
|
||||
mNumberChannels: channels,
|
||||
mDataByteSize: data_byte_size,
|
||||
mData: data,
|
||||
} = (*args.data.data).mBuffers[0];
|
||||
|
||||
let data = data as *mut ();
|
||||
let len = data_byte_size as usize / bytes_per_channel;
|
||||
let mut data = Data::from_parts(data, len, sample_format);
|
||||
|
||||
let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) {
|
||||
Err(err) => {
|
||||
(error_callback.lock().unwrap())(err.into());
|
||||
return Err(());
|
||||
}
|
||||
Ok(cb) => cb,
|
||||
};
|
||||
// TODO: Need a better way to get delay, for now we assume a double-buffer offset.
|
||||
let buffer_frames = len / channels as usize;
|
||||
let delay = frames_to_duration(buffer_frames, sample_rate);
|
||||
let playback = callback
|
||||
.add(delay)
|
||||
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
|
||||
let timestamp = crate::OutputStreamTimestamp { callback, playback };
|
||||
|
||||
let info = OutputCallbackInfo { timestamp };
|
||||
data_callback(&mut data, &info);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let stream = Stream::new(StreamInner {
|
||||
playing: true,
|
||||
_disconnect_listener: None,
|
||||
audio_unit,
|
||||
device_id: self.audio_device_id,
|
||||
});
|
||||
|
||||
// If we didn't request the default device, stop the stream if the
|
||||
// device disconnects.
|
||||
if !self.is_default {
|
||||
add_disconnect_listener(&stream, error_callback_disconnect)?;
|
||||
}
|
||||
|
||||
stream.inner.lock().unwrap().audio_unit.start()?;
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to set the device sample rate to the provided rate.
|
||||
/// Return an error if the requested sample rate is not supported by the device.
|
||||
fn set_sample_rate(
|
||||
audio_device_id: AudioObjectID,
|
||||
target_sample_rate: SampleRate,
|
||||
) -> Result<(), BuildStreamError> {
|
||||
// Get the current sample rate.
|
||||
let mut property_address = AudioObjectPropertyAddress {
|
||||
mSelector: kAudioDevicePropertyNominalSampleRate,
|
||||
mScope: kAudioObjectPropertyScopeGlobal,
|
||||
mElement: kAudioObjectPropertyElementMaster,
|
||||
};
|
||||
let sample_rate: f64 = 0.0;
|
||||
let data_size = mem::size_of::<f64>() as u32;
|
||||
let status = unsafe {
|
||||
AudioObjectGetPropertyData(
|
||||
audio_device_id,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
&sample_rate as *const _ as *mut _,
|
||||
)
|
||||
};
|
||||
coreaudio::Error::from_os_status(status)?;
|
||||
|
||||
// If the requested sample rate is different to the device sample rate, update the device.
|
||||
if sample_rate as u32 != target_sample_rate.0 {
|
||||
// Get available sample rate ranges.
|
||||
property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates;
|
||||
let data_size = 0u32;
|
||||
let status = unsafe {
|
||||
AudioObjectGetPropertyDataSize(
|
||||
audio_device_id,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
)
|
||||
};
|
||||
coreaudio::Error::from_os_status(status)?;
|
||||
let n_ranges = data_size as usize / mem::size_of::<AudioValueRange>();
|
||||
let mut ranges: Vec<u8> = vec![];
|
||||
ranges.reserve_exact(data_size as usize);
|
||||
let status = unsafe {
|
||||
AudioObjectGetPropertyData(
|
||||
audio_device_id,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
ranges.as_mut_ptr() as *mut _,
|
||||
)
|
||||
};
|
||||
coreaudio::Error::from_os_status(status)?;
|
||||
let ranges: *mut AudioValueRange = ranges.as_mut_ptr() as *mut _;
|
||||
let ranges: &'static [AudioValueRange] = unsafe { slice::from_raw_parts(ranges, n_ranges) };
|
||||
|
||||
// Now that we have the available ranges, pick the one matching the desired rate.
|
||||
let sample_rate = target_sample_rate.0;
|
||||
let maybe_index = ranges
|
||||
.iter()
|
||||
.position(|r| r.mMinimum as u32 == sample_rate && r.mMaximum as u32 == sample_rate);
|
||||
let range_index = match maybe_index {
|
||||
None => return Err(BuildStreamError::StreamConfigNotSupported),
|
||||
Some(i) => i,
|
||||
};
|
||||
|
||||
let (send, recv) = channel::<Result<f64, coreaudio::Error>>();
|
||||
let sample_rate_address = AudioObjectPropertyAddress {
|
||||
mSelector: kAudioDevicePropertyNominalSampleRate,
|
||||
mScope: kAudioObjectPropertyScopeGlobal,
|
||||
mElement: kAudioObjectPropertyElementMaster,
|
||||
};
|
||||
// Send sample rate updates back on a channel.
|
||||
let sample_rate_handler = move || {
|
||||
let mut rate: f64 = 0.0;
|
||||
let data_size = mem::size_of::<f64>();
|
||||
|
||||
let result = unsafe {
|
||||
AudioObjectGetPropertyData(
|
||||
audio_device_id,
|
||||
&sample_rate_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
&data_size as *const _ as *mut _,
|
||||
&mut rate as *const _ as *mut _,
|
||||
)
|
||||
};
|
||||
send.send(coreaudio::Error::from_os_status(result).map(|_| rate))
|
||||
.ok();
|
||||
};
|
||||
|
||||
let listener = AudioObjectPropertyListener::new(
|
||||
audio_device_id,
|
||||
sample_rate_address,
|
||||
sample_rate_handler,
|
||||
)?;
|
||||
|
||||
// Finally, set the sample rate.
|
||||
property_address.mSelector = kAudioDevicePropertyNominalSampleRate;
|
||||
let status = unsafe {
|
||||
AudioObjectSetPropertyData(
|
||||
audio_device_id,
|
||||
&property_address as *const _,
|
||||
0,
|
||||
null(),
|
||||
data_size,
|
||||
&ranges[range_index] as *const _ as *const _,
|
||||
)
|
||||
};
|
||||
coreaudio::Error::from_os_status(status)?;
|
||||
|
||||
// Wait for the reported_rate to change.
|
||||
//
|
||||
// This should not take longer than a few ms, but we timeout after 1 sec just in case.
|
||||
// We loop over potentially several events from the channel to ensure
|
||||
// that we catch the expected change in sample rate.
|
||||
let mut timeout = Duration::from_secs(1);
|
||||
let start = Instant::now();
|
||||
|
||||
loop {
|
||||
match recv.recv_timeout(timeout) {
|
||||
Err(err) => {
|
||||
let description = match err {
|
||||
RecvTimeoutError::Disconnected => {
|
||||
"sample rate listener channel disconnected unexpectedly"
|
||||
}
|
||||
RecvTimeoutError::Timeout => {
|
||||
"timeout waiting for sample rate update for device"
|
||||
}
|
||||
}
|
||||
.to_string();
|
||||
return Err(BackendSpecificError { description }.into());
|
||||
}
|
||||
Ok(Ok(reported_sample_rate)) => {
|
||||
if reported_sample_rate == target_sample_rate.0 as f64 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Err(_)) => {
|
||||
// TODO: should we consider collecting this error?
|
||||
}
|
||||
};
|
||||
timeout = timeout
|
||||
.checked_sub(start.elapsed())
|
||||
.unwrap_or(Duration::ZERO);
|
||||
}
|
||||
listener.remove()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Stream {
|
||||
inner: Arc<Mutex<StreamInner>>,
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
fn new(inner: StreamInner) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(inner)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTrait for Stream {
|
||||
fn play(&self) -> Result<(), PlayStreamError> {
|
||||
let mut stream = self.inner.lock().unwrap();
|
||||
|
||||
if !stream.playing {
|
||||
if let Err(e) = stream.audio_unit.start() {
|
||||
let description = format!("{}", e);
|
||||
let err = BackendSpecificError { description };
|
||||
return Err(err.into());
|
||||
}
|
||||
stream.playing = true;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<(), PauseStreamError> {
|
||||
let mut stream = self.inner.lock().unwrap();
|
||||
|
||||
if stream.playing {
|
||||
if let Err(e) = stream.audio_unit.stop() {
|
||||
let description = format!("{}", e);
|
||||
let err = BackendSpecificError { description };
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
stream.playing = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_io_buffer_frame_size_range(
|
||||
audio_unit: &AudioUnit,
|
||||
) -> Result<SupportedBufferSize, coreaudio::Error> {
|
||||
let buffer_size_range: AudioValueRange = audio_unit.get_property(
|
||||
kAudioDevicePropertyBufferFrameSizeRange,
|
||||
Scope::Global,
|
||||
Element::Output,
|
||||
)?;
|
||||
|
||||
Ok(SupportedBufferSize::Range {
|
||||
min: buffer_size_range.mMinimum as u32,
|
||||
max: buffer_size_range.mMaximum as u32,
|
||||
})
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
//! Helper code for registering audio object property listeners.
|
||||
use super::coreaudio::sys::{
|
||||
AudioObjectAddPropertyListener, AudioObjectID, AudioObjectPropertyAddress,
|
||||
AudioObjectRemovePropertyListener, OSStatus,
|
||||
};
|
||||
|
||||
use crate::BuildStreamError;
|
||||
|
||||
/// A double-indirection to be able to pass a closure (a fat pointer)
|
||||
/// via a single c_void.
|
||||
struct PropertyListenerCallbackWrapper(Box<dyn FnMut()>);
|
||||
|
||||
/// Maintain an audio object property listener.
|
||||
/// The listener will be removed when this type is dropped.
|
||||
pub struct AudioObjectPropertyListener {
|
||||
callback: Box<PropertyListenerCallbackWrapper>,
|
||||
property_address: AudioObjectPropertyAddress,
|
||||
audio_object_id: AudioObjectID,
|
||||
removed: bool,
|
||||
}
|
||||
|
||||
impl AudioObjectPropertyListener {
|
||||
/// Attach the provided callback as a audio object property listener.
|
||||
pub fn new<F: FnMut() + 'static>(
|
||||
audio_object_id: AudioObjectID,
|
||||
property_address: AudioObjectPropertyAddress,
|
||||
callback: F,
|
||||
) -> Result<Self, BuildStreamError> {
|
||||
let callback = Box::new(PropertyListenerCallbackWrapper(Box::new(callback)));
|
||||
unsafe {
|
||||
coreaudio::Error::from_os_status(AudioObjectAddPropertyListener(
|
||||
audio_object_id,
|
||||
&property_address as *const _,
|
||||
Some(property_listener_handler_shim),
|
||||
&*callback as *const _ as *mut _,
|
||||
))?;
|
||||
};
|
||||
Ok(Self {
|
||||
callback,
|
||||
audio_object_id,
|
||||
property_address,
|
||||
removed: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Explicitly remove the property listener.
|
||||
/// Use this method if you need to explicitly handle failure to remove
|
||||
/// the property listener.
|
||||
pub fn remove(mut self) -> Result<(), BuildStreamError> {
|
||||
self.remove_inner()
|
||||
}
|
||||
|
||||
fn remove_inner(&mut self) -> Result<(), BuildStreamError> {
|
||||
unsafe {
|
||||
coreaudio::Error::from_os_status(AudioObjectRemovePropertyListener(
|
||||
self.audio_object_id,
|
||||
&self.property_address as *const _,
|
||||
Some(property_listener_handler_shim),
|
||||
&*self.callback as *const _ as *mut _,
|
||||
))?;
|
||||
}
|
||||
self.removed = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AudioObjectPropertyListener {
|
||||
fn drop(&mut self) {
|
||||
if !self.removed {
|
||||
let _ = self.remove_inner();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback used to call user-provided closure as a property listener.
|
||||
unsafe extern "C" fn property_listener_handler_shim(
|
||||
_: AudioObjectID,
|
||||
_: u32,
|
||||
_: *const AudioObjectPropertyAddress,
|
||||
callback: *mut ::std::os::raw::c_void,
|
||||
) -> OSStatus {
|
||||
let wrapper = callback as *mut PropertyListenerCallbackWrapper;
|
||||
(*wrapper).0();
|
||||
0
|
||||
}
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
extern crate coreaudio;
|
||||
|
||||
use self::coreaudio::sys::{
|
||||
kAudioFormatFlagIsFloat, kAudioFormatFlagIsPacked, kAudioFormatLinearPCM,
|
||||
AudioStreamBasicDescription, OSStatus,
|
||||
};
|
||||
|
||||
use crate::DefaultStreamConfigError;
|
||||
use crate::{BuildStreamError, SupportedStreamConfigsError};
|
||||
|
||||
use crate::{BackendSpecificError, SampleFormat, StreamConfig};
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
mod ios;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
pub use self::ios::{
|
||||
enumerate::{Devices, SupportedInputConfigs, SupportedOutputConfigs},
|
||||
Device, Host, Stream,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use self::macos::{
|
||||
enumerate::{Devices, SupportedInputConfigs, SupportedOutputConfigs},
|
||||
Device, Host, Stream,
|
||||
};
|
||||
|
||||
/// Common helper methods used by both macOS and iOS
|
||||
|
||||
fn check_os_status(os_status: OSStatus) -> Result<(), BackendSpecificError> {
|
||||
match coreaudio::Error::from_os_status(os_status) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(err) => {
|
||||
let description = err.to_string();
|
||||
Err(BackendSpecificError { description })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a coreaudio AudioStreamBasicDescription from a CPAL Format.
|
||||
fn asbd_from_config(
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
) -> AudioStreamBasicDescription {
|
||||
let n_channels = config.channels as usize;
|
||||
let sample_rate = config.sample_rate.0;
|
||||
let bytes_per_channel = sample_format.sample_size();
|
||||
let bits_per_channel = bytes_per_channel * 8;
|
||||
let bytes_per_frame = n_channels * bytes_per_channel;
|
||||
let frames_per_packet = 1;
|
||||
let bytes_per_packet = frames_per_packet * bytes_per_frame;
|
||||
let format_flags = match sample_format {
|
||||
SampleFormat::F32 => kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked,
|
||||
_ => kAudioFormatFlagIsPacked,
|
||||
};
|
||||
AudioStreamBasicDescription {
|
||||
mBitsPerChannel: bits_per_channel as _,
|
||||
mBytesPerFrame: bytes_per_frame as _,
|
||||
mChannelsPerFrame: n_channels as _,
|
||||
mBytesPerPacket: bytes_per_packet as _,
|
||||
mFramesPerPacket: frames_per_packet as _,
|
||||
mFormatFlags: format_flags,
|
||||
mFormatID: kAudioFormatLinearPCM,
|
||||
mSampleRate: sample_rate as _,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn host_time_to_stream_instant(
|
||||
m_host_time: u64,
|
||||
) -> Result<crate::StreamInstant, BackendSpecificError> {
|
||||
let mut info: mach2::mach_time::mach_timebase_info = Default::default();
|
||||
let res = unsafe { mach2::mach_time::mach_timebase_info(&mut info) };
|
||||
check_os_status(res)?;
|
||||
let nanos = m_host_time * info.numer as u64 / info.denom as u64;
|
||||
let secs = nanos / 1_000_000_000;
|
||||
let subsec_nanos = nanos - secs * 1_000_000_000;
|
||||
Ok(crate::StreamInstant::new(secs as i64, subsec_nanos as u32))
|
||||
}
|
||||
|
||||
// Convert the given duration in frames at the given sample rate to a `std::time::Duration`.
|
||||
fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration {
|
||||
let secsf = frames as f64 / rate.0 as f64;
|
||||
let secs = secsf as u64;
|
||||
let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32;
|
||||
std::time::Duration::new(secs, nanos)
|
||||
}
|
||||
|
||||
// TODO need stronger error identification
|
||||
impl From<coreaudio::Error> for BuildStreamError {
|
||||
fn from(err: coreaudio::Error) -> BuildStreamError {
|
||||
match err {
|
||||
coreaudio::Error::RenderCallbackBufferFormatDoesNotMatchAudioUnitStreamFormat
|
||||
| coreaudio::Error::NoKnownSubtype
|
||||
| coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::FormatNotSupported)
|
||||
| coreaudio::Error::AudioCodec(_)
|
||||
| coreaudio::Error::AudioFormat(_) => BuildStreamError::StreamConfigNotSupported,
|
||||
_ => BuildStreamError::DeviceNotAvailable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<coreaudio::Error> for SupportedStreamConfigsError {
|
||||
fn from(err: coreaudio::Error) -> SupportedStreamConfigsError {
|
||||
let description = format!("{}", err);
|
||||
let err = BackendSpecificError { description };
|
||||
// Check for possible DeviceNotAvailable variant
|
||||
SupportedStreamConfigsError::BackendSpecific { err }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<coreaudio::Error> for DefaultStreamConfigError {
|
||||
fn from(err: coreaudio::Error) -> DefaultStreamConfigError {
|
||||
let description = format!("{}", err);
|
||||
let err = BackendSpecificError { description };
|
||||
// Check for possible DeviceNotAvailable variant
|
||||
DefaultStreamConfigError::BackendSpecific { err }
|
||||
}
|
||||
}
|
||||
-422
@@ -1,422 +0,0 @@
|
||||
use js_sys::Float32Array;
|
||||
use std::time::Duration;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
use web_sys::AudioContext;
|
||||
|
||||
use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use crate::{
|
||||
BufferSize, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError,
|
||||
InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat,
|
||||
SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig,
|
||||
SupportedStreamConfigRange, SupportedStreamConfigsError,
|
||||
};
|
||||
|
||||
// The emscripten backend currently works by instantiating an `AudioContext` object per `Stream`.
|
||||
// Creating a stream creates a new `AudioContext`. Destroying a stream destroys it. Creation of a
|
||||
// `Host` instance initializes the `stdweb` context.
|
||||
|
||||
/// The default emscripten host type.
|
||||
#[derive(Debug)]
|
||||
pub struct Host;
|
||||
|
||||
/// Content is false if the iterator is empty.
|
||||
pub struct Devices(bool);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Device;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Clone)]
|
||||
pub struct Stream {
|
||||
// A reference to an `AudioContext` object.
|
||||
audio_ctxt: AudioContext,
|
||||
}
|
||||
|
||||
// Index within the `streams` array of the events loop.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct StreamId(usize);
|
||||
|
||||
pub type SupportedInputConfigs = ::std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
pub type SupportedOutputConfigs = ::std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
|
||||
const MIN_CHANNELS: u16 = 1;
|
||||
const MAX_CHANNELS: u16 = 32;
|
||||
const MIN_SAMPLE_RATE: SampleRate = SampleRate(8_000);
|
||||
const MAX_SAMPLE_RATE: SampleRate = SampleRate(96_000);
|
||||
const DEFAULT_SAMPLE_RATE: SampleRate = SampleRate(44_100);
|
||||
const MIN_BUFFER_SIZE: u32 = 1;
|
||||
const MAX_BUFFER_SIZE: u32 = u32::MAX;
|
||||
const DEFAULT_BUFFER_SIZE: usize = 2048;
|
||||
const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
|
||||
|
||||
impl Host {
|
||||
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
||||
Ok(Host)
|
||||
}
|
||||
}
|
||||
|
||||
impl Devices {
|
||||
fn new() -> Result<Self, DevicesError> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Device {
|
||||
#[inline]
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
Ok("Default Device".to_owned())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
let buffer_size = SupportedBufferSize::Range {
|
||||
min: MIN_BUFFER_SIZE,
|
||||
max: MAX_BUFFER_SIZE,
|
||||
};
|
||||
let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS)
|
||||
.map(|channels| SupportedStreamConfigRange {
|
||||
channels,
|
||||
min_sample_rate: MIN_SAMPLE_RATE,
|
||||
max_sample_rate: MAX_SAMPLE_RATE,
|
||||
buffer_size: buffer_size.clone(),
|
||||
sample_format: SUPPORTED_SAMPLE_FORMAT,
|
||||
})
|
||||
.collect();
|
||||
Ok(configs.into_iter())
|
||||
}
|
||||
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
const EXPECT: &str = "expected at least one valid webaudio stream config";
|
||||
let config = self
|
||||
.supported_output_configs()
|
||||
.expect(EXPECT)
|
||||
.max_by(|a, b| a.cmp_default_heuristics(b))
|
||||
.unwrap()
|
||||
.with_sample_rate(DEFAULT_SAMPLE_RATE);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTrait for Host {
|
||||
type Devices = Devices;
|
||||
type Device = Device;
|
||||
|
||||
fn is_available() -> bool {
|
||||
// Assume this host is always available on emscripten.
|
||||
true
|
||||
}
|
||||
|
||||
fn devices(&self) -> Result<Self::Devices, DevicesError> {
|
||||
Devices::new()
|
||||
}
|
||||
|
||||
fn default_input_device(&self) -> Option<Self::Device> {
|
||||
default_input_device()
|
||||
}
|
||||
|
||||
fn default_output_device(&self) -> Option<Self::Device> {
|
||||
default_output_device()
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceTrait for Device {
|
||||
type SupportedInputConfigs = SupportedInputConfigs;
|
||||
type SupportedOutputConfigs = SupportedOutputConfigs;
|
||||
type Stream = Stream;
|
||||
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
Device::name(self)
|
||||
}
|
||||
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
Device::supported_input_configs(self)
|
||||
}
|
||||
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
Device::supported_output_configs(self)
|
||||
}
|
||||
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
Device::default_input_config(self)
|
||||
}
|
||||
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
Device::default_output_config(self)
|
||||
}
|
||||
|
||||
fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
_config: &StreamConfig,
|
||||
_sample_format: SampleFormat,
|
||||
_data_callback: D,
|
||||
_error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
_error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
if !valid_config(config, sample_format) {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
|
||||
let buffer_size_frames = match config.buffer_size {
|
||||
BufferSize::Fixed(v) => {
|
||||
if v == 0 {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
} else {
|
||||
v as usize
|
||||
}
|
||||
}
|
||||
BufferSize::Default => DEFAULT_BUFFER_SIZE,
|
||||
};
|
||||
|
||||
// Create the stream.
|
||||
let audio_ctxt = AudioContext::new().expect("webaudio is not present on this system");
|
||||
let stream = Stream { audio_ctxt };
|
||||
|
||||
// Use `set_timeout` to invoke a Rust callback repeatedly.
|
||||
//
|
||||
// The job of this callback is to fill the content of the audio buffers.
|
||||
//
|
||||
// See also: The call to `set_timeout` at the end of the `audio_callback_fn` which creates
|
||||
// the loop.
|
||||
set_timeout(
|
||||
10,
|
||||
stream.clone(),
|
||||
data_callback,
|
||||
config,
|
||||
sample_format,
|
||||
buffer_size_frames as u32,
|
||||
);
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTrait for Stream {
|
||||
fn play(&self) -> Result<(), PlayStreamError> {
|
||||
let future = JsFuture::from(
|
||||
self.audio_ctxt
|
||||
.resume()
|
||||
.expect("Could not resume the stream"),
|
||||
);
|
||||
spawn_local(async {
|
||||
match future.await {
|
||||
Ok(value) => assert!(value.is_undefined()),
|
||||
Err(value) => panic!("AudioContext.resume() promise was rejected: {:?}", value),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<(), PauseStreamError> {
|
||||
let future = JsFuture::from(
|
||||
self.audio_ctxt
|
||||
.suspend()
|
||||
.expect("Could not suspend the stream"),
|
||||
);
|
||||
spawn_local(async {
|
||||
match future.await {
|
||||
Ok(value) => assert!(value.is_undefined()),
|
||||
Err(value) => panic!("AudioContext.suspend() promise was rejected: {:?}", value),
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn audio_callback_fn<D>(
|
||||
mut data_callback: D,
|
||||
) -> impl FnOnce(Stream, StreamConfig, SampleFormat, u32)
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
{
|
||||
|stream, config, sample_format, buffer_size_frames| {
|
||||
let sample_rate = config.sample_rate.0;
|
||||
let buffer_size_samples = buffer_size_frames * config.channels as u32;
|
||||
let audio_ctxt = &stream.audio_ctxt;
|
||||
|
||||
// TODO: We should be re-using a buffer.
|
||||
let mut temporary_buffer = vec![0f32; buffer_size_samples as usize];
|
||||
|
||||
{
|
||||
let len = temporary_buffer.len();
|
||||
let data = temporary_buffer.as_mut_ptr() as *mut ();
|
||||
let mut data = unsafe { Data::from_parts(data, len, sample_format) };
|
||||
let now_secs: f64 = audio_ctxt.current_time();
|
||||
let callback = crate::StreamInstant::from_secs_f64(now_secs);
|
||||
// TODO: Use proper latency instead. Currently, unsupported on most browsers though, so
|
||||
// we estimate based on buffer size instead. Probably should use this, but it's only
|
||||
// supported by firefox (2020-04-28).
|
||||
// let latency_secs: f64 = audio_ctxt.outputLatency.try_into().unwrap();
|
||||
let buffer_duration = frames_to_duration(len, sample_rate as usize);
|
||||
let playback = callback
|
||||
.add(buffer_duration)
|
||||
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
|
||||
let timestamp = crate::OutputStreamTimestamp { callback, playback };
|
||||
let info = OutputCallbackInfo { timestamp };
|
||||
data_callback(&mut data, &info);
|
||||
}
|
||||
|
||||
let typed_array: Float32Array = temporary_buffer.as_slice().into();
|
||||
|
||||
debug_assert_eq!(temporary_buffer.len() % config.channels as usize, 0);
|
||||
|
||||
let src_buffer = Float32Array::new(typed_array.buffer().as_ref());
|
||||
let context = audio_ctxt;
|
||||
let buffer = context
|
||||
.create_buffer(
|
||||
config.channels as u32,
|
||||
buffer_size_frames as u32,
|
||||
sample_rate as f32,
|
||||
)
|
||||
.expect("Buffer could not be created");
|
||||
for channel in 0..config.channels {
|
||||
let mut buffer_content = buffer
|
||||
.get_channel_data(channel as u32)
|
||||
.expect("Should be impossible");
|
||||
for (i, buffer_content_item) in buffer_content.iter_mut().enumerate() {
|
||||
*buffer_content_item =
|
||||
src_buffer.get_index(i as u32 * config.channels as u32 + channel as u32);
|
||||
}
|
||||
}
|
||||
|
||||
let node = context
|
||||
.create_buffer_source()
|
||||
.expect("The buffer source node could not be created");
|
||||
node.set_buffer(Some(&buffer));
|
||||
context
|
||||
.destination()
|
||||
.connect_with_audio_node(&node)
|
||||
.expect("Could not connect the audio node to the destination");
|
||||
node.start().expect("Could not start the audio node");
|
||||
|
||||
// TODO: handle latency better ; right now we just use setInterval with the amount of sound
|
||||
// data that is in each buffer ; this is obviously bad, and also the schedule is too tight
|
||||
// and there may be underflows
|
||||
set_timeout(
|
||||
1000 * buffer_size_frames as i32 / sample_rate as i32,
|
||||
stream.clone().clone(),
|
||||
data_callback,
|
||||
&config,
|
||||
sample_format,
|
||||
buffer_size_frames as u32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_timeout<D>(
|
||||
time: i32,
|
||||
stream: Stream,
|
||||
data_callback: D,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
buffer_size_frames: u32,
|
||||
) where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
{
|
||||
let window = web_sys::window().expect("Not in a window somehow?");
|
||||
window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_4(
|
||||
&Closure::once_into_js(audio_callback_fn(data_callback))
|
||||
.dyn_ref::<js_sys::Function>()
|
||||
.expect("The function was somehow not a function"),
|
||||
time,
|
||||
&stream.into(),
|
||||
&((*config).clone()).into(),
|
||||
&Closure::once_into_js(move || sample_format),
|
||||
&buffer_size_frames.into(),
|
||||
)
|
||||
.expect("The timeout could not be set");
|
||||
}
|
||||
|
||||
impl Default for Devices {
|
||||
fn default() -> Devices {
|
||||
// We produce an empty iterator if the WebAudio API isn't available.
|
||||
Devices(is_webaudio_available())
|
||||
}
|
||||
}
|
||||
impl Iterator for Devices {
|
||||
type Item = Device;
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Device> {
|
||||
if self.0 {
|
||||
self.0 = false;
|
||||
Some(Device)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_input_device() -> Option<Device> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_output_device() -> Option<Device> {
|
||||
if is_webaudio_available() {
|
||||
Some(Device)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Detects whether the `AudioContext` global variable is available.
|
||||
fn is_webaudio_available() -> bool {
|
||||
AudioContext::new().is_ok()
|
||||
}
|
||||
|
||||
// Whether or not the given stream configuration is valid for building a stream.
|
||||
fn valid_config(conf: &StreamConfig, sample_format: SampleFormat) -> bool {
|
||||
conf.channels <= MAX_CHANNELS
|
||||
&& conf.channels >= MIN_CHANNELS
|
||||
&& conf.sample_rate <= MAX_SAMPLE_RATE
|
||||
&& conf.sample_rate >= MIN_SAMPLE_RATE
|
||||
&& sample_format == SUPPORTED_SAMPLE_FORMAT
|
||||
}
|
||||
|
||||
// Convert the given duration in frames at the given sample rate to a `std::time::Duration`.
|
||||
fn frames_to_duration(frames: usize, rate: usize) -> std::time::Duration {
|
||||
let secsf = frames as f64 / rate as f64;
|
||||
let secs = secsf as u64;
|
||||
let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32;
|
||||
std::time::Duration::new(secs, nanos)
|
||||
}
|
||||
-268
@@ -1,268 +0,0 @@
|
||||
use crate::traits::DeviceTrait;
|
||||
use crate::{
|
||||
BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError,
|
||||
InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError,
|
||||
SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange,
|
||||
SupportedStreamConfigsError,
|
||||
};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::stream::Stream;
|
||||
use super::JACK_SAMPLE_FORMAT;
|
||||
|
||||
pub type SupportedInputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
pub type SupportedOutputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
|
||||
const DEFAULT_NUM_CHANNELS: u16 = 2;
|
||||
const DEFAULT_SUPPORTED_CHANNELS: [u16; 10] = [1, 2, 4, 6, 8, 16, 24, 32, 48, 64];
|
||||
|
||||
/// If a device is for input or output.
|
||||
/// Until we have duplex stream support JACK clients and CPAL devices for JACK will be either input or output.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DeviceType {
|
||||
InputDevice,
|
||||
OutputDevice,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Device {
|
||||
name: String,
|
||||
sample_rate: SampleRate,
|
||||
buffer_size: SupportedBufferSize,
|
||||
device_type: DeviceType,
|
||||
start_server_automatically: bool,
|
||||
connect_ports_automatically: bool,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
fn new_device(
|
||||
name: String,
|
||||
connect_ports_automatically: bool,
|
||||
start_server_automatically: bool,
|
||||
device_type: DeviceType,
|
||||
) -> Result<Self, String> {
|
||||
// ClientOptions are bit flags that you can set with the constants provided
|
||||
let client_options = super::get_client_options(start_server_automatically);
|
||||
|
||||
// Create a dummy client to find out the sample rate of the server to be able to provide it as a possible config.
|
||||
// This client will be dropped, and a new one will be created when making the stream.
|
||||
// This is a hack due to the fact that the Client must be moved to create the AsyncClient.
|
||||
match super::get_client(&name, client_options) {
|
||||
Ok(client) => Ok(Device {
|
||||
// The name given to the client by JACK, could potentially be different from the name supplied e.g.if there is a name collision
|
||||
name: client.name().to_string(),
|
||||
sample_rate: SampleRate(client.sample_rate() as u32),
|
||||
buffer_size: SupportedBufferSize::Range {
|
||||
min: client.buffer_size(),
|
||||
max: client.buffer_size(),
|
||||
},
|
||||
device_type,
|
||||
start_server_automatically,
|
||||
connect_ports_automatically,
|
||||
}),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_output_device(
|
||||
name: &str,
|
||||
connect_ports_automatically: bool,
|
||||
start_server_automatically: bool,
|
||||
) -> Result<Self, String> {
|
||||
let output_client_name = format!("{}_out", name);
|
||||
Device::new_device(
|
||||
output_client_name,
|
||||
connect_ports_automatically,
|
||||
start_server_automatically,
|
||||
DeviceType::OutputDevice,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn default_input_device(
|
||||
name: &str,
|
||||
connect_ports_automatically: bool,
|
||||
start_server_automatically: bool,
|
||||
) -> Result<Self, String> {
|
||||
let input_client_name = format!("{}_in", name);
|
||||
Device::new_device(
|
||||
input_client_name,
|
||||
connect_ports_automatically,
|
||||
start_server_automatically,
|
||||
DeviceType::InputDevice,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn default_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
let channels = DEFAULT_NUM_CHANNELS;
|
||||
let sample_rate = self.sample_rate;
|
||||
let buffer_size = self.buffer_size.clone();
|
||||
// The sample format for JACK audio ports is always "32-bit float mono audio" in the current implementation.
|
||||
// Custom formats are allowed within JACK, but this is of niche interest.
|
||||
// The format can be found programmatically by calling jack::PortSpec::port_type() on a created port.
|
||||
let sample_format = JACK_SAMPLE_FORMAT;
|
||||
Ok(SupportedStreamConfig {
|
||||
channels,
|
||||
sample_rate,
|
||||
buffer_size,
|
||||
sample_format,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn supported_configs(&self) -> Vec<SupportedStreamConfigRange> {
|
||||
let f = match self.default_config() {
|
||||
Err(_) => return vec![],
|
||||
Ok(f) => f,
|
||||
};
|
||||
|
||||
let mut supported_configs = vec![];
|
||||
|
||||
for &channels in DEFAULT_SUPPORTED_CHANNELS.iter() {
|
||||
supported_configs.push(SupportedStreamConfigRange {
|
||||
channels,
|
||||
min_sample_rate: f.sample_rate,
|
||||
max_sample_rate: f.sample_rate,
|
||||
buffer_size: f.buffer_size.clone(),
|
||||
sample_format: f.sample_format,
|
||||
});
|
||||
}
|
||||
supported_configs
|
||||
}
|
||||
|
||||
pub fn is_input(&self) -> bool {
|
||||
matches!(self.device_type, DeviceType::InputDevice)
|
||||
}
|
||||
|
||||
pub fn is_output(&self) -> bool {
|
||||
matches!(self.device_type, DeviceType::OutputDevice)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceTrait for Device {
|
||||
type SupportedInputConfigs = SupportedInputConfigs;
|
||||
type SupportedOutputConfigs = SupportedOutputConfigs;
|
||||
type Stream = Stream;
|
||||
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
Ok(self.name.clone())
|
||||
}
|
||||
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
Ok(self.supported_configs().into_iter())
|
||||
}
|
||||
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
Ok(self.supported_configs().into_iter())
|
||||
}
|
||||
|
||||
/// Returns the default input config
|
||||
/// The sample format for JACK audio ports is always "32-bit float mono audio" unless using a custom type.
|
||||
/// The sample rate is set by the JACK server.
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
self.default_config()
|
||||
}
|
||||
|
||||
/// Returns the default output config
|
||||
/// The sample format for JACK audio ports is always "32-bit float mono audio" unless using a custom type.
|
||||
/// The sample rate is set by the JACK server.
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
self.default_config()
|
||||
}
|
||||
|
||||
fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
conf: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
if let DeviceType::OutputDevice = &self.device_type {
|
||||
// Trying to create an input stream from an output device
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
// The settings should be fine, create a Client
|
||||
let client_options = super::get_client_options(self.start_server_automatically);
|
||||
let client;
|
||||
match super::get_client(&self.name, client_options) {
|
||||
Ok(c) => client = c,
|
||||
Err(e) => {
|
||||
return Err(BuildStreamError::BackendSpecific {
|
||||
err: BackendSpecificError { description: e },
|
||||
})
|
||||
}
|
||||
};
|
||||
let mut stream = Stream::new_input(client, conf.channels, data_callback, error_callback);
|
||||
|
||||
if self.connect_ports_automatically {
|
||||
stream.connect_to_system_inputs();
|
||||
}
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
conf: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
if let DeviceType::InputDevice = &self.device_type {
|
||||
// Trying to create an output stream from an input device
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
|
||||
// The settings should be fine, create a Client
|
||||
let client_options = super::get_client_options(self.start_server_automatically);
|
||||
let client;
|
||||
match super::get_client(&self.name, client_options) {
|
||||
Ok(c) => client = c,
|
||||
Err(e) => {
|
||||
return Err(BuildStreamError::BackendSpecific {
|
||||
err: BackendSpecificError { description: e },
|
||||
})
|
||||
}
|
||||
};
|
||||
let mut stream = Stream::new_output(client, conf.channels, data_callback, error_callback);
|
||||
|
||||
if self.connect_ports_automatically {
|
||||
stream.connect_to_system_outputs();
|
||||
}
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Device {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
// Device::name() can never fail in this implementation
|
||||
self.name().unwrap() == other.name().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Device {}
|
||||
|
||||
impl Hash for Device {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.name().unwrap().hash(state);
|
||||
}
|
||||
}
|
||||
-180
@@ -1,180 +0,0 @@
|
||||
extern crate jack;
|
||||
|
||||
use crate::traits::HostTrait;
|
||||
use crate::{DevicesError, SampleFormat, SupportedStreamConfigRange};
|
||||
|
||||
mod device;
|
||||
pub use self::device::Device;
|
||||
pub use self::stream::Stream;
|
||||
mod stream;
|
||||
|
||||
const JACK_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
|
||||
|
||||
pub type SupportedInputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
pub type SupportedOutputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
pub type Devices = std::vec::IntoIter<Device>;
|
||||
|
||||
/// The JACK Host type
|
||||
#[derive(Debug)]
|
||||
pub struct Host {
|
||||
/// The name that the client will have in JACK.
|
||||
/// Until we have duplex streams two clients will be created adding "out" or "in" to the name
|
||||
/// since names have to be unique.
|
||||
name: String,
|
||||
/// If ports are to be connected to the system (soundcard) ports automatically (default is true).
|
||||
connect_ports_automatically: bool,
|
||||
/// If the JACK server should be started automatically if it isn't already when creating a Client (default is false).
|
||||
start_server_automatically: bool,
|
||||
/// A list of the devices that have been created from this Host.
|
||||
devices_created: Vec<Device>,
|
||||
}
|
||||
|
||||
impl Host {
|
||||
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
||||
let mut host = Host {
|
||||
name: "cpal_client".to_owned(),
|
||||
connect_ports_automatically: true,
|
||||
start_server_automatically: false,
|
||||
devices_created: vec![],
|
||||
};
|
||||
// Devices don't exist for JACK, they have to be created
|
||||
host.initialize_default_devices();
|
||||
Ok(host)
|
||||
}
|
||||
/// Set whether the ports should automatically be connected to system
|
||||
/// (default is true)
|
||||
pub fn set_connect_automatically(&mut self, do_connect: bool) {
|
||||
self.connect_ports_automatically = do_connect;
|
||||
}
|
||||
/// Set whether a JACK server should be automatically started if it isn't already.
|
||||
/// (default is false)
|
||||
pub fn set_start_server_automatically(&mut self, do_start_server: bool) {
|
||||
self.start_server_automatically = do_start_server;
|
||||
}
|
||||
|
||||
pub fn input_device_with_name(&mut self, name: &str) -> Option<Device> {
|
||||
self.name = name.to_owned();
|
||||
self.default_input_device()
|
||||
}
|
||||
|
||||
pub fn output_device_with_name(&mut self, name: &str) -> Option<Device> {
|
||||
self.name = name.to_owned();
|
||||
self.default_output_device()
|
||||
}
|
||||
|
||||
fn initialize_default_devices(&mut self) {
|
||||
let in_device_res = Device::default_input_device(
|
||||
&self.name,
|
||||
self.connect_ports_automatically,
|
||||
self.start_server_automatically,
|
||||
);
|
||||
|
||||
match in_device_res {
|
||||
Ok(device) => self.devices_created.push(device),
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
}
|
||||
}
|
||||
|
||||
let out_device_res = Device::default_output_device(
|
||||
&self.name,
|
||||
self.connect_ports_automatically,
|
||||
self.start_server_automatically,
|
||||
);
|
||||
match out_device_res {
|
||||
Ok(device) => self.devices_created.push(device),
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTrait for Host {
|
||||
type Devices = Devices;
|
||||
type Device = Device;
|
||||
|
||||
/// JACK is available if
|
||||
/// - the jack feature flag is set
|
||||
/// - libjack is installed (wouldn't compile without it)
|
||||
/// - the JACK server can be started
|
||||
///
|
||||
/// If the code compiles the necessary jack libraries are installed.
|
||||
/// There is no way to know if the user has set up a correct JACK configuration e.g. with qjackctl.
|
||||
/// Users can choose to automatically start the server if it isn't already started when creating a client
|
||||
/// so checking if the server is running could give a false negative in some use cases.
|
||||
/// For these reasons this function should always return true.
|
||||
fn is_available() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn devices(&self) -> Result<Self::Devices, DevicesError> {
|
||||
Ok(self.devices_created.clone().into_iter())
|
||||
}
|
||||
|
||||
fn default_input_device(&self) -> Option<Self::Device> {
|
||||
for device in &self.devices_created {
|
||||
if device.is_input() {
|
||||
return Some(device.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn default_output_device(&self) -> Option<Self::Device> {
|
||||
for device in &self.devices_created {
|
||||
if device.is_output() {
|
||||
return Some(device.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_client_options(start_server_automatically: bool) -> jack::ClientOptions {
|
||||
let mut client_options = jack::ClientOptions::empty();
|
||||
client_options.set(
|
||||
jack::ClientOptions::NO_START_SERVER,
|
||||
!start_server_automatically,
|
||||
);
|
||||
client_options
|
||||
}
|
||||
|
||||
fn get_client(name: &str, client_options: jack::ClientOptions) -> Result<jack::Client, String> {
|
||||
let c_res = jack::Client::new(name, client_options);
|
||||
match c_res {
|
||||
Ok((client, status)) => {
|
||||
// The ClientStatus can tell us many things
|
||||
if status.intersects(jack::ClientStatus::SERVER_ERROR) {
|
||||
return Err(String::from(
|
||||
"There was an error communicating with the JACK server!",
|
||||
));
|
||||
} else if status.intersects(jack::ClientStatus::SERVER_FAILED) {
|
||||
return Err(String::from("Could not connect to the JACK server!"));
|
||||
} else if status.intersects(jack::ClientStatus::VERSION_ERROR) {
|
||||
return Err(String::from(
|
||||
"Error connecting to JACK server: Client's protocol version does not match!",
|
||||
));
|
||||
} else if status.intersects(jack::ClientStatus::INIT_FAILURE) {
|
||||
return Err(String::from(
|
||||
"Error connecting to JACK server: Unable to initialize client!",
|
||||
));
|
||||
} else if status.intersects(jack::ClientStatus::SHM_FAILURE) {
|
||||
return Err(String::from(
|
||||
"Error connecting to JACK server: Unable to access shared memory!",
|
||||
));
|
||||
} else if status.intersects(jack::ClientStatus::NO_SUCH_CLIENT) {
|
||||
return Err(String::from(
|
||||
"Error connecting to JACK server: Requested client does not exist!",
|
||||
));
|
||||
} else if status.intersects(jack::ClientStatus::INVALID_OPTION) {
|
||||
return Err(String::from("Error connecting to JACK server: The operation contained an invalid or unsupported option!"));
|
||||
}
|
||||
|
||||
return Ok(client);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("Failed to open client because of error: {:?}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
-463
@@ -1,463 +0,0 @@
|
||||
use crate::traits::StreamTrait;
|
||||
use crate::ChannelCount;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
BackendSpecificError, Data, InputCallbackInfo, OutputCallbackInfo, PauseStreamError,
|
||||
PlayStreamError, SampleRate, StreamError,
|
||||
};
|
||||
|
||||
use super::JACK_SAMPLE_FORMAT;
|
||||
|
||||
type ErrorCallbackPtr = Arc<Mutex<dyn FnMut(StreamError) + Send + 'static>>;
|
||||
|
||||
pub struct Stream {
|
||||
// TODO: It might be faster to send a message when playing/pausing than to check this every iteration
|
||||
playing: Arc<AtomicBool>,
|
||||
async_client: jack::AsyncClient<JackNotificationHandler, LocalProcessHandler>,
|
||||
// Port names are stored in order to connect them to other ports in jack automatically
|
||||
input_port_names: Vec<String>,
|
||||
output_port_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
// TODO: Return error messages
|
||||
pub fn new_input<D, E>(
|
||||
client: jack::Client,
|
||||
channels: ChannelCount,
|
||||
data_callback: D,
|
||||
mut error_callback: E,
|
||||
) -> Stream
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
let mut ports = vec![];
|
||||
let mut port_names: Vec<String> = vec![];
|
||||
// Create ports
|
||||
for i in 0..channels {
|
||||
let port_try = client.register_port(&format!("in_{}", i), jack::AudioIn::default());
|
||||
match port_try {
|
||||
Ok(port) => {
|
||||
// Get the port name in order to later connect it automatically
|
||||
if let Ok(port_name) = port.name() {
|
||||
port_names.push(port_name);
|
||||
}
|
||||
// Store the port into a Vec to move to the ProcessHandler
|
||||
ports.push(port);
|
||||
}
|
||||
Err(e) => {
|
||||
// If port creation failed, send the error back via the error_callback
|
||||
error_callback(
|
||||
BackendSpecificError {
|
||||
description: e.to_string(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let playing = Arc::new(AtomicBool::new(true));
|
||||
|
||||
let error_callback_ptr = Arc::new(Mutex::new(error_callback)) as ErrorCallbackPtr;
|
||||
|
||||
let input_process_handler = LocalProcessHandler::new(
|
||||
vec![],
|
||||
ports,
|
||||
SampleRate(client.sample_rate() as u32),
|
||||
client.buffer_size() as usize,
|
||||
Some(Box::new(data_callback)),
|
||||
None,
|
||||
playing.clone(),
|
||||
Arc::clone(&error_callback_ptr),
|
||||
);
|
||||
|
||||
let notification_handler = JackNotificationHandler::new(error_callback_ptr);
|
||||
|
||||
let async_client = client
|
||||
.activate_async(notification_handler, input_process_handler)
|
||||
.unwrap();
|
||||
|
||||
Stream {
|
||||
playing,
|
||||
async_client,
|
||||
input_port_names: port_names,
|
||||
output_port_names: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_output<D, E>(
|
||||
client: jack::Client,
|
||||
channels: ChannelCount,
|
||||
data_callback: D,
|
||||
mut error_callback: E,
|
||||
) -> Stream
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
let mut ports = vec![];
|
||||
let mut port_names: Vec<String> = vec![];
|
||||
// Create ports
|
||||
for i in 0..channels {
|
||||
let port_try = client.register_port(&format!("out_{}", i), jack::AudioOut::default());
|
||||
match port_try {
|
||||
Ok(port) => {
|
||||
// Get the port name in order to later connect it automatically
|
||||
if let Ok(port_name) = port.name() {
|
||||
port_names.push(port_name);
|
||||
}
|
||||
// Store the port into a Vec to move to the ProcessHandler
|
||||
ports.push(port);
|
||||
}
|
||||
Err(e) => {
|
||||
// If port creation failed, send the error back via the error_callback
|
||||
error_callback(
|
||||
BackendSpecificError {
|
||||
description: e.to_string(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let playing = Arc::new(AtomicBool::new(true));
|
||||
|
||||
let error_callback_ptr = Arc::new(Mutex::new(error_callback)) as ErrorCallbackPtr;
|
||||
|
||||
let output_process_handler = LocalProcessHandler::new(
|
||||
ports,
|
||||
vec![],
|
||||
SampleRate(client.sample_rate() as u32),
|
||||
client.buffer_size() as usize,
|
||||
None,
|
||||
Some(Box::new(data_callback)),
|
||||
playing.clone(),
|
||||
Arc::clone(&error_callback_ptr),
|
||||
);
|
||||
|
||||
let notification_handler = JackNotificationHandler::new(error_callback_ptr);
|
||||
|
||||
let async_client = client
|
||||
.activate_async(notification_handler, output_process_handler)
|
||||
.unwrap();
|
||||
|
||||
Stream {
|
||||
playing,
|
||||
async_client,
|
||||
input_port_names: vec![],
|
||||
output_port_names: port_names,
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the standard system outputs in jack, system:playback_1 and system:playback_2
|
||||
/// This has to be done after the client is activated, doing it just after creating the ports doesn't work.
|
||||
pub fn connect_to_system_outputs(&mut self) {
|
||||
// Get the system ports
|
||||
let system_ports = self.async_client.as_client().ports(
|
||||
Some("system:playback_.*"),
|
||||
None,
|
||||
jack::PortFlags::empty(),
|
||||
);
|
||||
|
||||
// Connect outputs from this client to the system playback inputs
|
||||
for i in 0..self.output_port_names.len() {
|
||||
if i >= system_ports.len() {
|
||||
break;
|
||||
}
|
||||
match self
|
||||
.async_client
|
||||
.as_client()
|
||||
.connect_ports_by_name(&self.output_port_names[i], &system_ports[i])
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(e) => println!("Unable to connect to port with error {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the standard system outputs in jack, system:capture_1 and system:capture_2
|
||||
/// This has to be done after the client is activated, doing it just after creating the ports doesn't work.
|
||||
pub fn connect_to_system_inputs(&mut self) {
|
||||
// Get the system ports
|
||||
let system_ports = self.async_client.as_client().ports(
|
||||
Some("system:capture_.*"),
|
||||
None,
|
||||
jack::PortFlags::empty(),
|
||||
);
|
||||
|
||||
// Connect outputs from this client to the system playback inputs
|
||||
for i in 0..self.input_port_names.len() {
|
||||
if i >= system_ports.len() {
|
||||
break;
|
||||
}
|
||||
match self
|
||||
.async_client
|
||||
.as_client()
|
||||
.connect_ports_by_name(&system_ports[i], &self.input_port_names[i])
|
||||
{
|
||||
Ok(_) => (),
|
||||
Err(e) => println!("Unable to connect to port with error {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTrait for Stream {
|
||||
fn play(&self) -> Result<(), PlayStreamError> {
|
||||
self.playing.store(true, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<(), PauseStreamError> {
|
||||
self.playing.store(false, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalProcessHandler {
|
||||
/// No new ports are allowed to be created after the creation of the LocalProcessHandler as that would invalidate the buffer sizes
|
||||
out_ports: Vec<jack::Port<jack::AudioOut>>,
|
||||
in_ports: Vec<jack::Port<jack::AudioIn>>,
|
||||
|
||||
sample_rate: SampleRate,
|
||||
buffer_size: usize,
|
||||
input_data_callback: Option<Box<dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static>>,
|
||||
output_data_callback: Option<Box<dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static>>,
|
||||
|
||||
// JACK audio samples are 32-bit float (unless you do some custom dark magic)
|
||||
temp_input_buffer: Vec<f32>,
|
||||
temp_output_buffer: Vec<f32>,
|
||||
playing: Arc<AtomicBool>,
|
||||
creation_timestamp: std::time::Instant,
|
||||
/// This should not be called on `process`, only on `buffer_size` because it can block.
|
||||
error_callback_ptr: ErrorCallbackPtr,
|
||||
}
|
||||
|
||||
impl LocalProcessHandler {
|
||||
fn new(
|
||||
out_ports: Vec<jack::Port<jack::AudioOut>>,
|
||||
in_ports: Vec<jack::Port<jack::AudioIn>>,
|
||||
sample_rate: SampleRate,
|
||||
buffer_size: usize,
|
||||
input_data_callback: Option<Box<dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static>>,
|
||||
output_data_callback: Option<
|
||||
Box<dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static>,
|
||||
>,
|
||||
playing: Arc<AtomicBool>,
|
||||
error_callback_ptr: ErrorCallbackPtr,
|
||||
) -> Self {
|
||||
// These may be reallocated in the `buffer_size` callback.
|
||||
let temp_input_buffer = vec![0.0; in_ports.len() * buffer_size];
|
||||
let temp_output_buffer = vec![0.0; out_ports.len() * buffer_size];
|
||||
|
||||
LocalProcessHandler {
|
||||
out_ports,
|
||||
in_ports,
|
||||
sample_rate,
|
||||
buffer_size,
|
||||
input_data_callback,
|
||||
output_data_callback,
|
||||
temp_input_buffer,
|
||||
temp_output_buffer,
|
||||
playing,
|
||||
creation_timestamp: std::time::Instant::now(),
|
||||
error_callback_ptr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn temp_buffer_to_data(temp_input_buffer: &mut Vec<f32>, total_buffer_size: usize) -> Data {
|
||||
let slice = &temp_input_buffer[0..total_buffer_size];
|
||||
let data = slice.as_ptr() as *mut ();
|
||||
let len = total_buffer_size;
|
||||
let data = unsafe { Data::from_parts(data, len, JACK_SAMPLE_FORMAT) };
|
||||
data
|
||||
}
|
||||
|
||||
impl jack::ProcessHandler for LocalProcessHandler {
|
||||
fn process(&mut self, _: &jack::Client, process_scope: &jack::ProcessScope) -> jack::Control {
|
||||
if !self.playing.load(Ordering::SeqCst) {
|
||||
return jack::Control::Continue;
|
||||
}
|
||||
|
||||
// This should be equal to self.buffer_size, but the implementation will
|
||||
// work even if it is less. Will panic in `temp_buffer_to_data` if greater.
|
||||
let current_frame_count = process_scope.n_frames() as usize;
|
||||
|
||||
// Get timestamp data
|
||||
let cycle_times = process_scope.cycle_times();
|
||||
let current_start_usecs = match cycle_times {
|
||||
Ok(times) => times.current_usecs,
|
||||
Err(_) => {
|
||||
// jack was unable to get the current time information
|
||||
// Fall back to using Instants
|
||||
let now = std::time::Instant::now();
|
||||
let duration = now.duration_since(self.creation_timestamp);
|
||||
duration.as_micros() as u64
|
||||
}
|
||||
};
|
||||
let start_cycle_instant = micros_to_stream_instant(current_start_usecs);
|
||||
let start_callback_instant = start_cycle_instant
|
||||
.add(frames_to_duration(
|
||||
process_scope.frames_since_cycle_start() as usize,
|
||||
self.sample_rate,
|
||||
))
|
||||
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
|
||||
|
||||
if let Some(input_callback) = &mut self.input_data_callback {
|
||||
// Let's get the data from the input ports and run the callback
|
||||
|
||||
let num_in_channels = self.in_ports.len();
|
||||
|
||||
// Read the data from the input ports into the temporary buffer
|
||||
// Go through every channel and store its data in the temporary input buffer
|
||||
for ch_ix in 0..num_in_channels {
|
||||
let input_channel = &self.in_ports[ch_ix].as_slice(process_scope);
|
||||
for i in 0..current_frame_count {
|
||||
self.temp_input_buffer[ch_ix + i * num_in_channels] = input_channel[i];
|
||||
}
|
||||
}
|
||||
// Create a slice of exactly current_frame_count frames
|
||||
let data = temp_buffer_to_data(
|
||||
&mut self.temp_input_buffer,
|
||||
current_frame_count * num_in_channels,
|
||||
);
|
||||
// Create timestamp
|
||||
let frames_since_cycle_start = process_scope.frames_since_cycle_start() as usize;
|
||||
let duration_since_cycle_start =
|
||||
frames_to_duration(frames_since_cycle_start, self.sample_rate);
|
||||
let callback = start_callback_instant
|
||||
.add(duration_since_cycle_start)
|
||||
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
|
||||
let capture = start_callback_instant;
|
||||
let timestamp = crate::InputStreamTimestamp { callback, capture };
|
||||
let info = crate::InputCallbackInfo { timestamp };
|
||||
input_callback(&data, &info);
|
||||
}
|
||||
|
||||
if let Some(output_callback) = &mut self.output_data_callback {
|
||||
let num_out_channels = self.out_ports.len();
|
||||
|
||||
// Create a slice of exactly current_frame_count frames
|
||||
let mut data = temp_buffer_to_data(
|
||||
&mut self.temp_output_buffer,
|
||||
current_frame_count * num_out_channels,
|
||||
);
|
||||
// Create timestamp
|
||||
let frames_since_cycle_start = process_scope.frames_since_cycle_start() as usize;
|
||||
let duration_since_cycle_start =
|
||||
frames_to_duration(frames_since_cycle_start, self.sample_rate);
|
||||
let callback = start_callback_instant
|
||||
.add(duration_since_cycle_start)
|
||||
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
|
||||
let buffer_duration = frames_to_duration(current_frame_count, self.sample_rate);
|
||||
let playback = start_cycle_instant
|
||||
.add(buffer_duration)
|
||||
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
|
||||
let timestamp = crate::OutputStreamTimestamp { callback, playback };
|
||||
let info = crate::OutputCallbackInfo { timestamp };
|
||||
output_callback(&mut data, &info);
|
||||
|
||||
// Deinterlace
|
||||
for ch_ix in 0..num_out_channels {
|
||||
let output_channel = &mut self.out_ports[ch_ix].as_mut_slice(process_scope);
|
||||
for i in 0..current_frame_count {
|
||||
output_channel[i] = self.temp_output_buffer[ch_ix + i * num_out_channels];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue as normal
|
||||
jack::Control::Continue
|
||||
}
|
||||
|
||||
fn buffer_size(&mut self, _: &jack::Client, size: jack::Frames) -> jack::Control {
|
||||
// The `buffer_size` callback is actually called on the process thread, but
|
||||
// it does not need to be suitable for real-time use. Thus we can simply allocate
|
||||
// new buffers here. It is also fine to call the error callback.
|
||||
// Details: https://github.com/RustAudio/rust-jack/issues/137
|
||||
let new_size = size as usize;
|
||||
if new_size != self.buffer_size {
|
||||
self.buffer_size = new_size;
|
||||
self.temp_input_buffer = vec![0.0; self.in_ports.len() * new_size];
|
||||
self.temp_output_buffer = vec![0.0; self.out_ports.len() * new_size];
|
||||
let description = format!("buffer size changed to: {}", new_size);
|
||||
if let Ok(mut mutex_guard) = self.error_callback_ptr.lock() {
|
||||
let err = &mut *mutex_guard;
|
||||
err(BackendSpecificError { description }.into());
|
||||
}
|
||||
}
|
||||
|
||||
jack::Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
fn micros_to_stream_instant(micros: u64) -> crate::StreamInstant {
|
||||
let nanos = micros * 1000;
|
||||
let secs = micros / 1_000_000;
|
||||
let subsec_nanos = nanos - secs * 1_000_000_000;
|
||||
crate::StreamInstant::new(secs as i64, subsec_nanos as u32)
|
||||
}
|
||||
|
||||
// Convert the given duration in frames at the given sample rate to a `std::time::Duration`.
|
||||
fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration {
|
||||
let secsf = frames as f64 / rate.0 as f64;
|
||||
let secs = secsf as u64;
|
||||
let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32;
|
||||
std::time::Duration::new(secs, nanos)
|
||||
}
|
||||
|
||||
/// Receives notifications from the JACK server. It is unclear if this may be run concurrent with itself under JACK2 specs
|
||||
/// so it needs to be Sync.
|
||||
struct JackNotificationHandler {
|
||||
error_callback_ptr: ErrorCallbackPtr,
|
||||
init_sample_rate_flag: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl JackNotificationHandler {
|
||||
pub fn new(error_callback_ptr: ErrorCallbackPtr) -> Self {
|
||||
JackNotificationHandler {
|
||||
error_callback_ptr,
|
||||
init_sample_rate_flag: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_error(&mut self, description: String) {
|
||||
// This thread isn't the audio thread, it's fine to block
|
||||
if let Ok(mut mutex_guard) = self.error_callback_ptr.lock() {
|
||||
let err = &mut *mutex_guard;
|
||||
err(BackendSpecificError { description }.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl jack::NotificationHandler for JackNotificationHandler {
|
||||
fn shutdown(&mut self, _status: jack::ClientStatus, reason: &str) {
|
||||
self.send_error(format!("JACK was shut down for reason: {}", reason));
|
||||
}
|
||||
|
||||
fn sample_rate(&mut self, _: &jack::Client, srate: jack::Frames) -> jack::Control {
|
||||
match self.init_sample_rate_flag.load(Ordering::SeqCst) {
|
||||
false => {
|
||||
// One of these notifications is sent every time a client is started.
|
||||
self.init_sample_rate_flag.store(true, Ordering::SeqCst);
|
||||
jack::Control::Continue
|
||||
}
|
||||
true => {
|
||||
self.send_error(format!("sample rate changed to: {}", srate));
|
||||
// Since CPAL currently has no way of signaling a sample rate change in order to make
|
||||
// all necessary changes that would bring we choose to quit.
|
||||
jack::Control::Quit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn xrun(&mut self, _: &jack::Client) -> jack::Control {
|
||||
self.send_error(String::from("xrun (buffer over or under run)"));
|
||||
jack::Control::Continue
|
||||
}
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
))]
|
||||
pub(crate) mod alsa;
|
||||
#[cfg(all(windows, feature = "asio"))]
|
||||
pub(crate) mod asio;
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
pub(crate) mod coreaudio;
|
||||
#[cfg(target_os = "emscripten")]
|
||||
pub(crate) mod emscripten;
|
||||
#[cfg(all(
|
||||
any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
),
|
||||
feature = "jack"
|
||||
))]
|
||||
pub(crate) mod jack;
|
||||
pub(crate) mod null;
|
||||
#[cfg(target_os = "android")]
|
||||
pub(crate) mod oboe;
|
||||
#[cfg(windows)]
|
||||
pub(crate) mod wasapi;
|
||||
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
|
||||
pub(crate) mod webaudio;
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use crate::{
|
||||
BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError,
|
||||
InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat,
|
||||
StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange,
|
||||
SupportedStreamConfigsError,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Devices;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Device;
|
||||
|
||||
pub struct Host;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Stream;
|
||||
|
||||
pub struct SupportedInputConfigs;
|
||||
pub struct SupportedOutputConfigs;
|
||||
|
||||
impl Host {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
||||
Ok(Host)
|
||||
}
|
||||
}
|
||||
|
||||
impl Devices {
|
||||
pub fn new() -> Result<Self, DevicesError> {
|
||||
Ok(Devices)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceTrait for Device {
|
||||
type SupportedInputConfigs = SupportedInputConfigs;
|
||||
type SupportedOutputConfigs = SupportedOutputConfigs;
|
||||
type Stream = Stream;
|
||||
|
||||
#[inline]
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
Ok("null".to_owned())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
_config: &StreamConfig,
|
||||
_sample_format: SampleFormat,
|
||||
_data_callback: D,
|
||||
_error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Create an output stream.
|
||||
fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
_config: &StreamConfig,
|
||||
_sample_format: SampleFormat,
|
||||
_data_callback: D,
|
||||
_error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTrait for Host {
|
||||
type Devices = Devices;
|
||||
type Device = Device;
|
||||
|
||||
fn is_available() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn devices(&self) -> Result<Self::Devices, DevicesError> {
|
||||
Devices::new()
|
||||
}
|
||||
|
||||
fn default_input_device(&self) -> Option<Device> {
|
||||
None
|
||||
}
|
||||
|
||||
fn default_output_device(&self) -> Option<Device> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTrait for Stream {
|
||||
fn play(&self) -> Result<(), PlayStreamError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<(), PauseStreamError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Devices {
|
||||
type Item = Device;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Device> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SupportedInputConfigs {
|
||||
type Item = SupportedStreamConfigRange;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<SupportedStreamConfigRange> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SupportedOutputConfigs {
|
||||
type Item = SupportedStreamConfigRange;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<SupportedStreamConfigRange> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
extern crate jni;
|
||||
|
||||
use self::jni::Executor;
|
||||
use self::jni::{errors::Result as JResult, JNIEnv, JavaVM};
|
||||
|
||||
// constants from android.media.AudioFormat
|
||||
pub const ENCODING_PCM_16BIT: i32 = 2;
|
||||
pub const ENCODING_PCM_FLOAT: i32 = 4;
|
||||
pub const CHANNEL_OUT_MONO: i32 = 4;
|
||||
pub const CHANNEL_OUT_STEREO: i32 = 12;
|
||||
|
||||
fn with_attached<F, R>(closure: F) -> JResult<R>
|
||||
where
|
||||
F: FnOnce(&mut JNIEnv) -> JResult<R>,
|
||||
{
|
||||
let android_context = ndk_context::android_context();
|
||||
let vm = Arc::new(unsafe { JavaVM::from_raw(android_context.vm().cast())? });
|
||||
Executor::new(vm).with_attached(|env| closure(env))
|
||||
}
|
||||
|
||||
fn get_min_buffer_size(
|
||||
class: &'static str,
|
||||
sample_rate: i32,
|
||||
channel_mask: i32,
|
||||
format: i32,
|
||||
) -> i32 {
|
||||
// Unwrapping everything because these operations are not expected to fail
|
||||
// or throw exceptions. Android returns negative values for invalid parameters,
|
||||
// which is what we expect.
|
||||
with_attached(|env| {
|
||||
let class = env.find_class(class).unwrap();
|
||||
env.call_static_method(
|
||||
class,
|
||||
"getMinBufferSize",
|
||||
"(III)I",
|
||||
&[sample_rate.into(), channel_mask.into(), format.into()],
|
||||
)
|
||||
.unwrap()
|
||||
.i()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn get_audio_track_min_buffer_size(sample_rate: i32, channel_mask: i32, format: i32) -> i32 {
|
||||
get_min_buffer_size(
|
||||
"android/media/AudioTrack",
|
||||
sample_rate,
|
||||
channel_mask,
|
||||
format,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_audio_record_min_buffer_size(sample_rate: i32, channel_mask: i32, format: i32) -> i32 {
|
||||
get_min_buffer_size(
|
||||
"android/media/AudioRecord",
|
||||
sample_rate,
|
||||
channel_mask,
|
||||
format,
|
||||
)
|
||||
}
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
use std::convert::TryInto;
|
||||
use std::time::Duration;
|
||||
|
||||
extern crate oboe;
|
||||
|
||||
use crate::{
|
||||
BackendSpecificError, BuildStreamError, PauseStreamError, PlayStreamError, StreamError,
|
||||
StreamInstant,
|
||||
};
|
||||
|
||||
pub fn to_stream_instant(duration: Duration) -> StreamInstant {
|
||||
StreamInstant::new(
|
||||
duration.as_secs().try_into().unwrap(),
|
||||
duration.subsec_nanos(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn stream_instant<T: oboe::AudioStreamSafe + ?Sized>(stream: &mut T) -> StreamInstant {
|
||||
const CLOCK_MONOTONIC: i32 = 1;
|
||||
let ts = stream
|
||||
.get_timestamp(CLOCK_MONOTONIC)
|
||||
.unwrap_or(oboe::FrameTimestamp {
|
||||
position: 0,
|
||||
timestamp: 0,
|
||||
});
|
||||
to_stream_instant(Duration::from_nanos(ts.timestamp as u64))
|
||||
}
|
||||
|
||||
impl From<oboe::Error> for StreamError {
|
||||
fn from(error: oboe::Error) -> Self {
|
||||
use self::oboe::Error::*;
|
||||
match error {
|
||||
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
|
||||
e => (BackendSpecificError {
|
||||
description: e.to_string(),
|
||||
})
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<oboe::Error> for PlayStreamError {
|
||||
fn from(error: oboe::Error) -> Self {
|
||||
use self::oboe::Error::*;
|
||||
match error {
|
||||
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
|
||||
e => (BackendSpecificError {
|
||||
description: e.to_string(),
|
||||
})
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<oboe::Error> for PauseStreamError {
|
||||
fn from(error: oboe::Error) -> Self {
|
||||
use self::oboe::Error::*;
|
||||
match error {
|
||||
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
|
||||
e => (BackendSpecificError {
|
||||
description: e.to_string(),
|
||||
})
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<oboe::Error> for BuildStreamError {
|
||||
fn from(error: oboe::Error) -> Self {
|
||||
use self::oboe::Error::*;
|
||||
match error {
|
||||
Disconnected | Unavailable | Closed => Self::DeviceNotAvailable,
|
||||
NoFreeHandles => Self::StreamIdOverflow,
|
||||
InvalidFormat | InvalidRate => Self::StreamConfigNotSupported,
|
||||
IllegalArgument => Self::InvalidArgument,
|
||||
e => (BackendSpecificError {
|
||||
description: e.to_string(),
|
||||
})
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::time::Instant;
|
||||
|
||||
extern crate oboe;
|
||||
|
||||
use super::convert::{stream_instant, to_stream_instant};
|
||||
use crate::{Data, InputCallbackInfo, InputStreamTimestamp, SizedSample, StreamError};
|
||||
|
||||
pub struct CpalInputCallback<I, C> {
|
||||
data_cb: Box<dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static>,
|
||||
error_cb: Box<dyn FnMut(StreamError) + Send + 'static>,
|
||||
created: Instant,
|
||||
phantom_channel: PhantomData<C>,
|
||||
phantom_input: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl<I, C> CpalInputCallback<I, C> {
|
||||
pub fn new<D, E>(data_cb: D, error_cb: E) -> Self
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
Self {
|
||||
data_cb: Box::new(data_cb),
|
||||
error_cb: Box::new(error_cb),
|
||||
created: Instant::now(),
|
||||
phantom_channel: PhantomData,
|
||||
phantom_input: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_callback_info(
|
||||
&self,
|
||||
audio_stream: &mut dyn oboe::AudioInputStreamSafe,
|
||||
) -> InputCallbackInfo {
|
||||
InputCallbackInfo {
|
||||
timestamp: InputStreamTimestamp {
|
||||
callback: to_stream_instant(self.created.elapsed()),
|
||||
capture: stream_instant(audio_stream),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SizedSample, C: oboe::IsChannelCount> oboe::AudioInputCallback for CpalInputCallback<T, C>
|
||||
where
|
||||
(T, C): oboe::IsFrameType,
|
||||
{
|
||||
type FrameType = (T, C);
|
||||
|
||||
fn on_error_before_close(
|
||||
&mut self,
|
||||
_audio_stream: &mut dyn oboe::AudioInputStreamSafe,
|
||||
error: oboe::Error,
|
||||
) {
|
||||
(self.error_cb)(StreamError::from(error))
|
||||
}
|
||||
|
||||
fn on_error_after_close(
|
||||
&mut self,
|
||||
_audio_stream: &mut dyn oboe::AudioInputStreamSafe,
|
||||
error: oboe::Error,
|
||||
) {
|
||||
(self.error_cb)(StreamError::from(error))
|
||||
}
|
||||
|
||||
fn on_audio_ready(
|
||||
&mut self,
|
||||
audio_stream: &mut dyn oboe::AudioInputStreamSafe,
|
||||
audio_data: &[<<Self as oboe::AudioInputCallback>::FrameType as oboe::IsFrameType>::Type],
|
||||
) -> oboe::DataCallbackResult {
|
||||
let cb_info = self.make_callback_info(audio_stream);
|
||||
let channel_count = if C::CHANNEL_COUNT == oboe::ChannelCount::Mono {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
(self.data_cb)(
|
||||
&unsafe {
|
||||
Data::from_parts(
|
||||
audio_data.as_ptr() as *mut _,
|
||||
audio_data.len() * channel_count,
|
||||
T::FORMAT,
|
||||
)
|
||||
},
|
||||
&cb_info,
|
||||
);
|
||||
oboe::DataCallbackResult::Continue
|
||||
}
|
||||
}
|
||||
-499
@@ -1,499 +0,0 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp;
|
||||
use std::convert::TryInto;
|
||||
use std::time::Duration;
|
||||
use std::vec::IntoIter as VecIntoIter;
|
||||
|
||||
extern crate oboe;
|
||||
|
||||
use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use crate::{
|
||||
BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError,
|
||||
DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError,
|
||||
PlayStreamError, SampleFormat, SampleRate, SizedSample, StreamConfig, StreamError,
|
||||
SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange,
|
||||
SupportedStreamConfigsError,
|
||||
};
|
||||
|
||||
mod android_media;
|
||||
mod convert;
|
||||
mod input_callback;
|
||||
mod output_callback;
|
||||
|
||||
use self::android_media::{get_audio_record_min_buffer_size, get_audio_track_min_buffer_size};
|
||||
use self::input_callback::CpalInputCallback;
|
||||
use self::oboe::{AudioInputStream, AudioOutputStream};
|
||||
use self::output_callback::CpalOutputCallback;
|
||||
|
||||
// Android Java API supports up to 8 channels, but oboe API
|
||||
// only exposes mono and stereo.
|
||||
const CHANNEL_MASKS: [i32; 2] = [
|
||||
android_media::CHANNEL_OUT_MONO,
|
||||
android_media::CHANNEL_OUT_STEREO,
|
||||
];
|
||||
|
||||
const SAMPLE_RATES: [i32; 13] = [
|
||||
5512, 8000, 11025, 16000, 22050, 32000, 44100, 48000, 64000, 88200, 96000, 176_400, 192_000,
|
||||
];
|
||||
|
||||
pub struct Host;
|
||||
#[derive(Clone)]
|
||||
pub struct Device(Option<oboe::AudioDeviceInfo>);
|
||||
pub enum Stream {
|
||||
Input(Box<RefCell<dyn AudioInputStream>>),
|
||||
Output(Box<RefCell<dyn AudioOutputStream>>),
|
||||
}
|
||||
pub type SupportedInputConfigs = VecIntoIter<SupportedStreamConfigRange>;
|
||||
pub type SupportedOutputConfigs = VecIntoIter<SupportedStreamConfigRange>;
|
||||
pub type Devices = VecIntoIter<Device>;
|
||||
|
||||
impl Host {
|
||||
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
||||
Ok(Host)
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTrait for Host {
|
||||
type Devices = Devices;
|
||||
type Device = Device;
|
||||
|
||||
fn is_available() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn devices(&self) -> Result<Self::Devices, DevicesError> {
|
||||
if let Ok(devices) = oboe::AudioDeviceInfo::request(oboe::AudioDeviceDirection::InputOutput)
|
||||
{
|
||||
Ok(devices
|
||||
.into_iter()
|
||||
.map(|d| Device(Some(d)))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter())
|
||||
} else {
|
||||
Ok(vec![Device(None)].into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_input_device(&self) -> Option<Self::Device> {
|
||||
Some(Device(None))
|
||||
}
|
||||
|
||||
fn default_output_device(&self) -> Option<Self::Device> {
|
||||
Some(Device(None))
|
||||
}
|
||||
}
|
||||
|
||||
fn buffer_size_range_for_params(
|
||||
is_output: bool,
|
||||
sample_rate: i32,
|
||||
channel_mask: i32,
|
||||
android_format: i32,
|
||||
) -> SupportedBufferSize {
|
||||
let min_buffer_size = if is_output {
|
||||
get_audio_track_min_buffer_size(sample_rate, channel_mask, android_format)
|
||||
} else {
|
||||
get_audio_record_min_buffer_size(sample_rate, channel_mask, android_format)
|
||||
};
|
||||
if min_buffer_size > 0 {
|
||||
SupportedBufferSize::Range {
|
||||
min: min_buffer_size as u32,
|
||||
max: i32::MAX as u32,
|
||||
}
|
||||
} else {
|
||||
SupportedBufferSize::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
fn default_supported_configs(is_output: bool) -> VecIntoIter<SupportedStreamConfigRange> {
|
||||
// Have to "brute force" the parameter combinations with getMinBufferSize
|
||||
const FORMATS: [SampleFormat; 2] = [SampleFormat::I16, SampleFormat::F32];
|
||||
|
||||
let mut output = Vec::with_capacity(SAMPLE_RATES.len() * CHANNEL_MASKS.len() * FORMATS.len());
|
||||
for sample_format in &FORMATS {
|
||||
let android_format = if *sample_format == SampleFormat::I16 {
|
||||
android_media::ENCODING_PCM_16BIT
|
||||
} else {
|
||||
android_media::ENCODING_PCM_FLOAT
|
||||
};
|
||||
for (mask_idx, channel_mask) in CHANNEL_MASKS.iter().enumerate() {
|
||||
let channel_count = mask_idx + 1;
|
||||
for sample_rate in &SAMPLE_RATES {
|
||||
if let SupportedBufferSize::Range { min, max } = buffer_size_range_for_params(
|
||||
is_output,
|
||||
*sample_rate,
|
||||
*channel_mask,
|
||||
android_format,
|
||||
) {
|
||||
output.push(SupportedStreamConfigRange {
|
||||
channels: channel_count as u16,
|
||||
min_sample_rate: SampleRate(*sample_rate as u32),
|
||||
max_sample_rate: SampleRate(*sample_rate as u32),
|
||||
buffer_size: SupportedBufferSize::Range { min, max },
|
||||
sample_format: *sample_format,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output.into_iter()
|
||||
}
|
||||
|
||||
fn device_supported_configs(
|
||||
device: &oboe::AudioDeviceInfo,
|
||||
is_output: bool,
|
||||
) -> VecIntoIter<SupportedStreamConfigRange> {
|
||||
let sample_rates = if !device.sample_rates.is_empty() {
|
||||
device.sample_rates.as_slice()
|
||||
} else {
|
||||
&SAMPLE_RATES
|
||||
};
|
||||
|
||||
const ALL_CHANNELS: [i32; 2] = [1, 2];
|
||||
let channel_counts = if !device.channel_counts.is_empty() {
|
||||
device.channel_counts.as_slice()
|
||||
} else {
|
||||
&ALL_CHANNELS
|
||||
};
|
||||
|
||||
const ALL_FORMATS: [oboe::AudioFormat; 2] = [oboe::AudioFormat::I16, oboe::AudioFormat::F32];
|
||||
let formats = if !device.formats.is_empty() {
|
||||
device.formats.as_slice()
|
||||
} else {
|
||||
&ALL_FORMATS
|
||||
};
|
||||
|
||||
let mut output = Vec::with_capacity(sample_rates.len() * channel_counts.len() * formats.len());
|
||||
for sample_rate in sample_rates {
|
||||
for channel_count in channel_counts {
|
||||
assert!(*channel_count > 0);
|
||||
if *channel_count > 2 {
|
||||
// could be supported by the device, but oboe does not support more than 2 channels
|
||||
continue;
|
||||
}
|
||||
let channel_mask = CHANNEL_MASKS[*channel_count as usize - 1];
|
||||
for format in formats {
|
||||
let (android_format, sample_format) = match format {
|
||||
oboe::AudioFormat::I16 => {
|
||||
(android_media::ENCODING_PCM_16BIT, SampleFormat::I16)
|
||||
}
|
||||
oboe::AudioFormat::F32 => {
|
||||
(android_media::ENCODING_PCM_FLOAT, SampleFormat::F32)
|
||||
}
|
||||
_ => panic!("Unexpected format"),
|
||||
};
|
||||
let buffer_size = buffer_size_range_for_params(
|
||||
is_output,
|
||||
*sample_rate,
|
||||
channel_mask,
|
||||
android_format,
|
||||
);
|
||||
output.push(SupportedStreamConfigRange {
|
||||
channels: cmp::min(*channel_count as u16, 2u16),
|
||||
min_sample_rate: SampleRate(*sample_rate as u32),
|
||||
max_sample_rate: SampleRate(*sample_rate as u32),
|
||||
buffer_size,
|
||||
sample_format,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output.into_iter()
|
||||
}
|
||||
|
||||
fn configure_for_device<D, C, I>(
|
||||
builder: oboe::AudioStreamBuilder<D, C, I>,
|
||||
device: &Device,
|
||||
config: &StreamConfig,
|
||||
) -> oboe::AudioStreamBuilder<D, C, I> {
|
||||
let mut builder = if let Some(info) = &device.0 {
|
||||
builder.set_device_id(info.id)
|
||||
} else {
|
||||
builder
|
||||
};
|
||||
builder = builder.set_sample_rate(config.sample_rate.0.try_into().unwrap());
|
||||
match &config.buffer_size {
|
||||
BufferSize::Default => builder,
|
||||
BufferSize::Fixed(size) => builder.set_buffer_capacity_in_frames(*size as i32),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_input_stream<D, E, C, T>(
|
||||
device: &Device,
|
||||
config: &StreamConfig,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
builder: oboe::AudioStreamBuilder<oboe::Input, C, T>,
|
||||
) -> Result<Stream, BuildStreamError>
|
||||
where
|
||||
T: SizedSample + oboe::IsFormat + Send + 'static,
|
||||
C: oboe::IsChannelCount + Send + 'static,
|
||||
(T, C): oboe::IsFrameType,
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
let builder = configure_for_device(builder, device, config);
|
||||
let stream = builder
|
||||
.set_callback(CpalInputCallback::<T, C>::new(
|
||||
data_callback,
|
||||
error_callback,
|
||||
))
|
||||
.open_stream()?;
|
||||
Ok(Stream::Input(Box::new(RefCell::new(stream))))
|
||||
}
|
||||
|
||||
fn build_output_stream<D, E, C, T>(
|
||||
device: &Device,
|
||||
config: &StreamConfig,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
builder: oboe::AudioStreamBuilder<oboe::Output, C, T>,
|
||||
) -> Result<Stream, BuildStreamError>
|
||||
where
|
||||
T: SizedSample + oboe::IsFormat + Send + 'static,
|
||||
C: oboe::IsChannelCount + Send + 'static,
|
||||
(T, C): oboe::IsFrameType,
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
let builder = configure_for_device(builder, device, config);
|
||||
let stream = builder
|
||||
.set_callback(CpalOutputCallback::<T, C>::new(
|
||||
data_callback,
|
||||
error_callback,
|
||||
))
|
||||
.open_stream()?;
|
||||
Ok(Stream::Output(Box::new(RefCell::new(stream))))
|
||||
}
|
||||
|
||||
impl DeviceTrait for Device {
|
||||
type SupportedInputConfigs = SupportedInputConfigs;
|
||||
type SupportedOutputConfigs = SupportedOutputConfigs;
|
||||
type Stream = Stream;
|
||||
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
match &self.0 {
|
||||
None => Ok("default".to_owned()),
|
||||
Some(info) => Ok(info.product_name.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
if let Some(info) = &self.0 {
|
||||
Ok(device_supported_configs(info, false))
|
||||
} else {
|
||||
Ok(default_supported_configs(false))
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
if let Some(info) = &self.0 {
|
||||
Ok(device_supported_configs(info, true))
|
||||
} else {
|
||||
Ok(default_supported_configs(true))
|
||||
}
|
||||
}
|
||||
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
let mut configs: Vec<_> = self.supported_input_configs().unwrap().collect();
|
||||
configs.sort_by(|a, b| b.cmp_default_heuristics(a));
|
||||
let config = configs
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?
|
||||
.with_max_sample_rate();
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
let mut configs: Vec<_> = self.supported_output_configs().unwrap().collect();
|
||||
configs.sort_by(|a, b| b.cmp_default_heuristics(a));
|
||||
let config = configs
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(DefaultStreamConfigError::StreamTypeNotSupported)?
|
||||
.with_max_sample_rate();
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
match sample_format {
|
||||
SampleFormat::I16 => {
|
||||
let builder = oboe::AudioStreamBuilder::default()
|
||||
.set_input()
|
||||
.set_format::<i16>();
|
||||
if config.channels == 1 {
|
||||
build_input_stream(
|
||||
self,
|
||||
config,
|
||||
data_callback,
|
||||
error_callback,
|
||||
builder.set_mono(),
|
||||
)
|
||||
} else if config.channels == 2 {
|
||||
build_input_stream(
|
||||
self,
|
||||
config,
|
||||
data_callback,
|
||||
error_callback,
|
||||
builder.set_stereo(),
|
||||
)
|
||||
} else {
|
||||
Err(BackendSpecificError {
|
||||
description: "More than 2 channels are not supported by Oboe.".to_owned(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
SampleFormat::F32 => {
|
||||
let builder = oboe::AudioStreamBuilder::default()
|
||||
.set_input()
|
||||
.set_format::<f32>();
|
||||
if config.channels == 1 {
|
||||
build_input_stream(
|
||||
self,
|
||||
config,
|
||||
data_callback,
|
||||
error_callback,
|
||||
builder.set_mono(),
|
||||
)
|
||||
} else if config.channels == 2 {
|
||||
build_input_stream(
|
||||
self,
|
||||
config,
|
||||
data_callback,
|
||||
error_callback,
|
||||
builder.set_stereo(),
|
||||
)
|
||||
} else {
|
||||
Err(BackendSpecificError {
|
||||
description: "More than 2 channels are not supported by Oboe.".to_owned(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
sample_format => Err(BackendSpecificError {
|
||||
description: format!("{} format is not supported on Android.", sample_format),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
match sample_format {
|
||||
SampleFormat::I16 => {
|
||||
let builder = oboe::AudioStreamBuilder::default()
|
||||
.set_output()
|
||||
.set_format::<i16>();
|
||||
if config.channels == 1 {
|
||||
build_output_stream(
|
||||
self,
|
||||
config,
|
||||
data_callback,
|
||||
error_callback,
|
||||
builder.set_mono(),
|
||||
)
|
||||
} else if config.channels == 2 {
|
||||
build_output_stream(
|
||||
self,
|
||||
config,
|
||||
data_callback,
|
||||
error_callback,
|
||||
builder.set_stereo(),
|
||||
)
|
||||
} else {
|
||||
Err(BackendSpecificError {
|
||||
description: "More than 2 channels are not supported by Oboe.".to_owned(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
SampleFormat::F32 => {
|
||||
let builder = oboe::AudioStreamBuilder::default()
|
||||
.set_output()
|
||||
.set_format::<f32>();
|
||||
if config.channels == 1 {
|
||||
build_output_stream(
|
||||
self,
|
||||
config,
|
||||
data_callback,
|
||||
error_callback,
|
||||
builder.set_mono(),
|
||||
)
|
||||
} else if config.channels == 2 {
|
||||
build_output_stream(
|
||||
self,
|
||||
config,
|
||||
data_callback,
|
||||
error_callback,
|
||||
builder.set_stereo(),
|
||||
)
|
||||
} else {
|
||||
Err(BackendSpecificError {
|
||||
description: "More than 2 channels are not supported by Oboe.".to_owned(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
sample_format => Err(BackendSpecificError {
|
||||
description: format!("{} format is not supported on Android.", sample_format),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTrait for Stream {
|
||||
fn play(&self) -> Result<(), PlayStreamError> {
|
||||
match self {
|
||||
Self::Input(stream) => stream
|
||||
.borrow_mut()
|
||||
.request_start()
|
||||
.map_err(PlayStreamError::from),
|
||||
Self::Output(stream) => stream
|
||||
.borrow_mut()
|
||||
.request_start()
|
||||
.map_err(PlayStreamError::from),
|
||||
}
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<(), PauseStreamError> {
|
||||
match self {
|
||||
Self::Input(_) => Err(BackendSpecificError {
|
||||
description: "Pause called on the input stream.".to_owned(),
|
||||
}
|
||||
.into()),
|
||||
Self::Output(stream) => stream
|
||||
.borrow_mut()
|
||||
.request_pause()
|
||||
.map_err(PauseStreamError::from),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::time::Instant;
|
||||
|
||||
extern crate oboe;
|
||||
|
||||
use super::convert::{stream_instant, to_stream_instant};
|
||||
use crate::{Data, OutputCallbackInfo, OutputStreamTimestamp, SizedSample, StreamError};
|
||||
|
||||
pub struct CpalOutputCallback<I, C> {
|
||||
data_cb: Box<dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static>,
|
||||
error_cb: Box<dyn FnMut(StreamError) + Send + 'static>,
|
||||
created: Instant,
|
||||
phantom_channel: PhantomData<C>,
|
||||
phantom_input: PhantomData<I>,
|
||||
}
|
||||
|
||||
impl<I, C> CpalOutputCallback<I, C> {
|
||||
pub fn new<D, E>(data_cb: D, error_cb: E) -> Self
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
Self {
|
||||
data_cb: Box::new(data_cb),
|
||||
error_cb: Box::new(error_cb),
|
||||
created: Instant::now(),
|
||||
phantom_channel: PhantomData,
|
||||
phantom_input: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_callback_info(
|
||||
&self,
|
||||
audio_stream: &mut dyn oboe::AudioOutputStreamSafe,
|
||||
) -> OutputCallbackInfo {
|
||||
OutputCallbackInfo {
|
||||
timestamp: OutputStreamTimestamp {
|
||||
callback: to_stream_instant(self.created.elapsed()),
|
||||
playback: stream_instant(audio_stream),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: SizedSample, C: oboe::IsChannelCount> oboe::AudioOutputCallback for CpalOutputCallback<T, C>
|
||||
where
|
||||
(T, C): oboe::IsFrameType,
|
||||
{
|
||||
type FrameType = (T, C);
|
||||
|
||||
fn on_error_before_close(
|
||||
&mut self,
|
||||
_audio_stream: &mut dyn oboe::AudioOutputStreamSafe,
|
||||
error: oboe::Error,
|
||||
) {
|
||||
(self.error_cb)(StreamError::from(error))
|
||||
}
|
||||
|
||||
fn on_error_after_close(
|
||||
&mut self,
|
||||
_audio_stream: &mut dyn oboe::AudioOutputStreamSafe,
|
||||
error: oboe::Error,
|
||||
) {
|
||||
(self.error_cb)(StreamError::from(error))
|
||||
}
|
||||
|
||||
fn on_audio_ready(
|
||||
&mut self,
|
||||
audio_stream: &mut dyn oboe::AudioOutputStreamSafe,
|
||||
audio_data: &mut [<<Self as oboe::AudioOutputCallback>::FrameType as oboe::IsFrameType>::Type],
|
||||
) -> oboe::DataCallbackResult {
|
||||
let cb_info = self.make_callback_info(audio_stream);
|
||||
let channel_count = if C::CHANNEL_COUNT == oboe::ChannelCount::Mono {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
(self.data_cb)(
|
||||
&mut unsafe {
|
||||
Data::from_parts(
|
||||
audio_data.as_mut_ptr() as *mut _,
|
||||
audio_data.len() * channel_count,
|
||||
T::FORMAT,
|
||||
)
|
||||
},
|
||||
&cb_info,
|
||||
);
|
||||
oboe::DataCallbackResult::Continue
|
||||
}
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
//! Handles COM initialization and cleanup.
|
||||
|
||||
use super::IoError;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use windows::Win32::Foundation::RPC_E_CHANGED_MODE;
|
||||
use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED};
|
||||
|
||||
thread_local!(static COM_INITIALIZED: ComInitialized = {
|
||||
unsafe {
|
||||
// Try to initialize COM with STA by default to avoid compatibility issues with the ASIO
|
||||
// backend (where CoInitialize() is called by the ASIO SDK) or winit (where drag and drop
|
||||
// requires STA).
|
||||
// This call can fail with RPC_E_CHANGED_MODE if another library initialized COM with MTA.
|
||||
// That's OK though since COM ensures thread-safety/compatibility through marshalling when
|
||||
// necessary.
|
||||
let result = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
|
||||
if result.is_ok() || result == RPC_E_CHANGED_MODE {
|
||||
ComInitialized {
|
||||
result,
|
||||
_ptr: PhantomData,
|
||||
}
|
||||
} else {
|
||||
// COM initialization failed in another way, something is really wrong.
|
||||
panic!(
|
||||
"Failed to initialize COM: {}",
|
||||
IoError::from_raw_os_error(result.0)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/// RAII object that guards the fact that COM is initialized.
|
||||
///
|
||||
// We store a raw pointer because it's the only way at the moment to remove `Send`/`Sync` from the
|
||||
// object.
|
||||
struct ComInitialized {
|
||||
result: windows::core::HRESULT,
|
||||
_ptr: PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
impl Drop for ComInitialized {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
// Need to avoid calling CoUninitialize() if CoInitializeEx failed since it may have
|
||||
// returned RPC_E_MODE_CHANGED - which is OK, see above.
|
||||
if self.result.is_ok() {
|
||||
unsafe { CoUninitialize() };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensures that COM is initialized in this thread.
|
||||
#[inline]
|
||||
pub fn com_initialized() {
|
||||
COM_INITIALIZED.with(|_| {});
|
||||
}
|
||||
-1021
File diff suppressed because it is too large.
Load diff
-104
@@ -1,104 +0,0 @@
|
||||
pub use self::device::{
|
||||
default_input_device, default_output_device, Device, Devices, SupportedInputConfigs,
|
||||
SupportedOutputConfigs,
|
||||
};
|
||||
pub use self::stream::Stream;
|
||||
use crate::traits::HostTrait;
|
||||
use crate::BackendSpecificError;
|
||||
use crate::DevicesError;
|
||||
use std::io::Error as IoError;
|
||||
use windows::Win32::Media::Audio;
|
||||
|
||||
mod com;
|
||||
mod device;
|
||||
mod stream;
|
||||
|
||||
/// The WASAPI host, the default windows host type.
|
||||
///
|
||||
/// Note: If you use a WASAPI output device as an input device it will
|
||||
/// transparently enable loopback mode (see
|
||||
/// https://docs.microsoft.com/en-us/windows/win32/coreaudio/loopback-recording).
|
||||
#[derive(Debug)]
|
||||
pub struct Host;
|
||||
|
||||
impl Host {
|
||||
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
||||
Ok(Host)
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTrait for Host {
|
||||
type Devices = Devices;
|
||||
type Device = Device;
|
||||
|
||||
fn is_available() -> bool {
|
||||
// Assume WASAPI is always available on Windows.
|
||||
true
|
||||
}
|
||||
|
||||
fn devices(&self) -> Result<Self::Devices, DevicesError> {
|
||||
Devices::new()
|
||||
}
|
||||
|
||||
fn default_input_device(&self) -> Option<Self::Device> {
|
||||
default_input_device()
|
||||
}
|
||||
|
||||
fn default_output_device(&self) -> Option<Self::Device> {
|
||||
default_output_device()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<windows::core::Error> for BackendSpecificError {
|
||||
fn from(error: windows::core::Error) -> Self {
|
||||
BackendSpecificError {
|
||||
description: format!("{}", IoError::from(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ErrDeviceNotAvailable: From<BackendSpecificError> {
|
||||
fn device_not_available() -> Self;
|
||||
}
|
||||
|
||||
impl ErrDeviceNotAvailable for crate::BuildStreamError {
|
||||
fn device_not_available() -> Self {
|
||||
Self::DeviceNotAvailable
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrDeviceNotAvailable for crate::SupportedStreamConfigsError {
|
||||
fn device_not_available() -> Self {
|
||||
Self::DeviceNotAvailable
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrDeviceNotAvailable for crate::DefaultStreamConfigError {
|
||||
fn device_not_available() -> Self {
|
||||
Self::DeviceNotAvailable
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrDeviceNotAvailable for crate::StreamError {
|
||||
fn device_not_available() -> Self {
|
||||
Self::DeviceNotAvailable
|
||||
}
|
||||
}
|
||||
|
||||
fn windows_err_to_cpal_err<E: ErrDeviceNotAvailable>(e: windows::core::Error) -> E {
|
||||
windows_err_to_cpal_err_message::<E>(e, "")
|
||||
}
|
||||
|
||||
fn windows_err_to_cpal_err_message<E: ErrDeviceNotAvailable>(
|
||||
e: windows::core::Error,
|
||||
message: &str,
|
||||
) -> E {
|
||||
match e.code() {
|
||||
Audio::AUDCLNT_E_DEVICE_INVALIDATED => E::device_not_available(),
|
||||
_ => {
|
||||
let description = format!("{}{}", message, e);
|
||||
let err = BackendSpecificError { description };
|
||||
err.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
-559
@@ -1,559 +0,0 @@
|
||||
use super::windows_err_to_cpal_err;
|
||||
use crate::traits::StreamTrait;
|
||||
use crate::{
|
||||
BackendSpecificError, Data, InputCallbackInfo, OutputCallbackInfo, PauseStreamError,
|
||||
PlayStreamError, SampleFormat, StreamError,
|
||||
};
|
||||
use std::mem;
|
||||
use std::ptr;
|
||||
use std::sync::mpsc::{channel, Receiver, SendError, Sender};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use windows::Win32::Foundation;
|
||||
use windows::Win32::Foundation::HANDLE;
|
||||
use windows::Win32::Foundation::WAIT_OBJECT_0;
|
||||
use windows::Win32::Media::Audio;
|
||||
use windows::Win32::System::SystemServices;
|
||||
use windows::Win32::System::Threading;
|
||||
|
||||
pub struct Stream {
|
||||
/// The high-priority audio processing thread calling callbacks.
|
||||
/// Option used for moving out in destructor.
|
||||
///
|
||||
/// TODO: Actually set the thread priority.
|
||||
thread: Option<JoinHandle<()>>,
|
||||
|
||||
// Commands processed by the `run()` method that is currently running.
|
||||
// `pending_scheduled_event` must be signalled whenever a command is added here, so that it
|
||||
// will get picked up.
|
||||
commands: Sender<Command>,
|
||||
|
||||
// This event is signalled after a new entry is added to `commands`, so that the `run()`
|
||||
// method can be notified.
|
||||
pending_scheduled_event: Foundation::HANDLE,
|
||||
}
|
||||
|
||||
struct RunContext {
|
||||
// Streams that have been created in this event loop.
|
||||
stream: StreamInner,
|
||||
|
||||
// Handles corresponding to the `event` field of each element of `voices`. Must always be in
|
||||
// sync with `voices`, except that the first element is always `pending_scheduled_event`.
|
||||
handles: Vec<Foundation::HANDLE>,
|
||||
|
||||
commands: Receiver<Command>,
|
||||
}
|
||||
|
||||
// Once we start running the eventloop, the RunContext will not be moved.
|
||||
unsafe impl Send for RunContext {}
|
||||
|
||||
pub enum Command {
|
||||
PlayStream,
|
||||
PauseStream,
|
||||
Terminate,
|
||||
}
|
||||
|
||||
pub enum AudioClientFlow {
|
||||
Render {
|
||||
render_client: Audio::IAudioRenderClient,
|
||||
},
|
||||
Capture {
|
||||
capture_client: Audio::IAudioCaptureClient,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct StreamInner {
|
||||
pub audio_client: Audio::IAudioClient,
|
||||
pub audio_clock: Audio::IAudioClock,
|
||||
pub client_flow: AudioClientFlow,
|
||||
// Event that is signalled by WASAPI whenever audio data must be written.
|
||||
pub event: Foundation::HANDLE,
|
||||
// True if the stream is currently playing. False if paused.
|
||||
pub playing: bool,
|
||||
// Number of frames of audio data in the underlying buffer allocated by WASAPI.
|
||||
pub max_frames_in_buffer: u32,
|
||||
// Number of bytes that each frame occupies.
|
||||
pub bytes_per_frame: u16,
|
||||
// The configuration with which the stream was created.
|
||||
pub config: crate::StreamConfig,
|
||||
// The sample format with which the stream was created.
|
||||
pub sample_format: SampleFormat,
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
pub(crate) fn new_input<D, E>(
|
||||
stream_inner: StreamInner,
|
||||
mut data_callback: D,
|
||||
mut error_callback: E,
|
||||
) -> Stream
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
let pending_scheduled_event = unsafe {
|
||||
Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))
|
||||
}
|
||||
.expect("cpal: could not create input stream event");
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let run_context = RunContext {
|
||||
handles: vec![pending_scheduled_event, stream_inner.event],
|
||||
stream: stream_inner,
|
||||
commands: rx,
|
||||
};
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name("cpal_wasapi_in".to_owned())
|
||||
.spawn(move || run_input(run_context, &mut data_callback, &mut error_callback))
|
||||
.unwrap();
|
||||
|
||||
Stream {
|
||||
thread: Some(thread),
|
||||
commands: tx,
|
||||
pending_scheduled_event,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_output<D, E>(
|
||||
stream_inner: StreamInner,
|
||||
mut data_callback: D,
|
||||
mut error_callback: E,
|
||||
) -> Stream
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
let pending_scheduled_event = unsafe {
|
||||
Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))
|
||||
}
|
||||
.expect("cpal: could not create output stream event");
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let run_context = RunContext {
|
||||
handles: vec![pending_scheduled_event, stream_inner.event],
|
||||
stream: stream_inner,
|
||||
commands: rx,
|
||||
};
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name("cpal_wasapi_out".to_owned())
|
||||
.spawn(move || run_output(run_context, &mut data_callback, &mut error_callback))
|
||||
.unwrap();
|
||||
|
||||
Stream {
|
||||
thread: Some(thread),
|
||||
commands: tx,
|
||||
pending_scheduled_event,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn push_command(&self, command: Command) -> Result<(), SendError<Command>> {
|
||||
self.commands.send(command)?;
|
||||
unsafe {
|
||||
Threading::SetEvent(self.pending_scheduled_event).unwrap();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Stream {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
if self.push_command(Command::Terminate).is_ok() {
|
||||
self.thread.take().unwrap().join().unwrap();
|
||||
unsafe {
|
||||
let _ = Foundation::CloseHandle(self.pending_scheduled_event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTrait for Stream {
|
||||
fn play(&self) -> Result<(), PlayStreamError> {
|
||||
self.push_command(Command::PlayStream)
|
||||
.map_err(|_| crate::error::PlayStreamError::DeviceNotAvailable)?;
|
||||
Ok(())
|
||||
}
|
||||
fn pause(&self) -> Result<(), PauseStreamError> {
|
||||
self.push_command(Command::PauseStream)
|
||||
.map_err(|_| crate::error::PauseStreamError::DeviceNotAvailable)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StreamInner {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let _ = Foundation::CloseHandle(self.event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process any pending commands that are queued within the `RunContext`.
|
||||
// Returns `true` if the loop should continue running, `false` if it should terminate.
|
||||
fn process_commands(run_context: &mut RunContext) -> Result<bool, StreamError> {
|
||||
// Process the pending commands.
|
||||
for command in run_context.commands.try_iter() {
|
||||
match command {
|
||||
Command::PlayStream => unsafe {
|
||||
if !run_context.stream.playing {
|
||||
run_context
|
||||
.stream
|
||||
.audio_client
|
||||
.Start()
|
||||
.map_err(windows_err_to_cpal_err::<StreamError>)?;
|
||||
run_context.stream.playing = true;
|
||||
}
|
||||
},
|
||||
Command::PauseStream => unsafe {
|
||||
if run_context.stream.playing {
|
||||
run_context
|
||||
.stream
|
||||
.audio_client
|
||||
.Stop()
|
||||
.map_err(windows_err_to_cpal_err::<StreamError>)?;
|
||||
run_context.stream.playing = false;
|
||||
}
|
||||
},
|
||||
Command::Terminate => {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
// Wait for any of the given handles to be signalled.
|
||||
//
|
||||
// Returns the index of the `handle` that was signalled, or an `Err` if
|
||||
// `WaitForMultipleObjectsEx` fails.
|
||||
//
|
||||
// This is called when the `run` thread is ready to wait for the next event. The
|
||||
// next event might be some command submitted by the user (the first handle) or
|
||||
// might indicate that one of the streams is ready to deliver or receive audio.
|
||||
fn wait_for_handle_signal(handles: &[Foundation::HANDLE]) -> Result<usize, BackendSpecificError> {
|
||||
debug_assert!(handles.len() <= SystemServices::MAXIMUM_WAIT_OBJECTS as usize);
|
||||
let result = unsafe {
|
||||
Threading::WaitForMultipleObjectsEx(
|
||||
handles,
|
||||
false, // Don't wait for all, just wait for the first
|
||||
Threading::INFINITE, // TODO: allow setting a timeout
|
||||
false, // irrelevant parameter here
|
||||
)
|
||||
};
|
||||
if result == Foundation::WAIT_FAILED {
|
||||
let err = unsafe { Foundation::GetLastError() };
|
||||
let description = format!("`WaitForMultipleObjectsEx failed: {:?}", err);
|
||||
let err = BackendSpecificError { description };
|
||||
return Err(err);
|
||||
}
|
||||
// Notifying the corresponding task handler.
|
||||
let handle_idx = (result.0 - WAIT_OBJECT_0.0) as usize;
|
||||
Ok(handle_idx)
|
||||
}
|
||||
|
||||
// Get the number of available frames that are available for writing/reading.
|
||||
fn get_available_frames(stream: &StreamInner) -> Result<u32, StreamError> {
|
||||
unsafe {
|
||||
let padding = stream
|
||||
.audio_client
|
||||
.GetCurrentPadding()
|
||||
.map_err(windows_err_to_cpal_err::<StreamError>)?;
|
||||
Ok(stream.max_frames_in_buffer - padding)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_input(
|
||||
mut run_ctxt: RunContext,
|
||||
data_callback: &mut dyn FnMut(&Data, &InputCallbackInfo),
|
||||
error_callback: &mut dyn FnMut(StreamError),
|
||||
) {
|
||||
boost_current_thread_priority();
|
||||
|
||||
loop {
|
||||
match process_commands_and_await_signal(&mut run_ctxt, error_callback) {
|
||||
Some(ControlFlow::Break) => break,
|
||||
Some(ControlFlow::Continue) => continue,
|
||||
None => (),
|
||||
}
|
||||
let capture_client = match run_ctxt.stream.client_flow {
|
||||
AudioClientFlow::Capture { ref capture_client } => capture_client.clone(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
match process_input(
|
||||
&run_ctxt.stream,
|
||||
capture_client,
|
||||
data_callback,
|
||||
error_callback,
|
||||
) {
|
||||
ControlFlow::Break => break,
|
||||
ControlFlow::Continue => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_output(
|
||||
mut run_ctxt: RunContext,
|
||||
data_callback: &mut dyn FnMut(&mut Data, &OutputCallbackInfo),
|
||||
error_callback: &mut dyn FnMut(StreamError),
|
||||
) {
|
||||
boost_current_thread_priority();
|
||||
|
||||
loop {
|
||||
match process_commands_and_await_signal(&mut run_ctxt, error_callback) {
|
||||
Some(ControlFlow::Break) => break,
|
||||
Some(ControlFlow::Continue) => continue,
|
||||
None => (),
|
||||
}
|
||||
let render_client = match run_ctxt.stream.client_flow {
|
||||
AudioClientFlow::Render { ref render_client } => render_client.clone(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
match process_output(
|
||||
&run_ctxt.stream,
|
||||
render_client,
|
||||
data_callback,
|
||||
error_callback,
|
||||
) {
|
||||
ControlFlow::Break => break,
|
||||
ControlFlow::Continue => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn boost_current_thread_priority() {
|
||||
unsafe {
|
||||
let thread_id = Threading::GetCurrentThreadId();
|
||||
|
||||
let _ = Threading::SetThreadPriority(
|
||||
HANDLE(thread_id as isize),
|
||||
Threading::THREAD_PRIORITY_TIME_CRITICAL,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ControlFlow {
|
||||
Break,
|
||||
Continue,
|
||||
}
|
||||
|
||||
fn process_commands_and_await_signal(
|
||||
run_context: &mut RunContext,
|
||||
error_callback: &mut dyn FnMut(StreamError),
|
||||
) -> Option<ControlFlow> {
|
||||
// Process queued commands.
|
||||
match process_commands(run_context) {
|
||||
Ok(true) => (),
|
||||
Ok(false) => return Some(ControlFlow::Break),
|
||||
Err(err) => {
|
||||
error_callback(err);
|
||||
return Some(ControlFlow::Break);
|
||||
}
|
||||
};
|
||||
|
||||
// Wait for any of the handles to be signalled.
|
||||
let handle_idx = match wait_for_handle_signal(&run_context.handles) {
|
||||
Ok(idx) => idx,
|
||||
Err(err) => {
|
||||
error_callback(err.into());
|
||||
return Some(ControlFlow::Break);
|
||||
}
|
||||
};
|
||||
|
||||
// If `handle_idx` is 0, then it's `pending_scheduled_event` that was signalled in
|
||||
// order for us to pick up the pending commands. Otherwise, a stream needs data.
|
||||
if handle_idx == 0 {
|
||||
return Some(ControlFlow::Continue);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// The loop for processing pending input data.
|
||||
fn process_input(
|
||||
stream: &StreamInner,
|
||||
capture_client: Audio::IAudioCaptureClient,
|
||||
data_callback: &mut dyn FnMut(&Data, &InputCallbackInfo),
|
||||
error_callback: &mut dyn FnMut(StreamError),
|
||||
) -> ControlFlow {
|
||||
unsafe {
|
||||
// Get the available data in the shared buffer.
|
||||
let mut buffer: *mut u8 = ptr::null_mut();
|
||||
let mut flags = mem::MaybeUninit::uninit();
|
||||
loop {
|
||||
let mut frames_available = match capture_client.GetNextPacketSize() {
|
||||
Ok(0) => return ControlFlow::Continue,
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
error_callback(windows_err_to_cpal_err(err));
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
};
|
||||
let mut qpc_position: u64 = 0;
|
||||
let result = capture_client.GetBuffer(
|
||||
&mut buffer,
|
||||
&mut frames_available,
|
||||
flags.as_mut_ptr(),
|
||||
None,
|
||||
Some(&mut qpc_position),
|
||||
);
|
||||
|
||||
match result {
|
||||
// TODO: Can this happen?
|
||||
Err(e) if e.code() == Audio::AUDCLNT_S_BUFFER_EMPTY => continue,
|
||||
Err(e) => {
|
||||
error_callback(windows_err_to_cpal_err(e));
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
Ok(_) => (),
|
||||
}
|
||||
|
||||
debug_assert!(!buffer.is_null());
|
||||
|
||||
let data = buffer as *mut ();
|
||||
let len = frames_available as usize * stream.bytes_per_frame as usize
|
||||
/ stream.sample_format.sample_size();
|
||||
let data = Data::from_parts(data, len, stream.sample_format);
|
||||
|
||||
// The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds.
|
||||
let timestamp = match input_timestamp(stream, qpc_position) {
|
||||
Ok(ts) => ts,
|
||||
Err(err) => {
|
||||
error_callback(err);
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
};
|
||||
let info = InputCallbackInfo { timestamp };
|
||||
data_callback(&data, &info);
|
||||
|
||||
// Release the buffer.
|
||||
let result = capture_client
|
||||
.ReleaseBuffer(frames_available)
|
||||
.map_err(windows_err_to_cpal_err);
|
||||
if let Err(err) = result {
|
||||
error_callback(err);
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The loop for writing output data.
|
||||
fn process_output(
|
||||
stream: &StreamInner,
|
||||
render_client: Audio::IAudioRenderClient,
|
||||
data_callback: &mut dyn FnMut(&mut Data, &OutputCallbackInfo),
|
||||
error_callback: &mut dyn FnMut(StreamError),
|
||||
) -> ControlFlow {
|
||||
// The number of frames available for writing.
|
||||
let frames_available = match get_available_frames(stream) {
|
||||
Ok(0) => return ControlFlow::Continue, // TODO: Can this happen?
|
||||
Ok(n) => n,
|
||||
Err(err) => {
|
||||
error_callback(err);
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let buffer = match render_client.GetBuffer(frames_available) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
error_callback(windows_err_to_cpal_err(e));
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
};
|
||||
|
||||
debug_assert!(!buffer.is_null());
|
||||
|
||||
let data = buffer as *mut ();
|
||||
let len = frames_available as usize * stream.bytes_per_frame as usize
|
||||
/ stream.sample_format.sample_size();
|
||||
let mut data = Data::from_parts(data, len, stream.sample_format);
|
||||
let sample_rate = stream.config.sample_rate;
|
||||
let timestamp = match output_timestamp(stream, frames_available, sample_rate) {
|
||||
Ok(ts) => ts,
|
||||
Err(err) => {
|
||||
error_callback(err);
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
};
|
||||
let info = OutputCallbackInfo { timestamp };
|
||||
data_callback(&mut data, &info);
|
||||
|
||||
if let Err(err) = render_client.ReleaseBuffer(frames_available, 0) {
|
||||
error_callback(windows_err_to_cpal_err(err));
|
||||
return ControlFlow::Break;
|
||||
}
|
||||
}
|
||||
|
||||
ControlFlow::Continue
|
||||
}
|
||||
|
||||
/// Convert the given duration in frames at the given sample rate to a `std::time::Duration`.
|
||||
fn frames_to_duration(frames: u32, rate: crate::SampleRate) -> std::time::Duration {
|
||||
let secsf = frames as f64 / rate.0 as f64;
|
||||
let secs = secsf as u64;
|
||||
let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32;
|
||||
std::time::Duration::new(secs, nanos)
|
||||
}
|
||||
|
||||
/// Use the stream's `IAudioClock` to produce the current stream instant.
|
||||
///
|
||||
/// Uses the QPC position produced via the `GetPosition` method.
|
||||
fn stream_instant(stream: &StreamInner) -> Result<crate::StreamInstant, StreamError> {
|
||||
let mut position: u64 = 0;
|
||||
let mut qpc_position: u64 = 0;
|
||||
unsafe {
|
||||
stream
|
||||
.audio_clock
|
||||
.GetPosition(&mut position, Some(&mut qpc_position))
|
||||
.map_err(windows_err_to_cpal_err::<StreamError>)?;
|
||||
};
|
||||
// The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds.
|
||||
let qpc_nanos = qpc_position as i128 * 100;
|
||||
let instant = crate::StreamInstant::from_nanos_i128(qpc_nanos)
|
||||
.expect("performance counter out of range of `StreamInstant` representation");
|
||||
Ok(instant)
|
||||
}
|
||||
|
||||
/// Produce the input stream timestamp.
|
||||
///
|
||||
/// `buffer_qpc_position` is the `qpc_position` returned via the `GetBuffer` call on the capture
|
||||
/// client. It represents the instant at which the first sample of the retrieved buffer was
|
||||
/// captured.
|
||||
fn input_timestamp(
|
||||
stream: &StreamInner,
|
||||
buffer_qpc_position: u64,
|
||||
) -> Result<crate::InputStreamTimestamp, StreamError> {
|
||||
// The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds.
|
||||
let qpc_nanos = buffer_qpc_position as i128 * 100;
|
||||
let capture = crate::StreamInstant::from_nanos_i128(qpc_nanos)
|
||||
.expect("performance counter out of range of `StreamInstant` representation");
|
||||
let callback = stream_instant(stream)?;
|
||||
Ok(crate::InputStreamTimestamp { capture, callback })
|
||||
}
|
||||
|
||||
/// Produce the output stream timestamp.
|
||||
///
|
||||
/// `frames_available` is the number of frames available for writing as reported by subtracting the
|
||||
/// result of `GetCurrentPadding` from the maximum buffer size.
|
||||
///
|
||||
/// `sample_rate` is the rate at which audio frames are processed by the device.
|
||||
///
|
||||
/// TODO: The returned `playback` is an estimate that assumes audio is delivered immediately after
|
||||
/// `frames_available` are consumed. The reality is that there is likely a tiny amount of latency
|
||||
/// after this, but not sure how to determine this.
|
||||
fn output_timestamp(
|
||||
stream: &StreamInner,
|
||||
frames_available: u32,
|
||||
sample_rate: crate::SampleRate,
|
||||
) -> Result<crate::OutputStreamTimestamp, StreamError> {
|
||||
let callback = stream_instant(stream)?;
|
||||
let buffer_duration = frames_to_duration(frames_available, sample_rate);
|
||||
let playback = callback
|
||||
.add(buffer_duration)
|
||||
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
|
||||
Ok(crate::OutputStreamTimestamp { callback, playback })
|
||||
}
|
||||
-528
@@ -1,528 +0,0 @@
|
||||
extern crate js_sys;
|
||||
extern crate wasm_bindgen;
|
||||
extern crate web_sys;
|
||||
|
||||
use self::js_sys::eval;
|
||||
use self::wasm_bindgen::prelude::*;
|
||||
use self::wasm_bindgen::JsCast;
|
||||
use self::web_sys::{AudioContext, AudioContextOptions};
|
||||
use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use crate::{
|
||||
BackendSpecificError, BufferSize, BuildStreamError, Data, DefaultStreamConfigError,
|
||||
DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError,
|
||||
PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize,
|
||||
SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError,
|
||||
};
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Content is false if the iterator is empty.
|
||||
pub struct Devices(bool);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Device;
|
||||
|
||||
pub struct Host;
|
||||
|
||||
pub struct Stream {
|
||||
ctx: Arc<AudioContext>,
|
||||
on_ended_closures: Vec<Arc<RwLock<Option<Closure<dyn FnMut()>>>>>,
|
||||
config: StreamConfig,
|
||||
buffer_size_frames: usize,
|
||||
}
|
||||
|
||||
pub type SupportedInputConfigs = ::std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
pub type SupportedOutputConfigs = ::std::vec::IntoIter<SupportedStreamConfigRange>;
|
||||
|
||||
const MIN_CHANNELS: u16 = 1;
|
||||
const MAX_CHANNELS: u16 = 32;
|
||||
const MIN_SAMPLE_RATE: SampleRate = SampleRate(8_000);
|
||||
const MAX_SAMPLE_RATE: SampleRate = SampleRate(96_000);
|
||||
const DEFAULT_SAMPLE_RATE: SampleRate = SampleRate(44_100);
|
||||
const MIN_BUFFER_SIZE: u32 = 1;
|
||||
const MAX_BUFFER_SIZE: u32 = u32::MAX;
|
||||
const DEFAULT_BUFFER_SIZE: usize = 2048;
|
||||
const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
|
||||
|
||||
impl Host {
|
||||
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
||||
Ok(Host)
|
||||
}
|
||||
}
|
||||
|
||||
impl HostTrait for Host {
|
||||
type Devices = Devices;
|
||||
type Device = Device;
|
||||
|
||||
fn is_available() -> bool {
|
||||
// Assume this host is always available on webaudio.
|
||||
true
|
||||
}
|
||||
|
||||
fn devices(&self) -> Result<Self::Devices, DevicesError> {
|
||||
Devices::new()
|
||||
}
|
||||
|
||||
fn default_input_device(&self) -> Option<Self::Device> {
|
||||
default_input_device()
|
||||
}
|
||||
|
||||
fn default_output_device(&self) -> Option<Self::Device> {
|
||||
default_output_device()
|
||||
}
|
||||
}
|
||||
|
||||
impl Devices {
|
||||
fn new() -> Result<Self, DevicesError> {
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Device {
|
||||
#[inline]
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
Ok("Default Device".to_owned())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
// TODO
|
||||
Ok(Vec::new().into_iter())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
let buffer_size = SupportedBufferSize::Range {
|
||||
min: MIN_BUFFER_SIZE,
|
||||
max: MAX_BUFFER_SIZE,
|
||||
};
|
||||
let configs: Vec<_> = (MIN_CHANNELS..=MAX_CHANNELS)
|
||||
.map(|channels| SupportedStreamConfigRange {
|
||||
channels,
|
||||
min_sample_rate: MIN_SAMPLE_RATE,
|
||||
max_sample_rate: MAX_SAMPLE_RATE,
|
||||
buffer_size: buffer_size.clone(),
|
||||
sample_format: SUPPORTED_SAMPLE_FORMAT,
|
||||
})
|
||||
.collect();
|
||||
Ok(configs.into_iter())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
// TODO
|
||||
Err(DefaultStreamConfigError::StreamTypeNotSupported)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
const EXPECT: &str = "expected at least one valid webaudio stream config";
|
||||
let config = self
|
||||
.supported_output_configs()
|
||||
.expect(EXPECT)
|
||||
.max_by(|a, b| a.cmp_default_heuristics(b))
|
||||
.unwrap()
|
||||
.with_sample_rate(DEFAULT_SAMPLE_RATE);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
impl DeviceTrait for Device {
|
||||
type SupportedInputConfigs = SupportedInputConfigs;
|
||||
type SupportedOutputConfigs = SupportedOutputConfigs;
|
||||
type Stream = Stream;
|
||||
|
||||
#[inline]
|
||||
fn name(&self) -> Result<String, DeviceNameError> {
|
||||
Device::name(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
|
||||
Device::supported_input_configs(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
|
||||
Device::supported_output_configs(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
Device::default_input_config(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
||||
Device::default_output_config(self)
|
||||
}
|
||||
|
||||
fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
_config: &StreamConfig,
|
||||
_sample_format: SampleFormat,
|
||||
_data_callback: D,
|
||||
_error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
// TODO
|
||||
Err(BuildStreamError::StreamConfigNotSupported)
|
||||
}
|
||||
|
||||
/// Create an output stream.
|
||||
fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
_error_callback: E,
|
||||
_timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
if !valid_config(config, sample_format) {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
}
|
||||
|
||||
let n_channels = config.channels as usize;
|
||||
|
||||
let buffer_size_frames = match config.buffer_size {
|
||||
BufferSize::Fixed(v) => {
|
||||
if v == 0 {
|
||||
return Err(BuildStreamError::StreamConfigNotSupported);
|
||||
} else {
|
||||
v as usize
|
||||
}
|
||||
}
|
||||
BufferSize::Default => DEFAULT_BUFFER_SIZE,
|
||||
};
|
||||
let buffer_size_samples = buffer_size_frames * n_channels;
|
||||
let buffer_time_step_secs = buffer_time_step_secs(buffer_size_frames, config.sample_rate);
|
||||
|
||||
let data_callback = Arc::new(Mutex::new(Box::new(data_callback)));
|
||||
|
||||
// Create the WebAudio stream.
|
||||
let mut stream_opts = AudioContextOptions::new();
|
||||
stream_opts.sample_rate(config.sample_rate.0 as f32);
|
||||
let ctx = AudioContext::new_with_context_options(&stream_opts).map_err(
|
||||
|err| -> BuildStreamError {
|
||||
let description = format!("{:?}", err);
|
||||
let err = BackendSpecificError { description };
|
||||
err.into()
|
||||
},
|
||||
)?;
|
||||
|
||||
let destination = ctx.destination();
|
||||
|
||||
// If possible, set the destination's channel_count to the given config.channel.
|
||||
// If not, fallback on the default destination channel_count to keep previous behavior
|
||||
// and do not return an error.
|
||||
if config.channels as u32 <= destination.max_channel_count() {
|
||||
destination.set_channel_count(config.channels as u32);
|
||||
}
|
||||
|
||||
let ctx = Arc::new(ctx);
|
||||
|
||||
// A container for managing the lifecycle of the audio callbacks.
|
||||
let mut on_ended_closures: Vec<Arc<RwLock<Option<Closure<dyn FnMut()>>>>> = Vec::new();
|
||||
|
||||
// A cursor keeping track of the current time at which new frames should be scheduled.
|
||||
let time = Arc::new(RwLock::new(0f64));
|
||||
|
||||
// Create a set of closures / callbacks which will continuously fetch and schedule sample
|
||||
// playback. Starting with two workers, e.g. a front and back buffer so that audio frames
|
||||
// can be fetched in the background.
|
||||
for _i in 0..2 {
|
||||
let data_callback_handle = data_callback.clone();
|
||||
let ctx_handle = ctx.clone();
|
||||
let time_handle = time.clone();
|
||||
|
||||
// A set of temporary buffers to be used for intermediate sample transformation steps.
|
||||
let mut temporary_buffer = vec![0f32; buffer_size_samples];
|
||||
let mut temporary_channel_buffer = vec![0f32; buffer_size_frames];
|
||||
|
||||
#[cfg(target_feature = "atomics")]
|
||||
let temporary_channel_array_view: js_sys::Float32Array;
|
||||
#[cfg(target_feature = "atomics")]
|
||||
{
|
||||
let temporary_channel_array = js_sys::ArrayBuffer::new(
|
||||
(std::mem::size_of::<f32>() * buffer_size_frames) as u32,
|
||||
);
|
||||
temporary_channel_array_view = js_sys::Float32Array::new(&temporary_channel_array);
|
||||
}
|
||||
|
||||
// Create a webaudio buffer which will be reused to avoid allocations.
|
||||
let ctx_buffer = ctx
|
||||
.create_buffer(
|
||||
config.channels as u32,
|
||||
buffer_size_frames as u32,
|
||||
config.sample_rate.0 as f32,
|
||||
)
|
||||
.map_err(|err| -> BuildStreamError {
|
||||
let description = format!("{:?}", err);
|
||||
let err = BackendSpecificError { description };
|
||||
err.into()
|
||||
})?;
|
||||
|
||||
// A self reference to this closure for passing to future audio event calls.
|
||||
let on_ended_closure: Arc<RwLock<Option<Closure<dyn FnMut()>>>> =
|
||||
Arc::new(RwLock::new(None));
|
||||
let on_ended_closure_handle = on_ended_closure.clone();
|
||||
|
||||
on_ended_closure
|
||||
.write()
|
||||
.unwrap()
|
||||
.replace(Closure::wrap(Box::new(move || {
|
||||
let now = ctx_handle.current_time();
|
||||
let time_at_start_of_buffer = {
|
||||
let time_at_start_of_buffer = time_handle
|
||||
.read()
|
||||
.expect("Unable to get a read lock on the time cursor");
|
||||
// Synchronise first buffer as necessary (eg. keep the time value
|
||||
// referenced to the context clock).
|
||||
if *time_at_start_of_buffer > 0.001 {
|
||||
*time_at_start_of_buffer
|
||||
} else {
|
||||
// 25ms of time to fetch the first sample data, increase to avoid
|
||||
// initial underruns.
|
||||
now + 0.025
|
||||
}
|
||||
};
|
||||
|
||||
// Populate the sample data into an interleaved temporary buffer.
|
||||
{
|
||||
let len = temporary_buffer.len();
|
||||
let data = temporary_buffer.as_mut_ptr() as *mut ();
|
||||
let mut data = unsafe { Data::from_parts(data, len, sample_format) };
|
||||
let mut data_callback = data_callback_handle.lock().unwrap();
|
||||
let callback = crate::StreamInstant::from_secs_f64(now);
|
||||
let playback = crate::StreamInstant::from_secs_f64(time_at_start_of_buffer);
|
||||
let timestamp = crate::OutputStreamTimestamp { callback, playback };
|
||||
let info = OutputCallbackInfo { timestamp };
|
||||
(data_callback.deref_mut())(&mut data, &info);
|
||||
}
|
||||
|
||||
// Deinterleave the sample data and copy into the audio context buffer.
|
||||
// We do not reference the audio context buffer directly e.g. getChannelData.
|
||||
// As wasm-bindgen only gives us a copy, not a direct reference.
|
||||
for channel in 0..n_channels {
|
||||
for i in 0..buffer_size_frames {
|
||||
temporary_channel_buffer[i] =
|
||||
temporary_buffer[n_channels * i + channel];
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "atomics"))]
|
||||
{
|
||||
ctx_buffer
|
||||
.copy_to_channel(&mut temporary_channel_buffer, channel as i32)
|
||||
.expect(
|
||||
"Unable to write sample data into the audio context buffer",
|
||||
);
|
||||
}
|
||||
|
||||
// copyToChannel cannot be directly copied into from a SharedArrayBuffer,
|
||||
// which WASM memory is backed by if the 'atomics' flag is enabled.
|
||||
// This workaround copies the data into an intermediary buffer first.
|
||||
// There's a chance browsers may eventually relax that requirement.
|
||||
// See this issue: https://github.com/WebAudio/web-audio-api/issues/2565
|
||||
#[cfg(target_feature = "atomics")]
|
||||
{
|
||||
temporary_channel_array_view.copy_from(&mut temporary_channel_buffer);
|
||||
ctx_buffer
|
||||
.unchecked_ref::<ExternalArrayAudioBuffer>()
|
||||
.copy_to_channel(&temporary_channel_array_view, channel as i32)
|
||||
.expect(
|
||||
"Unable to write sample data into the audio context buffer",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Create an AudioBufferSourceNode, schedule it to playback the reused buffer
|
||||
// in the future.
|
||||
let source = ctx_handle
|
||||
.create_buffer_source()
|
||||
.expect("Unable to create a webaudio buffer source");
|
||||
source.set_buffer(Some(&ctx_buffer));
|
||||
source
|
||||
.connect_with_audio_node(&ctx_handle.destination())
|
||||
.expect(
|
||||
"Unable to connect the web audio buffer source to the context destination",
|
||||
);
|
||||
source.set_onended(Some(
|
||||
on_ended_closure_handle
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unchecked_ref(),
|
||||
));
|
||||
|
||||
source
|
||||
.start_with_when(time_at_start_of_buffer)
|
||||
.expect("Unable to start the webaudio buffer source");
|
||||
|
||||
// Keep track of when the next buffer worth of samples should be played.
|
||||
*time_handle.write().unwrap() = time_at_start_of_buffer + buffer_time_step_secs;
|
||||
}) as Box<dyn FnMut()>));
|
||||
|
||||
on_ended_closures.push(on_ended_closure);
|
||||
}
|
||||
|
||||
Ok(Stream {
|
||||
ctx,
|
||||
on_ended_closures,
|
||||
config: config.clone(),
|
||||
buffer_size_frames,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
/// Return the [`AudioContext`](https://developer.mozilla.org/docs/Web/API/AudioContext) used
|
||||
/// by this stream.
|
||||
pub fn audio_context(&self) -> &AudioContext {
|
||||
&*self.ctx
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamTrait for Stream {
|
||||
fn play(&self) -> Result<(), PlayStreamError> {
|
||||
let window = web_sys::window().unwrap();
|
||||
match self.ctx.resume() {
|
||||
Ok(_) => {
|
||||
// Begin webaudio playback, initially scheduling the closures to fire on a timeout
|
||||
// event.
|
||||
let mut offset_ms = 10;
|
||||
let time_step_secs =
|
||||
buffer_time_step_secs(self.buffer_size_frames, self.config.sample_rate);
|
||||
let time_step_ms = (time_step_secs * 1_000.0) as i32;
|
||||
for on_ended_closure in self.on_ended_closures.iter() {
|
||||
window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
on_ended_closure
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unchecked_ref(),
|
||||
offset_ms,
|
||||
)
|
||||
.unwrap();
|
||||
offset_ms += time_step_ms;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let description = format!("{:?}", err);
|
||||
let err = BackendSpecificError { description };
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<(), PauseStreamError> {
|
||||
match self.ctx.suspend() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
let description = format!("{:?}", err);
|
||||
let err = BackendSpecificError { description };
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Stream {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.ctx.close();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Devices {
|
||||
fn default() -> Devices {
|
||||
// We produce an empty iterator if the WebAudio API isn't available.
|
||||
Devices(is_webaudio_available())
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Devices {
|
||||
type Item = Device;
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Device> {
|
||||
if self.0 {
|
||||
self.0 = false;
|
||||
Some(Device)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_input_device() -> Option<Device> {
|
||||
// TODO
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_output_device() -> Option<Device> {
|
||||
if is_webaudio_available() {
|
||||
Some(Device)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Detects whether the `AudioContext` global variable is available.
|
||||
fn is_webaudio_available() -> bool {
|
||||
if let Ok(audio_context_is_defined) = eval("typeof AudioContext !== 'undefined'") {
|
||||
audio_context_is_defined.as_bool().unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Whether or not the given stream configuration is valid for building a stream.
|
||||
fn valid_config(conf: &StreamConfig, sample_format: SampleFormat) -> bool {
|
||||
conf.channels <= MAX_CHANNELS
|
||||
&& conf.channels >= MIN_CHANNELS
|
||||
&& conf.sample_rate <= MAX_SAMPLE_RATE
|
||||
&& conf.sample_rate >= MIN_SAMPLE_RATE
|
||||
&& sample_format == SUPPORTED_SAMPLE_FORMAT
|
||||
}
|
||||
|
||||
fn buffer_time_step_secs(buffer_size_frames: usize, sample_rate: SampleRate) -> f64 {
|
||||
buffer_size_frames as f64 / sample_rate.0 as f64
|
||||
}
|
||||
|
||||
#[cfg(target_feature = "atomics")]
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = AudioBuffer)]
|
||||
type ExternalArrayAudioBuffer;
|
||||
|
||||
# [wasm_bindgen(catch, method, structural, js_class = "AudioBuffer", js_name = copyToChannel)]
|
||||
pub fn copy_to_channel(
|
||||
this: &ExternalArrayAudioBuffer,
|
||||
source: &js_sys::Float32Array,
|
||||
channel_number: i32,
|
||||
) -> Result<(), JsValue>;
|
||||
}
|
||||
Vendored
-871
@@ -1,871 +0,0 @@
|
||||
//! # How to use cpal
|
||||
//!
|
||||
//! Here are some concepts cpal exposes:
|
||||
//!
|
||||
//! - A [`Host`] provides access to the available audio devices on the system.
|
||||
//! Some platforms have more than one host available, but every platform supported by CPAL has at
|
||||
//! least one [default_host] that is guaranteed to be available.
|
||||
//! - A [`Device`] is an audio device that may have any number of input and
|
||||
//! output streams.
|
||||
//! - A [`Stream`] is an open flow of audio data. Input streams allow you to
|
||||
//! receive audio data, output streams allow you to play audio data. You must choose which
|
||||
//! [Device] will run your stream before you can create one. Often, a default device can be
|
||||
//! retrieved via the [Host].
|
||||
//!
|
||||
//! The first step is to initialise the [`Host`]:
|
||||
//!
|
||||
//! ```
|
||||
//! use cpal::traits::HostTrait;
|
||||
//! let host = cpal::default_host();
|
||||
//! ```
|
||||
//!
|
||||
//! Then choose an available [`Device`]. The easiest way is to use the default input or output
|
||||
//! `Device` via the [`default_input_device()`] or [`default_output_device()`] methods on `host`.
|
||||
//!
|
||||
//! Alternatively, you can enumerate all the available devices with the [`devices()`] method.
|
||||
//! Beware that the `default_*_device()` functions return an `Option<Device>` in case no device
|
||||
//! is available for that stream type on the system.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use cpal::traits::HostTrait;
|
||||
//! # let host = cpal::default_host();
|
||||
//! let device = host.default_output_device().expect("no output device available");
|
||||
//! ```
|
||||
//!
|
||||
//! Before we can create a stream, we must decide what the configuration of the audio stream is
|
||||
//! going to be.
|
||||
//! You can query all the supported configurations with the
|
||||
//! [`supported_input_configs()`] and [`supported_output_configs()`] methods.
|
||||
//! These produce a list of [`SupportedStreamConfigRange`] structs which can later be turned into
|
||||
//! actual [`SupportedStreamConfig`] structs.
|
||||
//!
|
||||
//! If you don't want to query the list of configs,
|
||||
//! you can also build your own [`StreamConfig`] manually, but doing so could lead to an error when
|
||||
//! building the stream if the config is not supported by the device.
|
||||
//!
|
||||
//! > **Note**: the `supported_input/output_configs()` methods
|
||||
//! > could return an error for example if the device has been disconnected.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use cpal::traits::{DeviceTrait, HostTrait};
|
||||
//! # let host = cpal::default_host();
|
||||
//! # let device = host.default_output_device().unwrap();
|
||||
//! let mut supported_configs_range = device.supported_output_configs()
|
||||
//! .expect("error while querying configs");
|
||||
//! let supported_config = supported_configs_range.next()
|
||||
//! .expect("no supported config?!")
|
||||
//! .with_max_sample_rate();
|
||||
//! ```
|
||||
//!
|
||||
//! Now that we have everything for the stream, we are ready to create it from our selected device:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use cpal::Data;
|
||||
//! use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
//! # let host = cpal::default_host();
|
||||
//! # let device = host.default_output_device().unwrap();
|
||||
//! # let config = device.default_output_config().unwrap().into();
|
||||
//! let stream = device.build_output_stream(
|
||||
//! &config,
|
||||
//! move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||
//! // react to stream events and read or write stream data here.
|
||||
//! },
|
||||
//! move |err| {
|
||||
//! // react to errors here.
|
||||
//! },
|
||||
//! None // None=blocking, Some(Duration)=timeout
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! While the stream is running, the selected audio device will periodically call the data callback
|
||||
//! that was passed to the function. The callback is passed an instance of either [`&Data` or
|
||||
//! `&mut Data`](Data) depending on whether the stream is an input stream or output stream respectively.
|
||||
//!
|
||||
//! > **Note**: Creating and running a stream will *not* block the thread. On modern platforms, the
|
||||
//! > given callback is called by a dedicated, high-priority thread responsible for delivering
|
||||
//! > audio data to the system's audio device in a timely manner. On older platforms that only
|
||||
//! > provide a blocking API (e.g. ALSA), CPAL will create a thread in order to consistently
|
||||
//! > provide non-blocking behaviour (currently this is a thread per stream, but this may change to
|
||||
//! > use a single thread for all streams). *If this is an issue for your platform or design,
|
||||
//! > please share your issue and use-case with the CPAL team on the GitHub issue tracker for
|
||||
//! > consideration.*
|
||||
//!
|
||||
//! In this example, we simply fill the given output buffer with silence.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use cpal::{Data, Sample, SampleFormat, FromSample};
|
||||
//! use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
//! # let host = cpal::default_host();
|
||||
//! # let device = host.default_output_device().unwrap();
|
||||
//! # let supported_config = device.default_output_config().unwrap();
|
||||
//! let err_fn = |err| eprintln!("an error occurred on the output audio stream: {}", err);
|
||||
//! let sample_format = supported_config.sample_format();
|
||||
//! let config = supported_config.into();
|
||||
//! let stream = match sample_format {
|
||||
//! SampleFormat::F32 => device.build_output_stream(&config, write_silence::<f32>, err_fn, None),
|
||||
//! SampleFormat::I16 => device.build_output_stream(&config, write_silence::<i16>, err_fn, None),
|
||||
//! SampleFormat::U16 => device.build_output_stream(&config, write_silence::<u16>, err_fn, None),
|
||||
//! sample_format => panic!("Unsupported sample format '{sample_format}'")
|
||||
//! }.unwrap();
|
||||
//!
|
||||
//! fn write_silence<T: Sample>(data: &mut [T], _: &cpal::OutputCallbackInfo) {
|
||||
//! for sample in data.iter_mut() {
|
||||
//! *sample = Sample::EQUILIBRIUM;
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Not all platforms automatically run the stream upon creation. To ensure the stream has started,
|
||||
//! we can use [`Stream::play`](traits::StreamTrait::play).
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
//! # let host = cpal::default_host();
|
||||
//! # let device = host.default_output_device().unwrap();
|
||||
//! # let supported_config = device.default_output_config().unwrap();
|
||||
//! # let sample_format = supported_config.sample_format();
|
||||
//! # let config = supported_config.into();
|
||||
//! # let data_fn = move |_data: &mut cpal::Data, _: &cpal::OutputCallbackInfo| {};
|
||||
//! # let err_fn = move |_err| {};
|
||||
//! # let stream = device.build_output_stream_raw(&config, sample_format, data_fn, err_fn, None).unwrap();
|
||||
//! stream.play().unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! Some devices support pausing the audio stream. This can be useful for saving energy in moments
|
||||
//! of silence.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! # use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
//! # let host = cpal::default_host();
|
||||
//! # let device = host.default_output_device().unwrap();
|
||||
//! # let supported_config = device.default_output_config().unwrap();
|
||||
//! # let sample_format = supported_config.sample_format();
|
||||
//! # let config = supported_config.into();
|
||||
//! # let data_fn = move |_data: &mut cpal::Data, _: &cpal::OutputCallbackInfo| {};
|
||||
//! # let err_fn = move |_err| {};
|
||||
//! # let stream = device.build_output_stream_raw(&config, sample_format, data_fn, err_fn, None).unwrap();
|
||||
//! stream.pause().unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! [`default_input_device()`]: traits::HostTrait::default_input_device
|
||||
//! [`default_output_device()`]: traits::HostTrait::default_output_device
|
||||
//! [`devices()`]: traits::HostTrait::devices
|
||||
//! [`supported_input_configs()`]: traits::DeviceTrait::supported_input_configs
|
||||
//! [`supported_output_configs()`]: traits::DeviceTrait::supported_output_configs
|
||||
|
||||
#![recursion_limit = "2048"]
|
||||
|
||||
// Extern crate declarations with `#[macro_use]` must unfortunately be at crate root.
|
||||
#[cfg(target_os = "emscripten")]
|
||||
#[macro_use]
|
||||
extern crate wasm_bindgen;
|
||||
#[cfg(target_os = "emscripten")]
|
||||
extern crate js_sys;
|
||||
#[cfg(target_os = "emscripten")]
|
||||
extern crate web_sys;
|
||||
|
||||
pub use error::*;
|
||||
pub use platform::{
|
||||
available_hosts, default_host, host_from_id, Device, Devices, Host, HostId, Stream,
|
||||
SupportedInputConfigs, SupportedOutputConfigs, ALL_HOSTS,
|
||||
};
|
||||
pub use samples_formats::{FromSample, Sample, SampleFormat, SizedSample, I24, I48, U24, U48};
|
||||
use std::convert::TryInto;
|
||||
use std::ops::{Div, Mul};
|
||||
use std::time::Duration;
|
||||
#[cfg(target_os = "emscripten")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
mod error;
|
||||
mod host;
|
||||
pub mod platform;
|
||||
mod samples_formats;
|
||||
pub mod traits;
|
||||
|
||||
/// A host's device iterator yielding only *input* devices.
|
||||
pub type InputDevices<I> = std::iter::Filter<I, fn(&<I as Iterator>::Item) -> bool>;
|
||||
|
||||
/// A host's device iterator yielding only *output* devices.
|
||||
pub type OutputDevices<I> = std::iter::Filter<I, fn(&<I as Iterator>::Item) -> bool>;
|
||||
|
||||
/// Number of channels.
|
||||
pub type ChannelCount = u16;
|
||||
|
||||
/// The number of samples processed per second for a single channel of audio.
|
||||
#[cfg_attr(target_os = "emscripten", wasm_bindgen)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct SampleRate(pub u32);
|
||||
|
||||
impl<T> Mul<T> for SampleRate
|
||||
where
|
||||
u32: Mul<T, Output = u32>,
|
||||
{
|
||||
type Output = Self;
|
||||
fn mul(self, rhs: T) -> Self {
|
||||
SampleRate(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Div<T> for SampleRate
|
||||
where
|
||||
u32: Div<T, Output = u32>,
|
||||
{
|
||||
type Output = Self;
|
||||
fn div(self, rhs: T) -> Self {
|
||||
SampleRate(self.0 / rhs)
|
||||
}
|
||||
}
|
||||
|
||||
/// The desired number of frames for the hardware buffer.
|
||||
pub type FrameCount = u32;
|
||||
|
||||
/// The buffer size used by the device.
|
||||
///
|
||||
/// [`Default`] is used when no specific buffer size is set and uses the default
|
||||
/// behavior of the given host. Note, the default buffer size may be surprisingly
|
||||
/// large, leading to latency issues. If low latency is desired, [`Fixed(FrameCount)`]
|
||||
/// should be used in accordance with the [`SupportedBufferSize`] range produced by
|
||||
/// the [`SupportedStreamConfig`] API.
|
||||
///
|
||||
/// [`Default`]: BufferSize::Default
|
||||
/// [`Fixed(FrameCount)`]: BufferSize::Fixed
|
||||
/// [`SupportedStreamConfig`]: SupportedStreamConfig::buffer_size
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum BufferSize {
|
||||
Default,
|
||||
Fixed(FrameCount),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
impl wasm_bindgen::describe::WasmDescribe for BufferSize {
|
||||
fn describe() {}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
impl wasm_bindgen::convert::IntoWasmAbi for BufferSize {
|
||||
type Abi = Option<u32>;
|
||||
fn into_abi(self) -> Self::Abi {
|
||||
match self {
|
||||
Self::Default => None,
|
||||
Self::Fixed(fc) => Some(fc),
|
||||
}
|
||||
.into_abi()
|
||||
}
|
||||
}
|
||||
|
||||
/// The set of parameters used to describe how to open a stream.
|
||||
///
|
||||
/// The sample format is omitted in favour of using a sample type.
|
||||
#[cfg_attr(target_os = "emscripten", wasm_bindgen)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct StreamConfig {
|
||||
pub channels: ChannelCount,
|
||||
pub sample_rate: SampleRate,
|
||||
pub buffer_size: BufferSize,
|
||||
}
|
||||
|
||||
/// Describes the minimum and maximum supported buffer size for the device
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum SupportedBufferSize {
|
||||
Range {
|
||||
min: FrameCount,
|
||||
max: FrameCount,
|
||||
},
|
||||
/// In the case that the platform provides no way of getting the default
|
||||
/// buffersize before starting a stream.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Describes a range of supported stream configurations, retrieved via the
|
||||
/// [`Device::supported_input/output_configs`](traits::DeviceTrait#required-methods) method.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SupportedStreamConfigRange {
|
||||
pub(crate) channels: ChannelCount,
|
||||
/// Minimum value for the samples rate of the supported formats.
|
||||
pub(crate) min_sample_rate: SampleRate,
|
||||
/// Maximum value for the samples rate of the supported formats.
|
||||
pub(crate) max_sample_rate: SampleRate,
|
||||
/// Buffersize ranges supported by the device
|
||||
pub(crate) buffer_size: SupportedBufferSize,
|
||||
/// Type of data expected by the device.
|
||||
pub(crate) sample_format: SampleFormat,
|
||||
}
|
||||
|
||||
/// Describes a single supported stream configuration, retrieved via either a
|
||||
/// [`SupportedStreamConfigRange`] instance or one of the
|
||||
/// [`Device::default_input/output_config`](traits::DeviceTrait#required-methods) methods.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SupportedStreamConfig {
|
||||
channels: ChannelCount,
|
||||
sample_rate: SampleRate,
|
||||
buffer_size: SupportedBufferSize,
|
||||
sample_format: SampleFormat,
|
||||
}
|
||||
|
||||
/// A buffer of dynamically typed audio data, passed to raw stream callbacks.
|
||||
///
|
||||
/// Raw input stream callbacks receive `&Data`, while raw output stream callbacks expect `&mut
|
||||
/// Data`.
|
||||
#[cfg_attr(target_os = "emscripten", wasm_bindgen)]
|
||||
#[derive(Debug)]
|
||||
pub struct Data {
|
||||
data: *mut (),
|
||||
len: usize,
|
||||
sample_format: SampleFormat,
|
||||
}
|
||||
|
||||
/// A monotonic time instance associated with a stream, retrieved from either:
|
||||
///
|
||||
/// 1. A timestamp provided to the stream's underlying audio data callback or
|
||||
/// 2. The same time source used to generate timestamps for a stream's underlying audio data
|
||||
/// callback.
|
||||
///
|
||||
/// `StreamInstant` represents a duration since some unspecified origin occurring either before
|
||||
/// or equal to the moment the stream from which it was created begins.
|
||||
///
|
||||
/// ## Host `StreamInstant` Sources
|
||||
///
|
||||
/// | Host | Source |
|
||||
/// | ---- | ------ |
|
||||
/// | alsa | `snd_pcm_status_get_htstamp` |
|
||||
/// | coreaudio | `mach_absolute_time` |
|
||||
/// | wasapi | `QueryPerformanceCounter` |
|
||||
/// | asio | `timeGetTime` |
|
||||
/// | emscripten | `AudioContext.getOutputTimestamp` |
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||
pub struct StreamInstant {
|
||||
secs: i64,
|
||||
nanos: u32,
|
||||
}
|
||||
|
||||
/// A timestamp associated with a call to an input stream's data callback.
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct InputStreamTimestamp {
|
||||
/// The instant the stream's data callback was invoked.
|
||||
pub callback: StreamInstant,
|
||||
/// The instant that data was captured from the device.
|
||||
///
|
||||
/// E.g. The instant data was read from an ADC.
|
||||
pub capture: StreamInstant,
|
||||
}
|
||||
|
||||
/// A timestamp associated with a call to an output stream's data callback.
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct OutputStreamTimestamp {
|
||||
/// The instant the stream's data callback was invoked.
|
||||
pub callback: StreamInstant,
|
||||
/// The predicted instant that data written will be delivered to the device for playback.
|
||||
///
|
||||
/// E.g. The instant data will be played by a DAC.
|
||||
pub playback: StreamInstant,
|
||||
}
|
||||
|
||||
/// Information relevant to a single call to the user's input stream data callback.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InputCallbackInfo {
|
||||
timestamp: InputStreamTimestamp,
|
||||
}
|
||||
|
||||
/// Information relevant to a single call to the user's output stream data callback.
|
||||
#[cfg_attr(target_os = "emscripten", wasm_bindgen)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OutputCallbackInfo {
|
||||
timestamp: OutputStreamTimestamp,
|
||||
}
|
||||
|
||||
impl SupportedStreamConfig {
|
||||
pub fn new(
|
||||
channels: ChannelCount,
|
||||
sample_rate: SampleRate,
|
||||
buffer_size: SupportedBufferSize,
|
||||
sample_format: SampleFormat,
|
||||
) -> Self {
|
||||
Self {
|
||||
channels,
|
||||
sample_rate,
|
||||
buffer_size,
|
||||
sample_format,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channels(&self) -> ChannelCount {
|
||||
self.channels
|
||||
}
|
||||
|
||||
pub fn sample_rate(&self) -> SampleRate {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
pub fn buffer_size(&self) -> &SupportedBufferSize {
|
||||
&self.buffer_size
|
||||
}
|
||||
|
||||
pub fn sample_format(&self) -> SampleFormat {
|
||||
self.sample_format
|
||||
}
|
||||
|
||||
pub fn config(&self) -> StreamConfig {
|
||||
StreamConfig {
|
||||
channels: self.channels,
|
||||
sample_rate: self.sample_rate,
|
||||
buffer_size: BufferSize::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StreamInstant {
|
||||
/// The amount of time elapsed from another instant to this one.
|
||||
///
|
||||
/// Returns `None` if `earlier` is later than self.
|
||||
pub fn duration_since(&self, earlier: &Self) -> Option<Duration> {
|
||||
if self < earlier {
|
||||
None
|
||||
} else {
|
||||
(self.as_nanos() - earlier.as_nanos())
|
||||
.try_into()
|
||||
.ok()
|
||||
.map(Duration::from_nanos)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the instant in time after the given duration has passed.
|
||||
///
|
||||
/// Returns `None` if the resulting instant would exceed the bounds of the underlying data
|
||||
/// structure.
|
||||
pub fn add(&self, duration: Duration) -> Option<Self> {
|
||||
self.as_nanos()
|
||||
.checked_add(duration.as_nanos() as i128)
|
||||
.and_then(Self::from_nanos_i128)
|
||||
}
|
||||
|
||||
/// Returns the instant in time one `duration` ago.
|
||||
///
|
||||
/// Returns `None` if the resulting instant would underflow. As a result, it is important to
|
||||
/// consider that on some platforms the [`StreamInstant`] may begin at `0` from the moment the
|
||||
/// source stream is created.
|
||||
pub fn sub(&self, duration: Duration) -> Option<Self> {
|
||||
self.as_nanos()
|
||||
.checked_sub(duration.as_nanos() as i128)
|
||||
.and_then(Self::from_nanos_i128)
|
||||
}
|
||||
|
||||
fn as_nanos(&self) -> i128 {
|
||||
(self.secs as i128 * 1_000_000_000) + self.nanos as i128
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn from_nanos(nanos: i64) -> Self {
|
||||
let secs = nanos / 1_000_000_000;
|
||||
let subsec_nanos = nanos - secs * 1_000_000_000;
|
||||
Self::new(secs, subsec_nanos as u32)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn from_nanos_i128(nanos: i128) -> Option<Self> {
|
||||
let secs = nanos / 1_000_000_000;
|
||||
if secs > i64::MAX as i128 || secs < i64::MIN as i128 {
|
||||
None
|
||||
} else {
|
||||
let subsec_nanos = nanos - secs * 1_000_000_000;
|
||||
debug_assert!(subsec_nanos < u32::MAX as i128);
|
||||
Some(Self::new(secs as i64, subsec_nanos as u32))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn from_secs_f64(secs: f64) -> crate::StreamInstant {
|
||||
let s = secs.floor() as i64;
|
||||
let ns = ((secs - s as f64) * 1_000_000_000.0) as u32;
|
||||
Self::new(s, ns)
|
||||
}
|
||||
|
||||
fn new(secs: i64, nanos: u32) -> Self {
|
||||
StreamInstant { secs, nanos }
|
||||
}
|
||||
}
|
||||
|
||||
impl InputCallbackInfo {
|
||||
/// The timestamp associated with the call to an input stream's data callback.
|
||||
pub fn timestamp(&self) -> InputStreamTimestamp {
|
||||
self.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputCallbackInfo {
|
||||
/// The timestamp associated with the call to an output stream's data callback.
|
||||
pub fn timestamp(&self) -> OutputStreamTimestamp {
|
||||
self.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
impl Data {
|
||||
// Internal constructor for host implementations to use.
|
||||
//
|
||||
// The following requirements must be met in order for the safety of `Data`'s public API.
|
||||
//
|
||||
// - The `data` pointer must point to the first sample in the slice containing all samples.
|
||||
// - The `len` must describe the length of the buffer as a number of samples in the expected
|
||||
// format specified via the `sample_format` argument.
|
||||
// - The `sample_format` must correctly represent the underlying sample data delivered/expected
|
||||
// by the stream.
|
||||
pub(crate) unsafe fn from_parts(
|
||||
data: *mut (),
|
||||
len: usize,
|
||||
sample_format: SampleFormat,
|
||||
) -> Self {
|
||||
Data {
|
||||
data,
|
||||
len,
|
||||
sample_format,
|
||||
}
|
||||
}
|
||||
|
||||
/// The sample format of the internal audio data.
|
||||
pub fn sample_format(&self) -> SampleFormat {
|
||||
self.sample_format
|
||||
}
|
||||
|
||||
/// The full length of the buffer in samples.
|
||||
///
|
||||
/// The returned length is the same length as the slice of type `T` that would be returned via
|
||||
/// [`as_slice`](Self::as_slice) given a sample type that matches the inner sample format.
|
||||
pub fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
/// The raw slice of memory representing the underlying audio data as a slice of bytes.
|
||||
///
|
||||
/// It is up to the user to interpret the slice of memory based on [`Data::sample_format`].
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
let len = self.len * self.sample_format.sample_size();
|
||||
// The safety of this block relies on correct construction of the `Data` instance.
|
||||
// See the unsafe `from_parts` constructor for these requirements.
|
||||
unsafe { std::slice::from_raw_parts(self.data as *const u8, len) }
|
||||
}
|
||||
|
||||
/// The raw slice of memory representing the underlying audio data as a slice of bytes.
|
||||
///
|
||||
/// It is up to the user to interpret the slice of memory based on [`Data::sample_format`].
|
||||
pub fn bytes_mut(&mut self) -> &mut [u8] {
|
||||
let len = self.len * self.sample_format.sample_size();
|
||||
// The safety of this block relies on correct construction of the `Data` instance. See
|
||||
// the unsafe `from_parts` constructor for these requirements.
|
||||
unsafe { std::slice::from_raw_parts_mut(self.data as *mut u8, len) }
|
||||
}
|
||||
|
||||
/// Access the data as a slice of sample type `T`.
|
||||
///
|
||||
/// Returns `None` if the sample type does not match the expected sample format.
|
||||
pub fn as_slice<T>(&self) -> Option<&[T]>
|
||||
where
|
||||
T: SizedSample,
|
||||
{
|
||||
if T::FORMAT == self.sample_format {
|
||||
// The safety of this block relies on correct construction of the `Data` instance. See
|
||||
// the unsafe `from_parts` constructor for these requirements.
|
||||
unsafe { Some(std::slice::from_raw_parts(self.data as *const T, self.len)) }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Access the data as a slice of sample type `T`.
|
||||
///
|
||||
/// Returns `None` if the sample type does not match the expected sample format.
|
||||
pub fn as_slice_mut<T>(&mut self) -> Option<&mut [T]>
|
||||
where
|
||||
T: SizedSample,
|
||||
{
|
||||
if T::FORMAT == self.sample_format {
|
||||
// The safety of this block relies on correct construction of the `Data` instance. See
|
||||
// the unsafe `from_parts` constructor for these requirements.
|
||||
unsafe {
|
||||
Some(std::slice::from_raw_parts_mut(
|
||||
self.data as *mut T,
|
||||
self.len,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SupportedStreamConfigRange {
|
||||
pub fn new(
|
||||
channels: ChannelCount,
|
||||
min_sample_rate: SampleRate,
|
||||
max_sample_rate: SampleRate,
|
||||
buffer_size: SupportedBufferSize,
|
||||
sample_format: SampleFormat,
|
||||
) -> Self {
|
||||
Self {
|
||||
channels,
|
||||
min_sample_rate,
|
||||
max_sample_rate,
|
||||
buffer_size,
|
||||
sample_format,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channels(&self) -> ChannelCount {
|
||||
self.channels
|
||||
}
|
||||
|
||||
pub fn min_sample_rate(&self) -> SampleRate {
|
||||
self.min_sample_rate
|
||||
}
|
||||
|
||||
pub fn max_sample_rate(&self) -> SampleRate {
|
||||
self.max_sample_rate
|
||||
}
|
||||
|
||||
pub fn buffer_size(&self) -> &SupportedBufferSize {
|
||||
&self.buffer_size
|
||||
}
|
||||
|
||||
pub fn sample_format(&self) -> SampleFormat {
|
||||
self.sample_format
|
||||
}
|
||||
|
||||
/// Retrieve a [`SupportedStreamConfig`] with the given sample rate and buffer size.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the given `sample_rate` is outside the range specified within
|
||||
/// this [`SupportedStreamConfigRange`] instance. For a non-panicking
|
||||
/// variant, use [`try_with_sample_rate`](#method.try_with_sample_rate).
|
||||
pub fn with_sample_rate(self, sample_rate: SampleRate) -> SupportedStreamConfig {
|
||||
self.try_with_sample_rate(sample_rate)
|
||||
.expect("sample rate out of range")
|
||||
}
|
||||
|
||||
/// Retrieve a [`SupportedStreamConfig`] with the given sample rate and buffer size.
|
||||
///
|
||||
/// Returns `None` if the given sample rate is outside the range specified
|
||||
/// within this [`SupportedStreamConfigRange`] instance.
|
||||
pub fn try_with_sample_rate(self, sample_rate: SampleRate) -> Option<SupportedStreamConfig> {
|
||||
if self.min_sample_rate <= sample_rate && sample_rate <= self.max_sample_rate {
|
||||
Some(SupportedStreamConfig {
|
||||
channels: self.channels,
|
||||
sample_rate,
|
||||
sample_format: self.sample_format,
|
||||
buffer_size: self.buffer_size,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Turns this [`SupportedStreamConfigRange`] into a [`SupportedStreamConfig`] corresponding to the maximum samples rate.
|
||||
#[inline]
|
||||
pub fn with_max_sample_rate(self) -> SupportedStreamConfig {
|
||||
SupportedStreamConfig {
|
||||
channels: self.channels,
|
||||
sample_rate: self.max_sample_rate,
|
||||
sample_format: self.sample_format,
|
||||
buffer_size: self.buffer_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// A comparison function which compares two [`SupportedStreamConfigRange`]s in terms of their priority of
|
||||
/// use as a default stream format.
|
||||
///
|
||||
/// Some backends do not provide a default stream format for their audio devices. In these
|
||||
/// cases, CPAL attempts to decide on a reasonable default format for the user. To do this we
|
||||
/// use the "greatest" of all supported stream formats when compared with this method.
|
||||
///
|
||||
/// SupportedStreamConfigs are prioritised by the following heuristics:
|
||||
///
|
||||
/// **Channels**:
|
||||
///
|
||||
/// - Stereo
|
||||
/// - Mono
|
||||
/// - Max available channels
|
||||
///
|
||||
/// **Sample format**:
|
||||
/// - f32
|
||||
/// - i16
|
||||
/// - u16
|
||||
///
|
||||
/// **Sample rate**:
|
||||
///
|
||||
/// - 44100 (cd quality)
|
||||
/// - Max sample rate
|
||||
pub fn cmp_default_heuristics(&self, other: &Self) -> std::cmp::Ordering {
|
||||
use std::cmp::Ordering::Equal;
|
||||
use SampleFormat::{F32, I16, U16};
|
||||
|
||||
let cmp_stereo = (self.channels == 2).cmp(&(other.channels == 2));
|
||||
if cmp_stereo != Equal {
|
||||
return cmp_stereo;
|
||||
}
|
||||
|
||||
let cmp_mono = (self.channels == 1).cmp(&(other.channels == 1));
|
||||
if cmp_mono != Equal {
|
||||
return cmp_mono;
|
||||
}
|
||||
|
||||
let cmp_channels = self.channels.cmp(&other.channels);
|
||||
if cmp_channels != Equal {
|
||||
return cmp_channels;
|
||||
}
|
||||
|
||||
let cmp_f32 = (self.sample_format == F32).cmp(&(other.sample_format == F32));
|
||||
if cmp_f32 != Equal {
|
||||
return cmp_f32;
|
||||
}
|
||||
|
||||
let cmp_i16 = (self.sample_format == I16).cmp(&(other.sample_format == I16));
|
||||
if cmp_i16 != Equal {
|
||||
return cmp_i16;
|
||||
}
|
||||
|
||||
let cmp_u16 = (self.sample_format == U16).cmp(&(other.sample_format == U16));
|
||||
if cmp_u16 != Equal {
|
||||
return cmp_u16;
|
||||
}
|
||||
|
||||
const HZ_44100: SampleRate = SampleRate(44_100);
|
||||
let r44100_in_self = self.min_sample_rate <= HZ_44100 && HZ_44100 <= self.max_sample_rate;
|
||||
let r44100_in_other =
|
||||
other.min_sample_rate <= HZ_44100 && HZ_44100 <= other.max_sample_rate;
|
||||
let cmp_r44100 = r44100_in_self.cmp(&r44100_in_other);
|
||||
if cmp_r44100 != Equal {
|
||||
return cmp_r44100;
|
||||
}
|
||||
|
||||
self.max_sample_rate.cmp(&other.max_sample_rate)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cmp_default_heuristics() {
|
||||
let mut formats = [
|
||||
SupportedStreamConfigRange {
|
||||
buffer_size: SupportedBufferSize::Range { min: 256, max: 512 },
|
||||
channels: 2,
|
||||
min_sample_rate: SampleRate(1),
|
||||
max_sample_rate: SampleRate(96000),
|
||||
sample_format: SampleFormat::F32,
|
||||
},
|
||||
SupportedStreamConfigRange {
|
||||
buffer_size: SupportedBufferSize::Range { min: 256, max: 512 },
|
||||
channels: 1,
|
||||
min_sample_rate: SampleRate(1),
|
||||
max_sample_rate: SampleRate(96000),
|
||||
sample_format: SampleFormat::F32,
|
||||
},
|
||||
SupportedStreamConfigRange {
|
||||
buffer_size: SupportedBufferSize::Range { min: 256, max: 512 },
|
||||
channels: 2,
|
||||
min_sample_rate: SampleRate(1),
|
||||
max_sample_rate: SampleRate(96000),
|
||||
sample_format: SampleFormat::I16,
|
||||
},
|
||||
SupportedStreamConfigRange {
|
||||
buffer_size: SupportedBufferSize::Range { min: 256, max: 512 },
|
||||
channels: 2,
|
||||
min_sample_rate: SampleRate(1),
|
||||
max_sample_rate: SampleRate(96000),
|
||||
sample_format: SampleFormat::U16,
|
||||
},
|
||||
SupportedStreamConfigRange {
|
||||
buffer_size: SupportedBufferSize::Range { min: 256, max: 512 },
|
||||
channels: 2,
|
||||
min_sample_rate: SampleRate(1),
|
||||
max_sample_rate: SampleRate(22050),
|
||||
sample_format: SampleFormat::F32,
|
||||
},
|
||||
];
|
||||
|
||||
formats.sort_by(|a, b| a.cmp_default_heuristics(b));
|
||||
|
||||
// lowest-priority first:
|
||||
assert_eq!(formats[0].sample_format(), SampleFormat::F32);
|
||||
assert_eq!(formats[0].min_sample_rate(), SampleRate(1));
|
||||
assert_eq!(formats[0].max_sample_rate(), SampleRate(96000));
|
||||
assert_eq!(formats[0].channels(), 1);
|
||||
|
||||
assert_eq!(formats[1].sample_format(), SampleFormat::U16);
|
||||
assert_eq!(formats[1].min_sample_rate(), SampleRate(1));
|
||||
assert_eq!(formats[1].max_sample_rate(), SampleRate(96000));
|
||||
assert_eq!(formats[1].channels(), 2);
|
||||
|
||||
assert_eq!(formats[2].sample_format(), SampleFormat::I16);
|
||||
assert_eq!(formats[2].min_sample_rate(), SampleRate(1));
|
||||
assert_eq!(formats[2].max_sample_rate(), SampleRate(96000));
|
||||
assert_eq!(formats[2].channels(), 2);
|
||||
|
||||
assert_eq!(formats[3].sample_format(), SampleFormat::F32);
|
||||
assert_eq!(formats[3].min_sample_rate(), SampleRate(1));
|
||||
assert_eq!(formats[3].max_sample_rate(), SampleRate(22050));
|
||||
assert_eq!(formats[3].channels(), 2);
|
||||
|
||||
assert_eq!(formats[4].sample_format(), SampleFormat::F32);
|
||||
assert_eq!(formats[4].min_sample_rate(), SampleRate(1));
|
||||
assert_eq!(formats[4].max_sample_rate(), SampleRate(96000));
|
||||
assert_eq!(formats[4].channels(), 2);
|
||||
}
|
||||
|
||||
impl From<SupportedStreamConfig> for StreamConfig {
|
||||
fn from(conf: SupportedStreamConfig) -> Self {
|
||||
conf.config()
|
||||
}
|
||||
}
|
||||
|
||||
// If a backend does not provide an API for retrieving supported formats, we query it with a bunch
|
||||
// of commonly used rates. This is always the case for wasapi and is sometimes the case for alsa.
|
||||
//
|
||||
// If a rate you desire is missing from this list, feel free to add it!
|
||||
#[cfg(target_os = "windows")]
|
||||
const COMMON_SAMPLE_RATES: &[SampleRate] = &[
|
||||
SampleRate(5512),
|
||||
SampleRate(8000),
|
||||
SampleRate(11025),
|
||||
SampleRate(16000),
|
||||
SampleRate(22050),
|
||||
SampleRate(32000),
|
||||
SampleRate(44100),
|
||||
SampleRate(48000),
|
||||
SampleRate(64000),
|
||||
SampleRate(88200),
|
||||
SampleRate(96000),
|
||||
SampleRate(176400),
|
||||
SampleRate(192000),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_stream_instant() {
|
||||
let a = StreamInstant::new(2, 0);
|
||||
let b = StreamInstant::new(-2, 0);
|
||||
let min = StreamInstant::new(i64::MIN, 0);
|
||||
let max = StreamInstant::new(i64::MAX, 0);
|
||||
assert_eq!(
|
||||
a.sub(Duration::from_secs(1)),
|
||||
Some(StreamInstant::new(1, 0))
|
||||
);
|
||||
assert_eq!(
|
||||
a.sub(Duration::from_secs(2)),
|
||||
Some(StreamInstant::new(0, 0))
|
||||
);
|
||||
assert_eq!(
|
||||
a.sub(Duration::from_secs(3)),
|
||||
Some(StreamInstant::new(-1, 0))
|
||||
);
|
||||
assert_eq!(min.sub(Duration::from_secs(1)), None);
|
||||
assert_eq!(
|
||||
b.add(Duration::from_secs(1)),
|
||||
Some(StreamInstant::new(-1, 0))
|
||||
);
|
||||
assert_eq!(
|
||||
b.add(Duration::from_secs(2)),
|
||||
Some(StreamInstant::new(0, 0))
|
||||
);
|
||||
assert_eq!(
|
||||
b.add(Duration::from_secs(3)),
|
||||
Some(StreamInstant::new(1, 0))
|
||||
);
|
||||
assert_eq!(max.add(Duration::from_secs(1)), None);
|
||||
}
|
||||
-742
@@ -1,742 +0,0 @@
|
||||
//! Platform-specific items.
|
||||
//!
|
||||
//! This module also contains the implementation of the platform's dynamically dispatched [`Host`]
|
||||
//! type and its associated [`Device`], [`Stream`] and other associated types. These
|
||||
//! types are useful in the case that users require switching between audio host APIs at runtime.
|
||||
|
||||
#[doc(inline)]
|
||||
pub use self::platform_impl::*;
|
||||
|
||||
/// A macro to assist with implementing a platform's dynamically dispatched [`Host`] type.
|
||||
///
|
||||
/// These dynamically dispatched types are necessary to allow for users to switch between hosts at
|
||||
/// runtime.
|
||||
///
|
||||
/// For example the invocation `impl_platform_host(Wasapi wasapi "WASAPI", Asio asio "ASIO")`,
|
||||
/// this macro should expand to:
|
||||
///
|
||||
// This sample code block is marked as text because it's not a valid test,
|
||||
// it's just illustrative. (see rust issue #96573)
|
||||
/// ```text
|
||||
/// pub enum HostId {
|
||||
/// Wasapi,
|
||||
/// Asio,
|
||||
/// }
|
||||
///
|
||||
/// pub enum Host {
|
||||
/// Wasapi(crate::host::wasapi::Host),
|
||||
/// Asio(crate::host::asio::Host),
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// And so on for Device, Devices, Host, Stream, SupportedInputConfigs,
|
||||
/// SupportedOutputConfigs and all their necessary trait implementations.
|
||||
///
|
||||
macro_rules! impl_platform_host {
|
||||
($($(#[cfg($feat: meta)])? $HostVariant:ident $host_mod:ident $host_name:literal),*) => {
|
||||
/// All hosts supported by CPAL on this platform.
|
||||
pub const ALL_HOSTS: &'static [HostId] = &[
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
HostId::$HostVariant,
|
||||
)*
|
||||
];
|
||||
|
||||
/// The platform's dynamically dispatched `Host` type.
|
||||
///
|
||||
/// An instance of this `Host` type may represent one of the `Host`s available
|
||||
/// on the platform.
|
||||
///
|
||||
/// Use this type if you require switching between available hosts at runtime.
|
||||
///
|
||||
/// This type may be constructed via the [`host_from_id`] function. [`HostId`]s may
|
||||
/// be acquired via the [`ALL_HOSTS`] const, and the [`available_hosts`] function.
|
||||
pub struct Host(HostInner);
|
||||
|
||||
/// The `Device` implementation associated with the platform's dynamically dispatched
|
||||
/// [`Host`] type.
|
||||
#[derive(Clone)]
|
||||
pub struct Device(DeviceInner);
|
||||
|
||||
/// The `Devices` iterator associated with the platform's dynamically dispatched [`Host`]
|
||||
/// type.
|
||||
pub struct Devices(DevicesInner);
|
||||
|
||||
/// The `Stream` implementation associated with the platform's dynamically dispatched
|
||||
/// [`Host`] type.
|
||||
// Streams cannot be `Send` or `Sync` if we plan to support Android's AAudio API. This is
|
||||
// because the stream API is not thread-safe, and the API prohibits calling certain
|
||||
// functions within the callback.
|
||||
//
|
||||
// TODO: Confirm this and add more specific detail and references.
|
||||
#[must_use = "If the stream is not stored it will not play."]
|
||||
pub struct Stream(StreamInner, crate::platform::NotSendSyncAcrossAllPlatforms);
|
||||
|
||||
/// The `SupportedInputConfigs` iterator associated with the platform's dynamically
|
||||
/// dispatched [`Host`] type.
|
||||
pub struct SupportedInputConfigs(SupportedInputConfigsInner);
|
||||
|
||||
/// The `SupportedOutputConfigs` iterator associated with the platform's dynamically
|
||||
/// dispatched [`Host`] type.
|
||||
pub struct SupportedOutputConfigs(SupportedOutputConfigsInner);
|
||||
|
||||
/// Unique identifier for available hosts on the platform.
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum HostId {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
$HostVariant,
|
||||
)*
|
||||
}
|
||||
|
||||
/// Contains a platform specific [`Device`] implementation.
|
||||
#[derive(Clone)]
|
||||
pub enum DeviceInner {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
$HostVariant(crate::host::$host_mod::Device),
|
||||
)*
|
||||
}
|
||||
|
||||
/// Contains a platform specific [`Devices`] implementation.
|
||||
pub enum DevicesInner {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
$HostVariant(crate::host::$host_mod::Devices),
|
||||
)*
|
||||
}
|
||||
|
||||
/// Contains a platform specific [`Host`] implementation.
|
||||
pub enum HostInner {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
$HostVariant(crate::host::$host_mod::Host),
|
||||
)*
|
||||
}
|
||||
|
||||
/// Contains a platform specific [`Stream`] implementation.
|
||||
pub enum StreamInner {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
$HostVariant(crate::host::$host_mod::Stream),
|
||||
)*
|
||||
}
|
||||
|
||||
enum SupportedInputConfigsInner {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
$HostVariant(crate::host::$host_mod::SupportedInputConfigs),
|
||||
)*
|
||||
}
|
||||
|
||||
enum SupportedOutputConfigsInner {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
$HostVariant(crate::host::$host_mod::SupportedOutputConfigs),
|
||||
)*
|
||||
}
|
||||
|
||||
impl HostId {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
HostId::$HostVariant => $host_name,
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Devices {
|
||||
/// Returns a reference to the underlying platform specific implementation of this
|
||||
/// `Devices`.
|
||||
pub fn as_inner(&self) -> &DevicesInner {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the underlying platform specific implementation of
|
||||
/// this `Devices`.
|
||||
pub fn as_inner_mut(&mut self) -> &mut DevicesInner {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
/// Returns the underlying platform specific implementation of this `Devices`.
|
||||
pub fn into_inner(self) -> DevicesInner {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Device {
|
||||
/// Returns a reference to the underlying platform specific implementation of this
|
||||
/// `Device`.
|
||||
pub fn as_inner(&self) -> &DeviceInner {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the underlying platform specific implementation of
|
||||
/// this `Device`.
|
||||
pub fn as_inner_mut(&mut self) -> &mut DeviceInner {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
/// Returns the underlying platform specific implementation of this `Device`.
|
||||
pub fn into_inner(self) -> DeviceInner {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Host {
|
||||
/// The unique identifier associated with this `Host`.
|
||||
pub fn id(&self) -> HostId {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
HostInner::$HostVariant(_) => HostId::$HostVariant,
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying platform specific implementation of this
|
||||
/// `Host`.
|
||||
pub fn as_inner(&self) -> &HostInner {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the underlying platform specific implementation of
|
||||
/// this `Host`.
|
||||
pub fn as_inner_mut(&mut self) -> &mut HostInner {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
/// Returns the underlying platform specific implementation of this `Host`.
|
||||
pub fn into_inner(self) -> HostInner {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
/// Returns a reference to the underlying platform specific implementation of this
|
||||
/// `Stream`.
|
||||
pub fn as_inner(&self) -> &StreamInner {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the underlying platform specific implementation of
|
||||
/// this `Stream`.
|
||||
pub fn as_inner_mut(&mut self) -> &mut StreamInner {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
/// Returns the underlying platform specific implementation of this `Stream`.
|
||||
pub fn into_inner(self) -> StreamInner {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Devices {
|
||||
type Item = Device;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
DevicesInner::$HostVariant(ref mut d) => {
|
||||
d.next().map(DeviceInner::$HostVariant).map(Device::from)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
DevicesInner::$HostVariant(ref d) => d.size_hint(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SupportedInputConfigs {
|
||||
type Item = crate::SupportedStreamConfigRange;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
SupportedInputConfigsInner::$HostVariant(ref mut s) => s.next(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
SupportedInputConfigsInner::$HostVariant(ref d) => d.size_hint(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SupportedOutputConfigs {
|
||||
type Item = crate::SupportedStreamConfigRange;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
SupportedOutputConfigsInner::$HostVariant(ref mut s) => s.next(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
SupportedOutputConfigsInner::$HostVariant(ref d) => d.size_hint(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::traits::DeviceTrait for Device {
|
||||
type SupportedInputConfigs = SupportedInputConfigs;
|
||||
type SupportedOutputConfigs = SupportedOutputConfigs;
|
||||
type Stream = Stream;
|
||||
|
||||
fn name(&self) -> Result<String, crate::DeviceNameError> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
DeviceInner::$HostVariant(ref d) => d.name(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_input_configs(&self) -> Result<Self::SupportedInputConfigs, crate::SupportedStreamConfigsError> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
DeviceInner::$HostVariant(ref d) => {
|
||||
d.supported_input_configs()
|
||||
.map(SupportedInputConfigsInner::$HostVariant)
|
||||
.map(SupportedInputConfigs)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn supported_output_configs(&self) -> Result<Self::SupportedOutputConfigs, crate::SupportedStreamConfigsError> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
DeviceInner::$HostVariant(ref d) => {
|
||||
d.supported_output_configs()
|
||||
.map(SupportedOutputConfigsInner::$HostVariant)
|
||||
.map(SupportedOutputConfigs)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn default_input_config(&self) -> Result<crate::SupportedStreamConfig, crate::DefaultStreamConfigError> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
DeviceInner::$HostVariant(ref d) => d.default_input_config(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_config(&self) -> Result<crate::SupportedStreamConfig, crate::DefaultStreamConfigError> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
DeviceInner::$HostVariant(ref d) => d.default_output_config(),
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &crate::StreamConfig,
|
||||
sample_format: crate::SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
timeout: Option<std::time::Duration>,
|
||||
) -> Result<Self::Stream, crate::BuildStreamError>
|
||||
where
|
||||
D: FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(crate::StreamError) + Send + 'static,
|
||||
{
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
DeviceInner::$HostVariant(ref d) => d
|
||||
.build_input_stream_raw(
|
||||
config,
|
||||
sample_format,
|
||||
data_callback,
|
||||
error_callback,
|
||||
timeout,
|
||||
)
|
||||
.map(StreamInner::$HostVariant)
|
||||
.map(Stream::from),
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &crate::StreamConfig,
|
||||
sample_format: crate::SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
timeout: Option<std::time::Duration>,
|
||||
) -> Result<Self::Stream, crate::BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(crate::StreamError) + Send + 'static,
|
||||
{
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
DeviceInner::$HostVariant(ref d) => d
|
||||
.build_output_stream_raw(
|
||||
config,
|
||||
sample_format,
|
||||
data_callback,
|
||||
error_callback,
|
||||
timeout,
|
||||
)
|
||||
.map(StreamInner::$HostVariant)
|
||||
.map(Stream::from),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::traits::HostTrait for Host {
|
||||
type Devices = Devices;
|
||||
type Device = Device;
|
||||
|
||||
fn is_available() -> bool {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
if crate::host::$host_mod::Host::is_available() { return true; }
|
||||
)*
|
||||
false
|
||||
}
|
||||
|
||||
fn devices(&self) -> Result<Self::Devices, crate::DevicesError> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
HostInner::$HostVariant(ref h) => {
|
||||
h.devices().map(DevicesInner::$HostVariant).map(Devices::from)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn default_input_device(&self) -> Option<Self::Device> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
HostInner::$HostVariant(ref h) => {
|
||||
h.default_input_device().map(DeviceInner::$HostVariant).map(Device::from)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn default_output_device(&self) -> Option<Self::Device> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
HostInner::$HostVariant(ref h) => {
|
||||
h.default_output_device().map(DeviceInner::$HostVariant).map(Device::from)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::traits::StreamTrait for Stream {
|
||||
fn play(&self) -> Result<(), crate::PlayStreamError> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
StreamInner::$HostVariant(ref s) => {
|
||||
s.play()
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
fn pause(&self) -> Result<(), crate::PauseStreamError> {
|
||||
match self.0 {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
StreamInner::$HostVariant(ref s) => {
|
||||
s.pause()
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DeviceInner> for Device {
|
||||
fn from(d: DeviceInner) -> Self {
|
||||
Device(d)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DevicesInner> for Devices {
|
||||
fn from(d: DevicesInner) -> Self {
|
||||
Devices(d)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HostInner> for Host {
|
||||
fn from(h: HostInner) -> Self {
|
||||
Host(h)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StreamInner> for Stream {
|
||||
fn from(s: StreamInner) -> Self {
|
||||
Stream(s, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
impl From<crate::host::$host_mod::Device> for Device {
|
||||
fn from(h: crate::host::$host_mod::Device) -> Self {
|
||||
DeviceInner::$HostVariant(h).into()
|
||||
}
|
||||
}
|
||||
|
||||
$(#[cfg($feat)])?
|
||||
impl From<crate::host::$host_mod::Devices> for Devices {
|
||||
fn from(h: crate::host::$host_mod::Devices) -> Self {
|
||||
DevicesInner::$HostVariant(h).into()
|
||||
}
|
||||
}
|
||||
|
||||
$(#[cfg($feat)])?
|
||||
impl From<crate::host::$host_mod::Host> for Host {
|
||||
fn from(h: crate::host::$host_mod::Host) -> Self {
|
||||
HostInner::$HostVariant(h).into()
|
||||
}
|
||||
}
|
||||
|
||||
$(#[cfg($feat)])?
|
||||
impl From<crate::host::$host_mod::Stream> for Stream {
|
||||
fn from(h: crate::host::$host_mod::Stream) -> Self {
|
||||
StreamInner::$HostVariant(h).into()
|
||||
}
|
||||
}
|
||||
)*
|
||||
|
||||
/// Produces a list of hosts that are currently available on the system.
|
||||
pub fn available_hosts() -> Vec<HostId> {
|
||||
let mut host_ids = vec![];
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
if <crate::host::$host_mod::Host as crate::traits::HostTrait>::is_available() {
|
||||
host_ids.push(HostId::$HostVariant);
|
||||
}
|
||||
)*
|
||||
host_ids
|
||||
}
|
||||
|
||||
/// Given a unique host identifier, initialise and produce the host if it is available.
|
||||
pub fn host_from_id(id: HostId) -> Result<Host, crate::HostUnavailable> {
|
||||
match id {
|
||||
$(
|
||||
$(#[cfg($feat)])?
|
||||
HostId::$HostVariant => {
|
||||
crate::host::$host_mod::Host::new()
|
||||
.map(HostInner::$HostVariant)
|
||||
.map(Host::from)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Add pulseaudio and jack here eventually.
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd"
|
||||
))]
|
||||
mod platform_impl {
|
||||
pub use crate::host::alsa::{
|
||||
Device as AlsaDevice, Devices as AlsaDevices, Host as AlsaHost, Stream as AlsaStream,
|
||||
SupportedInputConfigs as AlsaSupportedInputConfigs,
|
||||
SupportedOutputConfigs as AlsaSupportedOutputConfigs,
|
||||
};
|
||||
#[cfg(feature = "jack")]
|
||||
pub use crate::host::jack::{
|
||||
Device as JackDevice, Devices as JackDevices, Host as JackHost, Stream as JackStream,
|
||||
SupportedInputConfigs as JackSupportedInputConfigs,
|
||||
SupportedOutputConfigs as JackSupportedOutputConfigs,
|
||||
};
|
||||
|
||||
impl_platform_host!(#[cfg(feature = "jack")] Jack jack "JACK", Alsa alsa "ALSA");
|
||||
|
||||
/// The default host for the current compilation target platform.
|
||||
pub fn default_host() -> Host {
|
||||
AlsaHost::new()
|
||||
.expect("the default host should always be available")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
mod platform_impl {
|
||||
pub use crate::host::coreaudio::{
|
||||
Device as CoreAudioDevice, Devices as CoreAudioDevices, Host as CoreAudioHost,
|
||||
Stream as CoreAudioStream, SupportedInputConfigs as CoreAudioSupportedInputConfigs,
|
||||
SupportedOutputConfigs as CoreAudioSupportedOutputConfigs,
|
||||
};
|
||||
|
||||
impl_platform_host!(CoreAudio coreaudio "CoreAudio");
|
||||
|
||||
/// The default host for the current compilation target platform.
|
||||
pub fn default_host() -> Host {
|
||||
CoreAudioHost::new()
|
||||
.expect("the default host should always be available")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "emscripten")]
|
||||
mod platform_impl {
|
||||
pub use crate::host::emscripten::{
|
||||
Device as EmscriptenDevice, Devices as EmscriptenDevices, Host as EmscriptenHost,
|
||||
Stream as EmscriptenStream, SupportedInputConfigs as EmscriptenSupportedInputConfigs,
|
||||
SupportedOutputConfigs as EmscriptenSupportedOutputConfigs,
|
||||
};
|
||||
|
||||
impl_platform_host!(Emscripten emscripten "Emscripten");
|
||||
|
||||
/// The default host for the current compilation target platform.
|
||||
pub fn default_host() -> Host {
|
||||
EmscriptenHost::new()
|
||||
.expect("the default host should always be available")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
|
||||
mod platform_impl {
|
||||
pub use crate::host::webaudio::{
|
||||
Device as WebAudioDevice, Devices as WebAudioDevices, Host as WebAudioHost,
|
||||
Stream as WebAudioStream, SupportedInputConfigs as WebAudioSupportedInputConfigs,
|
||||
SupportedOutputConfigs as WebAudioSupportedOutputConfigs,
|
||||
};
|
||||
|
||||
impl_platform_host!(WebAudio webaudio "WebAudio");
|
||||
|
||||
/// The default host for the current compilation target platform.
|
||||
pub fn default_host() -> Host {
|
||||
WebAudioHost::new()
|
||||
.expect("the default host should always be available")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod platform_impl {
|
||||
#[cfg(feature = "asio")]
|
||||
pub use crate::host::asio::{
|
||||
Device as AsioDevice, Devices as AsioDevices, Host as AsioHost, Stream as AsioStream,
|
||||
SupportedInputConfigs as AsioSupportedInputConfigs,
|
||||
SupportedOutputConfigs as AsioSupportedOutputConfigs,
|
||||
};
|
||||
pub use crate::host::wasapi::{
|
||||
Device as WasapiDevice, Devices as WasapiDevices, Host as WasapiHost,
|
||||
Stream as WasapiStream, SupportedInputConfigs as WasapiSupportedInputConfigs,
|
||||
SupportedOutputConfigs as WasapiSupportedOutputConfigs,
|
||||
};
|
||||
|
||||
impl_platform_host!(#[cfg(feature = "asio")] Asio asio "ASIO", Wasapi wasapi "WASAPI");
|
||||
|
||||
/// The default host for the current compilation target platform.
|
||||
pub fn default_host() -> Host {
|
||||
WasapiHost::new()
|
||||
.expect("the default host should always be available")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod platform_impl {
|
||||
pub use crate::host::oboe::{
|
||||
Device as OboeDevice, Devices as OboeDevices, Host as OboeHost, Stream as OboeStream,
|
||||
SupportedInputConfigs as OboeSupportedInputConfigs,
|
||||
SupportedOutputConfigs as OboeSupportedOutputConfigs,
|
||||
};
|
||||
|
||||
impl_platform_host!(Oboe oboe "Oboe");
|
||||
|
||||
/// The default host for the current compilation target platform.
|
||||
pub fn default_host() -> Host {
|
||||
OboeHost::new()
|
||||
.expect("the default host should always be available")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(
|
||||
windows,
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "macos",
|
||||
target_os = "ios",
|
||||
target_os = "emscripten",
|
||||
target_os = "android",
|
||||
all(target_arch = "wasm32", feature = "wasm-bindgen"),
|
||||
)))]
|
||||
mod platform_impl {
|
||||
pub use crate::host::null::{
|
||||
Device as NullDevice, Devices as NullDevices, Host as NullHost,
|
||||
SupportedInputConfigs as NullSupportedInputConfigs,
|
||||
SupportedOutputConfigs as NullSupportedOutputConfigs,
|
||||
};
|
||||
|
||||
impl_platform_host!(Null null "Null");
|
||||
|
||||
/// The default host for the current compilation target platform.
|
||||
pub fn default_host() -> Host {
|
||||
NullHost::new()
|
||||
.expect("the default host should always be available")
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
// The following zero-sized types are for applying Send/Sync restrictions to ensure
|
||||
// consistent behaviour across different platforms. These verbosely named types are used
|
||||
// (rather than using the markers directly) in the hope of making the compile errors
|
||||
// slightly more helpful.
|
||||
//
|
||||
// TODO: Remove these in favour of using negative trait bounds if they stabilise.
|
||||
|
||||
// A marker used to remove the `Send` and `Sync` traits.
|
||||
struct NotSendSyncAcrossAllPlatforms(std::marker::PhantomData<*mut ()>);
|
||||
|
||||
impl Default for NotSendSyncAcrossAllPlatforms {
|
||||
fn default() -> Self {
|
||||
NotSendSyncAcrossAllPlatforms(std::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
-167
@@ -1,167 +0,0 @@
|
||||
use std::{fmt::Display, mem};
|
||||
#[cfg(target_os = "emscripten")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub use dasp_sample::{FromSample, Sample, I24, I48, U24, U48};
|
||||
|
||||
/// Format that each sample has.
|
||||
#[cfg_attr(target_os = "emscripten", wasm_bindgen)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum SampleFormat {
|
||||
/// `i8` with a valid range of 'u8::MIN..=u8::MAX' with `0` being the origin
|
||||
I8,
|
||||
|
||||
/// `i16` with a valid range of 'u16::MIN..=u16::MAX' with `0` being the origin
|
||||
I16,
|
||||
|
||||
// /// `I24` with a valid range of '-(1 << 23)..(1 << 23)' with `0` being the origin
|
||||
// I24,
|
||||
/// `i32` with a valid range of 'u32::MIN..=u32::MAX' with `0` being the origin
|
||||
I32,
|
||||
|
||||
// /// `I24` with a valid range of '-(1 << 47)..(1 << 47)' with `0` being the origin
|
||||
// I48,
|
||||
/// `i64` with a valid range of 'u64::MIN..=u64::MAX' with `0` being the origin
|
||||
I64,
|
||||
|
||||
/// `u8` with a valid range of 'u8::MIN..=u8::MAX' with `1 << 7 == 128` being the origin
|
||||
U8,
|
||||
|
||||
/// `u16` with a valid range of 'u16::MIN..=u16::MAX' with `1 << 15 == 32768` being the origin
|
||||
U16,
|
||||
|
||||
// /// `U24` with a valid range of '0..16777216' with `1 << 23 == 8388608` being the origin
|
||||
// U24,
|
||||
/// `u32` with a valid range of 'u32::MIN..=u32::MAX' with `1 << 31` being the origin
|
||||
U32,
|
||||
|
||||
// /// `U48` with a valid range of '0..(1 << 48)' with `1 << 47` being the origin
|
||||
// U48,
|
||||
/// `u64` with a valid range of 'u64::MIN..=u64::MAX' with `1 << 63` being the origin
|
||||
U64,
|
||||
|
||||
/// `f32` with a valid range of `-1.0..1.0` with `0.0` being the origin
|
||||
F32,
|
||||
|
||||
/// `f64` with a valid range of -1.0..1.0 with 0.0 being the origin
|
||||
F64,
|
||||
}
|
||||
|
||||
impl SampleFormat {
|
||||
/// Returns the size in bytes of a sample of this format.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn sample_size(&self) -> usize {
|
||||
match *self {
|
||||
SampleFormat::I8 | SampleFormat::U8 => mem::size_of::<i8>(),
|
||||
SampleFormat::I16 | SampleFormat::U16 => mem::size_of::<i16>(),
|
||||
// SampleFormat::I24 | SampleFormat::U24 => 3,
|
||||
SampleFormat::I32 | SampleFormat::U32 => mem::size_of::<i32>(),
|
||||
// SampleFormat::I48 | SampleFormat::U48 => 6,
|
||||
SampleFormat::I64 | SampleFormat::U64 => mem::size_of::<i64>(),
|
||||
SampleFormat::F32 => mem::size_of::<f32>(),
|
||||
SampleFormat::F64 => mem::size_of::<f64>(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn is_int(&self) -> bool {
|
||||
//matches!(*self, SampleFormat::I8 | SampleFormat::I16 | SampleFormat::I24 | SampleFormat::I32 | SampleFormat::I48 | SampleFormat::I64)
|
||||
matches!(
|
||||
*self,
|
||||
SampleFormat::I8 | SampleFormat::I16 | SampleFormat::I32 | SampleFormat::I64
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn is_uint(&self) -> bool {
|
||||
//matches!(*self, SampleFormat::U8 | SampleFormat::U16 | SampleFormat::U24 | SampleFormat::U32 | SampleFormat::U48 | SampleFormat::U64)
|
||||
matches!(
|
||||
*self,
|
||||
SampleFormat::U8 | SampleFormat::U16 | SampleFormat::U32 | SampleFormat::U64
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn is_float(&self) -> bool {
|
||||
matches!(*self, SampleFormat::F32 | SampleFormat::F64)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SampleFormat {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match *self {
|
||||
SampleFormat::I8 => "i8",
|
||||
SampleFormat::I16 => "i16",
|
||||
// SampleFormat::I24 => "i24",
|
||||
SampleFormat::I32 => "i32",
|
||||
// SampleFormat::I48 => "i48",
|
||||
SampleFormat::I64 => "i64",
|
||||
SampleFormat::U8 => "u8",
|
||||
SampleFormat::U16 => "u16",
|
||||
// SampleFormat::U24 => "u24",
|
||||
SampleFormat::U32 => "u32",
|
||||
// SampleFormat::U48 => "u48",
|
||||
SampleFormat::U64 => "u64",
|
||||
SampleFormat::F32 => "f32",
|
||||
SampleFormat::F64 => "f64",
|
||||
}
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SizedSample: Sample {
|
||||
const FORMAT: SampleFormat;
|
||||
}
|
||||
|
||||
impl SizedSample for i8 {
|
||||
const FORMAT: SampleFormat = SampleFormat::I8;
|
||||
}
|
||||
|
||||
impl SizedSample for i16 {
|
||||
const FORMAT: SampleFormat = SampleFormat::I16;
|
||||
}
|
||||
|
||||
// impl SizedSample for I24 { const FORMAT: SampleFormat = SampleFormat::I24; }
|
||||
|
||||
impl SizedSample for i32 {
|
||||
const FORMAT: SampleFormat = SampleFormat::I32;
|
||||
}
|
||||
|
||||
// impl SizedSample for I48 { const FORMAT: SampleFormat = SampleFormat::I48; }
|
||||
|
||||
impl SizedSample for i64 {
|
||||
const FORMAT: SampleFormat = SampleFormat::I64;
|
||||
}
|
||||
|
||||
impl SizedSample for u8 {
|
||||
const FORMAT: SampleFormat = SampleFormat::U8;
|
||||
}
|
||||
|
||||
impl SizedSample for u16 {
|
||||
const FORMAT: SampleFormat = SampleFormat::U16;
|
||||
}
|
||||
|
||||
// impl SizedSample for U24 { const FORMAT: SampleFormat = SampleFormat::U24; }
|
||||
|
||||
impl SizedSample for u32 {
|
||||
const FORMAT: SampleFormat = SampleFormat::U32;
|
||||
}
|
||||
|
||||
// impl SizedSample for U48 { const FORMAT: SampleFormat = SampleFormat::U48; }
|
||||
|
||||
impl SizedSample for u64 {
|
||||
const FORMAT: SampleFormat = SampleFormat::U64;
|
||||
}
|
||||
|
||||
impl SizedSample for f32 {
|
||||
const FORMAT: SampleFormat = SampleFormat::F32;
|
||||
}
|
||||
|
||||
impl SizedSample for f64 {
|
||||
const FORMAT: SampleFormat = SampleFormat::F64;
|
||||
}
|
||||
Vendored
-221
@@ -1,221 +0,0 @@
|
||||
//! The suite of traits allowing CPAL to abstract over hosts, devices, event loops and stream IDs.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError,
|
||||
InputCallbackInfo, InputDevices, OutputCallbackInfo, OutputDevices, PauseStreamError,
|
||||
PlayStreamError, SampleFormat, SizedSample, StreamConfig, StreamError, SupportedStreamConfig,
|
||||
SupportedStreamConfigRange, SupportedStreamConfigsError,
|
||||
};
|
||||
|
||||
/// A [`Host`] provides access to the available audio devices on the system.
|
||||
///
|
||||
/// Each platform may have a number of available hosts depending on the system, each with their own
|
||||
/// pros and cons.
|
||||
///
|
||||
/// For example, WASAPI is the standard audio host API that ships with the Windows operating
|
||||
/// system. However, due to historical limitations with respect to performance and flexibility,
|
||||
/// Steinberg created the ASIO API providing better audio device support for pro audio and
|
||||
/// low-latency applications. As a result, it is common for some devices and device capabilities to
|
||||
/// only be available via ASIO, while others are only available via WASAPI.
|
||||
///
|
||||
/// Another great example is the Linux platform. While the ALSA host API is the lowest-level API
|
||||
/// available to almost all distributions of Linux, its flexibility is limited as it requires that
|
||||
/// each process have exclusive access to the devices with which they establish streams. PulseAudio
|
||||
/// is another popular host API that aims to solve this issue by providing user-space mixing,
|
||||
/// however it has its own limitations w.r.t. low-latency and high-performance audio applications.
|
||||
/// JACK is yet another host API that is more suitable to pro-audio applications, however it is
|
||||
/// less readily available by default in many Linux distributions and is known to be tricky to
|
||||
/// set up.
|
||||
///
|
||||
/// [`Host`]: crate::Host
|
||||
pub trait HostTrait {
|
||||
/// The type used for enumerating available devices by the host.
|
||||
type Devices: Iterator<Item = Self::Device>;
|
||||
/// The `Device` type yielded by the host.
|
||||
type Device: DeviceTrait;
|
||||
|
||||
/// Whether or not the host is available on the system.
|
||||
fn is_available() -> bool;
|
||||
|
||||
/// An iterator yielding all [`Device`](DeviceTrait)s currently available to the host on the system.
|
||||
///
|
||||
/// Can be empty if the system does not support audio in general.
|
||||
fn devices(&self) -> Result<Self::Devices, DevicesError>;
|
||||
|
||||
/// The default input audio device on the system.
|
||||
///
|
||||
/// Returns `None` if no input device is available.
|
||||
fn default_input_device(&self) -> Option<Self::Device>;
|
||||
|
||||
/// The default output audio device on the system.
|
||||
///
|
||||
/// Returns `None` if no output device is available.
|
||||
fn default_output_device(&self) -> Option<Self::Device>;
|
||||
|
||||
/// An iterator yielding all `Device`s currently available to the system that support one or more
|
||||
/// input stream formats.
|
||||
///
|
||||
/// Can be empty if the system does not support audio input.
|
||||
fn input_devices(&self) -> Result<InputDevices<Self::Devices>, DevicesError> {
|
||||
fn supports_input<D: DeviceTrait>(device: &D) -> bool {
|
||||
device
|
||||
.supported_input_configs()
|
||||
.map(|mut iter| iter.next().is_some())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
Ok(self.devices()?.filter(supports_input::<Self::Device>))
|
||||
}
|
||||
|
||||
/// An iterator yielding all `Device`s currently available to the system that support one or more
|
||||
/// output stream formats.
|
||||
///
|
||||
/// Can be empty if the system does not support audio output.
|
||||
fn output_devices(&self) -> Result<OutputDevices<Self::Devices>, DevicesError> {
|
||||
fn supports_output<D: DeviceTrait>(device: &D) -> bool {
|
||||
device
|
||||
.supported_output_configs()
|
||||
.map(|mut iter| iter.next().is_some())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
Ok(self.devices()?.filter(supports_output::<Self::Device>))
|
||||
}
|
||||
}
|
||||
|
||||
/// A device that is capable of audio input and/or output.
|
||||
///
|
||||
/// Please note that `Device`s may become invalid if they get disconnected. Therefore, all the
|
||||
/// methods that involve a device return a `Result` allowing the user to handle this case.
|
||||
pub trait DeviceTrait {
|
||||
/// The iterator type yielding supported input stream formats.
|
||||
type SupportedInputConfigs: Iterator<Item = SupportedStreamConfigRange>;
|
||||
/// The iterator type yielding supported output stream formats.
|
||||
type SupportedOutputConfigs: Iterator<Item = SupportedStreamConfigRange>;
|
||||
/// The stream type created by [`build_input_stream_raw`] and [`build_output_stream_raw`].
|
||||
///
|
||||
/// [`build_input_stream_raw`]: Self::build_input_stream_raw
|
||||
/// [`build_output_stream_raw`]: Self::build_output_stream_raw
|
||||
type Stream: StreamTrait;
|
||||
|
||||
/// The human-readable name of the device.
|
||||
fn name(&self) -> Result<String, DeviceNameError>;
|
||||
|
||||
/// An iterator yielding formats that are supported by the backend.
|
||||
///
|
||||
/// Can return an error if the device is no longer valid (e.g. it has been disconnected).
|
||||
fn supported_input_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError>;
|
||||
|
||||
/// An iterator yielding output stream formats that are supported by the device.
|
||||
///
|
||||
/// Can return an error if the device is no longer valid (e.g. it has been disconnected).
|
||||
fn supported_output_configs(
|
||||
&self,
|
||||
) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError>;
|
||||
|
||||
/// The default input stream format for the device.
|
||||
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError>;
|
||||
|
||||
/// The default output stream format for the device.
|
||||
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError>;
|
||||
|
||||
/// Create an input stream.
|
||||
fn build_input_stream<T, D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
mut data_callback: D,
|
||||
error_callback: E,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
T: SizedSample,
|
||||
D: FnMut(&[T], &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
self.build_input_stream_raw(
|
||||
config,
|
||||
T::FORMAT,
|
||||
move |data, info| {
|
||||
data_callback(
|
||||
data.as_slice()
|
||||
.expect("host supplied incorrect sample type"),
|
||||
info,
|
||||
)
|
||||
},
|
||||
error_callback,
|
||||
timeout,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create an output stream.
|
||||
fn build_output_stream<T, D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
mut data_callback: D,
|
||||
error_callback: E,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
T: SizedSample,
|
||||
D: FnMut(&mut [T], &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static,
|
||||
{
|
||||
self.build_output_stream_raw(
|
||||
config,
|
||||
T::FORMAT,
|
||||
move |data, info| {
|
||||
data_callback(
|
||||
data.as_slice_mut()
|
||||
.expect("host supplied incorrect sample type"),
|
||||
info,
|
||||
)
|
||||
},
|
||||
error_callback,
|
||||
timeout,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a dynamically typed input stream.
|
||||
fn build_input_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static;
|
||||
|
||||
/// Create a dynamically typed output stream.
|
||||
fn build_output_stream_raw<D, E>(
|
||||
&self,
|
||||
config: &StreamConfig,
|
||||
sample_format: SampleFormat,
|
||||
data_callback: D,
|
||||
error_callback: E,
|
||||
timeout: Option<Duration>,
|
||||
) -> Result<Self::Stream, BuildStreamError>
|
||||
where
|
||||
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
||||
E: FnMut(StreamError) + Send + 'static;
|
||||
}
|
||||
|
||||
/// A stream created from [`Device`](DeviceTrait), with methods to control playback.
|
||||
pub trait StreamTrait {
|
||||
/// Run the stream.
|
||||
///
|
||||
/// Note: Not all platforms automatically run the stream upon creation, so it is important to
|
||||
/// call `play` after creation if it is expected that the stream should run immediately.
|
||||
fn play(&self) -> Result<(), PlayStreamError>;
|
||||
|
||||
/// Some devices support pausing the audio stream. This can be useful for saving energy in
|
||||
/// moments of silence.
|
||||
///
|
||||
/// Note: Not all devices support suspending the stream at the hardware level. This method may
|
||||
/// fail in these cases.
|
||||
fn pause(&self) -> Result<(), PauseStreamError>;
|
||||
}
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
{"v":1}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "27f2b424060ff4a977285ff83ecd6c076205c121"
|
||||
},
|
||||
"path_in_vcs": ""
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main, master]
|
||||
|
||||
env:
|
||||
RUSTFLAGS: "-C debuginfo=0 -D warnings"
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||
toolchain: [stable, beta, nightly]
|
||||
include:
|
||||
- os: macos-latest
|
||||
MACOS: true
|
||||
- os: windows-latest
|
||||
- os: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: install linux deps
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y --no-install-recommends libasound2-dev pkg-config
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
|
||||
- name: install ${{ matrix.toolchain }} toolchain
|
||||
id: install_toolchain
|
||||
run: rustup toolchain install ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
|
||||
|
||||
- run: cargo clippy -- -D warnings
|
||||
if: matrix.toolchain == 'stable' && matrix.os == 'ubuntu-latest'
|
||||
|
||||
- run: |
|
||||
rustup component add rustfmt
|
||||
cargo fmt --all -- --check
|
||||
if: matrix.toolchain == 'stable' && matrix.os == 'ubuntu-latest'
|
||||
|
||||
- run: cargo test --all-targets
|
||||
- run: cargo test --features=symphonia-all --all-targets
|
||||
cargo-publish:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
env:
|
||||
CRATESIO_TOKEN: ${{ secrets.CRATESIO_TOKEN }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Update apt
|
||||
run: sudo apt update
|
||||
- name: Install alsa
|
||||
run: sudo apt install -y --no-install-recommends libasound2-dev pkg-config
|
||||
- name: Run cargo publish for rodio
|
||||
continue-on-error: true
|
||||
run: |
|
||||
RODIO_TMP=$(mktemp /tmp/rodioXXX.txt) || echo "::error::mktemp error"
|
||||
echo "RODIO_TMP=$RODIO_TMP" >> $GITHUB_ENV
|
||||
cargo publish --token $CRATESIO_TOKEN 2> $RODIO_TMP
|
||||
- name: Check if rodio is already published
|
||||
run: |
|
||||
empty=0
|
||||
RODIO_TMP="${{ env.RODIO_TMP }}"
|
||||
grep -q '[^[:space:]]' < $RODIO_TMP || empty=1
|
||||
[ $empty -eq 0 ] && cat $RODIO_TMP
|
||||
[ $empty -eq 1 ] || grep -q "is already uploaded" < $RODIO_TMP
|
||||
@@ -1,2 +0,0 @@
|
||||
target
|
||||
Cargo.lock
|
||||
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
reorder_imports = true
|
||||
use_try_shorthand = true
|
||||
Vendored
-137
@@ -1,137 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
# Version 0.19.0 (2024-06-29)
|
||||
|
||||
### Added
|
||||
- Adds a new source `track_position`. It keeps track of duration since the
|
||||
beginning of the underlying source.
|
||||
|
||||
### Fixed
|
||||
- Mp4a with decodable tracks after undecodable tracks now play. This matches
|
||||
VLC's behaviour.
|
||||
|
||||
# Version 0.18.1 (2024-05-23)
|
||||
|
||||
### Fixed
|
||||
- Seek no longer hangs if the sink is empty.
|
||||
|
||||
# Version 0.18.0 (2024-05-05)
|
||||
|
||||
### Changed
|
||||
- `Source` trait is now also implemented for `Box<dyn Source>` and `&mut Source`
|
||||
- `fn new_vorbis` is now also available when the `symphonia-vorbis` feature is enabled
|
||||
|
||||
### Added
|
||||
- Adds a new method `try_seek` to all sources. It returns either an error or
|
||||
seeks to the given position. A few sources are "unsupported" they return the
|
||||
error `Unsupported`.
|
||||
- Adds `SpatialSink::clear()` bringing it in line with `Sink`
|
||||
|
||||
### Fixed
|
||||
- channel upscaling now follows the 'WAVEFORMATEXTENSIBLE' format and no longer
|
||||
repeats the last source channel on all extra output channels.
|
||||
Stereo content playing on a 5.1 speaker set will now only use the front left
|
||||
and front right speaker instead of repeating the right sample on all speakers
|
||||
except the front left one.
|
||||
- `mp3::is_mp3()` no longer changes the position in the stream when the stream
|
||||
is mp3
|
||||
|
||||
# Version 0.17.3 (2023-10-23)
|
||||
|
||||
- Build fix for `minimp3` backend.
|
||||
|
||||
# Version 0.17.2 (2023-10-17)
|
||||
|
||||
- Add `EmptyCallback` source.
|
||||
- Fix index out of bounds bug.
|
||||
- Use non-vulnerable `minimp3` fork.
|
||||
- Add filter functions with additional q parameter.
|
||||
|
||||
# Version 0.17.1 (2023-02-25)
|
||||
|
||||
- Disable `symphonia`'s default features.
|
||||
|
||||
# Version 0.17.0 (2023-02-17)
|
||||
|
||||
- Update `cpal` to [0.15](https://github.com/RustAudio/cpal/blob/master/CHANGELOG.md#version-0150-2022-01-29).
|
||||
- Default to `symphonia` for mp3 decoding.
|
||||
|
||||
# Version 0.16.0 (2022-09-14)
|
||||
|
||||
- Update `cpal` to [0.14](https://github.com/RustAudio/cpal/blob/master/CHANGELOG.md#version-0140-2022-08-22).
|
||||
- Update `symphonia` to [0.5](https://github.com/pdeljanov/Symphonia/releases/tag/v0.5.1).
|
||||
|
||||
# Version 0.15.0 (2022-01-23)
|
||||
|
||||
- Remove requirement that the argument `Decoder::new` and `LoopedDecoder::new` implement `Send`.
|
||||
- Add optional symphonia backend.
|
||||
- `WavDecoder`'s `total_duration` now returns the total duration of the sound rather than the remaining duration.
|
||||
- Add 32-bit signed in WAV decoding.
|
||||
- `SineWave::new()` now takes a `f32` instead of a `u32`.
|
||||
- Add `len()` method to `SpatialSink`.
|
||||
|
||||
# Version 0.14.0 (2021-05-21)
|
||||
|
||||
- Re-export `cpal` in full.
|
||||
- Replace panics when calling `OutputStream::try_default`, `OutputStream::try_from_device` with new
|
||||
`StreamError` variants.
|
||||
- `OutputStream::try_default` will now fallback to non-default output devices if an `OutputStream`
|
||||
cannot be created from the default device.
|
||||
|
||||
# Version 0.13.1 (2021-03-28)
|
||||
|
||||
- Fix panic when no `pulseaudio-alsa` was installed.
|
||||
|
||||
# Version 0.13.0 (2020-11-03)
|
||||
|
||||
- Update `cpal` to [0.13](https://github.com/RustAudio/cpal/blob/master/CHANGELOG.md#version-0130-2020-10-28).
|
||||
- Add Android support.
|
||||
|
||||
# Version 0.12.0 (2020-10-05)
|
||||
|
||||
- Breaking: Update `cpal` to [0.12](https://github.com/RustAudio/cpal/blob/master/CHANGELOG.md#version-0120-2020-07-09).
|
||||
- Breaking: Rework API removing global "rodio audio processing" thread & adapting to the upstream cpal API changes.
|
||||
- Add new_X format specific methods to Decoder.
|
||||
- Fix resampler dependency on internal `Vec::capacity` behaviour.
|
||||
|
||||
# Version 0.11.0 (2020-03-16)
|
||||
|
||||
- Update `lewton` to [0.10](https://github.com/RustAudio/lewton/blob/master/CHANGELOG.md#release-0100---january-30-2020).
|
||||
- Breaking: Update `cpal` to [0.11](https://github.com/RustAudio/cpal/blob/master/CHANGELOG.md#version-0110-2019-12-11)
|
||||
|
||||
# Version 0.10.0 (2019-11-16)
|
||||
|
||||
- Removal of nalgebra in favour of own code.
|
||||
- Fix a bug that switched channels when resuming after having paused.
|
||||
- Attempt all supported output formats if the default format fails in `Sink::new`.
|
||||
- Breaking: Update `cpal` to [0.10](https://github.com/RustAudio/cpal/blob/master/CHANGELOG.md#version-0100-2019-07-05).
|
||||
|
||||
# Version 0.9.0 (2019-06-08)
|
||||
|
||||
- Remove exclusive `&mut` borrow requirements in `Sink` & `SpatialSink` setters.
|
||||
- Use `nalgebra` instead of `cgmath` for `Spatial` source.
|
||||
|
||||
# Version 0.8.1 (2018-09-18)
|
||||
|
||||
- Update `lewton` dependency to [0.9](https://github.com/RustAudio/lewton/blob/master/CHANGELOG.md#release-090---august-16-2018)
|
||||
- Change license from `Apache-2.0` only to `Apache-2.0 OR MIT`
|
||||
|
||||
# Version 0.8.0 (2018-06-22)
|
||||
|
||||
- Add mp3 decoding capabilities via `minimp3`
|
||||
|
||||
# Version 0.7.0 (2018-04-19)
|
||||
|
||||
- Update `cpal` dependency to 0.8, and adopt the new naming convention
|
||||
- BREAKING CHANGES:
|
||||
- renamed `Endpoint` to `Device`
|
||||
- split `default_endpoint()` into `default_output_device()` and `default_input_device()`
|
||||
- renamed `endpoints()` to `devices()`
|
||||
- introduced `output_devices()` and `input_devices()`
|
||||
Vendored
-109
@@ -1,109 +0,0 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "rodio"
|
||||
version = "0.19.0"
|
||||
exclude = [
|
||||
"assets/**",
|
||||
"tests/**",
|
||||
]
|
||||
description = "Audio playback library"
|
||||
documentation = "https://docs.rs/rodio"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"audio",
|
||||
"playback",
|
||||
"gamedev",
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/RustAudio/rodio"
|
||||
|
||||
[[example]]
|
||||
name = "music_m4a"
|
||||
required-features = [
|
||||
"symphonia-isomp4",
|
||||
"symphonia-aac",
|
||||
]
|
||||
|
||||
[dependencies.claxon]
|
||||
version = "0.4.2"
|
||||
optional = true
|
||||
|
||||
[dependencies.cpal]
|
||||
version = "0.15.3"
|
||||
|
||||
[dependencies.crossbeam-channel]
|
||||
version = "0.5.8"
|
||||
optional = true
|
||||
|
||||
[dependencies.hound]
|
||||
version = "3.3.1"
|
||||
optional = true
|
||||
|
||||
[dependencies.lewton]
|
||||
version = "0.10"
|
||||
optional = true
|
||||
|
||||
[dependencies.minimp3_fixed]
|
||||
version = "0.5.4"
|
||||
optional = true
|
||||
|
||||
[dependencies.symphonia]
|
||||
version = "0.5.4"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "1.0.49"
|
||||
|
||||
[dev-dependencies.quickcheck]
|
||||
version = "0.9.2"
|
||||
|
||||
[dev-dependencies.rstest]
|
||||
version = "0.18.2"
|
||||
|
||||
[dev-dependencies.rstest_reuse]
|
||||
version = "0.6.0"
|
||||
|
||||
[features]
|
||||
cpal-shared-stdcxx = ["cpal/oboe-shared-stdcxx"]
|
||||
default = [
|
||||
"flac",
|
||||
"vorbis",
|
||||
"wav",
|
||||
"mp3",
|
||||
]
|
||||
flac = ["claxon"]
|
||||
minimp3 = ["dep:minimp3_fixed"]
|
||||
mp3 = ["symphonia-mp3"]
|
||||
symphonia-aac = ["symphonia/aac"]
|
||||
symphonia-all = [
|
||||
"symphonia-aac",
|
||||
"symphonia-flac",
|
||||
"symphonia-isomp4",
|
||||
"symphonia-mp3",
|
||||
"symphonia-vorbis",
|
||||
"symphonia-wav",
|
||||
]
|
||||
symphonia-flac = ["symphonia/flac"]
|
||||
symphonia-isomp4 = ["symphonia/isomp4"]
|
||||
symphonia-mp3 = ["symphonia/mp3"]
|
||||
symphonia-vorbis = ["symphonia/vorbis"]
|
||||
symphonia-wav = [
|
||||
"symphonia/wav",
|
||||
"symphonia/pcm",
|
||||
"symphonia/adpcm",
|
||||
]
|
||||
vorbis = ["lewton"]
|
||||
wasm-bindgen = ["cpal/wasm-bindgen"]
|
||||
wav = ["hound"]
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
[package]
|
||||
name = "rodio"
|
||||
version = "0.19.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Audio playback library"
|
||||
keywords = ["audio", "playback", "gamedev"]
|
||||
repository = "https://github.com/RustAudio/rodio"
|
||||
documentation = "https://docs.rs/rodio"
|
||||
exclude = ["assets/**", "tests/**"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cpal = "0.15.3"
|
||||
claxon = { version = "0.4.2", optional = true }
|
||||
hound = { version = "3.3.1", optional = true }
|
||||
lewton = { version = "0.10", optional = true }
|
||||
minimp3_fixed = { version = "0.5.4", optional = true}
|
||||
symphonia = { version = "0.5.4", optional = true, default-features = false }
|
||||
crossbeam-channel = { version = "0.5.8", optional = true }
|
||||
thiserror = "1.0.49"
|
||||
|
||||
[features]
|
||||
default = ["flac", "vorbis", "wav", "mp3"]
|
||||
|
||||
flac = ["claxon"]
|
||||
vorbis = ["lewton"]
|
||||
wav = ["hound"]
|
||||
mp3 = ["symphonia-mp3"]
|
||||
minimp3 = ["dep:minimp3_fixed"]
|
||||
wasm-bindgen = ["cpal/wasm-bindgen"]
|
||||
cpal-shared-stdcxx = ["cpal/oboe-shared-stdcxx"]
|
||||
symphonia-aac = ["symphonia/aac"]
|
||||
symphonia-all = ["symphonia-aac", "symphonia-flac", "symphonia-isomp4", "symphonia-mp3", "symphonia-vorbis", "symphonia-wav"]
|
||||
symphonia-flac = ["symphonia/flac"]
|
||||
symphonia-isomp4 = ["symphonia/isomp4"]
|
||||
symphonia-mp3 = ["symphonia/mp3"]
|
||||
symphonia-vorbis = ["symphonia/vorbis"]
|
||||
symphonia-wav = ["symphonia/wav", "symphonia/pcm", "symphonia/adpcm"]
|
||||
|
||||
[dev-dependencies]
|
||||
quickcheck = "0.9.2"
|
||||
rstest = "0.18.2"
|
||||
rstest_reuse = "0.6.0"
|
||||
|
||||
[[example]]
|
||||
name = "music_m4a"
|
||||
required-features = ["symphonia-isomp4", "symphonia-aac"]
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2015 Pierre Krieger
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Vendored
-23
@@ -1,23 +0,0 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
Vendored
-35
@@ -1,35 +0,0 @@
|
||||
# Audio playback library
|
||||
|
||||
[](https://crates.io/crates/rodio)
|
||||
[](https://crates.io/crates/rodio)
|
||||
[](https://github.com/RustAudio/rodio/actions)
|
||||
|
||||
Rust playback library.
|
||||
|
||||
Playback is handled by [cpal](https://github.com/RustAudio/cpal). Format decoding can be handled either by [Symphonia](https://github.com/pdeljanov/Symphonia), or by format-specific decoders:
|
||||
|
||||
- MP3 by [minimp3](https://github.com/lieff/minimp3) (but defaults to [Symphonia](https://github.com/pdeljanov/Symphonia)).
|
||||
- WAV by [hound](https://github.com/ruud-v-a/hound).
|
||||
- Vorbis by [lewton](https://github.com/est31/lewton).
|
||||
- FLAC by [claxon](https://github.com/ruuda/claxon).
|
||||
- MP4 and AAC (both disabled by default) are handled only by [Symphonia](https://github.com/pdeljanov/Symphonia).
|
||||
|
||||
See [the docs](https://docs.rs/rodio/latest/rodio/#alternative-decoder-backends) for more details on backends.
|
||||
|
||||
# [Documentation](http://docs.rs/rodio)
|
||||
|
||||
[The documentation](http://docs.rs/rodio) contains an introduction to the library.
|
||||
|
||||
## License
|
||||
[License]: #license
|
||||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0), or
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
### License of your contributions
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
use std::io::BufReader;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
let (_stream, stream_handle) = rodio::OutputStream::try_default().unwrap();
|
||||
|
||||
let file = std::fs::File::open("assets/beep.wav").unwrap();
|
||||
let beep1 = stream_handle.play_once(BufReader::new(file)).unwrap();
|
||||
beep1.set_volume(0.2);
|
||||
println!("Started beep1");
|
||||
|
||||
thread::sleep(Duration::from_millis(1500));
|
||||
|
||||
let file = std::fs::File::open("assets/beep2.wav").unwrap();
|
||||
let beep2 = stream_handle.play_once(BufReader::new(file)).unwrap();
|
||||
beep2.set_volume(0.3);
|
||||
beep2.detach();
|
||||
println!("Started beep2");
|
||||
|
||||
thread::sleep(Duration::from_millis(1500));
|
||||
let file = std::fs::File::open("assets/beep3.ogg").unwrap();
|
||||
let beep3 = stream_handle.play_once(file).unwrap();
|
||||
beep3.set_volume(0.2);
|
||||
println!("Started beep3");
|
||||
|
||||
thread::sleep(Duration::from_millis(1500));
|
||||
drop(beep1);
|
||||
println!("Stopped beep1");
|
||||
|
||||
thread::sleep(Duration::from_millis(1500));
|
||||
drop(beep3);
|
||||
println!("Stopped beep3");
|
||||
|
||||
thread::sleep(Duration::from_millis(1500));
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use rodio::source::{SineWave, Source};
|
||||
use rodio::{dynamic_mixer, OutputStream, Sink};
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
// Construct a dynamic controller and mixer, stream_handle, and sink.
|
||||
let (controller, mixer) = dynamic_mixer::mixer::<f32>(2, 44_100);
|
||||
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||
let sink = Sink::try_new(&stream_handle).unwrap();
|
||||
|
||||
// Create four unique sources. The frequencies used here correspond
|
||||
// notes in the key of C and in octave 4: C4, or middle C on a piano,
|
||||
// E4, G4, and A4 respectively.
|
||||
let source_c = SineWave::new(261.63)
|
||||
.take_duration(Duration::from_secs_f32(1.))
|
||||
.amplify(0.20);
|
||||
let source_e = SineWave::new(329.63)
|
||||
.take_duration(Duration::from_secs_f32(1.))
|
||||
.amplify(0.20);
|
||||
let source_g = SineWave::new(392.0)
|
||||
.take_duration(Duration::from_secs_f32(1.))
|
||||
.amplify(0.20);
|
||||
let source_a = SineWave::new(440.0)
|
||||
.take_duration(Duration::from_secs_f32(1.))
|
||||
.amplify(0.20);
|
||||
|
||||
// Add sources C, E, G, and A to the mixer controller.
|
||||
controller.add(source_c);
|
||||
controller.add(source_e);
|
||||
controller.add(source_g);
|
||||
controller.add(source_a);
|
||||
|
||||
// Append the dynamic mixer to the sink to play a C major 6th chord.
|
||||
sink.append(mixer);
|
||||
|
||||
// Sleep the thread until sink is empty.
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
use std::io::BufReader;
|
||||
|
||||
fn main() {
|
||||
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
|
||||
let sink = rodio::Sink::try_new(&handle).unwrap();
|
||||
|
||||
let file = std::fs::File::open("assets/music.flac").unwrap();
|
||||
sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap());
|
||||
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
use std::io::BufReader;
|
||||
|
||||
fn main() {
|
||||
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
|
||||
let sink = rodio::Sink::try_new(&handle).unwrap();
|
||||
|
||||
let file = std::fs::File::open("assets/music.m4a").unwrap();
|
||||
sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap());
|
||||
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
use std::io::BufReader;
|
||||
|
||||
fn main() {
|
||||
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
|
||||
let sink = rodio::Sink::try_new(&handle).unwrap();
|
||||
|
||||
let file = std::fs::File::open("assets/music.mp3").unwrap();
|
||||
sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap());
|
||||
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
use std::io::BufReader;
|
||||
|
||||
fn main() {
|
||||
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
|
||||
let sink = rodio::Sink::try_new(&handle).unwrap();
|
||||
|
||||
let file = std::fs::File::open("assets/music.ogg").unwrap();
|
||||
sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap());
|
||||
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
use std::io::BufReader;
|
||||
|
||||
fn main() {
|
||||
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
|
||||
let sink = rodio::Sink::try_new(&handle).unwrap();
|
||||
|
||||
let file = std::fs::File::open("assets/music.wav").unwrap();
|
||||
sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap());
|
||||
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
use rodio::Source;
|
||||
use std::io::BufReader;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
|
||||
let sink = rodio::Sink::try_new(&handle).unwrap();
|
||||
|
||||
let file = std::fs::File::open("assets/music.ogg").unwrap();
|
||||
let source = rodio::Decoder::new(BufReader::new(file)).unwrap();
|
||||
let with_reverb = source.buffered().reverb(Duration::from_millis(40), 0.7);
|
||||
sink.append(with_reverb);
|
||||
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
use std::io::BufReader;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
|
||||
let sink = rodio::Sink::try_new(&handle).unwrap();
|
||||
|
||||
let file = std::fs::File::open("assets/music.mp3").unwrap();
|
||||
sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap());
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
sink.try_seek(Duration::from_secs(0)).unwrap();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
sink.try_seek(Duration::from_secs(4)).unwrap();
|
||||
|
||||
sink.sleep_until_end();
|
||||
|
||||
// wont do anything since the sound has ended already
|
||||
sink.try_seek(Duration::from_secs(5)).unwrap();
|
||||
println!("seek example ended");
|
||||
}
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
use std::io::BufReader;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use rodio::Source;
|
||||
|
||||
fn main() {
|
||||
let iter_duration = Duration::from_secs(5);
|
||||
let iter_distance = 5.;
|
||||
|
||||
let refresh_duration = Duration::from_millis(10);
|
||||
|
||||
let num_steps = iter_duration.as_secs_f32() / refresh_duration.as_secs_f32();
|
||||
let step_distance = iter_distance / num_steps;
|
||||
let num_steps = num_steps as u32;
|
||||
|
||||
let repeats = 5;
|
||||
|
||||
let total_duration = iter_duration * 2 * repeats;
|
||||
|
||||
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
|
||||
let mut positions = ([0., 0., 0.], [-1., 0., 0.], [1., 0., 0.]);
|
||||
let sink = rodio::SpatialSink::try_new(&handle, positions.0, positions.1, positions.2).unwrap();
|
||||
|
||||
let file = std::fs::File::open("assets/music.ogg").unwrap();
|
||||
let source = rodio::Decoder::new(BufReader::new(file))
|
||||
.unwrap()
|
||||
.repeat_infinite()
|
||||
.take_duration(total_duration);
|
||||
sink.append(source);
|
||||
// A sound emitter playing the music starting at the centre gradually moves to the right
|
||||
// until it stops and begins traveling to the left, it will eventually pass through the
|
||||
// listener again and go to the far left.
|
||||
// This is repeated 5 times.
|
||||
for _ in 0..repeats {
|
||||
for _ in 0..num_steps {
|
||||
thread::sleep(refresh_duration);
|
||||
positions.0[0] += step_distance;
|
||||
sink.set_emitter_position(positions.0);
|
||||
}
|
||||
for _ in 0..(num_steps * 2) {
|
||||
thread::sleep(refresh_duration);
|
||||
positions.0[0] -= step_distance;
|
||||
sink.set_emitter_position(positions.0);
|
||||
}
|
||||
for _ in 0..num_steps {
|
||||
thread::sleep(refresh_duration);
|
||||
positions.0[0] += step_distance;
|
||||
sink.set_emitter_position(positions.0);
|
||||
}
|
||||
}
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
//! Plays a tone alternating between right and left ears, with right being first.
|
||||
use std::io::BufReader;
|
||||
|
||||
fn main() {
|
||||
let (_stream, handle) = rodio::OutputStream::try_default().unwrap();
|
||||
let sink = rodio::Sink::try_new(&handle).unwrap();
|
||||
|
||||
let file = std::fs::File::open("assets/RL.ogg").unwrap();
|
||||
sink.append(rodio::Decoder::new(BufReader::new(file)).unwrap());
|
||||
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
Vendored
-200
@@ -1,200 +0,0 @@
|
||||
//! A simple source of samples coming from a buffer.
|
||||
//!
|
||||
//! The `SamplesBuffer` struct can be used to treat a list of values as a `Source`.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use rodio::buffer::SamplesBuffer;
|
||||
//! let _ = SamplesBuffer::new(1, 44100, vec![1i16, 2, 3, 4, 5, 6]);
|
||||
//! ```
|
||||
//!
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::source::SeekError;
|
||||
use crate::{Sample, Source};
|
||||
|
||||
/// A buffer of samples treated as a source.
|
||||
pub struct SamplesBuffer<S> {
|
||||
data: Vec<S>,
|
||||
pos: usize,
|
||||
channels: u16,
|
||||
sample_rate: u32,
|
||||
duration: Duration,
|
||||
}
|
||||
|
||||
impl<S> SamplesBuffer<S>
|
||||
where
|
||||
S: Sample,
|
||||
{
|
||||
/// Builds a new `SamplesBuffer`.
|
||||
///
|
||||
/// # Panic
|
||||
///
|
||||
/// - Panics if the number of channels is zero.
|
||||
/// - Panics if the samples rate is zero.
|
||||
/// - Panics if the length of the buffer is larger than approximately 16 billion elements.
|
||||
/// This is because the calculation of the duration would overflow.
|
||||
///
|
||||
pub fn new<D>(channels: u16, sample_rate: u32, data: D) -> SamplesBuffer<S>
|
||||
where
|
||||
D: Into<Vec<S>>,
|
||||
{
|
||||
assert!(channels != 0);
|
||||
assert!(sample_rate != 0);
|
||||
|
||||
let data = data.into();
|
||||
let duration_ns = 1_000_000_000u64.checked_mul(data.len() as u64).unwrap()
|
||||
/ sample_rate as u64
|
||||
/ channels as u64;
|
||||
let duration = Duration::new(
|
||||
duration_ns / 1_000_000_000,
|
||||
(duration_ns % 1_000_000_000) as u32,
|
||||
);
|
||||
|
||||
SamplesBuffer {
|
||||
data,
|
||||
pos: 0,
|
||||
channels,
|
||||
sample_rate,
|
||||
duration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Source for SamplesBuffer<S>
|
||||
where
|
||||
S: Sample,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.channels
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
Some(self.duration)
|
||||
}
|
||||
|
||||
// this is fast because all the samples are in memory already
|
||||
// and due to the constant sample_rate we can jump to the right
|
||||
// sample directly
|
||||
//
|
||||
/// This jumps in memory till the sample for `pos`.
|
||||
#[inline]
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
let curr_channel = self.pos % self.channels() as usize;
|
||||
let new_pos = pos.as_secs_f32() * self.sample_rate() as f32 * self.channels() as f32;
|
||||
// saturate pos at the end of the source
|
||||
let new_pos = new_pos as usize;
|
||||
let new_pos = new_pos.min(self.data.len());
|
||||
|
||||
// make sure the next sample is for the right channel
|
||||
let new_pos = new_pos.next_multiple_of(self.channels() as usize);
|
||||
let new_pos = new_pos - curr_channel;
|
||||
|
||||
self.pos = new_pos;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Iterator for SamplesBuffer<S>
|
||||
where
|
||||
S: Sample,
|
||||
{
|
||||
type Item = S;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<S> {
|
||||
let sample = self.data.get(self.pos)?;
|
||||
self.pos += 1;
|
||||
Some(*sample)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(self.data.len(), Some(self.data.len()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::buffer::SamplesBuffer;
|
||||
use crate::source::Source;
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let _ = SamplesBuffer::new(1, 44100, vec![0i16, 0, 0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn panic_if_zero_channels() {
|
||||
SamplesBuffer::new(0, 44100, vec![0i16, 0, 0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn panic_if_zero_sample_rate() {
|
||||
SamplesBuffer::new(1, 0, vec![0i16, 0, 0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_basic() {
|
||||
let buf = SamplesBuffer::new(2, 2, vec![0i16, 0, 0, 0, 0, 0]);
|
||||
let dur = buf.total_duration().unwrap();
|
||||
assert_eq!(dur.as_secs(), 1);
|
||||
assert_eq!(dur.subsec_nanos(), 500_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iteration() {
|
||||
let mut buf = SamplesBuffer::new(1, 44100, vec![1i16, 2, 3, 4, 5, 6]);
|
||||
assert_eq!(buf.next(), Some(1));
|
||||
assert_eq!(buf.next(), Some(2));
|
||||
assert_eq!(buf.next(), Some(3));
|
||||
assert_eq!(buf.next(), Some(4));
|
||||
assert_eq!(buf.next(), Some(5));
|
||||
assert_eq!(buf.next(), Some(6));
|
||||
assert_eq!(buf.next(), None);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod try_seek {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn channel_order_stays_correct() {
|
||||
const SAMPLE_RATE: u32 = 100;
|
||||
const CHANNELS: u16 = 2;
|
||||
let mut buf = SamplesBuffer::new(
|
||||
CHANNELS,
|
||||
SAMPLE_RATE,
|
||||
(0..2000i16).into_iter().collect::<Vec<_>>(),
|
||||
);
|
||||
buf.try_seek(Duration::from_secs(5)).unwrap();
|
||||
assert_eq!(
|
||||
buf.next(),
|
||||
Some(5i16 * SAMPLE_RATE as i16 * CHANNELS as i16)
|
||||
);
|
||||
|
||||
assert!(buf.next().is_some_and(|s| s % 2 == 1));
|
||||
assert!(buf.next().is_some_and(|s| s % 2 == 0));
|
||||
|
||||
buf.try_seek(Duration::from_secs(6)).unwrap();
|
||||
assert!(buf.next().is_some_and(|s| s % 2 == 1),);
|
||||
}
|
||||
}
|
||||
}
|
||||
-181
@@ -1,181 +0,0 @@
|
||||
use cpal::Sample;
|
||||
|
||||
/// Iterator that converts from a certain channel count to another.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChannelCountConverter<I>
|
||||
where
|
||||
I: Iterator,
|
||||
{
|
||||
input: I,
|
||||
from: cpal::ChannelCount,
|
||||
to: cpal::ChannelCount,
|
||||
sample_repeat: Option<I::Item>,
|
||||
next_output_sample_pos: cpal::ChannelCount,
|
||||
}
|
||||
|
||||
impl<I> ChannelCountConverter<I>
|
||||
where
|
||||
I: Iterator,
|
||||
{
|
||||
/// Initializes the iterator.
|
||||
///
|
||||
/// # Panic
|
||||
///
|
||||
/// Panics if `from` or `to` are equal to 0.
|
||||
///
|
||||
#[inline]
|
||||
pub fn new(
|
||||
input: I,
|
||||
from: cpal::ChannelCount,
|
||||
to: cpal::ChannelCount,
|
||||
) -> ChannelCountConverter<I> {
|
||||
assert!(from >= 1);
|
||||
assert!(to >= 1);
|
||||
|
||||
ChannelCountConverter {
|
||||
input,
|
||||
from,
|
||||
to,
|
||||
sample_repeat: None,
|
||||
next_output_sample_pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Destroys this iterator and returns the underlying iterator.
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> I {
|
||||
self.input
|
||||
}
|
||||
|
||||
/// Get mutable access to the iterator
|
||||
#[inline]
|
||||
pub fn inner_mut(&mut self) -> &mut I {
|
||||
&mut self.input
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Iterator for ChannelCountConverter<I>
|
||||
where
|
||||
I: Iterator,
|
||||
I::Item: Sample,
|
||||
{
|
||||
type Item = I::Item;
|
||||
|
||||
fn next(&mut self) -> Option<I::Item> {
|
||||
let result = match self.next_output_sample_pos {
|
||||
0 => {
|
||||
// save first sample for mono -> stereo conversion
|
||||
let value = self.input.next();
|
||||
self.sample_repeat = value;
|
||||
value
|
||||
}
|
||||
x if x < self.from => self.input.next(),
|
||||
1 => self.sample_repeat,
|
||||
_ => Some(I::Item::EQUILIBRIUM),
|
||||
};
|
||||
|
||||
if result.is_some() {
|
||||
self.next_output_sample_pos += 1;
|
||||
}
|
||||
|
||||
if self.next_output_sample_pos == self.to {
|
||||
self.next_output_sample_pos = 0;
|
||||
|
||||
if self.from > self.to {
|
||||
for _ in self.to..self.from {
|
||||
self.input.next(); // discarding extra input
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let (min, max) = self.input.size_hint();
|
||||
|
||||
let consumed = std::cmp::min(self.from, self.next_output_sample_pos) as usize;
|
||||
let calculate = |size| {
|
||||
(size + consumed) / self.from as usize * self.to as usize
|
||||
- self.next_output_sample_pos as usize
|
||||
};
|
||||
|
||||
let min = calculate(min);
|
||||
let max = max.map(calculate);
|
||||
|
||||
(min, max)
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> ExactSizeIterator for ChannelCountConverter<I>
|
||||
where
|
||||
I: ExactSizeIterator,
|
||||
I::Item: Sample,
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ChannelCountConverter;
|
||||
|
||||
#[test]
|
||||
fn remove_channels() {
|
||||
let input = vec![1u16, 2, 3, 4, 5, 6];
|
||||
let output = ChannelCountConverter::new(input.into_iter(), 3, 2).collect::<Vec<_>>();
|
||||
assert_eq!(output, [1, 2, 4, 5]);
|
||||
|
||||
let input = vec![1u16, 2, 3, 4, 5, 6, 7, 8];
|
||||
let output = ChannelCountConverter::new(input.into_iter(), 4, 1).collect::<Vec<_>>();
|
||||
assert_eq!(output, [1, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_channels() {
|
||||
let input = vec![1i16, 2, 3, 4];
|
||||
let output = ChannelCountConverter::new(input.into_iter(), 1, 2).collect::<Vec<_>>();
|
||||
assert_eq!(output, [1, 1, 2, 2, 3, 3, 4, 4]);
|
||||
|
||||
let input = vec![1i16, 2];
|
||||
let output = ChannelCountConverter::new(input.into_iter(), 1, 4).collect::<Vec<_>>();
|
||||
assert_eq!(output, [1, 1, 0, 0, 2, 2, 0, 0]);
|
||||
|
||||
let input = vec![1i16, 2, 3, 4];
|
||||
let output = ChannelCountConverter::new(input.into_iter(), 2, 4).collect::<Vec<_>>();
|
||||
assert_eq!(output, [1, 2, 0, 0, 3, 4, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_hint() {
|
||||
fn test(input: &[i16], from: cpal::ChannelCount, to: cpal::ChannelCount) {
|
||||
let mut converter = ChannelCountConverter::new(input.iter().copied(), from, to);
|
||||
let count = converter.clone().count();
|
||||
for left_in_iter in (0..=count).rev() {
|
||||
println!("left_in_iter = {}", left_in_iter);
|
||||
assert_eq!(converter.size_hint(), (left_in_iter, Some(left_in_iter)));
|
||||
converter.next();
|
||||
}
|
||||
assert_eq!(converter.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
test(&[1i16, 2, 3], 1, 2);
|
||||
test(&[1i16, 2, 3, 4], 2, 4);
|
||||
test(&[1i16, 2, 3, 4], 4, 2);
|
||||
test(&[1i16, 2, 3, 4, 5, 6], 3, 8);
|
||||
test(&[1i16, 2, 3, 4, 5, 6, 7, 8], 4, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn len_more() {
|
||||
let input = vec![1i16, 2, 3, 4];
|
||||
let output = ChannelCountConverter::new(input.into_iter(), 2, 3);
|
||||
assert_eq!(output.len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn len_less() {
|
||||
let input = vec![1i16, 2, 3, 4];
|
||||
let output = ChannelCountConverter::new(input.into_iter(), 2, 1);
|
||||
assert_eq!(output.len(), 2);
|
||||
}
|
||||
}
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
/*!
|
||||
This module contains function that will convert from one PCM format to another.
|
||||
|
||||
This includes conversion between sample formats, channels or sample rates.
|
||||
|
||||
*/
|
||||
|
||||
pub use self::channels::ChannelCountConverter;
|
||||
pub use self::sample::DataConverter;
|
||||
pub use self::sample::Sample;
|
||||
pub use self::sample_rate::SampleRateConverter;
|
||||
|
||||
mod channels;
|
||||
// TODO: < shouldn't be public ; there's a bug in Rust 1.4 and below that makes This
|
||||
// `pub` mandatory
|
||||
pub mod sample;
|
||||
mod sample_rate;
|
||||
-159
@@ -1,159 +0,0 @@
|
||||
use cpal::{FromSample, Sample as CpalSample};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// Converts the samples data type to `O`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DataConverter<I, O> {
|
||||
input: I,
|
||||
marker: PhantomData<O>,
|
||||
}
|
||||
|
||||
impl<I, O> DataConverter<I, O> {
|
||||
/// Builds a new converter.
|
||||
#[inline]
|
||||
pub fn new(input: I) -> DataConverter<I, O> {
|
||||
DataConverter {
|
||||
input,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Destroys this iterator and returns the underlying iterator.
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> I {
|
||||
self.input
|
||||
}
|
||||
|
||||
/// get mutable access to the iterator
|
||||
#[inline]
|
||||
pub fn inner_mut(&mut self) -> &mut I {
|
||||
&mut self.input
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O> Iterator for DataConverter<I, O>
|
||||
where
|
||||
I: Iterator,
|
||||
I::Item: Sample,
|
||||
O: FromSample<I::Item> + Sample,
|
||||
{
|
||||
type Item = O;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<O> {
|
||||
self.input.next().map(|s| CpalSample::from_sample(s))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.input.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, O> ExactSizeIterator for DataConverter<I, O>
|
||||
where
|
||||
I: ExactSizeIterator,
|
||||
I::Item: Sample,
|
||||
O: FromSample<I::Item> + Sample,
|
||||
{
|
||||
}
|
||||
|
||||
/// Represents a value of a single sample.
|
||||
///
|
||||
/// This trait is implemented by default on three types: `i16`, `u16` and `f32`.
|
||||
///
|
||||
/// - For `i16`, silence corresponds to the value `0`. The minimum and maximum amplitudes are
|
||||
/// represented by `i16::min_value()` and `i16::max_value()` respectively.
|
||||
/// - For `u16`, silence corresponds to the value `u16::max_value() / 2`. The minimum and maximum
|
||||
/// amplitudes are represented by `0` and `u16::max_value()` respectively.
|
||||
/// - For `f32`, silence corresponds to the value `0.0`. The minimum and maximum amplitudes are
|
||||
/// represented by `-1.0` and `1.0` respectively.
|
||||
///
|
||||
/// You can implement this trait on your own type as well if you wish so.
|
||||
///
|
||||
pub trait Sample: CpalSample {
|
||||
/// Linear interpolation between two samples.
|
||||
///
|
||||
/// The result should be equal to
|
||||
/// `first * numerator / denominator + second * (1 - numerator / denominator)`.
|
||||
fn lerp(first: Self, second: Self, numerator: u32, denominator: u32) -> Self;
|
||||
/// Multiplies the value of this sample by the given amount.
|
||||
fn amplify(self, value: f32) -> Self;
|
||||
|
||||
/// Calls `saturating_add` on the sample.
|
||||
fn saturating_add(self, other: Self) -> Self;
|
||||
|
||||
/// Returns the value corresponding to the absence of sound.
|
||||
fn zero_value() -> Self;
|
||||
}
|
||||
|
||||
impl Sample for u16 {
|
||||
#[inline]
|
||||
fn lerp(first: u16, second: u16, numerator: u32, denominator: u32) -> u16 {
|
||||
let a = first as i32;
|
||||
let b = second as i32;
|
||||
let n = numerator as i32;
|
||||
let d = denominator as i32;
|
||||
(a + (b - a) * n / d) as u16
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn amplify(self, value: f32) -> u16 {
|
||||
((self as f32) * value) as u16
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn saturating_add(self, other: u16) -> u16 {
|
||||
self.saturating_add(other)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn zero_value() -> u16 {
|
||||
32768
|
||||
}
|
||||
}
|
||||
|
||||
impl Sample for i16 {
|
||||
#[inline]
|
||||
fn lerp(first: i16, second: i16, numerator: u32, denominator: u32) -> i16 {
|
||||
(first as i32 + (second as i32 - first as i32) * numerator as i32 / denominator as i32)
|
||||
as i16
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn amplify(self, value: f32) -> i16 {
|
||||
((self as f32) * value) as i16
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn saturating_add(self, other: i16) -> i16 {
|
||||
self.saturating_add(other)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn zero_value() -> i16 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl Sample for f32 {
|
||||
#[inline]
|
||||
fn lerp(first: f32, second: f32, numerator: u32, denominator: u32) -> f32 {
|
||||
first + (second - first) * numerator as f32 / denominator as f32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn amplify(self, value: f32) -> f32 {
|
||||
self * value
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn saturating_add(self, other: f32) -> f32 {
|
||||
self + other
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn zero_value() -> f32 {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
use crate::conversions::Sample;
|
||||
|
||||
use std::mem;
|
||||
|
||||
/// Iterator that converts from a certain sample rate to another.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SampleRateConverter<I>
|
||||
where
|
||||
I: Iterator,
|
||||
{
|
||||
/// The iterator that gives us samples.
|
||||
input: I,
|
||||
/// We convert chunks of `from` samples into chunks of `to` samples.
|
||||
from: u32,
|
||||
/// We convert chunks of `from` samples into chunks of `to` samples.
|
||||
to: u32,
|
||||
/// Number of channels in the stream
|
||||
channels: cpal::ChannelCount,
|
||||
/// One sample per channel, extracted from `input`.
|
||||
current_frame: Vec<I::Item>,
|
||||
/// Position of `current_sample` modulo `from`.
|
||||
current_frame_pos_in_chunk: u32,
|
||||
/// The samples right after `current_sample` (one per channel), extracted from `input`.
|
||||
next_frame: Vec<I::Item>,
|
||||
/// The position of the next sample that the iterator should return, modulo `to`.
|
||||
/// This counter is incremented (modulo `to`) every time the iterator is called.
|
||||
next_output_frame_pos_in_chunk: u32,
|
||||
/// The buffer containing the samples waiting to be output.
|
||||
output_buffer: Vec<I::Item>,
|
||||
}
|
||||
|
||||
impl<I> SampleRateConverter<I>
|
||||
where
|
||||
I: Iterator,
|
||||
I::Item: Sample,
|
||||
{
|
||||
///
|
||||
///
|
||||
/// # Panic
|
||||
///
|
||||
/// Panics if `from` or `to` are equal to 0.
|
||||
///
|
||||
#[inline]
|
||||
pub fn new(
|
||||
mut input: I,
|
||||
from: cpal::SampleRate,
|
||||
to: cpal::SampleRate,
|
||||
num_channels: cpal::ChannelCount,
|
||||
) -> SampleRateConverter<I> {
|
||||
let from = from.0;
|
||||
let to = to.0;
|
||||
|
||||
assert!(from >= 1);
|
||||
assert!(to >= 1);
|
||||
|
||||
// finding greatest common divisor
|
||||
let gcd = {
|
||||
#[inline]
|
||||
fn gcd(a: u32, b: u32) -> u32 {
|
||||
if b == 0 {
|
||||
a
|
||||
} else {
|
||||
gcd(b, a % b)
|
||||
}
|
||||
}
|
||||
|
||||
gcd(from, to)
|
||||
};
|
||||
|
||||
let (first_samples, next_samples) = if from == to {
|
||||
// if `from` == `to` == 1, then we just pass through
|
||||
debug_assert_eq!(from, gcd);
|
||||
(Vec::new(), Vec::new())
|
||||
} else {
|
||||
let first = input
|
||||
.by_ref()
|
||||
.take(num_channels as usize)
|
||||
.collect::<Vec<_>>();
|
||||
let next = input
|
||||
.by_ref()
|
||||
.take(num_channels as usize)
|
||||
.collect::<Vec<_>>();
|
||||
(first, next)
|
||||
};
|
||||
|
||||
SampleRateConverter {
|
||||
input,
|
||||
from: from / gcd,
|
||||
to: to / gcd,
|
||||
channels: num_channels,
|
||||
current_frame_pos_in_chunk: 0,
|
||||
next_output_frame_pos_in_chunk: 0,
|
||||
current_frame: first_samples,
|
||||
next_frame: next_samples,
|
||||
output_buffer: Vec::with_capacity(num_channels as usize - 1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Destroys this iterator and returns the underlying iterator.
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> I {
|
||||
self.input
|
||||
}
|
||||
|
||||
/// get mutable access to the iterator
|
||||
#[inline]
|
||||
pub fn inner_mut(&mut self) -> &mut I {
|
||||
&mut self.input
|
||||
}
|
||||
|
||||
fn next_input_frame(&mut self) {
|
||||
self.current_frame_pos_in_chunk += 1;
|
||||
|
||||
mem::swap(&mut self.current_frame, &mut self.next_frame);
|
||||
self.next_frame.clear();
|
||||
for _ in 0..self.channels {
|
||||
if let Some(i) = self.input.next() {
|
||||
self.next_frame.push(i);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Iterator for SampleRateConverter<I>
|
||||
where
|
||||
I: Iterator,
|
||||
I::Item: Sample + Clone,
|
||||
{
|
||||
type Item = I::Item;
|
||||
|
||||
fn next(&mut self) -> Option<I::Item> {
|
||||
// the algorithm below doesn't work if `self.from == self.to`
|
||||
if self.from == self.to {
|
||||
debug_assert_eq!(self.from, 1);
|
||||
return self.input.next();
|
||||
}
|
||||
|
||||
// Short circuit if there are some samples waiting.
|
||||
if !self.output_buffer.is_empty() {
|
||||
return Some(self.output_buffer.remove(0));
|
||||
}
|
||||
|
||||
// The frame we are going to return from this function will be a linear interpolation
|
||||
// between `self.current_frame` and `self.next_frame`.
|
||||
|
||||
if self.next_output_frame_pos_in_chunk == self.to {
|
||||
// If we jump to the next frame, we reset the whole state.
|
||||
self.next_output_frame_pos_in_chunk = 0;
|
||||
|
||||
self.next_input_frame();
|
||||
while self.current_frame_pos_in_chunk != self.from {
|
||||
self.next_input_frame();
|
||||
}
|
||||
self.current_frame_pos_in_chunk = 0;
|
||||
} else {
|
||||
// Finding the position of the first sample of the linear interpolation.
|
||||
let req_left_sample =
|
||||
(self.from * self.next_output_frame_pos_in_chunk / self.to) % self.from;
|
||||
|
||||
// Advancing `self.current_frame`, `self.next_frame` and
|
||||
// `self.current_frame_pos_in_chunk` until the latter variable
|
||||
// matches `req_left_sample`.
|
||||
while self.current_frame_pos_in_chunk != req_left_sample {
|
||||
self.next_input_frame();
|
||||
debug_assert!(self.current_frame_pos_in_chunk < self.from);
|
||||
}
|
||||
}
|
||||
|
||||
// Merging `self.current_frame` and `self.next_frame` into `self.output_buffer`.
|
||||
// Note that `self.output_buffer` can be truncated if there is not enough data in
|
||||
// `self.next_frame`.
|
||||
let mut result = None;
|
||||
let numerator = (self.from * self.next_output_frame_pos_in_chunk) % self.to;
|
||||
for (off, (cur, next)) in self
|
||||
.current_frame
|
||||
.iter()
|
||||
.zip(self.next_frame.iter())
|
||||
.enumerate()
|
||||
{
|
||||
let sample = Sample::lerp(*cur, *next, numerator, self.to);
|
||||
|
||||
if off == 0 {
|
||||
result = Some(sample);
|
||||
} else {
|
||||
self.output_buffer.push(sample);
|
||||
}
|
||||
}
|
||||
|
||||
// Incrementing the counter for the next iteration.
|
||||
self.next_output_frame_pos_in_chunk += 1;
|
||||
|
||||
if result.is_some() {
|
||||
result
|
||||
} else {
|
||||
// draining `self.current_frame`
|
||||
if !self.current_frame.is_empty() {
|
||||
let r = Some(self.current_frame.remove(0));
|
||||
mem::swap(&mut self.output_buffer, &mut self.current_frame);
|
||||
self.current_frame.clear();
|
||||
r
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let apply = |samples: usize| {
|
||||
// `samples_after_chunk` will contain the number of samples remaining after the chunk
|
||||
// currently being processed
|
||||
let samples_after_chunk = samples;
|
||||
// adding the samples of the next chunk that may have already been read
|
||||
let samples_after_chunk = if self.current_frame_pos_in_chunk == self.from - 1 {
|
||||
samples_after_chunk + self.next_frame.len()
|
||||
} else {
|
||||
samples_after_chunk
|
||||
};
|
||||
// removing the samples of the current chunk that have not yet been read
|
||||
let samples_after_chunk = samples_after_chunk.saturating_sub(
|
||||
self.from
|
||||
.saturating_sub(self.current_frame_pos_in_chunk + 2) as usize
|
||||
* usize::from(self.channels),
|
||||
);
|
||||
// calculating the number of samples after the transformation
|
||||
// TODO: this is wrong here \|/
|
||||
let samples_after_chunk = samples_after_chunk * self.to as usize / self.from as usize;
|
||||
|
||||
// `samples_current_chunk` will contain the number of samples remaining to be output
|
||||
// for the chunk currently being processed
|
||||
let samples_current_chunk = (self.to - self.next_output_frame_pos_in_chunk) as usize
|
||||
* usize::from(self.channels);
|
||||
|
||||
samples_current_chunk + samples_after_chunk + self.output_buffer.len()
|
||||
};
|
||||
|
||||
if self.from == self.to {
|
||||
self.input.size_hint()
|
||||
} else {
|
||||
let (min, max) = self.input.size_hint();
|
||||
(apply(min), max.map(apply))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> ExactSizeIterator for SampleRateConverter<I>
|
||||
where
|
||||
I: ExactSizeIterator,
|
||||
I::Item: Sample + Clone,
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::SampleRateConverter;
|
||||
use core::time::Duration;
|
||||
use cpal::SampleRate;
|
||||
use quickcheck::quickcheck;
|
||||
|
||||
// TODO: Remove once cpal 0.12.2 is released and the dependency is updated
|
||||
// (cpal#483 implemented ops::Mul on SampleRate)
|
||||
const fn multiply_rate(r: SampleRate, k: u32) -> SampleRate {
|
||||
SampleRate(k * r.0)
|
||||
}
|
||||
|
||||
quickcheck! {
|
||||
/// Check that resampling an empty input produces no output.
|
||||
fn empty(from: u32, to: u32, n: u16) -> () {
|
||||
let from = if from == 0 { return; } else { SampleRate(from) };
|
||||
let to = if to == 0 { return; } else { SampleRate(to) };
|
||||
if n == 0 { return; }
|
||||
|
||||
let input: Vec<u16> = Vec::new();
|
||||
let output =
|
||||
SampleRateConverter::new(input.into_iter(), from, to, n)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(output, []);
|
||||
}
|
||||
|
||||
/// Check that resampling to the same rate does not change the signal.
|
||||
fn identity(from: u32, n: u16, input: Vec<u16>) -> () {
|
||||
let from = if from == 0 { return; } else { SampleRate(from) };
|
||||
if n == 0 { return; }
|
||||
|
||||
let output =
|
||||
SampleRateConverter::new(input.clone().into_iter(), from, from, n)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(input, output);
|
||||
}
|
||||
|
||||
/// Check that dividing the sample rate by k (integer) is the same as
|
||||
/// dropping a sample from each channel.
|
||||
fn divide_sample_rate(to: u32, k: u32, input: Vec<u16>, n: u16) -> () {
|
||||
let to = if to == 0 { return; } else { SampleRate(to) };
|
||||
let from = multiply_rate(to, k);
|
||||
if k == 0 || n == 0 { return; }
|
||||
|
||||
// Truncate the input, so it contains an integer number of frames.
|
||||
let input = {
|
||||
let ns = n as usize;
|
||||
let mut i = input;
|
||||
i.truncate(ns * (i.len() / ns));
|
||||
i
|
||||
};
|
||||
|
||||
let output =
|
||||
SampleRateConverter::new(input.clone().into_iter(), from, to, n)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(input.chunks_exact(n.into())
|
||||
.step_by(k as usize).collect::<Vec<_>>().concat(),
|
||||
output)
|
||||
}
|
||||
|
||||
/// Check that, after multiplying the sample rate by k, every k-th
|
||||
/// sample in the output matches exactly with the input.
|
||||
fn multiply_sample_rate(from: u32, k: u32, input: Vec<u16>, n: u16) -> () {
|
||||
let from = if from == 0 { return; } else { SampleRate(from) };
|
||||
let to = multiply_rate(from, k);
|
||||
if k == 0 || n == 0 { return; }
|
||||
|
||||
// Truncate the input, so it contains an integer number of frames.
|
||||
let input = {
|
||||
let ns = n as usize;
|
||||
let mut i = input;
|
||||
i.truncate(ns * (i.len() / ns));
|
||||
i
|
||||
};
|
||||
|
||||
let output =
|
||||
SampleRateConverter::new(input.clone().into_iter(), from, to, n)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(input,
|
||||
output.chunks_exact(n.into())
|
||||
.step_by(k as usize).collect::<Vec<_>>().concat()
|
||||
)
|
||||
}
|
||||
|
||||
#[ignore]
|
||||
/// Check that resampling does not change the audio duration,
|
||||
/// except by a negligible amount (± 1ms). Reproduces #316.
|
||||
/// Ignored, pending a bug fix.
|
||||
fn preserve_durations(d: Duration, freq: f32, to: u32) -> () {
|
||||
use crate::source::{SineWave, Source};
|
||||
|
||||
let to = if to == 0 { return; } else { SampleRate(to) };
|
||||
let source = SineWave::new(freq).take_duration(d);
|
||||
let from = SampleRate(source.sample_rate());
|
||||
|
||||
let resampled =
|
||||
SampleRateConverter::new(source, from, to, 1);
|
||||
let duration =
|
||||
Duration::from_secs_f32(resampled.count() as f32 / to.0 as f32);
|
||||
|
||||
let delta = if d < duration { duration - d } else { d - duration };
|
||||
assert!(delta < Duration::from_millis(1),
|
||||
"Resampled duration ({:?}) is not close to original ({:?}); Δ = {:?}",
|
||||
duration, d, delta);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upsample() {
|
||||
let input = vec![2u16, 16, 4, 18, 6, 20, 8, 22];
|
||||
let output =
|
||||
SampleRateConverter::new(input.into_iter(), SampleRate(2000), SampleRate(3000), 2);
|
||||
assert_eq!(output.len(), 12);
|
||||
|
||||
let output = output.collect::<Vec<_>>();
|
||||
assert_eq!(output, [2, 16, 3, 17, 4, 18, 6, 20, 7, 21, 8, 22]);
|
||||
}
|
||||
}
|
||||
-144
@@ -1,144 +0,0 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::mem;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::source::SeekError;
|
||||
use crate::Source;
|
||||
|
||||
use claxon::FlacReader;
|
||||
|
||||
/// Decoder for the Flac format.
|
||||
pub struct FlacDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
reader: FlacReader<R>,
|
||||
current_block: Vec<i32>,
|
||||
current_block_channel_len: usize,
|
||||
current_block_off: usize,
|
||||
bits_per_sample: u32,
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
samples: Option<u64>,
|
||||
}
|
||||
|
||||
impl<R> FlacDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
/// Attempts to decode the data as Flac.
|
||||
pub fn new(mut data: R) -> Result<FlacDecoder<R>, R> {
|
||||
if !is_flac(data.by_ref()) {
|
||||
return Err(data);
|
||||
}
|
||||
|
||||
let reader = FlacReader::new(data).unwrap();
|
||||
let spec = reader.streaminfo();
|
||||
|
||||
Ok(FlacDecoder {
|
||||
reader,
|
||||
current_block: Vec::with_capacity(
|
||||
spec.max_block_size as usize * spec.channels as usize,
|
||||
),
|
||||
current_block_channel_len: 1,
|
||||
current_block_off: 0,
|
||||
bits_per_sample: spec.bits_per_sample,
|
||||
sample_rate: spec.sample_rate,
|
||||
channels: spec.channels as u16,
|
||||
samples: spec.samples,
|
||||
})
|
||||
}
|
||||
pub fn into_inner(self) -> R {
|
||||
self.reader.into_inner()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Source for FlacDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.channels
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
// `samples` in FLAC means "inter-channel samples" aka frames
|
||||
// so we do not divide by `self.channels` here.
|
||||
self.samples
|
||||
.map(|s| Duration::from_micros(s * 1_000_000 / self.sample_rate as u64))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> {
|
||||
Err(SeekError::NotSupported {
|
||||
underlying_source: std::any::type_name::<Self>(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Iterator for FlacDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
type Item = i16;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<i16> {
|
||||
loop {
|
||||
if self.current_block_off < self.current_block.len() {
|
||||
// Read from current block.
|
||||
let real_offset = (self.current_block_off % self.channels as usize)
|
||||
* self.current_block_channel_len
|
||||
+ self.current_block_off / self.channels as usize;
|
||||
let raw_val = self.current_block[real_offset];
|
||||
self.current_block_off += 1;
|
||||
let real_val = match self.bits_per_sample.cmp(&16) {
|
||||
Ordering::Less => (raw_val << (16 - self.bits_per_sample)) as i16,
|
||||
Ordering::Equal => raw_val as i16,
|
||||
Ordering::Greater => (raw_val >> (self.bits_per_sample - 16)) as i16,
|
||||
};
|
||||
return Some(real_val);
|
||||
}
|
||||
|
||||
// Load the next block.
|
||||
self.current_block_off = 0;
|
||||
let buffer = mem::take(&mut self.current_block);
|
||||
match self.reader.blocks().read_next_or_eof(buffer) {
|
||||
Ok(Some(block)) => {
|
||||
self.current_block_channel_len = (block.len() / block.channels()) as usize;
|
||||
self.current_block = block.into_buffer();
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the stream contains Flac data, then resets it to where it was.
|
||||
fn is_flac<R>(mut data: R) -> bool
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let stream_pos = data.stream_position().unwrap();
|
||||
|
||||
if FlacReader::new(data.by_ref()).is_err() {
|
||||
data.seek(SeekFrom::Start(stream_pos)).unwrap();
|
||||
return false;
|
||||
}
|
||||
|
||||
data.seek(SeekFrom::Start(stream_pos)).unwrap();
|
||||
true
|
||||
}
|
||||
-575
@@ -1,575 +0,0 @@
|
||||
//! Decodes samples from an audio file.
|
||||
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
#[allow(unused_imports)]
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::mem;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::source::SeekError;
|
||||
use crate::Source;
|
||||
|
||||
#[cfg(feature = "symphonia")]
|
||||
use self::read_seek_source::ReadSeekSource;
|
||||
#[cfg(feature = "symphonia")]
|
||||
use ::symphonia::core::io::{MediaSource, MediaSourceStream};
|
||||
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
mod flac;
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
mod mp3;
|
||||
#[cfg(feature = "symphonia")]
|
||||
mod read_seek_source;
|
||||
#[cfg(feature = "symphonia")]
|
||||
pub mod symphonia;
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
mod vorbis;
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
mod wav;
|
||||
|
||||
/// Source of audio samples from decoding a file.
|
||||
///
|
||||
/// Supports MP3, WAV, Vorbis and Flac.
|
||||
pub struct Decoder<R>(DecoderImpl<R>)
|
||||
where
|
||||
R: Read + Seek;
|
||||
|
||||
pub struct LoopedDecoder<R>(DecoderImpl<R>)
|
||||
where
|
||||
R: Read + Seek;
|
||||
|
||||
// can not really reduce the size of the VorbisDecoder. There are not any
|
||||
// arrays just a lot of struct fields.
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum DecoderImpl<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
Wav(wav::WavDecoder<R>),
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
Vorbis(vorbis::VorbisDecoder<R>),
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
Flac(flac::FlacDecoder<R>),
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
Mp3(mp3::Mp3Decoder<R>),
|
||||
#[cfg(feature = "symphonia")]
|
||||
Symphonia(symphonia::SymphoniaDecoder),
|
||||
None(::std::marker::PhantomData<R>),
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> DecoderImpl<R> {
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<f32> {
|
||||
match self {
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
DecoderImpl::Wav(source) => source.next(),
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
DecoderImpl::Vorbis(source) => source.next(),
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
DecoderImpl::Flac(source) => source.next(),
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
DecoderImpl::Mp3(source) => source.next(),
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderImpl::Symphonia(source) => source.next(),
|
||||
DecoderImpl::None(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
match self {
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
DecoderImpl::Wav(source) => source.size_hint(),
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
DecoderImpl::Vorbis(source) => source.size_hint(),
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
DecoderImpl::Flac(source) => source.size_hint(),
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
DecoderImpl::Mp3(source) => source.size_hint(),
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderImpl::Symphonia(source) => source.size_hint(),
|
||||
DecoderImpl::None(_) => (0, None),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
match self {
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
DecoderImpl::Wav(source) => source.current_frame_len(),
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
DecoderImpl::Vorbis(source) => source.current_frame_len(),
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
DecoderImpl::Flac(source) => source.current_frame_len(),
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
DecoderImpl::Mp3(source) => source.current_frame_len(),
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderImpl::Symphonia(source) => source.current_frame_len(),
|
||||
DecoderImpl::None(_) => Some(0),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
match self {
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
DecoderImpl::Wav(source) => source.channels(),
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
DecoderImpl::Vorbis(source) => source.channels(),
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
DecoderImpl::Flac(source) => source.channels(),
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
DecoderImpl::Mp3(source) => source.channels(),
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderImpl::Symphonia(source) => source.channels(),
|
||||
DecoderImpl::None(_) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
match self {
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
DecoderImpl::Wav(source) => source.sample_rate(),
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
DecoderImpl::Vorbis(source) => source.sample_rate(),
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
DecoderImpl::Flac(source) => source.sample_rate(),
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
DecoderImpl::Mp3(source) => source.sample_rate(),
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderImpl::Symphonia(source) => source.sample_rate(),
|
||||
DecoderImpl::None(_) => 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
match self {
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
DecoderImpl::Wav(source) => source.total_duration(),
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
DecoderImpl::Vorbis(source) => source.total_duration(),
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
DecoderImpl::Flac(source) => source.total_duration(),
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
DecoderImpl::Mp3(source) => source.total_duration(),
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderImpl::Symphonia(source) => source.total_duration(),
|
||||
DecoderImpl::None(_) => Some(Duration::default()),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
match self {
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
DecoderImpl::Wav(source) => source.try_seek(pos),
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
DecoderImpl::Vorbis(source) => source.try_seek(pos),
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
DecoderImpl::Flac(source) => source.try_seek(pos),
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
DecoderImpl::Mp3(source) => source.try_seek(pos),
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderImpl::Symphonia(source) => source.try_seek(pos),
|
||||
DecoderImpl::None(_) => Err(SeekError::NotSupported {
|
||||
underlying_source: "DecoderImpl::None",
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Decoder<R>
|
||||
where
|
||||
R: Read + Seek + Send + Sync + 'static,
|
||||
{
|
||||
/// Builds a new decoder.
|
||||
///
|
||||
/// Attempts to automatically detect the format of the source of data.
|
||||
#[allow(unused_variables)]
|
||||
pub fn new(data: R) -> Result<Decoder<R>, DecoderError> {
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
let data = match wav::WavDecoder::new(data) {
|
||||
Err(data) => data,
|
||||
Ok(decoder) => {
|
||||
return Ok(Decoder(DecoderImpl::Wav(decoder)));
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
let data = match flac::FlacDecoder::new(data) {
|
||||
Err(data) => data,
|
||||
Ok(decoder) => {
|
||||
return Ok(Decoder(DecoderImpl::Flac(decoder)));
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
let data = match vorbis::VorbisDecoder::new(data) {
|
||||
Err(data) => data,
|
||||
Ok(decoder) => {
|
||||
return Ok(Decoder(DecoderImpl::Vorbis(decoder)));
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
let data = match mp3::Mp3Decoder::new(data) {
|
||||
Err(data) => data,
|
||||
Ok(decoder) => {
|
||||
return Ok(Decoder(DecoderImpl::Mp3(decoder)));
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "symphonia")]
|
||||
{
|
||||
let mss = MediaSourceStream::new(
|
||||
Box::new(ReadSeekSource::new(data)) as Box<dyn MediaSource>,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
match symphonia::SymphoniaDecoder::new(mss, None) {
|
||||
Err(e) => Err(e),
|
||||
Ok(decoder) => Ok(Decoder(DecoderImpl::Symphonia(decoder))),
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "symphonia"))]
|
||||
Err(DecoderError::UnrecognizedFormat)
|
||||
}
|
||||
pub fn new_looped(data: R) -> Result<LoopedDecoder<R>, DecoderError> {
|
||||
Self::new(data).map(LoopedDecoder::new)
|
||||
}
|
||||
|
||||
/// Builds a new decoder from wav data.
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
pub fn new_wav(data: R) -> Result<Decoder<R>, DecoderError> {
|
||||
match wav::WavDecoder::new(data) {
|
||||
Err(_) => Err(DecoderError::UnrecognizedFormat),
|
||||
Ok(decoder) => Ok(Decoder(DecoderImpl::Wav(decoder))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a new decoder from wav data.
|
||||
#[cfg(feature = "symphonia-wav")]
|
||||
pub fn new_wav(data: R) -> Result<Decoder<R>, DecoderError> {
|
||||
Decoder::new_symphonia(data, "wav")
|
||||
}
|
||||
|
||||
/// Builds a new decoder from flac data.
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
pub fn new_flac(data: R) -> Result<Decoder<R>, DecoderError> {
|
||||
match flac::FlacDecoder::new(data) {
|
||||
Err(_) => Err(DecoderError::UnrecognizedFormat),
|
||||
Ok(decoder) => Ok(Decoder(DecoderImpl::Flac(decoder))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a new decoder from flac data.
|
||||
#[cfg(feature = "symphonia-flac")]
|
||||
pub fn new_flac(data: R) -> Result<Decoder<R>, DecoderError> {
|
||||
Decoder::new_symphonia(data, "flac")
|
||||
}
|
||||
|
||||
/// Builds a new decoder from vorbis data.
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
pub fn new_vorbis(data: R) -> Result<Decoder<R>, DecoderError> {
|
||||
match vorbis::VorbisDecoder::new(data) {
|
||||
Err(_) => Err(DecoderError::UnrecognizedFormat),
|
||||
Ok(decoder) => Ok(Decoder(DecoderImpl::Vorbis(decoder))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a new decoder from vorbis data.
|
||||
#[cfg(feature = "symphonia-vorbis")]
|
||||
pub fn new_vorbis(data: R) -> Result<Decoder<R>, DecoderError> {
|
||||
Decoder::new_symphonia(data, "ogg")
|
||||
}
|
||||
|
||||
/// Builds a new decoder from mp3 data.
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
pub fn new_mp3(data: R) -> Result<Decoder<R>, DecoderError> {
|
||||
match mp3::Mp3Decoder::new(data) {
|
||||
Err(_) => Err(DecoderError::UnrecognizedFormat),
|
||||
Ok(decoder) => Ok(Decoder(DecoderImpl::Mp3(decoder))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a new decoder from mp3 data.
|
||||
#[cfg(feature = "symphonia-mp3")]
|
||||
pub fn new_mp3(data: R) -> Result<Decoder<R>, DecoderError> {
|
||||
Decoder::new_symphonia(data, "mp3")
|
||||
}
|
||||
|
||||
/// Builds a new decoder from aac data.
|
||||
#[cfg(feature = "symphonia-aac")]
|
||||
pub fn new_aac(data: R) -> Result<Decoder<R>, DecoderError> {
|
||||
Decoder::new_symphonia(data, "aac")
|
||||
}
|
||||
|
||||
/// Builds a new decoder from mp4 data.
|
||||
#[cfg(feature = "symphonia-isomp4")]
|
||||
pub fn new_mp4(data: R, hint: Mp4Type) -> Result<Decoder<R>, DecoderError> {
|
||||
Decoder::new_symphonia(data, &hint.to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "symphonia")]
|
||||
fn new_symphonia(data: R, hint: &str) -> Result<Decoder<R>, DecoderError> {
|
||||
let mss = MediaSourceStream::new(
|
||||
Box::new(ReadSeekSource::new(data)) as Box<dyn MediaSource>,
|
||||
Default::default(),
|
||||
);
|
||||
|
||||
match symphonia::SymphoniaDecoder::new(mss, Some(hint)) {
|
||||
Err(e) => Err(e),
|
||||
Ok(decoder) => Ok(Decoder(DecoderImpl::Symphonia(decoder))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Mp4Type {
|
||||
Mp4,
|
||||
M4a,
|
||||
M4p,
|
||||
M4b,
|
||||
M4r,
|
||||
M4v,
|
||||
Mov,
|
||||
}
|
||||
|
||||
impl FromStr for Mp4Type {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(input: &str) -> Result<Mp4Type, Self::Err> {
|
||||
match &input.to_lowercase()[..] {
|
||||
"mp4" => Ok(Mp4Type::Mp4),
|
||||
"m4a" => Ok(Mp4Type::M4a),
|
||||
"m4p" => Ok(Mp4Type::M4p),
|
||||
"m4b" => Ok(Mp4Type::M4b),
|
||||
"m4r" => Ok(Mp4Type::M4r),
|
||||
"m4v" => Ok(Mp4Type::M4v),
|
||||
"mov" => Ok(Mp4Type::Mov),
|
||||
_ => Err(format!("{} is not a valid mp4 extension", input)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Mp4Type {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let text = match self {
|
||||
Mp4Type::Mp4 => "mp4",
|
||||
Mp4Type::M4a => "m4a",
|
||||
Mp4Type::M4p => "m4p",
|
||||
Mp4Type::M4b => "m4b",
|
||||
Mp4Type::M4r => "m4r",
|
||||
Mp4Type::M4v => "m4v",
|
||||
Mp4Type::Mov => "mov",
|
||||
};
|
||||
write!(f, "{}", text)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> LoopedDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
fn new(decoder: Decoder<R>) -> LoopedDecoder<R> {
|
||||
Self(decoder.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Iterator for Decoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
type Item = f32;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<f32> {
|
||||
self.0.next()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.0.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Source for Decoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
self.0.current_frame_len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.0.channels()
|
||||
}
|
||||
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.0.sample_rate()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
self.0.total_duration()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
self.0.try_seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Iterator for LoopedDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
type Item = f32;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<f32> {
|
||||
if let Some(sample) = self.0.next() {
|
||||
Some(sample)
|
||||
} else {
|
||||
let decoder = mem::replace(&mut self.0, DecoderImpl::None(Default::default()));
|
||||
let (decoder, sample) = match decoder {
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
DecoderImpl::Wav(source) => {
|
||||
let mut reader = source.into_inner();
|
||||
reader.seek(SeekFrom::Start(0)).ok()?;
|
||||
let mut source = wav::WavDecoder::new(reader).ok()?;
|
||||
let sample = source.next();
|
||||
(DecoderImpl::Wav(source), sample)
|
||||
}
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
DecoderImpl::Vorbis(source) => {
|
||||
use lewton::inside_ogg::OggStreamReader;
|
||||
let mut reader = source.into_inner().into_inner();
|
||||
reader.seek_bytes(SeekFrom::Start(0)).ok()?;
|
||||
let mut source = vorbis::VorbisDecoder::from_stream_reader(
|
||||
OggStreamReader::from_ogg_reader(reader).ok()?,
|
||||
);
|
||||
let sample = source.next();
|
||||
(DecoderImpl::Vorbis(source), sample)
|
||||
}
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
DecoderImpl::Flac(source) => {
|
||||
let mut reader = source.into_inner();
|
||||
reader.seek(SeekFrom::Start(0)).ok()?;
|
||||
let mut source = flac::FlacDecoder::new(reader).ok()?;
|
||||
let sample = source.next();
|
||||
(DecoderImpl::Flac(source), sample)
|
||||
}
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
DecoderImpl::Mp3(source) => {
|
||||
let mut reader = source.into_inner();
|
||||
reader.seek(SeekFrom::Start(0)).ok()?;
|
||||
let mut source = mp3::Mp3Decoder::new(reader).ok()?;
|
||||
let sample = source.next();
|
||||
(DecoderImpl::Mp3(source), sample)
|
||||
}
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderImpl::Symphonia(source) => {
|
||||
let mut reader = source.into_inner();
|
||||
reader.seek(SeekFrom::Start(0)).ok()?;
|
||||
let mut source = symphonia::SymphoniaDecoder::new(reader, None).ok()?;
|
||||
let sample = source.next();
|
||||
(DecoderImpl::Symphonia(source), sample)
|
||||
}
|
||||
none @ DecoderImpl::None(_) => (none, None),
|
||||
};
|
||||
self.0 = decoder;
|
||||
sample
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.0.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Source for LoopedDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
self.0.current_frame_len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.0.channels()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.0.sample_rate()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
self.0.try_seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error that can happen when creating a decoder.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DecoderError {
|
||||
/// The format of the data has not been recognized.
|
||||
UnrecognizedFormat,
|
||||
|
||||
/// An IO error occurred while reading, writing, or seeking the stream.
|
||||
#[cfg(feature = "symphonia")]
|
||||
IoError(String),
|
||||
|
||||
/// The stream contained malformed data and could not be decoded or demuxed.
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecodeError(&'static str),
|
||||
|
||||
/// A default or user-defined limit was reached while decoding or demuxing the stream. Limits
|
||||
/// are used to prevent denial-of-service attacks from malicious streams.
|
||||
#[cfg(feature = "symphonia")]
|
||||
LimitError(&'static str),
|
||||
|
||||
/// The demuxer or decoder needs to be reset before continuing.
|
||||
#[cfg(feature = "symphonia")]
|
||||
ResetRequired,
|
||||
|
||||
/// No streams were found by the decoder
|
||||
#[cfg(feature = "symphonia")]
|
||||
NoStreams,
|
||||
}
|
||||
|
||||
impl fmt::Display for DecoderError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let text = match self {
|
||||
DecoderError::UnrecognizedFormat => "Unrecognized format",
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderError::IoError(msg) => &msg[..],
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderError::DecodeError(msg) => msg,
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderError::LimitError(msg) => msg,
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderError::ResetRequired => "Reset required",
|
||||
#[cfg(feature = "symphonia")]
|
||||
DecoderError::NoStreams => "No streams",
|
||||
};
|
||||
write!(f, "{}", text)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for DecoderError {}
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::source::SeekError;
|
||||
use crate::Source;
|
||||
|
||||
use minimp3::Decoder;
|
||||
use minimp3::Frame;
|
||||
use minimp3_fixed as minimp3;
|
||||
|
||||
pub struct Mp3Decoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
// decoder: SeekDecoder<R>,
|
||||
decoder: Decoder<R>,
|
||||
current_frame: Frame,
|
||||
current_frame_offset: usize,
|
||||
}
|
||||
|
||||
impl<R> Mp3Decoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
pub fn new(mut data: R) -> Result<Self, R> {
|
||||
if !is_mp3(data.by_ref()) {
|
||||
return Err(data);
|
||||
}
|
||||
// let mut decoder = SeekDecoder::new(data)
|
||||
let mut decoder = Decoder::new(data);
|
||||
// parameters are correct and minimp3 is used correctly
|
||||
// thus if we crash here one of these invariants is broken:
|
||||
// .expect("should be able to allocate memory, perform IO");
|
||||
// let current_frame = decoder.decode_frame()
|
||||
let current_frame = decoder.next_frame()
|
||||
// the reader makes enough data available therefore
|
||||
// if we crash here the invariant broken is:
|
||||
.expect("data should not corrupt");
|
||||
|
||||
Ok(Mp3Decoder {
|
||||
decoder,
|
||||
current_frame,
|
||||
current_frame_offset: 0,
|
||||
})
|
||||
}
|
||||
pub fn into_inner(self) -> R {
|
||||
self.decoder.into_inner()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Source for Mp3Decoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
Some(self.current_frame.data.len())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.current_frame.channels as _
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.current_frame.sample_rate as _
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
// TODO waiting for PR in minimp3_fixed or minimp3
|
||||
|
||||
// let pos = (pos.as_secs_f32() * self.sample_rate() as f32) as u64;
|
||||
// // do not trigger a sample_rate, channels and frame len update
|
||||
// // as the seek only takes effect after the current frame is done
|
||||
// self.decoder.seek_samples(pos)?;
|
||||
// Ok(())
|
||||
|
||||
Err(SeekError::NotSupported {
|
||||
underlying_source: std::any::type_name::<Self>(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Iterator for Mp3Decoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
type Item = i16;
|
||||
|
||||
fn next(&mut self) -> Option<i16> {
|
||||
if self.current_frame_offset == self.current_frame_len().unwrap() {
|
||||
if let Ok(frame) = self.decoder.next_frame() {
|
||||
// if let Ok(frame) = self.decoder.decode_frame() {
|
||||
self.current_frame = frame;
|
||||
self.current_frame_offset = 0;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let v = self.current_frame.data[self.current_frame_offset];
|
||||
self.current_frame_offset += 1;
|
||||
|
||||
Some(v)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the stream contains mp3 data, then resets it to where it was.
|
||||
fn is_mp3<R>(mut data: R) -> bool
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let stream_pos = data.seek(SeekFrom::Current(0)).unwrap();
|
||||
let mut decoder = Decoder::new(data.by_ref());
|
||||
let ok = decoder.next_frame().is_ok();
|
||||
data.seek(SeekFrom::Start(stream_pos)).unwrap();
|
||||
|
||||
ok
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
use std::io::{Read, Result, Seek, SeekFrom};
|
||||
|
||||
use symphonia::core::io::MediaSource;
|
||||
|
||||
pub struct ReadSeekSource<T: Read + Seek + Send + Sync> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<T: Read + Seek + Send + Sync> ReadSeekSource<T> {
|
||||
/// Instantiates a new `ReadSeekSource<T>` by taking ownership and wrapping the provided
|
||||
/// `Read + Seek`er.
|
||||
pub fn new(inner: T) -> Self {
|
||||
ReadSeekSource { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Read + Seek + Send + Sync> MediaSource for ReadSeekSource<T> {
|
||||
fn is_seekable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn byte_len(&self) -> Option<u64> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Read + Seek + Send + Sync> Read for ReadSeekSource<T> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
|
||||
self.inner.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Read + Seek + Send + Sync> Seek for ReadSeekSource<T> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
|
||||
self.inner.seek(pos)
|
||||
}
|
||||
}
|
||||
-291
@@ -1,291 +0,0 @@
|
||||
use std::time::Duration;
|
||||
use symphonia::{
|
||||
core::{
|
||||
audio::{AudioBufferRef, SampleBuffer, SignalSpec},
|
||||
codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL},
|
||||
errors::Error,
|
||||
formats::{FormatOptions, FormatReader, SeekedTo},
|
||||
io::MediaSourceStream,
|
||||
meta::MetadataOptions,
|
||||
probe::Hint,
|
||||
units::{self, Time},
|
||||
},
|
||||
default::get_probe,
|
||||
};
|
||||
|
||||
use crate::{source, Source};
|
||||
|
||||
use super::DecoderError;
|
||||
|
||||
// Decoder errors are not considered fatal.
|
||||
// The correct action is to just get a new packet and try again.
|
||||
// But a decode error in more than 3 consecutive packets is fatal.
|
||||
const MAX_DECODE_RETRIES: usize = 3;
|
||||
|
||||
pub(crate) struct SymphoniaDecoder {
|
||||
decoder: Box<dyn Decoder>,
|
||||
current_frame_offset: usize,
|
||||
format: Box<dyn FormatReader>,
|
||||
total_duration: Option<Time>,
|
||||
buffer: SampleBuffer<f32>,
|
||||
spec: SignalSpec,
|
||||
}
|
||||
|
||||
impl SymphoniaDecoder {
|
||||
pub(crate) fn new(
|
||||
mss: MediaSourceStream,
|
||||
extension: Option<&str>,
|
||||
) -> Result<Self, DecoderError> {
|
||||
match SymphoniaDecoder::init(mss, extension) {
|
||||
Err(e) => match e {
|
||||
Error::IoError(e) => Err(DecoderError::IoError(e.to_string())),
|
||||
Error::DecodeError(e) => Err(DecoderError::DecodeError(e)),
|
||||
Error::SeekError(_) => {
|
||||
unreachable!("Seek errors should not occur during initialization")
|
||||
}
|
||||
Error::Unsupported(_) => Err(DecoderError::UnrecognizedFormat),
|
||||
Error::LimitError(e) => Err(DecoderError::LimitError(e)),
|
||||
Error::ResetRequired => Err(DecoderError::ResetRequired),
|
||||
},
|
||||
Ok(Some(decoder)) => Ok(decoder),
|
||||
Ok(None) => Err(DecoderError::NoStreams),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_inner(self) -> MediaSourceStream {
|
||||
self.format.into_inner()
|
||||
}
|
||||
|
||||
fn init(
|
||||
mss: MediaSourceStream,
|
||||
extension: Option<&str>,
|
||||
) -> symphonia::core::errors::Result<Option<SymphoniaDecoder>> {
|
||||
let mut hint = Hint::new();
|
||||
if let Some(ext) = extension {
|
||||
hint.with_extension(ext);
|
||||
}
|
||||
let format_opts: FormatOptions = FormatOptions {
|
||||
enable_gapless: true,
|
||||
..Default::default()
|
||||
};
|
||||
let metadata_opts: MetadataOptions = Default::default();
|
||||
let mut probed = get_probe().format(&hint, mss, &format_opts, &metadata_opts)?;
|
||||
|
||||
let stream = match probed.format.default_track() {
|
||||
Some(stream) => stream,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Select the first supported track
|
||||
let track_id = probed
|
||||
.format
|
||||
.tracks()
|
||||
.iter()
|
||||
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
|
||||
.ok_or(symphonia::core::errors::Error::Unsupported(
|
||||
"No track with supported codec",
|
||||
))?
|
||||
.id;
|
||||
|
||||
let track = probed
|
||||
.format
|
||||
.tracks()
|
||||
.iter()
|
||||
.find(|track| track.id == track_id)
|
||||
.unwrap();
|
||||
|
||||
let mut decoder = symphonia::default::get_codecs()
|
||||
.make(&track.codec_params, &DecoderOptions::default())?;
|
||||
let total_duration = stream
|
||||
.codec_params
|
||||
.time_base
|
||||
.zip(stream.codec_params.n_frames)
|
||||
.map(|(base, frames)| base.calc_time(frames));
|
||||
|
||||
let mut decode_errors: usize = 0;
|
||||
let decoded = loop {
|
||||
let current_frame = match probed.format.next_packet() {
|
||||
Ok(packet) => packet,
|
||||
Err(Error::IoError(_)) => break decoder.last_decoded(),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// If the packet does not belong to the selected track, skip over it
|
||||
if current_frame.track_id() != track_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
match decoder.decode(¤t_frame) {
|
||||
Ok(decoded) => break decoded,
|
||||
Err(e) => match e {
|
||||
Error::DecodeError(_) => {
|
||||
decode_errors += 1;
|
||||
if decode_errors > MAX_DECODE_RETRIES {
|
||||
return Err(e);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => return Err(e),
|
||||
},
|
||||
}
|
||||
};
|
||||
let spec = decoded.spec().to_owned();
|
||||
let buffer = SymphoniaDecoder::get_buffer(decoded, &spec);
|
||||
Ok(Some(SymphoniaDecoder {
|
||||
decoder,
|
||||
current_frame_offset: 0,
|
||||
format: probed.format,
|
||||
total_duration,
|
||||
buffer,
|
||||
spec,
|
||||
}))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_buffer(decoded: AudioBufferRef, spec: &SignalSpec) -> SampleBuffer<f32> {
|
||||
let duration = units::Duration::from(decoded.capacity() as u64);
|
||||
let mut buffer = SampleBuffer::<f32>::new(duration, *spec);
|
||||
buffer.copy_interleaved_ref(decoded);
|
||||
buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl Source for SymphoniaDecoder {
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
Some(self.buffer.samples().len())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.spec.channels.count() as u16
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.spec.rate
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
self.total_duration
|
||||
.map(|Time { seconds, frac }| Duration::new(seconds, (1f64 / frac) as u32))
|
||||
}
|
||||
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), source::SeekError> {
|
||||
use symphonia::core::formats::{SeekMode, SeekTo};
|
||||
|
||||
let seek_beyond_end = self
|
||||
.total_duration()
|
||||
.is_some_and(|dur| dur.saturating_sub(pos).as_millis() < 1);
|
||||
|
||||
let time = if seek_beyond_end {
|
||||
let time = self.total_duration.expect("if guarantees this is Some");
|
||||
skip_back_a_tiny_bit(time) // some decoders can only seek to just before the end
|
||||
} else {
|
||||
pos.as_secs_f64().into()
|
||||
};
|
||||
|
||||
// make sure the next sample is for the right channel
|
||||
let to_skip = self.current_frame_offset % self.channels() as usize;
|
||||
|
||||
let seek_res = self
|
||||
.format
|
||||
.seek(
|
||||
SeekMode::Accurate,
|
||||
SeekTo::Time {
|
||||
time,
|
||||
track_id: None,
|
||||
},
|
||||
)
|
||||
.map_err(SeekError::BaseSeek)?;
|
||||
|
||||
self.refine_position(seek_res)?;
|
||||
self.current_frame_offset += to_skip;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum SeekError {
|
||||
#[error("Could not get next packet while refining seek position: {0:?}")]
|
||||
Refining(symphonia::core::errors::Error),
|
||||
#[error("Format reader failed to seek: {0:?}")]
|
||||
BaseSeek(symphonia::core::errors::Error),
|
||||
#[error("Decoding failed retrying on the next packet failed: {0:?}")]
|
||||
Retrying(symphonia::core::errors::Error),
|
||||
#[error("Decoding failed on multiple consecutive packets: {0:?}")]
|
||||
Decoding(symphonia::core::errors::Error),
|
||||
}
|
||||
|
||||
impl SymphoniaDecoder {
|
||||
/// note frame offset must be set after
|
||||
fn refine_position(&mut self, seek_res: SeekedTo) -> Result<(), source::SeekError> {
|
||||
let mut samples_to_pass = seek_res.required_ts - seek_res.actual_ts;
|
||||
let packet = loop {
|
||||
let candidate = self.format.next_packet().map_err(SeekError::Refining)?;
|
||||
if candidate.dur() > samples_to_pass {
|
||||
break candidate;
|
||||
} else {
|
||||
samples_to_pass -= candidate.dur();
|
||||
}
|
||||
};
|
||||
|
||||
let mut decoded = self.decoder.decode(&packet);
|
||||
for _ in 0..MAX_DECODE_RETRIES {
|
||||
if decoded.is_err() {
|
||||
let packet = self.format.next_packet().map_err(SeekError::Retrying)?;
|
||||
decoded = self.decoder.decode(&packet);
|
||||
}
|
||||
}
|
||||
|
||||
let decoded = decoded.map_err(SeekError::Decoding)?;
|
||||
decoded.spec().clone_into(&mut self.spec);
|
||||
self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec);
|
||||
self.current_frame_offset = samples_to_pass as usize * self.channels() as usize;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_back_a_tiny_bit(
|
||||
Time {
|
||||
mut seconds,
|
||||
mut frac,
|
||||
}: Time,
|
||||
) -> Time {
|
||||
frac -= 0.0001;
|
||||
if frac < 0.0 {
|
||||
seconds = seconds.saturating_sub(1);
|
||||
frac = 1.0 - frac;
|
||||
}
|
||||
Time { seconds, frac }
|
||||
}
|
||||
|
||||
impl Iterator for SymphoniaDecoder {
|
||||
type Item = f32;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<f32> {
|
||||
if self.current_frame_offset >= self.buffer.len() {
|
||||
let packet = self.format.next_packet().ok()?;
|
||||
let mut decoded = self.decoder.decode(&packet);
|
||||
for _ in 0..MAX_DECODE_RETRIES {
|
||||
if decoded.is_err() {
|
||||
let packet = self.format.next_packet().ok()?;
|
||||
decoded = self.decoder.decode(&packet);
|
||||
}
|
||||
}
|
||||
let decoded = decoded.ok()?;
|
||||
decoded.spec().clone_into(&mut self.spec);
|
||||
self.buffer = SymphoniaDecoder::get_buffer(decoded, &self.spec);
|
||||
self.current_frame_offset = 0;
|
||||
}
|
||||
|
||||
let sample = *self.buffer.samples().get(self.current_frame_offset)?;
|
||||
self.current_frame_offset += 1;
|
||||
|
||||
Some(sample)
|
||||
}
|
||||
}
|
||||
-144
@@ -1,144 +0,0 @@
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::source::SeekError;
|
||||
use crate::Source;
|
||||
|
||||
use lewton::inside_ogg::OggStreamReader;
|
||||
|
||||
/// Decoder for an OGG file that contains Vorbis sound format.
|
||||
pub struct VorbisDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
stream_reader: OggStreamReader<R>,
|
||||
current_data: Vec<i16>,
|
||||
next: usize,
|
||||
}
|
||||
|
||||
impl<R> VorbisDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
/// Attempts to decode the data as ogg/vorbis.
|
||||
pub fn new(mut data: R) -> Result<VorbisDecoder<R>, R> {
|
||||
if !is_vorbis(data.by_ref()) {
|
||||
return Err(data);
|
||||
}
|
||||
|
||||
let stream_reader = OggStreamReader::new(data).unwrap();
|
||||
Ok(Self::from_stream_reader(stream_reader))
|
||||
}
|
||||
pub fn from_stream_reader(mut stream_reader: OggStreamReader<R>) -> Self {
|
||||
let mut data = match stream_reader.read_dec_packet_itl() {
|
||||
Ok(Some(d)) => d,
|
||||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
// The first packet is always empty, therefore
|
||||
// we need to read the second frame to get some data
|
||||
if let Ok(Some(mut d)) = stream_reader.read_dec_packet_itl() {
|
||||
data.append(&mut d);
|
||||
}
|
||||
|
||||
VorbisDecoder {
|
||||
stream_reader,
|
||||
current_data: data,
|
||||
next: 0,
|
||||
}
|
||||
}
|
||||
pub fn into_inner(self) -> OggStreamReader<R> {
|
||||
self.stream_reader
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Source for VorbisDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
Some(self.current_data.len())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.stream_reader.ident_hdr.audio_channels as u16
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.stream_reader.ident_hdr.audio_sample_rate
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
|
||||
/// seek is broken, https://github.com/RustAudio/lewton/issues/73.
|
||||
// We could work around it by:
|
||||
// - using unsafe to create an instance of Self
|
||||
// - use mem::swap to turn the &mut self into a mut self
|
||||
// - take out the underlying Read+Seek
|
||||
// - make a new self and seek
|
||||
//
|
||||
// If this issue is fixed use the implementation in
|
||||
// commit: 3bafe32388b4eb7a48c6701e6c65044dc8c555e6
|
||||
#[inline]
|
||||
fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> {
|
||||
Err(SeekError::NotSupported {
|
||||
underlying_source: std::any::type_name::<Self>(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Iterator for VorbisDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
type Item = i16;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<i16> {
|
||||
if let Some(sample) = self.current_data.get(self.next).copied() {
|
||||
self.next += 1;
|
||||
if self.current_data.is_empty() {
|
||||
if let Ok(Some(data)) = self.stream_reader.read_dec_packet_itl() {
|
||||
self.current_data = data;
|
||||
self.next = 0;
|
||||
}
|
||||
}
|
||||
Some(sample)
|
||||
} else {
|
||||
if let Ok(Some(data)) = self.stream_reader.read_dec_packet_itl() {
|
||||
self.current_data = data;
|
||||
self.next = 0;
|
||||
}
|
||||
let sample = self.current_data.get(self.next).copied();
|
||||
self.next += 1;
|
||||
sample
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(self.current_data.len(), None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the stream contains Vorbis data, then resets it to where it was.
|
||||
fn is_vorbis<R>(mut data: R) -> bool
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let stream_pos = data.stream_position().unwrap();
|
||||
|
||||
if OggStreamReader::new(data.by_ref()).is_err() {
|
||||
data.seek(SeekFrom::Start(stream_pos)).unwrap();
|
||||
return false;
|
||||
}
|
||||
|
||||
data.seek(SeekFrom::Start(stream_pos)).unwrap();
|
||||
true
|
||||
}
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::source::SeekError;
|
||||
use crate::Source;
|
||||
|
||||
use hound::{SampleFormat, WavReader};
|
||||
|
||||
/// Decoder for the WAV format.
|
||||
pub struct WavDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
reader: SamplesIterator<R>,
|
||||
total_duration: Duration,
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
}
|
||||
|
||||
impl<R> WavDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
/// Attempts to decode the data as WAV.
|
||||
pub fn new(mut data: R) -> Result<WavDecoder<R>, R> {
|
||||
if !is_wave(data.by_ref()) {
|
||||
return Err(data);
|
||||
}
|
||||
|
||||
let reader = WavReader::new(data).unwrap();
|
||||
let spec = reader.spec();
|
||||
let len = reader.len() as u64;
|
||||
let reader = SamplesIterator {
|
||||
reader,
|
||||
samples_read: 0,
|
||||
};
|
||||
|
||||
let sample_rate = spec.sample_rate;
|
||||
let channels = spec.channels;
|
||||
let total_duration =
|
||||
Duration::from_micros((1_000_000 * len) / (sample_rate as u64 * channels as u64));
|
||||
|
||||
Ok(WavDecoder {
|
||||
reader,
|
||||
total_duration,
|
||||
sample_rate,
|
||||
channels,
|
||||
})
|
||||
}
|
||||
pub fn into_inner(self) -> R {
|
||||
self.reader.reader.into_inner()
|
||||
}
|
||||
}
|
||||
|
||||
struct SamplesIterator<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
reader: WavReader<R>,
|
||||
samples_read: u32, // wav header is u32 so this suffices
|
||||
}
|
||||
|
||||
impl<R> Iterator for SamplesIterator<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
type Item = i16;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<i16> {
|
||||
let spec = self.reader.spec();
|
||||
match (spec.sample_format, spec.bits_per_sample) {
|
||||
(SampleFormat::Float, 32) => self.reader.samples().next().map(|value| {
|
||||
self.samples_read += 1;
|
||||
f32_to_i16(value.unwrap_or(0.0))
|
||||
}),
|
||||
(SampleFormat::Int, 8) => self.reader.samples().next().map(|value| {
|
||||
self.samples_read += 1;
|
||||
i8_to_i16(value.unwrap_or(0))
|
||||
}),
|
||||
(SampleFormat::Int, 16) => self.reader.samples().next().map(|value| {
|
||||
self.samples_read += 1;
|
||||
value.unwrap_or(0)
|
||||
}),
|
||||
(SampleFormat::Int, 24) => self.reader.samples().next().map(|value| {
|
||||
self.samples_read += 1;
|
||||
i24_to_i16(value.unwrap_or(0))
|
||||
}),
|
||||
(SampleFormat::Int, 32) => self.reader.samples().next().map(|value| {
|
||||
self.samples_read += 1;
|
||||
i32_to_i16(value.unwrap_or(0))
|
||||
}),
|
||||
(sample_format, bits_per_sample) => panic!(
|
||||
"Unimplemented wav spec: {:?}, {}",
|
||||
sample_format, bits_per_sample
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let len = (self.reader.len() - self.samples_read) as usize;
|
||||
(len, Some(len))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> ExactSizeIterator for SamplesIterator<R> where R: Read + Seek {}
|
||||
|
||||
impl<R> Source for WavDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.channels
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.sample_rate
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
Some(self.total_duration)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
let file_len = self.reader.reader.duration();
|
||||
|
||||
let new_pos = pos.as_secs_f32() * self.sample_rate() as f32;
|
||||
let new_pos = new_pos as u32;
|
||||
let new_pos = new_pos.min(file_len); // saturate pos at the end of the source
|
||||
|
||||
// make sure the next sample is for the right channel
|
||||
let to_skip = self.reader.samples_read % self.channels() as u32;
|
||||
|
||||
self.reader
|
||||
.reader
|
||||
.seek(new_pos)
|
||||
.map_err(SeekError::HoundDecoder)?;
|
||||
self.reader.samples_read = new_pos * self.channels() as u32;
|
||||
|
||||
for _ in 0..to_skip {
|
||||
self.next();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Iterator for WavDecoder<R>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
type Item = i16;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<i16> {
|
||||
self.reader.next()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.reader.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> ExactSizeIterator for WavDecoder<R> where R: Read + Seek {}
|
||||
|
||||
/// Returns true if the stream contains WAV data, then resets it to where it was.
|
||||
fn is_wave<R>(mut data: R) -> bool
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let stream_pos = data.stream_position().unwrap();
|
||||
|
||||
if WavReader::new(data.by_ref()).is_err() {
|
||||
data.seek(SeekFrom::Start(stream_pos)).unwrap();
|
||||
return false;
|
||||
}
|
||||
|
||||
data.seek(SeekFrom::Start(stream_pos)).unwrap();
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns a 32 bit WAV float as an i16. WAV floats are typically in the range of
|
||||
/// [-1.0, 1.0] while i16s are in the range [-32768, 32767]. Note that this
|
||||
/// function definitely causes precision loss but hopefully this isn't too
|
||||
/// audiable when actually playing?
|
||||
fn f32_to_i16(f: f32) -> i16 {
|
||||
// prefer to clip the input rather than be excessively loud.
|
||||
(f.clamp(-1.0, 1.0) * i16::MAX as f32) as i16
|
||||
}
|
||||
|
||||
/// Returns an 8-bit WAV int as an i16. This scales the sample value by a factor
|
||||
/// of 256.
|
||||
fn i8_to_i16(i: i8) -> i16 {
|
||||
i as i16 * 256
|
||||
}
|
||||
|
||||
/// Returns a 24 bit WAV int as an i16. Note that this is a 24 bit integer, not a
|
||||
/// 32 bit one. 24 bit ints are in the range [−8,388,608, 8,388,607] while i16s
|
||||
/// are in the range [-32768, 32767]. Note that this function definitely causes
|
||||
/// precision loss but hopefully this isn't too audiable when actually playing?
|
||||
fn i24_to_i16(i: i32) -> i16 {
|
||||
(i >> 8) as i16
|
||||
}
|
||||
|
||||
/// Returns a 32 bit WAV int as an i16. 32 bit ints are in the range
|
||||
/// [-2,147,483,648, 2,147,483,647] while i16s are in the range [-32768, 32767].
|
||||
/// Note that this function definitely causes precision loss but hopefully this
|
||||
/// isn't too audiable when actually playing?
|
||||
fn i32_to_i16(i: i32) -> i16 {
|
||||
(i >> 16) as i16
|
||||
}
|
||||
-303
@@ -1,303 +0,0 @@
|
||||
//! Mixer that plays multiple sounds at the same time.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::source::{SeekError, Source, UniformSourceIterator};
|
||||
use crate::Sample;
|
||||
|
||||
/// Builds a new mixer.
|
||||
///
|
||||
/// You can choose the characteristics of the output thanks to this constructor. All the sounds
|
||||
/// added to the mixer will be converted to these values.
|
||||
///
|
||||
/// After creating a mixer, you can add new sounds with the controller.
|
||||
pub fn mixer<S>(
|
||||
channels: u16,
|
||||
sample_rate: u32,
|
||||
) -> (Arc<DynamicMixerController<S>>, DynamicMixer<S>)
|
||||
where
|
||||
S: Sample + Send + 'static,
|
||||
{
|
||||
let input = Arc::new(DynamicMixerController {
|
||||
has_pending: AtomicBool::new(false),
|
||||
pending_sources: Mutex::new(Vec::new()),
|
||||
channels,
|
||||
sample_rate,
|
||||
});
|
||||
|
||||
let output = DynamicMixer {
|
||||
current_sources: Vec::with_capacity(16),
|
||||
input: input.clone(),
|
||||
sample_count: 0,
|
||||
still_pending: vec![],
|
||||
still_current: vec![],
|
||||
};
|
||||
|
||||
(input, output)
|
||||
}
|
||||
|
||||
/// The input of the mixer.
|
||||
pub struct DynamicMixerController<S> {
|
||||
has_pending: AtomicBool,
|
||||
pending_sources: Mutex<Vec<Box<dyn Source<Item = S> + Send>>>,
|
||||
channels: u16,
|
||||
sample_rate: u32,
|
||||
}
|
||||
|
||||
impl<S> DynamicMixerController<S>
|
||||
where
|
||||
S: Sample + Send + 'static,
|
||||
{
|
||||
/// Adds a new source to mix to the existing ones.
|
||||
#[inline]
|
||||
pub fn add<T>(&self, source: T)
|
||||
where
|
||||
T: Source<Item = S> + Send + 'static,
|
||||
{
|
||||
let uniform_source = UniformSourceIterator::new(source, self.channels, self.sample_rate);
|
||||
self.pending_sources
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(Box::new(uniform_source) as Box<_>);
|
||||
self.has_pending.store(true, Ordering::SeqCst); // TODO: can we relax this ordering?
|
||||
}
|
||||
}
|
||||
|
||||
/// The output of the mixer. Implements `Source`.
|
||||
pub struct DynamicMixer<S> {
|
||||
// The current iterator that produces samples.
|
||||
current_sources: Vec<Box<dyn Source<Item = S> + Send>>,
|
||||
|
||||
// The pending sounds.
|
||||
input: Arc<DynamicMixerController<S>>,
|
||||
|
||||
// The number of samples produced so far.
|
||||
sample_count: usize,
|
||||
|
||||
// A temporary vec used in start_pending_sources.
|
||||
still_pending: Vec<Box<dyn Source<Item = S> + Send>>,
|
||||
|
||||
// A temporary vec used in sum_current_sources.
|
||||
still_current: Vec<Box<dyn Source<Item = S> + Send>>,
|
||||
}
|
||||
|
||||
impl<S> Source for DynamicMixer<S>
|
||||
where
|
||||
S: Sample + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.input.channels
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.input.sample_rate
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> {
|
||||
Err(SeekError::NotSupported {
|
||||
underlying_source: std::any::type_name::<Self>(),
|
||||
})
|
||||
|
||||
// uncomment when #510 is implemented (query position of playback)
|
||||
|
||||
// let mut org_positions = Vec::with_capacity(self.current_sources.len());
|
||||
// let mut encounterd_err = None;
|
||||
//
|
||||
// for source in &mut self.current_sources {
|
||||
// let pos = /* source.playback_pos() */ todo!();
|
||||
// if let Err(e) = source.try_seek(pos) {
|
||||
// encounterd_err = Some(e);
|
||||
// break;
|
||||
// } else {
|
||||
// // store pos in case we need to roll back
|
||||
// org_positions.push(pos);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if let Some(e) = encounterd_err {
|
||||
// // rollback seeks that happend before err
|
||||
// for (pos, source) in org_positions
|
||||
// .into_iter()
|
||||
// .zip(self.current_sources.iter_mut())
|
||||
// {
|
||||
// source.try_seek(pos)?;
|
||||
// }
|
||||
// Err(e)
|
||||
// } else {
|
||||
// Ok(())
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Iterator for DynamicMixer<S>
|
||||
where
|
||||
S: Sample + Send + 'static,
|
||||
{
|
||||
type Item = S;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<S> {
|
||||
if self.input.has_pending.load(Ordering::SeqCst) {
|
||||
self.start_pending_sources();
|
||||
}
|
||||
|
||||
self.sample_count += 1;
|
||||
|
||||
let sum = self.sum_current_sources();
|
||||
|
||||
if self.current_sources.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(sum)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(0, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> DynamicMixer<S>
|
||||
where
|
||||
S: Sample + Send + 'static,
|
||||
{
|
||||
// Samples from the #next() function are interlaced for each of the channels.
|
||||
// We need to ensure we start playing sources so that their samples are
|
||||
// in-step with the modulo of the samples produced so far. Otherwise, the
|
||||
// sound will play on the wrong channels, e.g. left / right will be reversed.
|
||||
fn start_pending_sources(&mut self) {
|
||||
let mut pending = self.input.pending_sources.lock().unwrap(); // TODO: relax ordering?
|
||||
|
||||
for source in pending.drain(..) {
|
||||
let in_step = self.sample_count % source.channels() as usize == 0;
|
||||
|
||||
if in_step {
|
||||
self.current_sources.push(source);
|
||||
} else {
|
||||
self.still_pending.push(source);
|
||||
}
|
||||
}
|
||||
std::mem::swap(&mut self.still_pending, &mut pending);
|
||||
|
||||
let has_pending = !pending.is_empty();
|
||||
self.input.has_pending.store(has_pending, Ordering::SeqCst); // TODO: relax ordering?
|
||||
}
|
||||
|
||||
fn sum_current_sources(&mut self) -> S {
|
||||
let mut sum = S::zero_value();
|
||||
|
||||
for mut source in self.current_sources.drain(..) {
|
||||
if let Some(value) = source.next() {
|
||||
sum = sum.saturating_add(value);
|
||||
self.still_current.push(source);
|
||||
}
|
||||
}
|
||||
std::mem::swap(&mut self.still_current, &mut self.current_sources);
|
||||
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::buffer::SamplesBuffer;
|
||||
use crate::dynamic_mixer;
|
||||
use crate::source::Source;
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let (tx, mut rx) = dynamic_mixer::mixer(1, 48000);
|
||||
|
||||
tx.add(SamplesBuffer::new(1, 48000, vec![10i16, -10, 10, -10]));
|
||||
tx.add(SamplesBuffer::new(1, 48000, vec![5i16, 5, 5, 5]));
|
||||
|
||||
assert_eq!(rx.channels(), 1);
|
||||
assert_eq!(rx.sample_rate(), 48000);
|
||||
assert_eq!(rx.next(), Some(15));
|
||||
assert_eq!(rx.next(), Some(-5));
|
||||
assert_eq!(rx.next(), Some(15));
|
||||
assert_eq!(rx.next(), Some(-5));
|
||||
assert_eq!(rx.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channels_conv() {
|
||||
let (tx, mut rx) = dynamic_mixer::mixer(2, 48000);
|
||||
|
||||
tx.add(SamplesBuffer::new(1, 48000, vec![10i16, -10, 10, -10]));
|
||||
tx.add(SamplesBuffer::new(1, 48000, vec![5i16, 5, 5, 5]));
|
||||
|
||||
assert_eq!(rx.channels(), 2);
|
||||
assert_eq!(rx.sample_rate(), 48000);
|
||||
assert_eq!(rx.next(), Some(15));
|
||||
assert_eq!(rx.next(), Some(15));
|
||||
assert_eq!(rx.next(), Some(-5));
|
||||
assert_eq!(rx.next(), Some(-5));
|
||||
assert_eq!(rx.next(), Some(15));
|
||||
assert_eq!(rx.next(), Some(15));
|
||||
assert_eq!(rx.next(), Some(-5));
|
||||
assert_eq!(rx.next(), Some(-5));
|
||||
assert_eq!(rx.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_conv() {
|
||||
let (tx, mut rx) = dynamic_mixer::mixer(1, 96000);
|
||||
|
||||
tx.add(SamplesBuffer::new(1, 48000, vec![10i16, -10, 10, -10]));
|
||||
tx.add(SamplesBuffer::new(1, 48000, vec![5i16, 5, 5, 5]));
|
||||
|
||||
assert_eq!(rx.channels(), 1);
|
||||
assert_eq!(rx.sample_rate(), 96000);
|
||||
assert_eq!(rx.next(), Some(15));
|
||||
assert_eq!(rx.next(), Some(5));
|
||||
assert_eq!(rx.next(), Some(-5));
|
||||
assert_eq!(rx.next(), Some(5));
|
||||
assert_eq!(rx.next(), Some(15));
|
||||
assert_eq!(rx.next(), Some(5));
|
||||
assert_eq!(rx.next(), Some(-5));
|
||||
assert_eq!(rx.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_afterwards() {
|
||||
let (tx, mut rx) = dynamic_mixer::mixer(1, 48000);
|
||||
|
||||
tx.add(SamplesBuffer::new(1, 48000, vec![10i16, -10, 10, -10]));
|
||||
|
||||
assert_eq!(rx.next(), Some(10));
|
||||
assert_eq!(rx.next(), Some(-10));
|
||||
|
||||
tx.add(SamplesBuffer::new(1, 48000, vec![5i16, 5, 6, 6, 7, 7, 7]));
|
||||
|
||||
assert_eq!(rx.next(), Some(15));
|
||||
assert_eq!(rx.next(), Some(-5));
|
||||
|
||||
assert_eq!(rx.next(), Some(6));
|
||||
assert_eq!(rx.next(), Some(6));
|
||||
|
||||
tx.add(SamplesBuffer::new(1, 48000, vec![2i16]));
|
||||
|
||||
assert_eq!(rx.next(), Some(9));
|
||||
assert_eq!(rx.next(), Some(7));
|
||||
assert_eq!(rx.next(), Some(7));
|
||||
|
||||
assert_eq!(rx.next(), None);
|
||||
}
|
||||
}
|
||||
Vendored
-137
@@ -1,137 +0,0 @@
|
||||
//! Audio playback library.
|
||||
//!
|
||||
//! The main concept of this library is the [`Source`] trait, which
|
||||
//! represents a sound (streaming or not). In order to play a sound, there are three steps:
|
||||
//!
|
||||
//! - Create an object that represents the streaming sound. It can be a sine wave, a buffer, a
|
||||
//! [`decoder`], etc. or even your own type that implements the [`Source`] trait.
|
||||
//! - Get an output stream handle to a physical device. For example, get a stream to the system's
|
||||
//! default sound device with [`OutputStream::try_default()`]
|
||||
//! - Call [`.play_raw(source)`](OutputStreamHandle::play_raw) on the output stream handle.
|
||||
//!
|
||||
//! The [`play_raw`](OutputStreamHandle::play_raw) function expects the source to produce [`f32`]s,
|
||||
//! which may not be the case. If you get a compilation error, try calling
|
||||
//! [`.convert_samples()`](Source::convert_samples) on the source to fix it.
|
||||
//!
|
||||
//! For example, here is how you would play an audio file:
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use std::fs::File;
|
||||
//! use std::io::BufReader;
|
||||
//! use rodio::{Decoder, OutputStream, source::Source};
|
||||
//!
|
||||
//! // Get an output stream handle to the default physical sound device
|
||||
//! let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||
//! // Load a sound from a file, using a path relative to Cargo.toml
|
||||
//! let file = BufReader::new(File::open("examples/music.ogg").unwrap());
|
||||
//! // Decode that sound file into a source
|
||||
//! let source = Decoder::new(file).unwrap();
|
||||
//! // Play the sound directly on the device
|
||||
//! stream_handle.play_raw(source.convert_samples());
|
||||
//!
|
||||
//! // The sound plays in a separate audio thread,
|
||||
//! // so we need to keep the main thread alive while it's playing.
|
||||
//! std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
//! ```
|
||||
//!
|
||||
//! ## Sink
|
||||
//!
|
||||
//! In order to make it easier to control the playback, the rodio library also provides a type
|
||||
//! named [`Sink`] which represents an audio track.
|
||||
//!
|
||||
//! Instead of playing the sound with [`play_raw`](OutputStreamHandle::play_raw), you can add it to
|
||||
//! a [`Sink`] instead.
|
||||
//!
|
||||
//! - Get a [`Sink`] to the output stream, and [`.append()`](Sink::append) your sound to it.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use std::fs::File;
|
||||
//! use std::io::BufReader;
|
||||
//! use std::time::Duration;
|
||||
//! use rodio::{Decoder, OutputStream, Sink};
|
||||
//! use rodio::source::{SineWave, Source};
|
||||
//!
|
||||
//! // _stream must live as long as the sink
|
||||
//! let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||
//! let sink = Sink::try_new(&stream_handle).unwrap();
|
||||
//!
|
||||
//! // Add a dummy source of the sake of the example.
|
||||
//! let source = SineWave::new(440.0).take_duration(Duration::from_secs_f32(0.25)).amplify(0.20);
|
||||
//! sink.append(source);
|
||||
//!
|
||||
//! // The sound plays in a separate thread. This call will block the current thread until the sink
|
||||
//! // has finished playing all its queued sounds.
|
||||
//! sink.sleep_until_end();
|
||||
//! ```
|
||||
//!
|
||||
//! The [`append`](Sink::append) method will add the sound at the end of the
|
||||
//! sink. It will be played when all the previous sounds have been played. If you want multiple
|
||||
//! sounds to play simultaneously, you should create multiple [`Sink`]s.
|
||||
//!
|
||||
//! The [`Sink`] type also provides utilities such as playing/pausing or controlling the volume.
|
||||
//!
|
||||
//! **Please note that the [`Sink`] requires the [`OutputStream`], make sure that the outputstream is not dropped before the sink.**
|
||||
//!
|
||||
//! ## Filters
|
||||
//!
|
||||
//! The [`Source`] trait provides various filters, similar to the standard [`Iterator`] trait.
|
||||
//!
|
||||
//! Example:
|
||||
//!
|
||||
//! ```
|
||||
//! use rodio::Source;
|
||||
//! use std::time::Duration;
|
||||
//!
|
||||
//! // Repeats the first five seconds of the sound forever.
|
||||
//! # let source = rodio::source::SineWave::new(440.0);
|
||||
//! let source = source.take_duration(Duration::from_secs(5)).repeat_infinite();
|
||||
//! ```
|
||||
//!
|
||||
//! ## Alternative Decoder Backends
|
||||
//!
|
||||
//! [Symphonia](https://github.com/pdeljanov/Symphonia) is an alternative decoder library that can be used in place
|
||||
//! of many of the default backends.
|
||||
//! Currently, the main benefit is that Symphonia is the only backend that supports M4A and AAC,
|
||||
//! but it may be used to implement additional optional functionality in the future.
|
||||
//!
|
||||
//! To use, enable either the `symphonia-all` feature to enable all Symphonia codecs
|
||||
//! or enable specific codecs using one of the `symphonia-{codec name}` features.
|
||||
//! If you enable one or more of the Symphonia codecs, you may want to set `default-features = false` in order
|
||||
//! to avoid adding extra crates to your binary.
|
||||
//! See the [available feature flags](https://docs.rs/crate/rodio/latest/features) for all options.
|
||||
//!
|
||||
//! ## How it works under the hood
|
||||
//!
|
||||
//! Rodio spawns a background thread that is dedicated to reading from the sources and sending
|
||||
//! the output to the device. Whenever you give up ownership of a [`Source`] in order to play it,
|
||||
//! it is sent to this background thread where it will be read by rodio.
|
||||
//!
|
||||
//! All the sounds are mixed together by rodio before being sent to the operating system or the
|
||||
//! hardware. Therefore there is no restriction on the number of sounds that play simultaneously or
|
||||
//! the number of sinks that can be created (except for the fact that creating too many will slow
|
||||
//! down your program).
|
||||
//!
|
||||
#![cfg_attr(test, deny(missing_docs))]
|
||||
pub use cpal::{
|
||||
self, traits::DeviceTrait, Device, Devices, DevicesError, InputDevices, OutputDevices,
|
||||
SupportedStreamConfig,
|
||||
};
|
||||
|
||||
mod conversions;
|
||||
mod sink;
|
||||
mod spatial_sink;
|
||||
mod stream;
|
||||
|
||||
pub mod buffer;
|
||||
pub mod decoder;
|
||||
pub mod dynamic_mixer;
|
||||
pub mod queue;
|
||||
pub mod source;
|
||||
pub mod static_buffer;
|
||||
|
||||
pub use crate::conversions::Sample;
|
||||
pub use crate::decoder::Decoder;
|
||||
pub use crate::sink::Sink;
|
||||
pub use crate::source::Source;
|
||||
pub use crate::spatial_sink::SpatialSink;
|
||||
pub use crate::stream::{OutputStream, OutputStreamHandle, PlayError, StreamError};
|
||||
Vendored
-319
@@ -1,319 +0,0 @@
|
||||
//! Queue that plays sounds one after the other.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::source::{Empty, SeekError, Source, Zero};
|
||||
use crate::Sample;
|
||||
|
||||
#[cfg(feature = "crossbeam-channel")]
|
||||
use crossbeam_channel::{unbounded as channel, Receiver, Sender};
|
||||
#[cfg(not(feature = "crossbeam-channel"))]
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
|
||||
/// Builds a new queue. It consists of an input and an output.
|
||||
///
|
||||
/// The input can be used to add sounds to the end of the queue, while the output implements
|
||||
/// `Source` and plays the sounds.
|
||||
///
|
||||
/// The parameter indicates how the queue should behave if the queue becomes empty:
|
||||
///
|
||||
/// - If you pass `true`, then the queue is infinite and will play a silence instead until you add
|
||||
/// a new sound.
|
||||
/// - If you pass `false`, then the queue will report that it has finished playing.
|
||||
///
|
||||
pub fn queue<S>(keep_alive_if_empty: bool) -> (Arc<SourcesQueueInput<S>>, SourcesQueueOutput<S>)
|
||||
where
|
||||
S: Sample + Send + 'static,
|
||||
{
|
||||
let input = Arc::new(SourcesQueueInput {
|
||||
next_sounds: Mutex::new(Vec::new()),
|
||||
keep_alive_if_empty: AtomicBool::new(keep_alive_if_empty),
|
||||
});
|
||||
|
||||
let output = SourcesQueueOutput {
|
||||
current: Box::new(Empty::<S>::new()) as Box<_>,
|
||||
signal_after_end: None,
|
||||
input: input.clone(),
|
||||
};
|
||||
|
||||
(input, output)
|
||||
}
|
||||
|
||||
// TODO: consider reimplementing this with `from_factory`
|
||||
|
||||
type Sound<S> = Box<dyn Source<Item = S> + Send>;
|
||||
type SignalDone = Option<Sender<()>>;
|
||||
|
||||
/// The input of the queue.
|
||||
pub struct SourcesQueueInput<S> {
|
||||
next_sounds: Mutex<Vec<(Sound<S>, SignalDone)>>,
|
||||
|
||||
// See constructor.
|
||||
keep_alive_if_empty: AtomicBool,
|
||||
}
|
||||
|
||||
impl<S> SourcesQueueInput<S>
|
||||
where
|
||||
S: Sample + Send + 'static,
|
||||
{
|
||||
/// Adds a new source to the end of the queue.
|
||||
#[inline]
|
||||
pub fn append<T>(&self, source: T)
|
||||
where
|
||||
T: Source<Item = S> + Send + 'static,
|
||||
{
|
||||
self.next_sounds
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((Box::new(source) as Box<_>, None));
|
||||
}
|
||||
|
||||
/// Adds a new source to the end of the queue.
|
||||
///
|
||||
/// The `Receiver` will be signalled when the sound has finished playing.
|
||||
///
|
||||
/// Enable the feature flag `crossbeam-channel` in rodio to use a `crossbeam_channel::Receiver` instead.
|
||||
#[inline]
|
||||
pub fn append_with_signal<T>(&self, source: T) -> Receiver<()>
|
||||
where
|
||||
T: Source<Item = S> + Send + 'static,
|
||||
{
|
||||
let (tx, rx) = channel();
|
||||
self.next_sounds
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((Box::new(source) as Box<_>, Some(tx)));
|
||||
rx
|
||||
}
|
||||
|
||||
/// Sets whether the queue stays alive if there's no more sound to play.
|
||||
///
|
||||
/// See also the constructor.
|
||||
pub fn set_keep_alive_if_empty(&self, keep_alive_if_empty: bool) {
|
||||
self.keep_alive_if_empty
|
||||
.store(keep_alive_if_empty, Ordering::Release);
|
||||
}
|
||||
|
||||
/// Removes all the sounds from the queue. Returns the number of sounds cleared.
|
||||
pub fn clear(&self) -> usize {
|
||||
let mut sounds = self.next_sounds.lock().unwrap();
|
||||
let len = sounds.len();
|
||||
sounds.clear();
|
||||
len
|
||||
}
|
||||
}
|
||||
/// The output of the queue. Implements `Source`.
|
||||
pub struct SourcesQueueOutput<S> {
|
||||
// The current iterator that produces samples.
|
||||
current: Box<dyn Source<Item = S> + Send>,
|
||||
|
||||
// Signal this sender before picking from `next`.
|
||||
signal_after_end: Option<Sender<()>>,
|
||||
|
||||
// The next sounds.
|
||||
input: Arc<SourcesQueueInput<S>>,
|
||||
}
|
||||
|
||||
const THRESHOLD: usize = 512;
|
||||
impl<S> Source for SourcesQueueOutput<S>
|
||||
where
|
||||
S: Sample + Send + 'static,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
// This function is non-trivial because the boundary between two sounds in the queue should
|
||||
// be a frame boundary as well.
|
||||
//
|
||||
// The current sound is free to return `None` for `current_frame_len()`, in which case
|
||||
// we *should* return the number of samples remaining the current sound.
|
||||
// This can be estimated with `size_hint()`.
|
||||
//
|
||||
// If the `size_hint` is `None` as well, we are in the worst case scenario. To handle this
|
||||
// situation we force a frame to have a maximum number of samples indicate by this
|
||||
// constant.
|
||||
|
||||
// Try the current `current_frame_len`.
|
||||
if let Some(val) = self.current.current_frame_len() {
|
||||
if val != 0 {
|
||||
return Some(val);
|
||||
} else if self.input.keep_alive_if_empty.load(Ordering::Acquire)
|
||||
&& self.input.next_sounds.lock().unwrap().is_empty()
|
||||
{
|
||||
// The next source will be a filler silence which will have the length of `THRESHOLD`
|
||||
return Some(THRESHOLD);
|
||||
}
|
||||
}
|
||||
|
||||
// Try the size hint.
|
||||
let (lower_bound, _) = self.current.size_hint();
|
||||
// The iterator default implementation just returns 0.
|
||||
// That's a problematic value, so skip it.
|
||||
if lower_bound > 0 {
|
||||
return Some(lower_bound);
|
||||
}
|
||||
|
||||
// Otherwise we use the constant value.
|
||||
Some(THRESHOLD)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.current.channels()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.current.sample_rate()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Only seeks within the current source.
|
||||
// We can not go back to previous sources. We could implement seek such
|
||||
// that it advances the queue if the position is beyond the current song.
|
||||
//
|
||||
// We would then however need to enable seeking backwards across sources too.
|
||||
// That no longer seems in line with the queue behaviour.
|
||||
//
|
||||
// A final pain point is that we would need the total duration for the
|
||||
// next few songs.
|
||||
#[inline]
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
self.current.try_seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Iterator for SourcesQueueOutput<S>
|
||||
where
|
||||
S: Sample + Send + 'static,
|
||||
{
|
||||
type Item = S;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<S> {
|
||||
loop {
|
||||
// Basic situation that will happen most of the time.
|
||||
if let Some(sample) = self.current.next() {
|
||||
return Some(sample);
|
||||
}
|
||||
|
||||
// Since `self.current` has finished, we need to pick the next sound.
|
||||
// In order to avoid inlining this expensive operation, the code is in another function.
|
||||
if self.go_next().is_err() {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(self.current.size_hint().0, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> SourcesQueueOutput<S>
|
||||
where
|
||||
S: Sample + Send + 'static,
|
||||
{
|
||||
// Called when `current` is empty and we must jump to the next element.
|
||||
// Returns `Ok` if the sound should continue playing, or an error if it should stop.
|
||||
//
|
||||
// This method is separate so that it is not inlined.
|
||||
fn go_next(&mut self) -> Result<(), ()> {
|
||||
if let Some(signal_after_end) = self.signal_after_end.take() {
|
||||
let _ = signal_after_end.send(());
|
||||
}
|
||||
|
||||
let (next, signal_after_end) = {
|
||||
let mut next = self.input.next_sounds.lock().unwrap();
|
||||
|
||||
if next.len() == 0 {
|
||||
let silence = Box::new(Zero::<S>::new_samples(1, 44100, THRESHOLD)) as Box<_>;
|
||||
if self.input.keep_alive_if_empty.load(Ordering::Acquire) {
|
||||
// Play a short silence in order to avoid spinlocking.
|
||||
(silence, None)
|
||||
} else {
|
||||
return Err(());
|
||||
}
|
||||
} else {
|
||||
next.remove(0)
|
||||
}
|
||||
};
|
||||
|
||||
self.current = next;
|
||||
self.signal_after_end = signal_after_end;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::buffer::SamplesBuffer;
|
||||
use crate::queue;
|
||||
use crate::source::Source;
|
||||
|
||||
#[test]
|
||||
#[ignore] // FIXME: samples rate and channel not updated immediately after transition
|
||||
fn basic() {
|
||||
let (tx, mut rx) = queue::queue(false);
|
||||
|
||||
tx.append(SamplesBuffer::new(1, 48000, vec![10i16, -10, 10, -10]));
|
||||
tx.append(SamplesBuffer::new(2, 96000, vec![5i16, 5, 5, 5]));
|
||||
|
||||
assert_eq!(rx.channels(), 1);
|
||||
assert_eq!(rx.sample_rate(), 48000);
|
||||
assert_eq!(rx.next(), Some(10));
|
||||
assert_eq!(rx.next(), Some(-10));
|
||||
assert_eq!(rx.next(), Some(10));
|
||||
assert_eq!(rx.next(), Some(-10));
|
||||
assert_eq!(rx.channels(), 2);
|
||||
assert_eq!(rx.sample_rate(), 96000);
|
||||
assert_eq!(rx.next(), Some(5));
|
||||
assert_eq!(rx.next(), Some(5));
|
||||
assert_eq!(rx.next(), Some(5));
|
||||
assert_eq!(rx.next(), Some(5));
|
||||
assert_eq!(rx.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immediate_end() {
|
||||
let (_, mut rx) = queue::queue::<i16>(false);
|
||||
assert_eq!(rx.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keep_alive() {
|
||||
let (tx, mut rx) = queue::queue(true);
|
||||
tx.append(SamplesBuffer::new(1, 48000, vec![10i16, -10, 10, -10]));
|
||||
|
||||
assert_eq!(rx.next(), Some(10));
|
||||
assert_eq!(rx.next(), Some(-10));
|
||||
assert_eq!(rx.next(), Some(10));
|
||||
assert_eq!(rx.next(), Some(-10));
|
||||
|
||||
for _ in 0..100000 {
|
||||
assert_eq!(rx.next(), Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // TODO: not yet implemented
|
||||
fn no_delay_when_added() {
|
||||
let (tx, mut rx) = queue::queue(true);
|
||||
|
||||
for _ in 0..500 {
|
||||
assert_eq!(rx.next(), Some(0));
|
||||
}
|
||||
|
||||
tx.append(SamplesBuffer::new(1, 48000, vec![10i16, -10, 10, -10]));
|
||||
assert_eq!(rx.next(), Some(10));
|
||||
assert_eq!(rx.next(), Some(-10));
|
||||
assert_eq!(rx.next(), Some(10));
|
||||
assert_eq!(rx.next(), Some(-10));
|
||||
}
|
||||
}
|
||||
Vendored
-428
@@ -1,428 +0,0 @@
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "crossbeam-channel")]
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
#[cfg(not(feature = "crossbeam-channel"))]
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
|
||||
use crate::source::SeekError;
|
||||
use crate::stream::{OutputStreamHandle, PlayError};
|
||||
use crate::{queue, source::Done, Sample, Source};
|
||||
use cpal::FromSample;
|
||||
|
||||
/// Handle to a device that outputs sounds.
|
||||
///
|
||||
/// Dropping the `Sink` stops all sounds. You can use `detach` if you want the sounds to continue
|
||||
/// playing.
|
||||
pub struct Sink {
|
||||
queue_tx: Arc<queue::SourcesQueueInput<f32>>,
|
||||
sleep_until_end: Mutex<Option<Receiver<()>>>,
|
||||
|
||||
controls: Arc<Controls>,
|
||||
sound_count: Arc<AtomicUsize>,
|
||||
|
||||
detached: bool,
|
||||
}
|
||||
|
||||
struct SeekOrder {
|
||||
pos: Duration,
|
||||
feedback: Sender<Result<(), SeekError>>,
|
||||
}
|
||||
|
||||
impl SeekOrder {
|
||||
fn new(pos: Duration) -> (Self, Receiver<Result<(), SeekError>>) {
|
||||
#[cfg(not(feature = "crossbeam-channel"))]
|
||||
let (tx, rx) = {
|
||||
use std::sync::mpsc;
|
||||
mpsc::channel()
|
||||
};
|
||||
|
||||
#[cfg(feature = "crossbeam-channel")]
|
||||
let (tx, rx) = {
|
||||
use crossbeam_channel::bounded;
|
||||
bounded(1)
|
||||
};
|
||||
(Self { pos, feedback: tx }, rx)
|
||||
}
|
||||
|
||||
fn attempt<S>(self, maybe_seekable: &mut S)
|
||||
where
|
||||
S: Source,
|
||||
S::Item: Sample + Send,
|
||||
{
|
||||
let res = maybe_seekable.try_seek(self.pos);
|
||||
let _ignore_receiver_dropped = self.feedback.send(res);
|
||||
}
|
||||
}
|
||||
|
||||
struct Controls {
|
||||
pause: AtomicBool,
|
||||
volume: Mutex<f32>,
|
||||
stopped: AtomicBool,
|
||||
speed: Mutex<f32>,
|
||||
to_clear: Mutex<u32>,
|
||||
seek: Mutex<Option<SeekOrder>>,
|
||||
position: Mutex<Duration>,
|
||||
}
|
||||
|
||||
impl Sink {
|
||||
/// Builds a new `Sink`, beginning playback on a stream.
|
||||
#[inline]
|
||||
pub fn try_new(stream: &OutputStreamHandle) -> Result<Sink, PlayError> {
|
||||
let (sink, queue_rx) = Sink::new_idle();
|
||||
stream.play_raw(queue_rx)?;
|
||||
Ok(sink)
|
||||
}
|
||||
|
||||
/// Builds a new `Sink`.
|
||||
#[inline]
|
||||
pub fn new_idle() -> (Sink, queue::SourcesQueueOutput<f32>) {
|
||||
let (queue_tx, queue_rx) = queue::queue(true);
|
||||
|
||||
let sink = Sink {
|
||||
queue_tx,
|
||||
sleep_until_end: Mutex::new(None),
|
||||
controls: Arc::new(Controls {
|
||||
pause: AtomicBool::new(false),
|
||||
volume: Mutex::new(1.0),
|
||||
stopped: AtomicBool::new(false),
|
||||
speed: Mutex::new(1.0),
|
||||
to_clear: Mutex::new(0),
|
||||
seek: Mutex::new(None),
|
||||
position: Mutex::new(Duration::ZERO),
|
||||
}),
|
||||
sound_count: Arc::new(AtomicUsize::new(0)),
|
||||
detached: false,
|
||||
};
|
||||
(sink, queue_rx)
|
||||
}
|
||||
|
||||
/// Appends a sound to the queue of sounds to play.
|
||||
#[inline]
|
||||
pub fn append<S>(&self, source: S)
|
||||
where
|
||||
S: Source + Send + 'static,
|
||||
f32: FromSample<S::Item>,
|
||||
S::Item: Sample + Send,
|
||||
{
|
||||
// Wait for queue to flush then resume stopped playback
|
||||
if self.controls.stopped.load(Ordering::SeqCst) {
|
||||
if self.sound_count.load(Ordering::SeqCst) > 0 {
|
||||
self.sleep_until_end();
|
||||
}
|
||||
self.controls.stopped.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
let controls = self.controls.clone();
|
||||
|
||||
let start_played = AtomicBool::new(false);
|
||||
|
||||
let source = source
|
||||
.speed(1.0)
|
||||
// must be placed before pausable but after speed & delay
|
||||
.track_position()
|
||||
.pausable(false)
|
||||
.amplify(1.0)
|
||||
.skippable()
|
||||
.stoppable()
|
||||
// if you change the duration update the docs for try_seek!
|
||||
.periodic_access(Duration::from_millis(100), move |src| {
|
||||
if controls.stopped.load(Ordering::SeqCst) {
|
||||
src.stop();
|
||||
*controls.position.lock().unwrap() = Duration::ZERO;
|
||||
}
|
||||
{
|
||||
let mut to_clear = controls.to_clear.lock().unwrap();
|
||||
if *to_clear > 0 {
|
||||
src.inner_mut().skip();
|
||||
*to_clear -= 1;
|
||||
*controls.position.lock().unwrap() = Duration::ZERO;
|
||||
} else {
|
||||
*controls.position.lock().unwrap() = src.inner().inner().inner().inner().get_pos();
|
||||
}
|
||||
}
|
||||
let amp = src.inner_mut().inner_mut();
|
||||
amp.set_factor(*controls.volume.lock().unwrap());
|
||||
amp.inner_mut()
|
||||
.set_paused(controls.pause.load(Ordering::SeqCst));
|
||||
amp.inner_mut()
|
||||
.inner_mut()
|
||||
.inner_mut()
|
||||
.set_factor(*controls.speed.lock().unwrap());
|
||||
if let Some(seek) = controls.seek.lock().unwrap().take() {
|
||||
seek.attempt(amp)
|
||||
}
|
||||
start_played.store(true, Ordering::SeqCst);
|
||||
})
|
||||
.convert_samples();
|
||||
self.sound_count.fetch_add(1, Ordering::Relaxed);
|
||||
let source = Done::new(source, self.sound_count.clone());
|
||||
*self.sleep_until_end.lock().unwrap() = Some(self.queue_tx.append_with_signal(source));
|
||||
}
|
||||
|
||||
/// Gets the volume of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" volume (unfiltered input). Any value other than 1.0 will
|
||||
/// multiply each sample by this value.
|
||||
#[inline]
|
||||
pub fn volume(&self) -> f32 {
|
||||
*self.controls.volume.lock().unwrap()
|
||||
}
|
||||
|
||||
/// Changes the volume of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0` will
|
||||
/// multiply each sample by this value.
|
||||
#[inline]
|
||||
pub fn set_volume(&self, value: f32) {
|
||||
*self.controls.volume.lock().unwrap() = value;
|
||||
}
|
||||
|
||||
/// Gets the speed of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" speed (unfiltered input). Any value other than `1.0` will
|
||||
/// change the play speed of the sound.
|
||||
#[inline]
|
||||
pub fn speed(&self) -> f32 {
|
||||
*self.controls.speed.lock().unwrap()
|
||||
}
|
||||
|
||||
/// Changes the speed of the sound.
|
||||
///
|
||||
/// The value `1.0` is the "normal" speed (unfiltered input). Any value other than `1.0` will
|
||||
/// change the play speed of the sound.
|
||||
#[inline]
|
||||
pub fn set_speed(&self, value: f32) {
|
||||
*self.controls.speed.lock().unwrap() = value;
|
||||
}
|
||||
|
||||
/// Resumes playback of a paused sink.
|
||||
///
|
||||
/// No effect if not paused.
|
||||
#[inline]
|
||||
pub fn play(&self) {
|
||||
self.controls.pause.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// There is no `can_seek()` method as it is impossible to use correctly. Between
|
||||
// checking if a source supports seeking and actually seeking the sink can
|
||||
// switch to a new source.
|
||||
|
||||
/// Attempts to seek to a given position in the current source.
|
||||
///
|
||||
/// This blocks between 0 and ~5 milliseconds.
|
||||
///
|
||||
/// As long as the duration of the source is known, seek is guaranteed to saturate
|
||||
/// at the end of the source. For example given a source that reports a total duration
|
||||
/// of 42 seconds calling `try_seek()` with 60 seconds as argument will seek to
|
||||
/// 42 seconds.
|
||||
///
|
||||
/// # Errors
|
||||
/// This function will return [`SeekError::NotSupported`] if one of the underlying
|
||||
/// sources does not support seeking.
|
||||
///
|
||||
/// It will return an error if an implementation ran
|
||||
/// into one during the seek.
|
||||
///
|
||||
/// When seeking beyond the end of a source this
|
||||
/// function might return an error if the duration of the source is not known.
|
||||
pub fn try_seek(&self, pos: Duration) -> Result<(), SeekError> {
|
||||
let (order, feedback) = SeekOrder::new(pos);
|
||||
*self.controls.seek.lock().unwrap() = Some(order);
|
||||
|
||||
if self.sound_count.load(Ordering::Acquire) == 0 {
|
||||
// No sound is playing, seek will not be performed
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match feedback.recv() {
|
||||
Ok(seek_res) => seek_res,
|
||||
// The feedback channel closed. Probably another seekorder was set
|
||||
// invalidating this one and closing the feedback channel
|
||||
// ... or the audio thread panicked.
|
||||
Err(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pauses playback of this sink.
|
||||
///
|
||||
/// No effect if already paused.
|
||||
///
|
||||
/// A paused sink can be resumed with `play()`.
|
||||
pub fn pause(&self) {
|
||||
self.controls.pause.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Gets if a sink is paused
|
||||
///
|
||||
/// Sinks can be paused and resumed using `pause()` and `play()`. This returns `true` if the
|
||||
/// sink is paused.
|
||||
pub fn is_paused(&self) -> bool {
|
||||
self.controls.pause.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Removes all currently loaded `Source`s from the `Sink`, and pauses it.
|
||||
///
|
||||
/// See `pause()` for information about pausing a `Sink`.
|
||||
pub fn clear(&self) {
|
||||
let len = self.sound_count.load(Ordering::SeqCst) as u32;
|
||||
*self.controls.to_clear.lock().unwrap() = len;
|
||||
self.sleep_until_end();
|
||||
self.pause();
|
||||
}
|
||||
|
||||
/// Skips to the next `Source` in the `Sink`
|
||||
///
|
||||
/// If there are more `Source`s appended to the `Sink` at the time,
|
||||
/// it will play the next one. Otherwise, the `Sink` will finish as if
|
||||
/// it had finished playing a `Source` all the way through.
|
||||
pub fn skip_one(&self) {
|
||||
let len = self.sound_count.load(Ordering::SeqCst) as u32;
|
||||
let mut to_clear = self.controls.to_clear.lock().unwrap();
|
||||
if len > *to_clear {
|
||||
*to_clear += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops the sink by emptying the queue.
|
||||
#[inline]
|
||||
pub fn stop(&self) {
|
||||
self.controls.stopped.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Destroys the sink without stopping the sounds that are still playing.
|
||||
#[inline]
|
||||
pub fn detach(mut self) {
|
||||
self.detached = true;
|
||||
}
|
||||
|
||||
/// Sleeps the current thread until the sound ends.
|
||||
#[inline]
|
||||
pub fn sleep_until_end(&self) {
|
||||
if let Some(sleep_until_end) = self.sleep_until_end.lock().unwrap().take() {
|
||||
let _ = sleep_until_end.recv();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this sink has no more sounds to play.
|
||||
#[inline]
|
||||
pub fn empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
/// Returns the number of sounds currently in the queue.
|
||||
#[allow(clippy::len_without_is_empty)]
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
self.sound_count.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Returns the position of the sound that's being played.
|
||||
///
|
||||
/// This takes into account any speedup or delay applied.
|
||||
///
|
||||
/// Example: if you apply a speedup of *2* to an mp3 decoder source and
|
||||
/// [`get_pos()`](Sink::get_pos) returns *5s* then the position in the mp3
|
||||
/// recording is *10s* from its start.
|
||||
#[inline]
|
||||
pub fn get_pos(&self) -> Duration {
|
||||
*self.controls.position.lock().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Sink {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
self.queue_tx.set_keep_alive_if_empty(false);
|
||||
|
||||
if !self.detached {
|
||||
self.controls.stopped.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::buffer::SamplesBuffer;
|
||||
use crate::{Sink, Source};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[test]
|
||||
fn test_pause_and_stop() {
|
||||
let (sink, mut queue_rx) = Sink::new_idle();
|
||||
|
||||
// assert_eq!(queue_rx.next(), Some(0.0));
|
||||
|
||||
let v = vec![10i16, -10, 20, -20, 30, -30];
|
||||
|
||||
// Low rate to ensure immediate control.
|
||||
sink.append(SamplesBuffer::new(1, 1, v.clone()));
|
||||
let mut src = SamplesBuffer::new(1, 1, v).convert_samples();
|
||||
|
||||
assert_eq!(queue_rx.next(), src.next());
|
||||
assert_eq!(queue_rx.next(), src.next());
|
||||
|
||||
sink.pause();
|
||||
|
||||
assert_eq!(queue_rx.next(), Some(0.0));
|
||||
|
||||
sink.play();
|
||||
|
||||
assert_eq!(queue_rx.next(), src.next());
|
||||
assert_eq!(queue_rx.next(), src.next());
|
||||
|
||||
sink.stop();
|
||||
|
||||
assert_eq!(queue_rx.next(), Some(0.0));
|
||||
|
||||
assert_eq!(sink.empty(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stop_and_start() {
|
||||
let (sink, mut queue_rx) = Sink::new_idle();
|
||||
|
||||
let v = vec![10i16, -10, 20, -20, 30, -30];
|
||||
|
||||
sink.append(SamplesBuffer::new(1, 1, v.clone()));
|
||||
let mut src = SamplesBuffer::new(1, 1, v.clone()).convert_samples();
|
||||
|
||||
assert_eq!(queue_rx.next(), src.next());
|
||||
assert_eq!(queue_rx.next(), src.next());
|
||||
|
||||
sink.stop();
|
||||
|
||||
assert!(sink.controls.stopped.load(Ordering::SeqCst));
|
||||
assert_eq!(queue_rx.next(), Some(0.0));
|
||||
|
||||
src = SamplesBuffer::new(1, 1, v.clone()).convert_samples();
|
||||
sink.append(SamplesBuffer::new(1, 1, v));
|
||||
|
||||
assert!(!sink.controls.stopped.load(Ordering::SeqCst));
|
||||
// Flush silence
|
||||
let mut queue_rx = queue_rx.skip_while(|v| *v == 0.0);
|
||||
|
||||
assert_eq!(queue_rx.next(), src.next());
|
||||
assert_eq!(queue_rx.next(), src.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_volume() {
|
||||
let (sink, mut queue_rx) = Sink::new_idle();
|
||||
|
||||
let v = vec![10i16, -10, 20, -20, 30, -30];
|
||||
|
||||
// High rate to avoid immediate control.
|
||||
sink.append(SamplesBuffer::new(2, 44100, v.clone()));
|
||||
let src = SamplesBuffer::new(2, 44100, v.clone()).convert_samples();
|
||||
|
||||
let mut src = src.amplify(0.5);
|
||||
sink.set_volume(0.5);
|
||||
|
||||
for _ in 0..v.len() {
|
||||
assert_eq!(queue_rx.next(), src.next());
|
||||
}
|
||||
}
|
||||
}
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{Sample, Source};
|
||||
|
||||
use super::SeekError;
|
||||
|
||||
/// Internal function that builds a `Amplify` object.
|
||||
pub fn amplify<I>(input: I, factor: f32) -> Amplify<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
Amplify { input, factor }
|
||||
}
|
||||
|
||||
/// Filter that modifies each sample by a given value.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Amplify<I> {
|
||||
input: I,
|
||||
factor: f32,
|
||||
}
|
||||
|
||||
impl<I> Amplify<I> {
|
||||
/// Modifies the amplification factor.
|
||||
#[inline]
|
||||
pub fn set_factor(&mut self, factor: f32) {
|
||||
self.factor = factor;
|
||||
}
|
||||
|
||||
/// Returns a reference to the inner source.
|
||||
#[inline]
|
||||
pub fn inner(&self) -> &I {
|
||||
&self.input
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the inner source.
|
||||
#[inline]
|
||||
pub fn inner_mut(&mut self) -> &mut I {
|
||||
&mut self.input
|
||||
}
|
||||
|
||||
/// Returns the inner source.
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> I {
|
||||
self.input
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Iterator for Amplify<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
type Item = I::Item;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<I::Item> {
|
||||
self.input.next().map(|value| value.amplify(self.factor))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.input.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> ExactSizeIterator for Amplify<I>
|
||||
where
|
||||
I: Source + ExactSizeIterator,
|
||||
I::Item: Sample,
|
||||
{
|
||||
}
|
||||
|
||||
impl<I> Source for Amplify<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
self.input.current_frame_len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.input.channels()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.input.sample_rate()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
self.input.total_duration()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
self.input.try_seek(pos)
|
||||
}
|
||||
}
|
||||
-251
@@ -1,251 +0,0 @@
|
||||
use std::f32::consts::PI;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::Source;
|
||||
|
||||
use super::SeekError;
|
||||
|
||||
// Implemented following http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt
|
||||
|
||||
/// Internal function that builds a `BltFilter` object.
|
||||
pub fn low_pass<I>(input: I, freq: u32) -> BltFilter<I>
|
||||
where
|
||||
I: Source<Item = f32>,
|
||||
{
|
||||
low_pass_with_q(input, freq, 0.5)
|
||||
}
|
||||
|
||||
pub fn high_pass<I>(input: I, freq: u32) -> BltFilter<I>
|
||||
where
|
||||
I: Source<Item = f32>,
|
||||
{
|
||||
high_pass_with_q(input, freq, 0.5)
|
||||
}
|
||||
|
||||
/// Same as low_pass but allows the q value (bandwidth) to be changed
|
||||
pub fn low_pass_with_q<I>(input: I, freq: u32, q: f32) -> BltFilter<I>
|
||||
where
|
||||
I: Source<Item = f32>,
|
||||
{
|
||||
BltFilter {
|
||||
input,
|
||||
formula: BltFormula::LowPass { freq, q },
|
||||
applier: None,
|
||||
x_n1: 0.0,
|
||||
x_n2: 0.0,
|
||||
y_n1: 0.0,
|
||||
y_n2: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as high_pass but allows the q value (bandwidth) to be changed
|
||||
pub fn high_pass_with_q<I>(input: I, freq: u32, q: f32) -> BltFilter<I>
|
||||
where
|
||||
I: Source<Item = f32>,
|
||||
{
|
||||
BltFilter {
|
||||
input,
|
||||
formula: BltFormula::HighPass { freq, q },
|
||||
applier: None,
|
||||
x_n1: 0.0,
|
||||
x_n2: 0.0,
|
||||
y_n1: 0.0,
|
||||
y_n2: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BltFilter<I> {
|
||||
input: I,
|
||||
formula: BltFormula,
|
||||
applier: Option<BltApplier>,
|
||||
x_n1: f32,
|
||||
x_n2: f32,
|
||||
y_n1: f32,
|
||||
y_n2: f32,
|
||||
}
|
||||
|
||||
impl<I> BltFilter<I> {
|
||||
/// Modifies this filter so that it becomes a low-pass filter.
|
||||
pub fn to_low_pass(&mut self, freq: u32) {
|
||||
self.to_low_pass_with_q(freq, 0.5);
|
||||
}
|
||||
|
||||
/// Modifies this filter so that it becomes a high-pass filter
|
||||
pub fn to_high_pass(&mut self, freq: u32) {
|
||||
self.to_high_pass_with_q(freq, 0.5);
|
||||
}
|
||||
|
||||
/// Same as to_low_pass but allows the q value (bandwidth) to be changed
|
||||
pub fn to_low_pass_with_q(&mut self, freq: u32, q: f32) {
|
||||
self.formula = BltFormula::LowPass { freq, q };
|
||||
self.applier = None;
|
||||
}
|
||||
|
||||
/// Same as to_high_pass but allows the q value (bandwidth) to be changed
|
||||
pub fn to_high_pass_with_q(&mut self, freq: u32, q: f32) {
|
||||
self.formula = BltFormula::HighPass { freq, q };
|
||||
self.applier = None;
|
||||
}
|
||||
|
||||
/// Returns a reference to the inner source.
|
||||
#[inline]
|
||||
pub fn inner(&self) -> &I {
|
||||
&self.input
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the inner source.
|
||||
#[inline]
|
||||
pub fn inner_mut(&mut self) -> &mut I {
|
||||
&mut self.input
|
||||
}
|
||||
|
||||
/// Returns the inner source.
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> I {
|
||||
self.input
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Iterator for BltFilter<I>
|
||||
where
|
||||
I: Source<Item = f32>,
|
||||
{
|
||||
type Item = f32;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<f32> {
|
||||
let last_in_frame = self.input.current_frame_len() == Some(1);
|
||||
|
||||
if self.applier.is_none() {
|
||||
self.applier = Some(self.formula.to_applier(self.input.sample_rate()));
|
||||
}
|
||||
|
||||
let sample = match self.input.next() {
|
||||
None => return None,
|
||||
Some(s) => s,
|
||||
};
|
||||
|
||||
let result = self
|
||||
.applier
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.apply(sample, self.x_n1, self.x_n2, self.y_n1, self.y_n2);
|
||||
|
||||
self.y_n2 = self.y_n1;
|
||||
self.x_n2 = self.x_n1;
|
||||
self.y_n1 = result;
|
||||
self.x_n1 = sample;
|
||||
|
||||
if last_in_frame {
|
||||
self.applier = None;
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.input.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> ExactSizeIterator for BltFilter<I> where I: Source<Item = f32> + ExactSizeIterator {}
|
||||
|
||||
impl<I> Source for BltFilter<I>
|
||||
where
|
||||
I: Source<Item = f32>,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
self.input.current_frame_len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.input.channels()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.input.sample_rate()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
self.input.total_duration()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
self.input.try_seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum BltFormula {
|
||||
LowPass { freq: u32, q: f32 },
|
||||
HighPass { freq: u32, q: f32 },
|
||||
}
|
||||
|
||||
impl BltFormula {
|
||||
fn to_applier(&self, sampling_frequency: u32) -> BltApplier {
|
||||
match *self {
|
||||
BltFormula::LowPass { freq, q } => {
|
||||
let w0 = 2.0 * PI * freq as f32 / sampling_frequency as f32;
|
||||
|
||||
let alpha = w0.sin() / (2.0 * q);
|
||||
let b1 = 1.0 - w0.cos();
|
||||
let b0 = b1 / 2.0;
|
||||
let b2 = b0;
|
||||
let a0 = 1.0 + alpha;
|
||||
let a1 = -2.0 * w0.cos();
|
||||
let a2 = 1.0 - alpha;
|
||||
|
||||
BltApplier {
|
||||
b0: b0 / a0,
|
||||
b1: b1 / a0,
|
||||
b2: b2 / a0,
|
||||
a1: a1 / a0,
|
||||
a2: a2 / a0,
|
||||
}
|
||||
}
|
||||
BltFormula::HighPass { freq, q } => {
|
||||
let w0 = 2.0 * PI * freq as f32 / sampling_frequency as f32;
|
||||
let cos_w0 = w0.cos();
|
||||
let alpha = w0.sin() / (2.0 * q);
|
||||
|
||||
let b0 = (1.0 + cos_w0) / 2.0;
|
||||
let b1 = -1.0 - cos_w0;
|
||||
let b2 = b0;
|
||||
let a0 = 1.0 + alpha;
|
||||
let a1 = -2.0 * cos_w0;
|
||||
let a2 = 1.0 - alpha;
|
||||
|
||||
BltApplier {
|
||||
b0: b0 / a0,
|
||||
b1: b1 / a0,
|
||||
b2: b2 / a0,
|
||||
a1: a1 / a0,
|
||||
a2: a2 / a0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct BltApplier {
|
||||
b0: f32,
|
||||
b1: f32,
|
||||
b2: f32,
|
||||
a1: f32,
|
||||
a2: f32,
|
||||
}
|
||||
|
||||
impl BltApplier {
|
||||
#[inline]
|
||||
fn apply(&self, x_n: f32, x_n1: f32, x_n2: f32, y_n1: f32, y_n2: f32) -> f32 {
|
||||
self.b0 * x_n + self.b1 * x_n1 + self.b2 * x_n2 - self.a1 * y_n1 - self.a2 * y_n2
|
||||
}
|
||||
}
|
||||
-268
@@ -1,268 +0,0 @@
|
||||
use std::cmp;
|
||||
use std::mem;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{Sample, Source};
|
||||
|
||||
use super::SeekError;
|
||||
|
||||
/// Internal function that builds a `Buffered` object.
|
||||
#[inline]
|
||||
pub fn buffered<I>(input: I) -> Buffered<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
let total_duration = input.total_duration();
|
||||
let first_frame = extract(input);
|
||||
|
||||
Buffered {
|
||||
current_frame: first_frame,
|
||||
position_in_frame: 0,
|
||||
total_duration,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator that at the same time extracts data from the iterator and stores it in a buffer.
|
||||
pub struct Buffered<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
/// Immutable reference to the next frame of data. Cannot be `Frame::Input`.
|
||||
current_frame: Arc<Frame<I>>,
|
||||
|
||||
/// The position in number of samples of this iterator inside `current_frame`.
|
||||
position_in_frame: usize,
|
||||
|
||||
/// Obtained once at creation and never modified again.
|
||||
total_duration: Option<Duration>,
|
||||
}
|
||||
|
||||
enum Frame<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
/// Data that has already been extracted from the iterator. Also contains a pointer to the
|
||||
/// next frame.
|
||||
Data(FrameData<I>),
|
||||
|
||||
/// No more data.
|
||||
End,
|
||||
|
||||
/// Unextracted data. The `Option` should never be `None` and is only here for easier data
|
||||
/// processing.
|
||||
Input(Mutex<Option<I>>),
|
||||
}
|
||||
|
||||
struct FrameData<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
data: Vec<I::Item>,
|
||||
channels: u16,
|
||||
rate: u32,
|
||||
next: Mutex<Arc<Frame<I>>>,
|
||||
}
|
||||
|
||||
impl<I> Drop for FrameData<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// This is necessary to prevent stack overflows deallocating long chains of the mutually
|
||||
// recursive `Frame` and `FrameData` types. This iteratively traverses as much of the
|
||||
// chain as needs to be deallocated, and repeatedly "pops" the head off the list. This
|
||||
// solves the problem, as when the time comes to actually deallocate the `FrameData`,
|
||||
// the `next` field will contain a `Frame::End`, or an `Arc` with additional references,
|
||||
// so the depth of recursive drops will be bounded.
|
||||
while let Ok(arc_next) = self.next.get_mut() {
|
||||
if let Some(next_ref) = Arc::get_mut(arc_next) {
|
||||
// This allows us to own the next Frame.
|
||||
let next = mem::replace(next_ref, Frame::End);
|
||||
if let Frame::Data(next_data) = next {
|
||||
// Swap the current FrameData with the next one, allowing the current one
|
||||
// to go out of scope.
|
||||
*self = next_data;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a frame from the input iterator.
|
||||
fn extract<I>(mut input: I) -> Arc<Frame<I>>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
let frame_len = input.current_frame_len();
|
||||
|
||||
if frame_len == Some(0) {
|
||||
return Arc::new(Frame::End);
|
||||
}
|
||||
|
||||
let channels = input.channels();
|
||||
let rate = input.sample_rate();
|
||||
let data: Vec<I::Item> = input
|
||||
.by_ref()
|
||||
.take(cmp::min(frame_len.unwrap_or(32768), 32768))
|
||||
.collect();
|
||||
|
||||
if data.is_empty() {
|
||||
return Arc::new(Frame::End);
|
||||
}
|
||||
|
||||
Arc::new(Frame::Data(FrameData {
|
||||
data,
|
||||
channels,
|
||||
rate,
|
||||
next: Mutex::new(Arc::new(Frame::Input(Mutex::new(Some(input))))),
|
||||
}))
|
||||
}
|
||||
|
||||
impl<I> Buffered<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
/// Advances to the next frame.
|
||||
fn next_frame(&mut self) {
|
||||
let next_frame = {
|
||||
let mut next_frame_ptr = match &*self.current_frame {
|
||||
Frame::Data(FrameData { next, .. }) => next.lock().unwrap(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let next_frame = match &**next_frame_ptr {
|
||||
Frame::Data(_) => next_frame_ptr.clone(),
|
||||
Frame::End => next_frame_ptr.clone(),
|
||||
Frame::Input(input) => {
|
||||
let input = input.lock().unwrap().take().unwrap();
|
||||
extract(input)
|
||||
}
|
||||
};
|
||||
|
||||
*next_frame_ptr = next_frame.clone();
|
||||
next_frame
|
||||
};
|
||||
|
||||
self.current_frame = next_frame;
|
||||
self.position_in_frame = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Iterator for Buffered<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
type Item = I::Item;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<I::Item> {
|
||||
let current_sample;
|
||||
let advance_frame;
|
||||
|
||||
match &*self.current_frame {
|
||||
Frame::Data(FrameData { data, .. }) => {
|
||||
current_sample = Some(data[self.position_in_frame]);
|
||||
self.position_in_frame += 1;
|
||||
advance_frame = self.position_in_frame >= data.len();
|
||||
}
|
||||
|
||||
Frame::End => {
|
||||
current_sample = None;
|
||||
advance_frame = false;
|
||||
}
|
||||
|
||||
Frame::Input(_) => unreachable!(),
|
||||
};
|
||||
|
||||
if advance_frame {
|
||||
self.next_frame();
|
||||
}
|
||||
|
||||
current_sample
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
// TODO:
|
||||
(0, None)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: uncomment when `size_hint` is fixed
|
||||
/*impl<I> ExactSizeIterator for Amplify<I> where I: Source + ExactSizeIterator, I::Item: Sample {
|
||||
}*/
|
||||
|
||||
impl<I> Source for Buffered<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
match &*self.current_frame {
|
||||
Frame::Data(FrameData { data, .. }) => Some(data.len() - self.position_in_frame),
|
||||
Frame::End => Some(0),
|
||||
Frame::Input(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
match *self.current_frame {
|
||||
Frame::Data(FrameData { channels, .. }) => channels,
|
||||
Frame::End => 1,
|
||||
Frame::Input(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
match *self.current_frame {
|
||||
Frame::Data(FrameData { rate, .. }) => rate,
|
||||
Frame::End => 44100,
|
||||
Frame::Input(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
self.total_duration
|
||||
}
|
||||
|
||||
/// Can not support seek, in the end state we lose the underlying source
|
||||
/// which makes seeking back impossible.
|
||||
#[inline]
|
||||
fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> {
|
||||
Err(SeekError::NotSupported {
|
||||
underlying_source: std::any::type_name::<Self>(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Clone for Buffered<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
#[inline]
|
||||
fn clone(&self) -> Buffered<I> {
|
||||
Buffered {
|
||||
current_frame: self.current_frame.clone(),
|
||||
position_in_frame: self.position_in_frame,
|
||||
total_duration: self.total_duration,
|
||||
}
|
||||
}
|
||||
}
|
||||
-148
@@ -1,148 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{Sample, Source};
|
||||
|
||||
use super::SeekError;
|
||||
|
||||
/// Combines channels in input into a single mono source, then plays that mono sound
|
||||
/// to each channel at the volume given for that channel.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChannelVolume<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
input: I,
|
||||
// Channel number is used as index for amplification value.
|
||||
channel_volumes: Vec<f32>,
|
||||
// Current listener being processed.
|
||||
current_channel: usize,
|
||||
current_sample: Option<I::Item>,
|
||||
}
|
||||
|
||||
impl<I> ChannelVolume<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
pub fn new(mut input: I, channel_volumes: Vec<f32>) -> ChannelVolume<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
let mut sample = None;
|
||||
for _ in 0..input.channels() {
|
||||
if let Some(s) = input.next() {
|
||||
sample = Some(
|
||||
sample
|
||||
.get_or_insert_with(I::Item::zero_value)
|
||||
.saturating_add(s),
|
||||
);
|
||||
}
|
||||
}
|
||||
ChannelVolume {
|
||||
input,
|
||||
channel_volumes,
|
||||
current_channel: 0,
|
||||
current_sample: sample,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the volume for a given channel number. Will panic if channel number
|
||||
/// was invalid.
|
||||
pub fn set_volume(&mut self, channel: usize, volume: f32) {
|
||||
self.channel_volumes[channel] = volume;
|
||||
}
|
||||
|
||||
/// Returns a reference to the inner source.
|
||||
#[inline]
|
||||
pub fn inner(&self) -> &I {
|
||||
&self.input
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the inner source.
|
||||
#[inline]
|
||||
pub fn inner_mut(&mut self) -> &mut I {
|
||||
&mut self.input
|
||||
}
|
||||
|
||||
/// Returns the inner source.
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> I {
|
||||
self.input
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Iterator for ChannelVolume<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
type Item = I::Item;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<I::Item> {
|
||||
// return value
|
||||
let ret = self
|
||||
.current_sample
|
||||
.map(|sample| sample.amplify(self.channel_volumes[self.current_channel]));
|
||||
self.current_channel += 1;
|
||||
if self.current_channel >= self.channel_volumes.len() {
|
||||
self.current_channel = 0;
|
||||
self.current_sample = None;
|
||||
for _ in 0..self.input.channels() {
|
||||
if let Some(s) = self.input.next() {
|
||||
self.current_sample = Some(
|
||||
self.current_sample
|
||||
.get_or_insert_with(I::Item::zero_value)
|
||||
.saturating_add(s),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.input.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> ExactSizeIterator for ChannelVolume<I>
|
||||
where
|
||||
I: Source + ExactSizeIterator,
|
||||
I::Item: Sample,
|
||||
{
|
||||
}
|
||||
|
||||
impl<I> Source for ChannelVolume<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
self.input.current_frame_len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.channel_volumes.len() as u16
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.input.sample_rate()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
self.input.total_duration()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
self.input.try_seek(pos)
|
||||
}
|
||||
}
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use cpal::FromSample;
|
||||
|
||||
use crate::source::{FadeIn, Mix, TakeDuration};
|
||||
use crate::{Sample, Source};
|
||||
|
||||
/// Mixes one sound fading out with another sound fading in for the given duration.
|
||||
///
|
||||
/// Only the crossfaded portion (beginning of fadeout, beginning of fadein) is returned.
|
||||
pub fn crossfade<I1, I2>(
|
||||
input_fadeout: I1,
|
||||
input_fadein: I2,
|
||||
duration: Duration,
|
||||
) -> Crossfade<I1, I2>
|
||||
where
|
||||
I1: Source,
|
||||
I2: Source,
|
||||
I1::Item: FromSample<I2::Item> + Sample,
|
||||
I2::Item: Sample,
|
||||
{
|
||||
let mut input_fadeout = input_fadeout.take_duration(duration);
|
||||
input_fadeout.set_filter_fadeout();
|
||||
let input_fadein = input_fadein.take_duration(duration).fade_in(duration);
|
||||
input_fadeout.mix(input_fadein)
|
||||
}
|
||||
|
||||
pub type Crossfade<I1, I2> = Mix<TakeDuration<I1>, FadeIn<TakeDuration<I2>>>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::buffer::SamplesBuffer;
|
||||
fn dummysource(length: u8) -> SamplesBuffer<f32> {
|
||||
let data: Vec<f32> = (1..=length).map(f32::from).collect();
|
||||
SamplesBuffer::new(1, 1, data)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_crossfade() {
|
||||
let source1 = dummysource(10);
|
||||
let source2 = dummysource(10);
|
||||
let mut mixed = crossfade(
|
||||
source1,
|
||||
source2,
|
||||
Duration::from_secs(5) + Duration::from_nanos(1),
|
||||
);
|
||||
assert_eq!(mixed.next(), Some(1.0));
|
||||
assert_eq!(mixed.next(), Some(2.0));
|
||||
assert_eq!(mixed.next(), Some(3.0));
|
||||
assert_eq!(mixed.next(), Some(4.0));
|
||||
assert_eq!(mixed.next(), Some(5.0));
|
||||
assert_eq!(mixed.next(), None);
|
||||
|
||||
let source1 = dummysource(10);
|
||||
let source2 = dummysource(10).amplify(0.0);
|
||||
let mut mixed = crossfade(
|
||||
source1,
|
||||
source2,
|
||||
Duration::from_secs(5) + Duration::from_nanos(1),
|
||||
);
|
||||
assert_eq!(mixed.next(), Some(1.0 * 1.0));
|
||||
assert_eq!(mixed.next(), Some(2.0 * 0.8));
|
||||
assert_eq!(mixed.next(), Some(3.0 * 0.6));
|
||||
assert_eq!(mixed.next(), Some(4.0 * 0.4));
|
||||
assert_eq!(mixed.next(), Some(5.0 * 0.2));
|
||||
assert_eq!(mixed.next(), None);
|
||||
}
|
||||
}
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{Sample, Source};
|
||||
|
||||
use super::SeekError;
|
||||
|
||||
fn remaining_samples(until_playback: Duration, sample_rate: u32, channels: u16) -> usize {
|
||||
let ns = until_playback.as_secs() * 1_000_000_000 + until_playback.subsec_nanos() as u64;
|
||||
let samples = ns * channels as u64 * sample_rate as u64 / 1_000_000_000;
|
||||
samples as usize
|
||||
}
|
||||
|
||||
/// Internal function that builds a `Delay` object.
|
||||
pub fn delay<I>(input: I, duration: Duration) -> Delay<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
Delay {
|
||||
remaining_samples: remaining_samples(duration, input.sample_rate(), input.channels()),
|
||||
requested_duration: duration,
|
||||
input,
|
||||
}
|
||||
}
|
||||
|
||||
/// A source that delays the given source by a certain amount.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Delay<I> {
|
||||
input: I,
|
||||
remaining_samples: usize,
|
||||
requested_duration: Duration,
|
||||
}
|
||||
|
||||
impl<I> Delay<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
/// Returns a reference to the inner source.
|
||||
#[inline]
|
||||
pub fn inner(&self) -> &I {
|
||||
&self.input
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the inner source.
|
||||
#[inline]
|
||||
pub fn inner_mut(&mut self) -> &mut I {
|
||||
&mut self.input
|
||||
}
|
||||
|
||||
/// Returns the inner source.
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> I {
|
||||
self.input
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Iterator for Delay<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
type Item = <I as Iterator>::Item;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<<I as Iterator>::Item> {
|
||||
if self.remaining_samples >= 1 {
|
||||
self.remaining_samples -= 1;
|
||||
Some(Sample::zero_value())
|
||||
} else {
|
||||
self.input.next()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let (min, max) = self.input.size_hint();
|
||||
(
|
||||
min + self.remaining_samples,
|
||||
max.map(|v| v + self.remaining_samples),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Source for Delay<I>
|
||||
where
|
||||
I: Iterator + Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
self.input
|
||||
.current_frame_len()
|
||||
.map(|val| val + self.remaining_samples)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.input.channels()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.input.sample_rate()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
self.input
|
||||
.total_duration()
|
||||
.map(|val| val + self.requested_duration)
|
||||
}
|
||||
|
||||
/// Pos is seen from the perspective of the api user.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// let mut source = inner_source.delay(Duration::from_secs(10));
|
||||
/// source.try_seek(Duration::from_secs(15));
|
||||
///
|
||||
/// // inner_source is now at pos: Duration::from_secs(5);
|
||||
/// ```
|
||||
///
|
||||
#[inline]
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
if pos < self.requested_duration {
|
||||
self.input.try_seek(Duration::ZERO)?;
|
||||
let until_playback = self.requested_duration - pos;
|
||||
self.remaining_samples =
|
||||
remaining_samples(until_playback, self.sample_rate(), self.channels());
|
||||
}
|
||||
let compensated_for_delay = pos.saturating_sub(self.requested_duration);
|
||||
self.input.try_seek(compensated_for_delay)
|
||||
}
|
||||
}
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{Sample, Source};
|
||||
|
||||
use super::SeekError;
|
||||
|
||||
/// When the inner source is empty this decrements an `AtomicUsize`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Done<I> {
|
||||
input: I,
|
||||
signal: Arc<AtomicUsize>,
|
||||
signal_sent: bool,
|
||||
}
|
||||
|
||||
impl<I> Done<I> {
|
||||
#[inline]
|
||||
pub fn new(input: I, signal: Arc<AtomicUsize>) -> Done<I> {
|
||||
Done {
|
||||
input,
|
||||
signal,
|
||||
signal_sent: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the inner source.
|
||||
#[inline]
|
||||
pub fn inner(&self) -> &I {
|
||||
&self.input
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the inner source.
|
||||
#[inline]
|
||||
pub fn inner_mut(&mut self) -> &mut I {
|
||||
&mut self.input
|
||||
}
|
||||
|
||||
/// Returns the inner source.
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> I {
|
||||
self.input
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Source> Iterator for Done<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
type Item = I::Item;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<I::Item> {
|
||||
let next = self.input.next();
|
||||
if !self.signal_sent && next.is_none() {
|
||||
self.signal.fetch_sub(1, Ordering::Relaxed);
|
||||
self.signal_sent = true;
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.input.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<I> Source for Done<I>
|
||||
where
|
||||
I: Source,
|
||||
I::Item: Sample,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
self.input.current_frame_len()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
self.input.channels()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
self.input.sample_rate()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
self.input.total_duration()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_seek(&mut self, pos: Duration) -> Result<(), SeekError> {
|
||||
self.input.try_seek(pos)
|
||||
}
|
||||
}
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{Sample, Source};
|
||||
|
||||
use super::SeekError;
|
||||
|
||||
/// An empty source.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Empty<S>(PhantomData<S>);
|
||||
|
||||
impl<S> Default for Empty<S> {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Empty<S> {
|
||||
#[inline]
|
||||
pub fn new() -> Empty<S> {
|
||||
Empty(PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Iterator for Empty<S> {
|
||||
type Item = S;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<S> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Source for Empty<S>
|
||||
where
|
||||
S: Sample,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
48000
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
Some(Duration::new(0, 0))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> {
|
||||
Err(SeekError::NotSupported {
|
||||
underlying_source: std::any::type_name::<Self>(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
use std::marker::PhantomData;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{Sample, Source};
|
||||
|
||||
use super::SeekError;
|
||||
|
||||
/// An empty source which executes a callback function
|
||||
pub struct EmptyCallback<S> {
|
||||
pub phantom_data: PhantomData<S>,
|
||||
pub callback: Box<dyn Send + Fn()>,
|
||||
}
|
||||
|
||||
impl<S> EmptyCallback<S> {
|
||||
#[inline]
|
||||
pub fn new(callback: Box<dyn Send + Fn()>) -> EmptyCallback<S> {
|
||||
EmptyCallback {
|
||||
phantom_data: PhantomData,
|
||||
callback,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Iterator for EmptyCallback<S> {
|
||||
type Item = S;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<S> {
|
||||
(self.callback)();
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Source for EmptyCallback<S>
|
||||
where
|
||||
S: Sample,
|
||||
{
|
||||
#[inline]
|
||||
fn current_frame_len(&self) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn channels(&self) -> u16 {
|
||||
1
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn sample_rate(&self) -> u32 {
|
||||
48000
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn total_duration(&self) -> Option<Duration> {
|
||||
Some(Duration::new(0, 0))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn try_seek(&mut self, _: Duration) -> Result<(), SeekError> {
|
||||
Err(SeekError::NotSupported {
|
||||
underlying_source: std::any::type_name::<Self>(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user