Bug 1601859 - Vendor cubeb-coreaudio. r=padenot

This technically breaks mach vendor rust because of the missing
licenses, but this will be fixed subsequently.

Differential Revision: https://phabricator.services.mozilla.com/D56160

--HG--
rename : media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/Cargo.toml => third_party/rust/coreaudio-sys-utils/Cargo.toml
rename : media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/aggregate_device.rs => third_party/rust/coreaudio-sys-utils/src/aggregate_device.rs
rename : media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/audio_object.rs => third_party/rust/coreaudio-sys-utils/src/audio_object.rs
rename : media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/audio_unit.rs => third_party/rust/coreaudio-sys-utils/src/audio_unit.rs
rename : media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/cf_mutable_dict.rs => third_party/rust/coreaudio-sys-utils/src/cf_mutable_dict.rs
rename : media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/dispatch.rs => third_party/rust/coreaudio-sys-utils/src/dispatch.rs
rename : media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/lib.rs => third_party/rust/coreaudio-sys-utils/src/lib.rs
rename : media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/string.rs => third_party/rust/coreaudio-sys-utils/src/string.rs
rename : media/libcubeb/cubeb-coreaudio-rs/Cargo.toml => third_party/rust/cubeb-coreaudio/Cargo.toml
rename : media/libcubeb/cubeb-coreaudio-rs/LICENSE => third_party/rust/cubeb-coreaudio/LICENSE
rename : media/libcubeb/cubeb-coreaudio-rs/src/backend/aggregate_device.rs => third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs
rename : media/libcubeb/cubeb-coreaudio-rs/src/backend/auto_array.rs => third_party/rust/cubeb-coreaudio/src/backend/auto_array.rs
rename : media/libcubeb/cubeb-coreaudio-rs/src/backend/auto_release.rs => third_party/rust/cubeb-coreaudio/src/backend/auto_release.rs
rename : media/libcubeb/cubeb-coreaudio-rs/src/backend/device_property.rs => third_party/rust/cubeb-coreaudio/src/backend/device_property.rs
rename : media/libcubeb/cubeb-coreaudio-rs/src/backend/mixer.rs => third_party/rust/cubeb-coreaudio/src/backend/mixer.rs
rename : media/libcubeb/cubeb-coreaudio-rs/src/backend/mod.rs => third_party/rust/cubeb-coreaudio/src/backend/mod.rs
rename : media/libcubeb/cubeb-coreaudio-rs/src/backend/resampler.rs => third_party/rust/cubeb-coreaudio/src/backend/resampler.rs
rename : media/libcubeb/cubeb-coreaudio-rs/src/backend/utils.rs => third_party/rust/cubeb-coreaudio/src/backend/utils.rs
rename : media/libcubeb/cubeb-coreaudio-rs/src/capi.rs => third_party/rust/cubeb-coreaudio/src/capi.rs
rename : media/libcubeb/cubeb-coreaudio-rs/src/lib.rs => third_party/rust/cubeb-coreaudio/src/lib.rs
extra : moz-landing-system : lando
This commit is contained in:
Mike Hommey 2019-12-10 15:43:17 +00:00
parent 37ebcfc664
commit 1c4ef0191c
45 changed files with 6443 additions and 102 deletions

View File

@ -57,6 +57,11 @@ branch = "stable"
git = "https://github.com/NikVolf/tokio-named-pipes"
replace-with = "vendored-sources"
[source."https://github.com/ChunMinChang/cubeb-coreaudio-rs"]
git = "https://github.com/ChunMinChang/cubeb-coreaudio-rs"
replace-with = "vendored-sources"
rev = "0920240e4166d2b562840c8062e149d63f7c3a02"
[source.crates-io]
replace-with = "vendored-sources"

8
Cargo.lock generated
View File

