summaryrefslogtreecommitdiffstats
path: root/third_party/rust/cubeb-coreaudio
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/cubeb-coreaudio')
-rw-r--r--third_party/rust/cubeb-coreaudio/.cargo-checksum.json1
-rw-r--r--third_party/rust/cubeb-coreaudio/.circleci/config.yml20
-rw-r--r--third_party/rust/cubeb-coreaudio/.editorconfig12
-rwxr-xr-xthird_party/rust/cubeb-coreaudio/.githooks/pre-push47
-rw-r--r--third_party/rust/cubeb-coreaudio/.github/workflows/test.yml42
-rw-r--r--third_party/rust/cubeb-coreaudio/.travis.yml22
-rw-r--r--third_party/rust/cubeb-coreaudio/Cargo.toml44
-rw-r--r--third_party/rust/cubeb-coreaudio/LICENSE13
-rw-r--r--third_party/rust/cubeb-coreaudio/README.md141
-rw-r--r--third_party/rust/cubeb-coreaudio/build-audiounit-rust-in-cubeb.sh11
-rwxr-xr-xthird_party/rust/cubeb-coreaudio/install_git_hook.sh3
-rwxr-xr-xthird_party/rust/cubeb-coreaudio/install_rustfmt_clippy.sh17
-rwxr-xr-xthird_party/rust/cubeb-coreaudio/run_device_tests.sh43
-rwxr-xr-xthird_party/rust/cubeb-coreaudio/run_sanitizers.sh27
-rwxr-xr-xthird_party/rust/cubeb-coreaudio/run_tests.sh54
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs691
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/auto_release.rs77
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/buffer_manager.rs355
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/device_property.rs360
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/mixer.rs492
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/mod.rs4423
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/resampler.rs84
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs400
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs1663
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs36
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs885
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs473
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs1215
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs614
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs12
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs572
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs215
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs1247
-rw-r--r--third_party/rust/cubeb-coreaudio/src/backend/utils.rs107
-rw-r--r--third_party/rust/cubeb-coreaudio/src/capi.rs19
-rw-r--r--third_party/rust/cubeb-coreaudio/src/lib.rs20
-rw-r--r--third_party/rust/cubeb-coreaudio/todo.md124
37 files changed, 14581 insertions, 0 deletions
diff --git a/third_party/rust/cubeb-coreaudio/.cargo-checksum.json b/third_party/rust/cubeb-coreaudio/.cargo-checksum.json
new file mode 100644
index 0000000000..5c8366f60a
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{".circleci/config.yml":"7f3dc865105ca8f33965a7958b1fe2e627ae2d5a703f3b2a4ab6e2e796018597",".editorconfig":"4e53b182bcc78b83d7e1b5c03efa14d22d4955c4ed2514d1ba4e99c1eb1a50ba",".githooks/pre-push":"8b8b26544cd56f54c0c33812551f786bb25cb08c86dbfeb6bf3daad881c826a1",".github/workflows/test.yml":"aa1998a3b104ad131805ca3513832cef3f65300192824f8b1efc9a5a0cc108f6",".travis.yml":"dc07bac53f70f16c9bdf52264bdc58500ae6018c1b4c567bc7642f6b4ca3cc35","Cargo.toml":"d7e757e664c23fae52028f1dfc5917f92523c08702e3a1f95e1fd38ed714416c","LICENSE":"6e6f56aff5bbf3cbc60747e152fb1a719bd0716aaf6d711c554f57d92e96297c","README.md":"0007782a05a5330f739ad789c19c82562c82e32386b0447000fc72c0d48405bc","build-audiounit-rust-in-cubeb.sh":"d228a05985dcd02ec1ecac66a2b64dae5a530804a25a7054ccc95905aedfb7ef","install_git_hook.sh":"d38c8e51e636f6b90b489621ac34ccd1d1b1f40dccce3d178ed1da1c5068f16d","install_rustfmt_clippy.sh":"4ae90d8dcb9757cb3ae4ae142ef80e5377c0dde61c63f4a3c32418646e80ca7b","run_device_tests.sh":"d717e598c96e4911d9494b18382d6bd3a8d5038b7d68d3166ad4336e237a97d8","run_sanitizers.sh":"84e93a0da137803018f37403511e8c92760be730426bf6cea34419d93d1a7ff8","run_tests.sh":"916a7ae4a406d2274417d6eca939a878db5adcb6144e5680d9d148bf90178f1c","src/backend/aggregate_device.rs":"43511107ba2a75a19340ac663c981362ca1b75b679b6c295d88b5035bd7e3619","src/backend/auto_release.rs":"050fdcee74cf46b9a8a85a877e166d72a853d33220f59cf734cbb6ea09daa441","src/backend/buffer_manager.rs":"e9bcf964347daa8952f98caa2746e34a31ea8908375204896593f56e4b6147ca","src/backend/device_property.rs":"a7622feaa41db1cd76fd35a85a022e44f4894e396a104a59008d5b8757d2ab4e","src/backend/mixer.rs":"ed299d3954e2a823060c870a8244673a7d4bca530830cb66b964d047a80ee3af","src/backend/mod.rs":"1591669c30a3d07754bfb39c9cb042cdd101f0ab89be13f6cdf74d376e441cf8","src/backend/resampler.rs":"48bf8f56ae8d60dbabca6417b768000619abee8731ac3902164b45651ac08a4d","src/backend/tests/aggregate_device.rs":"e3f94e118e1dd47941fbba4417de40bddc4254d9f06b1e938f58d8f1aa566a5c","src/backend/tests/api.rs":"cd7e7551e2e82b19da883621a494d2a6779c373f3ff2d12ee52fae8efec1e7b8","src/backend/tests/backlog.rs":"3b189a7e036543c467cc242af0ed3332721179ee2b1c8847a6db563546f1ac52","src/backend/tests/device_change.rs":"f68c2eaa55c3ec2a58894832fbca1e2a2e79e740b145f76a0f45452af465a934","src/backend/tests/device_property.rs":"ea0be5f8834be494cb33f854ce9d334b5763dc5287f949bcb4bd025d8a8b2d3b","src/backend/tests/interfaces.rs":"af8e3fdeb58226621699b29f1a90621b2260e3f17292dac54860cd05fe4eec71","src/backend/tests/manual.rs":"4a1634e86beb145d2703722a8be057a762953241329c82ee09acf7dc0f0d9d0c","src/backend/tests/mod.rs":"8dba770023d7f9c4228f0e11915347f0e07da5fd818e3ee4478c4b197af9aa2a","src/backend/tests/parallel.rs":"59632744e70616ab7037facb0787db339b96800c8cc397d203241548c5cfb7f5","src/backend/tests/tone.rs":"779cc14fc2a362bf7f26ce66ad70c0639501176175655a99b7fefb3c59d56c7a","src/backend/tests/utils.rs":"efb8b3709aff7ed5e2923566084de3e0709f3bd9c18a04f3310d7a3b86fa4b71","src/backend/utils.rs":"6c3ffbcd602e6cc9f56deb9ecb07b2eef2e6f074ef924178e466f380aae5c595","src/capi.rs":"21b66b70545bf04ec719928004d1d9adb45b24ced51288f5b2993d79aaf78f5f","src/lib.rs":"5e586d45cd6b3722f0a6736d9252593299269817a153eef1930a5fb9bfbb56f5","todo.md":"efc1f012eb9a331a040cad4ac03aa79307f25885f71b6fb38f3ad7af8d7d515c"},"package":null} \ No newline at end of file
diff --git a/third_party/rust/cubeb-coreaudio/.circleci/config.yml b/third_party/rust/cubeb-coreaudio/.circleci/config.yml
new file mode 100644
index 0000000000..2217485b8f
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/.circleci/config.yml
@@ -0,0 +1,20 @@
+# See lastest version from: https://circleci.com/docs/2.0/configuration-reference
+version: 2.1
+
+jobs:
+ regular_test:
+ macos: # indicate that we are using the macOS executor
+ xcode: 12.5.1 # indicate our selected version of Xcode
+ steps:
+ - checkout
+ - run: brew install cmake # for libcubeb in cubeb-sys crate
+ - run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
+ - run: rustc --version
+ - run: cargo --version
+ - run: cargo build --verbose
+ - run: sh run_tests.sh
+workflows:
+ version: 2
+ build_and_test:
+ jobs:
+ - regular_test
diff --git a/third_party/rust/cubeb-coreaudio/.editorconfig b/third_party/rust/cubeb-coreaudio/.editorconfig
new file mode 100644
index 0000000000..9e636725f1
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+
+[*.md]
+trim_trailing_whitespace = false \ No newline at end of file
diff --git a/third_party/rust/cubeb-coreaudio/.githooks/pre-push b/third_party/rust/cubeb-coreaudio/.githooks/pre-push
new file mode 100755
index 0000000000..288a2353fc
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/.githooks/pre-push
@@ -0,0 +1,47 @@
+#!/bin/bash -e
+
+# An example hook script to verify what is about to be pushed. Called by "git
+# push" after it has checked the remote status, but before anything has been
+# pushed. If this script exits with a non-zero status nothing will be pushed.
+#
+# This hook is called with the following parameters:
+#
+# $1 -- Name of the remote to which the push is being done
+# $2 -- URL to which the push is being done
+#
+# If pushing without using a named remote those arguments will be equal.
+#
+# Information about the commits which are being pushed is supplied as lines to
+# the standard input in the form:
+#
+# <local ref> <local sha1> <remote ref> <remote sha1>
+
+REMOTE="$1"
+URL="$2"
+
+PUSH_COMMAND=$(ps -ocommand= -p $PPID)
+
+IS_DESTRUCTIVE="\-\-delete|\-f"
+
+PROTECTED_BRANCH="trailblazer"
+CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
+
+WILL_DELETE_PROTECTED_BRANCH=":$PROTECTED_BRANCH"
+
+if [[ $PUSH_COMMAND =~ $IS_DESTRUCTIVE ]]; then
+ if [ $CURRENT_BRANCH = $PROTECTED_BRANCH ] || [[ $PUSH_COMMAND =~ $PROTECTED_BRANCH ]]; then
+ echo "'$PROTECTED_BRANCH' cannot be deleted!"
+ exit 1
+ fi
+ exit 0
+fi
+
+if [[ $PUSH_COMMAND =~ $WILL_DELETE_PROTECTED_BRANCH ]]; then
+ echo "'$PROTECTED_BRANCH' cannot be deleted!"
+ exit 1
+fi
+
+cargo fmt -- --check || (echo "Please run 'cargo fmt'"; false);
+
+cargo clippy -- -D warnings || (echo "Please run 'cargo clippy'"; false);
+
diff --git a/third_party/rust/cubeb-coreaudio/.github/workflows/test.yml b/third_party/rust/cubeb-coreaudio/.github/workflows/test.yml
new file mode 100644
index 0000000000..06fc86fa33
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/.github/workflows/test.yml
@@ -0,0 +1,42 @@
+name: Build & Test
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: macOS-latest
+ continue-on-error: ${{ matrix.experimental }}
+ strategy:
+ fail-fast: false
+ matrix:
+ rust: [stable]
+ experimental: [false]
+ include:
+ - rust: nightly
+ experimental: true
+
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ submodules: recursive
+
+ - name: Install Rust
+ run: rustup toolchain install ${{ matrix.rust }} --profile minimal --component rustfmt clippy
+
+ - name: Setup
+ run: |
+ rustup default ${{ matrix.rust }}
+ toolchain=$(rustup default)
+ echo "Use Rust toolchain: $toolchain"
+ rustc --version
+ cargo --version
+
+ - name: Build
+ run: cargo build --verbose
+
+ - name: Regular Test
+ run: sh run_tests.sh
+
+ - name: Sanitizer Test
+ if: ${{ matrix.rust == 'nightly' }}
+ run: sh run_sanitizers.sh
diff --git a/third_party/rust/cubeb-coreaudio/.travis.yml b/third_party/rust/cubeb-coreaudio/.travis.yml
new file mode 100644
index 0000000000..6ab5708bde
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/.travis.yml
@@ -0,0 +1,22 @@
+language: rust
+rust:
+ - stable
+ - beta
+ - nightly
+os:
+ - osx
+env:
+ - RUST_BACKTRACE=1
+install:
+ - sh install_rustfmt_clippy.sh
+before_script:
+ - rustc --version
+ - cargo --version
+script:
+ - cargo build --verbose
+ - sh run_tests.sh
+ - sh run_sanitizers.sh
+jobs:
+ allow_failures:
+ - rust: nightly
+ fast_finish: true
diff --git a/third_party/rust/cubeb-coreaudio/Cargo.toml b/third_party/rust/cubeb-coreaudio/Cargo.toml
new file mode 100644
index 0000000000..f9067d3c08
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/Cargo.toml
@@ -0,0 +1,44 @@
+# 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]
+name = "cubeb-coreaudio"
+version = "0.1.0"
+authors = [
+ "Chun-Min Chang <chun.m.chang@gmail.com>",
+ "Paul Adenot <paul@paul.cx>",
+]
+readme = "README.md"
+license = "ISC"
+
+[lib]
+crate-type = [
+ "staticlib",
+ "rlib",
+]
+
+[dependencies]
+atomic = "0.4"
+audio-mixer = "0.1"
+bitflags = "2"
+cubeb-backend = "0.12.0"
+float-cmp = "0.6"
+lazy_static = "1.2"
+libc = "0.2"
+mach = "0.3"
+ringbuf = "0.2.6"
+triple_buffer = "5.0.5"
+
+[dependencies.coreaudio-sys-utils]
+path = "coreaudio-sys-utils"
+
+[dev-dependencies]
+itertools = "0.11"
diff --git a/third_party/rust/cubeb-coreaudio/LICENSE b/third_party/rust/cubeb-coreaudio/LICENSE
new file mode 100644
index 0000000000..15ef92118b
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/LICENSE
@@ -0,0 +1,13 @@
+Copyright © 2018 Mozilla Foundation
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file
diff --git a/third_party/rust/cubeb-coreaudio/README.md b/third_party/rust/cubeb-coreaudio/README.md
new file mode 100644
index 0000000000..a2fd2493f1
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/README.md
@@ -0,0 +1,141 @@
+# cubeb-coreaudio-rs
+
+[![CircleCI](https://circleci.com/gh/mozilla/cubeb-coreaudio-rs.svg?style=svg)](https://circleci.com/gh/mozilla/cubeb-coreaudio-rs)
+[![Build & Test](https://github.com/mozilla/cubeb-coreaudio-rs/actions/workflows/test.yml/badge.svg)](https://github.com/mozilla/cubeb-coreaudio-rs/actions/workflows/test.yml)
+
+*Rust* implementation of [Cubeb][cubeb] on [the MacOS platform][cubeb-au].
+
+## Current Goals
+
+- Keep refactoring the implementation until it looks rusty! (it's translated from C at first.)
+ - Check the [todo list][todo]
+
+## Status
+
+This is now the _Firefox_'s default audio backend on *Mac OS*.
+
+## Install
+
+### Install cubeb-coreaudio within cubeb
+
+Run the following command:
+```sh
+curl https://raw.githubusercontent.com/mozilla/cubeb-coreaudio-rs/trailblazer/build-audiounit-rust-in-cubeb.sh | sh
+```
+
+### Other
+
+Just clone this repo
+
+## Test
+
+Please run `sh run_tests.sh`.
+
+Some tests cannot be run in parallel.
+They may operate the same device at the same time,
+or indirectly fire some system events that are listened by some tests.
+
+The tests that may affect others are marked `#[ignore]`.
+They will be run by `cargo test ... -- --ignored ...`
+after finishing normal tests.
+Most of the tests are executed in `run_tests.sh`.
+Only those tests commented with *FIXME* are left.
+
+### Git Hooks
+
+You can install _git hooks_ by running `install_git_hook.sh`.
+Then _pre-push_ script will be run and do the `cargo fmt` and `cargo clippy` check
+before the commits are pushed to remote.
+
+### Run Sanitizers
+
+Run _AddressSanitizer (ASan), LeakSanitizer (LSan), MemorySanitizer (MSan), ThreadSanitizer (TSan)_
+by `sh run_sanitizers.sh`.
+
+The above command will run all the test suits in *run_tests.sh* by all the available _sanitizers_.
+However, it takes a long time for finshing the tests.
+
+### Device Tests
+
+Run `run_device_tests.sh`.
+
+If you'd like to run all the device tests with sanitizers,
+use `RUSTFLAGS="-Z sanitizer=<SAN>" sh run_device_tests.sh`
+with valid `<SAN>` such as `address` or `thread`.
+
+#### Device Switching
+
+The system default device will be changed during our tests.
+All the available devices will take turns being the system default device.
+However, after finishing the tests, the default device will be set to the original one.
+The sounds in the tests should be able to continue whatever the system default device is.
+
+#### Device Plugging/Unplugging
+
+We implement APIs simulating plugging or unplugging a device
+by adding or removing an aggregate device programmatically.
+It's used to verify our callbacks for minitoring the system devices work.
+
+### Manual Test
+
+- Output devices switching
+ - `$ cargo test test_switch_output_device -- --ignored --nocapture`
+ - Enter `s` to switch output devices
+ - Enter `q` to finish test
+- Device collection change
+ - `cargo test test_device_collection_change -- --ignored --nocapture`
+ - Plug/Unplug devices to see events log.
+- Manual Stream Tester
+ - `cargo test test_stream_tester -- --ignored --nocapture`
+ - `c` to create a stream
+ - `d` to destroy a stream
+ - `s` to start the created stream
+ - `t` to stop the created stream
+ - `r` to register a device changed callback to the created stream
+ - `v` to set volume to the created stream
+ - `q` to quit the test
+ - It's useful to simulate the stream bahavior to reproduce the bug we found,
+ with some modified code.
+
+## TODO
+
+See [todo list][todo]
+
+## Issues
+
+- Atomic:
+ - We need atomic type around `f32` but there is no this type in the stardard Rust
+ - Using [atomic-rs](https://github.com/Amanieu/atomic-rs) to do this.
+- `kAudioDevicePropertyBufferFrameSize` cannot be set when another stream using the same device with smaller buffer size is active. See [here][chg-buf-sz] for details.
+
+### Test issues
+
+- Fail to run tests that depend on `AggregateDevice::create_blank_device` with the tests that work with the device event listeners
+ - The `AggregateDevice::create_blank_device` will add an aggregate device to the system and fire the device-change events indirectly.
+- `TestDeviceSwitcher` cannot work when there is an alive full-duplex stream
+ - An aggregate device will be created for a duplex stream when its input and output devices are different.
+ - `TestDeviceSwitcher` will cached the available devices, upon it's created, as the candidates for default device
+ - Hence the created aggregate device may be cached in `TestDeviceSwitcher`
+ - If the aggregate device is destroyed (when the destroying the duplex stream created it) but the `TestDeviceSwitcher` is still working,
+ it will set a destroyed device as the default device
+ - See details in [device_change.rs](src/backend/tests/device_change.rs)
+
+## Branches
+
+- [trailblazer][trailblazer]: Main branch
+- [plain-translation-from-c][from-c]: The code is rewritten from C code on a line-by-line basis
+- [ocs-disposal][ocs-disposal]: The first version that replace our custom mutex by Rust Mutex
+
+[cubeb]: https://github.com/mozilla/cubeb "Cross platform audio library"
+[cubeb-au]: https://github.com/mozilla/cubeb/blob/master/src/cubeb_audiounit.cpp "Cubeb AudioUnit"
+
+[chg-buf-sz]: https://cs.chromium.org/chromium/src/media/audio/mac/audio_manager_mac.cc?l=982-989&rcl=0207eefb445f9855c2ed46280cb835b6f08bdb30 "issue on changing buffer size"
+
+[todo]: todo.md
+
+[bmo1572273]: https://bugzilla.mozilla.org/show_bug.cgi?id=1572273
+[bmo1572273-c13]: https://bugzilla.mozilla.org/show_bug.cgi?id=1572273#c13
+
+[from-c]: https://github.com/mozilla/cubeb-coreaudio-rs/tree/plain-translation-from-c
+[ocs-disposal]: https://github.com/mozilla/cubeb-coreaudio-rs/tree/ocs-disposal
+[trailblazer]: https://github.com/mozilla/cubeb-coreaudio-rs/tree/trailblazer \ No newline at end of file
diff --git a/third_party/rust/cubeb-coreaudio/build-audiounit-rust-in-cubeb.sh b/third_party/rust/cubeb-coreaudio/build-audiounit-rust-in-cubeb.sh
new file mode 100644
index 0000000000..15cd63be62
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/build-audiounit-rust-in-cubeb.sh
@@ -0,0 +1,11 @@
+#!/bin/sh
+
+git clone --recursive https://github.com/mozilla/cubeb.git
+cd cubeb/src
+git clone https://github.com/mozilla/cubeb-coreaudio-rs.git
+cd ../..
+mkdir cubeb-build
+cd cubeb-build
+cmake ../cubeb -DBUILD_RUST_LIBS="ON"
+cmake --build .
+CUBEB_BACKEND="audiounit-rust" ctest \ No newline at end of file
diff --git a/third_party/rust/cubeb-coreaudio/install_git_hook.sh b/third_party/rust/cubeb-coreaudio/install_git_hook.sh
new file mode 100755
index 0000000000..c6a5af3fa1
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/install_git_hook.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+git config core.hooksPath .githooks
diff --git a/third_party/rust/cubeb-coreaudio/install_rustfmt_clippy.sh b/third_party/rust/cubeb-coreaudio/install_rustfmt_clippy.sh
new file mode 100755
index 0000000000..5063059c60
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/install_rustfmt_clippy.sh
@@ -0,0 +1,17 @@
+# https://github.com/rust-lang/rustup-components-history/blob/master/README.md
+# https://github.com/rust-lang/rust-clippy/blob/27acea0a1baac6cf3ac6debfdbce04f91e15d772/.travis.yml#L40-L46
+if ! rustup component add rustfmt; then
+ TARGET=$(rustc -Vv | awk '/host/{print $2}')
+ NIGHTLY=$(curl -s "https://rust-lang.github.io/rustup-components-history/${TARGET}/rustfmt")
+ curl -sSL "https://static.rust-lang.org/dist/${NIGHTLY}/rustfmt-nightly-${TARGET}.tar.xz" | \
+ tar -xJf - --strip-components=3 -C ~/.cargo/bin
+ rm -rf ~/.cargo/bin/doc
+fi
+
+if ! rustup component add clippy; then
+ TARGET=$(rustc -Vv | awk '/host/{print $2}')
+ NIGHTLY=$(curl -s "https://rust-lang.github.io/rustup-components-history/${TARGET}/clippy")
+ rustup default nightly-${NIGHTLY}
+ rustup component add rustfmt
+ rustup component add clippy
+fi
diff --git a/third_party/rust/cubeb-coreaudio/run_device_tests.sh b/third_party/rust/cubeb-coreaudio/run_device_tests.sh
new file mode 100755
index 0000000000..ae6df49713
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/run_device_tests.sh
@@ -0,0 +1,43 @@
+set -e
+
+echo "\n\nRun device-changed tests\n===================="
+
+if [[ -z "${RUST_BACKTRACE}" ]]; then
+ # Display backtrace for debugging
+ export RUST_BACKTRACE=1
+fi
+echo "RUST_BACKTRACE is set to ${RUST_BACKTRACE}\n"
+
+cargo test test_switch_device -- --ignored --nocapture
+
+cargo test test_plug_and_unplug_device -- --ignored --nocapture
+
+cargo test test_register_device_changed_callback_to_check_default_device_changed_input -- --ignored --nocapture
+cargo test test_register_device_changed_callback_to_check_default_device_changed_output -- --ignored --nocapture
+cargo test test_register_device_changed_callback_to_check_default_device_changed_duplex -- --ignored --nocapture
+cargo test test_register_device_changed_callback_to_check_input_alive_changed_input -- --ignored --nocapture
+cargo test test_register_device_changed_callback_to_check_input_alive_changed_duplex -- --ignored --nocapture
+
+cargo test test_destroy_input_stream_after_unplugging_a_nondefault_input_device -- --ignored --nocapture
+cargo test test_suspend_input_stream_by_unplugging_a_nondefault_input_device -- --ignored --nocapture
+
+cargo test test_destroy_input_stream_after_unplugging_a_default_input_device -- --ignored --nocapture
+cargo test test_reinit_input_stream_by_unplugging_a_default_input_device -- --ignored --nocapture
+
+cargo test test_destroy_output_stream_after_unplugging_a_nondefault_output_device -- --ignored --nocapture
+cargo test test_suspend_output_stream_by_unplugging_a_nondefault_output_device -- --ignored --nocapture
+
+cargo test test_destroy_output_stream_after_unplugging_a_default_output_device -- --ignored --nocapture
+cargo test test_reinit_output_stream_by_unplugging_a_default_output_device -- --ignored --nocapture
+
+cargo test test_destroy_duplex_stream_after_unplugging_a_nondefault_input_device -- --ignored --nocapture
+cargo test test_suspend_duplex_stream_by_unplugging_a_nondefault_input_device -- --ignored --nocapture
+
+cargo test test_destroy_duplex_stream_after_unplugging_a_nondefault_output_device -- --ignored --nocapture
+cargo test test_suspend_duplex_stream_by_unplugging_a_nondefault_output_device -- --ignored --nocapture
+
+cargo test test_destroy_duplex_stream_after_unplugging_a_default_input_device -- --ignored --nocapture
+cargo test test_reinit_duplex_stream_by_unplugging_a_default_input_device -- --ignored --nocapture
+
+cargo test test_destroy_duplex_stream_after_unplugging_a_default_output_device -- --ignored --nocapture
+cargo test test_reinit_duplex_stream_by_unplugging_a_default_output_device -- --ignored --nocapture
diff --git a/third_party/rust/cubeb-coreaudio/run_sanitizers.sh b/third_party/rust/cubeb-coreaudio/run_sanitizers.sh
new file mode 100755
index 0000000000..42992f2a41
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/run_sanitizers.sh
@@ -0,0 +1,27 @@
+# The option `Z` is only accepted on the nightly compiler
+# so changing to nightly toolchain by `rustup default nightly` is required.
+
+# See: https://github.com/rust-lang/rust/issues/39699 for more sanitizer support.
+
+toolchain=$(rustup default)
+echo "\nUse Rust toolchain: $toolchain"
+
+if [[ $toolchain != nightly* ]]; then
+ echo "The sanitizer is only available on Rust Nightly only. Skip."
+ exit
+fi
+
+# Bail out once getting an error.
+set -e
+
+# Ideally, sanitizers should be ("address" "leak" "memory" "thread") but
+# - `memory`: It doesn't works with target x86_64-apple-darwin
+# - `leak`: Get some errors that are out of our control. See:
+# https://github.com/mozilla/cubeb-coreaudio-rs/issues/45#issuecomment-591642931
+sanitizers=("address" "thread")
+for san in "${sanitizers[@]}"
+do
+ San="$(tr '[:lower:]' '[:upper:]' <<< ${san:0:1})${san:1}"
+ echo "\n\nRun ${San}Sanitizer\n------------------------------"
+ RUSTFLAGS="-Z sanitizer=${san}" sh run_tests.sh
+done
diff --git a/third_party/rust/cubeb-coreaudio/run_tests.sh b/third_party/rust/cubeb-coreaudio/run_tests.sh
new file mode 100755
index 0000000000..e119da1f03
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/run_tests.sh
@@ -0,0 +1,54 @@
+# Bail out once getting an error.
+set -e
+
+echo "\n\nTest suite for cubeb-coreaudio\n========================================"
+
+if [[ -z "${RUST_BACKTRACE}" ]]; then
+ # Display backtrace for debugging
+ export RUST_BACKTRACE=1
+fi
+echo "RUST_BACKTRACE is set to ${RUST_BACKTRACE}\n"
+
+# Run tests in the sub crate
+# Run the tests by `cargo * -p <SUB_CRATE>` if it's possible. By doing so, the duplicate compiling
+# between this crate and the <SUB_CRATE> can be saved. The compiling for <SUB_CRATE> can be reused
+# when running `cargo *` with this crate.
+# -------------------------------------------------------------------------------------------------
+SUB_CRATE="coreaudio-sys-utils"
+
+# Format check
+# `cargo fmt -p *` is only usable in workspaces, so a workaround is to enter to the sub crate
+# and then exit from it.
+cd $SUB_CRATE
+cargo fmt --all -- --check
+cd ..
+
+# Lints check
+cargo clippy -p $SUB_CRATE -- -D warnings
+
+# Regular Tests
+cargo test -p $SUB_CRATE
+
+# Run tests in the main crate
+# -------------------------------------------------------------------------------------------------
+# Format check
+cargo fmt --all -- --check
+
+# Lints check
+cargo clippy -- -D warnings
+
+# Regular Tests
+cargo test --verbose
+cargo test test_configure_output -- --ignored
+cargo test test_aggregate -- --ignored --test-threads=1
+
+# Parallel Tests
+cargo test test_parallel -- --ignored --nocapture --test-threads=1
+
+# Device-changed Tests
+sh run_device_tests.sh
+
+# Manual Tests
+# cargo test test_switch_output_device -- --ignored --nocapture
+# cargo test test_device_collection_change -- --ignored --nocapture
+# cargo test test_stream_tester -- --ignored --nocapture
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs b/third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs
new file mode 100644
index 0000000000..2738631b87
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs
@@ -0,0 +1,691 @@
+use super::*;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+pub const DRIFT_COMPENSATION: u32 = 1;
+
+#[derive(Debug)]
+pub struct AggregateDevice {
+ plugin_id: AudioObjectID,
+ device_id: AudioObjectID,
+ // For log only
+ input_id: AudioObjectID,
+ output_id: AudioObjectID,
+}
+
+#[derive(Debug)]
+pub enum Error {
+ OS(OSStatus),
+ Timeout(std::time::Duration),
+ LessThan2Devices(usize),
+}
+
+impl From<OSStatus> for Error {
+ fn from(status: OSStatus) -> Self {
+ Error::OS(status)
+ }
+}
+
+impl From<std::time::Duration> for Error {
+ fn from(duration: std::time::Duration) -> Self {
+ Error::Timeout(duration)
+ }
+}
+
+impl From<usize> for Error {
+ fn from(number: usize) -> Self {
+ Error::LessThan2Devices(number)
+ }
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::OS(status) => write!(f, "OSStatus({})", status),
+ Error::Timeout(duration) => write!(f, "Timeout({:?})", duration),
+ Error::LessThan2Devices(number) => write!(f, "LessThan2Devices({} only)", number),
+ }
+ }
+}
+
+impl AggregateDevice {
+ // Aggregate Device is a virtual audio interface which utilizes inputs and outputs
+ // of one or more physical audio interfaces. It is possible to use the clock of
+ // one of the devices as a master clock for all the combined devices and enable
+ // drift compensation for the devices that are not designated clock master.
+ //
+ // Creating a new aggregate device programmatically requires [0][1]:
+ // 1. Locate the base plug-in ("com.apple.audio.CoreAudio")
+ // 2. Create a dictionary that describes the aggregate device
+ // (don't add sub-devices in that step, prone to fail [0])
+ // 3. Ask the base plug-in to create the aggregate device (blank)
+ // 4. Add the array of sub-devices.
+ // 5. Set the master device (1st output device in our case)
+ // 6. Enable drift compensation for the non-master devices
+ //
+ // [0] https://lists.apple.com/archives/coreaudio-api/2006/Apr/msg00092.html
+ // [1] https://lists.apple.com/archives/coreaudio-api/2005/Jul/msg00150.html
+ // [2] CoreAudio.framework/Headers/AudioHardware.h
+ pub fn new(
+ input_id: AudioObjectID,
+ output_id: AudioObjectID,
+ ) -> std::result::Result<Self, Error> {
+ let plugin_id = Self::get_system_plugin_id()?;
+ let device_id = Self::create_blank_device_sync(plugin_id)?;
+
+ let mut cleanup = finally(|| {
+ let r = Self::destroy_device(plugin_id, device_id);
+ assert!(r.is_ok());
+ });
+
+ Self::set_sub_devices_sync(device_id, input_id, output_id)?;
+ Self::set_master_device(device_id, output_id)?;
+ Self::activate_clock_drift_compensation(device_id)?;
+ Self::workaround_for_airpod(device_id, input_id, output_id)?;
+
+ cleanup.dismiss();
+
+ cubeb_log!(
+ "Add devices input {} and output {} into an aggregate device {}",
+ input_id,
+ output_id,
+ device_id
+ );
+ Ok(Self {
+ plugin_id,
+ device_id,
+ input_id,
+ output_id,
+ })
+ }
+
+ pub fn get_device_id(&self) -> AudioObjectID {
+ self.device_id
+ }
+
+ // The following APIs are set to `pub` for testing purpose.
+ pub fn get_system_plugin_id() -> std::result::Result<AudioObjectID, Error> {
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioHardwarePropertyPlugInForBundleID,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut size: usize = 0;
+ let status =
+ audio_object_get_property_data_size(kAudioObjectSystemObject, &address, &mut size);
+ if status != NO_ERR {
+ return Err(Error::from(status));
+ }
+ assert_ne!(size, 0);
+
+ let mut plugin_id = kAudioObjectUnknown;
+ let mut in_bundle_ref = cfstringref_from_static_string("com.apple.audio.CoreAudio");
+ let mut translation_value = AudioValueTranslation {
+ mInputData: &mut in_bundle_ref as *mut CFStringRef as *mut c_void,
+ mInputDataSize: mem::size_of::<CFStringRef>() as u32,
+ mOutputData: &mut plugin_id as *mut AudioObjectID as *mut c_void,
+ mOutputDataSize: mem::size_of::<AudioObjectID>() as u32,
+ };
+ assert_eq!(size, mem::size_of_val(&translation_value));
+
+ let status = audio_object_get_property_data(
+ kAudioObjectSystemObject,
+ &address,
+ &mut size,
+ &mut translation_value,
+ );
+ unsafe {
+ CFRelease(in_bundle_ref as *const c_void);
+ }
+ if status == NO_ERR {
+ assert_ne!(plugin_id, kAudioObjectUnknown);
+ Ok(plugin_id)
+ } else {
+ Err(Error::from(status))
+ }
+ }
+
+ pub fn create_blank_device_sync(
+ plugin_id: AudioObjectID,
+ ) -> std::result::Result<AudioObjectID, Error> {
+ let waiting_time = Duration::new(5, 0);
+
+ let condvar_pair = Arc::new((Mutex::new(Vec::<AudioObjectID>::new()), Condvar::new()));
+ let mut cloned_condvar_pair = condvar_pair.clone();
+ let data_ptr = &mut cloned_condvar_pair as *mut Arc<(Mutex<Vec<AudioObjectID>>, Condvar)>;
+
+ let address = get_property_address(
+ Property::HardwareDevices,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ );
+
+ let status = audio_object_add_property_listener(
+ kAudioObjectSystemObject,
+ &address,
+ devices_changed_callback,
+ data_ptr as *mut c_void,
+ );
+ assert_eq!(status, NO_ERR);
+
+ let _teardown = finally(|| {
+ let status = audio_object_remove_property_listener(
+ kAudioObjectSystemObject,
+ &address,
+ devices_changed_callback,
+ data_ptr as *mut c_void,
+ );
+ assert_eq!(status, NO_ERR);
+ });
+
+ let device = Self::create_blank_device(plugin_id)?;
+
+ // Wait until the aggregate is created.
+ let (lock, cvar) = &*condvar_pair;
+ let devices = lock.lock().unwrap();
+ if !devices.contains(&device) {
+ let (devs, timeout_res) = cvar.wait_timeout(devices, waiting_time).unwrap();
+ if timeout_res.timed_out() {
+ cubeb_log!(
+ "Time out for waiting the creation of aggregate device {}!",
+ device
+ );
+ }
+ if !devs.contains(&device) {
+ return Err(Error::from(waiting_time));
+ }
+ }
+
+ extern "C" fn devices_changed_callback(
+ id: AudioObjectID,
+ _number_of_addresses: u32,
+ _addresses: *const AudioObjectPropertyAddress,
+ data: *mut c_void,
+ ) -> OSStatus {
+ assert_eq!(id, kAudioObjectSystemObject);
+ let pair = unsafe { &mut *(data as *mut Arc<(Mutex<Vec<AudioObjectID>>, Condvar)>) };
+ let (lock, cvar) = &**pair;
+ let mut devices = lock.lock().unwrap();
+ *devices = audiounit_get_devices();
+ cvar.notify_one();
+ NO_ERR
+ }
+
+ Ok(device)
+ }
+
+ pub fn create_blank_device(
+ plugin_id: AudioObjectID,
+ ) -> std::result::Result<AudioObjectID, Error> {
+ assert_ne!(plugin_id, kAudioObjectUnknown);
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioPlugInCreateAggregateDevice,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut size: usize = 0;
+ let status = audio_object_get_property_data_size(plugin_id, &address, &mut size);
+ if status != NO_ERR {
+ return Err(Error::from(status));
+ }
+ assert_ne!(size, 0);
+
+ let sys_time = SystemTime::now();
+ let time_id = sys_time.duration_since(UNIX_EPOCH).unwrap().as_nanos();
+ let device_name = format!("{}_{}", PRIVATE_AGGREGATE_DEVICE_NAME, time_id);
+ let device_uid = format!("org.mozilla.{}", device_name);
+
+ let mut device_id = kAudioObjectUnknown;
+ let status = unsafe {
+ let device_dict = CFMutableDictRef::default();
+
+ // Set the name of the device.
+ let device_name = cfstringref_from_string(&device_name);
+ device_dict.add_value(
+ cfstringref_from_static_string(AGGREGATE_DEVICE_NAME_KEY) as *const c_void,
+ device_name as *const c_void,
+ );
+ CFRelease(device_name as *const c_void);
+
+ // Set the uid of the device.
+ let device_uid = cfstringref_from_string(&device_uid);
+ device_dict.add_value(
+ cfstringref_from_static_string(AGGREGATE_DEVICE_UID_KEY) as *const c_void,
+ device_uid as *const c_void,
+ );
+ CFRelease(device_uid as *const c_void);
+
+ // Make the device private to the process creating it.
+ let private_value: i32 = 1;
+ let device_private_key = CFNumberCreate(
+ kCFAllocatorDefault,
+ i64::from(kCFNumberIntType),
+ &private_value as *const i32 as *const c_void,
+ );
+ device_dict.add_value(
+ cfstringref_from_static_string(AGGREGATE_DEVICE_PRIVATE_KEY) as *const c_void,
+ device_private_key as *const c_void,
+ );
+ CFRelease(device_private_key as *const c_void);
+
+ // Set the device to a stacked aggregate (i.e. multi-output device).
+ let stacked_value: i32 = 0; // 1 for normal aggregate device.
+ let device_stacked_key = CFNumberCreate(
+ kCFAllocatorDefault,
+ i64::from(kCFNumberIntType),
+ &stacked_value as *const i32 as *const c_void,
+ );
+ device_dict.add_value(
+ cfstringref_from_static_string(AGGREGATE_DEVICE_STACKED_KEY) as *const c_void,
+ device_stacked_key as *const c_void,
+ );
+ CFRelease(device_stacked_key as *const c_void);
+
+ // This call will fire `audiounit_collection_changed_callback` indirectly!
+ audio_object_get_property_data_with_qualifier(
+ plugin_id,
+ &address,
+ mem::size_of_val(&device_dict),
+ &device_dict,
+ &mut size,
+ &mut device_id,
+ )
+ };
+ if status == NO_ERR {
+ assert_ne!(device_id, kAudioObjectUnknown);
+ Ok(device_id)
+ } else {
+ Err(Error::from(status))
+ }
+ }
+
+ pub fn set_sub_devices_sync(
+ device_id: AudioDeviceID,
+ input_id: AudioDeviceID,
+ output_id: AudioDeviceID,
+ ) -> std::result::Result<(), Error> {
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioAggregateDevicePropertyFullSubDeviceList,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let waiting_time = Duration::new(5, 0);
+
+ let condvar_pair = Arc::new((Mutex::new(AudioObjectID::default()), Condvar::new()));
+ let mut cloned_condvar_pair = condvar_pair.clone();
+ let data_ptr = &mut cloned_condvar_pair as *mut Arc<(Mutex<AudioObjectID>, Condvar)>;
+
+ let status = audio_object_add_property_listener(
+ device_id,
+ &address,
+ devices_changed_callback,
+ data_ptr as *mut c_void,
+ );
+ if status != NO_ERR {
+ return Err(Error::from(status));
+ }
+
+ let remove_listener = || -> OSStatus {
+ audio_object_remove_property_listener(
+ device_id,
+ &address,
+ devices_changed_callback,
+ data_ptr as *mut c_void,
+ )
+ };
+
+ Self::set_sub_devices(device_id, input_id, output_id)?;
+
+ // Wait until the sub devices are added.
+ let (lock, cvar) = &*condvar_pair;
+ let device = lock.lock().unwrap();
+ if *device != device_id {
+ let (dev, timeout_res) = cvar.wait_timeout(device, waiting_time).unwrap();
+ if timeout_res.timed_out() {
+ cubeb_log!(
+ "Time out for waiting for adding devices({}, {}) to aggregate device {}!",
+ input_id,
+ output_id,
+ device_id
+ );
+ }
+ if *dev != device_id {
+ let status = remove_listener();
+ // If the error is kAudioHardwareBadObjectError, it implies `device_id` is somehow
+ // dead, so its listener should receive nothing. It's ok to leave here.
+ assert!(status == NO_ERR || status == (kAudioHardwareBadObjectError as OSStatus));
+ // TODO: Destroy the aggregate device immediately if error is not
+ // kAudioHardwareBadObjectError. Otherwise the `devices_changed_callback` is able
+ // to touch the `cloned_condvar_pair` after it's freed.
+ return Err(Error::from(waiting_time));
+ }
+ }
+
+ extern "C" fn devices_changed_callback(
+ id: AudioObjectID,
+ _number_of_addresses: u32,
+ _addresses: *const AudioObjectPropertyAddress,
+ data: *mut c_void,
+ ) -> OSStatus {
+ let pair = unsafe { &mut *(data as *mut Arc<(Mutex<AudioObjectID>, Condvar)>) };
+ let (lock, cvar) = &**pair;
+ let mut device = lock.lock().unwrap();
+ *device = id;
+ cvar.notify_one();
+ NO_ERR
+ }
+
+ let status = remove_listener();
+ assert_eq!(status, NO_ERR);
+ Ok(())
+ }
+
+ pub fn set_sub_devices(
+ device_id: AudioDeviceID,
+ input_id: AudioDeviceID,
+ output_id: AudioDeviceID,
+ ) -> std::result::Result<(), Error> {
+ assert_ne!(device_id, kAudioObjectUnknown);
+ assert_ne!(input_id, kAudioObjectUnknown);
+ assert_ne!(output_id, kAudioObjectUnknown);
+ assert_ne!(input_id, output_id);
+
+ let output_sub_devices = Self::get_sub_devices(output_id)?;
+ let input_sub_devices = Self::get_sub_devices(input_id)?;
+
+ unsafe {
+ let sub_devices = CFArrayCreateMutable(ptr::null(), 0, &kCFTypeArrayCallBacks);
+ // The order of the items in the array is significant and is used to determine the order of the streams
+ // of the AudioAggregateDevice.
+ for device in output_sub_devices {
+ let uid = get_device_global_uid(device)?;
+ CFArrayAppendValue(sub_devices, uid.get_raw() as *const c_void);
+ }
+
+ for device in input_sub_devices {
+ let uid = get_device_global_uid(device)?;
+ CFArrayAppendValue(sub_devices, uid.get_raw() as *const c_void);
+ }
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioAggregateDevicePropertyFullSubDeviceList,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let size = mem::size_of::<CFMutableArrayRef>();
+ let status = audio_object_set_property_data(device_id, &address, size, &sub_devices);
+ CFRelease(sub_devices as *const c_void);
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(Error::from(status))
+ }
+ }
+ }
+
+ pub fn get_sub_devices(
+ device_id: AudioDeviceID,
+ ) -> std::result::Result<Vec<AudioObjectID>, Error> {
+ assert_ne!(device_id, kAudioObjectUnknown);
+
+ let mut sub_devices = Vec::new();
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioAggregateDevicePropertyActiveSubDeviceList,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+ let mut size: usize = 0;
+ let status = audio_object_get_property_data_size(device_id, &address, &mut size);
+
+ if status == kAudioHardwareUnknownPropertyError as OSStatus {
+ // Return a vector containing the device itself if the device has no sub devices.
+ sub_devices.push(device_id);
+ return Ok(sub_devices);
+ } else if status != NO_ERR {
+ return Err(Error::from(status));
+ }
+
+ assert_ne!(size, 0);
+
+ let count = size / mem::size_of::<AudioObjectID>();
+ sub_devices = allocate_array(count);
+ let status = audio_object_get_property_data(
+ device_id,
+ &address,
+ &mut size,
+ sub_devices.as_mut_ptr(),
+ );
+
+ if status == NO_ERR {
+ Ok(sub_devices)
+ } else {
+ Err(Error::from(status))
+ }
+ }
+
+ pub fn set_master_device(
+ device_id: AudioDeviceID,
+ primary_id: AudioDeviceID,
+ ) -> std::result::Result<(), Error> {
+ assert_ne!(device_id, kAudioObjectUnknown);
+ assert_ne!(primary_id, kAudioObjectUnknown);
+
+ cubeb_log!(
+ "Set master device of the aggregate device {} to device {}",
+ device_id,
+ primary_id
+ );
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioAggregateDevicePropertyMasterSubDevice,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ // Master become the 1st sub device of the primary device
+ let output_sub_devices = Self::get_sub_devices(primary_id)?;
+ assert!(!output_sub_devices.is_empty());
+ let master_sub_device_uid = get_device_global_uid(output_sub_devices[0]).unwrap();
+ let master_sub_device = master_sub_device_uid.get_raw();
+ let size = mem::size_of::<CFStringRef>();
+ let status = audio_object_set_property_data(device_id, &address, size, &master_sub_device);
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(Error::from(status))
+ }
+ }
+
+ pub fn activate_clock_drift_compensation(
+ device_id: AudioObjectID,
+ ) -> std::result::Result<(), Error> {
+ assert_ne!(device_id, kAudioObjectUnknown);
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioObjectPropertyOwnedObjects,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let qualifier_data_size = mem::size_of::<AudioObjectID>();
+ let class_id: AudioClassID = kAudioSubDeviceClassID;
+ let qualifier_data = &class_id;
+
+ let mut size: usize = 0;
+ let status = audio_object_get_property_data_size_with_qualifier(
+ device_id,
+ &address,
+ qualifier_data_size,
+ qualifier_data,
+ &mut size,
+ );
+ if status != NO_ERR {
+ return Err(Error::from(status));
+ }
+ assert!(size > 0);
+ let subdevices_num = size / mem::size_of::<AudioObjectID>();
+ if subdevices_num < 2 {
+ cubeb_log!(
+ "Aggregate-device {} contains {} sub-devices only.\
+ We should have at least one input and one output device.",
+ device_id,
+ subdevices_num
+ );
+ return Err(Error::LessThan2Devices(subdevices_num));
+ }
+ let mut sub_devices: Vec<AudioObjectID> = allocate_array(subdevices_num);
+ let status = audio_object_get_property_data_with_qualifier(
+ device_id,
+ &address,
+ qualifier_data_size,
+ qualifier_data,
+ &mut size,
+ sub_devices.as_mut_ptr(),
+ );
+ if status != NO_ERR {
+ return Err(Error::from(status));
+ }
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioSubDevicePropertyDriftCompensation,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ // Start from the second device since the first is the master clock
+ for device in &sub_devices[1..] {
+ let status = audio_object_set_property_data(
+ *device,
+ &address,
+ mem::size_of::<u32>(),
+ &DRIFT_COMPENSATION,
+ );
+ if status != NO_ERR {
+ cubeb_log!(
+ "Failed to set drift compensation for {}. Ignore it.",
+ device
+ );
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn destroy_device(
+ plugin_id: AudioObjectID,
+ mut device_id: AudioDeviceID,
+ ) -> std::result::Result<(), Error> {
+ assert_ne!(plugin_id, kAudioObjectUnknown);
+ assert_ne!(device_id, kAudioObjectUnknown);
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioPlugInDestroyAggregateDevice,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut size: usize = 0;
+ let status = audio_object_get_property_data_size(plugin_id, &address, &mut size);
+ if status != NO_ERR {
+ return Err(Error::from(status));
+ }
+ assert!(size > 0);
+
+ let status = audio_object_get_property_data(plugin_id, &address, &mut size, &mut device_id);
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(Error::from(status))
+ }
+ }
+
+ pub fn workaround_for_airpod(
+ device_id: AudioDeviceID,
+ input_id: AudioDeviceID,
+ output_id: AudioDeviceID,
+ ) -> std::result::Result<(), Error> {
+ assert_ne!(device_id, kAudioObjectUnknown);
+ assert_ne!(input_id, kAudioObjectUnknown);
+ assert_ne!(output_id, kAudioObjectUnknown);
+ assert_ne!(input_id, output_id);
+
+ let label = get_device_label(input_id, DeviceType::INPUT)?;
+ let input_label = label.into_string();
+
+ let label = get_device_label(output_id, DeviceType::OUTPUT)?;
+ let output_label = label.into_string();
+
+ if input_label.contains("AirPods") && output_label.contains("AirPods") {
+ let input_rate =
+ get_device_sample_rate(input_id, DeviceType::INPUT | DeviceType::OUTPUT)?;
+ cubeb_log!(
+ "The nominal rate of the input device {}: {}",
+ input_id,
+ input_rate
+ );
+
+ let output_rate =
+ match get_device_sample_rate(output_id, DeviceType::INPUT | DeviceType::OUTPUT) {
+ Ok(rate) => format!("{}", rate),
+ Err(e) => format!("Error {}", e),
+ };
+ cubeb_log!(
+ "The nominal rate of the output device {}: {}",
+ output_id,
+ output_rate
+ );
+
+ let addr = AudioObjectPropertyAddress {
+ mSelector: kAudioDevicePropertyNominalSampleRate,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let status = audio_object_set_property_data(
+ device_id,
+ &addr,
+ mem::size_of::<f64>(),
+ &input_rate,
+ );
+ if status != NO_ERR {
+ return Err(Error::from(status));
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl Default for AggregateDevice {
+ fn default() -> Self {
+ Self {
+ plugin_id: kAudioObjectUnknown,
+ device_id: kAudioObjectUnknown,
+ input_id: kAudioObjectUnknown,
+ output_id: kAudioObjectUnknown,
+ }
+ }
+}
+
+impl Drop for AggregateDevice {
+ fn drop(&mut self) {
+ if self.plugin_id != kAudioObjectUnknown && self.device_id != kAudioObjectUnknown {
+ if let Err(r) = Self::destroy_device(self.plugin_id, self.device_id) {
+ cubeb_log!(
+ "Failed to destroyed aggregate device {}. Error: {}",
+ self.device_id,
+ r
+ );
+ } else {
+ cubeb_log!(
+ "Destroyed aggregate device {} (input {}, output {})",
+ self.device_id,
+ self.input_id,
+ self.output_id
+ );
+ }
+ }
+ }
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/auto_release.rs b/third_party/rust/cubeb-coreaudio/src/backend/auto_release.rs
new file mode 100644
index 0000000000..97e091a497
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/auto_release.rs
@@ -0,0 +1,77 @@
+use std::fmt;
+
+pub struct AutoRelease<T> {
+ ptr: *mut T,
+ release_func: unsafe extern "C" fn(*mut T),
+}
+
+impl<T> AutoRelease<T> {
+ pub fn new(ptr: *mut T, release_func: unsafe extern "C" fn(*mut T)) -> Self {
+ Self { ptr, release_func }
+ }
+
+ pub fn reset(&mut self, ptr: *mut T) {
+ self.release();
+ self.ptr = ptr;
+ }
+
+ pub fn as_ref(&self) -> &T {
+ assert!(!self.ptr.is_null());
+ unsafe { &*self.ptr }
+ }
+
+ pub fn as_mut(&mut self) -> &mut T {
+ assert!(!self.ptr.is_null());
+ unsafe { &mut *self.ptr }
+ }
+
+ pub fn as_ptr(&self) -> *const T {
+ self.ptr
+ }
+
+ fn release(&self) {
+ if !self.ptr.is_null() {
+ unsafe {
+ (self.release_func)(self.ptr);
+ }
+ }
+ }
+}
+
+impl<T> Drop for AutoRelease<T> {
+ fn drop(&mut self) {
+ self.release();
+ }
+}
+
+// Explicit Debug impl to work for the type T
+// that doesn't implement Debug trait.
+impl<T> fmt::Debug for AutoRelease<T> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("AutoRelease")
+ .field("ptr", &self.ptr)
+ .field("release_func", &self.release_func)
+ .finish()
+ }
+}
+
+#[test]
+fn test_auto_release() {
+ use std::mem;
+ use std::ptr;
+
+ unsafe extern "C" fn allocate() -> *mut libc::c_void {
+ // println!("Allocate!");
+ libc::calloc(1, mem::size_of::<u32>())
+ }
+
+ unsafe extern "C" fn deallocate(ptr: *mut libc::c_void) {
+ // println!("Deallocate!");
+ libc::free(ptr);
+ }
+
+ let mut auto_release = AutoRelease::new(ptr::null_mut(), deallocate);
+ let ptr = unsafe { allocate() };
+ auto_release.reset(ptr);
+ assert_eq!(auto_release.as_ptr(), ptr);
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/buffer_manager.rs b/third_party/rust/cubeb-coreaudio/src/backend/buffer_manager.rs
new file mode 100644
index 0000000000..6f1c299bfb
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/buffer_manager.rs
@@ -0,0 +1,355 @@
+use std::cmp::Ordering;
+use std::fmt;
+use std::os::raw::c_void;
+use std::slice;
+
+use cubeb_backend::SampleFormat;
+
+use super::ringbuf::RingBuffer;
+
+use self::LinearBuffer::*;
+use self::RingBufferConsumer::*;
+use self::RingBufferProducer::*;
+
+// Shuffles the data so that the first n channels of the interleaved buffer are overwritten by
+// the remaining channels.
+fn drop_first_n_channels_in_place<T: Copy>(
+ n: usize,
+ data: &mut [T],
+ frame_count: usize,
+ channel_count: usize,
+) {
+ // This function works if the numbers are equal but it's not particularly useful, so we hope to
+ // catch issues by checking using > and not >= here.
+ assert!(channel_count > n);
+ let mut read_idx: usize = 0;
+ let mut write_idx: usize = 0;
+
+ let channel_to_keep = channel_count - n;
+ for _ in 0..frame_count {
+ read_idx += n;
+ for _ in 0..channel_to_keep {
+ data[write_idx] = data[read_idx];
+ read_idx += 1;
+ write_idx += 1;
+ }
+ }
+}
+
+// It can be that the a stereo microphone is in use, but the user asked for mono input. In this
+// particular case, downmix the stereo pair into a mono channel. In all other cases, simply drop
+// the remaining channels before appending to the ringbuffer, becauses there is no right or wrong
+// way to do this, unlike with the output side, where proper channel matrixing can be done.
+// Return the number of valid samples in the buffer.
+fn remix_or_drop_channels<T: Copy + std::ops::Add<Output = T>>(
+ input_channels: usize,
+ output_channels: usize,
+ data: &mut [T],
+ frame_count: usize,
+) -> usize {
+ assert!(input_channels >= output_channels);
+ // Nothing to do, just return
+ if input_channels == output_channels {
+ return output_channels * frame_count;
+ }
+ // Simple stereo downmix
+ if input_channels == 2 && output_channels == 1 {
+ let mut read_idx = 0;
+ for (write_idx, _) in (0..frame_count).enumerate() {
+ data[write_idx] = data[read_idx] + data[read_idx + 1];
+ read_idx += 2;
+ }
+ return output_channels * frame_count;
+ }
+ // Drop excess channels
+ let mut read_idx = 0;
+ let mut write_idx = 0;
+ let channel_dropped_count = input_channels - output_channels;
+ for _ in 0..frame_count {
+ for _ in 0..output_channels {
+ data[write_idx] = data[read_idx];
+ write_idx += 1;
+ read_idx += 1;
+ }
+ read_idx += channel_dropped_count;
+ }
+ output_channels * frame_count
+}
+
+fn process_data<T: Copy + std::ops::Add<Output = T>>(
+ data: *mut c_void,
+ frame_count: usize,
+ input_channel_count: usize,
+ input_channels_to_ignore: usize,
+ output_channel_count: usize,
+) -> &'static [T] {
+ assert!(
+ input_channels_to_ignore == 0
+ || input_channel_count >= input_channels_to_ignore + output_channel_count
+ );
+ let input_slice = unsafe {
+ slice::from_raw_parts_mut::<T>(data as *mut T, frame_count * input_channel_count)
+ };
+ match input_channel_count.cmp(&output_channel_count) {
+ Ordering::Equal => {
+ assert_eq!(input_channels_to_ignore, 0);
+ input_slice
+ }
+ Ordering::Greater => {
+ if input_channels_to_ignore > 0 {
+ drop_first_n_channels_in_place(
+ input_channels_to_ignore,
+ input_slice,
+ frame_count,
+ input_channel_count,
+ );
+ }
+ let new_count_remixed = remix_or_drop_channels(
+ input_channel_count - input_channels_to_ignore,
+ output_channel_count,
+ input_slice,
+ frame_count,
+ );
+ unsafe { slice::from_raw_parts_mut::<T>(data as *mut T, new_count_remixed) }
+ }
+ Ordering::Less => {
+ assert!(input_channel_count < output_channel_count);
+ // Upmix happens on pull.
+ input_slice
+ }
+ }
+}
+
+pub enum RingBufferConsumer {
+ IntegerRingBufferConsumer(ringbuf::Consumer<i16>),
+ FloatRingBufferConsumer(ringbuf::Consumer<f32>),
+}
+
+pub enum RingBufferProducer {
+ IntegerRingBufferProducer(ringbuf::Producer<i16>),
+ FloatRingBufferProducer(ringbuf::Producer<f32>),
+}
+
+pub enum LinearBuffer {
+ IntegerLinearBuffer(Vec<i16>),
+ FloatLinearBuffer(Vec<f32>),
+}
+
+pub struct BufferManager {
+ consumer: RingBufferConsumer,
+ producer: RingBufferProducer,
+ linear_buffer: LinearBuffer,
+ // The number of channels in the interleaved data given to push_data
+ input_channel_count: usize,
+ // The number of channels that needs to be skipped in the beginning of input_channel_count
+ input_channels_to_ignore: usize,
+ // The number of channels we actually needs, which is also the channel count of the
+ // processed data stored in the internal ring buffer.
+ output_channel_count: usize,
+}
+
+impl BufferManager {
+ pub fn new(
+ format: SampleFormat,
+ buffer_size_frames: usize,
+ input_channel_count: usize,
+ input_channels_to_ignore: usize,
+ output_channel_count: usize,
+ ) -> Self {
+ assert!(
+ (input_channels_to_ignore == 0 && input_channel_count == 1)
+ || input_channel_count >= input_channels_to_ignore + output_channel_count
+ );
+ // 8 times the expected callback size, to handle the input callback being caled multiple
+ // times in a row correctly.
+ let buffer_element_count = output_channel_count * buffer_size_frames * 8;
+ match format {
+ SampleFormat::S16LE | SampleFormat::S16BE | SampleFormat::S16NE => {
+ let ring = RingBuffer::<i16>::new(buffer_element_count);
+ let (prod, cons) = ring.split();
+ Self {
+ producer: IntegerRingBufferProducer(prod),
+ consumer: IntegerRingBufferConsumer(cons),
+ linear_buffer: IntegerLinearBuffer(Vec::<i16>::with_capacity(
+ buffer_element_count,
+ )),
+ input_channel_count,
+ input_channels_to_ignore,
+ output_channel_count,
+ }
+ }
+ SampleFormat::Float32LE | SampleFormat::Float32BE | SampleFormat::Float32NE => {
+ let ring = RingBuffer::<f32>::new(buffer_element_count);
+ let (prod, cons) = ring.split();
+ Self {
+ producer: FloatRingBufferProducer(prod),
+ consumer: FloatRingBufferConsumer(cons),
+ linear_buffer: FloatLinearBuffer(Vec::<f32>::with_capacity(
+ buffer_element_count,
+ )),
+ input_channel_count,
+ input_channels_to_ignore,
+ output_channel_count,
+ }
+ }
+ }
+ }
+ fn stored_channel_count(&self) -> usize {
+ if self.output_channel_count > self.input_channel_count {
+ // This case allows upmix from mono on pull.
+ self.input_channel_count
+ } else {
+ // Other cases only downmix on push.
+ self.output_channel_count
+ }
+ }
+ fn input_channel_count(&self) -> usize {
+ self.input_channel_count
+ }
+ fn input_channels_to_ignore(&self) -> usize {
+ self.input_channels_to_ignore
+ }
+ fn output_channel_count(&self) -> usize {
+ self.output_channel_count
+ }
+ pub fn push_data(&mut self, data: *mut c_void, frame_count: usize) {
+ let to_push = frame_count * self.stored_channel_count();
+ let input_channel_count = self.input_channel_count();
+ let input_channels_to_ignore = self.input_channels_to_ignore();
+ let output_channel_count = self.output_channel_count();
+ let pushed = match &mut self.producer {
+ RingBufferProducer::FloatRingBufferProducer(p) => {
+ let processed_input = process_data(
+ data,
+ frame_count,
+ input_channel_count,
+ input_channels_to_ignore,
+ output_channel_count,
+ );
+ p.push_slice(processed_input)
+ }
+ RingBufferProducer::IntegerRingBufferProducer(p) => {
+ let processed_input = process_data(
+ data,
+ frame_count,
+ input_channel_count,
+ input_channels_to_ignore,
+ output_channel_count,
+ );
+ p.push_slice(processed_input)
+ }
+ };
+ assert!(pushed <= to_push);
+ if pushed != to_push {
+ cubeb_alog!(
+ "Input ringbuffer full, could only push {} instead of {}",
+ pushed,
+ to_push
+ );
+ }
+ }
+ fn pull_data(&mut self, data: *mut c_void, needed_samples: usize) {
+ assert_eq!(needed_samples % self.output_channel_count(), 0);
+ let needed_frames = needed_samples / self.output_channel_count();
+ let to_pull = needed_frames * self.stored_channel_count();
+ match &mut self.consumer {
+ IntegerRingBufferConsumer(p) => {
+ let input: &mut [i16] =
+ unsafe { slice::from_raw_parts_mut::<i16>(data as *mut i16, needed_samples) };
+ let pulled = p.pop_slice(input);
+ if pulled < to_pull {
+ cubeb_alog!(
+ "Underrun during input data pull: (needed: {}, available: {})",
+ to_pull,
+ pulled
+ );
+ for i in 0..(to_pull - pulled) {
+ input[pulled + i] = 0;
+ }
+ }
+ if needed_samples > to_pull {
+ // Mono upmix. This can happen with voice processing.
+ let mut write_idx = needed_samples;
+ for read_idx in (0..to_pull).rev() {
+ write_idx -= self.output_channel_count();
+ for offset in 0..self.output_channel_count() {
+ input[write_idx + offset] = input[read_idx];
+ }
+ }
+ }
+ }
+ FloatRingBufferConsumer(p) => {
+ let input: &mut [f32] =
+ unsafe { slice::from_raw_parts_mut::<f32>(data as *mut f32, needed_samples) };
+ let pulled = p.pop_slice(input);
+ if pulled < to_pull {
+ cubeb_alog!(
+ "Underrun during input data pull: (needed: {}, available: {})",
+ to_pull,
+ pulled
+ );
+ for i in 0..(to_pull - pulled) {
+ input[pulled + i] = 0.0;
+ }
+ }
+ if needed_samples > to_pull {
+ // Mono upmix. This can happen with voice processing.
+ let mut write_idx = needed_samples;
+ for read_idx in (0..to_pull).rev() {
+ write_idx -= self.output_channel_count();
+ for offset in 0..self.output_channel_count() {
+ input[write_idx + offset] = input[read_idx];
+ }
+ }
+ }
+ }
+ }
+ }
+ pub fn get_linear_data(&mut self, frame_count: usize) -> *mut c_void {
+ let output_sample_count = frame_count * self.output_channel_count();
+ let p = match &mut self.linear_buffer {
+ LinearBuffer::IntegerLinearBuffer(b) => {
+ b.resize(output_sample_count, 0);
+ b.as_mut_ptr() as *mut c_void
+ }
+ LinearBuffer::FloatLinearBuffer(b) => {
+ b.resize(output_sample_count, 0.);
+ b.as_mut_ptr() as *mut c_void
+ }
+ };
+ self.pull_data(p, output_sample_count);
+
+ p
+ }
+ pub fn available_frames(&self) -> usize {
+ assert_ne!(self.stored_channel_count(), 0);
+ let stored_samples = match &self.consumer {
+ IntegerRingBufferConsumer(p) => p.len(),
+ FloatRingBufferConsumer(p) => p.len(),
+ };
+ stored_samples / self.stored_channel_count()
+ }
+ pub fn trim(&mut self, final_frame_count: usize) {
+ let final_sample_count = final_frame_count * self.stored_channel_count();
+ match &mut self.consumer {
+ IntegerRingBufferConsumer(c) => {
+ let available = c.len();
+ assert!(available >= final_sample_count);
+ let to_pop = available - final_sample_count;
+ c.discard(to_pop);
+ }
+ FloatRingBufferConsumer(c) => {
+ let available = c.len();
+ assert!(available >= final_sample_count);
+ let to_pop = available - final_sample_count;
+ c.discard(to_pop);
+ }
+ }
+ }
+}
+
+impl fmt::Debug for BufferManager {
+ fn fmt(&self, _: &mut fmt::Formatter<'_>) -> fmt::Result {
+ Ok(())
+ }
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/device_property.rs b/third_party/rust/cubeb-coreaudio/src/backend/device_property.rs
new file mode 100644
index 0000000000..b9a50c6576
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/device_property.rs
@@ -0,0 +1,360 @@
+use super::*;
+
+pub fn get_device_uid(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<StringRef, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::DeviceUID, devtype);
+ let mut size = mem::size_of::<CFStringRef>();
+ let mut uid: CFStringRef = ptr::null();
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut uid);
+ if err == NO_ERR {
+ Ok(StringRef::new(uid as _))
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_device_model_uid(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<StringRef, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::ModelUID, devtype);
+ let mut size = mem::size_of::<CFStringRef>();
+ let mut uid: CFStringRef = ptr::null();
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut uid);
+ if err == NO_ERR {
+ Ok(StringRef::new(uid as _))
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_device_transport_type(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<u32, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::TransportType, devtype);
+ let mut size = mem::size_of::<u32>();
+ let mut transport: u32 = 0;
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut transport);
+ if err == NO_ERR {
+ Ok(transport)
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_device_source(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<u32, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::DeviceSource, devtype);
+ let mut size = mem::size_of::<u32>();
+ let mut source: u32 = 0;
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut source);
+ if err == NO_ERR {
+ Ok(source)
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_device_source_name(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<StringRef, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let mut source: u32 = get_device_source(id, devtype)?;
+ let address = get_property_address(Property::DeviceSourceName, devtype);
+ let mut size = mem::size_of::<AudioValueTranslation>();
+ let mut name: CFStringRef = ptr::null();
+ let mut trl = AudioValueTranslation {
+ mInputData: &mut source as *mut u32 as *mut c_void,
+ mInputDataSize: mem::size_of::<u32>() as u32,
+ mOutputData: &mut name as *mut CFStringRef as *mut c_void,
+ mOutputDataSize: mem::size_of::<CFStringRef>() as u32,
+ };
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut trl);
+ if err == NO_ERR {
+ Ok(StringRef::new(name as _))
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_device_name(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<StringRef, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::DeviceName, devtype);
+ let mut size = mem::size_of::<CFStringRef>();
+ let mut name: CFStringRef = ptr::null();
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut name);
+ if err == NO_ERR {
+ Ok(StringRef::new(name as _))
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_device_manufacturer(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<StringRef, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::DeviceManufacturer, devtype);
+ let mut size = mem::size_of::<CFStringRef>();
+ let mut manufacturer: CFStringRef = ptr::null();
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut manufacturer);
+ if err == NO_ERR {
+ Ok(StringRef::new(manufacturer as _))
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_device_buffer_frame_size_range(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<AudioValueRange, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::DeviceBufferFrameSizeRange, devtype);
+ let mut size = mem::size_of::<AudioValueRange>();
+ let mut range = AudioValueRange::default();
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut range);
+ if err == NO_ERR {
+ Ok(range)
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_device_latency(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<u32, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::DeviceLatency, devtype);
+ let mut size = mem::size_of::<u32>();
+ let mut latency: u32 = 0;
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut latency);
+ if err == NO_ERR {
+ Ok(latency)
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_device_streams(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<Vec<AudioStreamID>, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::DeviceStreams, devtype);
+
+ let mut size: usize = 0;
+ let err = audio_object_get_property_data_size(id, &address, &mut size);
+ if err != NO_ERR {
+ return Err(err);
+ }
+
+ let mut streams: Vec<AudioObjectID> = allocate_array_by_size(size);
+ let err = audio_object_get_property_data(id, &address, &mut size, streams.as_mut_ptr());
+ if err == NO_ERR {
+ Ok(streams)
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_device_sample_rate(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<f64, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::DeviceSampleRate, devtype);
+ let mut size = mem::size_of::<f64>();
+ let mut rate: f64 = 0.0;
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut rate);
+ if err == NO_ERR {
+ Ok(rate)
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_ranges_of_device_sample_rate(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<Vec<AudioValueRange>, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::DeviceSampleRates, devtype);
+
+ let mut size: usize = 0;
+ let err = audio_object_get_property_data_size(id, &address, &mut size);
+ if err != NO_ERR {
+ return Err(err);
+ }
+
+ let mut ranges: Vec<AudioValueRange> = allocate_array_by_size(size);
+ let err = audio_object_get_property_data(id, &address, &mut size, ranges.as_mut_ptr());
+ if err == NO_ERR {
+ Ok(ranges)
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_stream_latency(id: AudioStreamID) -> std::result::Result<u32, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(
+ Property::StreamLatency,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ );
+ let mut size = mem::size_of::<u32>();
+ let mut latency: u32 = 0;
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut latency);
+ if err == NO_ERR {
+ Ok(latency)
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_stream_terminal_type(id: AudioStreamID) -> std::result::Result<u32, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(
+ Property::StreamTerminalType,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ );
+ let mut size = mem::size_of::<u32>();
+ let mut terminal_type: u32 = 0;
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut terminal_type);
+ if err == NO_ERR {
+ Ok(terminal_type)
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_stream_virtual_format(
+ id: AudioStreamID,
+) -> std::result::Result<AudioStreamBasicDescription, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(
+ Property::StreamVirtualFormat,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ );
+ let mut size = mem::size_of::<AudioStreamBasicDescription>();
+ let mut format = AudioStreamBasicDescription::default();
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut format);
+ if err == NO_ERR {
+ Ok(format)
+ } else {
+ Err(err)
+ }
+}
+
+pub fn get_clock_domain(
+ id: AudioStreamID,
+ devtype: DeviceType,
+) -> std::result::Result<u32, OSStatus> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = get_property_address(Property::ClockDomain, devtype);
+ let mut size = mem::size_of::<u32>();
+ let mut clock_domain: u32 = 0;
+ let err = audio_object_get_property_data(id, &address, &mut size, &mut clock_domain);
+ if err == NO_ERR {
+ Ok(clock_domain)
+ } else {
+ Err(err)
+ }
+}
+
+pub enum Property {
+ DeviceBufferFrameSizeRange,
+ DeviceIsAlive,
+ DeviceLatency,
+ DeviceManufacturer,
+ DeviceName,
+ DeviceSampleRate,
+ DeviceSampleRates,
+ DeviceSource,
+ DeviceSourceName,
+ DeviceStreams,
+ DeviceUID,
+ HardwareDefaultInputDevice,
+ HardwareDefaultOutputDevice,
+ HardwareDevices,
+ ModelUID,
+ StreamLatency,
+ StreamTerminalType,
+ StreamVirtualFormat,
+ TransportType,
+ ClockDomain,
+}
+
+impl From<Property> for AudioObjectPropertySelector {
+ fn from(p: Property) -> Self {
+ match p {
+ Property::DeviceBufferFrameSizeRange => kAudioDevicePropertyBufferFrameSizeRange,
+ Property::DeviceIsAlive => kAudioDevicePropertyDeviceIsAlive,
+ Property::DeviceLatency => kAudioDevicePropertyLatency,
+ Property::DeviceManufacturer => kAudioObjectPropertyManufacturer,
+ Property::DeviceName => kAudioObjectPropertyName,
+ Property::DeviceSampleRate => kAudioDevicePropertyNominalSampleRate,
+ Property::DeviceSampleRates => kAudioDevicePropertyAvailableNominalSampleRates,
+ Property::DeviceSource => kAudioDevicePropertyDataSource,
+ Property::DeviceSourceName => kAudioDevicePropertyDataSourceNameForIDCFString,
+ Property::DeviceStreams => kAudioDevicePropertyStreams,
+ Property::DeviceUID => kAudioDevicePropertyDeviceUID,
+ Property::HardwareDefaultInputDevice => kAudioHardwarePropertyDefaultInputDevice,
+ Property::HardwareDefaultOutputDevice => kAudioHardwarePropertyDefaultOutputDevice,
+ Property::HardwareDevices => kAudioHardwarePropertyDevices,
+ Property::ModelUID => kAudioDevicePropertyModelUID,
+ Property::StreamLatency => kAudioStreamPropertyLatency,
+ Property::StreamTerminalType => kAudioStreamPropertyTerminalType,
+ Property::StreamVirtualFormat => kAudioStreamPropertyVirtualFormat,
+ Property::TransportType => kAudioDevicePropertyTransportType,
+ Property::ClockDomain => kAudioDevicePropertyClockDomain,
+ }
+ }
+}
+
+pub fn get_property_address(property: Property, devtype: DeviceType) -> AudioObjectPropertyAddress {
+ const GLOBAL: ffi::cubeb_device_type =
+ ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT;
+ let scope = match devtype.bits() {
+ ffi::CUBEB_DEVICE_TYPE_INPUT => kAudioDevicePropertyScopeInput,
+ ffi::CUBEB_DEVICE_TYPE_OUTPUT => kAudioDevicePropertyScopeOutput,
+ GLOBAL => kAudioObjectPropertyScopeGlobal,
+ _ => panic!("Invalid type"),
+ };
+ AudioObjectPropertyAddress {
+ mSelector: property.into(),
+ mScope: scope,
+ mElement: kAudioObjectPropertyElementMaster,
+ }
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/mixer.rs b/third_party/rust/cubeb-coreaudio/src/backend/mixer.rs
new file mode 100644
index 0000000000..a4f63926b1
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/mixer.rs
@@ -0,0 +1,492 @@
+use cubeb_backend::{ChannelLayout, SampleFormat};
+use std::mem;
+use std::os::raw::{c_int, c_void};
+
+extern crate audio_mixer;
+pub use self::audio_mixer::Channel;
+
+const CHANNEL_OERDER: [audio_mixer::Channel; audio_mixer::Channel::count()] = [
+ audio_mixer::Channel::FrontLeft,
+ audio_mixer::Channel::FrontRight,
+ audio_mixer::Channel::FrontCenter,
+ audio_mixer::Channel::LowFrequency,
+ audio_mixer::Channel::BackLeft,
+ audio_mixer::Channel::BackRight,
+ audio_mixer::Channel::FrontLeftOfCenter,
+ audio_mixer::Channel::FrontRightOfCenter,
+ audio_mixer::Channel::BackCenter,
+ audio_mixer::Channel::SideLeft,
+ audio_mixer::Channel::SideRight,
+ audio_mixer::Channel::TopCenter,
+ audio_mixer::Channel::TopFrontLeft,
+ audio_mixer::Channel::TopFrontCenter,
+ audio_mixer::Channel::TopFrontRight,
+ audio_mixer::Channel::TopBackLeft,
+ audio_mixer::Channel::TopBackCenter,
+ audio_mixer::Channel::TopBackRight,
+ audio_mixer::Channel::Silence,
+];
+
+pub fn get_channel_order(channel_layout: ChannelLayout) -> Vec<audio_mixer::Channel> {
+ let mut map = channel_layout.bits();
+ let mut order = Vec::new();
+ let mut channel_index: usize = 0;
+ while map != 0 {
+ if map & 1 == 1 {
+ order.push(CHANNEL_OERDER[channel_index]);
+ }
+ map >>= 1;
+ channel_index += 1;
+ }
+ order
+}
+
+fn get_default_channel_order(channel_count: usize) -> Vec<audio_mixer::Channel> {
+ assert_ne!(channel_count, 0);
+ let mut channels = Vec::with_capacity(channel_count);
+ for channel in CHANNEL_OERDER.iter().take(channel_count) {
+ channels.push(*channel);
+ }
+
+ if channel_count > CHANNEL_OERDER.len() {
+ channels.extend(vec![
+ audio_mixer::Channel::Silence;
+ channel_count - CHANNEL_OERDER.len()
+ ]);
+ }
+
+ channels
+}
+
+#[derive(Debug)]
+enum MixerType {
+ IntegerMixer(audio_mixer::Mixer<i16>),
+ FloatMixer(audio_mixer::Mixer<f32>),
+}
+
+impl MixerType {
+ fn new(
+ format: SampleFormat,
+ input_channels: &[audio_mixer::Channel],
+ output_channels: &[audio_mixer::Channel],
+ ) -> Self {
+ match format {
+ SampleFormat::S16LE | SampleFormat::S16BE | SampleFormat::S16NE => {
+ cubeb_log!("Create an integer type(i16) mixer");
+ Self::IntegerMixer(audio_mixer::Mixer::<i16>::new(
+ input_channels,
+ output_channels,
+ ))
+ }
+ SampleFormat::Float32LE | SampleFormat::Float32BE | SampleFormat::Float32NE => {
+ cubeb_log!("Create an floating type(f32) mixer");
+ Self::FloatMixer(audio_mixer::Mixer::<f32>::new(
+ input_channels,
+ output_channels,
+ ))
+ }
+ }
+ }
+
+ fn sample_size(&self) -> usize {
+ match self {
+ MixerType::IntegerMixer(_) => mem::size_of::<i16>(),
+ MixerType::FloatMixer(_) => mem::size_of::<f32>(),
+ }
+ }
+
+ fn input_channels(&self) -> &[Channel] {
+ match self {
+ MixerType::IntegerMixer(m) => m.input_channels(),
+ MixerType::FloatMixer(m) => m.input_channels(),
+ }
+ }
+
+ fn output_channels(&self) -> &[Channel] {
+ match self {
+ MixerType::IntegerMixer(m) => m.output_channels(),
+ MixerType::FloatMixer(m) => m.output_channels(),
+ }
+ }
+
+ fn mix(
+ &self,
+ input_buffer_ptr: *const (),
+ input_buffer_size: usize,
+ output_buffer_ptr: *mut (),
+ output_buffer_size: usize,
+ frames: usize,
+ ) {
+ use std::slice;
+
+ // Check input buffer size.
+ let size_needed = frames * self.input_channels().len() * self.sample_size();
+ assert!(input_buffer_size >= size_needed);
+ // Check output buffer size.
+ let size_needed = frames * self.output_channels().len() * self.sample_size();
+ assert!(output_buffer_size >= size_needed);
+
+ match self {
+ MixerType::IntegerMixer(m) => {
+ let in_buf_ptr = input_buffer_ptr as *const i16;
+ let out_buf_ptr = output_buffer_ptr as *mut i16;
+ let input_buffer = unsafe {
+ slice::from_raw_parts(in_buf_ptr, frames * self.input_channels().len())
+ };
+ let output_buffer = unsafe {
+ slice::from_raw_parts_mut(out_buf_ptr, frames * self.output_channels().len())
+ };
+ let mut in_buf = input_buffer.chunks(self.input_channels().len());
+ let mut out_buf = output_buffer.chunks_mut(self.output_channels().len());
+ for _ in 0..frames {
+ m.mix(in_buf.next().unwrap(), out_buf.next().unwrap());
+ }
+ }
+ MixerType::FloatMixer(m) => {
+ let in_buf_ptr = input_buffer_ptr as *const f32;
+ let out_buf_ptr = output_buffer_ptr as *mut f32;
+ let input_buffer = unsafe {
+ slice::from_raw_parts(in_buf_ptr, frames * self.input_channels().len())
+ };
+ let output_buffer = unsafe {
+ slice::from_raw_parts_mut(out_buf_ptr, frames * self.output_channels().len())
+ };
+ let mut in_buf = input_buffer.chunks(self.input_channels().len());
+ let mut out_buf = output_buffer.chunks_mut(self.output_channels().len());
+ for _ in 0..frames {
+ m.mix(in_buf.next().unwrap(), out_buf.next().unwrap());
+ }
+ }
+ };
+ }
+}
+
+#[derive(Debug)]
+pub struct Mixer {
+ mixer: MixerType,
+ // Only accessed from callback thread.
+ buffer: Vec<u8>,
+}
+
+impl Mixer {
+ pub fn new(
+ format: SampleFormat,
+ in_channel_count: usize,
+ input_layout: ChannelLayout,
+ out_channel_count: usize,
+ mut output_channels: Vec<audio_mixer::Channel>,
+ ) -> Self {
+ assert!(in_channel_count > 0);
+ assert!(out_channel_count > 0);
+
+ cubeb_log!(
+ "Creating a mixer with input channel count: {}, input layout: {:?},\
+ out channel count: {}, output channels: {:?}",
+ in_channel_count,
+ input_layout,
+ out_channel_count,
+ output_channels
+ );
+
+ let input_channels = if in_channel_count as u32 != input_layout.bits().count_ones() {
+ cubeb_log!(
+ "Mismatch between input channels and layout. Applying default layout instead"
+ );
+ get_default_channel_order(in_channel_count)
+ } else {
+ get_channel_order(input_layout)
+ };
+
+ // When having one or two channel, force mono or stereo. Some devices (namely,
+ // Bose QC35, mark 1 and 2), expose a single channel mapped to the right for
+ // some reason. Some devices (e.g., builtin speaker on MacBook Pro 2018) map
+ // the channel layout to the undefined channels.
+ if out_channel_count == 1 {
+ output_channels = vec![audio_mixer::Channel::FrontCenter];
+ } else if out_channel_count == 2 {
+ output_channels = vec![
+ audio_mixer::Channel::FrontLeft,
+ audio_mixer::Channel::FrontRight,
+ ];
+ }
+
+ let all_silence = vec![audio_mixer::Channel::Silence; out_channel_count];
+ if output_channels.is_empty()
+ || out_channel_count != output_channels.len()
+ || all_silence == output_channels
+ || Self::non_silent_duplicate_channel_present(&output_channels)
+ {
+ cubeb_log!("Use invalid layout. Apply default layout instead");
+ output_channels = get_default_channel_order(out_channel_count);
+ }
+
+ Self {
+ mixer: MixerType::new(format, &input_channels, &output_channels),
+ buffer: Vec::new(),
+ }
+ }
+
+ pub fn update_buffer_size(&mut self, frames: usize) -> bool {
+ let size_needed = frames * self.mixer.input_channels().len() * self.mixer.sample_size();
+ let elements_needed = size_needed / mem::size_of::<u8>();
+ if self.buffer.len() < elements_needed {
+ self.buffer.resize(elements_needed, 0);
+ true
+ } else {
+ false
+ }
+ }
+
+ pub fn get_buffer_mut_ptr(&mut self) -> *mut u8 {
+ self.buffer.as_mut_ptr()
+ }
+
+ // `update_buffer_size` must be called before this.
+ pub fn mix(&self, frames: usize, dest_buffer: *mut c_void, dest_buffer_size: usize) -> c_int {
+ let (src_buffer_ptr, src_buffer_size) = self.get_buffer_info();
+ self.mixer.mix(
+ src_buffer_ptr as *const (),
+ src_buffer_size,
+ dest_buffer as *mut (),
+ dest_buffer_size,
+ frames,
+ );
+ 0
+ }
+
+ fn get_buffer_info(&self) -> (*const u8, usize) {
+ (
+ self.buffer.as_ptr(),
+ self.buffer.len() * mem::size_of::<u8>(),
+ )
+ }
+
+ fn non_silent_duplicate_channel_present(channels: &[audio_mixer::Channel]) -> bool {
+ let mut bitmap: u32 = 0;
+ for channel in channels {
+ if channel != &Channel::Silence {
+ if (bitmap & channel.bitmask()) != 0 {
+ return true;
+ }
+ bitmap |= channel.bitmask();
+ }
+ }
+ false
+ }
+}
+
+// This test gives a clear channel order of the ChannelLayout passed from cubeb interface.
+#[test]
+fn test_get_channel_order() {
+ assert_eq!(
+ get_channel_order(ChannelLayout::MONO),
+ [Channel::FrontCenter]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::MONO_LFE),
+ [Channel::FrontCenter, Channel::LowFrequency]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::STEREO),
+ [Channel::FrontLeft, Channel::FrontRight]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::STEREO_LFE),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::LowFrequency
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_3F),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::FrontCenter
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_3F_LFE),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::FrontCenter,
+ Channel::LowFrequency
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_2F1),
+ [Channel::FrontLeft, Channel::FrontRight, Channel::BackCenter]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_2F1_LFE),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::LowFrequency,
+ Channel::BackCenter
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_3F1),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::FrontCenter,
+ Channel::BackCenter
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_3F1_LFE),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::FrontCenter,
+ Channel::LowFrequency,
+ Channel::BackCenter
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_2F2),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::SideLeft,
+ Channel::SideRight
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_2F2_LFE),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::LowFrequency,
+ Channel::SideLeft,
+ Channel::SideRight
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::QUAD),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::BackLeft,
+ Channel::BackRight
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::QUAD_LFE),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::LowFrequency,
+ Channel::BackLeft,
+ Channel::BackRight
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_3F2),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::FrontCenter,
+ Channel::SideLeft,
+ Channel::SideRight
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_3F2_LFE),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::FrontCenter,
+ Channel::LowFrequency,
+ Channel::SideLeft,
+ Channel::SideRight
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_3F2_BACK),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::FrontCenter,
+ Channel::BackLeft,
+ Channel::BackRight
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_3F2_LFE_BACK),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::FrontCenter,
+ Channel::LowFrequency,
+ Channel::BackLeft,
+ Channel::BackRight
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_3F3R_LFE),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::FrontCenter,
+ Channel::LowFrequency,
+ Channel::BackCenter,
+ Channel::SideLeft,
+ Channel::SideRight
+ ]
+ );
+ assert_eq!(
+ get_channel_order(ChannelLayout::_3F4_LFE),
+ [
+ Channel::FrontLeft,
+ Channel::FrontRight,
+ Channel::FrontCenter,
+ Channel::LowFrequency,
+ Channel::BackLeft,
+ Channel::BackRight,
+ Channel::SideLeft,
+ Channel::SideRight
+ ]
+ );
+}
+
+#[test]
+fn test_get_default_channel_order() {
+ for len in 1..CHANNEL_OERDER.len() + 10 {
+ let channels = get_default_channel_order(len);
+ if len <= CHANNEL_OERDER.len() {
+ assert_eq!(channels, &CHANNEL_OERDER[..len]);
+ } else {
+ let silences = vec![audio_mixer::Channel::Silence; len - CHANNEL_OERDER.len()];
+ assert_eq!(channels[..CHANNEL_OERDER.len()], CHANNEL_OERDER);
+ assert_eq!(&channels[CHANNEL_OERDER.len()..], silences.as_slice());
+ }
+ }
+}
+
+#[test]
+fn test_non_silent_duplicate_channels() {
+ let duplicate = [
+ Channel::FrontLeft,
+ Channel::Silence,
+ Channel::FrontRight,
+ Channel::FrontCenter,
+ Channel::Silence,
+ Channel::FrontRight,
+ ];
+ assert!(Mixer::non_silent_duplicate_channel_present(&duplicate));
+
+ let non_duplicate = [
+ Channel::FrontLeft,
+ Channel::Silence,
+ Channel::FrontRight,
+ Channel::FrontCenter,
+ Channel::Silence,
+ Channel::Silence,
+ ];
+ assert!(!Mixer::non_silent_duplicate_channel_present(&non_duplicate));
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/mod.rs b/third_party/rust/cubeb-coreaudio/src/backend/mod.rs
new file mode 100644
index 0000000000..61ae44fea1
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/mod.rs
@@ -0,0 +1,4423 @@
+// Copyright © 2018 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details.
+#![allow(unused_assignments)]
+#![allow(unused_must_use)]
+
+extern crate coreaudio_sys_utils;
+extern crate libc;
+extern crate ringbuf;
+
+mod aggregate_device;
+mod auto_release;
+mod buffer_manager;
+mod device_property;
+mod mixer;
+mod resampler;
+mod utils;
+
+use self::aggregate_device::*;
+use self::auto_release::*;
+use self::buffer_manager::*;
+use self::coreaudio_sys_utils::aggregate_device::*;
+use self::coreaudio_sys_utils::audio_device_extensions::*;
+use self::coreaudio_sys_utils::audio_object::*;
+use self::coreaudio_sys_utils::audio_unit::*;
+use self::coreaudio_sys_utils::cf_mutable_dict::*;
+use self::coreaudio_sys_utils::dispatch::*;
+use self::coreaudio_sys_utils::string::*;
+use self::coreaudio_sys_utils::sys::*;
+use self::device_property::*;
+use self::mixer::*;
+use self::resampler::*;
+use self::utils::*;
+use backend::ringbuf::RingBuffer;
+use cubeb_backend::{
+ ffi, ChannelLayout, Context, ContextOps, DeviceCollectionRef, DeviceId, DeviceRef, DeviceType,
+ Error, InputProcessingParams, Ops, Result, SampleFormat, State, Stream, StreamOps,
+ StreamParams, StreamParamsRef, StreamPrefs,
+};
+use mach::mach_time::{mach_absolute_time, mach_timebase_info};
+use std::cmp;
+use std::ffi::{CStr, CString};
+use std::fmt;
+use std::mem;
+use std::os::raw::{c_uint, c_void};
+use std::ptr;
+use std::slice;
+use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
+use std::sync::{Arc, Condvar, Mutex};
+use std::time::Duration;
+const NO_ERR: OSStatus = 0;
+
+const AU_OUT_BUS: AudioUnitElement = 0;
+const AU_IN_BUS: AudioUnitElement = 1;
+
+const DISPATCH_QUEUE_LABEL: &str = "org.mozilla.cubeb";
+const PRIVATE_AGGREGATE_DEVICE_NAME: &str = "CubebAggregateDevice";
+const VOICEPROCESSING_AGGREGATE_DEVICE_NAME: &str = "VPAUAggregateAudioDevice";
+
+const APPLE_STUDIO_DISPLAY_USB_ID: &str = "05AC:1114";
+
+// Testing empirically, some headsets report a minimal latency that is very low,
+// but this does not work in practice. Lie and say the minimum is 128 frames.
+const SAFE_MIN_LATENCY_FRAMES: u32 = 128;
+const SAFE_MAX_LATENCY_FRAMES: u32 = 512;
+
+bitflags! {
+ #[allow(non_camel_case_types)]
+ #[derive(Clone, Debug, PartialEq, Copy)]
+ struct device_flags: u32 {
+ const DEV_UNKNOWN = 0b0000_0000; // Unknown
+ const DEV_INPUT = 0b0000_0001; // Record device like mic
+ const DEV_OUTPUT = 0b0000_0010; // Playback device like speakers
+ const DEV_SELECTED_DEFAULT = 0b0000_0100; // User selected to use the system default device
+ }
+}
+
+lazy_static! {
+ static ref HOST_TIME_TO_NS_RATIO: (u32, u32) = {
+ let mut timebase_info = mach_timebase_info { numer: 0, denom: 0 };
+ unsafe {
+ mach_timebase_info(&mut timebase_info);
+ }
+ (timebase_info.numer, timebase_info.denom)
+ };
+}
+
+fn make_sized_audio_channel_layout(sz: usize) -> AutoRelease<AudioChannelLayout> {
+ assert!(sz >= mem::size_of::<AudioChannelLayout>());
+ assert_eq!(
+ (sz - mem::size_of::<AudioChannelLayout>()) % mem::size_of::<AudioChannelDescription>(),
+ 0
+ );
+ let acl = unsafe { libc::calloc(1, sz) } as *mut AudioChannelLayout;
+
+ unsafe extern "C" fn free_acl(acl: *mut AudioChannelLayout) {
+ libc::free(acl as *mut libc::c_void);
+ }
+
+ AutoRelease::new(acl, free_acl)
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Clone, Debug)]
+struct device_info {
+ id: AudioDeviceID,
+ flags: device_flags,
+}
+
+impl Default for device_info {
+ fn default() -> Self {
+ Self {
+ id: kAudioObjectUnknown,
+ flags: device_flags::DEV_UNKNOWN,
+ }
+ }
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Debug)]
+struct device_property_listener {
+ device: AudioDeviceID,
+ property: AudioObjectPropertyAddress,
+ listener: audio_object_property_listener_proc,
+}
+
+impl device_property_listener {
+ fn new(
+ device: AudioDeviceID,
+ property: AudioObjectPropertyAddress,
+ listener: audio_object_property_listener_proc,
+ ) -> Self {
+ Self {
+ device,
+ property,
+ listener,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+struct CAChannelLabel(AudioChannelLabel);
+
+impl From<CAChannelLabel> for mixer::Channel {
+ fn from(label: CAChannelLabel) -> mixer::Channel {
+ use self::coreaudio_sys_utils::sys;
+ match label.0 {
+ sys::kAudioChannelLabel_Left => mixer::Channel::FrontLeft,
+ sys::kAudioChannelLabel_Right => mixer::Channel::FrontRight,
+ sys::kAudioChannelLabel_Center | sys::kAudioChannelLabel_Mono => {
+ mixer::Channel::FrontCenter
+ }
+ sys::kAudioChannelLabel_LFEScreen => mixer::Channel::LowFrequency,
+ sys::kAudioChannelLabel_LeftSurround => mixer::Channel::BackLeft,
+ sys::kAudioChannelLabel_RightSurround => mixer::Channel::BackRight,
+ sys::kAudioChannelLabel_LeftCenter => mixer::Channel::FrontLeftOfCenter,
+ sys::kAudioChannelLabel_RightCenter => mixer::Channel::FrontRightOfCenter,
+ sys::kAudioChannelLabel_CenterSurround => mixer::Channel::BackCenter,
+ sys::kAudioChannelLabel_LeftSurroundDirect => mixer::Channel::SideLeft,
+ sys::kAudioChannelLabel_RightSurroundDirect => mixer::Channel::SideRight,
+ sys::kAudioChannelLabel_TopCenterSurround => mixer::Channel::TopCenter,
+ sys::kAudioChannelLabel_VerticalHeightLeft => mixer::Channel::TopFrontLeft,
+ sys::kAudioChannelLabel_VerticalHeightCenter => mixer::Channel::TopFrontCenter,
+ sys::kAudioChannelLabel_VerticalHeightRight => mixer::Channel::TopFrontRight,
+ sys::kAudioChannelLabel_TopBackLeft => mixer::Channel::TopBackLeft,
+ sys::kAudioChannelLabel_TopBackCenter => mixer::Channel::TopBackCenter,
+ sys::kAudioChannelLabel_TopBackRight => mixer::Channel::TopBackRight,
+ _ => mixer::Channel::Silence,
+ }
+ }
+}
+
+fn set_notification_runloop() {
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioHardwarePropertyRunLoop,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ // Ask HAL to manage its own thread for notification by setting the run_loop to NULL.
+ // Otherwise HAL may use main thread to fire notifications.
+ let run_loop: CFRunLoopRef = ptr::null_mut();
+ let size = mem::size_of::<CFRunLoopRef>();
+ let status =
+ audio_object_set_property_data(kAudioObjectSystemObject, &address, size, &run_loop);
+ if status != NO_ERR {
+ cubeb_log!("Could not make global CoreAudio notifications use their own thread.");
+ }
+}
+
+fn create_device_info(devid: AudioDeviceID, devtype: DeviceType) -> Option<device_info> {
+ assert_ne!(devid, kAudioObjectSystemObject);
+
+ let mut flags = match devtype {
+ DeviceType::INPUT => device_flags::DEV_INPUT,
+ DeviceType::OUTPUT => device_flags::DEV_OUTPUT,
+ _ => panic!("Only accept input or output type"),
+ };
+
+ if devid == kAudioObjectUnknown {
+ cubeb_log!("Using the system default device");
+ flags |= device_flags::DEV_SELECTED_DEFAULT;
+ get_default_device(devtype).map(|id| device_info { id, flags })
+ } else {
+ Some(device_info { id: devid, flags })
+ }
+}
+
+fn create_stream_description(stream_params: &StreamParams) -> Result<AudioStreamBasicDescription> {
+ assert!(stream_params.rate() > 0);
+ assert!(stream_params.channels() > 0);
+
+ let mut desc = AudioStreamBasicDescription::default();
+
+ match stream_params.format() {
+ SampleFormat::S16LE => {
+ desc.mBitsPerChannel = 16;
+ desc.mFormatFlags = kAudioFormatFlagIsSignedInteger;
+ }
+ SampleFormat::S16BE => {
+ desc.mBitsPerChannel = 16;
+ desc.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsBigEndian;
+ }
+ SampleFormat::Float32LE => {
+ desc.mBitsPerChannel = 32;
+ desc.mFormatFlags = kAudioFormatFlagIsFloat;
+ }
+ SampleFormat::Float32BE => {
+ desc.mBitsPerChannel = 32;
+ desc.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsBigEndian;
+ }
+ _ => {
+ return Err(Error::invalid_format());
+ }
+ }
+
+ desc.mFormatID = kAudioFormatLinearPCM;
+ desc.mFormatFlags |= kLinearPCMFormatFlagIsPacked;
+ desc.mSampleRate = f64::from(stream_params.rate());
+ desc.mChannelsPerFrame = stream_params.channels();
+
+ desc.mBytesPerFrame = (desc.mBitsPerChannel / 8) * desc.mChannelsPerFrame;
+ desc.mFramesPerPacket = 1;
+ desc.mBytesPerPacket = desc.mBytesPerFrame * desc.mFramesPerPacket;
+
+ desc.mReserved = 0;
+
+ Ok(desc)
+}
+
+fn set_volume(unit: AudioUnit, volume: f32) -> Result<()> {
+ assert!(!unit.is_null());
+ let r = audio_unit_set_parameter(
+ unit,
+ kHALOutputParam_Volume,
+ kAudioUnitScope_Global,
+ 0,
+ volume,
+ 0,
+ );
+ if r == NO_ERR {
+ Ok(())
+ } else {
+ cubeb_log!("AudioUnitSetParameter/kHALOutputParam_Volume rv={}", r);
+ Err(Error::error())
+ }
+}
+
+fn get_volume(unit: AudioUnit) -> Result<f32> {
+ assert!(!unit.is_null());
+ let mut volume: f32 = 0.0;
+ let r = audio_unit_get_parameter(
+ unit,
+ kHALOutputParam_Volume,
+ kAudioUnitScope_Global,
+ 0,
+ &mut volume,
+ );
+ if r == NO_ERR {
+ Ok(volume)
+ } else {
+ cubeb_log!("AudioUnitGetParameter/kHALOutputParam_Volume rv={}", r);
+ Err(Error::error())
+ }
+}
+
+fn set_input_mute(unit: AudioUnit, mute: bool) -> Result<()> {
+ assert!(!unit.is_null());
+ let mute: u32 = mute.into();
+ let mut old_mute: u32 = 0;
+ let r = audio_unit_get_property(
+ unit,
+ kAUVoiceIOProperty_MuteOutput,
+ kAudioUnitScope_Global,
+ AU_IN_BUS,
+ &mut old_mute,
+ &mut mem::size_of::<u32>(),
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitGetProperty/kAUVoiceIOProperty_MuteOutput rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+ if old_mute == mute {
+ return Ok(());
+ }
+ let r = audio_unit_set_property(
+ unit,
+ kAUVoiceIOProperty_MuteOutput,
+ kAudioUnitScope_Global,
+ AU_IN_BUS,
+ &mute,
+ mem::size_of::<u32>(),
+ );
+ if r == NO_ERR {
+ Ok(())
+ } else {
+ cubeb_log!(
+ "AudioUnitSetProperty/kAUVoiceIOProperty_MuteOutput rv={}",
+ r
+ );
+ Err(Error::error())
+ }
+}
+
+fn set_input_processing_params(unit: AudioUnit, params: InputProcessingParams) -> Result<()> {
+ assert!(!unit.is_null());
+ let aec = params.contains(InputProcessingParams::ECHO_CANCELLATION);
+ let ns = params.contains(InputProcessingParams::NOISE_SUPPRESSION);
+ let agc = params.contains(InputProcessingParams::AUTOMATIC_GAIN_CONTROL);
+
+ // AEC and NS are active as soon as VPIO is not bypassed, therefore the only combinations
+ // of those we can explicitly support are {} and {aec, ns}.
+ if aec != ns {
+ // No control to turn on AEC without NS or vice versa.
+ return Err(Error::error());
+ }
+
+ let mut old_agc: u32 = 0;
+ let r = audio_unit_get_property(
+ unit,
+ kAUVoiceIOProperty_VoiceProcessingEnableAGC,
+ kAudioUnitScope_Global,
+ AU_IN_BUS,
+ &mut old_agc,
+ &mut mem::size_of::<u32>(),
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitGetProperty/kAUVoiceIOProperty_VoiceProcessingEnableAGC rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+
+ if (old_agc == 1) != agc {
+ let agc = u32::from(agc);
+ let r = audio_unit_set_property(
+ unit,
+ kAUVoiceIOProperty_VoiceProcessingEnableAGC,
+ kAudioUnitScope_Global,
+ AU_IN_BUS,
+ &agc,
+ mem::size_of::<u32>(),
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitSetProperty/kAUVoiceIOProperty_VoiceProcessingEnableAGC rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+ }
+
+ let mut old_bypass: u32 = 0;
+ let r = audio_unit_get_property(
+ unit,
+ kAUVoiceIOProperty_BypassVoiceProcessing,
+ kAudioUnitScope_Global,
+ AU_IN_BUS,
+ &mut old_bypass,
+ &mut mem::size_of::<u32>(),
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitGetProperty/kAUVoiceIOProperty_BypassVoiceProcessing rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+
+ let bypass = u32::from(!aec);
+ if old_bypass != bypass {
+ let r = audio_unit_set_property(
+ unit,
+ kAUVoiceIOProperty_BypassVoiceProcessing,
+ kAudioUnitScope_Global,
+ AU_IN_BUS,
+ &bypass,
+ mem::size_of::<u32>(),
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitSetProperty/kAUVoiceIOProperty_BypassVoiceProcessing rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+ }
+
+ Ok(())
+}
+
+fn minimum_resampling_input_frames(
+ input_rate: f64,
+ output_rate: f64,
+ output_frames: usize,
+) -> usize {
+ assert!(!approx_eq!(f64, input_rate, 0_f64));
+ assert!(!approx_eq!(f64, output_rate, 0_f64));
+ if approx_eq!(f64, input_rate, output_rate) {
+ return output_frames;
+ }
+ (input_rate * output_frames as f64 / output_rate).ceil() as usize
+}
+
+fn audiounit_make_silent(io_data: &AudioBuffer) {
+ assert!(!io_data.mData.is_null());
+ let bytes = unsafe {
+ let ptr = io_data.mData as *mut u8;
+ let len = io_data.mDataByteSize as usize;
+ slice::from_raw_parts_mut(ptr, len)
+ };
+ for data in bytes.iter_mut() {
+ *data = 0;
+ }
+}
+
+extern "C" fn audiounit_input_callback(
+ user_ptr: *mut c_void,
+ flags: *mut AudioUnitRenderActionFlags,
+ tstamp: *const AudioTimeStamp,
+ bus: u32,
+ input_frames: u32,
+ _: *mut AudioBufferList,
+) -> OSStatus {
+ enum ErrorHandle {
+ Return(OSStatus),
+ Reinit,
+ }
+
+ assert!(input_frames > 0);
+ assert_eq!(bus, AU_IN_BUS);
+
+ assert!(!user_ptr.is_null());
+ let stm = unsafe { &mut *(user_ptr as *mut AudioUnitStream) };
+ let using_voice_processing_unit = stm.core_stream_data.using_voice_processing_unit();
+
+ if unsafe { *flags | kAudioTimeStampHostTimeValid } != 0 {
+ let now = unsafe { mach_absolute_time() };
+ let input_latency_frames = compute_input_latency(stm, unsafe { (*tstamp).mHostTime }, now);
+ stm.total_input_latency_frames
+ .store(input_latency_frames, Ordering::SeqCst);
+ }
+
+ if stm.stopped.load(Ordering::SeqCst) {
+ cubeb_log!("({:p}) input stopped", stm as *const AudioUnitStream);
+ return NO_ERR;
+ }
+
+ let handler = |stm: &mut AudioUnitStream,
+ flags: *mut AudioUnitRenderActionFlags,
+ tstamp: *const AudioTimeStamp,
+ bus: u32,
+ input_frames: u32|
+ -> ErrorHandle {
+ let input_buffer_manager = stm.core_stream_data.input_buffer_manager.as_mut().unwrap();
+ assert_eq!(
+ stm.core_stream_data.stm_ptr,
+ user_ptr as *const AudioUnitStream
+ );
+
+ // `flags` and `tstamp` must be non-null so they can be casted into the references.
+ assert!(!flags.is_null());
+ let flags = unsafe { &mut (*flags) };
+ assert!(!tstamp.is_null());
+ let tstamp = unsafe { &(*tstamp) };
+
+ // Create the AudioBufferList to store input.
+ let mut input_buffer_list = AudioBufferList::default();
+ input_buffer_list.mBuffers[0].mDataByteSize =
+ stm.core_stream_data.input_dev_desc.mBytesPerFrame * input_frames;
+ input_buffer_list.mBuffers[0].mData = ptr::null_mut();
+ input_buffer_list.mBuffers[0].mNumberChannels =
+ stm.core_stream_data.input_dev_desc.mChannelsPerFrame;
+ input_buffer_list.mNumberBuffers = 1;
+
+ debug_assert!(!stm.core_stream_data.input_unit.is_null());
+ let status = audio_unit_render(
+ stm.core_stream_data.input_unit,
+ flags,
+ tstamp,
+ bus,
+ input_frames,
+ &mut input_buffer_list,
+ );
+ if (status != NO_ERR)
+ && (status != kAudioUnitErr_CannotDoInCurrentContext
+ || stm.core_stream_data.output_unit.is_null())
+ {
+ return ErrorHandle::Return(status);
+ }
+ let handle = if status == kAudioUnitErr_CannotDoInCurrentContext {
+ assert!(!stm.core_stream_data.output_unit.is_null());
+ // kAudioUnitErr_CannotDoInCurrentContext is returned when using a BT
+ // headset and the profile is changed from A2DP to HFP/HSP. The previous
+ // output device is no longer valid and must be reset.
+ // For now state that no error occurred and feed silence, stream will be
+ // resumed once reinit has completed.
+ ErrorHandle::Reinit
+ } else {
+ assert_eq!(status, NO_ERR);
+ input_buffer_manager
+ .push_data(input_buffer_list.mBuffers[0].mData, input_frames as usize);
+ ErrorHandle::Return(status)
+ };
+
+ // Full Duplex. We'll call data_callback in the AudioUnit output callback. Record this
+ // callback for logging.
+ if !stm.core_stream_data.output_unit.is_null() {
+ let input_callback_data = InputCallbackData {
+ bytes: input_buffer_list.mBuffers[0].mDataByteSize,
+ rendered_frames: input_frames,
+ total_available: input_buffer_manager.available_frames(),
+ channels: input_buffer_list.mBuffers[0].mNumberChannels,
+ num_buf: input_buffer_list.mNumberBuffers,
+ };
+ stm.core_stream_data
+ .input_logging
+ .as_mut()
+ .unwrap()
+ .push(input_callback_data);
+ return handle;
+ }
+
+ cubeb_alogv!(
+ "({:p}) input: buffers {}, size {}, channels {}, rendered frames {}, total frames {}.",
+ stm.core_stream_data.stm_ptr,
+ input_buffer_list.mNumberBuffers,
+ input_buffer_list.mBuffers[0].mDataByteSize,
+ input_buffer_list.mBuffers[0].mNumberChannels,
+ input_frames,
+ input_buffer_manager.available_frames()
+ );
+
+ // Input only. Call the user callback through resampler.
+ // Resampler will deliver input buffer in the correct rate.
+ assert!(input_frames as usize <= input_buffer_manager.available_frames());
+ stm.frames_read.fetch_add(
+ input_buffer_manager.available_frames(),
+ atomic::Ordering::SeqCst,
+ );
+ let mut total_input_frames = input_buffer_manager.available_frames() as i64;
+ let input_buffer =
+ input_buffer_manager.get_linear_data(input_buffer_manager.available_frames());
+ let outframes = stm.core_stream_data.resampler.fill(
+ input_buffer,
+ &mut total_input_frames,
+ ptr::null_mut(),
+ 0,
+ );
+ if outframes < 0 {
+ stm.stopped.store(true, Ordering::SeqCst);
+ stm.notify_state_changed(State::Error);
+ let queue = stm.queue.clone();
+ // Use a new thread, through the queue, to avoid deadlock when calling
+ // AudioOutputUnitStop method from inside render callback
+ queue.run_async(move || {
+ stm.core_stream_data.stop_audiounits();
+ });
+ return handle;
+ }
+ if outframes < total_input_frames {
+ stm.draining.store(true, Ordering::SeqCst);
+ }
+
+ handle
+ };
+
+ // If the stream is drained, do nothing.
+ let handle = if !stm.draining.load(Ordering::SeqCst) {
+ handler(stm, flags, tstamp, bus, input_frames)
+ } else {
+ ErrorHandle::Return(NO_ERR)
+ };
+
+ // If the input (input-only stream) or the output is drained (duplex stream),
+ // cancel this callback. Note that for voice processing cases (a single unit),
+ // the output callback handles stopping the unit and notifying of state.
+ if !using_voice_processing_unit && stm.draining.load(Ordering::SeqCst) {
+ let r = stop_audiounit(stm.core_stream_data.input_unit);
+ assert!(r.is_ok());
+ // Only fire state-changed callback for input-only stream.
+ // The state-changed callback for the duplex stream is fired in the output callback.
+ if stm.core_stream_data.output_unit.is_null() {
+ stm.notify_state_changed(State::Drained);
+ }
+ }
+
+ match handle {
+ ErrorHandle::Reinit => {
+ stm.reinit_async();
+ NO_ERR
+ }
+ ErrorHandle::Return(s) => s,
+ }
+}
+
+fn host_time_to_ns(host_time: u64) -> u64 {
+ let mut rv: f64 = host_time as f64;
+ rv *= HOST_TIME_TO_NS_RATIO.0 as f64;
+ rv /= HOST_TIME_TO_NS_RATIO.1 as f64;
+ rv as u64
+}
+
+fn compute_output_latency(stm: &AudioUnitStream, audio_output_time: u64, now: u64) -> u32 {
+ const NS2S: u64 = 1_000_000_000;
+ let output_hw_rate = stm.core_stream_data.output_dev_desc.mSampleRate as u64;
+ let fixed_latency_ns =
+ (stm.output_device_latency_frames.load(Ordering::SeqCst) as u64 * NS2S) / output_hw_rate;
+ // The total output latency is the timestamp difference + the stream latency + the hardware
+ // latency.
+ let total_output_latency_ns =
+ fixed_latency_ns + host_time_to_ns(audio_output_time.saturating_sub(now));
+
+ (total_output_latency_ns * output_hw_rate / NS2S) as u32
+}
+
+fn compute_input_latency(stm: &AudioUnitStream, audio_input_time: u64, now: u64) -> u32 {
+ const NS2S: u64 = 1_000_000_000;
+ let input_hw_rate = stm.core_stream_data.input_dev_desc.mSampleRate as u64;
+ let fixed_latency_ns =
+ (stm.input_device_latency_frames.load(Ordering::SeqCst) as u64 * NS2S) / input_hw_rate;
+ // The total input latency is the timestamp difference + the stream latency +
+ // the hardware latency.
+ let total_input_latency_ns =
+ host_time_to_ns(now.saturating_sub(audio_input_time)) + fixed_latency_ns;
+
+ (total_input_latency_ns * input_hw_rate / NS2S) as u32
+}
+
+extern "C" fn audiounit_output_callback(
+ user_ptr: *mut c_void,
+ flags: *mut AudioUnitRenderActionFlags,
+ tstamp: *const AudioTimeStamp,
+ bus: u32,
+ output_frames: u32,
+ out_buffer_list: *mut AudioBufferList,
+) -> OSStatus {
+ assert_eq!(bus, AU_OUT_BUS);
+ assert!(!out_buffer_list.is_null());
+
+ assert!(!user_ptr.is_null());
+ let stm = unsafe { &mut *(user_ptr as *mut AudioUnitStream) };
+
+ let out_buffer_list_ref = unsafe { &mut (*out_buffer_list) };
+ assert_eq!(out_buffer_list_ref.mNumberBuffers, 1);
+ let buffers = unsafe {
+ let ptr = out_buffer_list_ref.mBuffers.as_mut_ptr();
+ let len = out_buffer_list_ref.mNumberBuffers as usize;
+ slice::from_raw_parts_mut(ptr, len)
+ };
+
+ if stm.stopped.load(Ordering::SeqCst) {
+ cubeb_alog!("({:p}) output stopped.", stm as *const AudioUnitStream);
+ audiounit_make_silent(&buffers[0]);
+ return NO_ERR;
+ }
+
+ if stm.draining.load(Ordering::SeqCst) {
+ // Cancel the output callback only. For duplex stream,
+ // the input callback will be cancelled in its own callback.
+ let r = stop_audiounit(stm.core_stream_data.output_unit);
+ assert!(r.is_ok());
+ stm.notify_state_changed(State::Drained);
+ audiounit_make_silent(&buffers[0]);
+ return NO_ERR;
+ }
+
+ let now = unsafe { mach_absolute_time() };
+
+ if unsafe { *flags | kAudioTimeStampHostTimeValid } != 0 {
+ let output_latency_frames =
+ compute_output_latency(stm, unsafe { (*tstamp).mHostTime }, now);
+ stm.total_output_latency_frames
+ .store(output_latency_frames, Ordering::SeqCst);
+ }
+ // Get output buffer
+ let output_buffer = match stm.core_stream_data.mixer.as_mut() {
+ None => buffers[0].mData,
+ Some(mixer) => {
+ // If remixing needs to occur, we can't directly work in our final
+ // destination buffer as data may be overwritten or too small to start with.
+ mixer.update_buffer_size(output_frames as usize);
+ mixer.get_buffer_mut_ptr() as *mut c_void
+ }
+ };
+
+ let prev_frames_written = stm.frames_written.load(Ordering::SeqCst);
+
+ stm.frames_written
+ .fetch_add(output_frames as usize, Ordering::SeqCst);
+
+ // Also get the input buffer if the stream is duplex
+ let (input_buffer, mut input_frames) = if !stm.core_stream_data.input_unit.is_null() {
+ let input_logging = &mut stm.core_stream_data.input_logging.as_mut().unwrap();
+ if input_logging.is_empty() {
+ cubeb_alogv!("no audio input data in output callback");
+ } else {
+ while let Some(input_callback_data) = input_logging.pop() {
+ cubeb_alogv!(
+ "input: buffers {}, size {}, channels {}, rendered frames {}, total frames {}.",
+ input_callback_data.num_buf,
+ input_callback_data.bytes,
+ input_callback_data.channels,
+ input_callback_data.rendered_frames,
+ input_callback_data.total_available
+ );
+ }
+ }
+ let input_buffer_manager = stm.core_stream_data.input_buffer_manager.as_mut().unwrap();
+ assert_ne!(stm.core_stream_data.input_dev_desc.mChannelsPerFrame, 0);
+ // If the output callback came first and this is a duplex stream, we need to
+ // fill in some additional silence in the resampler.
+ // Otherwise, if we had more than expected callbacks in a row, or we're
+ // currently switching, we add some silence as well to compensate for the
+ // fact that we're lacking some input data.
+ let input_frames_needed = minimum_resampling_input_frames(
+ stm.core_stream_data.input_dev_desc.mSampleRate,
+ f64::from(stm.core_stream_data.output_stream_params.rate()),
+ output_frames as usize,
+ );
+ let buffered_input_frames = input_buffer_manager.available_frames();
+ // Else if the input has buffered a lot already because the output started late, we
+ // need to trim the input buffer
+ if prev_frames_written == 0 && buffered_input_frames > input_frames_needed {
+ input_buffer_manager.trim(input_frames_needed);
+ let popped_frames = buffered_input_frames - input_frames_needed;
+ cubeb_alog!("Dropping {} frames in input buffer.", popped_frames);
+ }
+
+ let input_frames = if input_frames_needed > buffered_input_frames
+ && (stm.switching_device.load(Ordering::SeqCst)
+ || stm.reinit_pending.load(Ordering::SeqCst)
+ || stm.frames_read.load(Ordering::SeqCst) == 0)
+ {
+ // The silent frames will be inserted in `get_linear_data` below.
+ let silent_frames_to_push = input_frames_needed - buffered_input_frames;
+ cubeb_alog!(
+ "({:p}) Missing Frames: {} will append {} frames of input silence.",
+ stm.core_stream_data.stm_ptr,
+ if stm.frames_read.load(Ordering::SeqCst) == 0 {
+ "input hasn't started,"
+ } else if stm.switching_device.load(Ordering::SeqCst) {
+ "device switching,"
+ } else {
+ "reinit pending,"
+ },
+ silent_frames_to_push
+ );
+ input_frames_needed
+ } else {
+ buffered_input_frames
+ };
+
+ stm.frames_read.fetch_add(input_frames, Ordering::SeqCst);
+ (
+ input_buffer_manager.get_linear_data(input_frames),
+ input_frames as i64,
+ )
+ } else {
+ (ptr::null_mut::<c_void>(), 0)
+ };
+
+ cubeb_alogv!(
+ "({:p}) output: buffers {}, size {}, channels {}, frames {}.",
+ stm as *const AudioUnitStream,
+ buffers.len(),
+ buffers[0].mDataByteSize,
+ buffers[0].mNumberChannels,
+ output_frames
+ );
+
+ let outframes = stm.core_stream_data.resampler.fill(
+ input_buffer,
+ if input_buffer.is_null() {
+ ptr::null_mut()
+ } else {
+ &mut input_frames
+ },
+ output_buffer,
+ i64::from(output_frames),
+ );
+
+ if outframes < 0 || outframes > i64::from(output_frames) {
+ stm.stopped.store(true, Ordering::SeqCst);
+ stm.notify_state_changed(State::Error);
+ let queue = stm.queue.clone();
+ // Use a new thread, through the queue, to avoid deadlock when calling
+ // AudioOutputUnitStop method from inside render callback
+ queue.run_async(move || {
+ stm.core_stream_data.stop_audiounits();
+ });
+ audiounit_make_silent(&buffers[0]);
+ return NO_ERR;
+ }
+
+ stm.draining
+ .store(outframes < i64::from(output_frames), Ordering::SeqCst);
+ stm.output_callback_timing_data_write
+ .write(OutputCallbackTimingData {
+ frames_queued: stm.frames_queued,
+ timestamp: now,
+ buffer_size: outframes as u64,
+ });
+
+ stm.frames_queued += outframes as u64;
+
+ // Post process output samples.
+ if stm.draining.load(Ordering::SeqCst) {
+ // Clear missing frames (silence)
+ let frames_to_bytes = |frames: usize| -> usize {
+ let sample_size = cubeb_sample_size(stm.core_stream_data.output_stream_params.format());
+ let channel_count = stm.core_stream_data.output_stream_params.channels() as usize;
+ frames * sample_size * channel_count
+ };
+ let out_bytes = unsafe {
+ slice::from_raw_parts_mut(
+ output_buffer as *mut u8,
+ frames_to_bytes(output_frames as usize),
+ )
+ };
+ let start = frames_to_bytes(outframes as usize);
+ for byte in out_bytes.iter_mut().skip(start) {
+ *byte = 0;
+ }
+ }
+
+ // Mixing
+ if stm.core_stream_data.mixer.is_some() {
+ assert!(
+ buffers[0].mDataByteSize
+ >= stm.core_stream_data.output_dev_desc.mBytesPerFrame * output_frames
+ );
+ stm.core_stream_data.mixer.as_mut().unwrap().mix(
+ output_frames as usize,
+ buffers[0].mData,
+ buffers[0].mDataByteSize as usize,
+ );
+ }
+ NO_ERR
+}
+
+#[allow(clippy::cognitive_complexity)]
+extern "C" fn audiounit_property_listener_callback(
+ id: AudioObjectID,
+ address_count: u32,
+ addresses: *const AudioObjectPropertyAddress,
+ user: *mut c_void,
+) -> OSStatus {
+ assert_ne!(address_count, 0);
+
+ let stm = unsafe { &mut *(user as *mut AudioUnitStream) };
+ let addrs = unsafe { slice::from_raw_parts(addresses, address_count as usize) };
+ if stm.switching_device.load(Ordering::SeqCst) {
+ cubeb_log!(
+ "Switching is already taking place. Skipping event for device {}",
+ id
+ );
+ return NO_ERR;
+ }
+ stm.switching_device.store(true, Ordering::SeqCst);
+
+ let mut explicit_device_dead = false;
+
+ cubeb_log!(
+ "({:p}) Handling {} device changed events for device {}",
+ stm as *const AudioUnitStream,
+ address_count,
+ id
+ );
+ for (i, addr) in addrs.iter().enumerate() {
+ let p = PropertySelector::from(addr.mSelector);
+ cubeb_log!("Event #{}: {}", i, p);
+ assert_ne!(p, PropertySelector::Unknown);
+ if p == PropertySelector::DeviceIsAlive {
+ explicit_device_dead = true;
+ }
+ }
+
+ // Handle the events
+ if explicit_device_dead {
+ cubeb_log!("The user-selected input or output device is dead, entering error state");
+ stm.stopped.store(true, Ordering::SeqCst);
+
+ // Use a different thread, through the queue, to avoid deadlock when calling
+ // Get/SetProperties method from inside notify callback
+ stm.queue.clone().run_async(move || {
+ stm.core_stream_data.stop_audiounits();
+ stm.close_on_error();
+ });
+ return NO_ERR;
+ }
+ {
+ let callback = stm.device_changed_callback.lock().unwrap();
+ if let Some(device_changed_callback) = *callback {
+ unsafe {
+ device_changed_callback(stm.user_ptr);
+ }
+ }
+ }
+ stm.reinit_async();
+
+ NO_ERR
+}
+
+fn get_default_device(devtype: DeviceType) -> Option<AudioObjectID> {
+ match get_default_device_id(devtype) {
+ Err(e) => {
+ cubeb_log!("Cannot get default {:?} device. Error: {}", devtype, e);
+ None
+ }
+ Ok(id) if id == kAudioObjectUnknown => {
+ cubeb_log!("Get an invalid default {:?} device: {}", devtype, id);
+ None
+ }
+ Ok(id) => Some(id),
+ }
+}
+
+fn get_default_device_id(devtype: DeviceType) -> std::result::Result<AudioObjectID, OSStatus> {
+ let address = get_property_address(
+ match devtype {
+ DeviceType::INPUT => Property::HardwareDefaultInputDevice,
+ DeviceType::OUTPUT => Property::HardwareDefaultOutputDevice,
+ _ => panic!("Unsupport type"),
+ },
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ );
+
+ let mut devid: AudioDeviceID = kAudioObjectUnknown;
+ let mut size = mem::size_of::<AudioDeviceID>();
+ let status =
+ audio_object_get_property_data(kAudioObjectSystemObject, &address, &mut size, &mut devid);
+ if status == NO_ERR {
+ Ok(devid)
+ } else {
+ Err(status)
+ }
+}
+
+fn audiounit_convert_channel_layout(layout: &AudioChannelLayout) -> Result<Vec<mixer::Channel>> {
+ if layout.mChannelLayoutTag != kAudioChannelLayoutTag_UseChannelDescriptions {
+ // kAudioChannelLayoutTag_UseChannelBitmap
+ // kAudioChannelLayoutTag_Mono
+ // kAudioChannelLayoutTag_Stereo
+ // ....
+ cubeb_log!("Only handling UseChannelDescriptions for now.\n");
+ return Err(Error::error());
+ }
+
+ let channel_descriptions = unsafe {
+ slice::from_raw_parts(
+ layout.mChannelDescriptions.as_ptr(),
+ layout.mNumberChannelDescriptions as usize,
+ )
+ };
+
+ let mut channels = Vec::with_capacity(layout.mNumberChannelDescriptions as usize);
+ for description in channel_descriptions {
+ let label = CAChannelLabel(description.mChannelLabel);
+ channels.push(label.into());
+ }
+
+ Ok(channels)
+}
+
+fn audiounit_get_preferred_channel_layout(output_unit: AudioUnit) -> Result<Vec<mixer::Channel>> {
+ let mut rv = NO_ERR;
+ let mut size: usize = 0;
+ rv = audio_unit_get_property_info(
+ output_unit,
+ kAudioDevicePropertyPreferredChannelLayout,
+ kAudioUnitScope_Output,
+ AU_OUT_BUS,
+ &mut size,
+ None,
+ );
+ if rv != NO_ERR {
+ cubeb_log!(
+ "AudioUnitGetPropertyInfo/kAudioDevicePropertyPreferredChannelLayout rv={}",
+ rv
+ );
+ return Err(Error::error());
+ }
+ debug_assert!(size > 0);
+
+ let mut layout = make_sized_audio_channel_layout(size);
+ rv = audio_unit_get_property(
+ output_unit,
+ kAudioDevicePropertyPreferredChannelLayout,
+ kAudioUnitScope_Output,
+ AU_OUT_BUS,
+ layout.as_mut(),
+ &mut size,
+ );
+ if rv != NO_ERR {
+ cubeb_log!(
+ "AudioUnitGetProperty/kAudioDevicePropertyPreferredChannelLayout rv={}",
+ rv
+ );
+ return Err(Error::error());
+ }
+
+ audiounit_convert_channel_layout(layout.as_ref())
+}
+
+// This is for output AudioUnit only. Calling this by input-only AudioUnit is prone
+// to crash intermittently.
+fn audiounit_get_current_channel_layout(output_unit: AudioUnit) -> Result<Vec<mixer::Channel>> {
+ let mut rv = NO_ERR;
+ let mut size: usize = 0;
+ rv = audio_unit_get_property_info(
+ output_unit,
+ kAudioUnitProperty_AudioChannelLayout,
+ kAudioUnitScope_Output,
+ AU_OUT_BUS,
+ &mut size,
+ None,
+ );
+ if rv != NO_ERR {
+ cubeb_log!(
+ "AudioUnitGetPropertyInfo/kAudioUnitProperty_AudioChannelLayout rv={}",
+ rv
+ );
+ return Err(Error::error());
+ }
+ debug_assert!(size > 0);
+
+ let mut layout = make_sized_audio_channel_layout(size);
+ rv = audio_unit_get_property(
+ output_unit,
+ kAudioUnitProperty_AudioChannelLayout,
+ kAudioUnitScope_Output,
+ AU_OUT_BUS,
+ layout.as_mut(),
+ &mut size,
+ );
+ if rv != NO_ERR {
+ cubeb_log!(
+ "AudioUnitGetProperty/kAudioUnitProperty_AudioChannelLayout rv={}",
+ rv
+ );
+ return Err(Error::error());
+ }
+
+ audiounit_convert_channel_layout(layout.as_ref())
+}
+
+fn get_channel_layout(output_unit: AudioUnit) -> Result<Vec<mixer::Channel>> {
+ audiounit_get_current_channel_layout(output_unit)
+ .or_else(|_| {
+ // The kAudioUnitProperty_AudioChannelLayout property isn't known before
+ // macOS 10.12, attempt another method.
+ cubeb_log!(
+ "Cannot get current channel layout for audiounit @ {:p}. Trying preferred channel layout.",
+ output_unit
+ );
+ audiounit_get_preferred_channel_layout(output_unit)
+ })
+}
+
+fn start_audiounit(unit: AudioUnit) -> Result<()> {
+ let status = audio_output_unit_start(unit);
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ cubeb_log!("Cannot start audiounit @ {:p}. Error: {}", unit, status);
+ Err(Error::error())
+ }
+}
+
+fn stop_audiounit(unit: AudioUnit) -> Result<()> {
+ let status = audio_output_unit_stop(unit);
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ cubeb_log!("Cannot stop audiounit @ {:p}. Error: {}", unit, status);
+ Err(Error::error())
+ }
+}
+
+fn create_audiounit(device: &device_info) -> Result<AudioUnit> {
+ assert!(device
+ .flags
+ .intersects(device_flags::DEV_INPUT | device_flags::DEV_OUTPUT));
+ assert!(!device
+ .flags
+ .contains(device_flags::DEV_INPUT | device_flags::DEV_OUTPUT));
+
+ let unit = create_blank_audiounit()?;
+ let mut bus = AU_OUT_BUS;
+
+ if device.flags.contains(device_flags::DEV_INPUT) {
+ // Input only.
+ if let Err(e) = enable_audiounit_scope(unit, DeviceType::INPUT, true) {
+ cubeb_log!("Failed to enable audiounit input scope. Error: {}", e);
+ dispose_audio_unit(unit);
+ return Err(Error::error());
+ }
+ if let Err(e) = enable_audiounit_scope(unit, DeviceType::OUTPUT, false) {
+ cubeb_log!("Failed to disable audiounit output scope. Error: {}", e);
+ dispose_audio_unit(unit);
+ return Err(Error::error());
+ }
+ bus = AU_IN_BUS;
+ }
+
+ if device.flags.contains(device_flags::DEV_OUTPUT) {
+ // Output only.
+ if let Err(e) = enable_audiounit_scope(unit, DeviceType::OUTPUT, true) {
+ cubeb_log!("Failed to enable audiounit output scope. Error: {}", e);
+ dispose_audio_unit(unit);
+ return Err(Error::error());
+ }
+ if let Err(e) = enable_audiounit_scope(unit, DeviceType::INPUT, false) {
+ cubeb_log!("Failed to disable audiounit input scope. Error: {}", e);
+ dispose_audio_unit(unit);
+ return Err(Error::error());
+ }
+ bus = AU_OUT_BUS;
+ }
+
+ if let Err(e) = set_device_to_audiounit(unit, device.id, bus) {
+ cubeb_log!(
+ "Failed to set device {} to the created audiounit. Error: {}",
+ device.id,
+ e
+ );
+ dispose_audio_unit(unit);
+ return Err(Error::error());
+ }
+
+ Ok(unit)
+}
+
+fn create_voiceprocessing_audiounit(
+ in_device: &device_info,
+ out_device: &device_info,
+) -> Result<AudioUnit> {
+ assert!(in_device.flags.contains(device_flags::DEV_INPUT));
+ assert!(!in_device.flags.contains(device_flags::DEV_OUTPUT));
+ assert!(!out_device.flags.contains(device_flags::DEV_INPUT));
+ assert!(out_device.flags.contains(device_flags::DEV_OUTPUT));
+
+ let unit = create_typed_audiounit(kAudioUnitSubType_VoiceProcessingIO)?;
+
+ if let Err(e) = set_device_to_audiounit(unit, in_device.id, AU_IN_BUS) {
+ cubeb_log!(
+ "Failed to set in device {} to the created audiounit. Error: {}",
+ in_device.id,
+ e
+ );
+ dispose_audio_unit(unit);
+ return Err(Error::error());
+ }
+
+ if let Err(e) = set_device_to_audiounit(unit, out_device.id, AU_OUT_BUS) {
+ cubeb_log!(
+ "Failed to set out device {} to the created audiounit. Error: {}",
+ out_device.id,
+ e
+ );
+ dispose_audio_unit(unit);
+ return Err(Error::error());
+ }
+
+ Ok(unit)
+}
+
+fn enable_audiounit_scope(
+ unit: AudioUnit,
+ devtype: DeviceType,
+ enable_io: bool,
+) -> std::result::Result<(), OSStatus> {
+ assert!(!unit.is_null());
+
+ let enable = u32::from(enable_io);
+ let (scope, element) = match devtype {
+ DeviceType::INPUT => (kAudioUnitScope_Input, AU_IN_BUS),
+ DeviceType::OUTPUT => (kAudioUnitScope_Output, AU_OUT_BUS),
+ _ => panic!(
+ "Enable AudioUnit {:?} with unsupported type: {:?}",
+ unit, devtype
+ ),
+ };
+ let status = audio_unit_set_property(
+ unit,
+ kAudioOutputUnitProperty_EnableIO,
+ scope,
+ element,
+ &enable,
+ mem::size_of::<u32>(),
+ );
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(status)
+ }
+}
+
+fn set_device_to_audiounit(
+ unit: AudioUnit,
+ device_id: AudioObjectID,
+ bus: AudioUnitElement,
+) -> std::result::Result<(), OSStatus> {
+ assert!(!unit.is_null());
+
+ let status = audio_unit_set_property(
+ unit,
+ kAudioOutputUnitProperty_CurrentDevice,
+ kAudioUnitScope_Global,
+ bus,
+ &device_id,
+ mem::size_of::<AudioDeviceID>(),
+ );
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(status)
+ }
+}
+
+fn create_typed_audiounit(sub_type: c_uint) -> Result<AudioUnit> {
+ let desc = AudioComponentDescription {
+ componentType: kAudioUnitType_Output,
+ componentSubType: sub_type,
+ componentManufacturer: kAudioUnitManufacturer_Apple,
+ componentFlags: 0,
+ componentFlagsMask: 0,
+ };
+
+ let comp = unsafe { AudioComponentFindNext(ptr::null_mut(), &desc) };
+ if comp.is_null() {
+ cubeb_log!("Could not find matching audio hardware.");
+ return Err(Error::error());
+ }
+ let mut unit: AudioUnit = ptr::null_mut();
+ let status = unsafe { AudioComponentInstanceNew(comp, &mut unit) };
+ if status == NO_ERR {
+ assert!(!unit.is_null());
+ Ok(unit)
+ } else {
+ cubeb_log!("Fail to get a new AudioUnit. Error: {}", status);
+ Err(Error::error())
+ }
+}
+
+fn create_blank_audiounit() -> Result<AudioUnit> {
+ #[cfg(not(target_os = "ios"))]
+ return create_typed_audiounit(kAudioUnitSubType_HALOutput);
+ #[cfg(target_os = "ios")]
+ return create_typed_audiounit(kAudioUnitSubType_RemoteIO);
+}
+
+fn get_buffer_size(unit: AudioUnit, devtype: DeviceType) -> std::result::Result<u32, OSStatus> {
+ assert!(!unit.is_null());
+ let (scope, element) = match devtype {
+ DeviceType::INPUT => (kAudioUnitScope_Output, AU_IN_BUS),
+ DeviceType::OUTPUT => (kAudioUnitScope_Input, AU_OUT_BUS),
+ _ => panic!(
+ "Get buffer size of AudioUnit {:?} with unsupported type: {:?}",
+ unit, devtype
+ ),
+ };
+ let mut frames: u32 = 0;
+ let mut size = mem::size_of::<u32>();
+ let status = audio_unit_get_property(
+ unit,
+ kAudioDevicePropertyBufferFrameSize,
+ scope,
+ element,
+ &mut frames,
+ &mut size,
+ );
+ if status == NO_ERR {
+ Ok(frames)
+ } else {
+ Err(status)
+ }
+}
+
+fn set_buffer_size(
+ unit: AudioUnit,
+ devtype: DeviceType,
+ frames: u32,
+) -> std::result::Result<(), OSStatus> {
+ assert!(!unit.is_null());
+ let (scope, element) = match devtype {
+ DeviceType::INPUT => (kAudioUnitScope_Output, AU_IN_BUS),
+ DeviceType::OUTPUT => (kAudioUnitScope_Input, AU_OUT_BUS),
+ _ => panic!(
+ "Set buffer size of AudioUnit {:?} with unsupported type: {:?}",
+ unit, devtype
+ ),
+ };
+ let status = audio_unit_set_property(
+ unit,
+ kAudioDevicePropertyBufferFrameSize,
+ scope,
+ element,
+ &frames,
+ mem::size_of_val(&frames),
+ );
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(status)
+ }
+}
+
+#[allow(clippy::mutex_atomic)] // The mutex needs to be fed into Condvar::wait_timeout.
+fn set_buffer_size_sync(unit: AudioUnit, devtype: DeviceType, frames: u32) -> Result<()> {
+ let current_frames = get_buffer_size(unit, devtype).map_err(|e| {
+ cubeb_log!(
+ "Cannot get buffer size of AudioUnit {:?} for {:?}. Error: {}",
+ unit,
+ devtype,
+ e
+ );
+ Error::error()
+ })?;
+ if frames == current_frames {
+ cubeb_log!(
+ "The buffer frame size of AudioUnit {:?} for {:?} is already {}",
+ unit,
+ devtype,
+ frames
+ );
+ return Ok(());
+ }
+
+ let waiting_time = Duration::from_millis(100);
+ let pair = Arc::new((Mutex::new(false), Condvar::new()));
+ let mut pair2 = pair.clone();
+ let pair_ptr = &mut pair2;
+
+ assert_eq!(
+ audio_unit_add_property_listener(
+ unit,
+ kAudioDevicePropertyBufferFrameSize,
+ buffer_size_changed_callback,
+ pair_ptr,
+ ),
+ NO_ERR
+ );
+
+ let _teardown = finally(|| {
+ assert_eq!(
+ audio_unit_remove_property_listener_with_user_data(
+ unit,
+ kAudioDevicePropertyBufferFrameSize,
+ buffer_size_changed_callback,
+ pair_ptr,
+ ),
+ NO_ERR
+ );
+ });
+
+ set_buffer_size(unit, devtype, frames).map_err(|e| {
+ cubeb_log!(
+ "Failed to set buffer size for AudioUnit {:?} for {:?}. Error: {}",
+ unit,
+ devtype,
+ e
+ );
+ Error::error()
+ })?;
+
+ let (lock, cvar) = &*pair;
+ let changed = lock.lock().unwrap();
+ if !*changed {
+ let (chg, timeout_res) = cvar.wait_timeout(changed, waiting_time).unwrap();
+ if timeout_res.timed_out() {
+ cubeb_log!(
+ "Timed out for waiting the buffer frame size setting of AudioUnit {:?} for {:?}",
+ unit,
+ devtype
+ );
+ }
+ if !*chg {
+ return Err(Error::error());
+ }
+ }
+
+ let new_frames = get_buffer_size(unit, devtype).map_err(|e| {
+ cubeb_log!(
+ "Cannot get new buffer size of AudioUnit {:?} for {:?}. Error: {}",
+ unit,
+ devtype,
+ e
+ );
+ Error::error()
+ })?;
+ cubeb_log!(
+ "The new buffer frames size of AudioUnit {:?} for {:?} is {}",
+ unit,
+ devtype,
+ new_frames
+ );
+
+ extern "C" fn buffer_size_changed_callback(
+ in_client_data: *mut c_void,
+ _in_unit: AudioUnit,
+ in_property_id: AudioUnitPropertyID,
+ in_scope: AudioUnitScope,
+ in_element: AudioUnitElement,
+ ) {
+ if in_scope == 0 {
+ // filter out the callback for global scope.
+ return;
+ }
+ assert!(in_element == AU_IN_BUS || in_element == AU_OUT_BUS);
+ assert_eq!(in_property_id, kAudioDevicePropertyBufferFrameSize);
+ let pair = unsafe { &mut *(in_client_data as *mut Arc<(Mutex<bool>, Condvar)>) };
+ let (lock, cvar) = &**pair;
+ let mut changed = lock.lock().unwrap();
+ *changed = true;
+ cvar.notify_one();
+ }
+
+ Ok(())
+}
+
+fn convert_uint32_into_string(data: u32) -> CString {
+ let empty = CString::default();
+ if data == 0 {
+ return empty;
+ }
+
+ // Reverse 0xWXYZ into 0xZYXW.
+ let mut buffer = vec![b'\x00'; 4]; // 4 bytes for uint32.
+ buffer[0] = (data >> 24) as u8;
+ buffer[1] = (data >> 16) as u8;
+ buffer[2] = (data >> 8) as u8;
+ buffer[3] = (data) as u8;
+
+ // CString::new() will consume the input bytes vec and add a '\0' at the
+ // end of the bytes. The input bytes vec must not contain any 0 bytes in
+ // it in case causing memory leaks.
+ CString::new(buffer).unwrap_or(empty)
+}
+
+fn get_channel_count(
+ devid: AudioObjectID,
+ devtype: DeviceType,
+) -> std::result::Result<u32, OSStatus> {
+ assert_ne!(devid, kAudioObjectUnknown);
+
+ let mut streams = get_device_streams(devid, devtype)?;
+ let model_uid =
+ get_device_model_uid(devid, devtype).map_or_else(|_| String::new(), |s| s.into_string());
+
+ if devtype == DeviceType::INPUT {
+ // With VPIO, output devices will/may get a Tap that appears as input channels on the
+ // output device id. One could check for whether the output device has a tap enabled,
+ // but it is impossible to distinguish an output-only device from an input+output
+ // device. There have also been corner cases observed, where the device does NOT have
+ // a Tap enabled, but it still has the extra input channels from the Tap.
+ // We can check the terminal type of the input stream instead, the VPIO type is
+ // INPUT_UNDEFINED or an output type, we explicitly ignore those and keep all other cases.
+ streams.retain(|stream| {
+ let terminal_type = get_stream_terminal_type(*stream);
+ if terminal_type.is_err() {
+ return true;
+ }
+
+ #[allow(non_upper_case_globals)]
+ match terminal_type.unwrap() {
+ kAudioStreamTerminalTypeMicrophone
+ | kAudioStreamTerminalTypeHeadsetMicrophone
+ | kAudioStreamTerminalTypeReceiverMicrophone => true,
+ kAudioStreamTerminalTypeUnknown => {
+ cubeb_log!("Unknown TerminalType for input stream. Ignoring its channels.");
+ false
+ }
+ t if [
+ kAudioStreamTerminalTypeSpeaker,
+ kAudioStreamTerminalTypeHeadphones,
+ kAudioStreamTerminalTypeLFESpeaker,
+ kAudioStreamTerminalTypeReceiverSpeaker,
+ ]
+ .contains(&t) =>
+ {
+ cubeb_log!(
+ "Output TerminalType {:#06X} for input stream. Ignoring its channels.",
+ t
+ );
+ false
+ }
+ INPUT_UNDEFINED => {
+ cubeb_log!(
+ "INPUT_UNDEFINED TerminalType for input stream. Ignoring its channels."
+ );
+ false
+ }
+ // The input tap stream on the Studio Display Speakers has a terminal type that
+ // is not clearly output-specific. We special-case it here.
+ EXTERNAL_DIGITAL_AUDIO_INTERFACE
+ if model_uid.contains(APPLE_STUDIO_DISPLAY_USB_ID) =>
+ {
+ false
+ }
+ // Note INPUT_UNDEFINED is 0x200 and INPUT_MICROPHONE is 0x201
+ t if (INPUT_MICROPHONE..OUTPUT_UNDEFINED).contains(&t) => true,
+ t if (OUTPUT_UNDEFINED..BIDIRECTIONAL_UNDEFINED).contains(&t) => false,
+ t if (BIDIRECTIONAL_UNDEFINED..TELEPHONY_UNDEFINED).contains(&t) => true,
+ t if (TELEPHONY_UNDEFINED..EXTERNAL_UNDEFINED).contains(&t) => true,
+ t => {
+ cubeb_log!("Unknown TerminalType {:#06X} for input stream.", t);
+ true
+ }
+ }
+ });
+ }
+
+ let mut count = 0;
+ for stream in streams {
+ if let Ok(format) = get_stream_virtual_format(stream) {
+ count += format.mChannelsPerFrame;
+ }
+ }
+ Ok(count)
+}
+
+fn get_range_of_sample_rates(
+ devid: AudioObjectID,
+ devtype: DeviceType,
+) -> std::result::Result<(f64, f64), String> {
+ let result = get_ranges_of_device_sample_rate(devid, devtype);
+ if let Err(e) = result {
+ return Err(format!("status {}", e));
+ }
+ let rates = result.unwrap();
+ if rates.is_empty() {
+ return Err(String::from("No data"));
+ }
+ let (mut min, mut max) = (std::f64::MAX, std::f64::MIN);
+ for rate in rates {
+ if rate.mMaximum > max {
+ max = rate.mMaximum;
+ }
+ if rate.mMinimum < min {
+ min = rate.mMinimum;
+ }
+ }
+ Ok((min, max))
+}
+
+fn get_fixed_latency(devid: AudioObjectID, devtype: DeviceType) -> u32 {
+ let device_latency = match get_device_latency(devid, devtype) {
+ Ok(latency) => latency,
+ Err(e) => {
+ cubeb_log!(
+ "Cannot get the device latency for device {} in {:?} scope. Error: {}",
+ devid,
+ devtype,
+ e
+ );
+ 0 // default device latency
+ }
+ };
+
+ let stream_latency = get_device_streams(devid, devtype).and_then(|streams| {
+ if streams.is_empty() {
+ cubeb_log!(
+ "No stream on device {} in {:?} scope!",
+ devid,
+ devtype
+ );
+ Ok(0) // default stream latency
+ } else {
+ get_stream_latency(streams[0])
+ }
+ }).map_err(|e| {
+ cubeb_log!(
+ "Cannot get the stream, or the latency of the first stream on device {} in {:?} scope. Error: {}",
+ devid,
+ devtype,
+ e
+ );
+ e
+ }).unwrap_or(0); // default stream latency
+
+ device_latency + stream_latency
+}
+
+#[allow(non_upper_case_globals)]
+fn get_device_group_id(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<CString, OSStatus> {
+ match get_device_transport_type(id, devtype) {
+ Ok(kAudioDeviceTransportTypeBuiltIn) => {
+ cubeb_log!(
+ "The transport type is {:?}",
+ convert_uint32_into_string(kAudioDeviceTransportTypeBuiltIn)
+ );
+ match get_custom_group_id(id, devtype) {
+ Some(id) => return Ok(id),
+ None => {
+ cubeb_log!("Getting model UID instead.");
+ }
+ };
+ }
+ Ok(trans_type) => {
+ cubeb_log!(
+ "The transport type is {:?}. Getting model UID instead.",
+ convert_uint32_into_string(trans_type)
+ );
+ }
+ Err(e) => {
+ cubeb_log!(
+ "Error: {} when getting transport type. Get model uid instead.",
+ e
+ );
+ }
+ }
+
+ // Some devices (e.g. AirPods) might only set the model-uid in the global scope.
+ // The query might fail if the scope is input-only or output-only.
+ get_device_model_uid(id, devtype)
+ .or_else(|_| get_device_model_uid(id, DeviceType::INPUT | DeviceType::OUTPUT))
+ .map(|uid| uid.into_cstring())
+}
+
+fn get_custom_group_id(id: AudioDeviceID, devtype: DeviceType) -> Option<CString> {
+ const IMIC: u32 = 0x696D_6963; // "imic" (internal microphone)
+ const ISPK: u32 = 0x6973_706B; // "ispk" (internal speaker)
+ const EMIC: u32 = 0x656D_6963; // "emic" (external microphone)
+ const HDPN: u32 = 0x6864_706E; // "hdpn" (headphone)
+
+ match get_device_source(id, devtype) {
+ s @ Ok(IMIC) | s @ Ok(ISPK) => {
+ const GROUP_ID: &str = "builtin-internal-mic|spk";
+ cubeb_log!(
+ "Using hardcode group id: {} when source is: {:?}.",
+ GROUP_ID,
+ convert_uint32_into_string(s.unwrap())
+ );
+ return Some(CString::new(GROUP_ID).unwrap());
+ }
+ s @ Ok(EMIC) | s @ Ok(HDPN) => {
+ const GROUP_ID: &str = "builtin-external-mic|hdpn";
+ cubeb_log!(
+ "Using hardcode group id: {} when source is: {:?}.",
+ GROUP_ID,
+ convert_uint32_into_string(s.unwrap())
+ );
+ return Some(CString::new(GROUP_ID).unwrap());
+ }
+ Ok(s) => {
+ cubeb_log!(
+ "No custom group id when source is: {:?}.",
+ convert_uint32_into_string(s)
+ );
+ }
+ Err(e) => {
+ cubeb_log!("Error: {} when getting device source. ", e);
+ }
+ }
+ None
+}
+
+fn get_device_label(
+ id: AudioDeviceID,
+ devtype: DeviceType,
+) -> std::result::Result<StringRef, OSStatus> {
+ get_device_source_name(id, devtype).or_else(|_| get_device_name(id, devtype))
+}
+
+fn get_device_global_uid(id: AudioDeviceID) -> std::result::Result<StringRef, OSStatus> {
+ get_device_uid(id, DeviceType::INPUT | DeviceType::OUTPUT)
+}
+
+#[allow(clippy::cognitive_complexity)]
+fn create_cubeb_device_info(
+ devid: AudioObjectID,
+ devtype: DeviceType,
+) -> Result<ffi::cubeb_device_info> {
+ if devtype != DeviceType::INPUT && devtype != DeviceType::OUTPUT {
+ return Err(Error::error());
+ }
+ let channels = get_channel_count(devid, devtype).map_err(|e| {
+ cubeb_log!("Cannot get the channel count. Error: {}", e);
+ Error::error()
+ })?;
+ if channels == 0 {
+ // Invalid type for this device.
+ return Err(Error::error());
+ }
+
+ let mut dev_info = ffi::cubeb_device_info {
+ max_channels: channels,
+ ..Default::default()
+ };
+
+ assert!(
+ mem::size_of::<ffi::cubeb_devid>() >= mem::size_of_val(&devid),
+ "cubeb_devid can't represent devid"
+ );
+ dev_info.devid = devid as ffi::cubeb_devid;
+
+ match get_device_uid(devid, devtype) {
+ Ok(uid) => {
+ let c_string = uid.into_cstring();
+ dev_info.device_id = c_string.into_raw();
+ }
+ Err(e) => {
+ cubeb_log!(
+ "Cannot get the UID for device {} in {:?} scope. Error: {}",
+ devid,
+ devtype,
+ e
+ );
+ }
+ }
+
+ match get_device_group_id(devid, devtype) {
+ Ok(group_id) => {
+ dev_info.group_id = group_id.into_raw();
+ }
+ Err(e) => {
+ cubeb_log!(
+ "Cannot get the model UID for device {} in {:?} scope. Error: {}",
+ devid,
+ devtype,
+ e
+ );
+ }
+ }
+
+ let label = match get_device_label(devid, devtype) {
+ Ok(label) => label.into_cstring(),
+ Err(e) => {
+ cubeb_log!(
+ "Cannot get the label for device {} in {:?} scope. Error: {}",
+ devid,
+ devtype,
+ e
+ );
+ CString::default()
+ }
+ };
+ dev_info.friendly_name = label.into_raw();
+
+ match get_device_manufacturer(devid, devtype) {
+ Ok(vendor) => {
+ let vendor = vendor.into_cstring();
+ dev_info.vendor_name = vendor.into_raw();
+ }
+ Err(e) => {
+ cubeb_log!(
+ "Cannot get the manufacturer for device {} in {:?} scope. Error: {}",
+ devid,
+ devtype,
+ e
+ );
+ }
+ }
+
+ dev_info.device_type = match devtype {
+ DeviceType::INPUT => ffi::CUBEB_DEVICE_TYPE_INPUT,
+ DeviceType::OUTPUT => ffi::CUBEB_DEVICE_TYPE_OUTPUT,
+ _ => panic!("invalid type"),
+ };
+
+ dev_info.state = ffi::CUBEB_DEVICE_STATE_ENABLED;
+ dev_info.preferred = match get_default_device(devtype) {
+ Some(id) if id == devid => ffi::CUBEB_DEVICE_PREF_ALL,
+ _ => ffi::CUBEB_DEVICE_PREF_NONE,
+ };
+
+ dev_info.format = ffi::CUBEB_DEVICE_FMT_ALL;
+ dev_info.default_format = ffi::CUBEB_DEVICE_FMT_F32NE;
+
+ match get_device_sample_rate(devid, devtype) {
+ Ok(rate) => {
+ dev_info.default_rate = rate as u32;
+ }
+ Err(e) => {
+ cubeb_log!(
+ "Cannot get the sample rate for device {} in {:?} scope. Error: {}",
+ devid,
+ devtype,
+ e
+ );
+ }
+ }
+
+ match get_range_of_sample_rates(devid, devtype) {
+ Ok((min, max)) => {
+ dev_info.min_rate = min as u32;
+ dev_info.max_rate = max as u32;
+ }
+ Err(e) => {
+ cubeb_log!(
+ "Cannot get the range of sample rate for device {} in {:?} scope. Error: {}",
+ devid,
+ devtype,
+ e
+ );
+ }
+ }
+
+ let latency = get_fixed_latency(devid, devtype);
+
+ let (latency_low, latency_high) = match get_device_buffer_frame_size_range(devid, devtype) {
+ Ok(range) => (
+ latency + range.mMinimum as u32,
+ latency + range.mMaximum as u32,
+ ),
+ Err(e) => {
+ cubeb_log!("Cannot get the buffer frame size for device {} in {:?} scope. Using default value instead. Error: {}", devid, devtype, e);
+ (
+ 10 * dev_info.default_rate / 1000,
+ 100 * dev_info.default_rate / 1000,
+ )
+ }
+ };
+ dev_info.latency_lo = latency_low;
+ dev_info.latency_hi = latency_high;
+
+ Ok(dev_info)
+}
+
+fn destroy_cubeb_device_info(device: &mut ffi::cubeb_device_info) {
+ // This should be mapped to the memory allocation in `create_cubeb_device_info`.
+ // The `device_id`, `group_id`, `vendor_name` can be null pointer if the queries
+ // failed, while `friendly_name` will be assigned to a default empty "" string.
+ // Set the pointers to null in case it points to some released memory.
+ unsafe {
+ if !device.device_id.is_null() {
+ let _ = CString::from_raw(device.device_id as *mut _);
+ device.device_id = ptr::null();
+ }
+
+ if !device.group_id.is_null() {
+ let _ = CString::from_raw(device.group_id as *mut _);
+ device.group_id = ptr::null();
+ }
+
+ assert!(!device.friendly_name.is_null());
+ let _ = CString::from_raw(device.friendly_name as *mut _);
+ device.friendly_name = ptr::null();
+
+ if !device.vendor_name.is_null() {
+ let _ = CString::from_raw(device.vendor_name as *mut _);
+ device.vendor_name = ptr::null();
+ }
+ }
+}
+
+fn audiounit_get_devices() -> Vec<AudioObjectID> {
+ let mut size: usize = 0;
+ let address = get_property_address(
+ Property::HardwareDevices,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ );
+ let mut ret =
+ audio_object_get_property_data_size(kAudioObjectSystemObject, &address, &mut size);
+ if ret != NO_ERR {
+ return Vec::new();
+ }
+ // Total number of input and output devices.
+ let mut devices: Vec<AudioObjectID> = allocate_array_by_size(size);
+ ret = audio_object_get_property_data(
+ kAudioObjectSystemObject,
+ &address,
+ &mut size,
+ devices.as_mut_ptr(),
+ );
+ if ret != NO_ERR {
+ return Vec::new();
+ }
+ devices
+}
+
+fn audiounit_get_devices_of_type(devtype: DeviceType) -> Vec<AudioObjectID> {
+ assert!(devtype.intersects(DeviceType::INPUT | DeviceType::OUTPUT));
+
+ let mut devices = audiounit_get_devices();
+
+ // Remove the aggregate device from the list of devices (if any).
+ devices.retain(|&device| {
+ // TODO: (bug 1628411) Figure out when `device` is `kAudioObjectUnknown`.
+ if device == kAudioObjectUnknown {
+ false
+ } else if let Ok(uid) = get_device_global_uid(device) {
+ let uid = uid.into_string();
+ !uid.contains(PRIVATE_AGGREGATE_DEVICE_NAME)
+ && !uid.contains(VOICEPROCESSING_AGGREGATE_DEVICE_NAME)
+ } else {
+ // Fail to get device uid.
+ true
+ }
+ });
+
+ // Expected sorted but did not find anything in the docs.
+ devices.sort_unstable();
+ if devtype.contains(DeviceType::INPUT | DeviceType::OUTPUT) {
+ return devices;
+ }
+
+ let mut devices_in_scope = Vec::new();
+ for device in devices {
+ let label = match get_device_label(device, DeviceType::OUTPUT | DeviceType::INPUT) {
+ Ok(label) => label.into_string(),
+ Err(e) => format!("Unknown(error: {})", e),
+ };
+ let info = format!("{} ({})", device, label);
+
+ if let Ok(channels) = get_channel_count(device, devtype) {
+ cubeb_log!("Device {} has {} {:?}-channels", info, channels, devtype);
+ if channels > 0 {
+ devices_in_scope.push(device);
+ }
+ } else {
+ cubeb_log!("Cannot get the channel count for device {}. Ignored.", info);
+ }
+ }
+
+ devices_in_scope
+}
+
+extern "C" fn audiounit_collection_changed_callback(
+ _in_object_id: AudioObjectID,
+ _in_number_addresses: u32,
+ _in_addresses: *const AudioObjectPropertyAddress,
+ in_client_data: *mut c_void,
+) -> OSStatus {
+ let context = unsafe { &mut *(in_client_data as *mut AudioUnitContext) };
+
+ let queue = context.serial_queue.clone();
+
+ // This can be called from inside an AudioUnit function, dispatch to another queue.
+ queue.run_async(move || {
+ let ctx_ptr = context as *const AudioUnitContext;
+
+ let mut devices = context.devices.lock().unwrap();
+
+ if devices.input.changed_callback.is_none() && devices.output.changed_callback.is_none() {
+ return;
+ }
+ if devices.input.changed_callback.is_some() {
+ let input_devices = audiounit_get_devices_of_type(DeviceType::INPUT);
+ if devices.input.update_devices(input_devices) {
+ unsafe {
+ devices.input.changed_callback.unwrap()(
+ ctx_ptr as *mut ffi::cubeb,
+ devices.input.callback_user_ptr,
+ );
+ }
+ }
+ }
+ if devices.output.changed_callback.is_some() {
+ let output_devices = audiounit_get_devices_of_type(DeviceType::OUTPUT);
+ if devices.output.update_devices(output_devices) {
+ unsafe {
+ devices.output.changed_callback.unwrap()(
+ ctx_ptr as *mut ffi::cubeb,
+ devices.output.callback_user_ptr,
+ );
+ }
+ }
+ }
+ });
+
+ NO_ERR
+}
+
+#[derive(Debug)]
+struct DevicesData {
+ changed_callback: ffi::cubeb_device_collection_changed_callback,
+ callback_user_ptr: *mut c_void,
+ devices: Vec<AudioObjectID>,
+}
+
+impl DevicesData {
+ fn set(
+ &mut self,
+ changed_callback: ffi::cubeb_device_collection_changed_callback,
+ callback_user_ptr: *mut c_void,
+ devices: Vec<AudioObjectID>,
+ ) {
+ self.changed_callback = changed_callback;
+ self.callback_user_ptr = callback_user_ptr;
+ self.devices = devices;
+ }
+
+ fn update_devices(&mut self, devices: Vec<AudioObjectID>) -> bool {
+ // Elements in the vector expected sorted.
+ if self.devices == devices {
+ return false;
+ }
+ self.devices = devices;
+ true
+ }
+
+ fn clear(&mut self) {
+ self.changed_callback = None;
+ self.callback_user_ptr = ptr::null_mut();
+ self.devices.clear();
+ }
+
+ fn is_empty(&self) -> bool {
+ self.changed_callback.is_none()
+ && self.callback_user_ptr.is_null()
+ && self.devices.is_empty()
+ }
+}
+
+impl Default for DevicesData {
+ fn default() -> Self {
+ Self {
+ changed_callback: None,
+ callback_user_ptr: ptr::null_mut(),
+ devices: Vec::new(),
+ }
+ }
+}
+
+#[derive(Debug, Default)]
+struct SharedDevices {
+ input: DevicesData,
+ output: DevicesData,
+}
+
+#[derive(Debug, Default)]
+struct LatencyController {
+ streams: u32,
+ latency: Option<u32>,
+}
+
+impl LatencyController {
+ fn add_stream(&mut self, latency: u32) -> Option<u32> {
+ self.streams += 1;
+ // For the 1st stream set anything within safe min-max
+ if self.streams == 1 {
+ assert!(self.latency.is_none());
+ // Silently clamp the latency down to the platform default, because we
+ // synthetize the clock from the callbacks, and we want the clock to update often.
+ self.latency = Some(latency.clamp(SAFE_MIN_LATENCY_FRAMES, SAFE_MAX_LATENCY_FRAMES));
+ }
+ self.latency
+ }
+
+ fn subtract_stream(&mut self) -> Option<u32> {
+ self.streams -= 1;
+ if self.streams == 0 {
+ assert!(self.latency.is_some());
+ self.latency = None;
+ }
+ self.latency
+ }
+}
+
+pub const OPS: Ops = capi_new!(AudioUnitContext, AudioUnitStream);
+
+// The fisrt member of the Cubeb context must be a pointer to a Ops struct. The Ops struct is an
+// interface to link to all the Cubeb APIs, and the Cubeb interface use this assumption to operate
+// the Cubeb APIs on different implementation.
+// #[repr(C)] is used to prevent any padding from being added in the beginning of the AudioUnitContext.
+#[repr(C)]
+#[derive(Debug)]
+pub struct AudioUnitContext {
+ _ops: *const Ops,
+ serial_queue: Queue,
+ latency_controller: Mutex<LatencyController>,
+ devices: Mutex<SharedDevices>,
+}
+
+impl AudioUnitContext {
+ fn new() -> Self {
+ Self {
+ _ops: &OPS as *const _,
+ serial_queue: Queue::new(DISPATCH_QUEUE_LABEL),
+ latency_controller: Mutex::new(LatencyController::default()),
+ devices: Mutex::new(SharedDevices::default()),
+ }
+ }
+
+ fn active_streams(&self) -> u32 {
+ let controller = self.latency_controller.lock().unwrap();
+ controller.streams
+ }
+
+ fn update_latency_by_adding_stream(&self, latency_frames: u32) -> Option<u32> {
+ let mut controller = self.latency_controller.lock().unwrap();
+ controller.add_stream(latency_frames)
+ }
+
+ fn update_latency_by_removing_stream(&self) -> Option<u32> {
+ let mut controller = self.latency_controller.lock().unwrap();
+ controller.subtract_stream()
+ }
+
+ fn add_devices_changed_listener(
+ &mut self,
+ devtype: DeviceType,
+ collection_changed_callback: ffi::cubeb_device_collection_changed_callback,
+ user_ptr: *mut c_void,
+ ) -> Result<()> {
+ assert!(devtype.intersects(DeviceType::INPUT | DeviceType::OUTPUT));
+ assert!(collection_changed_callback.is_some());
+
+ let context_ptr = self as *mut AudioUnitContext;
+ let mut devices = self.devices.lock().unwrap();
+
+ // Note: second register without unregister first causes 'nope' error.
+ // Current implementation requires unregister before register a new cb.
+ if devtype.contains(DeviceType::INPUT) && devices.input.changed_callback.is_some()
+ || devtype.contains(DeviceType::OUTPUT) && devices.output.changed_callback.is_some()
+ {
+ return Err(Error::invalid_parameter());
+ }
+
+ if devices.input.changed_callback.is_none() && devices.output.changed_callback.is_none() {
+ let address = get_property_address(
+ Property::HardwareDevices,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ );
+ let ret = audio_object_add_property_listener(
+ kAudioObjectSystemObject,
+ &address,
+ audiounit_collection_changed_callback,
+ context_ptr,
+ );
+ if ret != NO_ERR {
+ cubeb_log!(
+ "Cannot add devices-changed listener for {:?}, Error: {}",
+ devtype,
+ ret
+ );
+ return Err(Error::error());
+ }
+ }
+
+ if devtype.contains(DeviceType::INPUT) {
+ // Expected empty after unregister.
+ assert!(devices.input.is_empty());
+ devices.input.set(
+ collection_changed_callback,
+ user_ptr,
+ audiounit_get_devices_of_type(DeviceType::INPUT),
+ );
+ }
+
+ if devtype.contains(DeviceType::OUTPUT) {
+ // Expected empty after unregister.
+ assert!(devices.output.is_empty());
+ devices.output.set(
+ collection_changed_callback,
+ user_ptr,
+ audiounit_get_devices_of_type(DeviceType::OUTPUT),
+ );
+ }
+
+ Ok(())
+ }
+
+ fn remove_devices_changed_listener(&mut self, devtype: DeviceType) -> Result<()> {
+ if !devtype.intersects(DeviceType::INPUT | DeviceType::OUTPUT) {
+ return Err(Error::invalid_parameter());
+ }
+
+ let context_ptr = self as *mut AudioUnitContext;
+ let mut devices = self.devices.lock().unwrap();
+
+ if devtype.contains(DeviceType::INPUT) {
+ devices.input.clear();
+ }
+
+ if devtype.contains(DeviceType::OUTPUT) {
+ devices.output.clear();
+ }
+
+ if devices.input.changed_callback.is_some() || devices.output.changed_callback.is_some() {
+ return Ok(());
+ }
+
+ let address = get_property_address(
+ Property::HardwareDevices,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ );
+ // Note: unregister a non registered cb is not a problem, not checking.
+ let r = audio_object_remove_property_listener(
+ kAudioObjectSystemObject,
+ &address,
+ audiounit_collection_changed_callback,
+ context_ptr,
+ );
+ if r == NO_ERR {
+ Ok(())
+ } else {
+ cubeb_log!(
+ "Cannot remove devices-changed listener for {:?}, Error: {}",
+ devtype,
+ r
+ );
+ Err(Error::error())
+ }
+ }
+}
+
+impl ContextOps for AudioUnitContext {
+ fn init(_context_name: Option<&CStr>) -> Result<Context> {
+ set_notification_runloop();
+ let ctx = Box::new(AudioUnitContext::new());
+ Ok(unsafe { Context::from_ptr(Box::into_raw(ctx) as *mut _) })
+ }
+
+ fn backend_id(&mut self) -> &'static CStr {
+ unsafe { CStr::from_ptr(b"audiounit-rust\0".as_ptr() as *const _) }
+ }
+ #[cfg(target_os = "ios")]
+ fn max_channel_count(&mut self) -> Result<u32> {
+ Ok(2u32)
+ }
+ #[cfg(not(target_os = "ios"))]
+ fn max_channel_count(&mut self) -> Result<u32> {
+ let device = match get_default_device(DeviceType::OUTPUT) {
+ None => {
+ cubeb_log!("Could not get default output device");
+ return Err(Error::error());
+ }
+ Some(id) => id,
+ };
+ get_channel_count(device, DeviceType::OUTPUT).map_err(|e| {
+ cubeb_log!("Cannot get the channel count. Error: {}", e);
+ Error::error()
+ })
+ }
+ #[cfg(target_os = "ios")]
+ fn min_latency(&mut self, _params: StreamParams) -> Result<u32> {
+ Err(not_supported());
+ }
+ #[cfg(not(target_os = "ios"))]
+ fn min_latency(&mut self, _params: StreamParams) -> Result<u32> {
+ let device = match get_default_device(DeviceType::OUTPUT) {
+ None => {
+ cubeb_log!("Could not get default output device");
+ return Err(Error::error());
+ }
+ Some(id) => id,
+ };
+
+ let range =
+ get_device_buffer_frame_size_range(device, DeviceType::OUTPUT).map_err(|e| {
+ cubeb_log!("Could not get acceptable latency range. Error: {}", e);
+ Error::error()
+ })?;
+
+ Ok(cmp::max(range.mMinimum as u32, SAFE_MIN_LATENCY_FRAMES))
+ }
+ #[cfg(target_os = "ios")]
+ fn preferred_sample_rate(&mut self) -> Result<u32> {
+ Err(not_supported());
+ }
+ #[cfg(not(target_os = "ios"))]
+ fn preferred_sample_rate(&mut self) -> Result<u32> {
+ let device = match get_default_device(DeviceType::OUTPUT) {
+ None => {
+ cubeb_log!("Could not get default output device");
+ return Err(Error::error());
+ }
+ Some(id) => id,
+ };
+ let rate = get_device_sample_rate(device, DeviceType::OUTPUT).map_err(|e| {
+ cubeb_log!(
+ "Cannot get the sample rate of the default output device. Error: {}",
+ e
+ );
+ Error::error()
+ })?;
+ Ok(rate as u32)
+ }
+ fn supported_input_processing_params(&mut self) -> Result<InputProcessingParams> {
+ Ok(InputProcessingParams::ECHO_CANCELLATION
+ | InputProcessingParams::NOISE_SUPPRESSION
+ | InputProcessingParams::AUTOMATIC_GAIN_CONTROL)
+ }
+ fn enumerate_devices(
+ &mut self,
+ devtype: DeviceType,
+ collection: &DeviceCollectionRef,
+ ) -> Result<()> {
+ let mut device_infos = Vec::new();
+ let dev_types = [DeviceType::INPUT, DeviceType::OUTPUT];
+ for dev_type in dev_types.iter() {
+ if !devtype.contains(*dev_type) {
+ continue;
+ }
+ let devices = audiounit_get_devices_of_type(*dev_type);
+ for device in devices {
+ if let Ok(info) = create_cubeb_device_info(device, *dev_type) {
+ device_infos.push(info);
+ }
+ }
+ }
+ let (ptr, len) = if device_infos.is_empty() {
+ (ptr::null_mut(), 0)
+ } else {
+ forget_vec(device_infos)
+ };
+ let coll = unsafe { &mut *collection.as_ptr() };
+ coll.device = ptr;
+ coll.count = len;
+ Ok(())
+ }
+ fn device_collection_destroy(&mut self, collection: &mut DeviceCollectionRef) -> Result<()> {
+ assert!(!collection.as_ptr().is_null());
+ let coll = unsafe { &mut *collection.as_ptr() };
+ if coll.device.is_null() {
+ return Ok(());
+ }
+
+ let mut devices = retake_forgotten_vec(coll.device, coll.count);
+ for device in &mut devices {
+ destroy_cubeb_device_info(device);
+ }
+ drop(devices); // Release the memory.
+ coll.device = ptr::null_mut();
+ coll.count = 0;
+ Ok(())
+ }
+ fn stream_init(
+ &mut self,
+ _stream_name: Option<&CStr>,
+ input_device: DeviceId,
+ input_stream_params: Option<&StreamParamsRef>,
+ output_device: DeviceId,
+ output_stream_params: Option<&StreamParamsRef>,
+ latency_frames: u32,
+ data_callback: ffi::cubeb_data_callback,
+ state_callback: ffi::cubeb_state_callback,
+ user_ptr: *mut c_void,
+ ) -> Result<Stream> {
+ if !input_device.is_null() && input_stream_params.is_none() {
+ cubeb_log!("Cannot init an input device without input stream params");
+ return Err(Error::invalid_parameter());
+ }
+
+ if !output_device.is_null() && output_stream_params.is_none() {
+ cubeb_log!("Cannot init an output device without output stream params");
+ return Err(Error::invalid_parameter());
+ }
+
+ if input_stream_params.is_none() && output_stream_params.is_none() {
+ cubeb_log!("Cannot init a stream without any stream params");
+ return Err(Error::invalid_parameter());
+ }
+
+ if data_callback.is_none() {
+ cubeb_log!("Cannot init a stream without a data callback");
+ return Err(Error::invalid_parameter());
+ }
+
+ // Latency cannot change if another stream is operating in parallel. In this case
+ // latency is set to the other stream value.
+ let global_latency_frames = self
+ .update_latency_by_adding_stream(latency_frames)
+ .unwrap();
+ if global_latency_frames != latency_frames {
+ cubeb_log!(
+ "Use global latency {} instead of the requested latency {}.",
+ global_latency_frames,
+ latency_frames
+ );
+ }
+
+ let in_stm_settings = if let Some(params) = input_stream_params {
+ let in_device =
+ match create_device_info(input_device as AudioDeviceID, DeviceType::INPUT) {
+ None => {
+ cubeb_log!("Fail to create device info for input");
+ return Err(Error::error());
+ }
+ Some(d) => d,
+ };
+ let stm_params = StreamParams::from(unsafe { *params.as_ptr() });
+ Some((stm_params, in_device))
+ } else {
+ None
+ };
+
+ let out_stm_settings = if let Some(params) = output_stream_params {
+ let out_device =
+ match create_device_info(output_device as AudioDeviceID, DeviceType::OUTPUT) {
+ None => {
+ cubeb_log!("Fail to create device info for output");
+ return Err(Error::error());
+ }
+ Some(d) => d,
+ };
+ let stm_params = StreamParams::from(unsafe { *params.as_ptr() });
+ Some((stm_params, out_device))
+ } else {
+ None
+ };
+
+ let mut boxed_stream = Box::new(AudioUnitStream::new(
+ self,
+ user_ptr,
+ data_callback,
+ state_callback,
+ global_latency_frames,
+ ));
+
+ // Rename the task queue to be an unique label.
+ let queue_label = format!("{}.{:p}", DISPATCH_QUEUE_LABEL, boxed_stream.as_ref());
+ boxed_stream.queue = Queue::new(queue_label.as_str());
+
+ boxed_stream.core_stream_data =
+ CoreStreamData::new(boxed_stream.as_ref(), in_stm_settings, out_stm_settings);
+
+ let mut result = Ok(());
+ boxed_stream.queue.clone().run_sync(|| {
+ result = boxed_stream.core_stream_data.setup();
+ });
+ if let Err(r) = result {
+ cubeb_log!(
+ "({:p}) Could not setup the audiounit stream.",
+ boxed_stream.as_ref()
+ );
+ return Err(r);
+ }
+
+ let cubeb_stream = unsafe { Stream::from_ptr(Box::into_raw(boxed_stream) as *mut _) };
+ cubeb_log!(
+ "({:p}) Cubeb stream init successful.",
+ cubeb_stream.as_ref()
+ );
+ Ok(cubeb_stream)
+ }
+ fn register_device_collection_changed(
+ &mut self,
+ devtype: DeviceType,
+ collection_changed_callback: ffi::cubeb_device_collection_changed_callback,
+ user_ptr: *mut c_void,
+ ) -> Result<()> {
+ if devtype == DeviceType::UNKNOWN {
+ return Err(Error::invalid_parameter());
+ }
+ if collection_changed_callback.is_some() {
+ self.add_devices_changed_listener(devtype, collection_changed_callback, user_ptr)
+ } else {
+ self.remove_devices_changed_listener(devtype)
+ }
+ }
+}
+
+impl Drop for AudioUnitContext {
+ fn drop(&mut self) {
+ let devices = self.devices.lock().unwrap();
+ assert!(
+ devices.input.changed_callback.is_none() && devices.output.changed_callback.is_none()
+ );
+
+ {
+ let controller = self.latency_controller.lock().unwrap();
+ // Disabling this assert for bug 1083664 -- we seem to leak a stream
+ // assert(controller.streams == 0);
+ if controller.streams > 0 {
+ cubeb_log!(
+ "({:p}) API misuse, {} streams active when context destroyed!",
+ self as *const AudioUnitContext,
+ controller.streams
+ );
+ }
+ }
+ // Make sure all the pending (device-collection-changed-callback) tasks
+ // in queue are done, and cancel all the tasks appended after `drop` is executed.
+ let queue = self.serial_queue.clone();
+ queue.run_final(|| {});
+ }
+}
+
+#[allow(clippy::non_send_fields_in_send_ty)]
+unsafe impl Send for AudioUnitContext {}
+unsafe impl Sync for AudioUnitContext {}
+
+// Holds the information for an audio input callback call, for debugging purposes.
+struct InputCallbackData {
+ bytes: u32,
+ rendered_frames: u32,
+ total_available: usize,
+ channels: u32,
+ num_buf: u32,
+}
+struct InputCallbackLogger {
+ prod: ringbuf::Producer<InputCallbackData>,
+ cons: ringbuf::Consumer<InputCallbackData>,
+}
+
+impl InputCallbackLogger {
+ fn new() -> Self {
+ let ring = RingBuffer::<InputCallbackData>::new(16);
+ let (prod, cons) = ring.split();
+ Self { prod, cons }
+ }
+
+ fn push(&mut self, data: InputCallbackData) {
+ self.prod.push(data);
+ }
+
+ fn pop(&mut self) -> Option<InputCallbackData> {
+ self.cons.pop()
+ }
+
+ fn is_empty(&self) -> bool {
+ self.cons.is_empty()
+ }
+}
+
+impl fmt::Debug for InputCallbackLogger {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "InputCallbackLogger {{ prod: {}, cons: {} }}",
+ self.prod.len(),
+ self.cons.len()
+ )
+ }
+}
+
+#[derive(Debug)]
+struct CoreStreamData<'ctx> {
+ stm_ptr: *const AudioUnitStream<'ctx>,
+ aggregate_device: Option<AggregateDevice>,
+ mixer: Option<Mixer>,
+ resampler: Resampler,
+ // Stream creation parameters.
+ input_stream_params: StreamParams,
+ output_stream_params: StreamParams,
+ // Device settings for AudioUnits.
+ input_dev_desc: AudioStreamBasicDescription,
+ output_dev_desc: AudioStreamBasicDescription,
+ // I/O AudioUnits.
+ input_unit: AudioUnit,
+ output_unit: AudioUnit,
+ // Info of the I/O devices.
+ input_device: device_info,
+ output_device: device_info,
+ input_processing_params: InputProcessingParams,
+ input_mute: bool,
+ input_buffer_manager: Option<BufferManager>,
+ // Listeners indicating what system events are monitored.
+ default_input_listener: Option<device_property_listener>,
+ default_output_listener: Option<device_property_listener>,
+ input_alive_listener: Option<device_property_listener>,
+ input_source_listener: Option<device_property_listener>,
+ output_alive_listener: Option<device_property_listener>,
+ output_source_listener: Option<device_property_listener>,
+ input_logging: Option<InputCallbackLogger>,
+}
+
+impl<'ctx> Default for CoreStreamData<'ctx> {
+ fn default() -> Self {
+ Self {
+ stm_ptr: ptr::null(),
+ aggregate_device: None,
+ mixer: None,
+ resampler: Resampler::default(),
+ input_stream_params: StreamParams::from(ffi::cubeb_stream_params {
+ format: ffi::CUBEB_SAMPLE_FLOAT32NE,
+ rate: 0,
+ channels: 0,
+ layout: ffi::CUBEB_LAYOUT_UNDEFINED,
+ prefs: ffi::CUBEB_STREAM_PREF_NONE,
+ }),
+ output_stream_params: StreamParams::from(ffi::cubeb_stream_params {
+ format: ffi::CUBEB_SAMPLE_FLOAT32NE,
+ rate: 0,
+ channels: 0,
+ layout: ffi::CUBEB_LAYOUT_UNDEFINED,
+ prefs: ffi::CUBEB_STREAM_PREF_NONE,
+ }),
+ input_dev_desc: AudioStreamBasicDescription::default(),
+ output_dev_desc: AudioStreamBasicDescription::default(),
+ input_unit: ptr::null_mut(),
+ output_unit: ptr::null_mut(),
+ input_device: device_info::default(),
+ output_device: device_info::default(),
+ input_processing_params: InputProcessingParams::NONE,
+ input_mute: false,
+ input_buffer_manager: None,
+ default_input_listener: None,
+ default_output_listener: None,
+ input_alive_listener: None,
+ input_source_listener: None,
+ output_alive_listener: None,
+ output_source_listener: None,
+ input_logging: None,
+ }
+ }
+}
+
+impl<'ctx> CoreStreamData<'ctx> {
+ fn new(
+ stm: &AudioUnitStream<'ctx>,
+ input_stream_settings: Option<(StreamParams, device_info)>,
+ output_stream_settings: Option<(StreamParams, device_info)>,
+ ) -> Self {
+ fn get_default_sttream_params() -> StreamParams {
+ StreamParams::from(ffi::cubeb_stream_params {
+ format: ffi::CUBEB_SAMPLE_FLOAT32NE,
+ rate: 0,
+ channels: 0,
+ layout: ffi::CUBEB_LAYOUT_UNDEFINED,
+ prefs: ffi::CUBEB_STREAM_PREF_NONE,
+ })
+ }
+ let (in_stm_params, in_dev) =
+ input_stream_settings.unwrap_or((get_default_sttream_params(), device_info::default()));
+ let (out_stm_params, out_dev) = output_stream_settings
+ .unwrap_or((get_default_sttream_params(), device_info::default()));
+ Self {
+ stm_ptr: stm,
+ aggregate_device: None,
+ mixer: None,
+ resampler: Resampler::default(),
+ input_stream_params: in_stm_params,
+ output_stream_params: out_stm_params,
+ input_dev_desc: AudioStreamBasicDescription::default(),
+ output_dev_desc: AudioStreamBasicDescription::default(),
+ input_unit: ptr::null_mut(),
+ output_unit: ptr::null_mut(),
+ input_device: in_dev,
+ output_device: out_dev,
+ input_processing_params: InputProcessingParams::NONE,
+ input_mute: false,
+ input_buffer_manager: None,
+ default_input_listener: None,
+ default_output_listener: None,
+ input_alive_listener: None,
+ input_source_listener: None,
+ output_alive_listener: None,
+ output_source_listener: None,
+ input_logging: None,
+ }
+ }
+
+ fn debug_assert_is_on_stream_queue(&self) {
+ if self.stm_ptr.is_null() {
+ return;
+ }
+ let stm = unsafe { &*self.stm_ptr };
+ stm.queue.debug_assert_is_current();
+ }
+
+ fn start_audiounits(&self) -> Result<()> {
+ self.debug_assert_is_on_stream_queue();
+ // Only allowed to be called after the stream is initialized
+ // and before the stream is destroyed.
+ debug_assert!(!self.input_unit.is_null() || !self.output_unit.is_null());
+
+ if !self.input_unit.is_null() {
+ start_audiounit(self.input_unit)?;
+ }
+ if self.using_voice_processing_unit() {
+ // Handle the VoiceProcessIO case where there is a single unit.
+ return Ok(());
+ }
+ if !self.output_unit.is_null() {
+ start_audiounit(self.output_unit)?;
+ }
+ Ok(())
+ }
+
+ fn stop_audiounits(&self) {
+ self.debug_assert_is_on_stream_queue();
+ if !self.input_unit.is_null() {
+ let r = stop_audiounit(self.input_unit);
+ assert!(r.is_ok());
+ }
+ if self.using_voice_processing_unit() {
+ // Handle the VoiceProcessIO case where there is a single unit.
+ return;
+ }
+ if !self.output_unit.is_null() {
+ let r = stop_audiounit(self.output_unit);
+ assert!(r.is_ok());
+ }
+ }
+
+ fn has_input(&self) -> bool {
+ self.input_stream_params.rate() > 0
+ }
+
+ fn has_output(&self) -> bool {
+ self.output_stream_params.rate() > 0
+ }
+
+ fn using_voice_processing_unit(&self) -> bool {
+ !self.input_unit.is_null() && self.input_unit == self.output_unit
+ }
+
+ fn same_clock_domain(&self) -> bool {
+ self.debug_assert_is_on_stream_queue();
+ // If not setting up a duplex stream, there is only one device,
+ // no reclocking necessary.
+ if !(self.has_input() && self.has_output()) {
+ return true;
+ }
+ let input_domain = match get_clock_domain(self.input_device.id, DeviceType::INPUT) {
+ Ok(clock_domain) => clock_domain,
+ Err(_) => {
+ cubeb_log!("Coudn't determine clock domains for input.");
+ return false;
+ }
+ };
+
+ let output_domain = match get_clock_domain(self.output_device.id, DeviceType::OUTPUT) {
+ Ok(clock_domain) => clock_domain,
+ Err(_) => {
+ cubeb_log!("Coudn't determine clock domains for input.");
+ return false;
+ }
+ };
+ input_domain == output_domain
+ }
+
+ fn should_block_vpio_for_device_pair(
+ &self,
+ in_device: &device_info,
+ out_device: &device_info,
+ ) -> bool {
+ self.debug_assert_is_on_stream_queue();
+ cubeb_log!("Evaluating device pair against VPIO block list");
+ let log_device = |id, devtype| -> std::result::Result<(), OSStatus> {
+ cubeb_log!("{} uid=\"{}\", model_uid=\"{}\", transport_type={:?}, source={:?}, source_name=\"{}\", name=\"{}\", manufacturer=\"{}\"",
+ if devtype == DeviceType::INPUT {
+ "Input"
+ } else {
+ debug_assert_eq!(devtype, DeviceType::OUTPUT);
+ "Output"
+ },
+ get_device_uid(id, devtype).map(|s| s.into_string()).unwrap_or_default(),
+ get_device_model_uid(id, devtype).map(|s| s.into_string()).unwrap_or_default(),
+ convert_uint32_into_string(get_device_transport_type(id, devtype).unwrap_or(0)),
+ convert_uint32_into_string(get_device_source(id, devtype).unwrap_or(0)),
+ get_device_source_name(id, devtype).map(|s| s.into_string()).unwrap_or_default(),
+ get_device_name(id, devtype).map(|s| s.into_string()).unwrap_or_default(),
+ get_device_manufacturer(id, devtype).map(|s| s.into_string()).unwrap_or_default());
+ Ok(())
+ };
+ log_device(in_device.id, DeviceType::INPUT);
+ log_device(out_device.id, DeviceType::OUTPUT);
+ match (
+ get_device_model_uid(in_device.id, DeviceType::INPUT).map(|s| s.to_string()),
+ get_device_model_uid(out_device.id, DeviceType::OUTPUT).map(|s| s.to_string()),
+ ) {
+ (Ok(in_model_uid), Ok(out_model_uid))
+ if in_model_uid.contains(APPLE_STUDIO_DISPLAY_USB_ID)
+ && out_model_uid.contains(APPLE_STUDIO_DISPLAY_USB_ID) =>
+ {
+ cubeb_log!("Both input and output device is an Apple Studio Display. BLOCKED");
+ true
+ }
+ _ => {
+ cubeb_log!("Device pair is not blocked");
+ false
+ }
+ }
+ }
+
+ fn create_audiounits(&mut self) -> Result<(device_info, device_info)> {
+ self.debug_assert_is_on_stream_queue();
+ let should_use_voice_processing_unit = self.has_input()
+ && self.has_output()
+ && self
+ .input_stream_params
+ .prefs()
+ .contains(StreamPrefs::VOICE)
+ && !self.should_block_vpio_for_device_pair(&self.input_device, &self.output_device);
+
+ let should_use_aggregate_device = {
+ // It's impossible to create an aggregate device from an aggregate device, and it's
+ // unnecessary to create an aggregate device when opening the same device input/output. In
+ // all other cases, use an aggregate device.
+ let mut either_already_aggregate = false;
+ if self.has_input() {
+ let input_is_aggregate =
+ get_device_transport_type(self.input_device.id, DeviceType::INPUT).unwrap_or(0)
+ == kAudioDeviceTransportTypeAggregate;
+ if input_is_aggregate {
+ either_already_aggregate = true;
+ }
+ cubeb_log!(
+ "Input device ID: {} (aggregate: {:?})",
+ self.input_device.id,
+ input_is_aggregate
+ );
+ }
+ if self.has_output() {
+ let output_is_aggregate =
+ get_device_transport_type(self.output_device.id, DeviceType::OUTPUT)
+ .unwrap_or(0)
+ == kAudioDeviceTransportTypeAggregate;
+ if output_is_aggregate {
+ either_already_aggregate = true;
+ }
+ cubeb_log!(
+ "Output device ID: {} (aggregate: {:?})",
+ self.output_device.id,
+ output_is_aggregate
+ );
+ }
+ // Only use an aggregate device when the device are different.
+ self.has_input()
+ && self.has_output()
+ && self.input_device.id != self.output_device.id
+ && !either_already_aggregate
+ };
+
+ // Create an AudioUnit:
+ // - If we're eligible to use voice processing, try creating a VoiceProcessingIO AudioUnit.
+ // - If we should use an aggregate device, try creating one and input and output AudioUnits next.
+ // - As last resort, create regular AudioUnits. This is also the normal non-duplex path.
+
+ if should_use_voice_processing_unit {
+ if let Ok(au) =
+ create_voiceprocessing_audiounit(&self.input_device, &self.output_device)
+ {
+ cubeb_log!("({:p}) Using VoiceProcessingIO AudioUnit", self.stm_ptr);
+ self.input_unit = au;
+ self.output_unit = au;
+ return Ok((self.input_device.clone(), self.output_device.clone()));
+ }
+ cubeb_log!(
+ "({:p}) Failed to create VoiceProcessingIO AudioUnit. Trying a regular one.",
+ self.stm_ptr
+ );
+ }
+
+ if should_use_aggregate_device {
+ if let Ok(device) = AggregateDevice::new(self.input_device.id, self.output_device.id) {
+ let in_dev_info = {
+ device_info {
+ id: device.get_device_id(),
+ ..self.input_device
+ }
+ };
+ let out_dev_info = {
+ device_info {
+ id: device.get_device_id(),
+ ..self.output_device
+ }
+ };
+
+ match (
+ create_audiounit(&in_dev_info),
+ create_audiounit(&out_dev_info),
+ ) {
+ (Ok(in_au), Ok(out_au)) => {
+ cubeb_log!(
+ "({:p}) Using an aggregate device {} for input and output.",
+ self.stm_ptr,
+ device.get_device_id()
+ );
+ self.aggregate_device = Some(device);
+ self.input_unit = in_au;
+ self.output_unit = out_au;
+ return Ok((in_dev_info, out_dev_info));
+ }
+ (Err(e), Ok(au)) => {
+ cubeb_log!(
+ "({:p}) Failed to create input AudioUnit for aggregate device. Error: {}.",
+ self.stm_ptr,
+ e
+ );
+ dispose_audio_unit(au);
+ }
+ (Ok(au), Err(e)) => {
+ cubeb_log!(
+ "({:p}) Failed to create output AudioUnit for aggregate device. Error: {}.",
+ self.stm_ptr,
+ e
+ );
+ dispose_audio_unit(au);
+ }
+ (Err(e), _) => {
+ cubeb_log!(
+ "({:p}) Failed to create AudioUnits for aggregate device. Error: {}.",
+ self.stm_ptr,
+ e
+ );
+ }
+ }
+ }
+ cubeb_log!(
+ "({:p}) Failed to set up aggregate device. Using regular AudioUnits.",
+ self.stm_ptr
+ );
+ }
+
+ if self.has_input() {
+ match create_audiounit(&self.input_device) {
+ Ok(in_au) => self.input_unit = in_au,
+ Err(e) => {
+ cubeb_log!(
+ "({:p}) Failed to create regular AudioUnit for input. Error: {}",
+ self.stm_ptr,
+ e
+ );
+ return Err(e);
+ }
+ }
+ }
+
+ if self.has_output() {
+ match create_audiounit(&self.output_device) {
+ Ok(out_au) => self.output_unit = out_au,
+ Err(e) => {
+ cubeb_log!(
+ "({:p}) Failed to create regular AudioUnit for output. Error: {}",
+ self.stm_ptr,
+ e
+ );
+ if !self.input_unit.is_null() {
+ dispose_audio_unit(self.input_unit);
+ self.input_unit = ptr::null_mut();
+ }
+ return Err(e);
+ }
+ }
+ }
+
+ Ok((self.input_device.clone(), self.output_device.clone()))
+ }
+
+ #[allow(clippy::cognitive_complexity)] // TODO: Refactoring.
+ fn setup(&mut self) -> Result<()> {
+ self.debug_assert_is_on_stream_queue();
+ if self
+ .input_stream_params
+ .prefs()
+ .contains(StreamPrefs::LOOPBACK)
+ || self
+ .output_stream_params
+ .prefs()
+ .contains(StreamPrefs::LOOPBACK)
+ {
+ cubeb_log!("({:p}) Loopback not supported for audiounit.", self.stm_ptr);
+ return Err(Error::not_supported());
+ }
+
+ let same_clock_domain = self.same_clock_domain();
+ let (in_dev_info, out_dev_info) = self.create_audiounits()?;
+ let using_voice_processing_unit = self.using_voice_processing_unit();
+
+ assert!(!self.stm_ptr.is_null());
+ let stream = unsafe { &(*self.stm_ptr) };
+
+ // Configure I/O stream
+ if self.has_input() {
+ assert!(!self.input_unit.is_null());
+
+ cubeb_log!(
+ "({:p}) Initializing input by device info: {:?}",
+ self.stm_ptr,
+ in_dev_info
+ );
+
+ let device_channel_count =
+ get_channel_count(self.input_device.id, DeviceType::INPUT).unwrap_or(0);
+ if device_channel_count < self.input_stream_params.channels() {
+ cubeb_log!(
+ "({:p}) Invalid input channel count; device={}, params={}",
+ self.stm_ptr,
+ device_channel_count,
+ self.input_stream_params.channels()
+ );
+ return Err(Error::invalid_parameter());
+ }
+
+ cubeb_log!(
+ "({:p}) Opening input side: rate {}, channels {}, format {:?}, layout {:?}, prefs {:?}, latency in frames {}, voice processing {}.",
+ self.stm_ptr,
+ self.input_stream_params.rate(),
+ self.input_stream_params.channels(),
+ self.input_stream_params.format(),
+ self.input_stream_params.layout(),
+ self.input_stream_params.prefs(),
+ stream.latency_frames,
+ using_voice_processing_unit
+ );
+
+ // Get input device hardware information.
+ let mut input_hw_desc = AudioStreamBasicDescription::default();
+ let mut size = mem::size_of::<AudioStreamBasicDescription>();
+ let r = audio_unit_get_property(
+ self.input_unit,
+ kAudioUnitProperty_StreamFormat,
+ kAudioUnitScope_Input,
+ AU_IN_BUS,
+ &mut input_hw_desc,
+ &mut size,
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitGetProperty/input/kAudioUnitProperty_StreamFormat rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+ cubeb_log!(
+ "({:p}) Input hardware description: {:?}",
+ self.stm_ptr,
+ input_hw_desc
+ );
+ // In some cases with VPIO the stream format's mChannelsPerFrame is higher than
+ // expected. Use get_channel_count as source of truth.
+ input_hw_desc.mChannelsPerFrame = device_channel_count;
+ // Notice: when we are using aggregate device, the input_hw_desc.mChannelsPerFrame is
+ // the total of all the input channel count of the devices added in the aggregate device.
+ // Due to our aggregate device settings, the data captured by the output device's input
+ // channels will be put in the beginning of the raw data given by the input callback.
+
+ // Always request all the input channels of the device, and only pass the correct
+ // channels to the audio callback.
+ let params = unsafe {
+ let mut p = *self.input_stream_params.as_ptr();
+ p.channels = if using_voice_processing_unit {
+ // VPIO is always MONO.
+ 1
+ } else {
+ input_hw_desc.mChannelsPerFrame
+ };
+ // Input AudioUnit must be configured with device's sample rate.
+ // we will resample inside input callback.
+ p.rate = input_hw_desc.mSampleRate as _;
+ StreamParams::from(p)
+ };
+
+ self.input_dev_desc = create_stream_description(&params).map_err(|e| {
+ cubeb_log!(
+ "({:p}) Setting format description for input failed.",
+ self.stm_ptr
+ );
+ e
+ })?;
+
+ assert_eq!(self.input_dev_desc.mSampleRate, input_hw_desc.mSampleRate);
+
+ // Use latency to set buffer size
+ assert_ne!(stream.latency_frames, 0);
+ if let Err(r) =
+ set_buffer_size_sync(self.input_unit, DeviceType::INPUT, stream.latency_frames)
+ {
+ cubeb_log!("({:p}) Error in change input buffer size.", self.stm_ptr);
+ return Err(r);
+ }
+
+ let r = audio_unit_set_property(
+ self.input_unit,
+ kAudioUnitProperty_StreamFormat,
+ kAudioUnitScope_Output,
+ AU_IN_BUS,
+ &self.input_dev_desc,
+ mem::size_of::<AudioStreamBasicDescription>(),
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitSetProperty/input/kAudioUnitProperty_StreamFormat rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+
+ // Frames per buffer in the input callback.
+ let r = audio_unit_set_property(
+ self.input_unit,
+ kAudioUnitProperty_MaximumFramesPerSlice,
+ kAudioUnitScope_Global,
+ AU_IN_BUS,
+ &stream.latency_frames,
+ mem::size_of::<u32>(),
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitSetProperty/input/kAudioUnitProperty_MaximumFramesPerSlice rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+
+ // When we use the aggregate device, the self.input_dev_desc.mChannelsPerFrame is the
+ // total input channel count of all the device added in the aggregate device. However,
+ // we only need the audio data captured by the requested input device, so we need to
+ // ignore some data captured by the audio input of the requested output device (e.g.,
+ // the requested output device is a USB headset with built-in mic), in the beginning of
+ // the raw data taken from input callback.
+ self.input_buffer_manager = Some(BufferManager::new(
+ self.input_stream_params.format(),
+ SAFE_MAX_LATENCY_FRAMES as usize,
+ self.input_dev_desc.mChannelsPerFrame as usize,
+ self.input_dev_desc
+ .mChannelsPerFrame
+ .saturating_sub(device_channel_count) as usize,
+ self.input_stream_params.channels() as usize,
+ ));
+
+ let aurcbs_in = AURenderCallbackStruct {
+ inputProc: Some(audiounit_input_callback),
+ inputProcRefCon: self.stm_ptr as *mut c_void,
+ };
+
+ let r = audio_unit_set_property(
+ self.input_unit,
+ kAudioOutputUnitProperty_SetInputCallback,
+ kAudioUnitScope_Global,
+ AU_OUT_BUS,
+ &aurcbs_in,
+ mem::size_of_val(&aurcbs_in),
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitSetProperty/input/kAudioOutputUnitProperty_SetInputCallback rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+
+ stream.frames_read.store(0, Ordering::SeqCst);
+
+ cubeb_log!(
+ "({:p}) Input audiounit init with device {} successfully.",
+ self.stm_ptr,
+ in_dev_info.id
+ );
+ }
+
+ if self.has_output() {
+ assert!(!self.output_unit.is_null());
+
+ cubeb_log!(
+ "({:p}) Initialize output by device info: {:?}",
+ self.stm_ptr,
+ out_dev_info
+ );
+
+ cubeb_log!(
+ "({:p}) Opening output side: rate {}, channels {}, format {:?}, layout {:?}, prefs {:?}, latency in frames {}, voice processing {}.",
+ self.stm_ptr,
+ self.output_stream_params.rate(),
+ self.output_stream_params.channels(),
+ self.output_stream_params.format(),
+ self.output_stream_params.layout(),
+ self.output_stream_params.prefs(),
+ stream.latency_frames,
+ using_voice_processing_unit
+ );
+
+ // Get output device hardware information.
+ let mut output_hw_desc = AudioStreamBasicDescription::default();
+ let mut size = mem::size_of::<AudioStreamBasicDescription>();
+ let r = audio_unit_get_property(
+ self.output_unit,
+ kAudioUnitProperty_StreamFormat,
+ kAudioUnitScope_Output,
+ AU_OUT_BUS,
+ &mut output_hw_desc,
+ &mut size,
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitGetProperty/output/kAudioUnitProperty_StreamFormat rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+ cubeb_log!(
+ "({:p}) Output hardware description: {:?}",
+ self.stm_ptr,
+ output_hw_desc
+ );
+
+ // In some cases with (other streams using) VPIO the stream format's mChannelsPerFrame
+ // is higher than expected. Use get_channel_count as source of truth.
+ output_hw_desc.mChannelsPerFrame =
+ get_channel_count(self.output_device.id, DeviceType::OUTPUT).unwrap_or(0);
+
+ // This has been observed in the wild.
+ if output_hw_desc.mChannelsPerFrame == 0 {
+ cubeb_log!(
+ "({:p}) Output hardware description channel count is zero",
+ self.stm_ptr
+ );
+ return Err(Error::error());
+ }
+
+ // Notice: when we are using aggregate device, the output_hw_desc.mChannelsPerFrame is
+ // the total of all the output channel count of the devices added in the aggregate device.
+ // Due to our aggregate device settings, the data recorded by the input device's output
+ // channels will be appended at the end of the raw data given by the output callback.
+ let params = unsafe {
+ let mut p = *self.output_stream_params.as_ptr();
+ p.channels = if using_voice_processing_unit {
+ // VPIO is always MONO.
+ 1
+ } else {
+ output_hw_desc.mChannelsPerFrame
+ };
+ if using_voice_processing_unit {
+ // VPIO will always use the sample rate of the input hw for both input and output,
+ // as reported to us. (We can override it but we cannot improve quality this way).
+ p.rate = self.input_dev_desc.mSampleRate as _;
+ }
+ StreamParams::from(p)
+ };
+
+ self.output_dev_desc = create_stream_description(&params).map_err(|e| {
+ cubeb_log!(
+ "({:p}) Could not initialize the audio stream description.",
+ self.stm_ptr
+ );
+ e
+ })?;
+
+ let device_layout = self
+ .get_output_channel_layout()
+ .map_err(|e| {
+ cubeb_log!(
+ "({:p}) Could not get any channel layout. Defaulting to no channels.",
+ self.stm_ptr
+ );
+ e
+ })
+ .unwrap_or_default();
+
+ cubeb_log!(
+ "({:p} Using output device channel layout {:?}",
+ self.stm_ptr,
+ device_layout
+ );
+
+ // The mixer will be set up when
+ // 1. using aggregate device whose input device has output channels
+ // 2. output device has more channels than we need
+ // 3. output device has different layout than the one we have
+ self.mixer = if self.output_dev_desc.mChannelsPerFrame
+ != self.output_stream_params.channels()
+ || device_layout != mixer::get_channel_order(self.output_stream_params.layout())
+ {
+ cubeb_log!("Incompatible channel layouts detected, setting up remixer");
+ // We will be remixing the data before it reaches the output device.
+ Some(Mixer::new(
+ self.output_stream_params.format(),
+ self.output_stream_params.channels() as usize,
+ self.output_stream_params.layout(),
+ self.output_dev_desc.mChannelsPerFrame as usize,
+ device_layout,
+ ))
+ } else {
+ None
+ };
+
+ let r = audio_unit_set_property(
+ self.output_unit,
+ kAudioUnitProperty_StreamFormat,
+ kAudioUnitScope_Input,
+ AU_OUT_BUS,
+ &self.output_dev_desc,
+ mem::size_of::<AudioStreamBasicDescription>(),
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitSetProperty/output/kAudioUnitProperty_StreamFormat rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+
+ // Use latency to set buffer size
+ assert_ne!(stream.latency_frames, 0);
+ if let Err(r) =
+ set_buffer_size_sync(self.output_unit, DeviceType::OUTPUT, stream.latency_frames)
+ {
+ cubeb_log!("({:p}) Error in change output buffer size.", self.stm_ptr);
+ return Err(r);
+ }
+
+ // Frames per buffer in the input callback.
+ let r = audio_unit_set_property(
+ self.output_unit,
+ kAudioUnitProperty_MaximumFramesPerSlice,
+ kAudioUnitScope_Global,
+ AU_OUT_BUS,
+ &stream.latency_frames,
+ mem::size_of::<u32>(),
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitSetProperty/output/kAudioUnitProperty_MaximumFramesPerSlice rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+
+ let aurcbs_out = AURenderCallbackStruct {
+ inputProc: Some(audiounit_output_callback),
+ inputProcRefCon: self.stm_ptr as *mut c_void,
+ };
+ let r = audio_unit_set_property(
+ self.output_unit,
+ kAudioUnitProperty_SetRenderCallback,
+ kAudioUnitScope_Global,
+ AU_OUT_BUS,
+ &aurcbs_out,
+ mem::size_of_val(&aurcbs_out),
+ );
+ if r != NO_ERR {
+ cubeb_log!(
+ "AudioUnitSetProperty/output/kAudioUnitProperty_SetRenderCallback rv={}",
+ r
+ );
+ return Err(Error::error());
+ }
+
+ stream.frames_written.store(0, Ordering::SeqCst);
+
+ cubeb_log!(
+ "({:p}) Output audiounit init with device {} successfully.",
+ self.stm_ptr,
+ out_dev_info.id
+ );
+ }
+
+ // We use a resampler because input AudioUnit operates
+ // reliable only in the capture device sample rate.
+ // Resampler will convert it to the user sample rate
+ // and deliver it to the callback.
+ let target_sample_rate = if self.has_input() {
+ self.input_stream_params.rate()
+ } else {
+ assert!(self.has_output());
+ self.output_stream_params.rate()
+ };
+
+ let resampler_input_params = if self.has_input() {
+ let mut p = unsafe { *(self.input_stream_params.as_ptr()) };
+ p.rate = self.input_dev_desc.mSampleRate as u32;
+ Some(p)
+ } else {
+ None
+ };
+ let resampler_output_params = if self.has_output() {
+ let mut p = unsafe { *(self.output_stream_params.as_ptr()) };
+ p.rate = self.output_dev_desc.mSampleRate as u32;
+ Some(p)
+ } else {
+ None
+ };
+
+ // Only reclock if there is an input and we couldn't use an aggregate device, and the
+ // devices are not part of the same clock domain.
+ let reclock_policy = if self.aggregate_device.is_none()
+ && !using_voice_processing_unit
+ && !same_clock_domain
+ {
+ cubeb_log!(
+ "Reclocking duplex steam using_aggregate_device={} same_clock_domain={}",
+ self.aggregate_device.is_some(),
+ same_clock_domain
+ );
+ ffi::CUBEB_RESAMPLER_RECLOCK_INPUT
+ } else {
+ ffi::CUBEB_RESAMPLER_RECLOCK_NONE
+ };
+
+ self.resampler = Resampler::new(
+ self.stm_ptr as *mut ffi::cubeb_stream,
+ resampler_input_params,
+ resampler_output_params,
+ target_sample_rate,
+ stream.data_callback,
+ stream.user_ptr,
+ ffi::CUBEB_RESAMPLER_QUALITY_DESKTOP,
+ reclock_policy,
+ );
+
+ // In duplex, the input thread might be different from the output thread, and we're logging
+ // everything from the output thread: relay the audio input callback information using a
+ // ring buffer to diagnose issues.
+ if self.has_input() && self.has_output() {
+ self.input_logging = Some(InputCallbackLogger::new());
+ }
+
+ if !self.input_unit.is_null() {
+ let r = audio_unit_initialize(self.input_unit);
+ if r != NO_ERR {
+ cubeb_log!("AudioUnitInitialize/input rv={}", r);
+ return Err(Error::error());
+ }
+
+ stream.input_device_latency_frames.store(
+ get_fixed_latency(self.input_device.id, DeviceType::INPUT),
+ Ordering::SeqCst,
+ );
+ }
+
+ if !self.output_unit.is_null() {
+ if self.input_unit != self.output_unit {
+ let r = audio_unit_initialize(self.output_unit);
+ if r != NO_ERR {
+ cubeb_log!("AudioUnitInitialize/output rv={}", r);
+ return Err(Error::error());
+ }
+ }
+
+ stream.output_device_latency_frames.store(
+ get_fixed_latency(self.output_device.id, DeviceType::OUTPUT),
+ Ordering::SeqCst,
+ );
+
+ let mut unit_s: f64 = 0.0;
+ let mut size = mem::size_of_val(&unit_s);
+ if audio_unit_get_property(
+ self.output_unit,
+ kAudioUnitProperty_Latency,
+ kAudioUnitScope_Global,
+ 0,
+ &mut unit_s,
+ &mut size,
+ ) == NO_ERR
+ {
+ stream.output_device_latency_frames.fetch_add(
+ (unit_s * self.output_dev_desc.mSampleRate) as u32,
+ Ordering::SeqCst,
+ );
+ }
+ }
+
+ if using_voice_processing_unit {
+ // The VPIO AudioUnit automatically ducks other audio streams on the VPIO
+ // output device. Its ramp duration is 0.5s when ducking, so unduck similarly
+ // now.
+ // NOTE: On MacOS 14 the ducking happens on creation of the VPIO AudioUnit.
+ // On MacOS 10.15 it happens on both creation and initialization, which
+ // is why we defer the unducking until now.
+ let r = audio_device_duck(self.output_device.id, 1.0, ptr::null_mut(), 0.5);
+ if r != NO_ERR {
+ cubeb_log!(
+ "({:p}) Failed to undo ducking of voiceprocessing on output device {}. Proceeding... Error: {}",
+ self.stm_ptr,
+ self.output_device.id,
+ r
+ );
+ }
+
+ // Always try to remember the applied input mute state. If it cannot be applied
+ // to the new device pair, we notify the client of an error and it will have to
+ // open a new stream.
+ if let Err(r) = set_input_mute(self.input_unit, self.input_mute) {
+ cubeb_log!(
+ "({:p}) Failed to set mute state of voiceprocessing. Error: {}",
+ self.stm_ptr,
+ r
+ );
+ return Err(r);
+ }
+
+ // Always try to remember the applied input processing params. If they cannot
+ // be applied in the new device pair, we notify the client of an error and it
+ // will have to open a new stream.
+ if let Err(r) =
+ set_input_processing_params(self.input_unit, self.input_processing_params)
+ {
+ cubeb_log!(
+ "({:p}) Failed to set params of voiceprocessing. Error: {}",
+ self.stm_ptr,
+ r
+ );
+ return Err(r);
+ }
+ }
+
+ if let Err(r) = self.install_system_changed_callback() {
+ cubeb_log!(
+ "({:p}) Could not install the device change callback.",
+ self.stm_ptr
+ );
+ return Err(r);
+ }
+
+ if let Err(r) = self.install_device_changed_callback() {
+ cubeb_log!(
+ "({:p}) Could not install all device change callback.",
+ self.stm_ptr
+ );
+ return Err(r);
+ }
+
+ // We have either default_input_listener or input_alive_listener.
+ // We cannot have both of them at the same time.
+ assert!(
+ !self.has_input()
+ || ((self.default_input_listener.is_some() != self.input_alive_listener.is_some())
+ && (self.default_input_listener.is_some()
+ || self.input_alive_listener.is_some()))
+ );
+
+ // We have either default_output_listener or output_alive_listener.
+ // We cannot have both of them at the same time.
+ assert!(
+ !self.has_output()
+ || ((self.default_output_listener.is_some()
+ != self.output_alive_listener.is_some())
+ && (self.default_output_listener.is_some()
+ || self.output_alive_listener.is_some()))
+ );
+
+ Ok(())
+ }
+
+ fn close(&mut self) {
+ self.debug_assert_is_on_stream_queue();
+ if !self.input_unit.is_null() {
+ audio_unit_uninitialize(self.input_unit);
+ if self.using_voice_processing_unit() {
+ // Handle the VoiceProcessIO case where there is a single unit.
+ self.output_unit = ptr::null_mut();
+ }
+
+ // Cannot unset self.input_unit yet, since the output callback might be live
+ // and reading it.
+ }
+
+ if !self.output_unit.is_null() {
+ audio_unit_uninitialize(self.output_unit);
+ dispose_audio_unit(self.output_unit);
+ self.output_unit = ptr::null_mut();
+ }
+
+ if !self.input_unit.is_null() {
+ dispose_audio_unit(self.input_unit);
+ self.input_unit = ptr::null_mut();
+ }
+
+ self.resampler.destroy();
+ self.mixer = None;
+ self.aggregate_device = None;
+
+ if self.uninstall_system_changed_callback().is_err() {
+ cubeb_log!(
+ "({:p}) Could not uninstall the system changed callback",
+ self.stm_ptr
+ );
+ }
+
+ if self.uninstall_device_changed_callback().is_err() {
+ cubeb_log!(
+ "({:p}) Could not uninstall all device change listeners",
+ self.stm_ptr
+ );
+ }
+ }
+
+ fn install_device_changed_callback(&mut self) -> Result<()> {
+ self.debug_assert_is_on_stream_queue();
+ assert!(!self.stm_ptr.is_null());
+ let stm = unsafe { &(*self.stm_ptr) };
+
+ if !self.output_unit.is_null() {
+ assert_ne!(self.output_device.id, kAudioObjectUnknown);
+ assert_ne!(self.output_device.id, kAudioObjectSystemObject);
+ assert!(
+ self.output_source_listener.is_none(),
+ "register output_source_listener without unregistering the one in use"
+ );
+ assert!(
+ self.output_alive_listener.is_none(),
+ "register output_alive_listener without unregistering the one in use"
+ );
+
+ // Get the notification when the data source on the same device changes,
+ // e.g., when the user plugs in a TRRS headset into the headphone jack.
+ self.output_source_listener = Some(device_property_listener::new(
+ self.output_device.id,
+ get_property_address(Property::DeviceSource, DeviceType::OUTPUT),
+ audiounit_property_listener_callback,
+ ));
+ let rv = stm.add_device_listener(self.output_source_listener.as_ref().unwrap());
+ if rv != NO_ERR {
+ self.output_source_listener = None;
+ cubeb_log!("AudioObjectAddPropertyListener/output/kAudioDevicePropertyDataSource rv={}, device id={}", rv, self.output_device.id);
+ return Err(Error::error());
+ }
+
+ // Get the notification when the output device is going away
+ // if the output doesn't follow the system default.
+ if !self
+ .output_device
+ .flags
+ .contains(device_flags::DEV_SELECTED_DEFAULT)
+ {
+ self.output_alive_listener = Some(device_property_listener::new(
+ self.output_device.id,
+ get_property_address(
+ Property::DeviceIsAlive,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ ),
+ audiounit_property_listener_callback,
+ ));
+ let rv = stm.add_device_listener(self.output_alive_listener.as_ref().unwrap());
+ if rv != NO_ERR {
+ self.output_alive_listener = None;
+ cubeb_log!("AudioObjectAddPropertyListener/output/kAudioDevicePropertyDeviceIsAlive rv={}, device id ={}", rv, self.output_device.id);
+ return Err(Error::error());
+ }
+ }
+ }
+
+ if !self.input_unit.is_null() {
+ assert_ne!(self.input_device.id, kAudioObjectUnknown);
+ assert_ne!(self.input_device.id, kAudioObjectSystemObject);
+ assert!(
+ self.input_source_listener.is_none(),
+ "register input_source_listener without unregistering the one in use"
+ );
+ assert!(
+ self.input_alive_listener.is_none(),
+ "register input_alive_listener without unregistering the one in use"
+ );
+
+ // Get the notification when the data source on the same device changes,
+ // e.g., when the user plugs in a TRRS mic into the headphone jack.
+ self.input_source_listener = Some(device_property_listener::new(
+ self.input_device.id,
+ get_property_address(Property::DeviceSource, DeviceType::INPUT),
+ audiounit_property_listener_callback,
+ ));
+ let rv = stm.add_device_listener(self.input_source_listener.as_ref().unwrap());
+ if rv != NO_ERR {
+ self.input_source_listener = None;
+ cubeb_log!("AudioObjectAddPropertyListener/input/kAudioDevicePropertyDataSource rv={}, device id={}", rv, self.input_device.id);
+ return Err(Error::error());
+ }
+
+ // Get the notification when the input device is going away
+ // if the input doesn't follow the system default.
+ if !self
+ .input_device
+ .flags
+ .contains(device_flags::DEV_SELECTED_DEFAULT)
+ {
+ self.input_alive_listener = Some(device_property_listener::new(
+ self.input_device.id,
+ get_property_address(
+ Property::DeviceIsAlive,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ ),
+ audiounit_property_listener_callback,
+ ));
+ let rv = stm.add_device_listener(self.input_alive_listener.as_ref().unwrap());
+ if rv != NO_ERR {
+ self.input_alive_listener = None;
+ cubeb_log!("AudioObjectAddPropertyListener/input/kAudioDevicePropertyDeviceIsAlive rv={}, device id ={}", rv, self.input_device.id);
+ return Err(Error::error());
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ fn install_system_changed_callback(&mut self) -> Result<()> {
+ self.debug_assert_is_on_stream_queue();
+ assert!(!self.stm_ptr.is_null());
+ let stm = unsafe { &(*self.stm_ptr) };
+
+ if !self.output_unit.is_null()
+ && self
+ .output_device
+ .flags
+ .contains(device_flags::DEV_SELECTED_DEFAULT)
+ {
+ assert!(
+ self.default_output_listener.is_none(),
+ "register default_output_listener without unregistering the one in use"
+ );
+
+ // Get the notification when the default output audio changes, e.g.,
+ // when the user plugs in a USB headset and the system chooses it automatically as the default,
+ // or when another device is chosen in the dropdown list.
+ self.default_output_listener = Some(device_property_listener::new(
+ kAudioObjectSystemObject,
+ get_property_address(
+ Property::HardwareDefaultOutputDevice,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ ),
+ audiounit_property_listener_callback,
+ ));
+ let r = stm.add_device_listener(self.default_output_listener.as_ref().unwrap());
+ if r != NO_ERR {
+ self.default_output_listener = None;
+ cubeb_log!("AudioObjectAddPropertyListener/output/kAudioHardwarePropertyDefaultOutputDevice rv={}", r);
+ return Err(Error::error());
+ }
+ }
+
+ if !self.input_unit.is_null()
+ && self
+ .input_device
+ .flags
+ .contains(device_flags::DEV_SELECTED_DEFAULT)
+ {
+ assert!(
+ self.default_input_listener.is_none(),
+ "register default_input_listener without unregistering the one in use"
+ );
+
+ // Get the notification when the default intput audio changes, e.g.,
+ // when the user plugs in a USB mic and the system chooses it automatically as the default,
+ // or when another device is chosen in the system preference.
+ self.default_input_listener = Some(device_property_listener::new(
+ kAudioObjectSystemObject,
+ get_property_address(
+ Property::HardwareDefaultInputDevice,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ ),
+ audiounit_property_listener_callback,
+ ));
+ let r = stm.add_device_listener(self.default_input_listener.as_ref().unwrap());
+ if r != NO_ERR {
+ self.default_input_listener = None;
+ cubeb_log!("AudioObjectAddPropertyListener/input/kAudioHardwarePropertyDefaultInputDevice rv={}", r);
+ return Err(Error::error());
+ }
+ }
+
+ Ok(())
+ }
+
+ fn uninstall_device_changed_callback(&mut self) -> Result<()> {
+ self.debug_assert_is_on_stream_queue();
+ if self.stm_ptr.is_null() {
+ assert!(
+ self.output_source_listener.is_none()
+ && self.output_alive_listener.is_none()
+ && self.input_source_listener.is_none()
+ && self.input_alive_listener.is_none()
+ );
+ return Ok(());
+ }
+
+ let stm = unsafe { &(*self.stm_ptr) };
+
+ // Failing to uninstall listeners is not a fatal error.
+ let mut r = Ok(());
+
+ if self.output_source_listener.is_some() {
+ let rv = stm.remove_device_listener(self.output_source_listener.as_ref().unwrap());
+ if rv != NO_ERR {
+ cubeb_log!("AudioObjectRemovePropertyListener/output/kAudioDevicePropertyDataSource rv={}, device id={}", rv, self.output_device.id);
+ r = Err(Error::error());
+ }
+ self.output_source_listener = None;
+ }
+
+ if self.output_alive_listener.is_some() {
+ let rv = stm.remove_device_listener(self.output_alive_listener.as_ref().unwrap());
+ if rv != NO_ERR {
+ cubeb_log!("AudioObjectRemovePropertyListener/output/kAudioDevicePropertyDeviceIsAlive rv={}, device id={}", rv, self.output_device.id);
+ r = Err(Error::error());
+ }
+ self.output_alive_listener = None;
+ }
+
+ if self.input_source_listener.is_some() {
+ let rv = stm.remove_device_listener(self.input_source_listener.as_ref().unwrap());
+ if rv != NO_ERR {
+ cubeb_log!("AudioObjectRemovePropertyListener/input/kAudioDevicePropertyDataSource rv={}, device id={}", rv, self.input_device.id);
+ r = Err(Error::error());
+ }
+ self.input_source_listener = None;
+ }
+
+ if self.input_alive_listener.is_some() {
+ let rv = stm.remove_device_listener(self.input_alive_listener.as_ref().unwrap());
+ if rv != NO_ERR {
+ cubeb_log!("AudioObjectRemovePropertyListener/input/kAudioDevicePropertyDeviceIsAlive rv={}, device id={}", rv, self.input_device.id);
+ r = Err(Error::error());
+ }
+ self.input_alive_listener = None;
+ }
+
+ r
+ }
+
+ fn uninstall_system_changed_callback(&mut self) -> Result<()> {
+ self.debug_assert_is_on_stream_queue();
+ if self.stm_ptr.is_null() {
+ assert!(
+ self.default_output_listener.is_none() && self.default_input_listener.is_none()
+ );
+ return Ok(());
+ }
+
+ let stm = unsafe { &(*self.stm_ptr) };
+
+ if self.default_output_listener.is_some() {
+ let r = stm.remove_device_listener(self.default_output_listener.as_ref().unwrap());
+ if r != NO_ERR {
+ return Err(Error::error());
+ }
+ self.default_output_listener = None;
+ }
+
+ if self.default_input_listener.is_some() {
+ let r = stm.remove_device_listener(self.default_input_listener.as_ref().unwrap());
+ if r != NO_ERR {
+ return Err(Error::error());
+ }
+ self.default_input_listener = None;
+ }
+
+ Ok(())
+ }
+
+ fn get_output_channel_layout(&self) -> Result<Vec<mixer::Channel>> {
+ self.debug_assert_is_on_stream_queue();
+ assert!(!self.output_unit.is_null());
+ if self.using_voice_processing_unit() {
+ return Ok(get_channel_order(ChannelLayout::MONO));
+ }
+ get_channel_layout(self.output_unit)
+ }
+}
+
+impl<'ctx> Drop for CoreStreamData<'ctx> {
+ fn drop(&mut self) {
+ self.debug_assert_is_on_stream_queue();
+ self.stop_audiounits();
+ self.close();
+ }
+}
+
+#[derive(Debug, Clone)]
+struct OutputCallbackTimingData {
+ frames_queued: u64,
+ timestamp: u64,
+ buffer_size: u64,
+}
+
+// The fisrt two members of the Cubeb stream must be a pointer to its Cubeb context and a void user
+// defined pointer. The Cubeb interface use this assumption to operate the Cubeb APIs.
+// #[repr(C)] is used to prevent any padding from being added in the beginning of the AudioUnitStream.
+#[repr(C)]
+#[derive(Debug)]
+// Allow exposing this private struct in public interfaces when running tests.
+#[cfg_attr(test, allow(private_in_public))]
+struct AudioUnitStream<'ctx> {
+ context: &'ctx mut AudioUnitContext,
+ user_ptr: *mut c_void,
+ // Task queue for the stream.
+ queue: Queue,
+
+ data_callback: ffi::cubeb_data_callback,
+ state_callback: ffi::cubeb_state_callback,
+ device_changed_callback: Mutex<ffi::cubeb_device_changed_callback>,
+ // Frame counters
+ frames_queued: u64,
+ // How many frames got read from the input since the stream started (includes
+ // padded silence)
+ frames_read: AtomicUsize,
+ // How many frames got written to the output device since the stream started
+ frames_written: AtomicUsize,
+ stopped: AtomicBool,
+ draining: AtomicBool,
+ reinit_pending: AtomicBool,
+ destroy_pending: AtomicBool,
+ // Latency requested by the user.
+ latency_frames: u32,
+ // Fixed latency, characteristic of the device.
+ output_device_latency_frames: AtomicU32,
+ input_device_latency_frames: AtomicU32,
+ // Total latency: the latency of the device + the OS latency
+ total_output_latency_frames: AtomicU32,
+ total_input_latency_frames: AtomicU32,
+ output_callback_timing_data_read: triple_buffer::Output<OutputCallbackTimingData>,
+ output_callback_timing_data_write: triple_buffer::Input<OutputCallbackTimingData>,
+ prev_position: u64,
+ // This is true if a device change callback is currently running.
+ switching_device: AtomicBool,
+ core_stream_data: CoreStreamData<'ctx>,
+}
+
+impl<'ctx> AudioUnitStream<'ctx> {
+ fn new(
+ context: &'ctx mut AudioUnitContext,
+ user_ptr: *mut c_void,
+ data_callback: ffi::cubeb_data_callback,
+ state_callback: ffi::cubeb_state_callback,
+ latency_frames: u32,
+ ) -> Self {
+ let output_callback_timing_data =
+ triple_buffer::TripleBuffer::new(OutputCallbackTimingData {
+ frames_queued: 0,
+ timestamp: 0,
+ buffer_size: 0,
+ });
+ let (output_callback_timing_data_write, output_callback_timing_data_read) =
+ output_callback_timing_data.split();
+ AudioUnitStream {
+ context,
+ user_ptr,
+ queue: Queue::new(DISPATCH_QUEUE_LABEL),
+ data_callback,
+ state_callback,
+ device_changed_callback: Mutex::new(None),
+ frames_queued: 0,
+ frames_read: AtomicUsize::new(0),
+ frames_written: AtomicUsize::new(0),
+ stopped: AtomicBool::new(true),
+ draining: AtomicBool::new(false),
+ reinit_pending: AtomicBool::new(false),
+ destroy_pending: AtomicBool::new(false),
+ latency_frames,
+ output_device_latency_frames: AtomicU32::new(0),
+ input_device_latency_frames: AtomicU32::new(0),
+ total_output_latency_frames: AtomicU32::new(0),
+ total_input_latency_frames: AtomicU32::new(0),
+ output_callback_timing_data_write,
+ output_callback_timing_data_read,
+ prev_position: 0,
+ switching_device: AtomicBool::new(false),
+ core_stream_data: CoreStreamData::default(),
+ }
+ }
+
+ fn add_device_listener(&self, listener: &device_property_listener) -> OSStatus {
+ self.queue.debug_assert_is_current();
+ audio_object_add_property_listener(
+ listener.device,
+ &listener.property,
+ listener.listener,
+ self as *const Self as *mut c_void,
+ )
+ }
+
+ fn remove_device_listener(&self, listener: &device_property_listener) -> OSStatus {
+ self.queue.debug_assert_is_current();
+ audio_object_remove_property_listener(
+ listener.device,
+ &listener.property,
+ listener.listener,
+ self as *const Self as *mut c_void,
+ )
+ }
+
+ fn notify_state_changed(&self, state: State) {
+ if self.state_callback.is_none() {
+ return;
+ }
+ let callback = self.state_callback.unwrap();
+ unsafe {
+ callback(
+ self as *const AudioUnitStream as *mut ffi::cubeb_stream,
+ self.user_ptr,
+ state.into(),
+ );
+ }
+ }
+
+ fn reinit(&mut self) -> Result<()> {
+ self.queue.debug_assert_is_current();
+ // Call stop_audiounits to avoid potential data race. If there is a running data callback,
+ // which locks a mutex inside CoreAudio framework, then this call will block the current
+ // thread until the callback is finished since this call asks to lock a mutex inside
+ // CoreAudio framework that is used by the data callback.
+ if !self.stopped.load(Ordering::SeqCst) {
+ self.core_stream_data.stop_audiounits();
+ }
+
+ debug_assert!(
+ !self.core_stream_data.input_unit.is_null()
+ || !self.core_stream_data.output_unit.is_null()
+ );
+ let vol_rv = if self.core_stream_data.output_unit.is_null() {
+ Err(Error::error())
+ } else {
+ get_volume(self.core_stream_data.output_unit)
+ };
+
+ self.core_stream_data.close();
+
+ // Use the new default device if this stream was set to follow the output device.
+ if self.core_stream_data.has_output()
+ && self
+ .core_stream_data
+ .output_device
+ .flags
+ .contains(device_flags::DEV_SELECTED_DEFAULT)
+ {
+ self.core_stream_data.output_device =
+ match create_device_info(kAudioObjectUnknown, DeviceType::OUTPUT) {
+ None => {
+ cubeb_log!("Fail to create device info for output");
+ return Err(Error::error());
+ }
+ Some(d) => d,
+ };
+ }
+
+ // Likewise, for the input side
+ if self.core_stream_data.has_input()
+ && self
+ .core_stream_data
+ .input_device
+ .flags
+ .contains(device_flags::DEV_SELECTED_DEFAULT)
+ {
+ self.core_stream_data.input_device =
+ match create_device_info(kAudioObjectUnknown, DeviceType::INPUT) {
+ None => {
+ cubeb_log!("Fail to create device info for input");
+ return Err(Error::error());
+ }
+ Some(d) => d,
+ }
+ }
+
+ self.core_stream_data.setup().map_err(|e| {
+ cubeb_log!("({:p}) Setup failed.", self.core_stream_data.stm_ptr);
+ e
+ })?;
+
+ if let Ok(volume) = vol_rv {
+ set_volume(self.core_stream_data.output_unit, volume);
+ }
+
+ // If the stream was running, start it again.
+ if !self.stopped.load(Ordering::SeqCst) {
+ self.core_stream_data.start_audiounits().map_err(|e| {
+ cubeb_log!(
+ "({:p}) Start audiounit failed.",
+ self.core_stream_data.stm_ptr
+ );
+ e
+ })?;
+ }
+
+ Ok(())
+ }
+
+ fn reinit_async(&mut self) {
+ if self.reinit_pending.swap(true, Ordering::SeqCst) {
+ // A reinit task is already pending, nothing more to do.
+ cubeb_log!(
+ "({:p}) re-init stream task already pending, cancelling request",
+ self as *const AudioUnitStream
+ );
+ return;
+ }
+
+ let queue = self.queue.clone();
+ // Use a new thread, through the queue, to avoid deadlock when calling
+ // Get/SetProperties method from inside notify callback
+ queue.run_async(move || {
+ let stm_ptr = self as *const AudioUnitStream;
+ if self.destroy_pending.load(Ordering::SeqCst) {
+ cubeb_log!(
+ "({:p}) stream pending destroy, cancelling reinit task",
+ stm_ptr
+ );
+ return;
+ }
+
+ if self.reinit().is_err() {
+ self.core_stream_data.close();
+ self.notify_state_changed(State::Error);
+ cubeb_log!(
+ "({:p}) Could not reopen the stream after switching.",
+ stm_ptr
+ );
+ }
+ self.switching_device.store(false, Ordering::SeqCst);
+ self.reinit_pending.store(false, Ordering::SeqCst);
+ });
+ }
+
+ fn close_on_error(&mut self) {
+ self.queue.debug_assert_is_current();
+ let stm_ptr = self as *const AudioUnitStream;
+
+ self.core_stream_data.close();
+ self.notify_state_changed(State::Error);
+ cubeb_log!("({:p}) Close the stream due to an error.", stm_ptr);
+
+ self.switching_device.store(false, Ordering::SeqCst);
+ }
+
+ fn destroy_internal(&mut self) {
+ self.queue.debug_assert_is_current();
+ self.core_stream_data.close();
+ assert!(self.context.active_streams() >= 1);
+ self.context.update_latency_by_removing_stream();
+ }
+
+ fn destroy(&mut self) {
+ self.queue.debug_assert_is_current();
+ if self
+ .core_stream_data
+ .uninstall_system_changed_callback()
+ .is_err()
+ {
+ cubeb_log!(
+ "({:p}) Could not uninstall the system changed callback",
+ self as *const AudioUnitStream
+ );
+ }
+
+ if self
+ .core_stream_data
+ .uninstall_device_changed_callback()
+ .is_err()
+ {
+ cubeb_log!(
+ "({:p}) Could not uninstall all device change listeners",
+ self as *const AudioUnitStream
+ );
+ }
+
+ // Execute the stream destroy work.
+ self.destroy_pending.store(true, Ordering::SeqCst);
+
+ // Call stop_audiounits to avoid potential data race. If there is a running data callback,
+ // which locks a mutex inside CoreAudio framework, then this call will block the current
+ // thread until the callback is finished since this call asks to lock a mutex inside
+ // CoreAudio framework that is used by the data callback.
+ if !self.stopped.load(Ordering::SeqCst) {
+ self.core_stream_data.stop_audiounits();
+ self.stopped.store(true, Ordering::SeqCst);
+ }
+
+ self.destroy_internal();
+
+ cubeb_log!(
+ "Cubeb stream ({:p}) destroyed successful.",
+ self as *const AudioUnitStream
+ );
+ }
+}
+
+impl<'ctx> Drop for AudioUnitStream<'ctx> {
+ fn drop(&mut self) {
+ // Execute destroy in serial queue to avoid collision with reinit when un/plug devices
+ self.queue.clone().run_final(move || {
+ self.destroy();
+ self.core_stream_data = CoreStreamData::default();
+ });
+ }
+}
+
+impl<'ctx> StreamOps for AudioUnitStream<'ctx> {
+ fn start(&mut self) -> Result<()> {
+ self.stopped.store(false, Ordering::SeqCst);
+ self.draining.store(false, Ordering::SeqCst);
+
+ // Execute start in serial queue to avoid racing with destroy or reinit.
+ let mut result = Err(Error::error());
+ let started = &mut result;
+ let stream = &self;
+ self.queue.run_sync(move || {
+ *started = stream.core_stream_data.start_audiounits();
+ });
+
+ result?;
+
+ self.notify_state_changed(State::Started);
+
+ cubeb_log!(
+ "Cubeb stream ({:p}) started successfully.",
+ self as *const AudioUnitStream
+ );
+ Ok(())
+ }
+ fn stop(&mut self) -> Result<()> {
+ self.stopped.store(true, Ordering::SeqCst);
+
+ // Execute stop in serial queue to avoid racing with destroy or reinit.
+ let stream = &self;
+ self.queue.run_sync(move || {
+ stream.core_stream_data.stop_audiounits();
+ });
+
+ self.notify_state_changed(State::Stopped);
+
+ cubeb_log!(
+ "Cubeb stream ({:p}) stopped successfully.",
+ self as *const AudioUnitStream
+ );
+ Ok(())
+ }
+ fn position(&mut self) -> Result<u64> {
+ let OutputCallbackTimingData {
+ frames_queued,
+ timestamp,
+ buffer_size,
+ } = self.output_callback_timing_data_read.read().clone();
+ let total_output_latency_frames =
+ u64::from(self.total_output_latency_frames.load(Ordering::SeqCst));
+ // If output latency is available, take it into account. Otherwise, use the number of
+ // frames played.
+ let position = if total_output_latency_frames != 0 {
+ if total_output_latency_frames > frames_queued {
+ 0
+ } else {
+ // Interpolate here to match other cubeb backends. Only return an interpolated time
+ // if we've played enough frames. If the stream is paused, clamp the interpolated
+ // number of frames to the buffer size.
+ const NS2S: u64 = 1_000_000_000;
+ let now = unsafe { mach_absolute_time() };
+ let diff = now - timestamp;
+ let interpolated_frames = cmp::min(
+ host_time_to_ns(diff)
+ * self.core_stream_data.output_stream_params.rate() as u64
+ / NS2S,
+ buffer_size,
+ );
+ (frames_queued - total_output_latency_frames) + interpolated_frames
+ }
+ } else {
+ frames_queued
+ };
+
+ // Ensure mononicity of the clock even when changing output device.
+ if position > self.prev_position {
+ self.prev_position = position;
+ }
+ Ok(self.prev_position)
+ }
+ #[cfg(target_os = "ios")]
+ fn latency(&mut self) -> Result<u32> {
+ Err(not_supported())
+ }
+ #[cfg(not(target_os = "ios"))]
+ fn latency(&mut self) -> Result<u32> {
+ Ok(self.total_output_latency_frames.load(Ordering::SeqCst))
+ }
+ #[cfg(target_os = "ios")]
+ fn input_latency(&mut self) -> Result<u32> {
+ Err(not_supported())
+ }
+ #[cfg(not(target_os = "ios"))]
+ fn input_latency(&mut self) -> Result<u32> {
+ let user_rate = self.core_stream_data.input_stream_params.rate();
+ let hw_rate = self.core_stream_data.input_dev_desc.mSampleRate as u32;
+ let frames = self.total_input_latency_frames.load(Ordering::SeqCst);
+ if frames != 0 {
+ if hw_rate == user_rate {
+ Ok(frames)
+ } else {
+ Ok((frames * user_rate) / hw_rate)
+ }
+ } else {
+ Err(Error::error())
+ }
+ }
+ fn set_volume(&mut self, volume: f32) -> Result<()> {
+ // Execute set_volume in serial queue to avoid racing with destroy or reinit.
+ let mut result = Err(Error::error());
+ let set = &mut result;
+ let stream = &self;
+ self.queue.run_sync(move || {
+ *set = set_volume(stream.core_stream_data.output_unit, volume);
+ });
+
+ result?;
+
+ cubeb_log!(
+ "Cubeb stream ({:p}) set volume to {}.",
+ self as *const AudioUnitStream,
+ volume
+ );
+ Ok(())
+ }
+ fn set_name(&mut self, _: &CStr) -> Result<()> {
+ Err(Error::not_supported())
+ }
+ fn current_device(&mut self) -> Result<&DeviceRef> {
+ Err(Error::not_supported())
+ }
+ fn set_input_mute(&mut self, mute: bool) -> Result<()> {
+ if self.core_stream_data.input_unit.is_null() {
+ return Err(Error::invalid_parameter());
+ }
+
+ if !self.core_stream_data.using_voice_processing_unit() {
+ return Err(Error::error());
+ }
+
+ // Execute set_input_mute in serial queue to avoid racing with destroy or reinit.
+ let mut result = Err(Error::error());
+ let set = &mut result;
+ let stream = &self;
+ self.queue.run_sync(move || {
+ *set = set_input_mute(stream.core_stream_data.input_unit, mute);
+ });
+
+ result?;
+
+ cubeb_log!(
+ "Cubeb stream ({:p}) set input mute to {}.",
+ self as *const AudioUnitStream,
+ mute
+ );
+ self.core_stream_data.input_mute = mute;
+ Ok(())
+ }
+ fn set_input_processing_params(&mut self, params: InputProcessingParams) -> Result<()> {
+ // CUBEB_ERROR_INVALID_PARAMETER if a given param is not supported by
+ // this backend, or if this stream does not have an input device
+ if self.core_stream_data.input_unit.is_null() {
+ return Err(Error::invalid_parameter());
+ }
+
+ if self
+ .context
+ .supported_input_processing_params()
+ .unwrap()
+ .intersection(params)
+ != params
+ {
+ return Err(Error::invalid_parameter());
+ }
+
+ // CUBEB_ERROR if params could not be applied
+ // note: only works with VoiceProcessingIO
+ if !self.core_stream_data.using_voice_processing_unit() {
+ return Err(Error::error());
+ }
+
+ // Execute set_input_processing_params in serial queue to avoid racing with destroy or reinit.
+ let mut result = Err(Error::error());
+ let set = &mut result;
+ let stream = &self;
+ self.queue.run_sync(move || {
+ *set = set_input_processing_params(stream.core_stream_data.input_unit, params);
+ });
+
+ result?;
+
+ cubeb_log!(
+ "Cubeb stream ({:p}) set input processing params to {:?}.",
+ self as *const AudioUnitStream,
+ params
+ );
+ self.core_stream_data.input_processing_params = params;
+ Ok(())
+ }
+ #[cfg(target_os = "ios")]
+ fn device_destroy(&mut self, device: &DeviceRef) -> Result<()> {
+ Err(not_supported())
+ }
+ #[cfg(not(target_os = "ios"))]
+ fn device_destroy(&mut self, device: &DeviceRef) -> Result<()> {
+ if device.as_ptr().is_null() {
+ Err(Error::error())
+ } else {
+ unsafe {
+ let mut dev: Box<ffi::cubeb_device> = Box::from_raw(device.as_ptr() as *mut _);
+ if !dev.output_name.is_null() {
+ let _ = CString::from_raw(dev.output_name as *mut _);
+ dev.output_name = ptr::null_mut();
+ }
+ if !dev.input_name.is_null() {
+ let _ = CString::from_raw(dev.input_name as *mut _);
+ dev.input_name = ptr::null_mut();
+ }
+ drop(dev);
+ }
+ Ok(())
+ }
+ }
+ fn register_device_changed_callback(
+ &mut self,
+ device_changed_callback: ffi::cubeb_device_changed_callback,
+ ) -> Result<()> {
+ let mut callback = self.device_changed_callback.lock().unwrap();
+ // Note: second register without unregister first causes 'nope' error.
+ // Current implementation requires unregister before register a new cb.
+ if device_changed_callback.is_some() && callback.is_some() {
+ Err(Error::invalid_parameter())
+ } else {
+ *callback = device_changed_callback;
+ Ok(())
+ }
+ }
+}
+
+#[allow(clippy::non_send_fields_in_send_ty)]
+unsafe impl<'ctx> Send for AudioUnitStream<'ctx> {}
+unsafe impl<'ctx> Sync for AudioUnitStream<'ctx> {}
+
+#[cfg(test)]
+mod tests;
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/resampler.rs b/third_party/rust/cubeb-coreaudio/src/backend/resampler.rs
new file mode 100644
index 0000000000..b72fc6310b
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/resampler.rs
@@ -0,0 +1,84 @@
+use super::auto_release::*;
+use cubeb_backend::ffi;
+use std::os::raw::{c_long, c_uint, c_void};
+use std::ptr;
+
+#[derive(Debug)]
+pub struct Resampler(AutoRelease<ffi::cubeb_resampler>);
+
+impl Resampler {
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ stream: *mut ffi::cubeb_stream,
+ mut input_params: Option<ffi::cubeb_stream_params>,
+ mut output_params: Option<ffi::cubeb_stream_params>,
+ target_rate: c_uint,
+ data_callback: ffi::cubeb_data_callback,
+ user_ptr: *mut c_void,
+ quality: ffi::cubeb_resampler_quality,
+ reclock: ffi::cubeb_resampler_reclock,
+ ) -> Self {
+ let raw_resampler = unsafe {
+ let in_params = match &mut input_params {
+ Some(p) => p,
+ None => ptr::null_mut(),
+ };
+ let out_params = match &mut output_params {
+ Some(p) => p,
+ None => ptr::null_mut(),
+ };
+ ffi::cubeb_resampler_create(
+ stream,
+ in_params,
+ out_params,
+ target_rate,
+ data_callback,
+ user_ptr,
+ quality,
+ reclock,
+ )
+ };
+ assert!(!raw_resampler.is_null(), "Failed to create resampler");
+ let resampler = AutoRelease::new(raw_resampler, ffi::cubeb_resampler_destroy);
+ Self(resampler)
+ }
+
+ pub fn fill(
+ &mut self,
+ input_buffer: *mut c_void,
+ input_frame_count: *mut c_long,
+ output_buffer: *mut c_void,
+ output_frames_needed: c_long,
+ ) -> c_long {
+ unsafe {
+ ffi::cubeb_resampler_fill(
+ self.0.as_mut(),
+ input_buffer,
+ input_frame_count,
+ output_buffer,
+ output_frames_needed,
+ )
+ }
+ }
+
+ pub fn destroy(&mut self) {
+ if !self.0.as_ptr().is_null() {
+ self.0.reset(ptr::null_mut());
+ }
+ }
+}
+
+impl Drop for Resampler {
+ fn drop(&mut self) {
+ self.destroy();
+ }
+}
+
+impl Default for Resampler {
+ fn default() -> Self {
+ Self(AutoRelease::new(
+ ptr::null_mut(),
+ ffi::cubeb_resampler_destroy,
+ ))
+ }
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs
new file mode 100644
index 0000000000..1d3c341ae8
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs
@@ -0,0 +1,400 @@
+use super::utils::{
+ test_get_all_devices, test_get_all_onwed_devices, test_get_default_device,
+ test_get_drift_compensations, test_get_master_device, DeviceFilter, Scope,
+};
+use super::*;
+
+// AggregateDevice::set_sub_devices
+// ------------------------------------
+#[test]
+#[should_panic]
+fn test_aggregate_set_sub_devices_for_an_unknown_aggregate_device() {
+ // If aggregate device id is kAudioObjectUnknown, we are unable to set device list.
+ let default_input = test_get_default_device(Scope::Input);
+ let default_output = test_get_default_device(Scope::Output);
+ if default_input.is_none() || default_output.is_none() {
+ panic!("No input or output device.");
+ }
+
+ let default_input = default_input.unwrap();
+ let default_output = default_output.unwrap();
+ assert!(
+ AggregateDevice::set_sub_devices(kAudioObjectUnknown, default_input, default_output)
+ .is_err()
+ );
+}
+
+#[test]
+#[should_panic]
+fn test_aggregate_set_sub_devices_for_unknown_devices() {
+ // If aggregate device id is kAudioObjectUnknown, we are unable to set device list.
+ assert!(AggregateDevice::set_sub_devices(
+ kAudioObjectUnknown,
+ kAudioObjectUnknown,
+ kAudioObjectUnknown
+ )
+ .is_err());
+}
+
+// AggregateDevice::get_sub_devices
+// ------------------------------------
+// You can check this by creating an aggregate device in `Audio MIDI Setup`
+// application and print out the sub devices of them!
+#[test]
+fn test_aggregate_get_sub_devices() {
+ let devices = test_get_all_devices(DeviceFilter::ExcludeCubebAggregateAndVPIO);
+ for device in devices {
+ // `AggregateDevice::get_sub_devices(device)` will return a single-element vector
+ // containing `device` itself if it's not an aggregate device. This test assumes devices
+ // is not an empty aggregate device (Test will panic when calling get_sub_devices with
+ // an empty aggregate device).
+ let sub_devices = AggregateDevice::get_sub_devices(device).unwrap();
+ // TODO: If the device is a blank aggregate device, then the assertion fails!
+ assert!(!sub_devices.is_empty());
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_aggregate_get_sub_devices_for_a_unknown_device() {
+ let devices = AggregateDevice::get_sub_devices(kAudioObjectUnknown).unwrap();
+ assert!(devices.is_empty());
+}
+
+// AggregateDevice::set_master_device
+// ------------------------------------
+#[test]
+#[should_panic]
+fn test_aggregate_set_master_device_for_an_unknown_aggregate_device() {
+ assert!(AggregateDevice::set_master_device(kAudioObjectUnknown, kAudioObjectUnknown).is_err());
+}
+
+// AggregateDevice::activate_clock_drift_compensation
+// ------------------------------------
+#[test]
+#[should_panic]
+fn test_aggregate_activate_clock_drift_compensation_for_an_unknown_aggregate_device() {
+ assert!(AggregateDevice::activate_clock_drift_compensation(kAudioObjectUnknown).is_err());
+}
+
+// AggregateDevice::destroy_device
+// ------------------------------------
+#[test]
+#[should_panic]
+fn test_aggregate_destroy_device_for_unknown_plugin_and_aggregate_devices() {
+ assert!(AggregateDevice::destroy_device(kAudioObjectUnknown, kAudioObjectUnknown).is_err())
+}
+
+#[test]
+#[should_panic]
+fn test_aggregate_destroy_aggregate_device_for_a_unknown_aggregate_device() {
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ assert!(AggregateDevice::destroy_device(plugin, kAudioObjectUnknown).is_err());
+}
+
+// Default Ignored Tests
+// ================================================================================================
+// The following tests that calls `AggregateDevice::create_blank_device` are marked `ignore` by
+// default since the device-collection-changed callbacks will be fired upon
+// `AggregateDevice::create_blank_device` is called (it will plug a new device in system!).
+// Some tests rely on the device-collection-changed callbacks in a certain way. The callbacks
+// fired from a unexpected `AggregateDevice::create_blank_device` will break those tests.
+
+// AggregateDevice::create_blank_device_sync
+// ------------------------------------
+#[test]
+#[ignore]
+fn test_aggregate_create_blank_device() {
+ // TODO: Test this when there is no available devices.
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ let device = AggregateDevice::create_blank_device_sync(plugin).unwrap();
+ let devices = test_get_all_devices(DeviceFilter::IncludeAll);
+ let device = devices.into_iter().find(|dev| dev == &device).unwrap();
+ let uid = get_device_global_uid(device).unwrap().into_string();
+ assert!(uid.contains(PRIVATE_AGGREGATE_DEVICE_NAME));
+ assert!(AggregateDevice::destroy_device(plugin, device).is_ok());
+}
+
+// AggregateDevice::get_sub_devices
+// ------------------------------------
+#[test]
+#[ignore]
+#[should_panic]
+fn test_aggregate_get_sub_devices_for_blank_aggregate_devices() {
+ // TODO: Test this when there is no available devices.
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ let device = AggregateDevice::create_blank_device_sync(plugin).unwrap();
+ // There is no sub device in a blank aggregate device!
+ // AggregateDevice::get_sub_devices guarantees returning a non-empty devices vector, so
+ // the following call will panic!
+ let sub_devices = AggregateDevice::get_sub_devices(device).unwrap();
+ assert!(sub_devices.is_empty());
+ assert!(AggregateDevice::destroy_device(plugin, device).is_ok());
+}
+
+// AggregateDevice::set_sub_devices_sync
+// ------------------------------------
+#[test]
+#[ignore]
+fn test_aggregate_set_sub_devices() {
+ let input_device = test_get_default_device(Scope::Input);
+ let output_device = test_get_default_device(Scope::Output);
+ if input_device.is_none() || output_device.is_none() || input_device == output_device {
+ println!("No input or output device to create an aggregate device.");
+ return;
+ }
+
+ let input_device = input_device.unwrap();
+ let output_device = output_device.unwrap();
+
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ let device = AggregateDevice::create_blank_device_sync(plugin).unwrap();
+ assert!(AggregateDevice::set_sub_devices_sync(device, input_device, output_device).is_ok());
+
+ let sub_devices = AggregateDevice::get_sub_devices(device).unwrap();
+ let input_sub_devices = AggregateDevice::get_sub_devices(input_device).unwrap();
+ let output_sub_devices = AggregateDevice::get_sub_devices(output_device).unwrap();
+
+ // TODO: There may be overlapping devices between input_sub_devices and output_sub_devices,
+ // but now AggregateDevice::set_sub_devices will add them directly.
+ assert_eq!(
+ sub_devices.len(),
+ input_sub_devices.len() + output_sub_devices.len()
+ );
+ for dev in &input_sub_devices {
+ assert!(sub_devices.contains(dev));
+ }
+ for dev in &output_sub_devices {
+ assert!(sub_devices.contains(dev));
+ }
+
+ let onwed_devices = test_get_all_onwed_devices(device);
+ let onwed_device_uids = get_device_uids(&onwed_devices);
+ let input_sub_device_uids = get_device_uids(&input_sub_devices);
+ let output_sub_device_uids = get_device_uids(&output_sub_devices);
+ for uid in &input_sub_device_uids {
+ assert!(onwed_device_uids.contains(uid));
+ }
+ for uid in &output_sub_device_uids {
+ assert!(onwed_device_uids.contains(uid));
+ }
+
+ assert!(AggregateDevice::destroy_device(plugin, device).is_ok());
+}
+
+#[test]
+#[ignore]
+#[should_panic]
+fn test_aggregate_set_sub_devices_for_unknown_input_devices() {
+ let output_device = test_get_default_device(Scope::Output);
+ if output_device.is_none() {
+ panic!("Need a output device for the test!");
+ }
+ let output_device = output_device.unwrap();
+
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ let device = AggregateDevice::create_blank_device_sync(plugin).unwrap();
+
+ assert!(AggregateDevice::set_sub_devices(device, kAudioObjectUnknown, output_device).is_err());
+
+ assert!(AggregateDevice::destroy_device(plugin, device).is_ok());
+}
+
+#[test]
+#[ignore]
+#[should_panic]
+fn test_aggregate_set_sub_devices_for_unknown_output_devices() {
+ let input_device = test_get_default_device(Scope::Input);
+ if input_device.is_none() {
+ panic!("Need a input device for the test!");
+ }
+ let input_device = input_device.unwrap();
+
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ let device = AggregateDevice::create_blank_device_sync(plugin).unwrap();
+
+ assert!(AggregateDevice::set_sub_devices(device, input_device, kAudioObjectUnknown).is_err());
+
+ assert!(AggregateDevice::destroy_device(plugin, device).is_ok());
+}
+
+fn get_device_uids(devices: &Vec<AudioObjectID>) -> Vec<String> {
+ devices
+ .iter()
+ .map(|device| get_device_global_uid(*device).unwrap().into_string())
+ .collect()
+}
+
+// AggregateDevice::set_master_device
+// ------------------------------------
+#[test]
+#[ignore]
+fn test_aggregate_set_master_device() {
+ let input_device = test_get_default_device(Scope::Input);
+ let output_device = test_get_default_device(Scope::Output);
+ if input_device.is_none() || output_device.is_none() || input_device == output_device {
+ println!("No input or output device to create an aggregate device.");
+ return;
+ }
+
+ let input_device = input_device.unwrap();
+ let output_device = output_device.unwrap();
+
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ let device = AggregateDevice::create_blank_device_sync(plugin).unwrap();
+ assert!(AggregateDevice::set_sub_devices_sync(device, input_device, output_device).is_ok());
+ assert!(AggregateDevice::set_master_device(device, output_device).is_ok());
+
+ // Check if master is set to the first sub device of the default output device.
+ let first_output_sub_device_uid =
+ get_device_uid(AggregateDevice::get_sub_devices(device).unwrap()[0]);
+ let master_device_uid = test_get_master_device(device);
+ assert_eq!(first_output_sub_device_uid, master_device_uid);
+
+ assert!(AggregateDevice::destroy_device(plugin, device).is_ok());
+}
+
+#[test]
+#[ignore]
+fn test_aggregate_set_master_device_for_a_blank_aggregate_device() {
+ let output_device = test_get_default_device(Scope::Output);
+ if output_device.is_none() {
+ println!("No output device to test.");
+ return;
+ }
+
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ let device = AggregateDevice::create_blank_device_sync(plugin).unwrap();
+ assert!(AggregateDevice::set_master_device(device, output_device.unwrap()).is_ok());
+
+ // TODO: it's really weird the aggregate device actually own nothing
+ // but its master device can be set successfully!
+ // The sub devices of this blank aggregate device (by `AggregateDevice::get_sub_devices`)
+ // and the own devices (by `test_get_all_onwed_devices`) is empty since the size returned
+ // from `audio_object_get_property_data_size` is 0.
+ // The CFStringRef of the master device returned from `test_get_master_device` is actually
+ // non-null.
+
+ assert!(AggregateDevice::destroy_device(plugin, device).is_ok());
+}
+
+fn get_device_uid(id: AudioObjectID) -> String {
+ get_device_global_uid(id).unwrap().into_string()
+}
+
+// AggregateDevice::activate_clock_drift_compensation
+// ------------------------------------
+#[test]
+#[ignore]
+fn test_aggregate_activate_clock_drift_compensation() {
+ let input_device = test_get_default_device(Scope::Input);
+ let output_device = test_get_default_device(Scope::Output);
+ if input_device.is_none() || output_device.is_none() || input_device == output_device {
+ println!("No input or output device to create an aggregate device.");
+ return;
+ }
+
+ let input_device = input_device.unwrap();
+ let output_device = output_device.unwrap();
+
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ let device = AggregateDevice::create_blank_device_sync(plugin).unwrap();
+ assert!(AggregateDevice::set_sub_devices_sync(device, input_device, output_device).is_ok());
+ assert!(AggregateDevice::set_master_device(device, output_device).is_ok());
+ assert!(AggregateDevice::activate_clock_drift_compensation(device).is_ok());
+
+ // Check the compensations.
+ let devices = test_get_all_onwed_devices(device);
+ let compensations = get_drift_compensations(&devices);
+ assert!(!compensations.is_empty());
+ assert_eq!(devices.len(), compensations.len());
+
+ for (i, compensation) in compensations.iter().enumerate() {
+ assert_eq!(*compensation, if i == 0 { 0 } else { DRIFT_COMPENSATION });
+ }
+
+ assert!(AggregateDevice::destroy_device(plugin, device).is_ok());
+}
+
+#[test]
+#[ignore]
+fn test_aggregate_activate_clock_drift_compensation_for_an_aggregate_device_without_master_device()
+{
+ let input_device = test_get_default_device(Scope::Input);
+ let output_device = test_get_default_device(Scope::Output);
+ if input_device.is_none() || output_device.is_none() || input_device == output_device {
+ println!("No input or output device to create an aggregate device.");
+ return;
+ }
+
+ let input_device = input_device.unwrap();
+ let output_device = output_device.unwrap();
+
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ let device = AggregateDevice::create_blank_device_sync(plugin).unwrap();
+ assert!(AggregateDevice::set_sub_devices_sync(device, input_device, output_device).is_ok());
+
+ // TODO: Is the master device the first output sub device by default if we
+ // don't set that ? Is it because we add the output sub device list
+ // before the input's one ? (See implementation of
+ // AggregateDevice::set_sub_devices).
+ let first_output_sub_device_uid =
+ get_device_uid(AggregateDevice::get_sub_devices(output_device).unwrap()[0]);
+ let master_device_uid = test_get_master_device(device);
+ assert_eq!(first_output_sub_device_uid, master_device_uid);
+
+ // Compensate the drift directly without setting master device.
+ assert!(AggregateDevice::activate_clock_drift_compensation(device).is_ok());
+
+ // Check the compensations.
+ let devices = test_get_all_onwed_devices(device);
+ let compensations = get_drift_compensations(&devices);
+ assert!(!compensations.is_empty());
+ assert_eq!(devices.len(), compensations.len());
+
+ for (i, compensation) in compensations.iter().enumerate() {
+ assert_eq!(*compensation, if i == 0 { 0 } else { DRIFT_COMPENSATION });
+ }
+
+ assert!(AggregateDevice::destroy_device(plugin, device).is_ok());
+}
+
+#[test]
+#[should_panic]
+#[ignore]
+fn test_aggregate_activate_clock_drift_compensation_for_a_blank_aggregate_device() {
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ let device = AggregateDevice::create_blank_device_sync(plugin).unwrap();
+
+ let sub_devices = AggregateDevice::get_sub_devices(device).unwrap();
+ assert!(sub_devices.is_empty());
+ let onwed_devices = test_get_all_onwed_devices(device);
+ assert!(onwed_devices.is_empty());
+
+ // Get a panic since no sub devices to be set compensation.
+ assert!(AggregateDevice::activate_clock_drift_compensation(device).is_err());
+
+ assert!(AggregateDevice::destroy_device(plugin, device).is_ok());
+}
+
+fn get_drift_compensations(devices: &Vec<AudioObjectID>) -> Vec<u32> {
+ assert!(!devices.is_empty());
+ let mut compensations = Vec::new();
+ for device in devices {
+ let compensation = test_get_drift_compensations(*device).unwrap();
+ compensations.push(compensation);
+ }
+
+ compensations
+}
+
+// AggregateDevice::destroy_device
+// ------------------------------------
+#[test]
+#[ignore]
+#[should_panic]
+fn test_aggregate_destroy_aggregate_device_for_a_unknown_plugin_device() {
+ let plugin = AggregateDevice::get_system_plugin_id().unwrap();
+ let device = AggregateDevice::create_blank_device_sync(plugin).unwrap();
+ assert!(AggregateDevice::destroy_device(kAudioObjectUnknown, device).is_err());
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs
new file mode 100644
index 0000000000..4cd86c094e
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs
@@ -0,0 +1,1663 @@
+use super::utils::{
+ test_audiounit_get_buffer_frame_size, test_audiounit_scope_is_enabled, test_create_audiounit,
+ test_device_channels_in_scope, test_device_in_scope, test_get_all_devices,
+ test_get_default_audiounit, test_get_default_device, test_get_default_raw_stream,
+ test_get_devices_in_scope, test_get_raw_context, ComponentSubType, DeviceFilter, PropertyScope,
+ Scope,
+};
+use super::*;
+
+// make_sized_audio_channel_layout
+// ------------------------------------
+#[test]
+fn test_make_sized_audio_channel_layout() {
+ for channels in 1..10 {
+ let size = mem::size_of::<AudioChannelLayout>()
+ + (channels - 1) * mem::size_of::<AudioChannelDescription>();
+ let _ = make_sized_audio_channel_layout(size);
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_make_sized_audio_channel_layout_with_wrong_size() {
+ // let _ = make_sized_audio_channel_layout(0);
+ let one_channel_size = mem::size_of::<AudioChannelLayout>();
+ let padding_size = 10;
+ assert_ne!(mem::size_of::<AudioChannelDescription>(), padding_size);
+ let wrong_size = one_channel_size + padding_size;
+ let _ = make_sized_audio_channel_layout(wrong_size);
+}
+
+// active_streams
+// update_latency_by_adding_stream
+// update_latency_by_removing_stream
+// ------------------------------------
+#[test]
+fn test_increase_and_decrease_context_streams() {
+ use std::thread;
+ const STREAMS: u32 = 10;
+
+ let context = AudioUnitContext::new();
+ let context_ptr_value = &context as *const AudioUnitContext as usize;
+
+ let mut join_handles = vec![];
+ for i in 0..STREAMS {
+ join_handles.push(thread::spawn(move || {
+ let context = unsafe { &*(context_ptr_value as *const AudioUnitContext) };
+
+ context.update_latency_by_adding_stream(i)
+ }));
+ }
+ let mut latencies = vec![];
+ for handle in join_handles {
+ latencies.push(handle.join().unwrap());
+ }
+ assert_eq!(context.active_streams(), STREAMS);
+ check_streams(&context, STREAMS);
+
+ check_latency(&context, latencies[0]);
+ for i in 0..latencies.len() - 1 {
+ assert_eq!(latencies[i], latencies[i + 1]);
+ }
+
+ let mut join_handles = vec![];
+ for _ in 0..STREAMS {
+ join_handles.push(thread::spawn(move || {
+ let context = unsafe { &*(context_ptr_value as *const AudioUnitContext) };
+ context.update_latency_by_removing_stream();
+ }));
+ }
+ for handle in join_handles {
+ let _ = handle.join();
+ }
+ check_streams(&context, 0);
+
+ check_latency(&context, None);
+}
+
+fn check_streams(context: &AudioUnitContext, number: u32) {
+ let guard = context.latency_controller.lock().unwrap();
+ assert_eq!(guard.streams, number);
+}
+
+fn check_latency(context: &AudioUnitContext, latency: Option<u32>) {
+ let guard = context.latency_controller.lock().unwrap();
+ assert_eq!(guard.latency, latency);
+}
+
+// make_silent
+// ------------------------------------
+#[test]
+fn test_make_silent() {
+ let mut array = allocate_array::<u32>(10);
+ for data in array.iter_mut() {
+ *data = 0xFFFF;
+ }
+
+ let mut buffer = AudioBuffer::default();
+ buffer.mData = array.as_mut_ptr() as *mut c_void;
+ buffer.mDataByteSize = (array.len() * mem::size_of::<u32>()) as u32;
+ buffer.mNumberChannels = 1;
+
+ audiounit_make_silent(&mut buffer);
+ for data in array {
+ assert_eq!(data, 0);
+ }
+}
+
+// minimum_resampling_input_frames
+// ------------------------------------
+#[test]
+fn test_minimum_resampling_input_frames() {
+ let input_rate = 48000_f64;
+ let output_rate = 44100_f64;
+
+ let frames = 100;
+ let times = input_rate / output_rate;
+ let expected = (frames as f64 * times).ceil() as usize;
+
+ assert_eq!(
+ minimum_resampling_input_frames(input_rate, output_rate, frames),
+ expected
+ );
+}
+
+#[test]
+#[should_panic]
+fn test_minimum_resampling_input_frames_zero_input_rate() {
+ minimum_resampling_input_frames(0_f64, 44100_f64, 1);
+}
+
+#[test]
+#[should_panic]
+fn test_minimum_resampling_input_frames_zero_output_rate() {
+ minimum_resampling_input_frames(48000_f64, 0_f64, 1);
+}
+
+#[test]
+fn test_minimum_resampling_input_frames_equal_input_output_rate() {
+ let frames = 100;
+ assert_eq!(
+ minimum_resampling_input_frames(44100_f64, 44100_f64, frames),
+ frames
+ );
+}
+
+// create_device_info
+// ------------------------------------
+#[test]
+fn test_create_device_info_from_unknown_input_device() {
+ if let Some(default_device_id) = test_get_default_device(Scope::Input) {
+ let default_device = create_device_info(kAudioObjectUnknown, DeviceType::INPUT).unwrap();
+ assert_eq!(default_device.id, default_device_id);
+ assert_eq!(
+ default_device.flags,
+ device_flags::DEV_INPUT | device_flags::DEV_SELECTED_DEFAULT
+ );
+ } else {
+ println!("No input device to perform test.");
+ }
+}
+
+#[test]
+fn test_create_device_info_from_unknown_output_device() {
+ if let Some(default_device_id) = test_get_default_device(Scope::Output) {
+ let default_device = create_device_info(kAudioObjectUnknown, DeviceType::OUTPUT).unwrap();
+ assert_eq!(default_device.id, default_device_id);
+ assert_eq!(
+ default_device.flags,
+ device_flags::DEV_OUTPUT | device_flags::DEV_SELECTED_DEFAULT
+ );
+ } else {
+ println!("No output device to perform test.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_set_device_info_to_system_input_device() {
+ let _device = create_device_info(kAudioObjectSystemObject, DeviceType::INPUT);
+}
+
+#[test]
+#[should_panic]
+fn test_set_device_info_to_system_output_device() {
+ let _device = create_device_info(kAudioObjectSystemObject, DeviceType::OUTPUT);
+}
+
+// FIXME: Is it ok to set input device to a nonexistent device ?
+#[ignore]
+#[test]
+#[should_panic]
+fn test_set_device_info_to_nonexistent_input_device() {
+ let nonexistent_id = std::u32::MAX;
+ let _device = create_device_info(nonexistent_id, DeviceType::INPUT);
+}
+
+// FIXME: Is it ok to set output device to a nonexistent device ?
+#[ignore]
+#[test]
+#[should_panic]
+fn test_set_device_info_to_nonexistent_output_device() {
+ let nonexistent_id = std::u32::MAX;
+ let _device = create_device_info(nonexistent_id, DeviceType::OUTPUT);
+}
+
+// add_listener (for default output device)
+// ------------------------------------
+#[test]
+fn test_add_listener_unknown_device() {
+ extern "C" fn callback(
+ _id: AudioObjectID,
+ _number_of_addresses: u32,
+ _addresses: *const AudioObjectPropertyAddress,
+ _data: *mut c_void,
+ ) -> OSStatus {
+ assert!(false, "Should not be called.");
+ kAudioHardwareUnspecifiedError as OSStatus
+ }
+
+ test_get_default_raw_stream(|stream| {
+ let listener = device_property_listener::new(
+ kAudioObjectUnknown,
+ get_property_address(
+ Property::HardwareDefaultOutputDevice,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ ),
+ callback,
+ );
+ let mut res: OSStatus = 0;
+ stream
+ .queue
+ .run_sync(|| res = stream.add_device_listener(&listener));
+ assert_eq!(res, kAudioHardwareBadObjectError as OSStatus);
+ });
+}
+
+// remove_listener (for default output device)
+// ------------------------------------
+#[test]
+fn test_add_listener_then_remove_system_device() {
+ extern "C" fn callback(
+ _id: AudioObjectID,
+ _number_of_addresses: u32,
+ _addresses: *const AudioObjectPropertyAddress,
+ _data: *mut c_void,
+ ) -> OSStatus {
+ assert!(false, "Should not be called.");
+ kAudioHardwareUnspecifiedError as OSStatus
+ }
+
+ test_get_default_raw_stream(|stream| {
+ let listener = device_property_listener::new(
+ kAudioObjectSystemObject,
+ get_property_address(
+ Property::HardwareDefaultOutputDevice,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ ),
+ callback,
+ );
+ let mut res: OSStatus = 0;
+ stream
+ .queue
+ .run_sync(|| res = stream.add_device_listener(&listener));
+ assert_eq!(res, NO_ERR);
+ stream
+ .queue
+ .run_sync(|| res = stream.remove_device_listener(&listener));
+ assert_eq!(res, NO_ERR);
+ });
+}
+
+#[test]
+fn test_remove_listener_without_adding_any_listener_before_system_device() {
+ extern "C" fn callback(
+ _id: AudioObjectID,
+ _number_of_addresses: u32,
+ _addresses: *const AudioObjectPropertyAddress,
+ _data: *mut c_void,
+ ) -> OSStatus {
+ assert!(false, "Should not be called.");
+ kAudioHardwareUnspecifiedError as OSStatus
+ }
+
+ test_get_default_raw_stream(|stream| {
+ let listener = device_property_listener::new(
+ kAudioObjectSystemObject,
+ get_property_address(
+ Property::HardwareDefaultOutputDevice,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ ),
+ callback,
+ );
+ let mut res: OSStatus = 0;
+ stream
+ .queue
+ .run_sync(|| res = stream.remove_device_listener(&listener));
+ assert_eq!(res, NO_ERR);
+ });
+}
+
+#[test]
+fn test_remove_listener_unknown_device() {
+ extern "C" fn callback(
+ _id: AudioObjectID,
+ _number_of_addresses: u32,
+ _addresses: *const AudioObjectPropertyAddress,
+ _data: *mut c_void,
+ ) -> OSStatus {
+ assert!(false, "Should not be called.");
+ kAudioHardwareUnspecifiedError as OSStatus
+ }
+
+ test_get_default_raw_stream(|stream| {
+ let listener = device_property_listener::new(
+ kAudioObjectUnknown,
+ get_property_address(
+ Property::HardwareDefaultOutputDevice,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ ),
+ callback,
+ );
+ let mut res: OSStatus = 0;
+ stream
+ .queue
+ .run_sync(|| res = stream.remove_device_listener(&listener));
+ assert_eq!(res, kAudioHardwareBadObjectError as OSStatus);
+ });
+}
+
+// get_default_device_id
+// ------------------------------------
+#[test]
+fn test_get_default_device_id() {
+ if test_get_default_device(Scope::Input).is_some() {
+ assert_ne!(
+ get_default_device_id(DeviceType::INPUT).unwrap(),
+ kAudioObjectUnknown,
+ );
+ }
+
+ if test_get_default_device(Scope::Output).is_some() {
+ assert_ne!(
+ get_default_device_id(DeviceType::OUTPUT).unwrap(),
+ kAudioObjectUnknown,
+ );
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_default_device_id_with_unknown_type() {
+ assert!(get_default_device_id(DeviceType::UNKNOWN).is_err());
+}
+
+#[test]
+#[should_panic]
+fn test_get_default_device_id_with_inout_type() {
+ assert!(get_default_device_id(DeviceType::INPUT | DeviceType::OUTPUT).is_err());
+}
+
+// convert_channel_layout
+// ------------------------------------
+#[test]
+fn test_convert_channel_layout() {
+ let pairs = [
+ (vec![kAudioObjectUnknown], vec![mixer::Channel::Silence]),
+ (
+ vec![kAudioChannelLabel_Mono],
+ vec![mixer::Channel::FrontCenter],
+ ),
+ (
+ vec![kAudioChannelLabel_Mono, kAudioChannelLabel_LFEScreen],
+ vec![mixer::Channel::FrontCenter, mixer::Channel::LowFrequency],
+ ),
+ (
+ vec![kAudioChannelLabel_Left, kAudioChannelLabel_Right],
+ vec![mixer::Channel::FrontLeft, mixer::Channel::FrontRight],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_Unknown,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::Silence,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_Unused,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::Silence,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_ForeignLanguage,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::Silence,
+ ],
+ ),
+ // The SMPTE layouts.
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_LFEScreen,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::LowFrequency,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_Center,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::FrontCenter,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_Center,
+ kAudioChannelLabel_LFEScreen,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::FrontCenter,
+ mixer::Channel::LowFrequency,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_CenterSurround,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::BackCenter,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_CenterSurround,
+ kAudioChannelLabel_LFEScreen,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::BackCenter,
+ mixer::Channel::LowFrequency,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_Center,
+ kAudioChannelLabel_CenterSurround,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::FrontCenter,
+ mixer::Channel::BackCenter,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_Center,
+ kAudioChannelLabel_CenterSurround,
+ kAudioChannelLabel_LFEScreen,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::FrontCenter,
+ mixer::Channel::BackCenter,
+ mixer::Channel::LowFrequency,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_LeftSurroundDirect,
+ kAudioChannelLabel_RightSurroundDirect,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::SideLeft,
+ mixer::Channel::SideRight,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_LeftSurroundDirect,
+ kAudioChannelLabel_RightSurroundDirect,
+ kAudioChannelLabel_LFEScreen,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::SideLeft,
+ mixer::Channel::SideRight,
+ mixer::Channel::LowFrequency,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_LeftSurround,
+ kAudioChannelLabel_RightSurround,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::BackLeft,
+ mixer::Channel::BackRight,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_LeftSurround,
+ kAudioChannelLabel_RightSurround,
+ kAudioChannelLabel_LFEScreen,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::BackLeft,
+ mixer::Channel::BackRight,
+ mixer::Channel::LowFrequency,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_Center,
+ kAudioChannelLabel_LeftSurroundDirect,
+ kAudioChannelLabel_RightSurroundDirect,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::FrontCenter,
+ mixer::Channel::SideLeft,
+ mixer::Channel::SideRight,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_Center,
+ kAudioChannelLabel_LeftSurroundDirect,
+ kAudioChannelLabel_RightSurroundDirect,
+ kAudioChannelLabel_LFEScreen,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::FrontCenter,
+ mixer::Channel::SideLeft,
+ mixer::Channel::SideRight,
+ mixer::Channel::LowFrequency,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_LeftSurround,
+ kAudioChannelLabel_RightSurround,
+ kAudioChannelLabel_Center,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::BackLeft,
+ mixer::Channel::BackRight,
+ mixer::Channel::FrontCenter,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_LeftSurround,
+ kAudioChannelLabel_RightSurround,
+ kAudioChannelLabel_Center,
+ kAudioChannelLabel_LFEScreen,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::BackLeft,
+ mixer::Channel::BackRight,
+ mixer::Channel::FrontCenter,
+ mixer::Channel::LowFrequency,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_Center,
+ kAudioChannelLabel_LFEScreen,
+ kAudioChannelLabel_CenterSurround,
+ kAudioChannelLabel_LeftSurroundDirect,
+ kAudioChannelLabel_RightSurroundDirect,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::FrontCenter,
+ mixer::Channel::LowFrequency,
+ mixer::Channel::BackCenter,
+ mixer::Channel::SideLeft,
+ mixer::Channel::SideRight,
+ ],
+ ),
+ (
+ vec![
+ kAudioChannelLabel_Left,
+ kAudioChannelLabel_Right,
+ kAudioChannelLabel_Center,
+ kAudioChannelLabel_LFEScreen,
+ kAudioChannelLabel_LeftSurround,
+ kAudioChannelLabel_RightSurround,
+ kAudioChannelLabel_LeftSurroundDirect,
+ kAudioChannelLabel_RightSurroundDirect,
+ ],
+ vec![
+ mixer::Channel::FrontLeft,
+ mixer::Channel::FrontRight,
+ mixer::Channel::FrontCenter,
+ mixer::Channel::LowFrequency,
+ mixer::Channel::BackLeft,
+ mixer::Channel::BackRight,
+ mixer::Channel::SideLeft,
+ mixer::Channel::SideRight,
+ ],
+ ),
+ ];
+
+ const MAX_CHANNELS: usize = 10;
+ // A Rust mapping structure of the AudioChannelLayout with MAX_CHANNELS channels
+ // https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.13.sdk/System/Library/Frameworks/CoreAudio.framework/Versions/A/Headers/CoreAudioTypes.h#L1332
+ #[repr(C)]
+ struct TestLayout {
+ tag: AudioChannelLayoutTag,
+ map: AudioChannelBitmap,
+ number_channel_descriptions: UInt32,
+ channel_descriptions: [AudioChannelDescription; MAX_CHANNELS],
+ }
+
+ impl Default for TestLayout {
+ fn default() -> Self {
+ Self {
+ tag: AudioChannelLayoutTag::default(),
+ map: AudioChannelBitmap::default(),
+ number_channel_descriptions: UInt32::default(),
+ channel_descriptions: [AudioChannelDescription::default(); MAX_CHANNELS],
+ }
+ }
+ }
+
+ let mut layout = TestLayout::default();
+ layout.tag = kAudioChannelLayoutTag_UseChannelDescriptions;
+
+ for (labels, expected_layout) in pairs.iter() {
+ assert!(labels.len() <= MAX_CHANNELS);
+ layout.number_channel_descriptions = labels.len() as u32;
+ for (idx, label) in labels.iter().enumerate() {
+ layout.channel_descriptions[idx].mChannelLabel = *label;
+ }
+ let layout_ref = unsafe { &(*(&layout as *const TestLayout as *const AudioChannelLayout)) };
+ assert_eq!(
+ &audiounit_convert_channel_layout(layout_ref).unwrap(),
+ expected_layout
+ );
+ }
+}
+
+// get_preferred_channel_layout
+// ------------------------------------
+#[test]
+fn test_get_preferred_channel_layout_output() {
+ match test_get_default_audiounit(Scope::Output) {
+ Some(unit) => assert!(!audiounit_get_preferred_channel_layout(unit.get_inner())
+ .unwrap()
+ .is_empty()),
+ None => println!("No output audiounit for test."),
+ }
+}
+
+// get_current_channel_layout
+// ------------------------------------
+#[test]
+fn test_get_current_channel_layout_output() {
+ match test_get_default_audiounit(Scope::Output) {
+ Some(unit) => assert!(!audiounit_get_current_channel_layout(unit.get_inner())
+ .unwrap()
+ .is_empty()),
+ None => println!("No output audiounit for test."),
+ }
+}
+
+// create_stream_description
+// ------------------------------------
+#[test]
+fn test_create_stream_description() {
+ let mut channels = 0;
+ for (bits, format, flags) in [
+ (
+ 16_u32,
+ ffi::CUBEB_SAMPLE_S16LE,
+ kAudioFormatFlagIsSignedInteger,
+ ),
+ (
+ 16_u32,
+ ffi::CUBEB_SAMPLE_S16BE,
+ kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsBigEndian,
+ ),
+ (32_u32, ffi::CUBEB_SAMPLE_FLOAT32LE, kAudioFormatFlagIsFloat),
+ (
+ 32_u32,
+ ffi::CUBEB_SAMPLE_FLOAT32BE,
+ kAudioFormatFlagIsFloat | kAudioFormatFlagIsBigEndian,
+ ),
+ ]
+ .iter()
+ {
+ let bytes = bits / 8;
+ channels += 1;
+
+ let mut raw = ffi::cubeb_stream_params::default();
+ raw.format = *format;
+ raw.rate = 48_000;
+ raw.channels = channels;
+ raw.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ raw.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+ let params = StreamParams::from(raw);
+ let description = create_stream_description(&params).unwrap();
+ assert_eq!(description.mFormatID, kAudioFormatLinearPCM);
+ assert_eq!(
+ description.mFormatFlags,
+ flags | kLinearPCMFormatFlagIsPacked
+ );
+ assert_eq!(description.mSampleRate as u32, raw.rate);
+ assert_eq!(description.mChannelsPerFrame, raw.channels);
+ assert_eq!(description.mBytesPerFrame, bytes * raw.channels);
+ assert_eq!(description.mFramesPerPacket, 1);
+ assert_eq!(description.mBytesPerPacket, bytes * raw.channels);
+ assert_eq!(description.mReserved, 0);
+ }
+}
+
+// create_blank_audiounit
+// ------------------------------------
+#[test]
+fn test_create_blank_audiounit() {
+ let unit = create_blank_audiounit().unwrap();
+ assert!(!unit.is_null());
+ // Destroy the AudioUnit
+ unsafe {
+ AudioUnitUninitialize(unit);
+ AudioComponentInstanceDispose(unit);
+ }
+}
+
+// enable_audiounit_scope
+// ------------------------------------
+#[test]
+fn test_enable_audiounit_scope() {
+ // It's ok to enable and disable the scopes of input or output
+ // for the unit whose subtype is kAudioUnitSubType_HALOutput
+ // even when there is no available input or output devices.
+ if let Some(unit) = test_create_audiounit(ComponentSubType::HALOutput) {
+ assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, true).is_ok());
+ assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, false).is_ok());
+ assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, true).is_ok());
+ assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, false).is_ok());
+ } else {
+ println!("No audiounit to perform test.");
+ }
+}
+
+#[test]
+fn test_enable_audiounit_scope_for_default_output_unit() {
+ if let Some(unit) = test_create_audiounit(ComponentSubType::DefaultOutput) {
+ assert_eq!(
+ enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, true).unwrap_err(),
+ kAudioUnitErr_InvalidProperty
+ );
+ assert_eq!(
+ enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, false).unwrap_err(),
+ kAudioUnitErr_InvalidProperty
+ );
+ assert_eq!(
+ enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, true).unwrap_err(),
+ kAudioUnitErr_InvalidProperty
+ );
+ assert_eq!(
+ enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, false).unwrap_err(),
+ kAudioUnitErr_InvalidProperty
+ );
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_enable_audiounit_scope_with_null_unit() {
+ let unit: AudioUnit = ptr::null_mut();
+ assert!(enable_audiounit_scope(unit, DeviceType::INPUT, false).is_err());
+}
+
+// create_audiounit
+// ------------------------------------
+#[test]
+fn test_for_create_audiounit() {
+ let flags_list = [device_flags::DEV_INPUT, device_flags::DEV_OUTPUT];
+
+ let default_input = test_get_default_device(Scope::Input);
+ let default_output = test_get_default_device(Scope::Output);
+
+ for flags in flags_list.iter() {
+ let mut device = device_info::default();
+ device.flags |= *flags;
+
+ // Check the output scope is enabled.
+ if device.flags.contains(device_flags::DEV_OUTPUT) && default_output.is_some() {
+ device.id = default_output.unwrap();
+ let unit = create_audiounit(&device).unwrap();
+ assert!(!unit.is_null());
+ assert!(test_audiounit_scope_is_enabled(unit, Scope::Output));
+
+ // Destroy the AudioUnit.
+ unsafe {
+ AudioUnitUninitialize(unit);
+ AudioComponentInstanceDispose(unit);
+ }
+ }
+
+ // Check the input scope is enabled.
+ if device.flags.contains(device_flags::DEV_INPUT) && default_input.is_some() {
+ let device_id = default_input.unwrap();
+ device.id = device_id;
+ let unit = create_audiounit(&device).unwrap();
+ assert!(!unit.is_null());
+ assert!(test_audiounit_scope_is_enabled(unit, Scope::Input));
+ // Destroy the AudioUnit.
+ unsafe {
+ AudioUnitUninitialize(unit);
+ AudioComponentInstanceDispose(unit);
+ }
+ }
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_create_audiounit_with_unknown_scope() {
+ let device = device_info::default();
+ let _unit = create_audiounit(&device);
+}
+
+// set_buffer_size_sync
+// ------------------------------------
+#[test]
+fn test_set_buffer_size_sync() {
+ test_set_buffer_size_by_scope(Scope::Input);
+ test_set_buffer_size_by_scope(Scope::Output);
+ fn test_set_buffer_size_by_scope(scope: Scope) {
+ let unit = test_get_default_audiounit(scope.clone());
+ if unit.is_none() {
+ println!("No audiounit for {:?}.", scope);
+ return;
+ }
+ let unit = unit.unwrap();
+ let prop_scope = match scope {
+ Scope::Input => PropertyScope::Output,
+ Scope::Output => PropertyScope::Input,
+ };
+ let mut buffer_frames = test_audiounit_get_buffer_frame_size(
+ unit.get_inner(),
+ scope.clone(),
+ prop_scope.clone(),
+ )
+ .unwrap();
+ assert_ne!(buffer_frames, 0);
+ buffer_frames *= 2;
+ assert!(
+ set_buffer_size_sync(unit.get_inner(), scope.clone().into(), buffer_frames).is_ok()
+ );
+ let new_buffer_frames =
+ test_audiounit_get_buffer_frame_size(unit.get_inner(), scope.clone(), prop_scope)
+ .unwrap();
+ assert_eq!(buffer_frames, new_buffer_frames);
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_set_buffer_size_sync_for_input_with_null_input_unit() {
+ test_set_buffer_size_sync_by_scope_with_null_unit(Scope::Input);
+}
+
+#[test]
+#[should_panic]
+fn test_set_buffer_size_sync_for_output_with_null_output_unit() {
+ test_set_buffer_size_sync_by_scope_with_null_unit(Scope::Output);
+}
+
+fn test_set_buffer_size_sync_by_scope_with_null_unit(scope: Scope) {
+ let unit: AudioUnit = ptr::null_mut();
+ assert!(set_buffer_size_sync(unit, scope.into(), 2048).is_err());
+}
+
+// get_volume, set_volume
+// ------------------------------------
+#[test]
+fn test_stream_get_volume() {
+ if let Some(unit) = test_get_default_audiounit(Scope::Output) {
+ let expected_volume: f32 = 0.5;
+ set_volume(unit.get_inner(), expected_volume);
+ assert_eq!(expected_volume, get_volume(unit.get_inner()).unwrap());
+ } else {
+ println!("No output audiounit.");
+ }
+}
+
+// convert_uint32_into_string
+// ------------------------------------
+#[test]
+fn test_convert_uint32_into_string() {
+ let empty = convert_uint32_into_string(0);
+ assert_eq!(empty, CString::default());
+
+ let data: u32 = ('R' as u32) << 24 | ('U' as u32) << 16 | ('S' as u32) << 8 | 'T' as u32;
+ let data_string = convert_uint32_into_string(data);
+ assert_eq!(data_string, CString::new("RUST").unwrap());
+}
+
+// get_channel_count
+// ------------------------------------
+#[test]
+fn test_get_channel_count() {
+ test_channel_count(Scope::Input);
+ test_channel_count(Scope::Output);
+
+ fn test_channel_count(scope: Scope) {
+ if let Some(device) = test_get_default_device(scope.clone()) {
+ let channels = get_channel_count(device, DeviceType::from(scope.clone())).unwrap();
+ assert!(channels > 0);
+ assert_eq!(
+ channels,
+ test_device_channels_in_scope(device, scope).unwrap()
+ );
+ } else {
+ println!("No device for {:?}.", scope);
+ }
+ }
+}
+
+#[test]
+fn test_get_channel_count_of_input_for_a_output_only_deivce() {
+ let devices = test_get_devices_in_scope(Scope::Output);
+ for device in devices {
+ // Skip in-out devices.
+ if test_device_in_scope(device, Scope::Input) {
+ continue;
+ }
+ let count = get_channel_count(device, DeviceType::INPUT).unwrap();
+ assert_eq!(count, 0);
+ }
+}
+
+#[test]
+fn test_get_channel_count_of_output_for_a_input_only_deivce() {
+ let devices = test_get_devices_in_scope(Scope::Input);
+ for device in devices {
+ // Skip in-out devices.
+ if test_device_in_scope(device, Scope::Output) {
+ continue;
+ }
+ let count = get_channel_count(device, DeviceType::OUTPUT).unwrap();
+ assert_eq!(count, 0);
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_channel_count_of_unknown_device() {
+ assert!(get_channel_count(kAudioObjectUnknown, DeviceType::OUTPUT).is_err());
+}
+
+#[test]
+fn test_get_channel_count_of_inout_type() {
+ test_channel_count(Scope::Input);
+ test_channel_count(Scope::Output);
+
+ fn test_channel_count(scope: Scope) {
+ if let Some(device) = test_get_default_device(scope.clone()) {
+ assert_eq!(
+ get_channel_count(device, DeviceType::INPUT | DeviceType::OUTPUT),
+ get_channel_count(device, DeviceType::INPUT).map(|c| c + get_channel_count(
+ device,
+ DeviceType::OUTPUT
+ )
+ .unwrap_or(0))
+ );
+ } else {
+ println!("No device for {:?}.", scope);
+ }
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_channel_count_of_unknwon_type() {
+ test_channel_count(Scope::Input);
+ test_channel_count(Scope::Output);
+
+ fn test_channel_count(scope: Scope) {
+ if let Some(device) = test_get_default_device(scope.clone()) {
+ assert!(get_channel_count(device, DeviceType::UNKNOWN).is_err());
+ } else {
+ panic!("Panic by default: No device for {:?}.", scope);
+ }
+ }
+}
+
+// get_range_of_sample_rates
+// ------------------------------------
+#[test]
+fn test_get_range_of_sample_rates() {
+ test_get_range_of_sample_rates_in_scope(Scope::Input);
+ test_get_range_of_sample_rates_in_scope(Scope::Output);
+
+ fn test_get_range_of_sample_rates_in_scope(scope: Scope) {
+ if let Some(device) = test_get_default_device(scope.clone()) {
+ let ranges = test_get_available_samplerate_of_device(device);
+ for range in ranges {
+ // Surprisingly, we can get the input/output sample rates from a non-input/non-output device.
+ check_samplerates(range);
+ }
+ } else {
+ println!("No device for {:?}.", scope);
+ }
+ }
+
+ fn test_get_available_samplerate_of_device(id: AudioObjectID) -> Vec<(f64, f64)> {
+ let scopes = [
+ DeviceType::INPUT,
+ DeviceType::OUTPUT,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ ];
+ let mut ranges = Vec::new();
+ for scope in scopes.iter() {
+ ranges.push(get_range_of_sample_rates(id, *scope).unwrap());
+ }
+ ranges
+ }
+
+ fn check_samplerates((min, max): (f64, f64)) {
+ assert!(min > 0.0);
+ assert!(max > 0.0);
+ assert!(min <= max);
+ }
+}
+
+// get_presentation_latency
+// ------------------------------------
+#[test]
+fn test_get_device_presentation_latency() {
+ test_get_device_presentation_latencies_in_scope(Scope::Input);
+ test_get_device_presentation_latencies_in_scope(Scope::Output);
+
+ fn test_get_device_presentation_latencies_in_scope(scope: Scope) {
+ if let Some(device) = test_get_default_device(scope.clone()) {
+ // TODO: The latencies very from devices to devices. Check nothing here.
+ let latency = get_fixed_latency(device, scope.clone().into());
+ println!(
+ "present latency on the device {} in scope {:?}: {}",
+ device, scope, latency
+ );
+ } else {
+ println!("No device for {:?}.", scope);
+ }
+ }
+}
+
+// get_device_group_id
+// ------------------------------------
+#[test]
+fn test_get_device_group_id() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ match get_device_group_id(device, DeviceType::INPUT) {
+ Ok(id) => println!("input group id: {:?}", id),
+ Err(e) => println!("No input group id. Error: {}", e),
+ }
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ match get_device_group_id(device, DeviceType::OUTPUT) {
+ Ok(id) => println!("output group id: {:?}", id),
+ Err(e) => println!("No output group id. Error: {}", e),
+ }
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+fn test_get_same_group_id_for_builtin_device_pairs() {
+ use std::collections::HashMap;
+
+ // These device sources have custom group id. See `get_custom_group_id`.
+ const IMIC: u32 = 0x696D_6963; // "imic"
+ const ISPK: u32 = 0x6973_706B; // "ispk"
+ const EMIC: u32 = 0x656D_6963; // "emic"
+ const HDPN: u32 = 0x6864_706E; // "hdpn"
+ let pairs = [(IMIC, ISPK), (EMIC, HDPN)];
+
+ let mut input_group_ids = HashMap::<u32, String>::new();
+ let input_devices = test_get_devices_in_scope(Scope::Input);
+ for device in input_devices.iter() {
+ match get_device_source(*device, DeviceType::INPUT) {
+ Ok(source) => match get_device_group_id(*device, DeviceType::INPUT) {
+ Ok(id) => assert!(input_group_ids
+ .insert(source, id.into_string().unwrap())
+ .is_none()),
+ Err(e) => assert!(input_group_ids
+ .insert(source, format!("Error {}", e))
+ .is_none()),
+ },
+ _ => {} // do nothing when failing to get source.
+ }
+ }
+
+ let mut output_group_ids = HashMap::<u32, String>::new();
+ let output_devices = test_get_devices_in_scope(Scope::Output);
+ for device in output_devices.iter() {
+ match get_device_source(*device, DeviceType::OUTPUT) {
+ Ok(source) => match get_device_group_id(*device, DeviceType::OUTPUT) {
+ Ok(id) => assert!(output_group_ids
+ .insert(source, id.into_string().unwrap())
+ .is_none()),
+ Err(e) => assert!(output_group_ids
+ .insert(source, format!("Error {}", e))
+ .is_none()),
+ },
+ _ => {} // do nothing when failing to get source.
+ }
+ }
+
+ for (input, output) in pairs.iter() {
+ let input_group_id = input_group_ids.get(input);
+ let output_group_id = output_group_ids.get(output);
+
+ if input_group_id.is_some() && output_group_id.is_some() {
+ assert_eq!(input_group_id, output_group_id);
+ }
+
+ input_group_ids.remove(input);
+ output_group_ids.remove(output);
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_group_id_by_unknown_device() {
+ assert!(get_device_group_id(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_label
+// ------------------------------------
+#[test]
+fn test_get_device_label() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ let name = get_device_label(device, DeviceType::INPUT).unwrap();
+ println!("input device label: {}", name.into_string());
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ let name = get_device_label(device, DeviceType::OUTPUT).unwrap();
+ println!("output device label: {}", name.into_string());
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_label_by_unknown_device() {
+ assert!(get_device_label(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_global_uid
+// ------------------------------------
+#[test]
+fn test_get_device_global_uid() {
+ // Input device.
+ if let Some(input) = test_get_default_device(Scope::Input) {
+ let uid = get_device_global_uid(input).unwrap();
+ let uid = uid.into_string();
+ assert!(!uid.is_empty());
+ }
+
+ // Output device.
+ if let Some(output) = test_get_default_device(Scope::Output) {
+ let uid = get_device_global_uid(output).unwrap();
+ let uid = uid.into_string();
+ assert!(!uid.is_empty());
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_global_uid_by_unknwon_device() {
+ // Unknown device.
+ assert!(get_device_global_uid(kAudioObjectUnknown).is_err());
+}
+
+// create_cubeb_device_info
+// destroy_cubeb_device_info
+// ------------------------------------
+#[test]
+fn test_create_cubeb_device_info() {
+ use std::collections::VecDeque;
+
+ test_create_device_from_hwdev_in_scope(Scope::Input);
+ test_create_device_from_hwdev_in_scope(Scope::Output);
+
+ fn test_create_device_from_hwdev_in_scope(scope: Scope) {
+ if let Some(device) = test_get_default_device(scope.clone()) {
+ let is_input = test_device_in_scope(device, Scope::Input);
+ let is_output = test_device_in_scope(device, Scope::Output);
+ let mut results = test_create_device_infos_by_device(device);
+ assert_eq!(results.len(), 2);
+ // Input device type:
+ let input_result = results.pop_front().unwrap();
+ if is_input {
+ let mut input_device_info = input_result.unwrap();
+ check_device_info_by_device(&input_device_info, device, Scope::Input);
+ destroy_cubeb_device_info(&mut input_device_info);
+ } else {
+ assert_eq!(input_result.unwrap_err(), Error::error());
+ }
+ // Output device type:
+ let output_result = results.pop_front().unwrap();
+ if is_output {
+ let mut output_device_info = output_result.unwrap();
+ check_device_info_by_device(&output_device_info, device, Scope::Output);
+ destroy_cubeb_device_info(&mut output_device_info);
+ } else {
+ assert_eq!(output_result.unwrap_err(), Error::error());
+ }
+ } else {
+ println!("No device for {:?}.", scope);
+ }
+ }
+
+ fn test_create_device_infos_by_device(
+ id: AudioObjectID,
+ ) -> VecDeque<std::result::Result<ffi::cubeb_device_info, Error>> {
+ let dev_types = [DeviceType::INPUT, DeviceType::OUTPUT];
+ let mut results = VecDeque::new();
+ for dev_type in dev_types.iter() {
+ results.push_back(create_cubeb_device_info(id, *dev_type));
+ }
+ results
+ }
+
+ fn check_device_info_by_device(info: &ffi::cubeb_device_info, id: AudioObjectID, scope: Scope) {
+ assert!(!info.devid.is_null());
+ assert!(mem::size_of_val(&info.devid) >= mem::size_of::<AudioObjectID>());
+ assert_eq!(info.devid as AudioObjectID, id);
+ assert!(!info.device_id.is_null());
+ assert!(!info.friendly_name.is_null());
+ assert!(!info.group_id.is_null());
+
+ // TODO: Hit a kAudioHardwareUnknownPropertyError for AirPods
+ // assert!(!info.vendor_name.is_null());
+
+ // FIXME: The device is defined to input-only or output-only, but some device is in-out!
+ assert_eq!(info.device_type, DeviceType::from(scope.clone()).bits());
+ assert_eq!(info.state, ffi::CUBEB_DEVICE_STATE_ENABLED);
+ // TODO: The preference is set when the device is default input/output device if the device
+ // info is created from input/output scope. Should the preference be set if the
+ // device is a default input/output device if the device info is created from
+ // output/input scope ? The device may be a in-out device!
+ assert_eq!(info.preferred, get_cubeb_device_pref(id, scope));
+
+ assert_eq!(info.format, ffi::CUBEB_DEVICE_FMT_ALL);
+ assert_eq!(info.default_format, ffi::CUBEB_DEVICE_FMT_F32NE);
+ assert!(info.max_channels > 0);
+ assert!(info.min_rate <= info.max_rate);
+ assert!(info.min_rate <= info.default_rate);
+ assert!(info.default_rate <= info.max_rate);
+
+ assert!(info.latency_lo > 0);
+ assert!(info.latency_hi > 0);
+ assert!(info.latency_lo <= info.latency_hi);
+
+ fn get_cubeb_device_pref(id: AudioObjectID, scope: Scope) -> ffi::cubeb_device_pref {
+ let default_device = test_get_default_device(scope);
+ if default_device.is_some() && default_device.unwrap() == id {
+ ffi::CUBEB_DEVICE_PREF_ALL
+ } else {
+ ffi::CUBEB_DEVICE_PREF_NONE
+ }
+ }
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_create_device_info_by_unknown_device() {
+ assert!(create_cubeb_device_info(kAudioObjectUnknown, DeviceType::OUTPUT).is_err());
+}
+
+#[test]
+fn test_create_device_info_with_unknown_type() {
+ test_create_device_info_with_unknown_type_by_scope(Scope::Input);
+ test_create_device_info_with_unknown_type_by_scope(Scope::Output);
+
+ fn test_create_device_info_with_unknown_type_by_scope(scope: Scope) {
+ if let Some(device) = test_get_default_device(scope.clone()) {
+ assert!(create_cubeb_device_info(device, DeviceType::UNKNOWN).is_err());
+ }
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_device_destroy_empty_device() {
+ let mut device = ffi::cubeb_device_info::default();
+
+ assert!(device.device_id.is_null());
+ assert!(device.group_id.is_null());
+ assert!(device.friendly_name.is_null());
+ assert!(device.vendor_name.is_null());
+
+ // `friendly_name` must be set.
+ destroy_cubeb_device_info(&mut device);
+
+ assert!(device.device_id.is_null());
+ assert!(device.group_id.is_null());
+ assert!(device.friendly_name.is_null());
+ assert!(device.vendor_name.is_null());
+}
+
+#[test]
+fn test_create_device_from_hwdev_with_inout_type() {
+ test_create_device_from_hwdev_with_inout_type_by_scope(Scope::Input);
+ test_create_device_from_hwdev_with_inout_type_by_scope(Scope::Output);
+
+ fn test_create_device_from_hwdev_with_inout_type_by_scope(scope: Scope) {
+ if let Some(device) = test_get_default_device(scope.clone()) {
+ // Get a kAudioHardwareUnknownPropertyError in get_channel_count actually.
+ assert!(
+ create_cubeb_device_info(device, DeviceType::INPUT | DeviceType::OUTPUT).is_err()
+ );
+ } else {
+ println!("No device for {:?}.", scope);
+ }
+ }
+}
+
+// get_devices_of_type
+// ------------------------------------
+#[test]
+fn test_get_devices_of_type() {
+ use std::collections::HashSet;
+
+ let all_devices = audiounit_get_devices_of_type(DeviceType::INPUT | DeviceType::OUTPUT);
+ let input_devices = audiounit_get_devices_of_type(DeviceType::INPUT);
+ let output_devices = audiounit_get_devices_of_type(DeviceType::OUTPUT);
+
+ let mut expected_all = test_get_all_devices(DeviceFilter::ExcludeCubebAggregateAndVPIO);
+ expected_all.sort();
+ assert_eq!(all_devices, expected_all);
+ for device in all_devices.iter() {
+ if test_device_in_scope(*device, Scope::Input) {
+ assert!(input_devices.contains(device));
+ }
+ if test_device_in_scope(*device, Scope::Output) {
+ assert!(output_devices.contains(device));
+ }
+ }
+
+ let input: HashSet<AudioObjectID> = input_devices.iter().cloned().collect();
+ let output: HashSet<AudioObjectID> = output_devices.iter().cloned().collect();
+ let union: HashSet<AudioObjectID> = input.union(&output).cloned().collect();
+ let mut union_devices: Vec<AudioObjectID> = union.iter().cloned().collect();
+ union_devices.sort();
+ assert_eq!(all_devices, union_devices);
+}
+
+#[test]
+#[should_panic]
+fn test_get_devices_of_type_unknown() {
+ let no_devs = audiounit_get_devices_of_type(DeviceType::UNKNOWN);
+ assert!(no_devs.is_empty());
+}
+
+// add_devices_changed_listener
+// ------------------------------------
+#[test]
+fn test_add_devices_changed_listener() {
+ use std::collections::HashMap;
+
+ extern "C" fn inout_callback(_: *mut ffi::cubeb, _: *mut c_void) {}
+ extern "C" fn in_callback(_: *mut ffi::cubeb, _: *mut c_void) {}
+ extern "C" fn out_callback(_: *mut ffi::cubeb, _: *mut c_void) {}
+
+ let mut map: HashMap<DeviceType, extern "C" fn(*mut ffi::cubeb, *mut c_void)> = HashMap::new();
+ map.insert(DeviceType::INPUT, in_callback);
+ map.insert(DeviceType::OUTPUT, out_callback);
+ map.insert(DeviceType::INPUT | DeviceType::OUTPUT, inout_callback);
+
+ test_get_raw_context(|context| {
+ for (devtype, callback) in map.iter() {
+ assert!(get_devices_changed_callback(context, Scope::Input).is_none());
+ assert!(get_devices_changed_callback(context, Scope::Output).is_none());
+
+ // Register a callback within a specific scope.
+ assert!(context
+ .add_devices_changed_listener(*devtype, Some(*callback), ptr::null_mut())
+ .is_ok());
+
+ if devtype.contains(DeviceType::INPUT) {
+ let cb = get_devices_changed_callback(context, Scope::Input);
+ assert!(cb.is_some());
+ assert_eq!(cb.unwrap(), *callback);
+ } else {
+ let cb = get_devices_changed_callback(context, Scope::Input);
+ assert!(cb.is_none());
+ }
+
+ if devtype.contains(DeviceType::OUTPUT) {
+ let cb = get_devices_changed_callback(context, Scope::Output);
+ assert!(cb.is_some());
+ assert_eq!(cb.unwrap(), *callback);
+ } else {
+ let cb = get_devices_changed_callback(context, Scope::Output);
+ assert!(cb.is_none());
+ }
+
+ // Unregister the callbacks within all scopes.
+ assert!(context
+ .remove_devices_changed_listener(DeviceType::INPUT | DeviceType::OUTPUT)
+ .is_ok());
+
+ assert!(get_devices_changed_callback(context, Scope::Input).is_none());
+ assert!(get_devices_changed_callback(context, Scope::Output).is_none());
+ }
+ });
+}
+
+#[test]
+#[should_panic]
+fn test_add_devices_changed_listener_in_unknown_scope() {
+ extern "C" fn callback(_: *mut ffi::cubeb, _: *mut c_void) {}
+
+ test_get_raw_context(|context| {
+ let _ = context.add_devices_changed_listener(
+ DeviceType::UNKNOWN,
+ Some(callback),
+ ptr::null_mut(),
+ );
+ });
+}
+
+#[test]
+#[should_panic]
+fn test_add_devices_changed_listener_with_none_callback() {
+ test_get_raw_context(|context| {
+ for devtype in &[DeviceType::INPUT, DeviceType::OUTPUT] {
+ assert!(context
+ .add_devices_changed_listener(*devtype, None, ptr::null_mut())
+ .is_ok());
+ }
+ });
+}
+
+// remove_devices_changed_listener
+// ------------------------------------
+#[test]
+fn test_remove_devices_changed_listener() {
+ use std::collections::HashMap;
+
+ extern "C" fn in_callback(_: *mut ffi::cubeb, _: *mut c_void) {}
+ extern "C" fn out_callback(_: *mut ffi::cubeb, _: *mut c_void) {}
+
+ let mut map: HashMap<DeviceType, extern "C" fn(*mut ffi::cubeb, *mut c_void)> = HashMap::new();
+ map.insert(DeviceType::INPUT, in_callback);
+ map.insert(DeviceType::OUTPUT, out_callback);
+
+ test_get_raw_context(|context| {
+ for (devtype, _callback) in map.iter() {
+ assert!(get_devices_changed_callback(context, Scope::Input).is_none());
+ assert!(get_devices_changed_callback(context, Scope::Output).is_none());
+
+ // Register callbacks within all scopes.
+ for (scope, listener) in map.iter() {
+ assert!(context
+ .add_devices_changed_listener(*scope, Some(*listener), ptr::null_mut())
+ .is_ok());
+ }
+
+ let input_callback = get_devices_changed_callback(context, Scope::Input);
+ assert!(input_callback.is_some());
+ assert_eq!(
+ input_callback.unwrap(),
+ *(map.get(&DeviceType::INPUT).unwrap())
+ );
+ let output_callback = get_devices_changed_callback(context, Scope::Output);
+ assert!(output_callback.is_some());
+ assert_eq!(
+ output_callback.unwrap(),
+ *(map.get(&DeviceType::OUTPUT).unwrap())
+ );
+
+ // Unregister the callbacks within one specific scopes.
+ assert!(context.remove_devices_changed_listener(*devtype).is_ok());
+
+ if devtype.contains(DeviceType::INPUT) {
+ let cb = get_devices_changed_callback(context, Scope::Input);
+ assert!(cb.is_none());
+ } else {
+ let cb = get_devices_changed_callback(context, Scope::Input);
+ assert!(cb.is_some());
+ assert_eq!(cb.unwrap(), *(map.get(&DeviceType::INPUT).unwrap()));
+ }
+
+ if devtype.contains(DeviceType::OUTPUT) {
+ let cb = get_devices_changed_callback(context, Scope::Output);
+ assert!(cb.is_none());
+ } else {
+ let cb = get_devices_changed_callback(context, Scope::Output);
+ assert!(cb.is_some());
+ assert_eq!(cb.unwrap(), *(map.get(&DeviceType::OUTPUT).unwrap()));
+ }
+
+ // Unregister the callbacks within all scopes.
+ assert!(context
+ .remove_devices_changed_listener(DeviceType::INPUT | DeviceType::OUTPUT)
+ .is_ok());
+ }
+ });
+}
+
+#[test]
+fn test_remove_devices_changed_listener_without_adding_listeners() {
+ test_get_raw_context(|context| {
+ for devtype in &[
+ DeviceType::INPUT,
+ DeviceType::OUTPUT,
+ DeviceType::INPUT | DeviceType::OUTPUT,
+ ] {
+ assert!(context.remove_devices_changed_listener(*devtype).is_ok());
+ }
+ });
+}
+
+#[test]
+fn test_remove_devices_changed_listener_within_all_scopes() {
+ use std::collections::HashMap;
+
+ extern "C" fn inout_callback(_: *mut ffi::cubeb, _: *mut c_void) {}
+ extern "C" fn in_callback(_: *mut ffi::cubeb, _: *mut c_void) {}
+ extern "C" fn out_callback(_: *mut ffi::cubeb, _: *mut c_void) {}
+
+ let mut map: HashMap<DeviceType, extern "C" fn(*mut ffi::cubeb, *mut c_void)> = HashMap::new();
+ map.insert(DeviceType::INPUT, in_callback);
+ map.insert(DeviceType::OUTPUT, out_callback);
+ map.insert(DeviceType::INPUT | DeviceType::OUTPUT, inout_callback);
+
+ test_get_raw_context(|context| {
+ for (devtype, callback) in map.iter() {
+ assert!(get_devices_changed_callback(context, Scope::Input).is_none());
+ assert!(get_devices_changed_callback(context, Scope::Output).is_none());
+
+ assert!(context
+ .add_devices_changed_listener(*devtype, Some(*callback), ptr::null_mut())
+ .is_ok());
+
+ if devtype.contains(DeviceType::INPUT) {
+ let cb = get_devices_changed_callback(context, Scope::Input);
+ assert!(cb.is_some());
+ assert_eq!(cb.unwrap(), *callback);
+ }
+
+ if devtype.contains(DeviceType::OUTPUT) {
+ let cb = get_devices_changed_callback(context, Scope::Output);
+ assert!(cb.is_some());
+ assert_eq!(cb.unwrap(), *callback);
+ }
+
+ assert!(context
+ .remove_devices_changed_listener(DeviceType::INPUT | DeviceType::OUTPUT)
+ .is_ok());
+
+ assert!(get_devices_changed_callback(context, Scope::Input).is_none());
+ assert!(get_devices_changed_callback(context, Scope::Output).is_none());
+ }
+ });
+}
+
+fn get_devices_changed_callback(
+ context: &AudioUnitContext,
+ scope: Scope,
+) -> ffi::cubeb_device_collection_changed_callback {
+ let devices_guard = context.devices.lock().unwrap();
+ match scope {
+ Scope::Input => devices_guard.input.changed_callback,
+ Scope::Output => devices_guard.output.changed_callback,
+ }
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs
new file mode 100644
index 0000000000..5342ec0f39
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs
@@ -0,0 +1,36 @@
+// Copyright © 2018 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details.
+use super::utils::test_get_default_raw_stream;
+use super::*;
+
+// Interface
+// ============================================================================
+// Remove these after test_ops_stream_register_device_changed_callback works.
+#[test]
+fn test_stream_register_device_changed_callback() {
+ extern "C" fn callback(_: *mut c_void) {}
+
+ test_get_default_raw_stream(|stream| {
+ assert!(stream
+ .register_device_changed_callback(Some(callback))
+ .is_ok());
+ assert!(stream.register_device_changed_callback(None).is_ok());
+ });
+}
+
+#[test]
+fn test_stream_register_device_changed_callback_twice() {
+ extern "C" fn callback1(_: *mut c_void) {}
+ extern "C" fn callback2(_: *mut c_void) {}
+
+ test_get_default_raw_stream(|stream| {
+ assert!(stream
+ .register_device_changed_callback(Some(callback1))
+ .is_ok());
+ assert!(stream
+ .register_device_changed_callback(Some(callback2))
+ .is_err());
+ });
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs
new file mode 100644
index 0000000000..c27dada7ad
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs
@@ -0,0 +1,885 @@
+// NOTICE:
+// Avoid running TestDeviceSwitcher with TestDevicePlugger or active full-duplex streams
+// sequentially!
+//
+// The TestDeviceSwitcher cannot work with any test that will create an aggregate device that is
+// soon being destroyed. The TestDeviceSwitcher will cache the available devices, upon it's
+// created, as the candidates for the default device. Therefore, those created aggregate devices
+// may be cached in TestDeviceSwitcher. However, those aggregate devices may be destroyed when
+// TestDeviceSwitcher is using them or they are in the cached list of TestDeviceSwitcher.
+//
+// Running those tests by setting `test-threads=1` doesn't really help (e.g.,
+// `cargo test test_register_device_changed_callback -- --ignored --nocapture --test-threads=1`).
+// The aggregate device won't be destroyed immediately when `kAudioPlugInDestroyAggregateDevice`
+// is set. As a result, the following tests requiring changing the devices will be run separately
+// in the run_tests.sh script and marked by `ignore` by default.
+
+use super::utils::{
+ get_devices_info_in_scope, test_create_device_change_listener, test_device_in_scope,
+ test_get_default_device, test_get_devices_in_scope,
+ test_get_stream_with_default_data_callback_by_type, test_ops_stream_operation,
+ test_set_default_device, Scope, StreamType, TestDevicePlugger, TestDeviceSwitcher,
+};
+use super::*;
+use std::sync::{LockResult, MutexGuard, WaitTimeoutResult};
+
+// Switch default devices used by the active streams, to test stream reinitialization
+// ================================================================================================
+#[ignore]
+#[test]
+fn test_switch_device() {
+ test_switch_device_in_scope(Scope::Input);
+ test_switch_device_in_scope(Scope::Output);
+}
+
+fn test_switch_device_in_scope(scope: Scope) {
+ println!(
+ "Switch default device for {:?} while the stream is working.",
+ scope
+ );
+
+ // Do nothing if there is no 2 available devices at least.
+ let devices = test_get_devices_in_scope(scope.clone());
+ if devices.len() < 2 {
+ println!("Need 2 devices for {:?} at least. Skip.", scope);
+ return;
+ }
+
+ let mut device_switcher = TestDeviceSwitcher::new(scope.clone());
+
+ let notifier = Arc::new(Notifier::new(0));
+ let also_notifier = notifier.clone();
+ let listener = test_create_device_change_listener(scope.clone(), move |_addresses| {
+ let mut cnt = notifier.lock().unwrap();
+ *cnt += 1;
+ notifier.notify(cnt);
+ NO_ERR
+ });
+ listener.start();
+
+ let changed_watcher = Watcher::new(&also_notifier);
+ test_get_started_stream_in_scope(scope.clone(), move |_stream| loop {
+ let mut guard = changed_watcher.lock().unwrap();
+ let start_cnt = guard.clone();
+ device_switcher.next();
+ guard = changed_watcher
+ .wait_while(guard, |cnt| *cnt == start_cnt)
+ .unwrap();
+ if *guard >= devices.len() {
+ break;
+ }
+ });
+}
+
+fn test_get_started_stream_in_scope<F>(scope: Scope, operation: F)
+where
+ F: FnOnce(*mut ffi::cubeb_stream),
+{
+ use std::f32::consts::PI;
+ const SAMPLE_FREQUENCY: u32 = 48_000;
+
+ // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
+ // (in the comments).
+ let mut stream_params = ffi::cubeb_stream_params::default();
+ stream_params.format = ffi::CUBEB_SAMPLE_S16NE;
+ stream_params.rate = SAMPLE_FREQUENCY;
+ stream_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+ stream_params.channels = 1;
+ stream_params.layout = ffi::CUBEB_LAYOUT_MONO;
+
+ let (input_params, output_params) = match scope {
+ Scope::Input => (
+ &mut stream_params as *mut ffi::cubeb_stream_params,
+ ptr::null_mut(),
+ ),
+ Scope::Output => (
+ ptr::null_mut(),
+ &mut stream_params as *mut ffi::cubeb_stream_params,
+ ),
+ };
+
+ extern "C" fn state_callback(
+ stream: *mut ffi::cubeb_stream,
+ user_ptr: *mut c_void,
+ state: ffi::cubeb_state,
+ ) {
+ assert!(!stream.is_null());
+ assert!(!user_ptr.is_null());
+ assert_ne!(state, ffi::CUBEB_STATE_ERROR);
+ }
+
+ extern "C" fn input_data_callback(
+ stream: *mut ffi::cubeb_stream,
+ user_ptr: *mut c_void,
+ input_buffer: *const c_void,
+ output_buffer: *mut c_void,
+ nframes: i64,
+ ) -> i64 {
+ assert!(!stream.is_null());
+ assert!(!user_ptr.is_null());
+ assert!(!input_buffer.is_null());
+ assert!(output_buffer.is_null());
+ nframes
+ }
+
+ let mut position: i64 = 0; // TODO: Use Atomic instead.
+
+ fn f32_to_i16_sample(x: f32) -> i16 {
+ (x * f32::from(i16::max_value())) as i16
+ }
+
+ extern "C" fn output_data_callback(
+ stream: *mut ffi::cubeb_stream,
+ user_ptr: *mut c_void,
+ input_buffer: *const c_void,
+ output_buffer: *mut c_void,
+ nframes: i64,
+ ) -> i64 {
+ assert!(!stream.is_null());
+ assert!(!user_ptr.is_null());
+ assert!(input_buffer.is_null());
+ assert!(!output_buffer.is_null());
+
+ let buffer = unsafe {
+ let ptr = output_buffer as *mut i16;
+ let len = nframes as usize;
+ slice::from_raw_parts_mut(ptr, len)
+ };
+
+ let position = unsafe { &mut *(user_ptr as *mut i64) };
+
+ // Generate tone on the fly.
+ for data in buffer.iter_mut() {
+ let t1 = (2.0 * PI * 350.0 * (*position) as f32 / SAMPLE_FREQUENCY as f32).sin();
+ let t2 = (2.0 * PI * 440.0 * (*position) as f32 / SAMPLE_FREQUENCY as f32).sin();
+ *data = f32_to_i16_sample(0.5 * (t1 + t2));
+ *position += 1;
+ }
+
+ nframes
+ }
+
+ test_ops_stream_operation(
+ "stream",
+ ptr::null_mut(), // Use default input device.
+ input_params,
+ ptr::null_mut(), // Use default output device.
+ output_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ match scope {
+ Scope::Input => Some(input_data_callback),
+ Scope::Output => Some(output_data_callback),
+ },
+ Some(state_callback),
+ &mut position as *mut i64 as *mut c_void,
+ |stream| {
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+ operation(stream);
+ assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK);
+ },
+ );
+}
+
+// Plug and unplug devices, to test device collection changed callback
+// ================================================================================================
+#[ignore]
+#[test]
+fn test_plug_and_unplug_device() {
+ test_plug_and_unplug_device_in_scope(Scope::Input);
+ test_plug_and_unplug_device_in_scope(Scope::Output);
+}
+
+fn test_plug_and_unplug_device_in_scope(scope: Scope) {
+ let default_device = test_get_default_device(scope.clone());
+ if default_device.is_none() {
+ println!("No device for {:?} to test", scope);
+ return;
+ }
+
+ println!("Run test for {:?}", scope);
+ println!("NOTICE: The test will hang if the default input or output is an aggregate device.\nWe will fix this later.");
+
+ let default_device = default_device.unwrap();
+ let is_input = test_device_in_scope(default_device, Scope::Input);
+ let is_output = test_device_in_scope(default_device, Scope::Output);
+
+ let mut context = AudioUnitContext::new();
+
+ // Register the devices-changed callbacks.
+ #[derive(Clone, PartialEq)]
+ struct Counts {
+ input: u32,
+ output: u32,
+ }
+ impl Counts {
+ fn new() -> Self {
+ Self {
+ input: 0,
+ output: 0,
+ }
+ }
+ }
+ let counts = Arc::new(Notifier::new(Counts::new()));
+ let counts_notifier_ptr = counts.as_ref() as *const Notifier<Counts>;
+
+ assert!(context
+ .register_device_collection_changed(
+ DeviceType::INPUT,
+ Some(input_changed_callback),
+ counts_notifier_ptr as *mut c_void,
+ )
+ .is_ok());
+
+ assert!(context
+ .register_device_collection_changed(
+ DeviceType::OUTPUT,
+ Some(output_changed_callback),
+ counts_notifier_ptr as *mut c_void,
+ )
+ .is_ok());
+
+ let counts_watcher = Watcher::new(&counts);
+
+ let mut device_plugger = TestDevicePlugger::new(scope).unwrap();
+
+ {
+ // Simulate adding devices and monitor the devices-changed callbacks.
+ let mut counts_guard = counts.lock().unwrap();
+ let counts_start = counts_guard.clone();
+
+ assert!(device_plugger.plug().is_ok());
+
+ counts_guard = counts_watcher
+ .wait_while(counts_guard, |counts| {
+ (is_input && counts.input == counts_start.input)
+ || (is_output && counts.output == counts_start.output)
+ })
+ .unwrap();
+
+ // Check changed count.
+ assert_eq!(counts_guard.input, if is_input { 1 } else { 0 });
+ assert_eq!(counts_guard.output, if is_output { 1 } else { 0 });
+ }
+
+ {
+ // Simulate removing devices and monitor the devices-changed callbacks.
+ let mut counts_guard = counts.lock().unwrap();
+ let counts_start = counts_guard.clone();
+
+ assert!(device_plugger.unplug().is_ok());
+
+ counts_guard = counts_watcher
+ .wait_while(counts_guard, |counts| {
+ (is_input && counts.input == counts_start.input)
+ || (is_output && counts.output == counts_start.output)
+ })
+ .unwrap();
+
+ // Check changed count.
+ assert_eq!(counts_guard.input, if is_input { 2 } else { 0 });
+ assert_eq!(counts_guard.output, if is_output { 2 } else { 0 });
+ }
+
+ extern "C" fn input_changed_callback(context: *mut ffi::cubeb, data: *mut c_void) {
+ println!(
+ "Input device collection @ {:p} is changed. Data @ {:p}",
+ context, data
+ );
+ let notifier = unsafe { &*(data as *const Notifier<Counts>) };
+ {
+ let mut counts = notifier.lock().unwrap();
+ counts.input += 1;
+ notifier.notify(counts);
+ }
+ }
+
+ extern "C" fn output_changed_callback(context: *mut ffi::cubeb, data: *mut c_void) {
+ println!(
+ "output device collection @ {:p} is changed. Data @ {:p}",
+ context, data
+ );
+ let notifier = unsafe { &*(data as *const Notifier<Counts>) };
+ {
+ let mut counts = notifier.lock().unwrap();
+ counts.output += 1;
+ notifier.notify(counts);
+ }
+ }
+
+ context.register_device_collection_changed(DeviceType::OUTPUT, None, ptr::null_mut());
+ context.register_device_collection_changed(DeviceType::INPUT, None, ptr::null_mut());
+}
+
+// Switch default devices used by the active streams, to test device changed callback
+// ================================================================================================
+#[ignore]
+#[test]
+fn test_register_device_changed_callback_to_check_default_device_changed_input() {
+ test_register_device_changed_callback_to_check_default_device_changed(StreamType::INPUT);
+}
+
+#[ignore]
+#[test]
+fn test_register_device_changed_callback_to_check_default_device_changed_output() {
+ test_register_device_changed_callback_to_check_default_device_changed(StreamType::OUTPUT);
+}
+
+#[ignore]
+#[test]
+fn test_register_device_changed_callback_to_check_default_device_changed_duplex() {
+ test_register_device_changed_callback_to_check_default_device_changed(StreamType::DUPLEX);
+}
+
+fn test_register_device_changed_callback_to_check_default_device_changed(stm_type: StreamType) {
+ println!("NOTICE: The test will hang if the default input or output is an aggregate device.\nWe will fix this later.");
+
+ let inputs = if stm_type.contains(StreamType::INPUT) {
+ let devices = test_get_devices_in_scope(Scope::Input).len();
+ if devices >= 2 {
+ Some(devices)
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
+ let outputs = if stm_type.contains(StreamType::OUTPUT) {
+ let devices = test_get_devices_in_scope(Scope::Output).len();
+ if devices >= 2 {
+ Some(devices)
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
+ if inputs.is_none() && outputs.is_none() {
+ println!("No enough devices to run the test!");
+ return;
+ }
+
+ let changed_count = Arc::new(Notifier::new(0u32));
+ let notifier_ptr = changed_count.as_ref() as *const Notifier<u32>;
+
+ test_get_stream_with_device_changed_callback(
+ "stream: test callback for default device changed",
+ stm_type,
+ None, // Use default input device.
+ None, // Use default output device.
+ notifier_ptr as *mut c_void,
+ state_callback,
+ device_changed_callback,
+ |stream| {
+ // If the duplex stream uses different input and output device,
+ // an aggregate device will be created and it will work for this duplex stream.
+ // This aggregate device will be added into the device list, but it won't
+ // be assigned to the default device, since the device list for setting
+ // default device is cached upon {input, output}_device_switcher is initialized.
+
+ let changed_watcher = Watcher::new(&changed_count);
+
+ if let Some(devices) = inputs {
+ let mut device_switcher = TestDeviceSwitcher::new(Scope::Input);
+ for _ in 0..devices {
+ // While the stream is re-initializing for the default device switch,
+ // switching for the default device again will be ignored.
+ while stream.switching_device.load(atomic::Ordering::SeqCst) {
+ std::hint::spin_loop()
+ }
+ let guard = changed_watcher.lock().unwrap();
+ let start_cnt = guard.clone();
+ device_switcher.next();
+ changed_watcher
+ .wait_while(guard, |cnt| *cnt == start_cnt)
+ .unwrap();
+ }
+ }
+
+ if let Some(devices) = outputs {
+ let mut device_switcher = TestDeviceSwitcher::new(Scope::Output);
+ for _ in 0..devices {
+ // While the stream is re-initializing for the default device switch,
+ // switching for the default device again will be ignored.
+ while stream.switching_device.load(atomic::Ordering::SeqCst) {
+ std::hint::spin_loop()
+ }
+ let guard = changed_watcher.lock().unwrap();
+ let start_cnt = guard.clone();
+ device_switcher.next();
+ changed_watcher
+ .wait_while(guard, |cnt| *cnt == start_cnt)
+ .unwrap();
+ }
+ }
+ },
+ );
+
+ extern "C" fn state_callback(
+ stream: *mut ffi::cubeb_stream,
+ _user_ptr: *mut c_void,
+ state: ffi::cubeb_state,
+ ) {
+ assert!(!stream.is_null());
+ assert_ne!(state, ffi::CUBEB_STATE_ERROR);
+ }
+
+ extern "C" fn device_changed_callback(data: *mut c_void) {
+ println!("Device change callback. data @ {:p}", data);
+ let notifier = unsafe { &*(data as *const Notifier<u32>) };
+ let mut count_guard = notifier.lock().unwrap();
+ *count_guard += 1;
+ notifier.notify(count_guard);
+ }
+}
+
+// Unplug the devices used by the active streams, to test
+// 1) device changed callback, or state callback
+// 2) stream reinitialization that may race with stream destroying
+// ================================================================================================
+
+// Input-only stream
+// -----------------
+
+// Unplug the non-default input device for an input stream
+// ------------------------------------------------------------------------------------------------
+
+#[ignore]
+#[test]
+fn test_destroy_input_stream_after_unplugging_a_nondefault_input_device() {
+ // The stream can be destroyed before running device-changed event handler
+ test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, false, 0);
+}
+
+#[ignore]
+#[test]
+fn test_suspend_input_stream_by_unplugging_a_nondefault_input_device() {
+ // Expect to get an error state callback by device-changed event handler
+ test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, false, 2000);
+}
+
+// Unplug the default input device for an input stream
+// ------------------------------------------------------------------------------------------------
+#[ignore]
+#[test]
+fn test_destroy_input_stream_after_unplugging_a_default_input_device() {
+ // Expect to get an device-changed callback by device-changed event handler,
+ // which will reinitialize the stream behind the scenes, at the same when
+ // the stream is being destroyed
+ test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, true, 0);
+}
+
+#[ignore]
+#[test]
+fn test_reinit_input_stream_by_unplugging_a_default_input_device() {
+ // Expect to get an device-changed callback by device-changed event handler,
+ // which will reinitialize the stream behind the scenes
+ test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, true, 2000);
+}
+
+// Output-only stream
+// ------------------
+
+// Unplug the non-default output device for an output stream
+// ------------------------------------------------------------------------------------------------
+#[ignore]
+#[test]
+fn test_destroy_output_stream_after_unplugging_a_nondefault_output_device() {
+ test_unplug_a_device_on_an_active_stream(StreamType::OUTPUT, Scope::Output, false, 0);
+}
+
+#[ignore]
+#[test]
+fn test_suspend_output_stream_by_unplugging_a_nondefault_output_device() {
+ test_unplug_a_device_on_an_active_stream(StreamType::OUTPUT, Scope::Output, false, 2000);
+}
+
+// Unplug the default output device for an output stream
+// ------------------------------------------------------------------------------------------------
+
+#[ignore]
+#[test]
+fn test_destroy_output_stream_after_unplugging_a_default_output_device() {
+ // Expect to get an device-changed callback by device-changed event handler,
+ // which will reinitialize the stream behind the scenes, at the same when
+ // the stream is being destroyed
+ test_unplug_a_device_on_an_active_stream(StreamType::OUTPUT, Scope::Output, true, 0);
+}
+
+#[ignore]
+#[test]
+fn test_reinit_output_stream_by_unplugging_a_default_output_device() {
+ // Expect to get an device-changed callback by device-changed event handler,
+ // which will reinitialize the stream behind the scenes
+ test_unplug_a_device_on_an_active_stream(StreamType::OUTPUT, Scope::Output, true, 2000);
+}
+
+// Duplex stream
+// -------------
+
+// Unplug the non-default input device for a duplex stream
+// ------------------------------------------------------------------------------------------------
+
+#[ignore]
+#[test]
+fn test_destroy_duplex_stream_after_unplugging_a_nondefault_input_device() {
+ // The stream can be destroyed before running device-changed event handler
+ test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, false, 0);
+}
+
+#[ignore]
+#[test]
+fn test_suspend_duplex_stream_by_unplugging_a_nondefault_input_device() {
+ // Expect to get an error state callback by device-changed event handler
+ test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, false, 2000);
+}
+
+// Unplug the non-default output device for a duplex stream
+// ------------------------------------------------------------------------------------------------
+
+#[ignore]
+#[test]
+fn test_destroy_duplex_stream_after_unplugging_a_nondefault_output_device() {
+ test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Output, false, 0);
+}
+
+#[ignore]
+#[test]
+fn test_suspend_duplex_stream_by_unplugging_a_nondefault_output_device() {
+ test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Output, false, 2000);
+}
+
+// Unplug the non-default in-out device for a duplex stream
+// ------------------------------------------------------------------------------------------------
+// TODO: Implement an in-out TestDevicePlugger
+
+// Unplug the default input device for a duplex stream
+// ------------------------------------------------------------------------------------------------
+
+#[ignore]
+#[test]
+fn test_destroy_duplex_stream_after_unplugging_a_default_input_device() {
+ // Expect to get an device-changed callback by device-changed event handler,
+ // which will reinitialize the stream behind the scenes, at the same when
+ // the stream is being destroyed
+ test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, true, 0);
+}
+
+#[ignore]
+#[test]
+fn test_reinit_duplex_stream_by_unplugging_a_default_input_device() {
+ // Expect to get an device-changed callback by device-changed event handler,
+ // which will reinitialize the stream behind the scenes
+ test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, true, 2000);
+}
+
+// Unplug the default ouput device for a duplex stream
+// ------------------------------------------------------------------------------------------------
+
+#[ignore]
+#[test]
+fn test_destroy_duplex_stream_after_unplugging_a_default_output_device() {
+ // Expect to get an device-changed callback by device-changed event handler,
+ // which will reinitialize the stream behind the scenes, at the same when
+ // the stream is being destroyed
+ test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Output, true, 0);
+}
+
+#[ignore]
+#[test]
+fn test_reinit_duplex_stream_by_unplugging_a_default_output_device() {
+ // Expect to get an device-changed callback by device-changed event handler,
+ // which will reinitialize the stream behind the scenes
+ test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Output, true, 2000);
+}
+
+fn test_unplug_a_device_on_an_active_stream(
+ stream_type: StreamType,
+ device_scope: Scope,
+ set_device_to_default: bool,
+ wait_up_to_ms: u64,
+) {
+ let has_input = test_get_default_device(Scope::Input).is_some();
+ let has_output = test_get_default_device(Scope::Output).is_some();
+
+ if stream_type.contains(StreamType::INPUT) && !has_input {
+ println!("No input device for input or duplex stream.");
+ return;
+ }
+
+ if stream_type.contains(StreamType::OUTPUT) && !has_output {
+ println!("No output device for output or duplex stream.");
+ return;
+ }
+
+ let default_device_before_plugging = test_get_default_device(device_scope.clone()).unwrap();
+ println!(
+ "Before plugging, default {:?} device is {}",
+ device_scope, default_device_before_plugging
+ );
+
+ let mut plugger = TestDevicePlugger::new(device_scope.clone()).unwrap();
+ assert!(plugger.plug().is_ok());
+ assert_ne!(plugger.get_device_id(), kAudioObjectUnknown);
+ println!(
+ "Create plugger device: {} for {:?}",
+ plugger.get_device_id(),
+ device_scope
+ );
+
+ let default_device_after_plugging = test_get_default_device(device_scope.clone()).unwrap();
+ println!(
+ "After plugging, default {:?} device is {}",
+ device_scope, default_device_after_plugging
+ );
+
+ // The new device, plugger, is possible to be set to the default device.
+ // Before running the test, we need to set the default device to the correct one.
+ if set_device_to_default {
+ // plugger should be the default device for the test.
+ // If it's not, then set it to the default device.
+ if default_device_after_plugging != plugger.get_device_id() {
+ let prev_def_dev =
+ test_set_default_device(plugger.get_device_id(), device_scope.clone()).unwrap();
+ assert_eq!(prev_def_dev, default_device_after_plugging);
+ }
+ } else {
+ // plugger should NOT be the default device for the test.
+ // If it is, reset the default device to another one.
+ if default_device_after_plugging == plugger.get_device_id() {
+ let prev_def_dev =
+ test_set_default_device(default_device_before_plugging, device_scope.clone())
+ .unwrap();
+ assert_eq!(prev_def_dev, default_device_after_plugging);
+ }
+ }
+
+ // Ignore the return devices' info since we only need to print them.
+ let _ = get_devices_info_in_scope(device_scope.clone());
+ println!(
+ "Current default {:?} device is {}",
+ device_scope,
+ test_get_default_device(device_scope.clone()).unwrap()
+ );
+
+ let (input_device, output_device) = match device_scope {
+ Scope::Input => (
+ if set_device_to_default {
+ None // default input device.
+ } else {
+ Some(plugger.get_device_id())
+ },
+ None,
+ ),
+ Scope::Output => (
+ None,
+ if set_device_to_default {
+ None // default output device.
+ } else {
+ Some(plugger.get_device_id())
+ },
+ ),
+ };
+
+ #[derive(Clone, PartialEq)]
+ struct Data {
+ changed_count: u32,
+ states: Vec<ffi::cubeb_state>,
+ }
+
+ impl Data {
+ fn new() -> Self {
+ Self {
+ changed_count: 0,
+ states: vec![],
+ }
+ }
+ }
+
+ let notifier = Arc::new(Notifier::new(Data::new()));
+ let notifier_ptr = notifier.as_ref() as *const Notifier<Data>;
+
+ test_get_stream_with_device_changed_callback(
+ "stream: test stream reinit/destroy after unplugging a device",
+ stream_type,
+ input_device,
+ output_device,
+ notifier_ptr as *mut c_void,
+ state_callback,
+ device_changed_callback,
+ |stream| {
+ stream.start();
+
+ let changed_watcher = Watcher::new(&notifier);
+ let mut data_guard = notifier.lock().unwrap();
+ assert_eq!(data_guard.states.last().unwrap(), &ffi::CUBEB_STATE_STARTED);
+
+ println!(
+ "Stream runs on the device {} for {:?}",
+ plugger.get_device_id(),
+ device_scope
+ );
+
+ let dev = plugger.get_device_id();
+ let start_changed_count = data_guard.changed_count.clone();
+
+ assert!(plugger.unplug().is_ok());
+
+ if set_device_to_default {
+ // The stream will be reinitialized if it follows the default input or output device.
+ println!("Waiting for default device to change and reinit");
+ data_guard = changed_watcher
+ .wait_while(data_guard, |data| {
+ data.changed_count == start_changed_count
+ || data.states.last().unwrap_or(&ffi::CUBEB_STATE_ERROR)
+ != &ffi::CUBEB_STATE_STARTED
+ })
+ .unwrap();
+ } else if wait_up_to_ms > 0 {
+ // stream can be dropped immediately before device-changed callback
+ // so we only check the states if we wait for it explicitly.
+ println!("Waiting for non-default device to enter error state");
+ let (new_guard, timeout_res) = changed_watcher
+ .wait_timeout_while(data_guard, Duration::from_millis(wait_up_to_ms), |data| {
+ data.states.last().unwrap_or(&ffi::CUBEB_STATE_STARTED)
+ != &ffi::CUBEB_STATE_ERROR
+ })
+ .unwrap();
+ assert!(!timeout_res.timed_out());
+ data_guard = new_guard;
+ }
+
+ println!(
+ "Device {} for {:?} has been unplugged. The default {:?} device now is {}",
+ dev,
+ device_scope,
+ device_scope,
+ test_get_default_device(device_scope.clone()).unwrap()
+ );
+
+ println!("The stream is going to be destroyed soon");
+ },
+ );
+
+ extern "C" fn state_callback(
+ stream: *mut ffi::cubeb_stream,
+ user_ptr: *mut c_void,
+ state: ffi::cubeb_state,
+ ) {
+ println!("Device change callback. user_ptr @ {:p}", user_ptr);
+ assert!(!stream.is_null());
+ println!(
+ "state: {}",
+ match state {
+ ffi::CUBEB_STATE_STARTED => "started",
+ ffi::CUBEB_STATE_STOPPED => "stopped",
+ ffi::CUBEB_STATE_DRAINED => "drained",
+ ffi::CUBEB_STATE_ERROR => "error",
+ _ => "unknown",
+ }
+ );
+ let notifier = unsafe { &mut *(user_ptr as *mut Notifier<Data>) };
+ let mut data_guard = notifier.lock().unwrap();
+ data_guard.states.push(state);
+ notifier.notify(data_guard);
+ }
+
+ extern "C" fn device_changed_callback(user_ptr: *mut c_void) {
+ println!("Device change callback. user_ptr @ {:p}", user_ptr);
+ let notifier = unsafe { &mut *(user_ptr as *mut Notifier<Data>) };
+ let mut data_guard = notifier.lock().unwrap();
+ data_guard.changed_count += 1;
+ notifier.notify(data_guard);
+ }
+}
+
+struct Notifier<T> {
+ value: Mutex<T>,
+ cvar: Condvar,
+}
+
+impl<T> Notifier<T> {
+ fn new(value: T) -> Self {
+ Self {
+ value: Mutex::new(value),
+ cvar: Condvar::new(),
+ }
+ }
+
+ fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
+ self.value.lock()
+ }
+
+ fn notify(&self, _guard: MutexGuard<'_, T>) {
+ self.cvar.notify_all();
+ }
+}
+
+struct Watcher<T: Clone + PartialEq> {
+ notifier: Arc<Notifier<T>>,
+}
+
+impl<T: Clone + PartialEq> Watcher<T> {
+ fn new(value: &Arc<Notifier<T>>) -> Self {
+ Self {
+ notifier: Arc::clone(value),
+ }
+ }
+
+ fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
+ self.notifier.lock()
+ }
+
+ fn wait_while<'a, F>(
+ &self,
+ guard: MutexGuard<'a, T>,
+ condition: F,
+ ) -> LockResult<MutexGuard<'a, T>>
+ where
+ F: FnMut(&mut T) -> bool,
+ {
+ self.notifier.cvar.wait_while(guard, condition)
+ }
+
+ fn wait_timeout_while<'a, F>(
+ &self,
+ guard: MutexGuard<'a, T>,
+ dur: Duration,
+ condition: F,
+ ) -> LockResult<(MutexGuard<'a, T>, WaitTimeoutResult)>
+ where
+ F: FnMut(&mut T) -> bool,
+ {
+ self.notifier.cvar.wait_timeout_while(guard, dur, condition)
+ }
+}
+
+fn test_get_stream_with_device_changed_callback<F>(
+ name: &'static str,
+ stm_type: StreamType,
+ input_device: Option<AudioObjectID>,
+ output_device: Option<AudioObjectID>,
+ data: *mut c_void,
+ state_callback: extern "C" fn(*mut ffi::cubeb_stream, *mut c_void, ffi::cubeb_state),
+ device_changed_callback: extern "C" fn(*mut c_void),
+ operation: F,
+) where
+ F: FnOnce(&mut AudioUnitStream),
+{
+ test_get_stream_with_default_data_callback_by_type(
+ name,
+ stm_type,
+ input_device,
+ output_device,
+ state_callback,
+ data,
+ |stream| {
+ assert!(stream
+ .register_device_changed_callback(Some(device_changed_callback))
+ .is_ok());
+ operation(stream);
+ assert!(stream.register_device_changed_callback(None).is_ok());
+ },
+ );
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs
new file mode 100644
index 0000000000..8277a7642d
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs
@@ -0,0 +1,473 @@
+use super::utils::{test_get_default_device, Scope};
+use super::*;
+
+// get_device_uid
+// ------------------------------------
+#[test]
+fn test_get_device_uid() {
+ // Input device.
+ if let Some(input) = test_get_default_device(Scope::Input) {
+ let uid = get_device_uid(input, DeviceType::INPUT).unwrap();
+ let uid = uid.into_string();
+ assert!(!uid.is_empty());
+ }
+
+ // Output device.
+ if let Some(output) = test_get_default_device(Scope::Output) {
+ let uid = get_device_uid(output, DeviceType::OUTPUT).unwrap();
+ let uid = uid.into_string();
+ assert!(!uid.is_empty());
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_uid_by_unknwon_device() {
+ // Unknown device.
+ assert!(get_device_uid(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_model_uid
+// ------------------------------------
+// Some devices (e.g., AirPods) fail to get model uid.
+#[test]
+fn test_get_device_model_uid() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ match get_device_model_uid(device, DeviceType::INPUT) {
+ Ok(uid) => println!("input model uid: {}", uid.into_string()),
+ Err(e) => println!("No input model uid. Error: {}", e),
+ }
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ match get_device_model_uid(device, DeviceType::OUTPUT) {
+ Ok(uid) => println!("output model uid: {}", uid.into_string()),
+ Err(e) => println!("No output model uid. Error: {}", e),
+ }
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_model_uid_by_unknown_device() {
+ assert!(get_device_model_uid(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_transport_type
+// ------------------------------------
+#[test]
+fn test_get_device_transport_type() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ match get_device_transport_type(device, DeviceType::INPUT) {
+ Ok(trans_type) => println!(
+ "input transport type: {:X}, {:?}",
+ trans_type,
+ convert_uint32_into_string(trans_type)
+ ),
+ Err(e) => println!("No input transport type. Error: {}", e),
+ }
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ match get_device_transport_type(device, DeviceType::OUTPUT) {
+ Ok(trans_type) => println!(
+ "output transport type: {:X}, {:?}",
+ trans_type,
+ convert_uint32_into_string(trans_type)
+ ),
+ Err(e) => println!("No output transport type. Error: {}", e),
+ }
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_transport_type_by_unknown_device() {
+ assert!(get_device_transport_type(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_source
+// ------------------------------------
+// Some USB headsets (e.g., Plantronic .Audio 628) fails to get data source.
+#[test]
+fn test_get_device_source() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ match get_device_source(device, DeviceType::INPUT) {
+ Ok(source) => println!(
+ "input source: {:X}, {:?}",
+ source,
+ convert_uint32_into_string(source)
+ ),
+ Err(e) => println!("No input data source. Error: {}", e),
+ }
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ match get_device_source(device, DeviceType::OUTPUT) {
+ Ok(source) => println!(
+ "output source: {:X}, {:?}",
+ source,
+ convert_uint32_into_string(source)
+ ),
+ Err(e) => println!("No output data source. Error: {}", e),
+ }
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_source_by_unknown_device() {
+ assert!(get_device_source(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_source_name
+// ------------------------------------
+#[test]
+fn test_get_device_source_name() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ match get_device_source_name(device, DeviceType::INPUT) {
+ Ok(name) => println!("input: {}", name.into_string()),
+ Err(e) => println!("No input data source name. Error: {}", e),
+ }
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ match get_device_source_name(device, DeviceType::OUTPUT) {
+ Ok(name) => println!("output: {}", name.into_string()),
+ Err(e) => println!("No output data source name. Error: {}", e),
+ }
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_source_name_by_unknown_device() {
+ assert!(get_device_source_name(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_name
+// ------------------------------------
+#[test]
+fn test_get_device_name() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ let name = get_device_name(device, DeviceType::INPUT).unwrap();
+ println!("input device name: {}", name.into_string());
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ let name = get_device_name(device, DeviceType::OUTPUT).unwrap();
+ println!("output device name: {}", name.into_string());
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_name_by_unknown_device() {
+ assert!(get_device_name(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_manufacturer
+// ------------------------------------
+#[test]
+fn test_get_device_manufacturer() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ // Some devices like AirPods cannot get the vendor info so we print the error directly.
+ // TODO: Replace `map` and `unwrap_or_else` by `map_or_else`
+ let name = get_device_manufacturer(device, DeviceType::INPUT)
+ .map(|name| name.into_string())
+ .unwrap_or_else(|e| format!("Error: {}", e));
+ println!("input device vendor: {}", name);
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ // Some devices like AirPods cannot get the vendor info so we print the error directly.
+ // TODO: Replace `map` and `unwrap_or_else` by `map_or_else`
+ let name = get_device_manufacturer(device, DeviceType::OUTPUT)
+ .map(|name| name.into_string())
+ .unwrap_or_else(|e| format!("Error: {}", e));
+ println!("output device vendor: {}", name);
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_manufacturer_by_unknown_device() {
+ assert!(get_device_manufacturer(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_buffer_frame_size_range
+// ------------------------------------
+#[test]
+fn test_get_device_buffer_frame_size_range() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ let range = get_device_buffer_frame_size_range(device, DeviceType::INPUT).unwrap();
+ println!(
+ "range of input buffer frame size: {}-{}",
+ range.mMinimum, range.mMaximum
+ );
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ let range = get_device_buffer_frame_size_range(device, DeviceType::OUTPUT).unwrap();
+ println!(
+ "range of output buffer frame size: {}-{}",
+ range.mMinimum, range.mMaximum
+ );
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_buffer_frame_size_range_by_unknown_device() {
+ assert!(get_device_buffer_frame_size_range(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_latency
+// ------------------------------------
+#[test]
+fn test_get_device_latency() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ let latency = get_device_latency(device, DeviceType::INPUT).unwrap();
+ println!("latency of input device: {}", latency);
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ let latency = get_device_latency(device, DeviceType::OUTPUT).unwrap();
+ println!("latency of output device: {}", latency);
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_latency_by_unknown_device() {
+ assert!(get_device_latency(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_streams
+// ------------------------------------
+#[test]
+fn test_get_device_streams() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ let streams = get_device_streams(device, DeviceType::INPUT).unwrap();
+ println!("streams on the input device: {:?}", streams);
+ assert!(!streams.is_empty());
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ let streams = get_device_streams(device, DeviceType::OUTPUT).unwrap();
+ println!("streams on the output device: {:?}", streams);
+ assert!(!streams.is_empty());
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_streams_by_unknown_device() {
+ assert!(get_device_streams(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_device_sample_rate
+// ------------------------------------
+#[test]
+fn test_get_device_sample_rate() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ let rate = get_device_sample_rate(device, DeviceType::INPUT).unwrap();
+ println!("input sample rate: {}", rate);
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ let rate = get_device_sample_rate(device, DeviceType::OUTPUT).unwrap();
+ println!("output sample rate: {}", rate);
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_device_sample_rate_by_unknown_device() {
+ assert!(get_device_sample_rate(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_ranges_of_device_sample_rate
+// ------------------------------------
+#[test]
+fn test_get_ranges_of_device_sample_rate() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ let ranges = get_ranges_of_device_sample_rate(device, DeviceType::INPUT).unwrap();
+ println!("ranges of input sample rate: {:?}", ranges);
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ let ranges = get_ranges_of_device_sample_rate(device, DeviceType::OUTPUT).unwrap();
+ println!("ranges of output sample rate: {:?}", ranges);
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_ranges_of_device_sample_rate_by_unknown_device() {
+ assert!(get_ranges_of_device_sample_rate(kAudioObjectUnknown, DeviceType::INPUT).is_err());
+}
+
+// get_stream_latency
+// ------------------------------------
+#[test]
+fn test_get_stream_latency() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ let streams = get_device_streams(device, DeviceType::INPUT).unwrap();
+ for stream in streams {
+ let latency = get_stream_latency(stream).unwrap();
+ println!("latency of the input stream {} is {}", stream, latency);
+ }
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ let streams = get_device_streams(device, DeviceType::OUTPUT).unwrap();
+ for stream in streams {
+ let latency = get_stream_latency(stream).unwrap();
+ println!("latency of the output stream {} is {}", stream, latency);
+ }
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_stream_latency_by_unknown_device() {
+ assert!(get_stream_latency(kAudioObjectUnknown).is_err());
+}
+
+// get_stream_virtual_format
+// ------------------------------------
+#[test]
+fn test_get_stream_virtual_format() {
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ let streams = get_device_streams(device, DeviceType::INPUT).unwrap();
+ let formats = streams
+ .iter()
+ .map(|s| get_stream_virtual_format(*s))
+ .collect::<Vec<std::result::Result<AudioStreamBasicDescription, OSStatus>>>();
+ println!("input stream formats: {:?}", formats);
+ assert!(!formats.is_empty());
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ let streams = get_device_streams(device, DeviceType::OUTPUT).unwrap();
+ let formats = streams
+ .iter()
+ .map(|s| get_stream_virtual_format(*s))
+ .collect::<Vec<std::result::Result<AudioStreamBasicDescription, OSStatus>>>();
+ println!("output stream formats: {:?}", formats);
+ assert!(!formats.is_empty());
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_stream_virtual_format_by_unknown_stream() {
+ assert!(get_stream_virtual_format(kAudioObjectUnknown).is_err());
+}
+
+// get_stream_terminal_type
+// ------------------------------------
+
+#[test]
+fn test_get_stream_terminal_type() {
+ fn terminal_type_to_device_type(terminal_type: u32) -> Option<DeviceType> {
+ #[allow(non_upper_case_globals)]
+ match terminal_type {
+ kAudioStreamTerminalTypeMicrophone
+ | kAudioStreamTerminalTypeHeadsetMicrophone
+ | kAudioStreamTerminalTypeReceiverMicrophone => Some(DeviceType::INPUT),
+ kAudioStreamTerminalTypeSpeaker
+ | kAudioStreamTerminalTypeHeadphones
+ | kAudioStreamTerminalTypeLFESpeaker
+ | kAudioStreamTerminalTypeReceiverSpeaker => Some(DeviceType::OUTPUT),
+ t if t > INPUT_UNDEFINED && t < OUTPUT_UNDEFINED => Some(DeviceType::INPUT),
+ t if t > OUTPUT_UNDEFINED && t < BIDIRECTIONAL_UNDEFINED => Some(DeviceType::OUTPUT),
+ t => {
+ println!("UNKNOWN TerminalType {:#06x}", t);
+ None
+ }
+ }
+ }
+ if let Some(device) = test_get_default_device(Scope::Input) {
+ let streams = get_device_streams(device, DeviceType::INPUT).unwrap();
+ for stream in streams {
+ assert_eq!(
+ terminal_type_to_device_type(get_stream_terminal_type(stream).unwrap()),
+ Some(DeviceType::INPUT)
+ );
+ }
+ } else {
+ println!("No input device.");
+ }
+
+ if let Some(device) = test_get_default_device(Scope::Output) {
+ let streams = get_device_streams(device, DeviceType::OUTPUT).unwrap();
+ for stream in streams {
+ assert_eq!(
+ terminal_type_to_device_type(get_stream_terminal_type(stream).unwrap()),
+ Some(DeviceType::OUTPUT)
+ );
+ }
+ } else {
+ println!("No output device.");
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_get_stream_terminal_type_by_unknown_stream() {
+ assert!(get_stream_terminal_type(kAudioObjectUnknown).is_err());
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs
new file mode 100644
index 0000000000..340fec002d
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs
@@ -0,0 +1,1215 @@
+extern crate itertools;
+
+use self::itertools::iproduct;
+use super::utils::{
+ get_devices_info_in_scope, noop_data_callback, test_device_channels_in_scope,
+ test_get_default_device, test_ops_context_operation, test_ops_stream_operation, Scope,
+};
+use super::*;
+
+// Context Operations
+// ------------------------------------------------------------------------------------------------
+#[test]
+fn test_ops_context_init_and_destroy() {
+ test_ops_context_operation("context: init and destroy", |_context_ptr| {});
+}
+
+#[test]
+fn test_ops_context_backend_id() {
+ test_ops_context_operation("context: backend id", |context_ptr| {
+ let backend = unsafe {
+ let ptr = OPS.get_backend_id.unwrap()(context_ptr);
+ CStr::from_ptr(ptr).to_string_lossy().into_owned()
+ };
+ assert_eq!(backend, "audiounit-rust");
+ });
+}
+
+#[test]
+fn test_ops_context_max_channel_count() {
+ test_ops_context_operation("context: max channel count", |context_ptr| {
+ let output_exists = test_get_default_device(Scope::Output).is_some();
+ let mut max_channel_count = 0;
+ let r = unsafe { OPS.get_max_channel_count.unwrap()(context_ptr, &mut max_channel_count) };
+ if output_exists {
+ assert_eq!(r, ffi::CUBEB_OK);
+ assert_ne!(max_channel_count, 0);
+ } else {
+ assert_eq!(r, ffi::CUBEB_ERROR);
+ assert_eq!(max_channel_count, 0);
+ }
+ });
+}
+
+#[test]
+fn test_ops_context_min_latency() {
+ test_ops_context_operation("context: min latency", |context_ptr| {
+ let output_exists = test_get_default_device(Scope::Output).is_some();
+ let params = ffi::cubeb_stream_params::default();
+ let mut latency = u32::max_value();
+ let r = unsafe { OPS.get_min_latency.unwrap()(context_ptr, params, &mut latency) };
+ if output_exists {
+ assert_eq!(r, ffi::CUBEB_OK);
+ assert!(latency >= SAFE_MIN_LATENCY_FRAMES);
+ assert!(SAFE_MAX_LATENCY_FRAMES >= latency);
+ } else {
+ assert_eq!(r, ffi::CUBEB_ERROR);
+ assert_eq!(latency, u32::max_value());
+ }
+ });
+}
+
+#[test]
+fn test_ops_context_preferred_sample_rate() {
+ test_ops_context_operation("context: preferred sample rate", |context_ptr| {
+ let output_exists = test_get_default_device(Scope::Output).is_some();
+ let mut rate = u32::max_value();
+ let r = unsafe { OPS.get_preferred_sample_rate.unwrap()(context_ptr, &mut rate) };
+ if output_exists {
+ assert_eq!(r, ffi::CUBEB_OK);
+ assert_ne!(rate, u32::max_value());
+ assert_ne!(rate, 0);
+ } else {
+ assert_eq!(r, ffi::CUBEB_ERROR);
+ assert_eq!(rate, u32::max_value());
+ }
+ });
+}
+
+#[test]
+fn test_ops_context_supported_input_processing_params() {
+ test_ops_context_operation(
+ "context: supported input processing params",
+ |context_ptr| {
+ let mut params = ffi::CUBEB_INPUT_PROCESSING_PARAM_NONE;
+ let r = unsafe {
+ OPS.get_supported_input_processing_params.unwrap()(context_ptr, &mut params)
+ };
+ assert_eq!(r, ffi::CUBEB_OK);
+ assert_eq!(
+ params,
+ ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL
+ );
+ },
+ );
+}
+
+#[test]
+fn test_ops_context_enumerate_devices_unknown() {
+ test_ops_context_operation("context: enumerate devices (unknown)", |context_ptr| {
+ let mut coll = ffi::cubeb_device_collection {
+ device: ptr::null_mut(),
+ count: 0,
+ };
+ assert_eq!(
+ unsafe {
+ OPS.enumerate_devices.unwrap()(
+ context_ptr,
+ ffi::CUBEB_DEVICE_TYPE_UNKNOWN,
+ &mut coll,
+ )
+ },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(coll.count, 0);
+ assert_eq!(coll.device, ptr::null_mut());
+ assert_eq!(
+ unsafe { OPS.device_collection_destroy.unwrap()(context_ptr, &mut coll) },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(coll.count, 0);
+ assert_eq!(coll.device, ptr::null_mut());
+ });
+}
+
+#[test]
+fn test_ops_context_enumerate_devices_input() {
+ test_ops_context_operation("context: enumerate devices (input)", |context_ptr| {
+ let having_input = test_get_default_device(Scope::Input).is_some();
+ let mut coll = ffi::cubeb_device_collection {
+ device: ptr::null_mut(),
+ count: 0,
+ };
+ assert_eq!(
+ unsafe {
+ OPS.enumerate_devices.unwrap()(context_ptr, ffi::CUBEB_DEVICE_TYPE_INPUT, &mut coll)
+ },
+ ffi::CUBEB_OK
+ );
+ if having_input {
+ assert_ne!(coll.count, 0);
+ assert_ne!(coll.device, ptr::null_mut());
+ } else {
+ assert_eq!(coll.count, 0);
+ assert_eq!(coll.device, ptr::null_mut());
+ }
+ assert_eq!(
+ unsafe { OPS.device_collection_destroy.unwrap()(context_ptr, &mut coll) },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(coll.count, 0);
+ assert_eq!(coll.device, ptr::null_mut());
+ });
+}
+
+#[test]
+fn test_ops_context_enumerate_devices_output() {
+ test_ops_context_operation("context: enumerate devices (output)", |context_ptr| {
+ let output_exists = test_get_default_device(Scope::Output).is_some();
+ let mut coll = ffi::cubeb_device_collection {
+ device: ptr::null_mut(),
+ count: 0,
+ };
+ assert_eq!(
+ unsafe {
+ OPS.enumerate_devices.unwrap()(
+ context_ptr,
+ ffi::CUBEB_DEVICE_TYPE_OUTPUT,
+ &mut coll,
+ )
+ },
+ ffi::CUBEB_OK
+ );
+ if output_exists {
+ assert_ne!(coll.count, 0);
+ assert_ne!(coll.device, ptr::null_mut());
+ } else {
+ assert_eq!(coll.count, 0);
+ assert_eq!(coll.device, ptr::null_mut());
+ }
+ assert_eq!(
+ unsafe { OPS.device_collection_destroy.unwrap()(context_ptr, &mut coll) },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(coll.count, 0);
+ assert_eq!(coll.device, ptr::null_mut());
+ });
+}
+
+#[test]
+fn test_ops_context_device_collection_destroy() {
+ // Destroy a dummy device collection, without calling enumerate_devices to allocate memory for the device collection
+ test_ops_context_operation("context: device collection destroy", |context_ptr| {
+ let mut coll = ffi::cubeb_device_collection {
+ device: ptr::null_mut(),
+ count: 0,
+ };
+ assert_eq!(
+ unsafe { OPS.device_collection_destroy.unwrap()(context_ptr, &mut coll) },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(coll.device, ptr::null_mut());
+ assert_eq!(coll.count, 0);
+ });
+}
+
+#[test]
+fn test_ops_context_register_device_collection_changed_unknown() {
+ test_ops_context_operation(
+ "context: register device collection changed (unknown)",
+ |context_ptr| {
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ ffi::CUBEB_DEVICE_TYPE_UNKNOWN,
+ None,
+ ptr::null_mut(),
+ )
+ },
+ ffi::CUBEB_ERROR_INVALID_PARAMETER
+ );
+ },
+ );
+}
+
+#[test]
+fn test_ops_context_register_device_collection_changed_twice_input() {
+ test_ops_context_register_device_collection_changed_twice(ffi::CUBEB_DEVICE_TYPE_INPUT);
+}
+
+#[test]
+fn test_ops_context_register_device_collection_changed_twice_output() {
+ test_ops_context_register_device_collection_changed_twice(ffi::CUBEB_DEVICE_TYPE_OUTPUT);
+}
+
+#[test]
+fn test_ops_context_register_device_collection_changed_twice_inout() {
+ test_ops_context_register_device_collection_changed_twice(
+ ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT,
+ );
+}
+
+fn test_ops_context_register_device_collection_changed_twice(devtype: u32) {
+ extern "C" fn callback(_: *mut ffi::cubeb, _: *mut c_void) {}
+ let label_input: &'static str = "context: register device collection changed twice (input)";
+ let label_output: &'static str = "context: register device collection changed twice (output)";
+ let label_inout: &'static str = "context: register device collection changed twice (inout)";
+ let label = if devtype == ffi::CUBEB_DEVICE_TYPE_INPUT {
+ label_input
+ } else if devtype == ffi::CUBEB_DEVICE_TYPE_OUTPUT {
+ label_output
+ } else if devtype == ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT {
+ label_inout
+ } else {
+ return;
+ };
+
+ test_ops_context_operation(label, |context_ptr| {
+ // Register a callback within the defined scope.
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ devtype,
+ Some(callback),
+ ptr::null_mut(),
+ )
+ },
+ ffi::CUBEB_OK
+ );
+
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ devtype,
+ Some(callback),
+ ptr::null_mut(),
+ )
+ },
+ ffi::CUBEB_ERROR_INVALID_PARAMETER
+ );
+ // Unregister
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ devtype,
+ None,
+ ptr::null_mut(),
+ )
+ },
+ ffi::CUBEB_OK
+ );
+ });
+}
+
+#[test]
+fn test_ops_context_register_device_collection_changed() {
+ extern "C" fn callback(_: *mut ffi::cubeb, _: *mut c_void) {}
+ test_ops_context_operation(
+ "context: register device collection changed",
+ |context_ptr| {
+ let devtypes: [ffi::cubeb_device_type; 3] = [
+ ffi::CUBEB_DEVICE_TYPE_INPUT,
+ ffi::CUBEB_DEVICE_TYPE_OUTPUT,
+ ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT,
+ ];
+
+ for devtype in &devtypes {
+ // Register a callback in the defined scoped.
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ *devtype,
+ Some(callback),
+ ptr::null_mut(),
+ )
+ },
+ ffi::CUBEB_OK
+ );
+
+ // Unregister all callbacks regardless of the scope.
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT,
+ None,
+ ptr::null_mut(),
+ )
+ },
+ ffi::CUBEB_OK
+ );
+
+ // Register callback in the defined scoped again.
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ *devtype,
+ Some(callback),
+ ptr::null_mut(),
+ )
+ },
+ ffi::CUBEB_OK
+ );
+
+ // Unregister callback within the defined scope.
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ *devtype,
+ None,
+ ptr::null_mut(),
+ )
+ },
+ ffi::CUBEB_OK
+ );
+ }
+ },
+ );
+}
+
+#[test]
+fn test_ops_context_register_device_collection_changed_with_a_duplex_stream() {
+ use std::thread;
+ use std::time::Duration;
+
+ extern "C" fn callback(_: *mut ffi::cubeb, got_called_ptr: *mut c_void) {
+ let got_called = unsafe { &mut *(got_called_ptr as *mut bool) };
+ *got_called = true;
+ }
+
+ test_ops_context_operation(
+ "context: register device collection changed and create a duplex stream",
+ |context_ptr| {
+ let got_called = Box::new(false);
+ let got_called_ptr = Box::into_raw(got_called);
+
+ // Register a callback monitoring both input and output device collection.
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT,
+ Some(callback),
+ got_called_ptr as *mut c_void,
+ )
+ },
+ ffi::CUBEB_OK
+ );
+
+ // The aggregate device is very likely to be created in the system
+ // when creating a duplex stream. We need to make sure it won't trigger
+ // the callback.
+ test_default_duplex_stream_operation("duplex stream", |_stream| {
+ // Do nothing but wait for device-collection change.
+ thread::sleep(Duration::from_millis(200));
+ });
+
+ // Unregister the callback.
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ ffi::CUBEB_DEVICE_TYPE_INPUT | ffi::CUBEB_DEVICE_TYPE_OUTPUT,
+ None,
+ got_called_ptr as *mut c_void,
+ )
+ },
+ ffi::CUBEB_OK
+ );
+
+ let got_called = unsafe { Box::from_raw(got_called_ptr) };
+ assert!(!got_called.as_ref());
+ },
+ );
+}
+
+#[test]
+#[ignore]
+fn test_ops_context_register_device_collection_changed_manual() {
+ test_ops_context_operation(
+ "(manual) context: register device collection changed",
+ |context_ptr| {
+ println!("context @ {:p}", context_ptr);
+
+ struct Data {
+ context: *mut ffi::cubeb,
+ touched: u32, // TODO: Use AtomicU32 instead
+ }
+
+ extern "C" fn input_callback(context: *mut ffi::cubeb, user: *mut c_void) {
+ println!("input > context @ {:p}", context);
+ let data = unsafe { &mut (*(user as *mut Data)) };
+ assert_eq!(context, data.context);
+ data.touched += 1;
+ }
+
+ extern "C" fn output_callback(context: *mut ffi::cubeb, user: *mut c_void) {
+ println!("output > context @ {:p}", context);
+ let data = unsafe { &mut (*(user as *mut Data)) };
+ assert_eq!(context, data.context);
+ data.touched += 1;
+ }
+
+ let mut data = Data {
+ context: context_ptr,
+ touched: 0,
+ };
+
+ // Register a callback for input scope.
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ ffi::CUBEB_DEVICE_TYPE_INPUT,
+ Some(input_callback),
+ &mut data as *mut Data as *mut c_void,
+ )
+ },
+ ffi::CUBEB_OK
+ );
+
+ // Register a callback for output scope.
+ assert_eq!(
+ unsafe {
+ OPS.register_device_collection_changed.unwrap()(
+ context_ptr,
+ ffi::CUBEB_DEVICE_TYPE_OUTPUT,
+ Some(output_callback),
+ &mut data as *mut Data as *mut c_void,
+ )
+ },
+ ffi::CUBEB_OK
+ );
+
+ while data.touched < 2 {}
+ },
+ );
+}
+
+#[test]
+fn test_ops_context_stream_init_no_stream_params() {
+ let name = "context: stream_init with no stream params";
+ test_ops_context_operation(name, |context_ptr| {
+ let mut stream: *mut ffi::cubeb_stream = ptr::null_mut();
+ let stream_name = CString::new(name).expect("Failed to create stream name");
+ assert_eq!(
+ unsafe {
+ OPS.stream_init.unwrap()(
+ context_ptr,
+ &mut stream,
+ stream_name.as_ptr(),
+ ptr::null_mut(), // Use default input device.
+ ptr::null_mut(), // No input parameters.
+ ptr::null_mut(), // Use default output device.
+ ptr::null_mut(), // No output parameters.
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(noop_data_callback),
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ )
+ },
+ ffi::CUBEB_ERROR_INVALID_PARAMETER
+ );
+ assert!(stream.is_null());
+ });
+}
+
+#[test]
+fn test_ops_context_stream_init_no_input_stream_params() {
+ let name = "context: stream_init with no input stream params";
+ let input_device = test_get_default_device(Scope::Input);
+ if input_device.is_none() {
+ println!("No input device to perform input tests for \"{}\".", name);
+ return;
+ }
+ test_ops_context_operation(name, |context_ptr| {
+ let mut stream: *mut ffi::cubeb_stream = ptr::null_mut();
+ let stream_name = CString::new(name).expect("Failed to create stream name");
+ assert_eq!(
+ unsafe {
+ OPS.stream_init.unwrap()(
+ context_ptr,
+ &mut stream,
+ stream_name.as_ptr(),
+ input_device.unwrap() as ffi::cubeb_devid,
+ ptr::null_mut(), // No input parameters.
+ ptr::null_mut(), // Use default output device.
+ ptr::null_mut(), // No output parameters.
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(noop_data_callback),
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ )
+ },
+ ffi::CUBEB_ERROR_INVALID_PARAMETER
+ );
+ assert!(stream.is_null());
+ });
+}
+
+#[test]
+fn test_ops_context_stream_init_no_output_stream_params() {
+ let name = "context: stream_init with no output stream params";
+ let output_device = test_get_default_device(Scope::Output);
+ if output_device.is_none() {
+ println!("No output device to perform output tests for \"{}\".", name);
+ return;
+ }
+ test_ops_context_operation(name, |context_ptr| {
+ let mut stream: *mut ffi::cubeb_stream = ptr::null_mut();
+ let stream_name = CString::new(name).expect("Failed to create stream name");
+ assert_eq!(
+ unsafe {
+ OPS.stream_init.unwrap()(
+ context_ptr,
+ &mut stream,
+ stream_name.as_ptr(),
+ ptr::null_mut(), // Use default input device.
+ ptr::null_mut(), // No input parameters.
+ output_device.unwrap() as ffi::cubeb_devid,
+ ptr::null_mut(), // No output parameters.
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(noop_data_callback),
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ )
+ },
+ ffi::CUBEB_ERROR_INVALID_PARAMETER
+ );
+ assert!(stream.is_null());
+ });
+}
+
+#[test]
+fn test_ops_context_stream_init_no_data_callback() {
+ let name = "context: stream_init with no data callback";
+ test_ops_context_operation(name, |context_ptr| {
+ let mut stream: *mut ffi::cubeb_stream = ptr::null_mut();
+ let stream_name = CString::new(name).expect("Failed to create stream name");
+
+ let mut output_params = ffi::cubeb_stream_params::default();
+ output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ output_params.rate = 44100;
+ output_params.channels = 2;
+ output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ assert_eq!(
+ unsafe {
+ OPS.stream_init.unwrap()(
+ context_ptr,
+ &mut stream,
+ stream_name.as_ptr(),
+ ptr::null_mut(), // Use default input device.
+ ptr::null_mut(), // No input parameters.
+ ptr::null_mut(), // Use default output device.
+ &mut output_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ None, // No data callback.
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ )
+ },
+ ffi::CUBEB_ERROR_INVALID_PARAMETER
+ );
+ assert!(stream.is_null());
+ });
+}
+
+#[test]
+fn test_ops_context_stream_init_channel_rate_combinations() {
+ let name = "context: stream_init with various channels and rates";
+ test_ops_context_operation(name, |context_ptr| {
+ let mut stream: *mut ffi::cubeb_stream = ptr::null_mut();
+ let stream_name = CString::new(name).expect("Failed to create stream name");
+
+ const MAX_NUM_CHANNELS: u32 = 32;
+ let channel_values: Vec<u32> = vec![1, 2, 3, 4, 6];
+ let freq_values: Vec<u32> = vec![16000, 24000, 44100, 48000];
+ let is_float_values: Vec<bool> = vec![false, true];
+
+ for (channels, freq, is_float) in iproduct!(channel_values, freq_values, is_float_values) {
+ assert!(channels < MAX_NUM_CHANNELS);
+ println!("--------------------------");
+ println!(
+ "Testing {} channel(s), {} Hz, {}\n",
+ channels,
+ freq,
+ if is_float { "float" } else { "short" }
+ );
+
+ let mut output_params = ffi::cubeb_stream_params::default();
+ output_params.format = if is_float {
+ ffi::CUBEB_SAMPLE_FLOAT32NE
+ } else {
+ ffi::CUBEB_SAMPLE_S16NE
+ };
+ output_params.rate = freq;
+ output_params.channels = channels;
+ output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ assert_eq!(
+ unsafe {
+ OPS.stream_init.unwrap()(
+ context_ptr,
+ &mut stream,
+ stream_name.as_ptr(),
+ ptr::null_mut(), // Use default input device.
+ ptr::null_mut(), // No input parameters.
+ ptr::null_mut(), // Use default output device.
+ &mut output_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(noop_data_callback), // No data callback.
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ )
+ },
+ ffi::CUBEB_OK
+ );
+ assert!(!stream.is_null());
+ }
+ });
+}
+
+// Stream Operations
+// ------------------------------------------------------------------------------------------------
+fn test_default_output_stream_operation<F>(name: &'static str, operation: F)
+where
+ F: FnOnce(*mut ffi::cubeb_stream),
+{
+ // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
+ // (in the comments).
+ let mut output_params = ffi::cubeb_stream_params::default();
+ output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ output_params.rate = 44100;
+ output_params.channels = 2;
+ output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ test_ops_stream_operation(
+ name,
+ ptr::null_mut(), // Use default input device.
+ ptr::null_mut(), // No input parameters.
+ ptr::null_mut(), // Use default output device.
+ &mut output_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(noop_data_callback),
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ operation,
+ );
+}
+
+fn test_default_duplex_stream_operation<F>(name: &'static str, operation: F)
+where
+ F: FnOnce(*mut ffi::cubeb_stream),
+{
+ // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
+ // (in the comments).
+ let mut input_params = ffi::cubeb_stream_params::default();
+ input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ input_params.rate = 48000;
+ input_params.channels = 1;
+ input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ input_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ let mut output_params = ffi::cubeb_stream_params::default();
+ output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ output_params.rate = 44100;
+ output_params.channels = 2;
+ output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ test_ops_stream_operation(
+ name,
+ ptr::null_mut(), // Use default input device.
+ &mut input_params,
+ ptr::null_mut(), // Use default output device.
+ &mut output_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(noop_data_callback),
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ operation,
+ );
+}
+
+fn test_stereo_input_duplex_stream_operation<F>(name: &'static str, operation: F)
+where
+ F: FnOnce(*mut ffi::cubeb_stream),
+{
+ let mut input_devices = get_devices_info_in_scope(Scope::Input);
+ input_devices.retain(|d| test_device_channels_in_scope(d.id, Scope::Input).unwrap_or(0) >= 2);
+ if input_devices.is_empty() {
+ println!("No stereo input device present. Skipping stereo-input test.");
+ return;
+ }
+
+ let mut input_params = ffi::cubeb_stream_params::default();
+ input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ input_params.rate = 48000;
+ input_params.channels = 2;
+ input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ input_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ let mut output_params = ffi::cubeb_stream_params::default();
+ output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ output_params.rate = 48000;
+ output_params.channels = 2;
+ output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ test_ops_stream_operation(
+ name,
+ input_devices[0].id as ffi::cubeb_devid,
+ &mut input_params,
+ ptr::null_mut(), // Use default output device.
+ &mut output_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(noop_data_callback),
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ operation,
+ );
+}
+
+fn test_default_duplex_voice_stream_operation<F>(name: &'static str, operation: F)
+where
+ F: FnOnce(*mut ffi::cubeb_stream),
+{
+ // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
+ // (in the comments).
+ let mut input_params = ffi::cubeb_stream_params::default();
+ input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ input_params.rate = 44100;
+ input_params.channels = 1;
+ input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ input_params.prefs = ffi::CUBEB_STREAM_PREF_VOICE;
+
+ let mut output_params = ffi::cubeb_stream_params::default();
+ output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ output_params.rate = 48000;
+ output_params.channels = 2;
+ output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ output_params.prefs = ffi::CUBEB_STREAM_PREF_VOICE;
+
+ test_ops_stream_operation(
+ name,
+ ptr::null_mut(), // Use default input device.
+ &mut input_params,
+ ptr::null_mut(), // Use default output device.
+ &mut output_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(noop_data_callback),
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ operation,
+ );
+}
+
+fn test_stereo_input_duplex_voice_stream_operation<F>(name: &'static str, operation: F)
+where
+ F: FnOnce(*mut ffi::cubeb_stream),
+{
+ let mut input_devices = get_devices_info_in_scope(Scope::Input);
+ input_devices.retain(|d| test_device_channels_in_scope(d.id, Scope::Input).unwrap_or(0) >= 2);
+ if input_devices.is_empty() {
+ println!("No stereo input device present. Skipping stereo-input test.");
+ return;
+ }
+
+ let mut input_params = ffi::cubeb_stream_params::default();
+ input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ input_params.rate = 44100;
+ input_params.channels = 2;
+ input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ input_params.prefs = ffi::CUBEB_STREAM_PREF_VOICE;
+
+ let mut output_params = ffi::cubeb_stream_params::default();
+ output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ output_params.rate = 44100;
+ output_params.channels = 2;
+ output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ output_params.prefs = ffi::CUBEB_STREAM_PREF_VOICE;
+
+ test_ops_stream_operation(
+ name,
+ input_devices[0].id as ffi::cubeb_devid,
+ &mut input_params,
+ ptr::null_mut(), // Use default output device.
+ &mut output_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(noop_data_callback),
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ operation,
+ );
+}
+
+#[test]
+fn test_ops_stream_init_and_destroy() {
+ test_default_output_stream_operation("stream: init and destroy", |_stream| {});
+}
+
+#[test]
+fn test_ops_stream_start() {
+ test_default_output_stream_operation("stream: start", |stream| {
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+ });
+}
+
+#[test]
+fn test_ops_stream_stop() {
+ test_default_output_stream_operation("stream: stop", |stream| {
+ assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK);
+ });
+}
+
+#[test]
+fn test_ops_stream_position() {
+ test_default_output_stream_operation("stream: position", |stream| {
+ let mut position = u64::max_value();
+ assert_eq!(
+ unsafe { OPS.stream_get_position.unwrap()(stream, &mut position) },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(position, 0);
+ });
+}
+
+#[test]
+fn test_ops_stream_latency() {
+ test_default_output_stream_operation("stream: latency", |stream| {
+ let mut latency = u32::max_value();
+ assert_eq!(
+ unsafe { OPS.stream_get_latency.unwrap()(stream, &mut latency) },
+ ffi::CUBEB_OK
+ );
+ assert_ne!(latency, u32::max_value());
+ });
+}
+
+#[test]
+fn test_ops_stream_set_volume() {
+ test_default_output_stream_operation("stream: set volume", |stream| {
+ assert_eq!(
+ unsafe { OPS.stream_set_volume.unwrap()(stream, 0.5) },
+ ffi::CUBEB_OK
+ );
+ });
+}
+
+#[test]
+fn test_ops_stream_current_device() {
+ test_default_output_stream_operation("stream: get current device and destroy it", |stream| {
+ if test_get_default_device(Scope::Input).is_none()
+ || test_get_default_device(Scope::Output).is_none()
+ {
+ println!("stream_get_current_device only works when the machine has both input and output devices");
+ return;
+ }
+
+ let mut device: *mut ffi::cubeb_device = ptr::null_mut();
+ if unsafe { OPS.stream_get_current_device.unwrap()(stream, &mut device) } != ffi::CUBEB_OK {
+ // It can happen when we fail to get the device source.
+ println!("stream_get_current_device fails. Skip this test.");
+ return;
+ }
+
+ assert!(!device.is_null());
+ // Uncomment the below to print out the results.
+ // let deviceref = unsafe { DeviceRef::from_ptr(device) };
+ // println!(
+ // "output: {}",
+ // deviceref.output_name().unwrap_or("(no device name)")
+ // );
+ // println!(
+ // "input: {}",
+ // deviceref.input_name().unwrap_or("(no device name)")
+ // );
+ assert_eq!(
+ unsafe { OPS.stream_device_destroy.unwrap()(stream, device) },
+ ffi::CUBEB_OK
+ );
+ });
+}
+
+#[test]
+fn test_ops_stream_device_destroy() {
+ test_default_output_stream_operation("stream: destroy null device", |stream| {
+ assert_eq!(
+ unsafe { OPS.stream_device_destroy.unwrap()(stream, ptr::null_mut()) },
+ ffi::CUBEB_OK // It returns OK anyway.
+ );
+ });
+}
+
+#[test]
+fn test_ops_stream_register_device_changed_callback() {
+ extern "C" fn callback(_: *mut c_void) {}
+
+ test_default_output_stream_operation("stream: register device changed callback", |stream| {
+ assert_eq!(
+ unsafe { OPS.stream_register_device_changed_callback.unwrap()(stream, Some(callback)) },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(
+ unsafe { OPS.stream_register_device_changed_callback.unwrap()(stream, Some(callback)) },
+ ffi::CUBEB_ERROR_INVALID_PARAMETER
+ );
+ assert_eq!(
+ unsafe { OPS.stream_register_device_changed_callback.unwrap()(stream, None) },
+ ffi::CUBEB_OK
+ );
+ });
+}
+
+#[test]
+fn test_ops_stereo_input_duplex_stream_init_and_destroy() {
+ test_stereo_input_duplex_stream_operation(
+ "stereo-input duplex stream: init and destroy",
+ |_stream| {},
+ );
+}
+
+#[test]
+fn test_ops_stereo_input_duplex_stream_start() {
+ test_stereo_input_duplex_stream_operation("stereo-input duplex stream: start", |stream| {
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+ });
+}
+
+#[test]
+fn test_ops_stereo_input_duplex_stream_stop() {
+ test_stereo_input_duplex_stream_operation("stereo-input duplex stream: stop", |stream| {
+ assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK);
+ });
+}
+
+#[test]
+fn test_ops_duplex_voice_stream_init_and_destroy() {
+ test_default_duplex_voice_stream_operation(
+ "duplex voice stream: init and destroy",
+ |_stream| {},
+ );
+}
+
+#[test]
+fn test_ops_duplex_voice_stream_start() {
+ test_default_duplex_voice_stream_operation("duplex voice stream: start", |stream| {
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+ });
+}
+
+#[test]
+fn test_ops_duplex_voice_stream_stop() {
+ test_default_duplex_voice_stream_operation("duplex voice stream: stop", |stream| {
+ assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK);
+ });
+}
+
+#[test]
+fn test_ops_duplex_voice_stream_set_input_mute() {
+ test_default_duplex_voice_stream_operation("duplex voice stream: mute", |stream| {
+ assert_eq!(
+ unsafe { OPS.stream_set_input_mute.unwrap()(stream, 1) },
+ ffi::CUBEB_OK
+ );
+ });
+}
+
+#[test]
+fn test_ops_duplex_voice_stream_set_input_mute_before_start() {
+ test_default_duplex_voice_stream_operation(
+ "duplex voice stream: mute before start",
+ |stream| {
+ assert_eq!(
+ unsafe { OPS.stream_set_input_mute.unwrap()(stream, 1) },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+ },
+ );
+}
+
+#[test]
+fn test_ops_duplex_voice_stream_set_input_mute_before_start_with_reinit() {
+ test_default_duplex_voice_stream_operation(
+ "duplex voice stream: mute before start with reinit",
+ |stream| {
+ assert_eq!(
+ unsafe { OPS.stream_set_input_mute.unwrap()(stream, 1) },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+
+ // Hacky cast, but testing this here was simplest for now.
+ let stm = unsafe { &mut *(stream as *mut AudioUnitStream) };
+ stm.reinit_async();
+ let queue = stm.queue.clone();
+ let mut mute_after_reinit = false;
+ queue.run_sync(|| {
+ let mut mute: u32 = 0;
+ let r = audio_unit_get_property(
+ stm.core_stream_data.input_unit,
+ kAUVoiceIOProperty_MuteOutput,
+ kAudioUnitScope_Global,
+ AU_IN_BUS,
+ &mut mute,
+ &mut mem::size_of::<u32>(),
+ );
+ assert_eq!(r, NO_ERR);
+ mute_after_reinit = mute == 1;
+ });
+ assert_eq!(mute_after_reinit, true);
+ },
+ );
+}
+
+#[test]
+fn test_ops_duplex_voice_stream_set_input_mute_after_start() {
+ test_default_duplex_voice_stream_operation("duplex voice stream: mute after start", |stream| {
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+ assert_eq!(
+ unsafe { OPS.stream_set_input_mute.unwrap()(stream, 1) },
+ ffi::CUBEB_OK
+ );
+ });
+}
+
+#[test]
+fn test_ops_duplex_voice_stream_set_input_processing_params() {
+ test_default_duplex_voice_stream_operation("duplex voice stream: processing", |stream| {
+ let params: ffi::cubeb_input_processing_params =
+ ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL;
+ assert_eq!(
+ unsafe { OPS.stream_set_input_processing_params.unwrap()(stream, params) },
+ ffi::CUBEB_OK
+ );
+ });
+}
+
+#[test]
+fn test_ops_duplex_voice_stream_set_input_processing_params_before_start() {
+ test_default_duplex_voice_stream_operation(
+ "duplex voice stream: processing before start",
+ |stream| {
+ let params: ffi::cubeb_input_processing_params =
+ ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL;
+ assert_eq!(
+ unsafe { OPS.stream_set_input_processing_params.unwrap()(stream, params) },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+ },
+ );
+}
+
+#[test]
+fn test_ops_duplex_voice_stream_set_input_processing_params_before_start_with_reinit() {
+ test_default_duplex_voice_stream_operation(
+ "duplex voice stream: processing before start with reinit",
+ |stream| {
+ let params: ffi::cubeb_input_processing_params =
+ ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL;
+ assert_eq!(
+ unsafe { OPS.stream_set_input_processing_params.unwrap()(stream, params) },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+
+ // Hacky cast, but testing this here was simplest for now.
+ let stm = unsafe { &mut *(stream as *mut AudioUnitStream) };
+ stm.reinit_async();
+ let queue = stm.queue.clone();
+ let mut params_after_reinit: ffi::cubeb_input_processing_params =
+ ffi::CUBEB_INPUT_PROCESSING_PARAM_NONE;
+ queue.run_sync(|| {
+ let mut params: ffi::cubeb_input_processing_params =
+ ffi::CUBEB_INPUT_PROCESSING_PARAM_NONE;
+ let mut agc: u32 = 0;
+ let r = audio_unit_get_property(
+ stm.core_stream_data.input_unit,
+ kAUVoiceIOProperty_VoiceProcessingEnableAGC,
+ kAudioUnitScope_Global,
+ AU_IN_BUS,
+ &mut agc,
+ &mut mem::size_of::<u32>(),
+ );
+ assert_eq!(r, NO_ERR);
+ if agc == 1 {
+ params = params | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL;
+ }
+ let mut bypass: u32 = 0;
+ let r = audio_unit_get_property(
+ stm.core_stream_data.input_unit,
+ kAUVoiceIOProperty_BypassVoiceProcessing,
+ kAudioUnitScope_Global,
+ AU_IN_BUS,
+ &mut bypass,
+ &mut mem::size_of::<u32>(),
+ );
+ assert_eq!(r, NO_ERR);
+ if bypass == 0 {
+ params = params
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION;
+ }
+ params_after_reinit = params;
+ });
+ assert_eq!(params, params_after_reinit);
+ },
+ );
+}
+
+#[test]
+fn test_ops_duplex_voice_stream_set_input_processing_params_after_start() {
+ test_default_duplex_voice_stream_operation(
+ "duplex voice stream: processing after start",
+ |stream| {
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+ let params: ffi::cubeb_input_processing_params =
+ ffi::CUBEB_INPUT_PROCESSING_PARAM_ECHO_CANCELLATION
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_NOISE_SUPPRESSION
+ | ffi::CUBEB_INPUT_PROCESSING_PARAM_AUTOMATIC_GAIN_CONTROL;
+ assert_eq!(
+ unsafe { OPS.stream_set_input_processing_params.unwrap()(stream, params) },
+ ffi::CUBEB_OK
+ );
+ },
+ );
+}
+
+#[test]
+fn test_ops_stereo_input_duplex_voice_stream_init_and_destroy() {
+ test_stereo_input_duplex_voice_stream_operation(
+ "stereo-input duplex voice stream: init and destroy",
+ |_stream| {},
+ );
+}
+
+#[test]
+fn test_ops_stereo_input_duplex_voice_stream_start() {
+ test_stereo_input_duplex_voice_stream_operation(
+ "stereo-input duplex voice stream: start",
+ |stream| {
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+ },
+ );
+}
+
+#[test]
+fn test_ops_stereo_input_duplex_voice_stream_stop() {
+ test_stereo_input_duplex_voice_stream_operation(
+ "stereo-input duplex voice stream: stop",
+ |stream| {
+ assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK);
+ },
+ );
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs
new file mode 100644
index 0000000000..b2b2241cc9
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs
@@ -0,0 +1,614 @@
+use super::utils::{
+ test_get_devices_in_scope, test_ops_context_operation, test_ops_stream_operation, Scope,
+ StreamType, TestDeviceInfo, TestDeviceSwitcher,
+};
+use super::*;
+use std::io;
+use std::sync::atomic::AtomicBool;
+
+#[ignore]
+#[test]
+fn test_switch_output_device() {
+ use std::f32::consts::PI;
+
+ const SAMPLE_FREQUENCY: u32 = 48_000;
+
+ // Do nothing if there is no 2 available output devices at least.
+ let devices = test_get_devices_in_scope(Scope::Output);
+ if devices.len() < 2 {
+ println!("Need 2 output devices at least.");
+ return;
+ }
+
+ let mut output_device_switcher = TestDeviceSwitcher::new(Scope::Output);
+
+ // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
+ // (in the comments).
+ let mut output_params = ffi::cubeb_stream_params::default();
+ output_params.format = ffi::CUBEB_SAMPLE_S16NE;
+ output_params.rate = SAMPLE_FREQUENCY;
+ output_params.channels = 1;
+ output_params.layout = ffi::CUBEB_LAYOUT_MONO;
+ output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ // Used to calculate the tone's wave.
+ let mut position: i64 = 0; // TODO: Use Atomic instead.
+
+ test_ops_stream_operation(
+ "stream: North American dial tone",
+ ptr::null_mut(), // Use default input device.
+ ptr::null_mut(), // No input parameters.
+ ptr::null_mut(), // Use default output device.
+ &mut output_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(data_callback),
+ Some(state_callback),
+ &mut position as *mut i64 as *mut c_void,
+ |stream| {
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+ println!("Start playing! Enter 's' to switch device. Enter 'q' to quit.");
+ loop {
+ let mut input = String::new();
+ let _ = io::stdin().read_line(&mut input);
+ assert_eq!(input.pop().unwrap(), '\n');
+ match input.as_str() {
+ "s" => {
+ output_device_switcher.next();
+ }
+ "q" => {
+ println!("Quit.");
+ break;
+ }
+ x => {
+ println!("Unknown command: {}", x);
+ }
+ }
+ }
+ assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK);
+ },
+ );
+
+ extern "C" fn state_callback(
+ stream: *mut ffi::cubeb_stream,
+ user_ptr: *mut c_void,
+ state: ffi::cubeb_state,
+ ) {
+ assert!(!stream.is_null());
+ assert!(!user_ptr.is_null());
+ assert_ne!(state, ffi::CUBEB_STATE_ERROR);
+ }
+
+ extern "C" fn data_callback(
+ stream: *mut ffi::cubeb_stream,
+ user_ptr: *mut c_void,
+ _input_buffer: *const c_void,
+ output_buffer: *mut c_void,
+ nframes: i64,
+ ) -> i64 {
+ assert!(!stream.is_null());
+ assert!(!user_ptr.is_null());
+ assert!(!output_buffer.is_null());
+
+ let buffer = unsafe {
+ let ptr = output_buffer as *mut i16;
+ let len = nframes as usize;
+ slice::from_raw_parts_mut(ptr, len)
+ };
+
+ let position = unsafe { &mut *(user_ptr as *mut i64) };
+
+ // Generate tone on the fly.
+ for data in buffer.iter_mut() {
+ let t1 = (2.0 * PI * 350.0 * (*position) as f32 / SAMPLE_FREQUENCY as f32).sin();
+ let t2 = (2.0 * PI * 440.0 * (*position) as f32 / SAMPLE_FREQUENCY as f32).sin();
+ *data = f32_to_i16_sample(0.5 * (t1 + t2));
+ *position += 1;
+ }
+
+ nframes
+ }
+
+ fn f32_to_i16_sample(x: f32) -> i16 {
+ (x * f32::from(i16::max_value())) as i16
+ }
+}
+
+#[ignore]
+#[test]
+fn test_device_collection_change() {
+ const DUMMY_PTR: *mut c_void = 0xDEAD_BEEF as *mut c_void;
+ let mut context = AudioUnitContext::new();
+ println!("Context allocated @ {:p}", &context);
+
+ extern "C" fn input_changed_callback(context: *mut ffi::cubeb, data: *mut c_void) {
+ println!(
+ "Input device collection @ {:p} is changed. Data @ {:p}",
+ context, data
+ );
+ assert_eq!(data, DUMMY_PTR);
+ }
+
+ extern "C" fn output_changed_callback(context: *mut ffi::cubeb, data: *mut c_void) {
+ println!(
+ "output device collection @ {:p} is changed. Data @ {:p}",
+ context, data
+ );
+ assert_eq!(data, DUMMY_PTR);
+ }
+
+ context.register_device_collection_changed(
+ DeviceType::INPUT,
+ Some(input_changed_callback),
+ DUMMY_PTR,
+ );
+
+ context.register_device_collection_changed(
+ DeviceType::OUTPUT,
+ Some(output_changed_callback),
+ DUMMY_PTR,
+ );
+
+ println!("Unplug/Plug device to see the event log.\nEnter anything to finish.");
+ let mut input = String::new();
+ let _ = std::io::stdin().read_line(&mut input);
+}
+
+#[ignore]
+#[test]
+fn test_stream_tester() {
+ test_ops_context_operation("context: stream tester", |context_ptr| {
+ let mut stream_ptr: *mut ffi::cubeb_stream = ptr::null_mut();
+ let enable_loopback = AtomicBool::new(false);
+ loop {
+ println!(
+ "commands:\n\
+ \t'q': quit\n\
+ \t'c': create a stream\n\
+ \t'd': destroy a stream\n\
+ \t's': start the created stream\n\
+ \t't': stop the created stream\n\
+ \t'r': register a device changed callback\n\
+ \t'l': set loopback (DUPLEX-only)\n\
+ \t'v': set volume\n\
+ \t'm': set input mute\n\
+ \t'p': set input processing"
+ );
+
+ let mut command = String::new();
+ let _ = io::stdin().read_line(&mut command);
+ assert_eq!(command.pop().unwrap(), '\n');
+
+ match command.as_str() {
+ "q" => {
+ println!("Quit.");
+ destroy_stream(&mut stream_ptr);
+ break;
+ }
+ "c" => create_stream(&mut stream_ptr, context_ptr, &enable_loopback),
+ "d" => destroy_stream(&mut stream_ptr),
+ "s" => start_stream(stream_ptr),
+ "t" => stop_stream(stream_ptr),
+ "r" => register_device_change_callback(stream_ptr),
+ "l" => set_loopback(stream_ptr, &enable_loopback),
+ "v" => set_volume(stream_ptr),
+ "m" => set_input_mute(stream_ptr),
+ "p" => set_input_processing(stream_ptr),
+ x => println!("Unknown command: {}", x),
+ }
+ }
+ });
+
+ fn start_stream(stream_ptr: *mut ffi::cubeb_stream) {
+ if stream_ptr.is_null() {
+ println!("No stream can start.");
+ return;
+ }
+ assert_eq!(
+ unsafe { OPS.stream_start.unwrap()(stream_ptr) },
+ ffi::CUBEB_OK
+ );
+ println!("Stream {:p} started.", stream_ptr);
+ }
+
+ fn stop_stream(stream_ptr: *mut ffi::cubeb_stream) {
+ if stream_ptr.is_null() {
+ println!("No stream can stop.");
+ return;
+ }
+ assert_eq!(
+ unsafe { OPS.stream_stop.unwrap()(stream_ptr) },
+ ffi::CUBEB_OK
+ );
+ println!("Stream {:p} stopped.", stream_ptr);
+ }
+
+ fn set_volume(stream_ptr: *mut ffi::cubeb_stream) {
+ if stream_ptr.is_null() {
+ println!("No stream can set volume.");
+ return;
+ }
+ const VOL: f32 = 0.5;
+ assert_eq!(
+ unsafe { OPS.stream_set_volume.unwrap()(stream_ptr, VOL) },
+ ffi::CUBEB_OK
+ );
+ println!("Set stream {:p} volume to {}", stream_ptr, VOL);
+ }
+
+ fn set_loopback(stream_ptr: *mut ffi::cubeb_stream, enable_loopback: &AtomicBool) {
+ if stream_ptr.is_null() {
+ println!("No stream can set loopback.");
+ return;
+ }
+ let stm = unsafe { &mut *(stream_ptr as *mut AudioUnitStream) };
+ if !stm.core_stream_data.has_input() || !stm.core_stream_data.has_output() {
+ println!("Duplex stream needed to set loopback");
+ return;
+ }
+ let mut loopback: Option<bool> = None;
+ while loopback.is_none() {
+ println!("Select action:\n1) Enable loopback, 2) Disable loopback");
+ let mut input = String::new();
+ let _ = io::stdin().read_line(&mut input);
+ assert_eq!(input.pop().unwrap(), '\n');
+ loopback = match input.as_str() {
+ "1" => Some(true),
+ "2" => Some(false),
+ _ => {
+ println!("Invalid action. Select again.\n");
+ None
+ }
+ }
+ }
+ let loopback = loopback.unwrap();
+ enable_loopback.store(loopback, Ordering::SeqCst);
+ println!(
+ "Loopback {} for stream {:p}",
+ if loopback { "enabled" } else { "disabled" },
+ stream_ptr
+ );
+ }
+
+ fn set_input_mute(stream_ptr: *mut ffi::cubeb_stream) {
+ if stream_ptr.is_null() {
+ println!("No stream can set input mute.");
+ return;
+ }
+ let stm = unsafe { &mut *(stream_ptr as *mut AudioUnitStream) };
+ if !stm.core_stream_data.has_input() {
+ println!("Input stream needed to set loopback");
+ return;
+ }
+ let mut mute: Option<bool> = None;
+ while mute.is_none() {
+ println!("Select action:\n1) Mute, 2) Unmute");
+ let mut input = String::new();
+ let _ = io::stdin().read_line(&mut input);
+ assert_eq!(input.pop().unwrap(), '\n');
+ mute = match input.as_str() {
+ "1" => Some(true),
+ "2" => Some(false),
+ _ => {
+ println!("Invalid action. Select again.\n");
+ None
+ }
+ }
+ }
+ let mute = mute.unwrap();
+ let res = unsafe { OPS.stream_set_input_mute.unwrap()(stream_ptr, mute.into()) };
+ println!(
+ "{} set stream {:p} input {}",
+ if res == ffi::CUBEB_OK {
+ "Successfully"
+ } else {
+ "Failed to"
+ },
+ stream_ptr,
+ if mute { "mute" } else { "unmute" }
+ );
+ }
+
+ fn set_input_processing(stream_ptr: *mut ffi::cubeb_stream) {
+ if stream_ptr.is_null() {
+ println!("No stream can set input processing.");
+ return;
+ }
+ let stm = unsafe { &mut *(stream_ptr as *mut AudioUnitStream) };
+ if !stm.core_stream_data.using_voice_processing_unit() {
+ println!("Duplex stream with voice processing needed to set input processing params");
+ return;
+ }
+ let mut params = InputProcessingParams::NONE;
+ {
+ let mut bypass = u32::from(true);
+ let mut size: usize = mem::size_of::<u32>();
+ assert_eq!(
+ audio_unit_get_property(
+ stm.core_stream_data.input_unit,
+ kAudioUnitProperty_BypassEffect,
+ kAudioUnitScope_Global,
+ AU_IN_BUS,
+ &mut bypass,
+ &mut size,
+ ),
+ NO_ERR
+ );
+ assert_eq!(size, mem::size_of::<u32>());
+ if bypass == 0 {
+ params.set(InputProcessingParams::ECHO_CANCELLATION, true);
+ params.set(InputProcessingParams::NOISE_SUPPRESSION, true);
+ }
+ }
+ let mut done = false;
+ while !done {
+ println!(
+ "Supported params: {:?}\nCurrent params: {:?}\nSelect action:\n\
+ \t1) Set None\n\
+ \t2) Toggle Echo Cancellation\n\
+ \t3) Toggle Noise Suppression\n\
+ \t4) Toggle Automatic Gain Control\n\
+ \t5) Toggle Voice Isolation\n\
+ \t6) Set All\n\
+ \t0) Done",
+ stm.context.supported_input_processing_params().unwrap(),
+ params
+ );
+ let mut input = String::new();
+ let _ = io::stdin().read_line(&mut input);
+ assert_eq!(input.pop().unwrap(), '\n');
+ match input.as_str() {
+ "1" => params = InputProcessingParams::NONE,
+ "2" => params.toggle(InputProcessingParams::ECHO_CANCELLATION),
+ "3" => params.toggle(InputProcessingParams::NOISE_SUPPRESSION),
+ "4" => params.toggle(InputProcessingParams::AUTOMATIC_GAIN_CONTROL),
+ "5" => params.toggle(InputProcessingParams::VOICE_ISOLATION),
+ "6" => params = InputProcessingParams::all(),
+ "0" => done = true,
+ _ => println!("Invalid action. Select again.\n"),
+ }
+ }
+ let res =
+ unsafe { OPS.stream_set_input_processing_params.unwrap()(stream_ptr, params.bits()) };
+ println!(
+ "{} set stream {:p} input processing params to {:?}",
+ if res == ffi::CUBEB_OK {
+ "Successfully"
+ } else {
+ "Failed to"
+ },
+ stream_ptr,
+ params,
+ );
+ }
+
+ fn register_device_change_callback(stream_ptr: *mut ffi::cubeb_stream) {
+ extern "C" fn callback(user_ptr: *mut c_void) {
+ println!("user pointer @ {:p}", user_ptr);
+ assert!(user_ptr.is_null());
+ }
+
+ if stream_ptr.is_null() {
+ println!("No stream for registering the callback.");
+ return;
+ }
+ assert_eq!(
+ unsafe {
+ OPS.stream_register_device_changed_callback.unwrap()(stream_ptr, Some(callback))
+ },
+ ffi::CUBEB_OK
+ );
+ println!("Stream {:p} now has a device change callback.", stream_ptr);
+ }
+
+ fn destroy_stream(stream_ptr: &mut *mut ffi::cubeb_stream) {
+ if stream_ptr.is_null() {
+ println!("No need to destroy stream.");
+ return;
+ }
+ unsafe {
+ OPS.stream_destroy.unwrap()(*stream_ptr);
+ }
+ println!("Stream {:p} destroyed.", *stream_ptr);
+ *stream_ptr = ptr::null_mut();
+ }
+
+ fn create_stream(
+ stream_ptr: &mut *mut ffi::cubeb_stream,
+ context_ptr: *mut ffi::cubeb,
+ enable_loopback: &AtomicBool,
+ ) {
+ if !stream_ptr.is_null() {
+ println!("Stream has been created.");
+ return;
+ }
+
+ let mut stream_type = StreamType::empty();
+ while stream_type.is_empty() {
+ println!("Select stream type:\n1) Input 2) Output 3) In-Out Duplex 4) Back");
+ let mut input = String::new();
+ let _ = io::stdin().read_line(&mut input);
+ assert_eq!(input.pop().unwrap(), '\n');
+ stream_type = match input.as_str() {
+ "1" => StreamType::INPUT,
+ "2" => StreamType::OUTPUT,
+ "3" => StreamType::DUPLEX,
+ "4" => {
+ println!("Do nothing.");
+ return;
+ }
+ _ => {
+ println!("Invalid type. Select again.\n");
+ StreamType::empty()
+ }
+ }
+ }
+
+ let device_selector = |scope: Scope| -> AudioObjectID {
+ loop {
+ println!(
+ "Select {} device:\n",
+ if scope == Scope::Input {
+ "input"
+ } else {
+ "output"
+ }
+ );
+ let mut list = vec![];
+ list.push(kAudioObjectUnknown);
+ println!("{:>4}: System default", 0);
+ let devices = test_get_devices_in_scope(scope.clone());
+ for (idx, device) in devices.iter().enumerate() {
+ list.push(*device);
+ let info = TestDeviceInfo::new(*device, scope.clone());
+ println!(
+ "{:>4}: {}\n\tAudioObjectID: {}\n\tuid: {}",
+ idx + 1,
+ info.label,
+ device,
+ info.uid
+ );
+ }
+
+ let mut input = String::new();
+ io::stdin().read_line(&mut input).unwrap();
+ let n: usize = match input.trim().parse() {
+ Err(_) => {
+ println!("Invalid option. Try again.\n");
+ continue;
+ }
+ Ok(n) => n,
+ };
+ if n >= list.len() {
+ println!("Invalid option. Try again.\n");
+ continue;
+ }
+ return list[n];
+ }
+ };
+
+ let mut input_params = get_dummy_stream_params(Scope::Input);
+ let mut output_params = get_dummy_stream_params(Scope::Output);
+
+ let (input_device, input_stream_params) = if stream_type.contains(StreamType::INPUT) {
+ (
+ device_selector(Scope::Input),
+ &mut input_params as *mut ffi::cubeb_stream_params,
+ )
+ } else {
+ (
+ kAudioObjectUnknown, /* default input device */
+ ptr::null_mut(),
+ )
+ };
+
+ let (output_device, output_stream_params) = if stream_type.contains(StreamType::OUTPUT) {
+ (
+ device_selector(Scope::Output),
+ &mut output_params as *mut ffi::cubeb_stream_params,
+ )
+ } else {
+ (
+ kAudioObjectUnknown, /* default output device */
+ ptr::null_mut(),
+ )
+ };
+
+ let stream_name = CString::new("stream tester").unwrap();
+
+ assert_eq!(
+ unsafe {
+ OPS.stream_init.unwrap()(
+ context_ptr,
+ stream_ptr,
+ stream_name.as_ptr(),
+ input_device as ffi::cubeb_devid,
+ input_stream_params,
+ output_device as ffi::cubeb_devid,
+ output_stream_params,
+ 4096, // latency
+ Some(data_callback),
+ Some(state_callback),
+ enable_loopback as *const AtomicBool as *mut c_void, // user pointer
+ )
+ },
+ ffi::CUBEB_OK
+ );
+ assert!(!stream_ptr.is_null());
+ println!("Stream {:p} created.", *stream_ptr);
+
+ extern "C" fn state_callback(
+ stream: *mut ffi::cubeb_stream,
+ _user_ptr: *mut c_void,
+ state: ffi::cubeb_state,
+ ) {
+ assert!(!stream.is_null());
+ let s = State::from(state);
+ println!("state: {:?}", s);
+ }
+
+ extern "C" fn data_callback(
+ stream: *mut ffi::cubeb_stream,
+ user_ptr: *mut c_void,
+ input_buffer: *const c_void,
+ output_buffer: *mut c_void,
+ nframes: i64,
+ ) -> i64 {
+ assert!(!stream.is_null());
+
+ let enable_loopback = unsafe { &mut *(user_ptr as *mut AtomicBool) };
+ let loopback = enable_loopback.load(Ordering::SeqCst);
+ if loopback && !input_buffer.is_null() && !output_buffer.is_null() {
+ // Dupe the mono input to stereo
+ let stm = unsafe { &mut *(stream as *mut AudioUnitStream) };
+ assert_eq!(stm.core_stream_data.input_stream_params.channels(), 1);
+ let channels = stm.core_stream_data.output_stream_params.channels() as usize;
+ let sample_size =
+ cubeb_sample_size(stm.core_stream_data.output_stream_params.format());
+ for f in 0..(nframes as usize) {
+ let input_offset = f * sample_size;
+ let output_offset = input_offset * channels;
+ for c in 0..channels {
+ unsafe {
+ ptr::copy(
+ input_buffer.add(input_offset) as *const u8,
+ output_buffer.add(output_offset + (sample_size * c)) as *mut u8,
+ sample_size,
+ )
+ };
+ }
+ }
+ } else if !output_buffer.is_null() {
+ // Feed silence data to output buffer
+ let stm = unsafe { &mut *(stream as *mut AudioUnitStream) };
+ let channels = stm.core_stream_data.output_stream_params.channels();
+ let samples = nframes as usize * channels as usize;
+ let sample_size =
+ cubeb_sample_size(stm.core_stream_data.output_stream_params.format());
+ unsafe {
+ ptr::write_bytes(output_buffer, 0, samples * sample_size);
+ }
+ }
+
+ nframes
+ }
+
+ fn get_dummy_stream_params(scope: Scope) -> ffi::cubeb_stream_params {
+ // The stream format for input and output must be same.
+ const STREAM_FORMAT: u32 = ffi::CUBEB_SAMPLE_FLOAT32NE;
+
+ // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
+ // (in the comments).
+ let mut stream_params = ffi::cubeb_stream_params::default();
+ stream_params.prefs = ffi::CUBEB_STREAM_PREF_VOICE;
+ let (format, rate, channels, layout) = match scope {
+ Scope::Input => (STREAM_FORMAT, 48000, 1, ffi::CUBEB_LAYOUT_MONO),
+ Scope::Output => (STREAM_FORMAT, 44100, 2, ffi::CUBEB_LAYOUT_STEREO),
+ };
+ stream_params.format = format;
+ stream_params.rate = rate;
+ stream_params.channels = channels;
+ stream_params.layout = layout;
+ stream_params
+ }
+ }
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs
new file mode 100644
index 0000000000..0c193d0dc8
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs
@@ -0,0 +1,12 @@
+use super::*;
+
+mod aggregate_device;
+mod api;
+mod backlog;
+mod device_change;
+mod device_property;
+mod interfaces;
+mod manual;
+mod parallel;
+mod tone;
+mod utils;
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs
new file mode 100644
index 0000000000..16063d0011
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs
@@ -0,0 +1,572 @@
+use super::utils::{
+ noop_data_callback, test_audiounit_get_buffer_frame_size, test_get_default_audiounit,
+ test_get_default_device, test_ops_context_operation, PropertyScope, Scope,
+};
+use super::*;
+use std::thread;
+
+// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
+// currently used by other streams in other tests.
+#[ignore]
+#[test]
+fn test_parallel_ops_init_streams_in_parallel_input() {
+ const THREADS: u32 = 50;
+ create_streams_by_ops_in_parallel_with_different_latency(
+ THREADS,
+ StreamType::Input,
+ |streams| {
+ // All the latency frames should be the same value as the first stream's one, since the
+ // latency frames cannot be changed if another stream is operating in parallel.
+ let mut latency_frames = vec![];
+ let mut in_buffer_frame_sizes = vec![];
+
+ for stream in streams {
+ latency_frames.push(stream.latency_frames);
+
+ assert!(!stream.core_stream_data.input_unit.is_null());
+ let in_buffer_frame_size = test_audiounit_get_buffer_frame_size(
+ stream.core_stream_data.input_unit,
+ Scope::Input,
+ PropertyScope::Output,
+ )
+ .unwrap();
+ in_buffer_frame_sizes.push(in_buffer_frame_size);
+
+ assert!(stream.core_stream_data.output_unit.is_null());
+ }
+
+ // Make sure all the latency frames are same as the first stream's one.
+ for i in 0..latency_frames.len() - 1 {
+ assert_eq!(latency_frames[i], latency_frames[i + 1]);
+ }
+
+ // Make sure all the buffer frame sizes on output scope of the input audiounit are same
+ // as the defined latency of the first initial stream.
+ for i in 0..in_buffer_frame_sizes.len() - 1 {
+ assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]);
+ }
+ },
+ );
+}
+
+// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
+// currently used by other streams in other tests.
+#[ignore]
+#[test]
+fn test_parallel_ops_init_streams_in_parallel_output() {
+ const THREADS: u32 = 50;
+ create_streams_by_ops_in_parallel_with_different_latency(
+ THREADS,
+ StreamType::Output,
+ |streams| {
+ // All the latency frames should be the same value as the first stream's one, since the
+ // latency frames cannot be changed if another stream is operating in parallel.
+ let mut latency_frames = vec![];
+ let mut out_buffer_frame_sizes = vec![];
+
+ for stream in streams {
+ latency_frames.push(stream.latency_frames);
+
+ assert!(stream.core_stream_data.input_unit.is_null());
+
+ assert!(!stream.core_stream_data.output_unit.is_null());
+ let out_buffer_frame_size = test_audiounit_get_buffer_frame_size(
+ stream.core_stream_data.output_unit,
+ Scope::Output,
+ PropertyScope::Input,
+ )
+ .unwrap();
+ out_buffer_frame_sizes.push(out_buffer_frame_size);
+ }
+
+ // Make sure all the latency frames are same as the first stream's one.
+ for i in 0..latency_frames.len() - 1 {
+ assert_eq!(latency_frames[i], latency_frames[i + 1]);
+ }
+
+ // Make sure all the buffer frame sizes on input scope of the output audiounit are same
+ // as the defined latency of the first initial stream.
+ for i in 0..out_buffer_frame_sizes.len() - 1 {
+ assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]);
+ }
+ },
+ );
+}
+
+// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
+// currently used by other streams in other tests.
+#[ignore]
+#[test]
+fn test_parallel_ops_init_streams_in_parallel_duplex() {
+ const THREADS: u32 = 50;
+ create_streams_by_ops_in_parallel_with_different_latency(
+ THREADS,
+ StreamType::Duplex,
+ |streams| {
+ // All the latency frames should be the same value as the first stream's one, since the
+ // latency frames cannot be changed if another stream is operating in parallel.
+ let mut latency_frames = vec![];
+ let mut in_buffer_frame_sizes = vec![];
+ let mut out_buffer_frame_sizes = vec![];
+
+ for stream in streams {
+ latency_frames.push(stream.latency_frames);
+
+ assert!(!stream.core_stream_data.input_unit.is_null());
+ let in_buffer_frame_size = test_audiounit_get_buffer_frame_size(
+ stream.core_stream_data.input_unit,
+ Scope::Input,
+ PropertyScope::Output,
+ )
+ .unwrap();
+ in_buffer_frame_sizes.push(in_buffer_frame_size);
+
+ assert!(!stream.core_stream_data.output_unit.is_null());
+ let out_buffer_frame_size = test_audiounit_get_buffer_frame_size(
+ stream.core_stream_data.output_unit,
+ Scope::Output,
+ PropertyScope::Input,
+ )
+ .unwrap();
+ out_buffer_frame_sizes.push(out_buffer_frame_size);
+ }
+
+ // Make sure all the latency frames are same as the first stream's one.
+ for i in 0..latency_frames.len() - 1 {
+ assert_eq!(latency_frames[i], latency_frames[i + 1]);
+ }
+
+ // Make sure all the buffer frame sizes on output scope of the input audiounit are same
+ // as the defined latency of the first initial stream.
+ for i in 0..in_buffer_frame_sizes.len() - 1 {
+ assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]);
+ }
+
+ // Make sure all the buffer frame sizes on input scope of the output audiounit are same
+ // as the defined latency of the first initial stream.
+ for i in 0..out_buffer_frame_sizes.len() - 1 {
+ assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]);
+ }
+ },
+ );
+}
+
+fn create_streams_by_ops_in_parallel_with_different_latency<F>(
+ amount: u32,
+ stm_type: StreamType,
+ callback: F,
+) where
+ F: FnOnce(Vec<&AudioUnitStream>),
+{
+ let default_input = test_get_default_device(Scope::Input);
+ let default_output = test_get_default_device(Scope::Output);
+
+ let has_input = stm_type == StreamType::Input || stm_type == StreamType::Duplex;
+ let has_output = stm_type == StreamType::Output || stm_type == StreamType::Duplex;
+
+ if has_input && default_input.is_none() {
+ println!("No input device to perform the test.");
+ return;
+ }
+
+ if has_output && default_output.is_none() {
+ println!("No output device to perform the test.");
+ return;
+ }
+
+ test_ops_context_operation("context: init and destroy", |context_ptr| {
+ let context_ptr_value = context_ptr as usize;
+
+ let mut join_handles = vec![];
+ for i in 0..amount {
+ // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
+ // (in the comments).
+ let mut input_params = ffi::cubeb_stream_params::default();
+ input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ input_params.rate = 48_000;
+ input_params.channels = 1;
+ input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ input_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ let mut output_params = ffi::cubeb_stream_params::default();
+ output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ output_params.rate = 44100;
+ output_params.channels = 2;
+ output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ // Latency cannot be changed if another stream is operating in parallel. All the latecy
+ // should be set to the same latency value of the first stream that is operating in the
+ // context.
+ let latency_frames = SAFE_MIN_LATENCY_FRAMES + i;
+ assert!(latency_frames < SAFE_MAX_LATENCY_FRAMES);
+
+ // Create many streams within the same context. The order of the stream creation
+ // is random (The order of execution of the spawned threads is random.).assert!
+ // It's super dangerous to pass `context_ptr_value` across threads and convert it back
+ // to a pointer. However, it's the cheapest way to make sure the inside mutex works.
+ let thread_name = format!("stream {} @ context {:?}", i, context_ptr);
+ join_handles.push(
+ thread::Builder::new()
+ .name(thread_name)
+ .spawn(move || {
+ let context_ptr = context_ptr_value as *mut ffi::cubeb;
+ let mut stream: *mut ffi::cubeb_stream = ptr::null_mut();
+ let stream_name = CString::new(format!("stream {}", i)).unwrap();
+ assert_eq!(
+ unsafe {
+ OPS.stream_init.unwrap()(
+ context_ptr,
+ &mut stream,
+ stream_name.as_ptr(),
+ ptr::null_mut(), // Use default input device.
+ if has_input {
+ &mut input_params
+ } else {
+ ptr::null_mut()
+ },
+ ptr::null_mut(), // Use default output device.
+ if has_output {
+ &mut output_params
+ } else {
+ ptr::null_mut()
+ },
+ latency_frames,
+ Some(noop_data_callback),
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ )
+ },
+ ffi::CUBEB_OK
+ );
+ assert!(!stream.is_null());
+ stream as usize
+ })
+ .unwrap(),
+ );
+ }
+
+ let mut streams = vec![];
+ // Wait for finishing the tasks on the different threads.
+ for handle in join_handles {
+ let stream_ptr_value = handle.join().unwrap();
+ let stream = unsafe { Box::from_raw(stream_ptr_value as *mut AudioUnitStream) };
+ streams.push(stream);
+ }
+
+ let stream_refs: Vec<&AudioUnitStream> = streams.iter().map(|stm| stm.as_ref()).collect();
+ callback(stream_refs);
+ });
+}
+
+// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
+// currently used by other streams in other tests.
+#[ignore]
+#[test]
+fn test_parallel_init_streams_in_parallel_input() {
+ const THREADS: u32 = 10;
+ create_streams_in_parallel_with_different_latency(THREADS, StreamType::Input, |streams| {
+ // All the latency frames should be the same value as the first stream's one, since the
+ // latency frames cannot be changed if another stream is operating in parallel.
+ let mut latency_frames = vec![];
+ let mut in_buffer_frame_sizes = vec![];
+
+ for stream in streams {
+ latency_frames.push(stream.latency_frames);
+
+ assert!(!stream.core_stream_data.input_unit.is_null());
+ let in_buffer_frame_size = test_audiounit_get_buffer_frame_size(
+ stream.core_stream_data.input_unit,
+ Scope::Input,
+ PropertyScope::Output,
+ )
+ .unwrap();
+ in_buffer_frame_sizes.push(in_buffer_frame_size);
+
+ assert!(stream.core_stream_data.output_unit.is_null());
+ }
+
+ // Make sure all the latency frames are same as the first stream's one.
+ for i in 0..latency_frames.len() - 1 {
+ assert_eq!(latency_frames[i], latency_frames[i + 1]);
+ }
+
+ // Make sure all the buffer frame sizes on output scope of the input audiounit are same
+ // as the defined latency of the first initial stream.
+ for i in 0..in_buffer_frame_sizes.len() - 1 {
+ assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]);
+ }
+ });
+}
+
+// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
+// currently used by other streams in other tests.
+#[ignore]
+#[test]
+fn test_parallel_init_streams_in_parallel_output() {
+ const THREADS: u32 = 10;
+ create_streams_in_parallel_with_different_latency(THREADS, StreamType::Output, |streams| {
+ // All the latency frames should be the same value as the first stream's one, since the
+ // latency frames cannot be changed if another stream is operating in parallel.
+ let mut latency_frames = vec![];
+ let mut out_buffer_frame_sizes = vec![];
+
+ for stream in streams {
+ latency_frames.push(stream.latency_frames);
+
+ assert!(stream.core_stream_data.input_unit.is_null());
+
+ assert!(!stream.core_stream_data.output_unit.is_null());
+ let out_buffer_frame_size = test_audiounit_get_buffer_frame_size(
+ stream.core_stream_data.output_unit,
+ Scope::Output,
+ PropertyScope::Input,
+ )
+ .unwrap();
+ out_buffer_frame_sizes.push(out_buffer_frame_size);
+ }
+
+ // Make sure all the latency frames are same as the first stream's one.
+ for i in 0..latency_frames.len() - 1 {
+ assert_eq!(latency_frames[i], latency_frames[i + 1]);
+ }
+
+ // Make sure all the buffer frame sizes on input scope of the output audiounit are same
+ // as the defined latency of the first initial stream.
+ for i in 0..out_buffer_frame_sizes.len() - 1 {
+ assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]);
+ }
+ });
+}
+
+// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
+// currently used by other streams in other tests.
+#[ignore]
+#[test]
+fn test_parallel_init_streams_in_parallel_duplex() {
+ const THREADS: u32 = 10;
+ create_streams_in_parallel_with_different_latency(THREADS, StreamType::Duplex, |streams| {
+ // All the latency frames should be the same value as the first stream's one, since the
+ // latency frames cannot be changed if another stream is operating in parallel.
+ let mut latency_frames = vec![];
+ let mut in_buffer_frame_sizes = vec![];
+ let mut out_buffer_frame_sizes = vec![];
+
+ for stream in streams {
+ latency_frames.push(stream.latency_frames);
+
+ assert!(!stream.core_stream_data.input_unit.is_null());
+ let in_buffer_frame_size = test_audiounit_get_buffer_frame_size(
+ stream.core_stream_data.input_unit,
+ Scope::Input,
+ PropertyScope::Output,
+ )
+ .unwrap();
+ in_buffer_frame_sizes.push(in_buffer_frame_size);
+
+ assert!(!stream.core_stream_data.output_unit.is_null());
+ let out_buffer_frame_size = test_audiounit_get_buffer_frame_size(
+ stream.core_stream_data.output_unit,
+ Scope::Output,
+ PropertyScope::Input,
+ )
+ .unwrap();
+ out_buffer_frame_sizes.push(out_buffer_frame_size);
+ }
+
+ // Make sure all the latency frames are same as the first stream's one.
+ for i in 0..latency_frames.len() - 1 {
+ assert_eq!(latency_frames[i], latency_frames[i + 1]);
+ }
+
+ // Make sure all the buffer frame sizes on output scope of the input audiounit are same
+ // as the defined latency of the first initial stream.
+ for i in 0..in_buffer_frame_sizes.len() - 1 {
+ assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]);
+ }
+
+ // Make sure all the buffer frame sizes on input scope of the output audiounit are same
+ // as the defined latency of the first initial stream.
+ for i in 0..out_buffer_frame_sizes.len() - 1 {
+ assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]);
+ }
+ });
+}
+
+fn create_streams_in_parallel_with_different_latency<F>(
+ amount: u32,
+ stm_type: StreamType,
+ callback: F,
+) where
+ F: FnOnce(Vec<&AudioUnitStream>),
+{
+ let default_input = test_get_default_device(Scope::Input);
+ let default_output = test_get_default_device(Scope::Output);
+
+ let has_input = stm_type == StreamType::Input || stm_type == StreamType::Duplex;
+ let has_output = stm_type == StreamType::Output || stm_type == StreamType::Duplex;
+
+ if has_input && default_input.is_none() {
+ println!("No input device to perform the test.");
+ return;
+ }
+
+ if has_output && default_output.is_none() {
+ println!("No output device to perform the test.");
+ return;
+ }
+
+ let mut context = AudioUnitContext::new();
+
+ let context_ptr_value = &mut context as *mut AudioUnitContext as usize;
+
+ let mut join_handles = vec![];
+ for i in 0..amount {
+ // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
+ // (in the comments).
+ let mut input_params = ffi::cubeb_stream_params::default();
+ input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ input_params.rate = 48_000;
+ input_params.channels = 1;
+ input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ input_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ let mut output_params = ffi::cubeb_stream_params::default();
+ output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
+ output_params.rate = 44100;
+ output_params.channels = 2;
+ output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
+ output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ // Latency cannot be changed if another stream is operating in parallel. All the latecy
+ // should be set to the same latency value of the first stream that is operating in the
+ // context.
+ let latency_frames = SAFE_MIN_LATENCY_FRAMES + i;
+ assert!(latency_frames < SAFE_MAX_LATENCY_FRAMES);
+
+ // Create many streams within the same context. The order of the stream creation
+ // is random. (The order of execution of the spawned threads is random.)
+ // It's super dangerous to pass `context_ptr_value` across threads and convert it back
+ // to a reference. However, it's the cheapest way to make sure the inside mutex works.
+ let thread_name = format!("stream {} @ context {:?}", i, context_ptr_value);
+ join_handles.push(
+ thread::Builder::new()
+ .name(thread_name)
+ .spawn(move || {
+ let context = unsafe { &mut *(context_ptr_value as *mut AudioUnitContext) };
+ let input_params = unsafe { StreamParamsRef::from_ptr(&mut input_params) };
+ let output_params = unsafe { StreamParamsRef::from_ptr(&mut output_params) };
+ let stream = context
+ .stream_init(
+ None,
+ ptr::null_mut(), // Use default input device.
+ if has_input { Some(input_params) } else { None },
+ ptr::null_mut(), // Use default output device.
+ if has_output {
+ Some(output_params)
+ } else {
+ None
+ },
+ latency_frames,
+ Some(noop_data_callback),
+ None, // No state callback.
+ ptr::null_mut(), // No user data pointer.
+ )
+ .unwrap();
+ assert!(!stream.as_ptr().is_null());
+ let stream_ptr_value = stream.as_ptr() as usize;
+ // Prevent the stream from being destroyed by leaking this stream.
+ mem::forget(stream);
+ stream_ptr_value
+ })
+ .unwrap(),
+ );
+ }
+
+ let mut streams = vec![];
+ // Wait for finishing the tasks on the different threads.
+ for handle in join_handles {
+ let stream_ptr_value = handle.join().unwrap();
+ // Retake the leaked stream.
+ let stream = unsafe { Box::from_raw(stream_ptr_value as *mut AudioUnitStream) };
+ streams.push(stream);
+ }
+
+ let stream_refs: Vec<&AudioUnitStream> = streams.iter().map(|stm| stm.as_ref()).collect();
+ callback(stream_refs);
+}
+
+#[derive(Debug, PartialEq)]
+enum StreamType {
+ Input,
+ Output,
+ Duplex,
+}
+
+// This is used to interfere other active streams.
+// From this testing, it's ok to set the buffer frame size of a device that is currently used by
+// other tests. It works on OSX 10.13, not sure if it works on other versions.
+// However, other tests may check the buffer frame size they set at the same time,
+// so we ignore this by default incase those checks fail.
+#[ignore]
+#[test]
+fn test_set_buffer_frame_size_in_parallel() {
+ test_set_buffer_frame_size_in_parallel_in_scope(Scope::Input);
+ test_set_buffer_frame_size_in_parallel_in_scope(Scope::Output);
+}
+
+fn test_set_buffer_frame_size_in_parallel_in_scope(scope: Scope) {
+ const THREADS: u32 = 100;
+
+ let unit = test_get_default_audiounit(scope.clone());
+ if unit.is_none() {
+ println!("No unit for {:?}", scope);
+ return;
+ }
+
+ let (unit_scope, unit_element, prop_scope) = match scope {
+ Scope::Input => (kAudioUnitScope_Output, AU_IN_BUS, PropertyScope::Output),
+ Scope::Output => (kAudioUnitScope_Input, AU_OUT_BUS, PropertyScope::Input),
+ };
+
+ let mut units = vec![];
+ let mut join_handles = vec![];
+ for i in 0..THREADS {
+ let latency_frames = SAFE_MIN_LATENCY_FRAMES + i;
+ assert!(latency_frames < SAFE_MAX_LATENCY_FRAMES);
+ units.push(test_get_default_audiounit(scope.clone()).unwrap());
+ let unit_value = units.last().unwrap().get_inner() as usize;
+ join_handles.push(thread::spawn(move || {
+ let status = audio_unit_set_property(
+ unit_value as AudioUnit,
+ kAudioDevicePropertyBufferFrameSize,
+ unit_scope,
+ unit_element,
+ &latency_frames,
+ mem::size_of::<u32>(),
+ );
+ (latency_frames, status)
+ }));
+ }
+
+ let mut latencies = vec![];
+ let mut statuses = vec![];
+ for handle in join_handles {
+ let (latency, status) = handle.join().unwrap();
+ latencies.push(latency);
+ statuses.push(status);
+ }
+
+ let mut buffer_frames_list = vec![];
+ for unit in units.iter() {
+ buffer_frames_list.push(unit.get_buffer_frame_size(scope.clone(), prop_scope.clone()));
+ }
+
+ for status in statuses {
+ assert_eq!(status, NO_ERR);
+ }
+
+ for i in 0..buffer_frames_list.len() - 1 {
+ assert_eq!(buffer_frames_list[i], buffer_frames_list[i + 1]);
+ }
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs
new file mode 100644
index 0000000000..42cb9ee997
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs
@@ -0,0 +1,215 @@
+use super::utils::{test_get_default_device, test_ops_stream_operation, Scope};
+use super::*;
+use std::sync::atomic::{AtomicI64, Ordering};
+
+#[test]
+fn test_dial_tone() {
+ use std::f32::consts::PI;
+ use std::thread;
+ use std::time::Duration;
+
+ const SAMPLE_FREQUENCY: u32 = 48_000;
+
+ // Do nothing if there is no available output device.
+ if test_get_default_device(Scope::Output).is_none() {
+ println!("No output device.");
+ return;
+ }
+
+ // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
+ // (in the comments).
+ let mut output_params = ffi::cubeb_stream_params::default();
+ output_params.format = ffi::CUBEB_SAMPLE_S16NE;
+ output_params.rate = SAMPLE_FREQUENCY;
+ output_params.channels = 1;
+ output_params.layout = ffi::CUBEB_LAYOUT_MONO;
+ output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+
+ struct Closure {
+ buffer_size: AtomicI64,
+ phase: i64,
+ }
+ let mut closure = Closure {
+ buffer_size: AtomicI64::new(0),
+ phase: 0,
+ };
+ let closure_ptr = &mut closure as *mut Closure as *mut c_void;
+
+ test_ops_stream_operation(
+ "stream: North American dial tone",
+ ptr::null_mut(), // Use default input device.
+ ptr::null_mut(), // No input parameters.
+ ptr::null_mut(), // Use default output device.
+ &mut output_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(data_callback),
+ Some(state_callback),
+ closure_ptr,
+ |stream| {
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+
+ #[derive(Debug)]
+ enum State {
+ WaitingForStart,
+ PositionIncreasing,
+ Paused,
+ Resumed,
+ End,
+ }
+ let mut state = State::WaitingForStart;
+ let mut position: u64 = 0;
+ let mut prev_position: u64 = 0;
+ let mut count = 0;
+ const CHECK_COUNT: i32 = 10;
+ loop {
+ thread::sleep(Duration::from_millis(50));
+ assert_eq!(
+ unsafe { OPS.stream_get_position.unwrap()(stream, &mut position) },
+ ffi::CUBEB_OK
+ );
+ println!(
+ "State: {:?}, position: {}, previous position: {}",
+ state, position, prev_position
+ );
+ match &mut state {
+ State::WaitingForStart => {
+ // It's expected to have 0 for a few iterations here: the stream can take
+ // some time to start.
+ if position != prev_position {
+ assert!(position > prev_position);
+ prev_position = position;
+ state = State::PositionIncreasing;
+ }
+ }
+ State::PositionIncreasing => {
+ // wait a few iterations, check monotony
+ if position != prev_position {
+ assert!(position > prev_position);
+ prev_position = position;
+ count += 1;
+ if count > CHECK_COUNT {
+ state = State::Paused;
+ count = 0;
+ assert_eq!(
+ unsafe { OPS.stream_stop.unwrap()(stream) },
+ ffi::CUBEB_OK
+ );
+ // Update the position once paused.
+ assert_eq!(
+ unsafe {
+ OPS.stream_get_position.unwrap()(stream, &mut position)
+ },
+ ffi::CUBEB_OK
+ );
+ prev_position = position;
+ }
+ }
+ }
+ State::Paused => {
+ // The cubeb_stream_stop call above should synchrously stop the callbacks,
+ // hence the clock, the assert below must always holds, modulo the client
+ // side interpolation.
+ assert!(
+ position == prev_position
+ || position - prev_position
+ <= closure.buffer_size.load(Ordering::SeqCst) as u64
+ );
+ count += 1;
+ prev_position = position;
+ if count > CHECK_COUNT {
+ state = State::Resumed;
+ count = 0;
+ assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
+ }
+ }
+ State::Resumed => {
+ // wait a few iterations, this can take some time to start
+ if position != prev_position {
+ assert!(position > prev_position);
+ prev_position = position;
+ count += 1;
+ if count > CHECK_COUNT {
+ state = State::End;
+ count = 0;
+ assert_eq!(
+ unsafe { OPS.stream_stop.unwrap()(stream) },
+ ffi::CUBEB_OK
+ );
+ assert_eq!(
+ unsafe {
+ OPS.stream_get_position.unwrap()(stream, &mut position)
+ },
+ ffi::CUBEB_OK
+ );
+ prev_position = position;
+ }
+ }
+ }
+ State::End => {
+ // The cubeb_stream_stop call above should synchrously stop the callbacks,
+ // hence the clock, the assert below must always holds, modulo the client
+ // side interpolation.
+ assert!(
+ position == prev_position
+ || position - prev_position
+ <= closure.buffer_size.load(Ordering::SeqCst) as u64
+ );
+ if position == prev_position {
+ count += 1;
+ if count > CHECK_COUNT {
+ break;
+ }
+ }
+ }
+ }
+ }
+ assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK);
+ },
+ );
+
+ extern "C" fn state_callback(
+ stream: *mut ffi::cubeb_stream,
+ user_ptr: *mut c_void,
+ state: ffi::cubeb_state,
+ ) {
+ assert!(!stream.is_null());
+ assert!(!user_ptr.is_null());
+ assert_ne!(state, ffi::CUBEB_STATE_ERROR);
+ }
+
+ extern "C" fn data_callback(
+ stream: *mut ffi::cubeb_stream,
+ user_ptr: *mut c_void,
+ _input_buffer: *const c_void,
+ output_buffer: *mut c_void,
+ nframes: i64,
+ ) -> i64 {
+ assert!(!stream.is_null());
+ assert!(!user_ptr.is_null());
+ assert!(!output_buffer.is_null());
+
+ let buffer = unsafe {
+ let ptr = output_buffer as *mut i16;
+ let len = nframes as usize;
+ slice::from_raw_parts_mut(ptr, len)
+ };
+
+ let closure = unsafe { &mut *(user_ptr as *mut Closure) };
+
+ closure.buffer_size.store(nframes, Ordering::SeqCst);
+
+ // Generate tone on the fly.
+ for data in buffer.iter_mut() {
+ let t1 = (2.0 * PI * 350.0 * (closure.phase) as f32 / SAMPLE_FREQUENCY as f32).sin();
+ let t2 = (2.0 * PI * 440.0 * (closure.phase) as f32 / SAMPLE_FREQUENCY as f32).sin();
+ *data = f32_to_i16_sample(0.5 * (t1 + t2));
+ closure.phase += 1;
+ }
+
+ nframes
+ }
+
+ fn f32_to_i16_sample(x: f32) -> i16 {
+ (x * f32::from(i16::max_value())) as i16
+ }
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs
new file mode 100644
index 0000000000..ef07aeeeb4
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs
@@ -0,0 +1,1247 @@
+use super::*;
+
+// Common Utils
+// ------------------------------------------------------------------------------------------------
+pub extern "C" fn noop_data_callback(
+ stream: *mut ffi::cubeb_stream,
+ _user_ptr: *mut c_void,
+ _input_buffer: *const c_void,
+ output_buffer: *mut c_void,
+ nframes: i64,
+) -> i64 {
+ assert!(!stream.is_null());
+
+ // Feed silence data to output buffer
+ if !output_buffer.is_null() {
+ let stm = unsafe { &mut *(stream as *mut AudioUnitStream) };
+ let channels = stm.core_stream_data.output_stream_params.channels();
+ let samples = nframes as usize * channels as usize;
+ let sample_size = cubeb_sample_size(stm.core_stream_data.output_stream_params.format());
+ unsafe {
+ ptr::write_bytes(output_buffer, 0, samples * sample_size);
+ }
+ }
+
+ nframes
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Scope {
+ Input,
+ Output,
+}
+
+impl From<Scope> for DeviceType {
+ fn from(scope: Scope) -> Self {
+ match scope {
+ Scope::Input => DeviceType::INPUT,
+ Scope::Output => DeviceType::OUTPUT,
+ }
+ }
+}
+
+#[derive(Clone)]
+pub enum PropertyScope {
+ Input,
+ Output,
+}
+
+pub fn test_get_default_device(scope: Scope) -> Option<AudioObjectID> {
+ let address = AudioObjectPropertyAddress {
+ mSelector: match scope {
+ Scope::Input => kAudioHardwarePropertyDefaultInputDevice,
+ Scope::Output => kAudioHardwarePropertyDefaultOutputDevice,
+ },
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut devid: AudioObjectID = kAudioObjectUnknown;
+ let mut size = mem::size_of::<AudioObjectID>();
+ let status = unsafe {
+ AudioObjectGetPropertyData(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut UInt32,
+ &mut devid as *mut AudioObjectID as *mut c_void,
+ )
+ };
+ if status != NO_ERR || devid == kAudioObjectUnknown {
+ return None;
+ }
+ Some(devid)
+}
+
+// TODO: Create a GetProperty trait and add a default implementation for it, then implement it
+// for TestAudioUnit so the member method like `get_buffer_frame_size` can reuse the trait
+// method get_property_data.
+#[derive(Debug)]
+pub struct TestAudioUnit(AudioUnit);
+
+impl TestAudioUnit {
+ fn new(unit: AudioUnit) -> Self {
+ assert!(!unit.is_null());
+ Self(unit)
+ }
+ pub fn get_inner(&self) -> AudioUnit {
+ self.0
+ }
+ pub fn get_buffer_frame_size(
+ &self,
+ scope: Scope,
+ prop_scope: PropertyScope,
+ ) -> std::result::Result<u32, OSStatus> {
+ test_audiounit_get_buffer_frame_size(self.0, scope, prop_scope)
+ }
+}
+
+impl Drop for TestAudioUnit {
+ fn drop(&mut self) {
+ unsafe {
+ AudioUnitUninitialize(self.0);
+ AudioComponentInstanceDispose(self.0);
+ }
+ }
+}
+
+// TODO: 1. Return Result with custom errors.
+// 2. Allow to create a in-out unit.
+pub fn test_get_default_audiounit(scope: Scope) -> Option<TestAudioUnit> {
+ let device = test_get_default_device(scope.clone());
+ let unit = test_create_audiounit(ComponentSubType::HALOutput);
+ if device.is_none() || unit.is_none() {
+ return None;
+ }
+ let unit = unit.unwrap();
+ let device = device.unwrap();
+ match scope {
+ Scope::Input => {
+ if test_enable_audiounit_in_scope(unit.get_inner(), Scope::Input, true).is_err()
+ || test_enable_audiounit_in_scope(unit.get_inner(), Scope::Output, false).is_err()
+ {
+ return None;
+ }
+ }
+ Scope::Output => {
+ if test_enable_audiounit_in_scope(unit.get_inner(), Scope::Input, false).is_err()
+ || test_enable_audiounit_in_scope(unit.get_inner(), Scope::Output, true).is_err()
+ {
+ return None;
+ }
+ }
+ }
+
+ let status = unsafe {
+ AudioUnitSetProperty(
+ unit.get_inner(),
+ kAudioOutputUnitProperty_CurrentDevice,
+ kAudioUnitScope_Global,
+ 0, // Global bus
+ &device as *const AudioObjectID as *const c_void,
+ mem::size_of::<AudioObjectID>() as u32,
+ )
+ };
+ if status == NO_ERR {
+ Some(unit)
+ } else {
+ None
+ }
+}
+
+pub enum ComponentSubType {
+ HALOutput,
+ DefaultOutput,
+}
+
+// TODO: Return Result with custom errors.
+// Surprisingly the AudioUnit can be created even when there is no any device on the platform,
+// no matter its subtype is HALOutput or DefaultOutput.
+pub fn test_create_audiounit(unit_type: ComponentSubType) -> Option<TestAudioUnit> {
+ let desc = AudioComponentDescription {
+ componentType: kAudioUnitType_Output,
+ componentSubType: match unit_type {
+ ComponentSubType::HALOutput => kAudioUnitSubType_HALOutput,
+ ComponentSubType::DefaultOutput => kAudioUnitSubType_DefaultOutput,
+ },
+ componentManufacturer: kAudioUnitManufacturer_Apple,
+ componentFlags: 0,
+ componentFlagsMask: 0,
+ };
+ let comp = unsafe { AudioComponentFindNext(ptr::null_mut(), &desc) };
+ if comp.is_null() {
+ return None;
+ }
+ let mut unit: AudioUnit = ptr::null_mut();
+ let status = unsafe { AudioComponentInstanceNew(comp, &mut unit) };
+ // TODO: Is unit possible to be null when no error returns ?
+ if status != NO_ERR || unit.is_null() {
+ None
+ } else {
+ Some(TestAudioUnit::new(unit))
+ }
+}
+
+fn test_enable_audiounit_in_scope(
+ unit: AudioUnit,
+ scope: Scope,
+ enable: bool,
+) -> std::result::Result<(), OSStatus> {
+ assert!(!unit.is_null());
+ let (scope, element) = match scope {
+ Scope::Input => (kAudioUnitScope_Input, AU_IN_BUS),
+ Scope::Output => (kAudioUnitScope_Output, AU_OUT_BUS),
+ };
+ let on_off: u32 = if enable { 1 } else { 0 };
+ let status = unsafe {
+ AudioUnitSetProperty(
+ unit,
+ kAudioOutputUnitProperty_EnableIO,
+ scope,
+ element,
+ &on_off as *const u32 as *const c_void,
+ mem::size_of::<u32>() as u32,
+ )
+ };
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(status)
+ }
+}
+
+pub enum DeviceFilter {
+ ExcludeCubebAggregateAndVPIO,
+ IncludeAll,
+}
+pub fn test_get_all_devices(filter: DeviceFilter) -> Vec<AudioObjectID> {
+ let mut devices = Vec::new();
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioHardwarePropertyDevices,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+ let mut size: usize = 0;
+ let status = unsafe {
+ AudioObjectGetPropertyDataSize(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ // size will be 0 if there is no device at all.
+ if status != NO_ERR || size == 0 {
+ return devices;
+ }
+ assert_eq!(size % mem::size_of::<AudioObjectID>(), 0);
+ let elements = size / mem::size_of::<AudioObjectID>();
+ devices.resize(elements, kAudioObjectUnknown);
+ let status = unsafe {
+ AudioObjectGetPropertyData(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ devices.as_mut_ptr() as *mut c_void,
+ )
+ };
+ if status != NO_ERR {
+ devices.clear();
+ return devices;
+ }
+ for device in devices.iter() {
+ assert_ne!(*device, kAudioObjectUnknown);
+ }
+
+ match filter {
+ DeviceFilter::ExcludeCubebAggregateAndVPIO => {
+ devices.retain(|&device| {
+ if let Ok(uid) = get_device_global_uid(device) {
+ let uid = uid.into_string();
+ !uid.contains(PRIVATE_AGGREGATE_DEVICE_NAME)
+ && !uid.contains(VOICEPROCESSING_AGGREGATE_DEVICE_NAME)
+ } else {
+ true
+ }
+ });
+ }
+ _ => {}
+ }
+
+ devices
+}
+
+pub fn test_get_devices_in_scope(scope: Scope) -> Vec<AudioObjectID> {
+ let mut devices = test_get_all_devices(DeviceFilter::ExcludeCubebAggregateAndVPIO);
+ devices.retain(|device| test_device_in_scope(*device, scope.clone()));
+ devices
+}
+
+pub fn get_devices_info_in_scope(scope: Scope) -> Vec<TestDeviceInfo> {
+ fn print_info(info: &TestDeviceInfo) {
+ println!("{:>4}: {}\n\tuid: {}", info.id, info.label, info.uid);
+ }
+
+ println!(
+ "\n{:?} devices\n\
+ --------------------",
+ scope
+ );
+
+ let mut infos = vec![];
+ let devices = test_get_devices_in_scope(scope.clone());
+ for device in devices {
+ infos.push(TestDeviceInfo::new(device, scope.clone()));
+ print_info(infos.last().unwrap());
+ }
+ println!();
+
+ infos
+}
+
+#[derive(Debug)]
+pub struct TestDeviceInfo {
+ pub id: AudioObjectID,
+ pub label: String,
+ pub uid: String,
+}
+impl TestDeviceInfo {
+ pub fn new(id: AudioObjectID, scope: Scope) -> Self {
+ Self {
+ id,
+ label: Self::get_label(id, scope.clone()),
+ uid: Self::get_uid(id, scope),
+ }
+ }
+
+ fn get_label(id: AudioObjectID, scope: Scope) -> String {
+ match get_device_uid(id, scope.into()) {
+ Ok(uid) => uid.into_string(),
+ Err(status) => format!("Unknow. Error: {}", status).to_string(),
+ }
+ }
+
+ fn get_uid(id: AudioObjectID, scope: Scope) -> String {
+ match get_device_label(id, scope.into()) {
+ Ok(label) => label.into_string(),
+ Err(status) => format!("Unknown. Error: {}", status).to_string(),
+ }
+ }
+}
+
+pub fn test_device_channels_in_scope(
+ id: AudioObjectID,
+ scope: Scope,
+) -> std::result::Result<u32, OSStatus> {
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioDevicePropertyStreamConfiguration,
+ mScope: match scope {
+ Scope::Input => kAudioDevicePropertyScopeInput,
+ Scope::Output => kAudioDevicePropertyScopeOutput,
+ },
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+ let mut size: usize = 0;
+ let status = unsafe {
+ AudioObjectGetPropertyDataSize(
+ id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ if status != NO_ERR {
+ return Err(status);
+ }
+ if size == 0 {
+ return Ok(0);
+ }
+ let byte_len = size / mem::size_of::<u8>();
+ let mut bytes = vec![0u8; byte_len];
+ let status = unsafe {
+ AudioObjectGetPropertyData(
+ id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ bytes.as_mut_ptr() as *mut c_void,
+ )
+ };
+ if status != NO_ERR {
+ return Err(status);
+ }
+ let buf_list = unsafe { &*(bytes.as_mut_ptr() as *mut AudioBufferList) };
+ let buf_len = buf_list.mNumberBuffers as usize;
+ if buf_len == 0 {
+ return Ok(0);
+ }
+ let buf_ptr = buf_list.mBuffers.as_ptr() as *const AudioBuffer;
+ let buffers = unsafe { slice::from_raw_parts(buf_ptr, buf_len) };
+ let mut channels: u32 = 0;
+ for buffer in buffers {
+ channels += buffer.mNumberChannels;
+ }
+ Ok(channels)
+}
+
+pub fn test_device_in_scope(id: AudioObjectID, scope: Scope) -> bool {
+ let channels = test_device_channels_in_scope(id, scope);
+ channels.is_ok() && channels.unwrap() > 0
+}
+
+pub fn test_get_all_onwed_devices(id: AudioDeviceID) -> Vec<AudioObjectID> {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioObjectPropertyOwnedObjects,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let qualifier_data_size = mem::size_of::<AudioObjectID>();
+ let class_id: AudioClassID = kAudioSubDeviceClassID;
+ let qualifier_data = &class_id;
+ let mut size: usize = 0;
+
+ unsafe {
+ assert_eq!(
+ AudioObjectGetPropertyDataSize(
+ id,
+ &address,
+ qualifier_data_size as u32,
+ qualifier_data as *const u32 as *const c_void,
+ &mut size as *mut usize as *mut u32
+ ),
+ NO_ERR
+ );
+ }
+ assert_ne!(size, 0);
+
+ let elements = size / mem::size_of::<AudioObjectID>();
+ let mut devices: Vec<AudioObjectID> = allocate_array(elements);
+
+ unsafe {
+ assert_eq!(
+ AudioObjectGetPropertyData(
+ id,
+ &address,
+ qualifier_data_size as u32,
+ qualifier_data as *const u32 as *const c_void,
+ &mut size as *mut usize as *mut u32,
+ devices.as_mut_ptr() as *mut c_void
+ ),
+ NO_ERR
+ );
+ }
+
+ devices
+}
+
+pub fn test_get_master_device(id: AudioObjectID) -> String {
+ assert_ne!(id, kAudioObjectUnknown);
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioAggregateDevicePropertyMasterSubDevice,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut master: CFStringRef = ptr::null_mut();
+ let mut size = mem::size_of::<CFStringRef>();
+ assert_eq!(
+ audio_object_get_property_data(id, &address, &mut size, &mut master),
+ NO_ERR
+ );
+ assert!(!master.is_null());
+
+ let master = StringRef::new(master as _);
+ master.into_string()
+}
+
+pub fn test_get_drift_compensations(id: AudioObjectID) -> std::result::Result<u32, OSStatus> {
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioSubDevicePropertyDriftCompensation,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+ let mut size = mem::size_of::<u32>();
+ let mut compensation = u32::max_value();
+ let status = unsafe {
+ AudioObjectGetPropertyData(
+ id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ &mut compensation as *mut u32 as *mut c_void,
+ )
+ };
+ if status == NO_ERR {
+ Ok(compensation)
+ } else {
+ Err(status)
+ }
+}
+
+pub fn test_audiounit_scope_is_enabled(unit: AudioUnit, scope: Scope) -> bool {
+ assert!(!unit.is_null());
+ let mut has_io: UInt32 = 0;
+ let (scope, element) = match scope {
+ Scope::Input => (kAudioUnitScope_Input, AU_IN_BUS),
+ Scope::Output => (kAudioUnitScope_Output, AU_OUT_BUS),
+ };
+ let mut size = mem::size_of::<UInt32>();
+ assert_eq!(
+ audio_unit_get_property(
+ unit,
+ kAudioOutputUnitProperty_HasIO,
+ scope,
+ element,
+ &mut has_io,
+ &mut size
+ ),
+ NO_ERR
+ );
+ has_io != 0
+}
+
+pub fn test_audiounit_get_buffer_frame_size(
+ unit: AudioUnit,
+ scope: Scope,
+ prop_scope: PropertyScope,
+) -> std::result::Result<u32, OSStatus> {
+ let element = match scope {
+ Scope::Input => AU_IN_BUS,
+ Scope::Output => AU_OUT_BUS,
+ };
+ let prop_scope = match prop_scope {
+ PropertyScope::Input => kAudioUnitScope_Input,
+ PropertyScope::Output => kAudioUnitScope_Output,
+ };
+ let mut buffer_frames: u32 = 0;
+ let mut size = mem::size_of::<u32>();
+ let status = unsafe {
+ AudioUnitGetProperty(
+ unit,
+ kAudioDevicePropertyBufferFrameSize,
+ prop_scope,
+ element,
+ &mut buffer_frames as *mut u32 as *mut c_void,
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ if status == NO_ERR {
+ Ok(buffer_frames)
+ } else {
+ Err(status)
+ }
+}
+
+// Surprisingly it's ok to set
+// 1. a unknown device
+// 2. a non-input/non-output device
+// 3. the current default input/output device
+// as the new default input/output device by apple's API. We need to check the above things by ourselves.
+// This function returns an Ok containing the previous default device id on success.
+// Otherwise, it returns an Err containing the error code with OSStatus type
+pub fn test_set_default_device(
+ device: AudioObjectID,
+ scope: Scope,
+) -> std::result::Result<AudioObjectID, OSStatus> {
+ assert!(test_device_in_scope(device, scope.clone()));
+ let default = test_get_default_device(scope.clone()).unwrap();
+ if default == device {
+ // Do nothing if device is already the default device
+ return Ok(device);
+ }
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: match scope {
+ Scope::Input => kAudioHardwarePropertyDefaultInputDevice,
+ Scope::Output => kAudioHardwarePropertyDefaultOutputDevice,
+ },
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+ let size = mem::size_of::<AudioObjectID>();
+ let status = unsafe {
+ AudioObjectSetPropertyData(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ size as u32,
+ &device as *const AudioObjectID as *const c_void,
+ )
+ };
+ let new_default = test_get_default_device(scope.clone()).unwrap();
+ if new_default == default {
+ Err(-1)
+ } else if status == NO_ERR {
+ Ok(default)
+ } else {
+ Err(status)
+ }
+}
+
+pub struct TestDeviceSwitcher {
+ scope: Scope,
+ devices: Vec<AudioObjectID>,
+ current_device_index: usize,
+}
+
+impl TestDeviceSwitcher {
+ pub fn new(scope: Scope) -> Self {
+ let infos = get_devices_info_in_scope(scope.clone());
+ let devices: Vec<AudioObjectID> = infos.into_iter().map(|info| info.id).collect();
+ let current = test_get_default_device(scope.clone()).unwrap();
+ let index = devices
+ .iter()
+ .position(|device| *device == current)
+ .unwrap();
+ Self {
+ scope,
+ devices,
+ current_device_index: index,
+ }
+ }
+
+ pub fn next(&mut self) {
+ let current = self.devices[self.current_device_index];
+ let next_index = (self.current_device_index + 1) % self.devices.len();
+ let next = self.devices[next_index];
+ println!(
+ "Switch device for {:?}: {} -> {}",
+ self.scope, current, next
+ );
+ match self.set_device(next) {
+ Ok(prev) => {
+ assert_eq!(prev, current);
+ self.current_device_index = next_index;
+ }
+ _ => {
+ self.devices.remove(next_index);
+ if next_index < self.current_device_index {
+ self.current_device_index -= 1;
+ }
+ self.next();
+ }
+ }
+ }
+
+ fn set_device(&self, device: AudioObjectID) -> std::result::Result<AudioObjectID, OSStatus> {
+ test_set_default_device(device, self.scope.clone())
+ }
+}
+
+pub fn test_create_device_change_listener<F>(scope: Scope, listener: F) -> TestPropertyListener<F>
+where
+ F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
+{
+ let address = AudioObjectPropertyAddress {
+ mSelector: match scope {
+ Scope::Input => kAudioHardwarePropertyDefaultInputDevice,
+ Scope::Output => kAudioHardwarePropertyDefaultOutputDevice,
+ },
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+ TestPropertyListener::new(kAudioObjectSystemObject, address, listener)
+}
+
+pub struct TestPropertyListener<F>
+where
+ F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
+{
+ device: AudioObjectID,
+ property: AudioObjectPropertyAddress,
+ callback: F,
+}
+
+impl<F> TestPropertyListener<F>
+where
+ F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
+{
+ pub fn new(device: AudioObjectID, property: AudioObjectPropertyAddress, callback: F) -> Self {
+ Self {
+ device,
+ property,
+ callback,
+ }
+ }
+
+ pub fn start(&self) -> std::result::Result<(), OSStatus> {
+ let status = unsafe {
+ AudioObjectAddPropertyListener(
+ self.device,
+ &self.property,
+ Some(Self::render),
+ self as *const Self as *mut c_void,
+ )
+ };
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(status)
+ }
+ }
+
+ pub fn stop(&self) -> std::result::Result<(), OSStatus> {
+ let status = unsafe {
+ AudioObjectRemovePropertyListener(
+ self.device,
+ &self.property,
+ Some(Self::render),
+ self as *const Self as *mut c_void,
+ )
+ };
+ if status == NO_ERR {
+ Ok(())
+ } else {
+ Err(status)
+ }
+ }
+
+ extern "C" fn render(
+ id: AudioObjectID,
+ number_of_addresses: u32,
+ addresses: *const AudioObjectPropertyAddress,
+ data: *mut c_void,
+ ) -> OSStatus {
+ let listener = unsafe { &*(data as *mut Self) };
+ assert_eq!(id, listener.device);
+ let addrs = unsafe { slice::from_raw_parts(addresses, number_of_addresses as usize) };
+ (listener.callback)(addrs)
+ }
+}
+
+impl<F> Drop for TestPropertyListener<F>
+where
+ F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus,
+{
+ fn drop(&mut self) {
+ self.stop();
+ }
+}
+
+// TODO: It doesn't work if default input or output is an aggregate device! Probably we need to do
+// the same thing as what audiounit_set_aggregate_sub_device_list does.
+#[derive(Debug)]
+pub struct TestDevicePlugger {
+ scope: Scope,
+ plugin_id: AudioObjectID,
+ device_id: AudioObjectID,
+}
+
+impl TestDevicePlugger {
+ pub fn new(scope: Scope) -> std::result::Result<Self, OSStatus> {
+ let plugin_id = Self::get_system_plugin_id()?;
+ Ok(Self {
+ scope,
+ plugin_id,
+ device_id: kAudioObjectUnknown,
+ })
+ }
+
+ pub fn get_device_id(&self) -> AudioObjectID {
+ self.device_id
+ }
+
+ pub fn plug(&mut self) -> std::result::Result<(), OSStatus> {
+ self.device_id = self.create_aggregate_device()?;
+ Ok(())
+ }
+
+ pub fn unplug(&mut self) -> std::result::Result<(), OSStatus> {
+ self.destroy_aggregate_device()
+ }
+
+ fn is_plugging(&self) -> bool {
+ self.device_id != kAudioObjectUnknown
+ }
+
+ fn destroy_aggregate_device(&mut self) -> std::result::Result<(), OSStatus> {
+ assert_ne!(self.plugin_id, kAudioObjectUnknown);
+ assert_ne!(self.device_id, kAudioObjectUnknown);
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioPlugInDestroyAggregateDevice,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut size: usize = 0;
+ let status = unsafe {
+ AudioObjectGetPropertyDataSize(
+ self.plugin_id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ if status != NO_ERR {
+ return Err(status);
+ }
+ assert_ne!(size, 0);
+
+ let status = unsafe {
+ // This call can simulate removing a device.
+ AudioObjectGetPropertyData(
+ self.plugin_id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ &mut self.device_id as *mut AudioDeviceID as *mut c_void,
+ )
+ };
+ if status == NO_ERR {
+ self.device_id = kAudioObjectUnknown;
+ Ok(())
+ } else {
+ Err(status)
+ }
+ }
+
+ fn create_aggregate_device(&self) -> std::result::Result<AudioObjectID, OSStatus> {
+ use std::time::{SystemTime, UNIX_EPOCH};
+
+ const TEST_AGGREGATE_DEVICE_NAME: &str = "TestAggregateDevice";
+
+ assert_ne!(self.plugin_id, kAudioObjectUnknown);
+
+ let sub_devices = Self::get_sub_devices(self.scope.clone());
+ if sub_devices.is_none() {
+ return Err(kAudioCodecUnspecifiedError as OSStatus);
+ }
+ let sub_devices = sub_devices.unwrap();
+
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioPlugInCreateAggregateDevice,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut size: usize = 0;
+ let status = unsafe {
+ AudioObjectGetPropertyDataSize(
+ self.plugin_id,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ if status != NO_ERR {
+ return Err(status);
+ }
+ assert_ne!(size, 0);
+
+ let sys_time = SystemTime::now();
+ let time_id = sys_time.duration_since(UNIX_EPOCH).unwrap().as_nanos();
+ let device_name = format!("{}_{}", TEST_AGGREGATE_DEVICE_NAME, time_id);
+ let device_uid = format!("org.mozilla.{}", device_name);
+
+ let mut device_id = kAudioObjectUnknown;
+ let status = unsafe {
+ let device_dict = CFDictionaryCreateMutable(
+ kCFAllocatorDefault,
+ 0,
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks,
+ );
+
+ // Set the name of this device.
+ let device_name = cfstringref_from_string(&device_name);
+ CFDictionaryAddValue(
+ device_dict,
+ cfstringref_from_static_string(AGGREGATE_DEVICE_NAME_KEY) as *const c_void,
+ device_name as *const c_void,
+ );
+ CFRelease(device_name as *const c_void);
+
+ // Set the uid of this device.
+ let device_uid = cfstringref_from_string(&device_uid);
+ CFDictionaryAddValue(
+ device_dict,
+ cfstringref_from_static_string(AGGREGATE_DEVICE_UID_KEY) as *const c_void,
+ device_uid as *const c_void,
+ );
+ CFRelease(device_uid as *const c_void);
+
+ // Make this device NOT private to the process creating it.
+ // On MacOS 14 devicechange events are not triggered when it is private.
+ let private_value: i32 = 0;
+ let device_private_key = CFNumberCreate(
+ kCFAllocatorDefault,
+ i64::from(kCFNumberIntType),
+ &private_value as *const i32 as *const c_void,
+ );
+ CFDictionaryAddValue(
+ device_dict,
+ cfstringref_from_static_string(AGGREGATE_DEVICE_PRIVATE_KEY) as *const c_void,
+ device_private_key as *const c_void,
+ );
+ CFRelease(device_private_key as *const c_void);
+
+ // Set this device to be a stacked aggregate (i.e. multi-output device).
+ let stacked_value: i32 = 0; // 1 for normal aggregate device.
+ let device_stacked_key = CFNumberCreate(
+ kCFAllocatorDefault,
+ i64::from(kCFNumberIntType),
+ &stacked_value as *const i32 as *const c_void,
+ );
+ CFDictionaryAddValue(
+ device_dict,
+ cfstringref_from_static_string(AGGREGATE_DEVICE_STACKED_KEY) as *const c_void,
+ device_stacked_key as *const c_void,
+ );
+ CFRelease(device_stacked_key as *const c_void);
+
+ // Set sub devices for this device.
+ CFDictionaryAddValue(
+ device_dict,
+ cfstringref_from_static_string(AGGREGATE_DEVICE_SUB_DEVICE_LIST_KEY)
+ as *const c_void,
+ sub_devices as *const c_void,
+ );
+ CFRelease(sub_devices as *const c_void);
+
+ // This call can simulate adding a device.
+ let status = AudioObjectGetPropertyData(
+ self.plugin_id,
+ &address,
+ mem::size_of_val(&device_dict) as u32,
+ &device_dict as *const CFMutableDictionaryRef as *const c_void,
+ &mut size as *mut usize as *mut u32,
+ &mut device_id as *mut AudioDeviceID as *mut c_void,
+ );
+ CFRelease(device_dict as *const c_void);
+ status
+ };
+ if status == NO_ERR {
+ assert_ne!(device_id, kAudioObjectUnknown);
+ Ok(device_id)
+ } else {
+ Err(status)
+ }
+ }
+
+ fn get_system_plugin_id() -> std::result::Result<AudioObjectID, OSStatus> {
+ let address = AudioObjectPropertyAddress {
+ mSelector: kAudioHardwarePropertyPlugInForBundleID,
+ mScope: kAudioObjectPropertyScopeGlobal,
+ mElement: kAudioObjectPropertyElementMaster,
+ };
+
+ let mut size: usize = 0;
+ let status = unsafe {
+ AudioObjectGetPropertyDataSize(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ )
+ };
+ if status != NO_ERR {
+ return Err(status);
+ }
+ assert_ne!(size, 0);
+
+ let mut plugin_id = kAudioObjectUnknown;
+ let mut in_bundle_ref = cfstringref_from_static_string("com.apple.audio.CoreAudio");
+ let mut translation_value = AudioValueTranslation {
+ mInputData: &mut in_bundle_ref as *mut CFStringRef as *mut c_void,
+ mInputDataSize: mem::size_of::<CFStringRef>() as u32,
+ mOutputData: &mut plugin_id as *mut AudioObjectID as *mut c_void,
+ mOutputDataSize: mem::size_of::<AudioObjectID>() as u32,
+ };
+ assert_eq!(size, mem::size_of_val(&translation_value));
+
+ let status = unsafe {
+ let status = AudioObjectGetPropertyData(
+ kAudioObjectSystemObject,
+ &address,
+ 0,
+ ptr::null(),
+ &mut size as *mut usize as *mut u32,
+ &mut translation_value as *mut AudioValueTranslation as *mut c_void,
+ );
+ CFRelease(in_bundle_ref as *const c_void);
+ status
+ };
+ if status == NO_ERR {
+ assert_ne!(plugin_id, kAudioObjectUnknown);
+ Ok(plugin_id)
+ } else {
+ Err(status)
+ }
+ }
+
+ // TODO: This doesn't work as what we expect when the default deivce in the scope is an
+ // aggregate device. We should get the list of all the active sub devices and put
+ // them into the array, if the device is an aggregate device. See the code in
+ // AggregateDevice::get_sub_devices and audiounit_set_aggregate_sub_device_list.
+ fn get_sub_devices(scope: Scope) -> Option<CFArrayRef> {
+ let device = test_get_default_device(scope);
+ device?;
+ let device = device.unwrap();
+ let uid = get_device_global_uid(device);
+ if uid.is_err() {
+ return None;
+ }
+ let uid = uid.unwrap();
+ unsafe {
+ let list = CFArrayCreateMutable(ptr::null(), 0, &kCFTypeArrayCallBacks);
+ let sub_device_dict = CFDictionaryCreateMutable(
+ ptr::null(),
+ 0,
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks,
+ );
+ CFDictionaryAddValue(
+ sub_device_dict,
+ cfstringref_from_static_string(SUB_DEVICE_UID_KEY) as *const c_void,
+ uid.get_raw() as *const c_void,
+ );
+ CFArrayAppendValue(list, sub_device_dict as *const c_void);
+ CFRelease(sub_device_dict as *const c_void);
+ Some(list)
+ }
+ }
+}
+
+impl Drop for TestDevicePlugger {
+ fn drop(&mut self) {
+ if self.is_plugging() {
+ self.unplug();
+ }
+ }
+}
+
+// Test Templates
+// ------------------------------------------------------------------------------------------------
+pub fn test_ops_context_operation<F>(name: &'static str, operation: F)
+where
+ F: FnOnce(*mut ffi::cubeb),
+{
+ let name_c_string = CString::new(name).expect("Failed to create context name");
+ let mut context = ptr::null_mut::<ffi::cubeb>();
+ assert_eq!(
+ unsafe { OPS.init.unwrap()(&mut context, name_c_string.as_ptr()) },
+ ffi::CUBEB_OK
+ );
+ assert!(!context.is_null());
+ operation(context);
+ unsafe { OPS.destroy.unwrap()(context) }
+}
+
+// The in-out stream initializeed with different device will create an aggregate_device and
+// result in firing device-collection-changed callbacks. Run in-out streams with tests
+// capturing device-collection-changed callbacks may cause troubles.
+pub fn test_ops_stream_operation<F>(
+ name: &'static str,
+ input_device: ffi::cubeb_devid,
+ input_stream_params: *mut ffi::cubeb_stream_params,
+ output_device: ffi::cubeb_devid,
+ output_stream_params: *mut ffi::cubeb_stream_params,
+ latency_frames: u32,
+ data_callback: ffi::cubeb_data_callback,
+ state_callback: ffi::cubeb_state_callback,
+ user_ptr: *mut c_void,
+ operation: F,
+) where
+ F: FnOnce(*mut ffi::cubeb_stream),
+{
+ test_ops_context_operation("context: stream operation", |context_ptr| {
+ // Do nothing if there is no input/output device to perform input/output tests.
+ if !input_stream_params.is_null() && test_get_default_device(Scope::Input).is_none() {
+ println!("No input device to perform input tests for \"{}\".", name);
+ return;
+ }
+
+ if !output_stream_params.is_null() && test_get_default_device(Scope::Output).is_none() {
+ println!("No output device to perform output tests for \"{}\".", name);
+ return;
+ }
+
+ let mut stream: *mut ffi::cubeb_stream = ptr::null_mut();
+ let stream_name = CString::new(name).expect("Failed to create stream name");
+ assert_eq!(
+ unsafe {
+ OPS.stream_init.unwrap()(
+ context_ptr,
+ &mut stream,
+ stream_name.as_ptr(),
+ input_device,
+ input_stream_params,
+ output_device,
+ output_stream_params,
+ latency_frames,
+ data_callback,
+ state_callback,
+ user_ptr,
+ )
+ },
+ ffi::CUBEB_OK
+ );
+ assert!(!stream.is_null());
+ operation(stream);
+ unsafe {
+ OPS.stream_destroy.unwrap()(stream);
+ }
+ });
+}
+
+pub fn test_get_raw_context<F>(operation: F)
+where
+ F: FnOnce(&mut AudioUnitContext),
+{
+ let mut context = AudioUnitContext::new();
+ operation(&mut context);
+}
+
+pub fn test_get_default_raw_stream<F>(operation: F)
+where
+ F: FnOnce(&mut AudioUnitStream),
+{
+ test_get_raw_stream(ptr::null_mut(), None, None, 0, operation);
+}
+
+fn test_get_raw_stream<F>(
+ user_ptr: *mut c_void,
+ data_callback: ffi::cubeb_data_callback,
+ state_callback: ffi::cubeb_state_callback,
+ latency_frames: u32,
+ operation: F,
+) where
+ F: FnOnce(&mut AudioUnitStream),
+{
+ let mut context = AudioUnitContext::new();
+
+ // Add a stream to the context since we are about to create one.
+ // AudioUnitStream::drop() will check the context has at least one stream.
+ let global_latency_frames = context.update_latency_by_adding_stream(latency_frames);
+
+ let mut stream = AudioUnitStream::new(
+ &mut context,
+ user_ptr,
+ data_callback,
+ state_callback,
+ global_latency_frames.unwrap(),
+ );
+ stream.core_stream_data = CoreStreamData::new(&stream, None, None);
+
+ operation(&mut stream);
+}
+
+pub fn test_get_stream_with_default_data_callback_by_type<F>(
+ name: &'static str,
+ stm_type: StreamType,
+ input_device: Option<AudioObjectID>,
+ output_device: Option<AudioObjectID>,
+ state_callback: extern "C" fn(*mut ffi::cubeb_stream, *mut c_void, ffi::cubeb_state),
+ data: *mut c_void,
+ operation: F,
+) where
+ F: FnOnce(&mut AudioUnitStream),
+{
+ let mut input_params = get_dummy_stream_params(Scope::Input);
+ let mut output_params = get_dummy_stream_params(Scope::Output);
+
+ let in_params = if stm_type.contains(StreamType::INPUT) {
+ &mut input_params as *mut ffi::cubeb_stream_params
+ } else {
+ ptr::null_mut()
+ };
+ let out_params = if stm_type.contains(StreamType::OUTPUT) {
+ &mut output_params as *mut ffi::cubeb_stream_params
+ } else {
+ ptr::null_mut()
+ };
+ let in_device = if let Some(id) = input_device {
+ id as ffi::cubeb_devid
+ } else {
+ ptr::null_mut()
+ };
+ let out_device = if let Some(id) = output_device {
+ id as ffi::cubeb_devid
+ } else {
+ ptr::null_mut()
+ };
+
+ test_ops_stream_operation_with_default_data_callback(
+ name,
+ in_device,
+ in_params,
+ out_device,
+ out_params,
+ state_callback,
+ data,
+ |stream| {
+ let stm = unsafe { &mut *(stream as *mut AudioUnitStream) };
+ operation(stm);
+ },
+ );
+}
+
+bitflags! {
+ pub struct StreamType: u8 {
+ const INPUT = 0x01;
+ const OUTPUT = 0x02;
+ const DUPLEX = 0x03;
+ }
+}
+
+fn get_dummy_stream_params(scope: Scope) -> ffi::cubeb_stream_params {
+ // The stream format for input and output must be same.
+ const STREAM_FORMAT: u32 = ffi::CUBEB_SAMPLE_FLOAT32NE;
+
+ // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
+ // (in the comments).
+ let mut stream_params = ffi::cubeb_stream_params::default();
+ stream_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;
+ let (format, rate, channels, layout) = match scope {
+ Scope::Input => (STREAM_FORMAT, 48000, 1, ffi::CUBEB_LAYOUT_MONO),
+ Scope::Output => (STREAM_FORMAT, 44100, 2, ffi::CUBEB_LAYOUT_STEREO),
+ };
+ stream_params.format = format;
+ stream_params.rate = rate;
+ stream_params.channels = channels;
+ stream_params.layout = layout;
+ stream_params
+}
+
+fn test_ops_stream_operation_with_default_data_callback<F>(
+ name: &'static str,
+ input_device: ffi::cubeb_devid,
+ input_stream_params: *mut ffi::cubeb_stream_params,
+ output_device: ffi::cubeb_devid,
+ output_stream_params: *mut ffi::cubeb_stream_params,
+ state_callback: extern "C" fn(*mut ffi::cubeb_stream, *mut c_void, ffi::cubeb_state),
+ data: *mut c_void,
+ operation: F,
+) where
+ F: FnOnce(*mut ffi::cubeb_stream),
+{
+ test_ops_stream_operation(
+ name,
+ input_device,
+ input_stream_params,
+ output_device,
+ output_stream_params,
+ 4096, // TODO: Get latency by get_min_latency instead ?
+ Some(noop_data_callback),
+ Some(state_callback),
+ data,
+ operation,
+ );
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/backend/utils.rs b/third_party/rust/cubeb-coreaudio/src/backend/utils.rs
new file mode 100644
index 0000000000..246e337ef5
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/backend/utils.rs
@@ -0,0 +1,107 @@
+// Copyright © 2018 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details.
+use cubeb_backend::SampleFormat as fmt;
+use std::mem;
+
+pub fn allocate_array_by_size<T: Clone + Default>(size: usize) -> Vec<T> {
+ assert_eq!(size % mem::size_of::<T>(), 0);
+ let elements = size / mem::size_of::<T>();
+ allocate_array::<T>(elements)
+}
+
+pub fn allocate_array<T: Clone + Default>(elements: usize) -> Vec<T> {
+ vec![T::default(); elements]
+}
+
+pub fn forget_vec<T>(v: Vec<T>) -> (*mut T, usize) {
+ // Drop any excess capacity by into_boxed_slice.
+ let mut slice = v.into_boxed_slice();
+ let ptr_and_len = (slice.as_mut_ptr(), slice.len());
+ mem::forget(slice); // Leak the memory to the external code.
+ ptr_and_len
+}
+
+#[inline]
+pub fn retake_forgotten_vec<T>(ptr: *mut T, len: usize) -> Vec<T> {
+ unsafe { Vec::from_raw_parts(ptr, len, len) }
+}
+
+pub fn cubeb_sample_size(format: fmt) -> usize {
+ match format {
+ fmt::S16LE | fmt::S16BE | fmt::S16NE => mem::size_of::<i16>(),
+ fmt::Float32LE | fmt::Float32BE | fmt::Float32NE => mem::size_of::<f32>(),
+ }
+}
+
+pub struct Finalizer<F: FnOnce()>(Option<F>);
+
+impl<F: FnOnce()> Finalizer<F> {
+ pub fn dismiss(&mut self) {
+ let _ = self.0.take();
+ assert!(self.0.is_none());
+ }
+}
+
+impl<F: FnOnce()> Drop for Finalizer<F> {
+ fn drop(&mut self) {
+ if let Some(f) = self.0.take() {
+ f();
+ }
+ }
+}
+
+pub fn finally<F: FnOnce()>(f: F) -> Finalizer<F> {
+ Finalizer(Some(f))
+}
+
+#[test]
+fn test_forget_vec_and_retake_it() {
+ let expected: Vec<u32> = (10..20).collect();
+ let leaked = expected.clone();
+ let (ptr, len) = forget_vec(leaked);
+ let retaken = retake_forgotten_vec(ptr, len);
+ for (idx, data) in retaken.iter().enumerate() {
+ assert_eq!(*data, expected[idx]);
+ }
+}
+
+#[test]
+fn test_cubeb_sample_size() {
+ let pairs = [
+ (fmt::S16LE, mem::size_of::<i16>()),
+ (fmt::S16BE, mem::size_of::<i16>()),
+ (fmt::S16NE, mem::size_of::<i16>()),
+ (fmt::Float32LE, mem::size_of::<f32>()),
+ (fmt::Float32BE, mem::size_of::<f32>()),
+ (fmt::Float32NE, mem::size_of::<f32>()),
+ ];
+
+ for pair in pairs.iter() {
+ let (fotmat, size) = pair;
+ assert_eq!(cubeb_sample_size(*fotmat), *size);
+ }
+}
+
+#[test]
+fn test_finally() {
+ let mut x = 0;
+
+ {
+ let y = &mut x;
+ let _finally = finally(|| {
+ *y = 100;
+ });
+ }
+ assert_eq!(x, 100);
+
+ {
+ let y = &mut x;
+ let mut finally = finally(|| {
+ *y = 200;
+ });
+ finally.dismiss();
+ }
+ assert_eq!(x, 100);
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/capi.rs b/third_party/rust/cubeb-coreaudio/src/capi.rs
new file mode 100644
index 0000000000..1fd9e96f10
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/capi.rs
@@ -0,0 +1,19 @@
+// Copyright © 2018 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details.
+
+use crate::backend::AudioUnitContext;
+use cubeb_backend::{capi, ffi};
+use std::os::raw::{c_char, c_int};
+
+/// # Safety
+///
+/// This function should only be called once per process.
+#[no_mangle]
+pub unsafe extern "C" fn audiounit_rust_init(
+ c: *mut *mut ffi::cubeb,
+ context_name: *const c_char,
+) -> c_int {
+ capi::capi_init::<AudioUnitContext>(c, context_name)
+}
diff --git a/third_party/rust/cubeb-coreaudio/src/lib.rs b/third_party/rust/cubeb-coreaudio/src/lib.rs
new file mode 100644
index 0000000000..87efbf54d6
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/src/lib.rs
@@ -0,0 +1,20 @@
+// Copyright © 2018 Mozilla Foundation
+//
+// This program is made available under an ISC-style license. See the
+// accompanying file LICENSE for details.
+
+extern crate atomic;
+#[macro_use]
+extern crate bitflags;
+#[macro_use]
+extern crate cubeb_backend;
+#[macro_use]
+extern crate float_cmp;
+#[macro_use]
+extern crate lazy_static;
+extern crate mach;
+
+mod backend;
+mod capi;
+
+pub use crate::capi::audiounit_rust_init;
diff --git a/third_party/rust/cubeb-coreaudio/todo.md b/third_party/rust/cubeb-coreaudio/todo.md
new file mode 100644
index 0000000000..7e07476bd2
--- /dev/null
+++ b/third_party/rust/cubeb-coreaudio/todo.md
@@ -0,0 +1,124 @@
+# TO DO
+
+## General
+
+- Resolve the [issues](https://github.com/mozilla/cubeb-coreaudio-rs/issues)
+- Some of bugs are found when adding tests. Search *FIXME* to find them.
+- Remove `#[allow(non_camel_case_types)]`, `#![allow(unused_assignments)]`, `#![allow(unused_must_use)]`
+- Use `ErrorChain`
+- Centralize the error log in one place
+- Support `enumerate_devices` with in-out type?
+- Monitor `kAudioDevicePropertyDeviceIsAlive` for output device.
+- Create a wrapper for `CFArrayCreateMutable` like what we do for `CFMutableDictionaryRef`
+- Create a wrapper for property listener’s callback
+- Use `Option<AggregateDevice>` rather than `AggregateDevice` for `aggregate_device` in `CoreStreamData`
+
+### Type of stream
+
+- Use `Option<device_info>` rather than `device_info` for `{input, output}_device` in `CoreStreamData`
+- Use `Option<StreamParams>` rather than `StreamParams` for `{input, output}_stream_params` in `CoreStreamData`
+- Same as `{input, output}_desc`, `{input, output}_hw_rate`, ...etc
+- It would much clearer if we have a `struct X` to wrap all the above stuff and use `input_x` and `output_x` in `CoreStreamData`
+
+## Separate the stream implementation from the interface
+
+The goal is to separate the audio stream into two parts(modules).
+One is _inner_, the other is _outer_.
+
+- The _outer_ stream implements the cubeb interface, based on the _inner_ stream.
+- The _inner_ stream implements the stream operations based on the _CoreAudio_ APIs.
+Now the _outer_ stream is named `AudioUnitStream`, the _inner_ stream is named `CoreStreamData`.
+
+The problem now is that we don't have a clear boundry of the data ownership
+between the _outer_ stream and _inner_ stream. They access the data owned by the other.
+
+- `audiounit_property_listener_callback` is tied to _outer_ stream
+but the event listeners are in _inner_ stream
+- `audiounit_input_callback`, `audiounit_output_callback` are registered by the _inner_ stream
+but the main logic are tied to _outer_ stream
+
+### Callback separation
+
+- Create static callbacks in _inner_ stream
+- Render _inner_ stream's callbacks to _outer_ stream's callbacks
+
+### Reinitialization
+
+If the _outer_ stream and the _inner_ stream are separate properly,
+when we need to reinitialize the stream, we can just drop the _inner_ stream
+and create a new one. It's easier than the current implementation.
+
+## Aggregate device
+
+### Usage policy
+
+- [BMO 1563475][bmo1563475]: Only use _aggregate device_ when the mic is a input-only and the speaker is output-only device.
+- Test if we should do drift compensation.
+- Add a test for `should_use_aggregate_device`
+ - Create a dummy stream and check
+ - Check again after reinit
+ - Input only: expect false
+ - Output only: expect false
+ - Duplex
+ - Default input and output are different and they are mic-only and speaker-only: expect true
+ - Otherwise: expect false
+
+[bmo1563475]: https://bugzilla.mozilla.org/show_bug.cgi?id=1563475#c4
+
+### Get sub devices
+
+- A better pattern for `AggregateDevice::get_sub_devices`
+
+### Set sub devices
+
+- We will add overlapping devices between `input_sub_devices` and `output_sub_devices`.
+ - if they are same device
+ - if either one of them or both of them are aggregate devices
+
+### Setting master device
+
+- Check if the first subdevice of the default output device is in the list of
+ sub devices list of the aggregate device
+- Check the `name: CFStringRef` of the master device is not `NULL`
+
+### Mixer
+
+- Don't force output device to mono or stereo when the output device has one or two channel
+ - unless the output devicv is _Bose QC35, mark 1 and 2_.
+
+## Interface to system types and APIs
+
+- Check if we need `AudioDeviceID` and `AudioObjectID` at the same time
+- Create wrapper for `AudioObjectGetPropertyData(Size)` with _qualifier_ info
+- Create wrapper for `CF` related types
+- Create wrapper struct for `AudioObjectId`
+ - Add `get_data`, `get_data_size`, `set_data`
+- Create wrapper struct for `AudioUnit`
+ - Implement `get_data`, `set_data`
+- Create wrapper for `audio_unit_{add, remove}_property_listener`, `audio_object_{add, remove}_property_listener` and their callbacks
+ - Add/Remove listener with generic `*mut T` data, fire their callback with generic `*mut T` data
+
+## [Cubeb Interface][cubeb-rs]
+
+- `current_device` should be the in-use device of the current stream rather than default input and output device.
+- Implement `From` trait for `enum cubeb_device_type` so we can use `devtype.into()` to get `ffi::CUBEB_DEVICE_TYPE_*`.
+- Implement `to_owned` in [`StreamParamsRef`][cubeb-rs-stmparamsref]
+- Check the passed parameters like what [cubeb.c does][cubeb-stm-check]!
+ - Check the input `StreamParams` parameters properly, or we will set a invalid format into `AudioUnit`.
+ - For example, for a duplex stream, the format of the input stream and output stream should be same.
+ Using different stream formats will cause memory corruption
+ since our resampler assumes the types (_short_ or _float_) of input stream (buffer) and output stream (buffer) are same
+ (The resampler will use the format of the input stream if it exists, otherwise it uses the format of the output stream).
+ - In fact, we should check **all** the parameters properly so we can make sure we don't mess up the streams/devices settings!
+
+[cubeb-rs]: https://github.com/djg/cubeb-rs "cubeb-rs"
+[cubeb-rs-stmparamsref]: https://github.com/djg/cubeb-rs/blob/78ed9459b8ac2ca50ea37bb72f8a06847eb8d379/cubeb-core/src/stream.rs#L61 "StreamParamsRef"
+[cubeb-stm-check]: https://github.com/mozilla/cubeb/blob/a971bf1a045b0e5dcaffd2a15c3255677f43cd2d/src/cubeb.c#L70-L108
+
+## Test
+
+- Rewrite some tests under _cubeb/test/*_ in _Rust_ as part of the integration tests
+ - Add tests for capturing/recording, output, duplex streams
+- Update the manual tests
+ - Those tests are created in the middle of the development. Thay might be not outdated now.
+- Add in-out support for `TestDevicePlugger`