@ -608,6 +608,7 @@ dependencies = [
[[package]]
name = "coreaudio-sys-utils"
version = "0.1.0"
source = "git+https://github.com/ChunMinChang/cubeb-coreaudio-rs?rev=0920240e4166d2b562840c8062e149d63f7c3a02#0920240e4166d2b562840c8062e149d63f7c3a02"
dependencies = [
"core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"coreaudio-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
@ -833,10 +834,11 @@ dependencies = [
[[package]]
name = "cubeb-coreaudio"
version = "0.1.0"
source = "git+https://github.com/ChunMinChang/cubeb-coreaudio-rs?rev=0920240e4166d2b562840c8062e149d63f7c3a02#0920240e4166d2b562840c8062e149d63f7c3a02"
dependencies = [
"atomic 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"coreaudio-sys-utils 0.1.0",
"coreaudio-sys-utils 0.1.0 (git+https://github.com/ChunMinChang/cubeb-coreaudio-rs?rev=0920240e4166d2b562840c8062e149d63f7c3a02)",
"cubeb-backend 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.59 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1453,7 +1455,7 @@ dependencies = [
"bookmark_sync 0.1.0",
"cert_storage 0.0.1",
"cose-c 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"cubeb-coreaudio 0.1.0",
"cubeb-coreaudio 0.1.0 (git+https://github.com/ChunMinChang/cubeb-coreaudio-rs?rev=0920240e4166d2b562840c8062e149d63f7c3a02)",
"cubeb-pulse 0.3.0",
"cubeb-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"encoding_glue 0.1.0",
@ -4755,6 +4757,7 @@ dependencies = [
"checksum core-graphics 0.17.3 (registry+https://github.com/rust-lang/crates.io-index)" = "56790968ab1c8a1202a102e6de05fc6e1ec87da99e4e93e9a7d13efbfc1e95a9"
"checksum core-text 13.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f3f46450d6f2397261af420b4ccce23807add2e45fa206410a03d66fb7f050ae"
"checksum coreaudio-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e8f5954c1c7ccb55340443e8b29fca24013545a5e7d72c1ca7db4fc02b982ce"
"checksum coreaudio-sys-utils 0.1.0 (git+https://github.com/ChunMinChang/cubeb-coreaudio-rs?rev=0920240e4166d2b562840c8062e149d63f7c3a02)" = "<none>"
"checksum cose 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "72fa26cb151d3ae4b70f63d67d0fed57ce04220feafafbae7f503bef7aae590d"
"checksum cose-c 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "49726015ab0ca765144fcca61e4a7a543a16b795a777fa53f554da2fffff9a94"
"checksum cranelift-bforest 0.51.0 (git+https://github.com/bytecodealliance/cranelift?rev=4727b70b67abfa4f3ae1c276454a0da7a76e1d49)" = "<none>"
@ -4778,6 +4781,7 @@ dependencies = [
"checksum cubeb 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3cbcdfde9ea319160af6eff068ffaa96aad3532e1b5c0ebc134614cfacacae24"
"checksum cubeb-backend 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5a1e7add4e7642a8aebb24172922318482bed52389a12cb339f728bbd4c4ed9c"
"checksum cubeb-core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfd9b2ea1cb6afed9419b0d18fc4093df552ccb2300eb57793629f8cd370b4c8"
"checksum cubeb-coreaudio 0.1.0 (git+https://github.com/ChunMinChang/cubeb-coreaudio-rs?rev=0920240e4166d2b562840c8062e149d63f7c3a02)" = "<none>"
"checksum cubeb-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "309c5839c5fa03c08363bd308566cbe4654b25a9984342d7546a33d55b80a3d6"
"checksum d3d12 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc7ed48e89905e5e146bcc1951cc3facb9e44aea9adf5dc01078cda1bd24b662"
"checksum darling 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe629a532efad5526454efb0700f86d5ad7ff001acb37e431c8bf017a432a8e"

View File

@ -1,6 +0,0 @@
The source from this directory was copied from the cubeb-coreaudio-rs
git repository using the update.sh script.
The cubeb-coreaudio-rs git repository is: https://github.com/ChunMinChang/cubeb-coreaudio-rs
The git commit ID used was 0920240e4166d2b562840c8062e149d63f7c3a02 (2019-11-13 09:18:08 -0800)

View File

@ -1,11 +0,0 @@
diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/backend/mod.rs b/media/libcubeb/cubeb-coreaudio-rs/src/backend/mod.rs
index f2da36fee65b..d9c402ee14b8 100644
--- a/media/libcubeb/cubeb-coreaudio-rs/src/backend/mod.rs
+++ b/media/libcubeb/cubeb-coreaudio-rs/src/backend/mod.rs
@@ -4118,6 +4118,3 @@ impl<'ctx> StreamOps for AudioUnitStream<'ctx> {
// An unsafe workaround to pass AudioUnitStream across threads.
unsafe impl<'ctx> Send for AudioUnitStream<'ctx> {}
unsafe impl<'ctx> Sync for AudioUnitStream<'ctx> {}
-
-#[cfg(test)]
-mod tests;

View File

@ -1,42 +0,0 @@
use super::coreaudio_sys_utils::sys::*;
pub const DEFAULT_INPUT_DEVICE_PROPERTY_ADDRESS: AudioObjectPropertyAddress =
AudioObjectPropertyAddress {
mSelector: kAudioHardwarePropertyDefaultInputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
pub const DEFAULT_OUTPUT_DEVICE_PROPERTY_ADDRESS: AudioObjectPropertyAddress =
AudioObjectPropertyAddress {
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
pub const DEVICE_IS_ALIVE_PROPERTY_ADDRESS: AudioObjectPropertyAddress =
AudioObjectPropertyAddress {
mSelector: kAudioDevicePropertyDeviceIsAlive,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
pub const DEVICES_PROPERTY_ADDRESS: AudioObjectPropertyAddress = AudioObjectPropertyAddress {
mSelector: kAudioHardwarePropertyDevices,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster,
};
pub const INPUT_DATA_SOURCE_PROPERTY_ADDRESS: AudioObjectPropertyAddress =
AudioObjectPropertyAddress {
mSelector: kAudioDevicePropertyDataSource,
mScope: kAudioDevicePropertyScopeInput,
mElement: kAudioObjectPropertyElementMaster,
};
pub const OUTPUT_DATA_SOURCE_PROPERTY_ADDRESS: AudioObjectPropertyAddress =
AudioObjectPropertyAddress {
mSelector: kAudioDevicePropertyDataSource,
mScope: kAudioDevicePropertyScopeOutput,
mElement: kAudioObjectPropertyElementMaster,
};

View File

@ -1,40 +0,0 @@
# Usage: sh update.sh <upstream_src_directory>
set -e
cp -p $1/LICENSE .
cp -p $1/Cargo.toml .
cp -r $1/coreaudio-sys-utils .
test -d src || mkdir -p src
# Copy all the files under src folder, except tests.
rsync -av --progress $1/src/ src/ --exclude backend/tests
if [ -d $1/.git ]; then
rev=$(cd $1 && git rev-parse --verify HEAD)
date=$(cd $1 && git show -s --format=%ci HEAD)
dirty=$(cd $1 && git diff-index --name-only HEAD)
set +e
pre_rev=$(grep -o '[[:xdigit:]]\{40\}' README_MOZILLA)
commits=$(cd $1 && git log --pretty=format:'%h - %s' $pre_rev..$rev)
set -e
fi
if [ -n "$rev" ]; then
version=$rev
if [ -n "$dirty" ]; then
version=$version-dirty
echo "WARNING: updating from a dirty git repository."
fi
echo "$version ($date)"
sed -i.bak -e "/The git commit ID used was/ s/[0-9a-f]\{40\}\(-dirty\)\{0,1\} .\{1,100\}/$version ($date)/" README_MOZILLA
rm README_MOZILLA.bak
[ -n "$commits" ] && echo -e "Pick commits:\n$commits"
else
echo "Remember to update README_MOZILLA with the version details."
fi
# Apply patches for gecko
for patch in *.patch; do
[ -f "$patch" ] || continue
echo "Apply $patch"
patch -p4 < $patch
done

View File

@ -0,0 +1 @@
{"files":{"Cargo.toml":"077906135ef930990c17a944953291db52f5e2e178cad992872228f6dc35d263","src/aggregate_device.rs":"7d2bd5f5fd7f3d008ebb69ad81f522ca0cb73db6d7b3e50ed1a63ea26ff721f4","src/audio_object.rs":"df10160d9fd83a2c23a49e69b78d39db3a9d6389607df6acfc05821293b6af5f","src/audio_unit.rs":"bc743a1b8033ab5459520c75d7f5230d24cda5ea1198a5b4e1594256af308f47","src/cf_mutable_dict.rs":"fc42edd270c6dfb02f123214d2d8e487bbd62b5bd923b71eec13190fd0104d2a","src/dispatch.rs":"c3d43571f610cb8524ef49b5928da8363651507bb2ccec443be58c8688e111cb","src/lib.rs":"bcc559d69ef6ed0cbea5b2a36fec89d8c011eb9da70e2f26c00f881ad97a2546","src/string.rs":"ddce19b0f0e6aceb64fa96d2f15f6b191051255f58b340737028fb464087d4e1"},"package":null}

View File

@ -0,0 +1 @@
{"files":{".editorconfig":"4e53b182bcc78b83d7e1b5c03efa14d22d4955c4ed2514d1ba4e99c1eb1a50ba",".travis.yml":"bea421508af5f4d00b941866dae0ae7d94a51b9276688a7f626686e8ed8fbbf3","Cargo.toml":"208c7d2c2240a1e38070313a981f61c72f81a017faf93d5ca350e0fae3a35df4","LICENSE":"6e6f56aff5bbf3cbc60747e152fb1a719bd0716aaf6d711c554f57d92e96297c","README.md":"72d8a890d6bda3cdba393432e5ae2018a385980785ebb2b96e9c3f82a48a1b59","run_tests.sh":"871864068903c37b04857f3509361f93fbbcf2b81d74eae5715ac2b451458813","src/backend/aggregate_device.rs":"7cd732f3e1e71876753515b26ee69a315414e58869216e99ff95c6828408c4db","src/backend/auto_array.rs":"5f35545baba2b005e13a2225bd1cbdd94ffc2097554d61479929bfc5442a6dd6","src/backend/auto_release.rs":"050fdcee74cf46b9a8a85a877e166d72a853d33220f59cf734cbb6ea09daa441","src/backend/device_property.rs":"1b066b48ed09026a9286b1b8f40e2720854c3410b0f02c795a580792fda34ea9","src/backend/mixer.rs":"74dcac459493e2f919b61ed3bebe500027e422eb06b1ecd30b73a47079c61f7c","src/backend/mod.rs":"7b57ec50f24cffbacd323162b6b9357c276c151e2844c8e32fbe779b18ef3179","src/backend/resampler.rs":"fd1281d28a4db1659d2f75e43b8457651745e1b6eb5a53a77f04d752135f6dc7","src/backend/tests/aggregate_device.rs":"107f5c637844cd5ae43d2b42cec4ef3369bb702751586078c0a9d50f039161cd","src/backend/tests/api.rs":"d76c1574179085e0c1342614cd674e5aa211dd456f3725aeb294def6b32750fd","src/backend/tests/backlog.rs":"3b189a7e036543c467cc242af0ed3332721179ee2b1c8847a6db563546f1ac52","src/backend/tests/device_change.rs":"e098dafaeedd7fbbf58378c2e74e7a245dba1cf8832695c1b08932a669131e44","src/backend/tests/device_property.rs":"b1a9ae79aa5b9a3f180040d0ef0954b134680d586882d2062c5e017b555bff57","src/backend/tests/interfaces.rs":"01fc2d54ddb50f014a44f9c137f078645738bcc81e48140a3e7ae68e18a61b6b","src/backend/tests/manual.rs":"066a7d981dc02d7a3fd83486b51e27ea1c9223496167ccb23a6478fde073e882","src/backend/tests/mod.rs":"8dba770023d7f9c4228f0e11915347f0e07da5fd818e3ee4478c4b197af9aa2a","src/backend/tests/parallel.rs":"f9e1883660d6146b6e5075806561f5f689810e25c5e7764dfd28c9b939821a49","src/backend/tests/tone.rs":"16150438317ce501986734167b5fb97bfec567228acbcd8f3b4c4484c22f29e0","src/backend/tests/utils.rs":"eb552657e68e67b8a60d04ad1bbb46cd1401bcafa27d383dccf4db141c8089c5","src/backend/utils.rs":"ee77bc266d672d3d9e23eb3290c1f897687394c6e459338804a17433380a6fd2","src/capi.rs":"61f8f0c4373adaefba1eb6e7084687e83a10136db96438bc35884327668e411f","src/lib.rs":"1ff4b738ed194061fca4ff745f847dea4de4e7a4fa1f898e7b4ad5e70c62386d","todo.md":"a66296c220cad24d08ee780308007a702f7e421edf0bb60464c3ce8feeda1882"},"package":null}

View File

@ -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

View File

@ -0,0 +1,13 @@
language: rust
rust:
- stable
- beta
- nightly
os:
- osx
before_script:
- rustc --version
- cargo --version
script:
- cargo build --verbose
- sh run_tests.sh

View File

@ -0,0 +1,105 @@
# cubeb-coreaudio-rs
[![Build Status](https://travis-ci.org/ChunMinChang/cubeb-coreaudio-rs.svg?branch=trailblazer)](https://travis-ci.org/ChunMinChang/cubeb-coreaudio-rs)
*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] first
## Status
The code is currently tested in the _Firefox Nightly_ under a _perf_.
- Try it:
- Open `about:config`
- Add a perf `media.cubeb.backend` with string `audiounit-rust`
- Restart Firefox Nightly
- Open `about:support`
- Check if the `Audio Backend` in `Media` section is `audiounit-rust` or not
- Retart Firefox Nightly again if it's not.
## 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 *FIXIT* are left.
### 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 change events listener
- `$ cargo test test_add_then_remove_listeners -- --ignored --nocapture`
- Plug/Unplug devices or switch input/output devices to see events log.
- Device collection change
- `cargo test test_device_collection_change -- --ignored --nocapture`
- Plug/Unplug devices to see events log.
## 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.
- No guarantee on `audiounit_set_channel_layout`
- This call doesn't work all the times
- Returned `NO_ERR` doesn't guarantee the layout is set to the one we want
- The layouts on some devices won't be changed even no errors are returned,
e.g., we can set _stereo_ layout to a _4-channels aggregate device_ with _QUAD_ layout
(created by Audio MIDI Setup) without any error. However, the layout
of this 4-channels aggregate device is still QUAD after setting it without error
- Another weird thing is that we will get a `kAudioUnitErr_InvalidPropertyValue`
if we set the layout to _QUAD_. It's the same layout as its original one but it cannot be set!
- `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/kinetiknz/cubeb "Cross platform audio library"
[cubeb-au]: https://github.com/kinetiknz/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/ChunMinChang/cubeb-coreaudio-rs/tree/plain-translation-from-c
[ocs-disposal]: https://github.com/ChunMinChang/cubeb-coreaudio-rs/tree/ocs-disposal
[trailblazer]: https://github.com/ChunMinChang/cubeb-coreaudio-rs/tree/trailblazer

View File

@ -0,0 +1,44 @@
# 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
cargo test test_switch_device -- --ignored --nocapture
cargo test test_plug_and_unplug_device -- --ignored --nocapture
# cargo test test_register_device_changed_callback -- --ignored --nocapture --test-threads=1
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_destroy_input_stream_after_unplugging_a_default_input_device -- --ignored --nocapture
# FIXIT: The following test will hang since we don't monitor the alive status of the output device
# cargo test test_destroy_output_stream_after_unplugging_a_nondefault_output_device -- --ignored --nocapture
cargo test test_destroy_output_stream_after_unplugging_a_default_output_device -- --ignored --nocapture
cargo test test_destroy_duplex_stream_after_unplugging_a_nondefault_input_device -- --ignored --nocapture
cargo test test_destroy_duplex_stream_after_unplugging_a_default_input_device -- --ignored --nocapture
# FIXIT: The following test will hang since we don't monitor the alive status of the output device
# cargo test test_destroy_duplex_stream_after_unplugging_a_nondefault_output_device -- --ignored --nocapture
cargo test test_destroy_duplex_stream_after_unplugging_a_default_output_device -- --ignored --nocapture
cargo test test_reinit_input_stream_by_unplugging_a_nondefault_input_device -- --ignored --nocapture
cargo test test_reinit_input_stream_by_unplugging_a_default_input_device -- --ignored --nocapture
# FIXIT: The following test will hang since we don't monitor the alive status of the output device
# cargo test test_reinit_output_stream_by_unplugging_a_nondefault_output_device -- --ignored --nocapture
cargo test test_reinit_output_stream_by_unplugging_a_default_output_device -- --ignored --nocapture
cargo test test_reinit_duplex_stream_by_unplugging_a_nondefault_input_device -- --ignored --nocapture
cargo test test_reinit_duplex_stream_by_unplugging_a_default_input_device -- --ignored --nocapture
# FIXIT: The following test will hang since we don't monitor the alive status of the output device
# cargo test test_reinit_duplex_stream_by_unplugging_a_nondefault_output_device -- --ignored --nocapture
cargo test test_reinit_duplex_stream_by_unplugging_a_default_output_device -- --ignored --nocapture
# Manual Tests
# cargo test test_switch_output_device -- --ignored --nocapture
# cargo test test_add_then_remove_listeners -- --ignored --nocapture
# cargo test test_device_collection_change -- --ignored --nocapture

View File

@ -3528,3 +3528,6 @@ impl<'ctx> StreamOps for AudioUnitStream<'ctx> {
unsafe impl<'ctx> Send for AudioUnitStream<'ctx> {}
unsafe impl<'ctx> Sync for AudioUnitStream<'ctx> {}
#[cfg(test)]
mod tests;

View File

@ -0,0 +1,401 @@
use super::utils::{
test_get_all_devices, test_get_all_onwed_devices, test_get_default_device,
test_get_drift_compensations, test_get_master_device, 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();
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).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();
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).is_ok());
// Check if master is set to the first sub device of the default output device.
// TODO: What if the output device in the aggregate device is not 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).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).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());
}

File diff suppressed because it is too large Load Diff

View File

@ -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());
});
}

View File

@ -0,0 +1,736 @@
// 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::{
test_create_device_change_listener, test_device_in_scope, test_get_default_device,
test_get_devices_in_scope, test_ops_stream_operation, test_set_default_device, Scope,
TestDevicePlugger, TestDeviceSwitcher,
};
use super::*;
use std::fmt::Debug;
use std::thread;
#[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) {
// 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.", scope);
return;
}
println!(
"Switch default device for {:?} while the stream is working.",
scope
);
let device_switcher = TestDeviceSwitcher::new(scope.clone());
let count = Arc::new(Mutex::new(0));
let also_count = Arc::clone(&count);
let listener = test_create_device_change_listener(scope.clone(), move |_addresses| {
let mut cnt = also_count.lock().unwrap();
*cnt += 1;
NO_ERR
});
listener.start();
let mut changed_watcher = Watcher::new(&count);
test_get_started_stream_in_scope(scope.clone(), move |_stream| loop {
thread::sleep(Duration::from_millis(500));
changed_watcher.prepare();
assert!(device_switcher.next().unwrap());
changed_watcher.wait_for_change();
if changed_watcher.current_result() >= 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);
},
);
}
#[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.
let input_count = Arc::new(Mutex::new(0u32));
let also_input_count = Arc::clone(&input_count);
let input_mtx_ptr = also_input_count.as_ref() as *const Mutex<u32>;
assert!(context
.register_device_collection_changed(
DeviceType::INPUT,
Some(input_changed_callback),
input_mtx_ptr as *mut c_void,
)
.is_ok());
let output_count = Arc::new(Mutex::new(0u32));
let also_output_count = Arc::clone(&output_count);
let output_mtx_ptr = also_output_count.as_ref() as *const Mutex<u32>;
assert!(context
.register_device_collection_changed(
DeviceType::OUTPUT,
Some(output_changed_callback),
output_mtx_ptr as *mut c_void,
)
.is_ok());
let mut input_watcher = Watcher::new(&input_count);
let mut output_watcher = Watcher::new(&output_count);
let mut device_plugger = TestDevicePlugger::new(scope).unwrap();
// Simulate adding devices and monitor the devices-changed callbacks.
input_watcher.prepare();
output_watcher.prepare();
assert!(device_plugger.plug().is_ok());
if is_input {
input_watcher.wait_for_change();
}
if is_output {
output_watcher.wait_for_change();
}
// Check changed count.
check_result(is_input, (1, 0), &input_watcher);
check_result(is_output, (1, 0), &output_watcher);
// Simulate removing devices and monitor the devices-changed callbacks.
input_watcher.prepare();
output_watcher.prepare();
assert!(device_plugger.unplug().is_ok());
if is_input {
input_watcher.wait_for_change();
}
if is_output {
output_watcher.wait_for_change();
}
check_result(is_input, (2, 0), &input_watcher);
check_result(is_output, (2, 0), &output_watcher);
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 count = unsafe { &*(data as *const Mutex<u32>) };
{
let mut guard = count.lock().unwrap();
*guard += 1;
}
}
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 count = unsafe { &*(data as *const Mutex<u32>) };
{
let mut guard = count.lock().unwrap();
*guard += 1;
}
}
fn check_result<T: Clone + Debug + PartialEq>(
in_scope: bool,
expected: (T, T),
watcher: &Watcher<T>,
) {
assert_eq!(
watcher.current_result(),
if in_scope { expected.0 } else { expected.1 }
);
}
}
#[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 input_devices = test_get_devices_in_scope(Scope::Input).len();
let output_devices = test_get_devices_in_scope(Scope::Output).len();
let input_available = input_devices >= 2;
let output_available = output_devices >= 2;
let run_available = match stm_type {
StreamType::INPUT => input_available,
StreamType::OUTPUT => output_available,
StreamType::DUPLEX => input_available | output_available,
_ => {
println!("Only test input, output, or duplex stream!");
return;
}
};
if !run_available {
println!("No enough devices to run the test!");
}
let changed_count = Arc::new(Mutex::new(0u32));
let also_changed_count = Arc::clone(&changed_count);
let mtx_ptr = also_changed_count.as_ref() as *const Mutex<u32>;
let input_count = if stm_type.contains(StreamType::INPUT) {
input_devices
} else {
0
};
let output_count = if stm_type.contains(StreamType::OUTPUT) {
output_devices
} else {
0
};
let input_device_switcher = TestDeviceSwitcher::new(Scope::Input);
let output_device_switcher = TestDeviceSwitcher::new(Scope::Output);
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.
mtx_ptr as *mut c_void,
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 mut changed_watcher = Watcher::new(&changed_count);
for _ in 0..input_count {
// 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) {}
changed_watcher.prepare();
assert!(input_device_switcher.next().unwrap());
changed_watcher.wait_for_change();
}
for _ in 0..output_count {
// 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) {}
changed_watcher.prepare();
assert!(output_device_switcher.next().unwrap());
changed_watcher.wait_for_change();
}
},
);
extern "C" fn callback(data: *mut c_void) {
println!("Device change callback. data @ {:p}", data);
let count = unsafe { &*(data as *const Mutex<u32>) };
{
let mut guard = count.lock().unwrap();
*guard += 1;
}
}
}
#[ignore]
#[test]
fn test_destroy_input_stream_after_unplugging_a_nondefault_input_device() {
test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, false, 0);
}
#[ignore]
#[test]
fn test_destroy_input_stream_after_unplugging_a_default_input_device() {
test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, true, 0);
}
// FIXIT: The following test will hang since we don't monitor the alive status of the output device
#[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_destroy_output_stream_after_unplugging_a_default_output_device() {
test_unplug_a_device_on_an_active_stream(StreamType::OUTPUT, Scope::Output, true, 0);
}
#[ignore]
#[test]
fn test_destroy_duplex_stream_after_unplugging_a_nondefault_input_device() {
test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, false, 0);
}
#[ignore]
#[test]
fn test_destroy_duplex_stream_after_unplugging_a_default_input_device() {
test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, true, 0);
}
// FIXIT: The following test will hang since we don't monitor the alive status of the output device
#[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_destroy_duplex_stream_after_unplugging_a_default_output_device() {
test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Output, true, 0);
}
#[ignore]
#[test]
fn test_reinit_input_stream_by_unplugging_a_nondefault_input_device() {
test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, false, 500);
}
#[ignore]
#[test]
fn test_reinit_input_stream_by_unplugging_a_default_input_device() {
test_unplug_a_device_on_an_active_stream(StreamType::INPUT, Scope::Input, true, 500);
}
// FIXIT: The following test will hang since we don't monitor the alive status of the output device
#[ignore]
#[test]
fn test_reinit_output_stream_by_unplugging_a_nondefault_output_device() {
test_unplug_a_device_on_an_active_stream(StreamType::OUTPUT, Scope::Output, false, 500);
}
#[ignore]
#[test]
fn test_reinit_output_stream_by_unplugging_a_default_output_device() {
test_unplug_a_device_on_an_active_stream(StreamType::OUTPUT, Scope::Output, true, 500);
}
#[ignore]
#[test]
fn test_reinit_duplex_stream_by_unplugging_a_nondefault_input_device() {
test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, false, 500);
}
#[ignore]
#[test]
fn test_reinit_duplex_stream_by_unplugging_a_default_input_device() {
test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Input, true, 500);
}
// FIXIT: The following test will hang since we don't monitor the alive status of the output device
#[ignore]
#[test]
fn test_reinit_duplex_stream_by_unplugging_a_nondefault_output_device() {
test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Output, false, 500);
}
#[ignore]
#[test]
fn test_reinit_duplex_stream_by_unplugging_a_default_output_device() {
test_unplug_a_device_on_an_active_stream(StreamType::DUPLEX, Scope::Output, true, 500);
}
fn test_unplug_a_device_on_an_active_stream(
stream_type: StreamType,
device_scope: Scope,
set_device_to_default: bool,
wait_for_reinit_millis: 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 ouput or duplex stream.");
return;
}
let mut plugger = TestDevicePlugger::new(device_scope.clone()).unwrap();
assert!(plugger.plug().is_ok());
assert_ne!(plugger.get_device_id(), kAudioObjectUnknown);
if set_device_to_default {
assert!(test_set_default_device(plugger.get_device_id(), device_scope.clone()).unwrap());
}
let (input_device, output_device) = match device_scope {
Scope::Input => (Some(plugger.get_device_id()), None),
Scope::Output => (None, Some(plugger.get_device_id())),
};
let changed_count = Arc::new(Mutex::new(0u32));
let also_changed_count = Arc::clone(&changed_count);
let mtx_ptr = also_changed_count.as_ref() as *const Mutex<u32>;
test_get_stream_with_device_changed_callback(
"stream: test stream reinit/destroy after unplugging a device",
stream_type,
input_device,
output_device,
mtx_ptr as *mut c_void,
callback,
|stream| {
let mut changed_watcher = Watcher::new(&changed_count);
changed_watcher.prepare();
stream.start();
// Wait for stream data callback.
thread::sleep(Duration::from_millis(200));
assert!(plugger.unplug().is_ok());
changed_watcher.wait_for_change();
// Wait for stream re-initialization or destroy stream directly.
if wait_for_reinit_millis > 0 {
thread::sleep(Duration::from_millis(wait_for_reinit_millis));
}
},
);
extern "C" fn callback(data: *mut c_void) {
println!("Device change callback. data @ {:p}", data);
let count = unsafe { &*(data as *const Mutex<u32>) };
{
let mut guard = count.lock().unwrap();
*guard += 1;
}
}
}
struct Watcher<T: Clone + PartialEq> {
watching: Arc<Mutex<T>>,
current: Option<T>,
}
impl<T: Clone + PartialEq> Watcher<T> {
fn new(value: &Arc<Mutex<T>>) -> Self {
Self {
watching: Arc::clone(value),
current: None,
}
}
fn prepare(&mut self) {
self.current = Some(self.current_result());
}
fn wait_for_change(&self) {
loop {
if self.current_result() != self.current.clone().unwrap() {
break;
}
}
}
fn current_result(&self) -> T {
let guard = self.watching.lock().unwrap();
guard.clone()
}
}
bitflags! {
struct StreamType: u8 {
const INPUT = 0b01;
const OUTPUT = 0b10;
const DUPLEX = Self::INPUT.bits | Self::OUTPUT.bits;
}
}
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,
callback: extern "C" fn(*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_default_callbacks_stream_operation(
name,
in_device,
in_params,
out_device,
out_params,
data,
|stream| {
let stm = unsafe { &mut *(stream as *mut AudioUnitStream) };
assert!(stm.register_device_changed_callback(Some(callback)).is_ok());
operation(stm);
assert!(stm.register_device_changed_callback(None).is_ok());
},
);
}
fn test_ops_default_callbacks_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,
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(data_callback),
Some(state_callback),
data,
operation,
);
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 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
}
}
// The stream format for input and output must be same.
const STREAM_FORMAT: u32 = ffi::CUBEB_SAMPLE_FLOAT32NE;
fn get_dummy_stream_params(scope: Scope) -> ffi::cubeb_stream_params {
// 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
}

View File

@ -0,0 +1,422 @@
use super::utils::{test_get_default_device, Scope};
use super::*;
// 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());
}
// 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_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) {
if let Ok(source) = get_device_source(device, DeviceType::INPUT) {
println!(
"input: {:X}, {:?}",
source,
convert_uint32_into_string(source)
);
} else {
println!("No input data source.");
}
} else {
println!("No input device.");
}
if let Some(device) = test_get_default_device(Scope::Output) {
if let Ok(source) = get_device_source(device, DeviceType::OUTPUT) {
println!(
"output: {:X}, {:?}",
source,
convert_uint32_into_string(source)
);
} else {
println!("No output data source.");
}
} 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) {
if let Ok(name) = get_device_source_name(device, DeviceType::INPUT) {
println!("input: {}", name.into_string());
} else {
println!("No input data source name.");
}
} else {
println!("No input device.");
}
if let Some(device) = test_get_default_device(Scope::Output) {
if let Ok(name) = get_device_source_name(device, DeviceType::OUTPUT) {
println!("output: {}", name.into_string());
} else {
println!("No output data source name.");
}
} 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_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_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);
} 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);
} 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_device_stream_format
// ------------------------------------
#[test]
fn test_get_device_stream_format() {
if let Some(device) = test_get_default_device(Scope::Input) {
let format = get_device_stream_format(device, DeviceType::INPUT).unwrap();
println!("input stream format: {:?}", format);
} else {
println!("No input device.");
}
if let Some(device) = test_get_default_device(Scope::Output) {
let format = get_device_stream_format(device, DeviceType::OUTPUT).unwrap();
println!("output stream format: {:?}", format);
} else {
println!("No output device.");
}
}
#[test]
#[should_panic]
fn test_get_device_stream_format_by_unknown_device() {
assert!(get_device_stream_format(kAudioObjectUnknown, DeviceType::INPUT).is_err());
}
// get_device_stream_configuration
// ------------------------------------
#[test]
fn test_get_device_stream_configuration() {
if let Some(device) = test_get_default_device(Scope::Input) {
let buffers = get_device_stream_configuration(device, DeviceType::INPUT).unwrap();
println!("input stream config: {:?}", buffers);
dbg!(buffers);
} else {
println!("No input device.");
}
if let Some(device) = test_get_default_device(Scope::Output) {
let buffers = get_device_stream_configuration(device, DeviceType::OUTPUT).unwrap();
println!("output stream config: {:?}", buffers);
dbg!(buffers);
} else {
println!("No output device.");
}
}
#[test]
#[should_panic]
fn test_get_device_stream_configuration_by_unknown_device() {
assert!(get_device_stream_configuration(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, DeviceType::INPUT).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, DeviceType::OUTPUT).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, DeviceType::INPUT).is_err());
}

View File

@ -0,0 +1,547 @@
use super::utils::{
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_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
);
});
}
#[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]
#[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 {}
},
);
}
// 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 ?
None, // No 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_reset_default_device() {
test_default_output_stream_operation("stream: reset default device", |stream| {
assert_eq!(
unsafe { OPS.stream_reset_default_device.unwrap()(stream) },
ffi::CUBEB_ERROR_NOT_SUPPORTED
);
});
}
#[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();
assert_eq!(
unsafe { OPS.stream_get_current_device.unwrap()(stream, &mut device) },
ffi::CUBEB_OK
);
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.
);
});
}
// Enable this after cubeb-rs version is updated to one that implements
// stream_register_device_changed_callback operation.
// #[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
// );
// });
// }

View File

@ -0,0 +1,253 @@
use super::utils::{
test_get_default_device, test_get_default_raw_stream, test_get_devices_in_scope,
test_ops_stream_operation, Scope, TestDeviceSwitcher,
};
use super::*;
#[ignore]
#[test]
fn test_switch_output_device() {
use std::f32::consts::PI;
use std::io;
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 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" => {
assert!(output_device_switcher.next().unwrap());
}
"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_add_then_remove_listeners() {
extern "C" fn callback(
id: AudioObjectID,
number_of_addresses: u32,
addresses: *const AudioObjectPropertyAddress,
data: *mut c_void,
) -> OSStatus {
println!("device: {}, data @ {:p}", id, data);
let addrs = unsafe { std::slice::from_raw_parts(addresses, number_of_addresses as usize) };
for (i, addr) in addrs.iter().enumerate() {
let property_selector = PropertySelector::new(addr.mSelector);
println!(
"address {}\n\tselector {}({})\n\tscope {}\n\telement {}",
i, addr.mSelector, property_selector, addr.mScope, addr.mElement
);
}
NO_ERR
}
test_get_default_raw_stream(|stream| {
let mut listeners = Vec::new();
let default_output_listener = device_property_listener::new(
kAudioObjectSystemObject,
get_property_address(
Property::HardwareDefaultOutputDevice,
DeviceType::INPUT | DeviceType::OUTPUT,
),
callback,
);
listeners.push(default_output_listener);
let default_input_listener = device_property_listener::new(
kAudioObjectSystemObject,
get_property_address(
Property::HardwareDefaultInputDevice,
DeviceType::INPUT | DeviceType::OUTPUT,
),
callback,
);
listeners.push(default_input_listener);
if let Some(device) = test_get_default_device(Scope::Output) {
let output_source_listener = device_property_listener::new(
device,
get_property_address(Property::DeviceSource, DeviceType::OUTPUT),
callback,
);
listeners.push(output_source_listener);
}
if let Some(device) = test_get_default_device(Scope::Input) {
let input_source_listener = device_property_listener::new(
device,
get_property_address(Property::DeviceSource, DeviceType::INPUT),
callback,
);
listeners.push(input_source_listener);
let input_alive_listener = device_property_listener::new(
device,
get_property_address(
Property::DeviceIsAlive,
DeviceType::INPUT | DeviceType::OUTPUT,
),
callback,
);
listeners.push(input_alive_listener);
}
if listeners.is_empty() {
println!("No listeners to test.");
return;
}
add_listeners(stream, &listeners);
println!("Unplug/Plug device or switch input/output device to see the event log.\nEnter anything to finish.");
let mut input = String::new();
let _ = std::io::stdin().read_line(&mut input);
remove_listeners(stream, &listeners);
});
fn add_listeners(stream: &AudioUnitStream, listeners: &Vec<device_property_listener>) {
for listener in listeners {
assert_eq!(stream.add_device_listener(listener), NO_ERR);
}
}
fn remove_listeners(stream: &AudioUnitStream, listeners: &Vec<device_property_listener>) {
for listener in listeners {
assert_eq!(stream.remove_device_listener(listener), NO_ERR);
}
}
}
#[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);
}

View File

@ -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;

View File

@ -0,0 +1,572 @@
use super::utils::{
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,
None, // No 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 context = AudioUnitContext::new();
let context_ptr_value = &context as *const 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,
None, // No 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]);
}
}

View File

@ -0,0 +1,90 @@
use super::utils::{test_get_default_device, test_ops_stream_operation, Scope};
use super::*;
#[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;
// 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);
thread::sleep(Duration::from_millis(500));
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
}
}

File diff suppressed because it is too large Load Diff

106
third_party/rust/cubeb-coreaudio/todo.md vendored Normal file
View File

@ -0,0 +1,106 @@
# TO DO
## General
- Some of bugs are found when adding tests. Search *FIXIT* 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 listeners 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`
### Generics
- Create a _generics_ for `input_linear_buffer`
- Consider replacing `AutoArrayWrapper` by [`ringbuf`](https://github.com/agerasev/ringbuf)
## 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
- We always set the master device to the first subdevice of the default output device
but the output device (forming the aggregate device) may not be the default output 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`
## 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
## Interface to other module
- Create a binding layer for the `resampler`
## [Cubeb Interface][cubeb-rs]
- 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][cubeb] does!
- 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"
## 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.

View File

@ -19,7 +19,7 @@ static_prefs = { path = "../../../../modules/libpref/init/static_prefs" }
profiler_helper = { path = "../../../../tools/profiler/rust-helper", optional = true }
mozurl = { path = "../../../../netwerk/base/mozurl" }
webrender_bindings = { path = "../../../../gfx/webrender_bindings", optional = true }
cubeb-coreaudio = { path = "../../../../media/libcubeb/cubeb-coreaudio-rs", optional = true }
cubeb-coreaudio = { git = "https://github.com/ChunMinChang/cubeb-coreaudio-rs", rev = "0920240e4166d2b562840c8062e149d63f7c3a02", optional = true }
cubeb-pulse = { path = "../../../../media/libcubeb/cubeb-pulse-rs", optional = true, features=["pulse-dlopen"] }
cubeb-sys = { version = "0.6", optional = true, features=["gecko-in-tree"] }
encoding_glue = { path = "../../../../intl/encoding_glue" }