diff --git a/.cargo/config.in b/.cargo/config.in index 43aeb69f957f..d382841a3e87 100644 --- a/.cargo/config.in +++ b/.cargo/config.in @@ -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" diff --git a/Cargo.lock b/Cargo.lock index bb808e8a439d..c4fe35e32658 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)" = "" "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)" = "" @@ -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)" = "" "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" diff --git a/media/libcubeb/cubeb-coreaudio-rs/README_MOZILLA b/media/libcubeb/cubeb-coreaudio-rs/README_MOZILLA deleted file mode 100644 index 63ed70016022..000000000000 --- a/media/libcubeb/cubeb-coreaudio-rs/README_MOZILLA +++ /dev/null @@ -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) diff --git a/media/libcubeb/cubeb-coreaudio-rs/remove-tests.patch b/media/libcubeb/cubeb-coreaudio-rs/remove-tests.patch deleted file mode 100644 index 356142db8e9e..000000000000 --- a/media/libcubeb/cubeb-coreaudio-rs/remove-tests.patch +++ /dev/null @@ -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; diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/backend/property_address.rs b/media/libcubeb/cubeb-coreaudio-rs/src/backend/property_address.rs deleted file mode 100644 index aa31601d9a4e..000000000000 --- a/media/libcubeb/cubeb-coreaudio-rs/src/backend/property_address.rs +++ /dev/null @@ -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, - }; diff --git a/media/libcubeb/cubeb-coreaudio-rs/update.sh b/media/libcubeb/cubeb-coreaudio-rs/update.sh deleted file mode 100755 index 9cd51d589d76..000000000000 --- a/media/libcubeb/cubeb-coreaudio-rs/update.sh +++ /dev/null @@ -1,40 +0,0 @@ -# Usage: sh update.sh -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 diff --git a/third_party/rust/coreaudio-sys-utils/.cargo-checksum.json b/third_party/rust/coreaudio-sys-utils/.cargo-checksum.json new file mode 100644 index 000000000000..f0efc11452b2 --- /dev/null +++ b/third_party/rust/coreaudio-sys-utils/.cargo-checksum.json @@ -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} \ No newline at end of file diff --git a/media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/Cargo.toml b/third_party/rust/coreaudio-sys-utils/Cargo.toml similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/Cargo.toml rename to third_party/rust/coreaudio-sys-utils/Cargo.toml diff --git a/media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/aggregate_device.rs b/third_party/rust/coreaudio-sys-utils/src/aggregate_device.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/aggregate_device.rs rename to third_party/rust/coreaudio-sys-utils/src/aggregate_device.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/audio_object.rs b/third_party/rust/coreaudio-sys-utils/src/audio_object.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/audio_object.rs rename to third_party/rust/coreaudio-sys-utils/src/audio_object.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/audio_unit.rs b/third_party/rust/coreaudio-sys-utils/src/audio_unit.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/audio_unit.rs rename to third_party/rust/coreaudio-sys-utils/src/audio_unit.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/cf_mutable_dict.rs b/third_party/rust/coreaudio-sys-utils/src/cf_mutable_dict.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/cf_mutable_dict.rs rename to third_party/rust/coreaudio-sys-utils/src/cf_mutable_dict.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/dispatch.rs b/third_party/rust/coreaudio-sys-utils/src/dispatch.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/dispatch.rs rename to third_party/rust/coreaudio-sys-utils/src/dispatch.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/lib.rs b/third_party/rust/coreaudio-sys-utils/src/lib.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/lib.rs rename to third_party/rust/coreaudio-sys-utils/src/lib.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/string.rs b/third_party/rust/coreaudio-sys-utils/src/string.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/coreaudio-sys-utils/src/string.rs rename to third_party/rust/coreaudio-sys-utils/src/string.rs diff --git a/third_party/rust/cubeb-coreaudio/.cargo-checksum.json b/third_party/rust/cubeb-coreaudio/.cargo-checksum.json new file mode 100644 index 000000000000..c5301e717f8b --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/.cargo-checksum.json @@ -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} \ No newline at end of file diff --git a/third_party/rust/cubeb-coreaudio/.editorconfig b/third_party/rust/cubeb-coreaudio/.editorconfig new file mode 100644 index 000000000000..9e636725f190 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/third_party/rust/cubeb-coreaudio/.travis.yml b/third_party/rust/cubeb-coreaudio/.travis.yml new file mode 100644 index 000000000000..a5058ffae0f9 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/.travis.yml @@ -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 diff --git a/media/libcubeb/cubeb-coreaudio-rs/Cargo.toml b/third_party/rust/cubeb-coreaudio/Cargo.toml similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/Cargo.toml rename to third_party/rust/cubeb-coreaudio/Cargo.toml diff --git a/media/libcubeb/cubeb-coreaudio-rs/LICENSE b/third_party/rust/cubeb-coreaudio/LICENSE similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/LICENSE rename to third_party/rust/cubeb-coreaudio/LICENSE diff --git a/third_party/rust/cubeb-coreaudio/README.md b/third_party/rust/cubeb-coreaudio/README.md new file mode 100644 index 000000000000..fc62e9ea9b0e --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/README.md @@ -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 \ No newline at end of file diff --git a/third_party/rust/cubeb-coreaudio/run_tests.sh b/third_party/rust/cubeb-coreaudio/run_tests.sh new file mode 100644 index 000000000000..0d01d4e927d5 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/run_tests.sh @@ -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 diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/backend/aggregate_device.rs b/third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/src/backend/aggregate_device.rs rename to third_party/rust/cubeb-coreaudio/src/backend/aggregate_device.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/backend/auto_array.rs b/third_party/rust/cubeb-coreaudio/src/backend/auto_array.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/src/backend/auto_array.rs rename to third_party/rust/cubeb-coreaudio/src/backend/auto_array.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/backend/auto_release.rs b/third_party/rust/cubeb-coreaudio/src/backend/auto_release.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/src/backend/auto_release.rs rename to third_party/rust/cubeb-coreaudio/src/backend/auto_release.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/backend/device_property.rs b/third_party/rust/cubeb-coreaudio/src/backend/device_property.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/src/backend/device_property.rs rename to third_party/rust/cubeb-coreaudio/src/backend/device_property.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/backend/mixer.rs b/third_party/rust/cubeb-coreaudio/src/backend/mixer.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/src/backend/mixer.rs rename to third_party/rust/cubeb-coreaudio/src/backend/mixer.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/backend/mod.rs b/third_party/rust/cubeb-coreaudio/src/backend/mod.rs similarity index 99% rename from media/libcubeb/cubeb-coreaudio-rs/src/backend/mod.rs rename to third_party/rust/cubeb-coreaudio/src/backend/mod.rs index e8685aef8f76..c61d73335394 100644 --- a/media/libcubeb/cubeb-coreaudio-rs/src/backend/mod.rs +++ b/third_party/rust/cubeb-coreaudio/src/backend/mod.rs @@ -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; diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/backend/resampler.rs b/third_party/rust/cubeb-coreaudio/src/backend/resampler.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/src/backend/resampler.rs rename to third_party/rust/cubeb-coreaudio/src/backend/resampler.rs diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs new file mode 100644 index 000000000000..d28e1a98a208 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/aggregate_device.rs @@ -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) -> Vec { + 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) -> Vec { + assert!(!devices.is_empty()); + let mut compensations = Vec::new(); + for device in devices { + let compensation = test_get_drift_compensations(*device).unwrap(); + compensations.push(compensation); + } + + compensations +} + +// AggregateDevice::destroy_device +// ------------------------------------ +#[test] +#[ignore] +#[should_panic] +fn test_aggregate_destroy_aggregate_device_for_a_unknown_plugin_device() { + let plugin = AggregateDevice::get_system_plugin_id().unwrap(); + let device = AggregateDevice::create_blank_device_sync(plugin).unwrap(); + assert!(AggregateDevice::destroy_device(kAudioObjectUnknown, device).is_err()); +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs new file mode 100644 index 000000000000..71343e262b51 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/api.rs @@ -0,0 +1,1996 @@ +use super::utils::{ + test_audiounit_get_buffer_frame_size, test_audiounit_scope_is_enabled, test_create_audiounit, + test_device_channels_in_scope, test_device_in_scope, test_get_all_devices, + test_get_default_audiounit, test_get_default_device, test_get_default_raw_stream, + test_get_default_source_name, test_get_devices_in_scope, test_get_raw_context, + ComponentSubType, PropertyScope, Scope, +}; +use super::*; +use std::any::Any; +use std::fmt::Debug; + +// make_sized_audio_channel_layout +// ------------------------------------ +#[test] +fn test_make_sized_audio_channel_layout() { + for channels in 1..10 { + let size = mem::size_of::() + + (channels - 1) * mem::size_of::(); + let _ = make_sized_audio_channel_layout(size); + } +} + +#[test] +#[should_panic] +fn test_make_sized_audio_channel_layout_with_wrong_size() { + // let _ = make_sized_audio_channel_layout(0); + let one_channel_size = mem::size_of::(); + let padding_size = 10; + assert_ne!(mem::size_of::(), padding_size); + let wrong_size = one_channel_size + padding_size; + let _ = make_sized_audio_channel_layout(wrong_size); +} + +// has_input +// ------------------------------------ +// TODO + +// has_output +// ------------------------------------ +// TODO + +// channel_label_to_cubeb_channel +// ------------------------------------ +// Convert a CAChannelLabel into a ChannelLayout +#[test] +fn test_channel_label_to_cubeb_channel_layout() { + let pairs = [ + (kAudioChannelLabel_Left, ChannelLayout::FRONT_LEFT), + (kAudioChannelLabel_Right, ChannelLayout::FRONT_RIGHT), + (kAudioChannelLabel_Center, ChannelLayout::FRONT_CENTER), + (kAudioChannelLabel_LFEScreen, ChannelLayout::LOW_FREQUENCY), + (kAudioChannelLabel_LeftSurround, ChannelLayout::BACK_LEFT), + (kAudioChannelLabel_RightSurround, ChannelLayout::BACK_RIGHT), + ( + kAudioChannelLabel_LeftCenter, + ChannelLayout::FRONT_LEFT_OF_CENTER, + ), + ( + kAudioChannelLabel_RightCenter, + ChannelLayout::FRONT_RIGHT_OF_CENTER, + ), + ( + kAudioChannelLabel_CenterSurround, + ChannelLayout::BACK_CENTER, + ), + ( + kAudioChannelLabel_LeftSurroundDirect, + ChannelLayout::SIDE_LEFT, + ), + ( + kAudioChannelLabel_RightSurroundDirect, + ChannelLayout::SIDE_RIGHT, + ), + ( + kAudioChannelLabel_TopCenterSurround, + ChannelLayout::TOP_CENTER, + ), + ( + kAudioChannelLabel_VerticalHeightLeft, + ChannelLayout::TOP_FRONT_LEFT, + ), + ( + kAudioChannelLabel_VerticalHeightCenter, + ChannelLayout::TOP_FRONT_CENTER, + ), + ( + kAudioChannelLabel_VerticalHeightRight, + ChannelLayout::TOP_FRONT_RIGHT, + ), + (kAudioChannelLabel_TopBackLeft, ChannelLayout::TOP_BACK_LEFT), + ( + kAudioChannelLabel_TopBackCenter, + ChannelLayout::TOP_BACK_CENTER, + ), + ( + kAudioChannelLabel_TopBackRight, + ChannelLayout::TOP_BACK_RIGHT, + ), + (kAudioChannelLabel_Unknown, ChannelLayout::UNDEFINED), + ]; + + for (label, channel) in pairs.iter() { + let channel_label = CAChannelLabel(*label); + let layout: ChannelLayout = channel_label.into(); + assert_eq!(layout, *channel); + } +} + +// cubeb_channel_to_channel_label +// ------------------------------------ +// Convert a ChannelLayout into a CAChannelLabel +#[test] +fn test_cubeb_channel_layout_to_channel_label() { + let pairs = [ + (ChannelLayout::FRONT_LEFT, kAudioChannelLabel_Left), + (ChannelLayout::FRONT_RIGHT, kAudioChannelLabel_Right), + (ChannelLayout::FRONT_CENTER, kAudioChannelLabel_Center), + (ChannelLayout::LOW_FREQUENCY, kAudioChannelLabel_LFEScreen), + (ChannelLayout::BACK_LEFT, kAudioChannelLabel_LeftSurround), + (ChannelLayout::BACK_RIGHT, kAudioChannelLabel_RightSurround), + ( + ChannelLayout::FRONT_LEFT_OF_CENTER, + kAudioChannelLabel_LeftCenter, + ), + ( + ChannelLayout::FRONT_RIGHT_OF_CENTER, + kAudioChannelLabel_RightCenter, + ), + ( + ChannelLayout::BACK_CENTER, + kAudioChannelLabel_CenterSurround, + ), + ( + ChannelLayout::SIDE_LEFT, + kAudioChannelLabel_LeftSurroundDirect, + ), + ( + ChannelLayout::SIDE_RIGHT, + kAudioChannelLabel_RightSurroundDirect, + ), + ( + ChannelLayout::TOP_CENTER, + kAudioChannelLabel_TopCenterSurround, + ), + ( + ChannelLayout::TOP_FRONT_LEFT, + kAudioChannelLabel_VerticalHeightLeft, + ), + ( + ChannelLayout::TOP_FRONT_CENTER, + kAudioChannelLabel_VerticalHeightCenter, + ), + ( + ChannelLayout::TOP_FRONT_RIGHT, + kAudioChannelLabel_VerticalHeightRight, + ), + (ChannelLayout::TOP_BACK_LEFT, kAudioChannelLabel_TopBackLeft), + ( + ChannelLayout::TOP_BACK_CENTER, + kAudioChannelLabel_TopBackCenter, + ), + ( + ChannelLayout::TOP_BACK_RIGHT, + kAudioChannelLabel_TopBackRight, + ), + ]; + + for (channel, label) in pairs.iter() { + let channel_label = CAChannelLabel(*label); + assert_eq!(CAChannelLabel::from(*channel), channel_label); + } +} + +#[test] +#[should_panic] +fn test_cubeb_channel_layout_to_channel_label_with_invalid_channel() { + let _label = CAChannelLabel::from(ChannelLayout::_3F4_LFE); +} + +#[test] +#[should_panic] +fn test_cubeb_channel_layout_to_channel_label_with_unknown_channel() { + assert_eq!( + ChannelLayout::from(ffi::CHANNEL_UNKNOWN), + ChannelLayout::UNDEFINED + ); + let _label = CAChannelLabel::from(ChannelLayout::UNDEFINED); +} + +// active_streams +// update_latency_by_adding_stream +// update_latency_by_removing_stream +// ------------------------------------ +#[test] +fn test_increase_and_decrease_context_streams() { + use std::thread; + const STREAMS: u32 = 10; + + let context = AudioUnitContext::new(); + let context_ptr_value = &context as *const AudioUnitContext as usize; + + let mut join_handles = vec![]; + for i in 0..STREAMS { + join_handles.push(thread::spawn(move || { + let context = unsafe { &*(context_ptr_value as *const AudioUnitContext) }; + let global_latency = context.update_latency_by_adding_stream(i); + global_latency + })); + } + let mut latencies = vec![]; + for handle in join_handles { + latencies.push(handle.join().unwrap()); + } + assert_eq!(context.active_streams(), STREAMS); + check_streams(&context, STREAMS); + + check_latency(&context, latencies[0]); + for i in 0..latencies.len() - 1 { + assert_eq!(latencies[i], latencies[i + 1]); + } + + let mut join_handles = vec![]; + for _ in 0..STREAMS { + join_handles.push(thread::spawn(move || { + let context = unsafe { &*(context_ptr_value as *const AudioUnitContext) }; + context.update_latency_by_removing_stream(); + })); + } + for handle in join_handles { + let _ = handle.join(); + } + check_streams(&context, 0); + + check_latency(&context, None); +} + +fn check_streams(context: &AudioUnitContext, number: u32) { + let guard = context.latency_controller.lock().unwrap(); + assert_eq!(guard.streams, number); +} + +fn check_latency(context: &AudioUnitContext, latency: Option) { + let guard = context.latency_controller.lock().unwrap(); + assert_eq!(guard.latency, latency); +} + +// make_silent +// ------------------------------------ +#[test] +fn test_make_silent() { + let mut array = allocate_array::(10); + for data in array.iter_mut() { + *data = 0xFFFF; + } + + let mut buffer = AudioBuffer::default(); + buffer.mData = array.as_mut_ptr() as *mut c_void; + buffer.mDataByteSize = (array.len() * mem::size_of::()) as u32; + buffer.mNumberChannels = 1; + + audiounit_make_silent(&mut buffer); + for data in array { + assert_eq!(data, 0); + } +} + +// render_input +// ------------------------------------ +// TODO + +// input_callback +// ------------------------------------ +// TODO + +// minimum_resampling_input_frames +// ------------------------------------ +#[test] +fn test_minimum_resampling_input_frames() { + let input_rate = 48000_f64; + let output_rate = 44100_f64; + + let frames = 100; + let times = input_rate / output_rate; + let expected = (frames as f64 * times).ceil() as i64; + + assert_eq!( + minimum_resampling_input_frames(input_rate, output_rate, frames), + expected + ); +} + +#[test] +#[should_panic] +fn test_minimum_resampling_input_frames_zero_input_rate() { + minimum_resampling_input_frames(0_f64, 44100_f64, 1); +} + +#[test] +#[should_panic] +fn test_minimum_resampling_input_frames_zero_output_rate() { + minimum_resampling_input_frames(48000_f64, 0_f64, 1); +} + +#[test] +fn test_minimum_resampling_input_frames_equal_input_output_rate() { + let frames = 100; + assert_eq!( + minimum_resampling_input_frames(44100_f64, 44100_f64, frames), + frames + ); +} + +// output_callback +// ------------------------------------ +// TODO + +// create_device_info +// ------------------------------------ +#[test] +fn test_create_device_info_from_unknown_input_device() { + if let Some(default_device_id) = test_get_default_device(Scope::Input) { + let default_device = create_device_info(kAudioObjectUnknown, DeviceType::INPUT).unwrap(); + assert_eq!(default_device.id, default_device_id); + assert_eq!( + default_device.flags, + device_flags::DEV_INPUT + | device_flags::DEV_SELECTED_DEFAULT + | device_flags::DEV_SYSTEM_DEFAULT + ); + } else { + println!("No input device to perform test."); + } +} + +#[test] +fn test_create_device_info_from_unknown_output_device() { + if let Some(default_device_id) = test_get_default_device(Scope::Output) { + let default_device = create_device_info(kAudioObjectUnknown, DeviceType::OUTPUT).unwrap(); + assert_eq!(default_device.id, default_device_id); + assert_eq!( + default_device.flags, + device_flags::DEV_OUTPUT + | device_flags::DEV_SELECTED_DEFAULT + | device_flags::DEV_SYSTEM_DEFAULT + ); + } else { + println!("No output device to perform test."); + } +} + +#[test] +#[should_panic] +fn test_set_device_info_to_system_input_device() { + let _device = create_device_info(kAudioObjectSystemObject, DeviceType::INPUT); +} + +#[test] +#[should_panic] +fn test_set_device_info_to_system_output_device() { + let _device = create_device_info(kAudioObjectSystemObject, DeviceType::OUTPUT); +} + +// FIXIT: Is it ok to set input device to a nonexistent device ? +#[ignore] +#[test] +#[should_panic] +fn test_set_device_info_to_nonexistent_input_device() { + let nonexistent_id = std::u32::MAX; + let _device = create_device_info(nonexistent_id, DeviceType::INPUT); +} + +// FIXIT: Is it ok to set output device to a nonexistent device ? +#[ignore] +#[test] +#[should_panic] +fn test_set_device_info_to_nonexistent_output_device() { + let nonexistent_id = std::u32::MAX; + let _device = create_device_info(nonexistent_id, DeviceType::OUTPUT); +} + +// reinit_stream +// ------------------------------------ +// TODO + +// reinit_stream_async +// ------------------------------------ +// TODO + +// event_addr_to_string +// ------------------------------------ +// TODO + +// property_listener_callback +// ------------------------------------ +// TODO + +// add_listener (for default output device) +// ------------------------------------ +#[test] +fn test_add_listener_unknown_device() { + extern "C" fn callback( + _id: AudioObjectID, + _number_of_addresses: u32, + _addresses: *const AudioObjectPropertyAddress, + _data: *mut c_void, + ) -> OSStatus { + assert!(false, "Should not be called."); + kAudioHardwareUnspecifiedError as OSStatus + } + + test_get_default_raw_stream(|stream| { + let listener = device_property_listener::new( + kAudioObjectUnknown, + get_property_address( + Property::HardwareDefaultOutputDevice, + DeviceType::INPUT | DeviceType::OUTPUT, + ), + callback, + ); + assert_eq!( + stream.add_device_listener(&listener), + kAudioHardwareBadObjectError as OSStatus + ); + }); +} + +// remove_listener (for default output device) +// ------------------------------------ +#[test] +fn test_add_listener_then_remove_system_device() { + extern "C" fn callback( + _id: AudioObjectID, + _number_of_addresses: u32, + _addresses: *const AudioObjectPropertyAddress, + _data: *mut c_void, + ) -> OSStatus { + assert!(false, "Should not be called."); + kAudioHardwareUnspecifiedError as OSStatus + } + + test_get_default_raw_stream(|stream| { + let listener = device_property_listener::new( + kAudioObjectSystemObject, + get_property_address( + Property::HardwareDefaultOutputDevice, + DeviceType::INPUT | DeviceType::OUTPUT, + ), + callback, + ); + assert_eq!(stream.add_device_listener(&listener), NO_ERR); + assert_eq!(stream.remove_device_listener(&listener), NO_ERR); + }); +} + +#[test] +fn test_remove_listener_without_adding_any_listener_before_system_device() { + extern "C" fn callback( + _id: AudioObjectID, + _number_of_addresses: u32, + _addresses: *const AudioObjectPropertyAddress, + _data: *mut c_void, + ) -> OSStatus { + assert!(false, "Should not be called."); + kAudioHardwareUnspecifiedError as OSStatus + } + + test_get_default_raw_stream(|stream| { + let listener = device_property_listener::new( + kAudioObjectSystemObject, + get_property_address( + Property::HardwareDefaultOutputDevice, + DeviceType::INPUT | DeviceType::OUTPUT, + ), + callback, + ); + assert_eq!(stream.remove_device_listener(&listener), NO_ERR); + }); +} + +#[test] +fn test_remove_listener_unknown_device() { + extern "C" fn callback( + _id: AudioObjectID, + _number_of_addresses: u32, + _addresses: *const AudioObjectPropertyAddress, + _data: *mut c_void, + ) -> OSStatus { + assert!(false, "Should not be called."); + kAudioHardwareUnspecifiedError as OSStatus + } + + test_get_default_raw_stream(|stream| { + let listener = device_property_listener::new( + kAudioObjectUnknown, + get_property_address( + Property::HardwareDefaultOutputDevice, + DeviceType::INPUT | DeviceType::OUTPUT, + ), + callback, + ); + assert_eq!( + stream.remove_device_listener(&listener), + kAudioHardwareBadObjectError as OSStatus + ); + }); +} + +// install_system_changed_callback +// ------------------------------------ +// TODO + +// uninstall_system_changed_callback +// ------------------------------------ +// TODO + +// get_default_device_id +// ------------------------------------ +#[test] +fn test_get_default_device_id() { + if test_get_default_device(Scope::Input).is_some() { + assert_ne!( + audiounit_get_default_device_id(DeviceType::INPUT), + kAudioObjectUnknown, + ); + } + + if test_get_default_device(Scope::Output).is_some() { + assert_ne!( + audiounit_get_default_device_id(DeviceType::OUTPUT), + kAudioObjectUnknown, + ); + } +} + +#[test] +#[should_panic] +fn test_get_default_device_id_with_unknown_type() { + assert_eq!( + audiounit_get_default_device_id(DeviceType::UNKNOWN), + kAudioObjectUnknown, + ); +} + +#[test] +#[should_panic] +fn test_get_default_device_id_with_inout_type() { + assert_eq!( + audiounit_get_default_device_id(DeviceType::INPUT | DeviceType::OUTPUT), + kAudioObjectUnknown, + ); +} + +// convert_channel_layout +// ------------------------------------ +#[test] +fn test_convert_channel_layout() { + let pairs = [ + // The single channel is mapped to mono now. + (vec![kAudioObjectUnknown], ChannelLayout::MONO), + (vec![kAudioChannelLabel_Mono], ChannelLayout::MONO), + // The dual channels are mapped to stereo now. + ( + vec![kAudioChannelLabel_Mono, kAudioChannelLabel_LFEScreen], + ChannelLayout::STEREO, + ), + ( + vec![kAudioChannelLabel_Left, kAudioChannelLabel_Right], + ChannelLayout::STEREO, + ), + // The Layouts containing any unknonwn channel will be mapped to UNDEFINED. + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Unknown, + ], + ChannelLayout::UNDEFINED, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Unused, + ], + ChannelLayout::UNDEFINED, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_ForeignLanguage, + ], + ChannelLayout::UNDEFINED, + ), + // The SMPTE layouts. + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LFEScreen, + ], + ChannelLayout::STEREO_LFE, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + ], + ChannelLayout::_3F, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_LFEScreen, + ], + ChannelLayout::_3F_LFE, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_CenterSurround, + ], + ChannelLayout::_2F1, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_CenterSurround, + kAudioChannelLabel_LFEScreen, + ], + ChannelLayout::_2F1_LFE, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_CenterSurround, + ], + ChannelLayout::_3F1, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_CenterSurround, + kAudioChannelLabel_LFEScreen, + ], + ChannelLayout::_3F1_LFE, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + ], + ChannelLayout::_2F2, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + kAudioChannelLabel_LFEScreen, + ], + ChannelLayout::_2F2_LFE, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurround, + kAudioChannelLabel_RightSurround, + ], + ChannelLayout::QUAD, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurround, + kAudioChannelLabel_RightSurround, + kAudioChannelLabel_LFEScreen, + ], + ChannelLayout::QUAD_LFE, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + ], + ChannelLayout::_3F2, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + kAudioChannelLabel_LFEScreen, + ], + ChannelLayout::_3F2_LFE, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurround, + kAudioChannelLabel_RightSurround, + kAudioChannelLabel_Center, + ], + ChannelLayout::_3F2_BACK, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_LeftSurround, + kAudioChannelLabel_RightSurround, + kAudioChannelLabel_Center, + kAudioChannelLabel_LFEScreen, + ], + ChannelLayout::_3F2_LFE_BACK, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_LFEScreen, + kAudioChannelLabel_CenterSurround, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + ], + ChannelLayout::_3F3R_LFE, + ), + ( + vec![ + kAudioChannelLabel_Left, + kAudioChannelLabel_Right, + kAudioChannelLabel_Center, + kAudioChannelLabel_LFEScreen, + kAudioChannelLabel_LeftSurround, + kAudioChannelLabel_RightSurround, + kAudioChannelLabel_LeftSurroundDirect, + kAudioChannelLabel_RightSurroundDirect, + ], + ChannelLayout::_3F4_LFE, + ), + ]; + + const MAX_CHANNELS: usize = 10; + // A Rust mapping structure of the AudioChannelLayout with MAX_CHANNELS channels + // https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.13.sdk/System/Library/Frameworks/CoreAudio.framework/Versions/A/Headers/CoreAudioTypes.h#L1332 + #[repr(C)] + struct TestLayout { + tag: AudioChannelLayoutTag, + map: AudioChannelBitmap, + number_channel_descriptions: UInt32, + channel_descriptions: [AudioChannelDescription; MAX_CHANNELS], + } + + impl Default for TestLayout { + fn default() -> Self { + Self { + tag: AudioChannelLayoutTag::default(), + map: AudioChannelBitmap::default(), + number_channel_descriptions: UInt32::default(), + channel_descriptions: [AudioChannelDescription::default(); MAX_CHANNELS], + } + } + } + + let mut layout = TestLayout::default(); + layout.tag = kAudioChannelLayoutTag_UseChannelDescriptions; + + for (labels, expected_layout) in pairs.iter() { + assert!(labels.len() <= MAX_CHANNELS); + layout.number_channel_descriptions = labels.len() as u32; + for (idx, label) in labels.iter().enumerate() { + layout.channel_descriptions[idx].mChannelLabel = *label; + } + let layout_ref = unsafe { &(*(&layout as *const TestLayout as *const AudioChannelLayout)) }; + assert_eq!( + audiounit_convert_channel_layout(layout_ref), + *expected_layout + ); + } +} + +// get_preferred_channel_layout +// ------------------------------------ +#[test] +fn test_get_preferred_channel_layout_output() { + // Predefined whitelist + use std::collections::HashMap; + let devices_layouts: HashMap<&'static str, ChannelLayout> = [ + ("hdpn", ChannelLayout::STEREO), + ("ispk", ChannelLayout::STEREO), + ("FApd", ChannelLayout::STEREO), + ] + .into_iter() + .cloned() + .collect(); + + let source = test_get_default_source_name(Scope::Output); + let unit = test_get_default_audiounit(Scope::Output); + if source.is_none() || unit.is_none() { + println!("No output audiounit or device source name found."); + return; + } + + let source = source.unwrap(); + let unit = unit.unwrap(); + if let Some(layout) = devices_layouts.get(source.as_str()) { + assert_eq!( + audiounit_get_preferred_channel_layout(unit.get_inner()), + *layout + ); + } else { + println!("Device {} is not in the whitelist.", source); + } +} + +// TODO: Should it be banned ? It only works with output audiounit for now. +// #[test] +// fn test_get_preferred_channel_layout_input() { +// } + +// get_current_channel_layout +// ------------------------------------ +#[test] +fn test_get_current_channel_layout_output() { + // Predefined whitelist + use std::collections::HashMap; + let devices_layouts: HashMap<&'static str, ChannelLayout> = [ + ("hdpn", ChannelLayout::STEREO), + ("ispk", ChannelLayout::STEREO), + ("FApd", ChannelLayout::STEREO), + ] + .into_iter() + .cloned() + .collect(); + + let source = test_get_default_source_name(Scope::Output); + let unit = test_get_default_audiounit(Scope::Output); + if source.is_none() || unit.is_none() { + println!("No output audiounit or device source name found."); + return; + } + + let source = source.unwrap(); + let unit = unit.unwrap(); + if let Some(layout) = devices_layouts.get(source.as_str()) { + assert_eq!( + audiounit_get_current_channel_layout(unit.get_inner()), + *layout + ); + } else { + println!("Device {} is not in the whitelist.", source); + } +} + +// TODO: Should it be banned ? It only works with output audiounit for now. +// #[test] +// fn test_get_current_channel_layout_input() { +// } + +// create_stream_description +// ------------------------------------ +#[test] +fn test_create_stream_description() { + let mut channels = 0; + for (bits, format, flags) in [ + ( + 16_u32, + ffi::CUBEB_SAMPLE_S16LE, + kAudioFormatFlagIsSignedInteger, + ), + ( + 16_u32, + ffi::CUBEB_SAMPLE_S16BE, + kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsBigEndian, + ), + (32_u32, ffi::CUBEB_SAMPLE_FLOAT32LE, kAudioFormatFlagIsFloat), + ( + 32_u32, + ffi::CUBEB_SAMPLE_FLOAT32BE, + kAudioFormatFlagIsFloat | kAudioFormatFlagIsBigEndian, + ), + ] + .iter() + { + let bytes = bits / 8; + channels += 1; + + let mut raw = ffi::cubeb_stream_params::default(); + raw.format = *format; + raw.rate = 48_000; + raw.channels = channels; + raw.layout = ffi::CUBEB_LAYOUT_UNDEFINED; + raw.prefs = ffi::CUBEB_STREAM_PREF_NONE; + let params = StreamParams::from(raw); + let description = create_stream_description(¶ms).unwrap(); + assert_eq!(description.mFormatID, kAudioFormatLinearPCM); + assert_eq!( + description.mFormatFlags, + flags | kLinearPCMFormatFlagIsPacked + ); + assert_eq!(description.mSampleRate as u32, raw.rate); + assert_eq!(description.mChannelsPerFrame, raw.channels); + assert_eq!(description.mBytesPerFrame, bytes * raw.channels); + assert_eq!(description.mFramesPerPacket, 1); + assert_eq!(description.mBytesPerPacket, bytes * raw.channels); + assert_eq!(description.mReserved, 0); + } +} + +// set_channel_layout +// ------------------------------------ +#[test] +fn test_set_channel_layout_output() { + // Predefined whitelist + use std::collections::HashMap; + let devices_layouts: HashMap<&'static str, ChannelLayout> = [ + ("hdpn", ChannelLayout::STEREO), + ("ispk", ChannelLayout::STEREO), + ("FApd", ChannelLayout::STEREO), + ] + .into_iter() + .cloned() + .collect(); + + let source = test_get_default_source_name(Scope::Output); + let unit = test_get_default_audiounit(Scope::Output); + if source.is_none() || unit.is_none() { + println!("No output audiounit or device source name found."); + return; + } + + let source = source.unwrap(); + let unit = unit.unwrap(); + if let Some(layout) = devices_layouts.get(source.as_str()) { + assert!(audiounit_set_channel_layout(unit.get_inner(), *layout).is_ok()); + assert_eq!( + audiounit_get_current_channel_layout(unit.get_inner()), + *layout + ); + } else { + println!("Device {} is not in the whitelist.", source); + } +} + +#[test] +fn test_set_channel_layout_output_undefind() { + if let Some(unit) = test_get_default_audiounit(Scope::Output) { + // Get original layout. + let original_layout = audiounit_get_current_channel_layout(unit.get_inner()); + // Leave layout as it is. + assert!(audiounit_set_channel_layout(unit.get_inner(), ChannelLayout::UNDEFINED).is_ok()); + // Check the layout is same as the original one. + assert_eq!( + audiounit_get_current_channel_layout(unit.get_inner()), + original_layout + ); + } else { + println!("No output audiounit."); + } +} + +#[test] +fn test_set_channel_layout_input() { + if let Some(unit) = test_get_default_audiounit(Scope::Input) { + // Get original layout. + let original_layout = audiounit_get_current_channel_layout(unit.get_inner()); + // Leave layout as it is. + assert!(audiounit_set_channel_layout(unit.get_inner(), ChannelLayout::UNDEFINED).is_ok()); + // Check the layout is same as the original one. + assert_eq!( + audiounit_get_current_channel_layout(unit.get_inner()), + original_layout + ); + } else { + println!("No input audiounit."); + } +} + +#[test] +#[should_panic] +fn test_set_channel_layout_with_null_unit() { + assert!(audiounit_set_channel_layout(ptr::null_mut(), ChannelLayout::UNDEFINED).is_err()); +} + +// create_default_audiounit +// ------------------------------------ +#[test] +fn test_create_default_audiounit() { + let flags_list = [ + device_flags::DEV_UNKNOWN, + device_flags::DEV_INPUT, + device_flags::DEV_OUTPUT, + device_flags::DEV_INPUT | device_flags::DEV_OUTPUT, + device_flags::DEV_INPUT | device_flags::DEV_SYSTEM_DEFAULT, + device_flags::DEV_OUTPUT | device_flags::DEV_SYSTEM_DEFAULT, + device_flags::DEV_INPUT | device_flags::DEV_OUTPUT | device_flags::DEV_SYSTEM_DEFAULT, + ]; + + for flags in flags_list.iter() { + let unit = create_default_audiounit(*flags).unwrap(); + assert!(!unit.is_null()); + // Destroy the AudioUnits + unsafe { + AudioUnitUninitialize(unit); + AudioComponentInstanceDispose(unit); + } + } +} + +// enable_audiounit_scope +// ------------------------------------ +#[test] +fn test_enable_audiounit_scope() { + // It's ok to enable and disable the scopes of input or output + // for the unit whose subtype is kAudioUnitSubType_HALOutput + // even when there is no available input or output devices. + if let Some(unit) = test_create_audiounit(ComponentSubType::HALOutput) { + assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, true).is_ok()); + assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, false).is_ok()); + assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, true).is_ok()); + assert!(enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, false).is_ok()); + } else { + println!("No audiounit to perform test."); + } +} + +#[test] +fn test_enable_audiounit_scope_for_default_output_unit() { + if let Some(unit) = test_create_audiounit(ComponentSubType::DefaultOutput) { + assert_eq!( + enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, true).unwrap_err(), + kAudioUnitErr_InvalidProperty + ); + assert_eq!( + enable_audiounit_scope(unit.get_inner(), DeviceType::OUTPUT, false).unwrap_err(), + kAudioUnitErr_InvalidProperty + ); + assert_eq!( + enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, true).unwrap_err(), + kAudioUnitErr_InvalidProperty + ); + assert_eq!( + enable_audiounit_scope(unit.get_inner(), DeviceType::INPUT, false).unwrap_err(), + kAudioUnitErr_InvalidProperty + ); + } +} + +#[test] +#[should_panic] +fn test_enable_audiounit_scope_with_null_unit() { + let unit: AudioUnit = ptr::null_mut(); + assert!(enable_audiounit_scope(unit, DeviceType::INPUT, false).is_err()); +} + +// create_audiounit +// ------------------------------------ +#[test] +fn test_for_create_audiounit() { + let flags_list = [ + device_flags::DEV_INPUT, + device_flags::DEV_OUTPUT, + device_flags::DEV_INPUT | device_flags::DEV_SYSTEM_DEFAULT, + device_flags::DEV_OUTPUT | device_flags::DEV_SYSTEM_DEFAULT, + ]; + + let default_input = test_get_default_device(Scope::Input); + let default_output = test_get_default_device(Scope::Output); + + for flags in flags_list.iter() { + let mut device = device_info::default(); + device.flags |= *flags; + + // Check the output scope is enabled. + if device.flags.contains(device_flags::DEV_OUTPUT) && default_output.is_some() { + let device_id = default_output.clone().unwrap(); + device.id = device_id; + let unit = create_audiounit(&device).unwrap(); + assert!(!unit.is_null()); + assert!(test_audiounit_scope_is_enabled(unit, Scope::Output)); + + // For default output device, the input scope is enabled + // if it's also a input device. Otherwise, it's disabled. + if device + .flags + .contains(device_flags::DEV_INPUT | device_flags::DEV_SYSTEM_DEFAULT) + { + assert_eq!( + test_device_in_scope(device_id, Scope::Input), + test_audiounit_scope_is_enabled(unit, Scope::Input) + ); + + // Destroy the audioUnit. + unsafe { + AudioUnitUninitialize(unit); + AudioComponentInstanceDispose(unit); + } + continue; + } + + // Destroy the audioUnit. + unsafe { + AudioUnitUninitialize(unit); + AudioComponentInstanceDispose(unit); + } + } + + // Check the input scope is enabled. + if device.flags.contains(device_flags::DEV_INPUT) && default_input.is_some() { + let device_id = default_input.clone().unwrap(); + device.id = device_id; + let unit = create_audiounit(&device).unwrap(); + assert!(!unit.is_null()); + assert!(test_audiounit_scope_is_enabled(unit, Scope::Input)); + // Destroy the audioUnit. + unsafe { + AudioUnitUninitialize(unit); + AudioComponentInstanceDispose(unit); + } + } + } +} + +#[test] +#[should_panic] +fn test_create_audiounit_with_unknown_scope() { + let device = device_info::default(); + let _unit = create_audiounit(&device); +} + +// create_auto_array +// ------------------------------------ +#[test] +fn test_create_auto_array() { + let buffer_f32 = [3.1_f32, 4.1, 5.9, 2.6, 5.35]; + let buffer_i16 = [13_i16, 21, 34, 55, 89, 144]; + + // Test if the stream latency frame is 4096 + test_create_auto_array_impl(&buffer_f32, 4096); + test_create_auto_array_impl(&buffer_i16, 4096); +} + +#[test] +#[should_panic] +fn test_create_auto_array_with_zero_latency_f32() { + let buffer_f32 = [3.1_f32, 4.1, 5.9, 2.6, 5.35]; + test_create_auto_array_impl(&buffer_f32, 0); +} + +#[test] +#[should_panic] +fn test_create_auto_array_with_zero_latency_i16() { + let buffer_i16 = [13_i16, 21, 34, 55, 89, 144]; + test_create_auto_array_impl(&buffer_i16, 0); +} + +fn test_create_auto_array_impl(buffer: &[T], latency: u32) { + const CHANNEL: u32 = 2; + const BUF_CAPACITY: usize = 1; + + let type_id = std::any::TypeId::of::(); + let format = if type_id == std::any::TypeId::of::() { + kAudioFormatFlagIsFloat + } else if type_id == std::any::TypeId::of::() { + kAudioFormatFlagIsSignedInteger + } else { + panic!("Unsupported type!"); + }; + + let mut desc = AudioStreamBasicDescription::default(); + desc.mFormatFlags |= format; + desc.mChannelsPerFrame = CHANNEL; + + let mut array = create_auto_array(desc, latency, BUF_CAPACITY).unwrap(); + array.push(buffer.as_ptr() as *const c_void, buffer.len()); + assert_eq!(array.elements(), buffer.len()); + let data = array.as_ptr() as *const T; + for (idx, item) in buffer.iter().enumerate() { + unsafe { + assert_eq!(*data.add(idx), *item); + } + } +} + +#[test] +#[should_panic] +fn test_create_auto_array_with_empty_audiodescription() { + let desc = AudioStreamBasicDescription::default(); + assert_eq!( + create_auto_array(desc, 256, 1).unwrap_err(), + Error::invalid_format() + ); +} + +#[test] +fn test_create_auto_array_with_invalid_audiodescription() { + let mut desc = AudioStreamBasicDescription::default(); + desc.mFormatFlags |= kAudioFormatFlagIsBigEndian; + desc.mChannelsPerFrame = 100; + assert_eq!( + create_auto_array(desc, 256, 1).unwrap_err(), + Error::invalid_format() + ); +} + +// clamp_latency +// ------------------------------------ +#[test] +fn test_clamp_latency() { + let range = 0..2 * SAFE_MAX_LATENCY_FRAMES; + assert!(range.start < SAFE_MIN_LATENCY_FRAMES); + // assert!(range.end < SAFE_MAX_LATENCY_FRAMES); + for latency_frames in range { + let clamp = clamp_latency(latency_frames); + assert!(clamp >= SAFE_MIN_LATENCY_FRAMES); + assert!(clamp <= SAFE_MAX_LATENCY_FRAMES); + } +} + +// set_buffer_size_sync +// ------------------------------------ +#[test] +fn test_set_buffer_size_sync() { + test_set_buffer_size_by_scope(Scope::Input); + test_set_buffer_size_by_scope(Scope::Output); + fn test_set_buffer_size_by_scope(scope: Scope) { + let unit = test_get_default_audiounit(scope.clone()); + if unit.is_none() { + println!("No audiounit for {:?}.", scope); + return; + } + let unit = unit.unwrap(); + let prop_scope = match scope { + Scope::Input => PropertyScope::Output, + Scope::Output => PropertyScope::Input, + }; + let mut buffer_frames = test_audiounit_get_buffer_frame_size( + unit.get_inner(), + scope.clone(), + prop_scope.clone(), + ) + .unwrap(); + assert_ne!(buffer_frames, 0); + buffer_frames *= 2; + assert!( + set_buffer_size_sync(unit.get_inner(), scope.clone().into(), buffer_frames).is_ok() + ); + let new_buffer_frames = + test_audiounit_get_buffer_frame_size(unit.get_inner(), scope.clone(), prop_scope) + .unwrap(); + assert_eq!(buffer_frames, new_buffer_frames); + } +} + +#[test] +#[should_panic] +fn test_set_buffer_size_sync_for_input_with_null_input_unit() { + test_set_buffer_size_sync_by_scope_with_null_unit(Scope::Input); +} + +#[test] +#[should_panic] +fn test_set_buffer_size_sync_for_output_with_null_output_unit() { + test_set_buffer_size_sync_by_scope_with_null_unit(Scope::Output); +} + +fn test_set_buffer_size_sync_by_scope_with_null_unit(scope: Scope) { + let unit: AudioUnit = ptr::null_mut(); + assert!(set_buffer_size_sync(unit, scope.into(), 2048).is_err()); +} + +// setup_stream +// ------------------------------------ +// TODO + +// stream_destroy_internal +// ------------------------------------ +// TODO + +// stream_destroy +// ------------------------------------ +// TODO + +// stream_start_internal +// ------------------------------------ +// TODO + +// stream_start +// ------------------------------------ +// TODO + +// stream_stop_internal +// ------------------------------------ +// TODO + +// get_volume, set_volume +// ------------------------------------ +#[test] +fn test_stream_get_volume() { + if let Some(unit) = test_get_default_audiounit(Scope::Output) { + let expected_volume: f32 = 0.5; + set_volume(unit.get_inner(), expected_volume); + assert_eq!(expected_volume, get_volume(unit.get_inner()).unwrap()); + } else { + println!("No output audiounit."); + } +} + +// convert_uint32_into_string +// ------------------------------------ +#[test] +fn test_convert_uint32_into_string() { + let empty = convert_uint32_into_string(0); + assert_eq!(empty, CString::default()); + + let data: u32 = ('R' as u32) << 24 | ('U' as u32) << 16 | ('S' as u32) << 8 | 'T' as u32; + let data_string = convert_uint32_into_string(data); + assert_eq!(data_string, CString::new("RUST").unwrap()); +} + +// get_default_datasource_string +// ------------------------------------ +#[test] +fn test_get_default_device_name() { + test_get_default_device_name_in_scope(Scope::Input); + test_get_default_device_name_in_scope(Scope::Output); + + fn test_get_default_device_name_in_scope(scope: Scope) { + if let Some(name) = test_get_default_source_name(scope.clone()) { + let source = audiounit_get_default_datasource_string(scope.into()) + .unwrap() + .into_string() + .unwrap(); + assert_eq!(name, source); + } else { + println!("No source name for {:?}", scope); + } + } +} + +// strref_to_cstr_utf8 +// ------------------------------------ +// TODO + +// is_device_a_type_of +// ------------------------------------ +#[test] +fn test_is_device_a_type_of() { + test_is_device_in_scope(Scope::Input); + test_is_device_in_scope(Scope::Output); + + fn test_is_device_in_scope(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + assert!(is_device_a_type_of(device, scope.into())); + } else { + println!("No device for {:?}.", scope); + } + } +} + +// get_channel_count +// ------------------------------------ +#[test] +fn test_get_channel_count() { + test_channel_count(Scope::Input); + test_channel_count(Scope::Output); + + fn test_channel_count(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + let channels = get_channel_count(device, DeviceType::from(scope.clone())).unwrap(); + assert!(channels > 0); + assert_eq!( + channels, + test_device_channels_in_scope(device, scope).unwrap() + ); + } else { + println!("No device for {:?}.", scope); + } + } +} + +#[test] +fn test_get_channel_count_of_input_for_a_output_only_deivce() { + let devices = test_get_devices_in_scope(Scope::Output); + for device in devices { + // Skip in-out devices. + if test_device_in_scope(device, Scope::Input) { + continue; + } + let count = get_channel_count(device, DeviceType::INPUT).unwrap(); + assert_eq!(count, 0); + } +} + +#[test] +fn test_get_channel_count_of_output_for_a_input_only_deivce() { + let devices = test_get_devices_in_scope(Scope::Input); + for device in devices { + // Skip in-out devices. + if test_device_in_scope(device, Scope::Output) { + continue; + } + let count = get_channel_count(device, DeviceType::OUTPUT).unwrap(); + assert_eq!(count, 0); + } +} + +#[test] +#[should_panic] +fn test_get_channel_count_of_unknown_device() { + assert_eq!( + get_channel_count(kAudioObjectUnknown, DeviceType::OUTPUT).unwrap_err(), + Error::error() + ); +} + +#[test] +fn test_get_channel_count_of_inout_type() { + test_channel_count(Scope::Input); + test_channel_count(Scope::Output); + + fn test_channel_count(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + assert_eq!( + // Get a kAudioHardwareUnknownPropertyError in get_channel_count actually. + get_channel_count(device, DeviceType::INPUT | DeviceType::OUTPUT).unwrap_err(), + Error::error() + ); + } else { + println!("No device for {:?}.", scope); + } + } +} + +#[test] +#[should_panic] +fn test_get_channel_count_of_unknwon_type() { + test_channel_count(Scope::Input); + test_channel_count(Scope::Output); + + fn test_channel_count(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + assert_eq!( + get_channel_count(device, DeviceType::UNKNOWN).unwrap_err(), + Error::error() + ); + } else { + panic!("Panic by default: No device for {:?}.", scope); + } + } +} + +// get_range_of_sample_rates +// ------------------------------------ +#[test] +fn test_get_range_of_sample_rates() { + test_get_range_of_sample_rates_in_scope(Scope::Input); + test_get_range_of_sample_rates_in_scope(Scope::Output); + + fn test_get_range_of_sample_rates_in_scope(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + let ranges = test_get_available_samplerate_of_device(device); + for range in ranges { + // Surprisingly, we can get the input/output sample rates from a non-input/non-output device. + check_samplerates(range); + } + } else { + println!("No device for {:?}.", scope); + } + } + + fn test_get_available_samplerate_of_device(id: AudioObjectID) -> Vec<(f64, f64)> { + let scopes = [ + DeviceType::INPUT, + DeviceType::OUTPUT, + DeviceType::INPUT | DeviceType::OUTPUT, + ]; + let mut ranges = Vec::new(); + for scope in scopes.iter() { + ranges.push(get_range_of_sample_rates(id, *scope).unwrap()); + } + ranges + } + + fn check_samplerates((min, max): (f64, f64)) { + assert!(min > 0.0); + assert!(max > 0.0); + assert!(min <= max); + } +} + +// get_presentation_latency +// ------------------------------------ +#[test] +fn test_get_device_presentation_latency() { + test_get_device_presentation_latencies_in_scope(Scope::Input); + test_get_device_presentation_latencies_in_scope(Scope::Output); + + fn test_get_device_presentation_latencies_in_scope(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + // TODO: The latencies very from devices to devices. Check nothing here. + let latency = get_presentation_latency(device, scope.clone().into()); + println!( + "present latency on the device {} in scope {:?}: {}", + device, scope, latency + ); + } else { + println!("No device for {:?}.", scope); + } + } +} + +// create_cubeb_device_info +// ------------------------------------ +#[test] +fn test_create_cubeb_device_info() { + use std::collections::VecDeque; + + test_create_device_from_hwdev_in_scope(Scope::Input); + test_create_device_from_hwdev_in_scope(Scope::Output); + + fn test_create_device_from_hwdev_in_scope(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + let is_input = test_device_in_scope(device, Scope::Input); + let is_output = test_device_in_scope(device, Scope::Output); + let mut results = test_create_device_infos_by_device(device); + assert_eq!(results.len(), 2); + // Input device type: + if is_input { + check_device_info_by_device( + results.pop_front().unwrap().unwrap(), + device, + Scope::Input, + ); + } else { + assert_eq!(results.pop_front().unwrap().unwrap_err(), Error::error()); + } + // Output device type: + if is_output { + check_device_info_by_device( + results.pop_front().unwrap().unwrap(), + device, + Scope::Output, + ); + } else { + assert_eq!(results.pop_front().unwrap().unwrap_err(), Error::error()); + } + } else { + println!("No device for {:?}.", scope); + } + } + + fn test_create_device_infos_by_device( + id: AudioObjectID, + ) -> VecDeque> { + let dev_types = [DeviceType::INPUT, DeviceType::OUTPUT]; + let mut results = VecDeque::new(); + for dev_type in dev_types.iter() { + results.push_back(create_cubeb_device_info(id, *dev_type)); + } + results + } + + fn check_device_info_by_device(info: ffi::cubeb_device_info, id: AudioObjectID, scope: Scope) { + assert!(!info.devid.is_null()); + assert!(mem::size_of_val(&info.devid) >= mem::size_of::()); + assert_eq!(info.devid as AudioObjectID, id); + assert!(!info.device_id.is_null()); + assert!(!info.friendly_name.is_null()); + assert_eq!(info.group_id, info.device_id); + // TODO: Hit a kAudioHardwareUnknownPropertyError for AirPods + // assert!(!info.vendor_name.is_null()); + + // FIXIT: The device is defined to input-only or output-only, but some device is in-out! + assert_eq!(info.device_type, DeviceType::from(scope.clone()).bits()); + assert_eq!(info.state, ffi::CUBEB_DEVICE_STATE_ENABLED); + // TODO: The preference is set when the device is default input/output device if the device + // info is created from input/output scope. Should the preference be set if the + // device is a default input/output device if the device info is created from + // output/input scope ? The device may be a in-out device! + assert_eq!(info.preferred, get_cubeb_device_pref(id, scope)); + + assert_eq!(info.format, ffi::CUBEB_DEVICE_FMT_ALL); + assert_eq!(info.default_format, ffi::CUBEB_DEVICE_FMT_F32NE); + assert!(info.max_channels > 0); + assert!(info.min_rate <= info.max_rate); + assert!(info.min_rate <= info.default_rate); + assert!(info.default_rate <= info.max_rate); + + assert!(info.latency_lo > 0); + assert!(info.latency_hi > 0); + assert!(info.latency_lo <= info.latency_hi); + + fn get_cubeb_device_pref(id: AudioObjectID, scope: Scope) -> ffi::cubeb_device_pref { + let default_device = test_get_default_device(scope); + if default_device.is_some() && default_device.unwrap() == id { + ffi::CUBEB_DEVICE_PREF_ALL + } else { + ffi::CUBEB_DEVICE_PREF_NONE + } + } + } +} + +#[test] +#[should_panic] +fn test_create_device_info_by_unknown_device() { + assert!(create_cubeb_device_info(kAudioObjectUnknown, DeviceType::OUTPUT).is_err()); +} + +#[test] +#[should_panic] +fn test_create_device_info_with_unknown_type() { + test_create_device_info_with_unknown_type_by_scope(Scope::Input); + test_create_device_info_with_unknown_type_by_scope(Scope::Output); + + fn test_create_device_info_with_unknown_type_by_scope(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + assert!(create_cubeb_device_info(device, DeviceType::UNKNOWN).is_err()); + } else { + panic!("Panic by default: No device for {:?}.", scope); + } + } +} + +#[test] +fn test_create_device_from_hwdev_with_inout_type() { + test_create_device_from_hwdev_with_inout_type_by_scope(Scope::Input); + test_create_device_from_hwdev_with_inout_type_by_scope(Scope::Output); + + fn test_create_device_from_hwdev_with_inout_type_by_scope(scope: Scope) { + if let Some(device) = test_get_default_device(scope.clone()) { + // Get a kAudioHardwareUnknownPropertyError in get_channel_count actually. + assert!( + create_cubeb_device_info(device, DeviceType::INPUT | DeviceType::OUTPUT).is_err() + ); + } else { + println!("No device for {:?}.", scope); + } + } +} + +// is_aggregate_device +// ------------------------------------ +#[test] +fn test_is_aggregate_device() { + let mut aggregate_name = String::from(PRIVATE_AGGREGATE_DEVICE_NAME); + aggregate_name.push_str("_something"); + let aggregate_name_cstring = CString::new(aggregate_name).unwrap(); + + let mut info = ffi::cubeb_device_info::default(); + info.friendly_name = aggregate_name_cstring.as_ptr(); + assert!(is_aggregate_device(&info)); + + let non_aggregate_name_cstring = CString::new("Hello World!").unwrap(); + info.friendly_name = non_aggregate_name_cstring.as_ptr(); + assert!(!is_aggregate_device(&info)); +} + +// device_destroy +// ------------------------------------ +#[test] +fn test_device_destroy() { + let mut device = ffi::cubeb_device_info::default(); + + let device_id = CString::new("test: device id").unwrap(); + let friendly_name = CString::new("test: friendly name").unwrap(); + let vendor_name = CString::new("test: vendor name").unwrap(); + + device.device_id = device_id.into_raw(); + // The group_id is a mirror to device_id in our implementation, so we could skip it. + device.group_id = device.device_id; + device.friendly_name = friendly_name.into_raw(); + device.vendor_name = vendor_name.into_raw(); + + audiounit_device_destroy(&mut device); + + assert!(device.device_id.is_null()); + assert!(device.group_id.is_null()); + assert!(device.friendly_name.is_null()); + assert!(device.vendor_name.is_null()); +} + +#[test] +#[should_panic] +fn test_device_destroy_with_different_device_id_and_group_id() { + let mut device = ffi::cubeb_device_info::default(); + + let device_id = CString::new("test: device id").unwrap(); + let group_id = CString::new("test: group id").unwrap(); + let friendly_name = CString::new("test: friendly name").unwrap(); + let vendor_name = CString::new("test: vendor name").unwrap(); + + device.device_id = device_id.into_raw(); + device.group_id = group_id.into_raw(); + device.friendly_name = friendly_name.into_raw(); + device.vendor_name = vendor_name.into_raw(); + + audiounit_device_destroy(&mut device); + // Hit the assertion above, so we will leak some memory allocated for the above cstring. + + assert!(device.device_id.is_null()); + assert!(device.group_id.is_null()); + assert!(device.friendly_name.is_null()); + assert!(device.vendor_name.is_null()); +} + +#[test] +fn test_device_destroy_empty_device() { + let mut device = ffi::cubeb_device_info::default(); + + assert!(device.device_id.is_null()); + assert!(device.group_id.is_null()); + assert!(device.friendly_name.is_null()); + assert!(device.vendor_name.is_null()); + + audiounit_device_destroy(&mut device); + + assert!(device.device_id.is_null()); + assert!(device.group_id.is_null()); + assert!(device.friendly_name.is_null()); + assert!(device.vendor_name.is_null()); +} + +// get_devices_of_type +// ------------------------------------ +#[test] +fn test_get_devices_of_type() { + use std::collections::HashSet; + + let all_devices = audiounit_get_devices_of_type(DeviceType::INPUT | DeviceType::OUTPUT); + let input_devices = audiounit_get_devices_of_type(DeviceType::INPUT); + let output_devices = audiounit_get_devices_of_type(DeviceType::OUTPUT); + + let mut expected_all = test_get_all_devices(); + expected_all.sort(); + assert_eq!(all_devices, expected_all); + for device in all_devices.iter() { + if test_device_in_scope(*device, Scope::Input) { + assert!(input_devices.contains(device)); + } + if test_device_in_scope(*device, Scope::Output) { + assert!(output_devices.contains(device)); + } + } + + let input: HashSet = input_devices.iter().cloned().collect(); + let output: HashSet = output_devices.iter().cloned().collect(); + let union: HashSet = input.union(&output).cloned().collect(); + let mut union_devices: Vec = union.iter().cloned().collect(); + union_devices.sort(); + assert_eq!(all_devices, union_devices); +} + +#[test] +#[should_panic] +fn test_get_devices_of_type_unknown() { + let no_devs = audiounit_get_devices_of_type(DeviceType::UNKNOWN); + assert!(no_devs.is_empty()); +} + +// add_devices_changed_listener +// ------------------------------------ +#[test] +fn test_add_devices_changed_listener() { + use std::collections::HashMap; + + extern "C" fn inout_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + extern "C" fn in_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + extern "C" fn out_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + + let mut map: HashMap = HashMap::new(); + map.insert(DeviceType::INPUT, in_callback); + map.insert(DeviceType::OUTPUT, out_callback); + map.insert(DeviceType::INPUT | DeviceType::OUTPUT, inout_callback); + + test_get_raw_context(|context| { + for (devtype, callback) in map.iter() { + assert!(get_devices_changed_callback(context, Scope::Input).is_none()); + assert!(get_devices_changed_callback(context, Scope::Output).is_none()); + + // Register a callback within a specific scope. + assert!(context + .add_devices_changed_listener(*devtype, Some(*callback), ptr::null_mut()) + .is_ok()); + + if devtype.contains(DeviceType::INPUT) { + let cb = get_devices_changed_callback(context, Scope::Input); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *callback); + } else { + let cb = get_devices_changed_callback(context, Scope::Input); + assert!(cb.is_none()); + } + + if devtype.contains(DeviceType::OUTPUT) { + let cb = get_devices_changed_callback(context, Scope::Output); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *callback); + } else { + let cb = get_devices_changed_callback(context, Scope::Output); + assert!(cb.is_none()); + } + + // Unregister the callbacks within all scopes. + assert!(context + .remove_devices_changed_listener(DeviceType::INPUT | DeviceType::OUTPUT) + .is_ok()); + + assert!(get_devices_changed_callback(context, Scope::Input).is_none()); + assert!(get_devices_changed_callback(context, Scope::Output).is_none()); + } + }); +} + +#[test] +#[should_panic] +fn test_add_devices_changed_listener_in_unknown_scope() { + extern "C" fn callback(_: *mut ffi::cubeb, _: *mut c_void) {} + + test_get_raw_context(|context| { + let _ = context.add_devices_changed_listener( + DeviceType::UNKNOWN, + Some(callback), + ptr::null_mut(), + ); + }); +} + +#[test] +#[should_panic] +fn test_add_devices_changed_listener_with_none_callback() { + test_get_raw_context(|context| { + for devtype in &[DeviceType::INPUT, DeviceType::OUTPUT] { + assert!(context + .add_devices_changed_listener(*devtype, None, ptr::null_mut()) + .is_ok()); + } + }); +} + +// remove_devices_changed_listener +// ------------------------------------ +#[test] +fn test_remove_devices_changed_listener() { + use std::collections::HashMap; + + extern "C" fn in_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + extern "C" fn out_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + + let mut map: HashMap = HashMap::new(); + map.insert(DeviceType::INPUT, in_callback); + map.insert(DeviceType::OUTPUT, out_callback); + + test_get_raw_context(|context| { + for (devtype, _callback) in map.iter() { + assert!(get_devices_changed_callback(context, Scope::Input).is_none()); + assert!(get_devices_changed_callback(context, Scope::Output).is_none()); + + // Register callbacks within all scopes. + for (scope, listener) in map.iter() { + assert!(context + .add_devices_changed_listener(*scope, Some(*listener), ptr::null_mut()) + .is_ok()); + } + + let input_callback = get_devices_changed_callback(context, Scope::Input); + assert!(input_callback.is_some()); + assert_eq!( + input_callback.unwrap(), + *(map.get(&DeviceType::INPUT).unwrap()) + ); + let output_callback = get_devices_changed_callback(context, Scope::Output); + assert!(output_callback.is_some()); + assert_eq!( + output_callback.unwrap(), + *(map.get(&DeviceType::OUTPUT).unwrap()) + ); + + // Unregister the callbacks within one specific scopes. + assert!(context.remove_devices_changed_listener(*devtype).is_ok()); + + if devtype.contains(DeviceType::INPUT) { + let cb = get_devices_changed_callback(context, Scope::Input); + assert!(cb.is_none()); + } else { + let cb = get_devices_changed_callback(context, Scope::Input); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *(map.get(&DeviceType::INPUT).unwrap())); + } + + if devtype.contains(DeviceType::OUTPUT) { + let cb = get_devices_changed_callback(context, Scope::Output); + assert!(cb.is_none()); + } else { + let cb = get_devices_changed_callback(context, Scope::Output); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *(map.get(&DeviceType::OUTPUT).unwrap())); + } + + // Unregister the callbacks within all scopes. + assert!(context + .remove_devices_changed_listener(DeviceType::INPUT | DeviceType::OUTPUT) + .is_ok()); + } + }); +} + +#[test] +fn test_remove_devices_changed_listener_without_adding_listeners() { + test_get_raw_context(|context| { + for devtype in &[ + DeviceType::INPUT, + DeviceType::OUTPUT, + DeviceType::INPUT | DeviceType::OUTPUT, + ] { + assert!(context.remove_devices_changed_listener(*devtype).is_ok()); + } + }); +} + +#[test] +fn test_remove_devices_changed_listener_within_all_scopes() { + use std::collections::HashMap; + + extern "C" fn inout_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + extern "C" fn in_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + extern "C" fn out_callback(_: *mut ffi::cubeb, _: *mut c_void) {} + + let mut map: HashMap = HashMap::new(); + map.insert(DeviceType::INPUT, in_callback); + map.insert(DeviceType::OUTPUT, out_callback); + map.insert(DeviceType::INPUT | DeviceType::OUTPUT, inout_callback); + + test_get_raw_context(|context| { + for (devtype, callback) in map.iter() { + assert!(get_devices_changed_callback(context, Scope::Input).is_none()); + assert!(get_devices_changed_callback(context, Scope::Output).is_none()); + + assert!(context + .add_devices_changed_listener(*devtype, Some(*callback), ptr::null_mut()) + .is_ok()); + + if devtype.contains(DeviceType::INPUT) { + let cb = get_devices_changed_callback(context, Scope::Input); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *callback); + } + + if devtype.contains(DeviceType::OUTPUT) { + let cb = get_devices_changed_callback(context, Scope::Output); + assert!(cb.is_some()); + assert_eq!(cb.unwrap(), *callback); + } + + assert!(context + .remove_devices_changed_listener(DeviceType::INPUT | DeviceType::OUTPUT) + .is_ok()); + + assert!(get_devices_changed_callback(context, Scope::Input).is_none()); + assert!(get_devices_changed_callback(context, Scope::Output).is_none()); + } + }); +} + +fn get_devices_changed_callback( + context: &AudioUnitContext, + scope: Scope, +) -> ffi::cubeb_device_collection_changed_callback { + let devices_guard = context.devices.lock().unwrap(); + match scope { + Scope::Input => devices_guard.input.changed_callback, + Scope::Output => devices_guard.output.changed_callback, + } +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs new file mode 100644 index 000000000000..5342ec0f3940 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/backlog.rs @@ -0,0 +1,36 @@ +// Copyright © 2018 Mozilla Foundation +// +// This program is made available under an ISC-style license. See the +// accompanying file LICENSE for details. +use super::utils::test_get_default_raw_stream; +use super::*; + +// Interface +// ============================================================================ +// Remove these after test_ops_stream_register_device_changed_callback works. +#[test] +fn test_stream_register_device_changed_callback() { + extern "C" fn callback(_: *mut c_void) {} + + test_get_default_raw_stream(|stream| { + assert!(stream + .register_device_changed_callback(Some(callback)) + .is_ok()); + assert!(stream.register_device_changed_callback(None).is_ok()); + }); +} + +#[test] +fn test_stream_register_device_changed_callback_twice() { + extern "C" fn callback1(_: *mut c_void) {} + extern "C" fn callback2(_: *mut c_void) {} + + test_get_default_raw_stream(|stream| { + assert!(stream + .register_device_changed_callback(Some(callback1)) + .is_ok()); + assert!(stream + .register_device_changed_callback(Some(callback2)) + .is_err()); + }); +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs new file mode 100644 index 000000000000..f7cd8aac810b --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_change.rs @@ -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(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; + + 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; + + 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) }; + { + 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) }; + { + let mut guard = count.lock().unwrap(); + *guard += 1; + } + } + + fn check_result( + in_scope: bool, + expected: (T, T), + watcher: &Watcher, + ) { + 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; + + 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) }; + { + 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; + + 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) }; + { + let mut guard = count.lock().unwrap(); + *guard += 1; + } + } +} + +struct Watcher { + watching: Arc>, + current: Option, +} + +impl Watcher { + fn new(value: &Arc>) -> 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( + name: &'static str, + stm_type: StreamType, + input_device: Option, + output_device: Option, + 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( + 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 +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs new file mode 100644 index 000000000000..28390dda6c04 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/device_property.rs @@ -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()); +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs new file mode 100644 index 000000000000..6d98fbc48e83 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/interfaces.rs @@ -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(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 +// ); +// }); +// } diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs new file mode 100644 index 000000000000..6b9099edb8c4 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/manual.rs @@ -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) { + for listener in listeners { + assert_eq!(stream.add_device_listener(listener), NO_ERR); + } + } + + fn remove_listeners(stream: &AudioUnitStream, listeners: &Vec) { + 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); +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs new file mode 100644 index 000000000000..0c193d0dc8fa --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/mod.rs @@ -0,0 +1,12 @@ +use super::*; + +mod aggregate_device; +mod api; +mod backlog; +mod device_change; +mod device_property; +mod interfaces; +mod manual; +mod parallel; +mod tone; +mod utils; diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs new file mode 100644 index 000000000000..e971737cca84 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/parallel.rs @@ -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( + 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( + 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::(), + ); + (latency_frames, status) + })); + } + + let mut latencies = vec![]; + let mut statuses = vec![]; + for handle in join_handles { + let (latency, status) = handle.join().unwrap(); + latencies.push(latency); + statuses.push(status); + } + + let mut buffer_frames_list = vec![]; + for unit in units.iter() { + buffer_frames_list.push(unit.get_buffer_frame_size(scope.clone(), prop_scope.clone())); + } + + for status in statuses { + assert_eq!(status, NO_ERR); + } + + for i in 0..buffer_frames_list.len() - 1 { + assert_eq!(buffer_frames_list[i], buffer_frames_list[i + 1]); + } +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs new file mode 100644 index 000000000000..e3fa87e44282 --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/tone.rs @@ -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 + } +} diff --git a/third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs b/third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs new file mode 100644 index 000000000000..a18b4ad3b47f --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/src/backend/tests/utils.rs @@ -0,0 +1,1081 @@ +use super::*; + +// Common Utils +// ------------------------------------------------------------------------------------------------ +#[derive(Clone, Debug, PartialEq)] +pub enum Scope { + Input, + Output, +} + +impl From for DeviceType { + fn from(scope: Scope) -> Self { + match scope { + Scope::Input => DeviceType::INPUT, + Scope::Output => DeviceType::OUTPUT, + } + } +} + +#[derive(Clone)] +pub enum PropertyScope { + Input, + Output, +} + +pub fn test_get_default_device(scope: Scope) -> Option { + let address = AudioObjectPropertyAddress { + mSelector: match scope { + Scope::Input => kAudioHardwarePropertyDefaultInputDevice, + Scope::Output => kAudioHardwarePropertyDefaultOutputDevice, + }, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let mut devid: AudioObjectID = kAudioObjectUnknown; + let mut size = mem::size_of::(); + let status = unsafe { + AudioObjectGetPropertyData( + kAudioObjectSystemObject, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut UInt32, + &mut devid as *mut AudioObjectID as *mut c_void, + ) + }; + if status != NO_ERR || devid == kAudioObjectUnknown { + return None; + } + Some(devid) +} + +// TODO: Create a GetProperty trait and add a default implementation for it, then implement it +// for TestAudioUnit so the member method like `get_buffer_frame_size` can reuse the trait +// method get_property_data. +#[derive(Debug)] +pub struct TestAudioUnit(AudioUnit); + +impl TestAudioUnit { + fn new(unit: AudioUnit) -> Self { + assert!(!unit.is_null()); + Self(unit) + } + pub fn get_inner(&self) -> AudioUnit { + self.0 + } + pub fn get_buffer_frame_size( + &self, + scope: Scope, + prop_scope: PropertyScope, + ) -> std::result::Result { + test_audiounit_get_buffer_frame_size(self.0, scope, prop_scope) + } +} + +impl Drop for TestAudioUnit { + fn drop(&mut self) { + unsafe { + AudioUnitUninitialize(self.0); + AudioComponentInstanceDispose(self.0); + } + } +} + +// TODO: 1. Return Result with custom errors. +// 2. Allow to create a in-out unit. +pub fn test_get_default_audiounit(scope: Scope) -> Option { + let device = test_get_default_device(scope.clone()); + let unit = test_create_audiounit(ComponentSubType::HALOutput); + if device.is_none() || unit.is_none() { + return None; + } + let unit = unit.unwrap(); + let device = device.unwrap(); + match scope { + Scope::Input => { + if test_enable_audiounit_in_scope(unit.get_inner(), Scope::Input, true).is_err() + || test_enable_audiounit_in_scope(unit.get_inner(), Scope::Output, false).is_err() + { + return None; + } + } + Scope::Output => { + if test_enable_audiounit_in_scope(unit.get_inner(), Scope::Input, false).is_err() + || test_enable_audiounit_in_scope(unit.get_inner(), Scope::Output, true).is_err() + { + return None; + } + } + } + + let status = unsafe { + AudioUnitSetProperty( + unit.get_inner(), + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + 0, // Global bus + &device as *const AudioObjectID as *const c_void, + mem::size_of::() as u32, + ) + }; + if status == NO_ERR { + Some(unit) + } else { + None + } +} + +pub enum ComponentSubType { + HALOutput, + DefaultOutput, +} + +// TODO: Return Result with custom errors. +// Surprisingly the AudioUnit can be created even when there is no any device on the platform, +// no matter its subtype is HALOutput or DefaultOutput. +pub fn test_create_audiounit(unit_type: ComponentSubType) -> Option { + let desc = AudioComponentDescription { + componentType: kAudioUnitType_Output, + componentSubType: match unit_type { + ComponentSubType::HALOutput => kAudioUnitSubType_HALOutput, + ComponentSubType::DefaultOutput => kAudioUnitSubType_DefaultOutput, + }, + componentManufacturer: kAudioUnitManufacturer_Apple, + componentFlags: 0, + componentFlagsMask: 0, + }; + let comp = unsafe { AudioComponentFindNext(ptr::null_mut(), &desc) }; + if comp.is_null() { + return None; + } + let mut unit: AudioUnit = ptr::null_mut(); + let status = unsafe { AudioComponentInstanceNew(comp, &mut unit) }; + // TODO: Is unit possible to be null when no error returns ? + if status != NO_ERR || unit.is_null() { + None + } else { + Some(TestAudioUnit::new(unit)) + } +} + +fn test_enable_audiounit_in_scope( + unit: AudioUnit, + scope: Scope, + enable: bool, +) -> std::result::Result<(), OSStatus> { + assert!(!unit.is_null()); + let (scope, element) = match scope { + Scope::Input => (kAudioUnitScope_Input, AU_IN_BUS), + Scope::Output => (kAudioUnitScope_Output, AU_OUT_BUS), + }; + let on_off: u32 = if enable { 1 } else { 0 }; + let status = unsafe { + AudioUnitSetProperty( + unit, + kAudioOutputUnitProperty_EnableIO, + scope, + element, + &on_off as *const u32 as *const c_void, + mem::size_of::() as u32, + ) + }; + if status == NO_ERR { + Ok(()) + } else { + Err(status) + } +} + +pub fn test_get_default_source_name(scope: Scope) -> Option { + if let Some(source) = test_get_default_source_data(scope) { + Some(u32_to_string(source)) + } else { + None + } +} + +pub fn test_get_default_source_data(scope: Scope) -> Option { + let device = test_get_default_device(scope.clone()); + if device.is_none() { + return None; + } + + let device = device.unwrap(); + let address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyDataSource, + mScope: match scope { + Scope::Input => kAudioDevicePropertyScopeInput, + Scope::Output => kAudioDevicePropertyScopeOutput, + }, + mElement: kAudioObjectPropertyElementMaster, + }; + let mut size = mem::size_of::(); + let mut data: u32 = 0; + + let status = unsafe { + AudioObjectGetPropertyData( + device, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + &mut data as *mut u32 as *mut c_void, + ) + }; + + // TODO: Can data be 0 when no error returns ? + if status == NO_ERR && data > 0 { + Some(data) + } else { + None + } +} + +fn u32_to_string(data: u32) -> String { + // Reverse 0xWXYZ into 0xZYXW. + let mut buffer = [b'\x00'; 4]; // 4 bytes for u32. + buffer[0] = (data >> 24) as u8; + buffer[1] = (data >> 16) as u8; + buffer[2] = (data >> 8) as u8; + buffer[3] = (data) as u8; + String::from_utf8_lossy(&buffer).to_string() +} + +pub fn test_get_all_devices() -> Vec { + let mut devices = Vec::new(); + let address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyDevices, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + let mut size: usize = 0; + let status = unsafe { + AudioObjectGetPropertyDataSize( + kAudioObjectSystemObject, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + ) + }; + // size will be 0 if there is no device at all. + if status != NO_ERR || size == 0 { + return devices; + } + assert_eq!(size % mem::size_of::(), 0); + let elements = size / mem::size_of::(); + devices.resize(elements, kAudioObjectUnknown); + let status = unsafe { + AudioObjectGetPropertyData( + kAudioObjectSystemObject, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + devices.as_mut_ptr() as *mut c_void, + ) + }; + if status != NO_ERR { + devices.clear(); + return devices; + } + for device in devices.iter() { + assert_ne!(*device, kAudioObjectUnknown); + } + devices +} + +pub fn test_get_devices_in_scope(scope: Scope) -> Vec { + let mut devices = test_get_all_devices(); + devices.retain(|device| test_device_in_scope(*device, scope.clone())); + devices +} + +pub fn test_device_channels_in_scope( + id: AudioObjectID, + scope: Scope, +) -> std::result::Result { + let address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyStreamConfiguration, + mScope: match scope { + Scope::Input => kAudioDevicePropertyScopeInput, + Scope::Output => kAudioDevicePropertyScopeOutput, + }, + mElement: kAudioObjectPropertyElementMaster, + }; + let mut size: usize = 0; + let status = unsafe { + AudioObjectGetPropertyDataSize( + id, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + ) + }; + if status != NO_ERR { + return Err(status); + } + if size == 0 { + return Ok(0); + } + let byte_len = size / mem::size_of::(); + let mut bytes = vec![0u8; byte_len]; + let status = unsafe { + AudioObjectGetPropertyData( + id, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + bytes.as_mut_ptr() as *mut c_void, + ) + }; + if status != NO_ERR { + return Err(status); + } + let buf_list = unsafe { &*(bytes.as_mut_ptr() as *mut AudioBufferList) }; + let buf_len = buf_list.mNumberBuffers as usize; + if buf_len == 0 { + return Ok(0); + } + let buf_ptr = buf_list.mBuffers.as_ptr() as *const AudioBuffer; + let buffers = unsafe { slice::from_raw_parts(buf_ptr, buf_len) }; + let mut channels: u32 = 0; + for buffer in buffers { + channels += buffer.mNumberChannels; + } + Ok(channels) +} + +pub fn test_device_in_scope(id: AudioObjectID, scope: Scope) -> bool { + let channels = test_device_channels_in_scope(id, scope); + channels.is_ok() && channels.unwrap() > 0 +} + +pub fn test_get_all_onwed_devices(id: AudioDeviceID) -> Vec { + assert_ne!(id, kAudioObjectUnknown); + + let address = AudioObjectPropertyAddress { + mSelector: kAudioObjectPropertyOwnedObjects, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let qualifier_data_size = mem::size_of::(); + let class_id: AudioClassID = kAudioSubDeviceClassID; + let qualifier_data = &class_id; + let mut size: usize = 0; + + unsafe { + assert_eq!( + AudioObjectGetPropertyDataSize( + id, + &address, + qualifier_data_size as u32, + qualifier_data as *const u32 as *const c_void, + &mut size as *mut usize as *mut u32 + ), + NO_ERR + ); + } + assert_ne!(size, 0); + + let elements = size / mem::size_of::(); + let mut devices: Vec = allocate_array(elements); + + unsafe { + assert_eq!( + AudioObjectGetPropertyData( + id, + &address, + qualifier_data_size as u32, + qualifier_data as *const u32 as *const c_void, + &mut size as *mut usize as *mut u32, + devices.as_mut_ptr() as *mut c_void + ), + NO_ERR + ); + } + + devices +} + +pub fn test_get_master_device(id: AudioObjectID) -> String { + assert_ne!(id, kAudioObjectUnknown); + + let address = AudioObjectPropertyAddress { + mSelector: kAudioAggregateDevicePropertyMasterSubDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let mut master: CFStringRef = ptr::null_mut(); + let mut size = mem::size_of::(); + assert_eq!( + audio_object_get_property_data(id, &address, &mut size, &mut master), + NO_ERR + ); + assert!(!master.is_null()); + + let master = StringRef::new(master as _); + master.into_string() +} + +pub fn test_get_drift_compensations(id: AudioObjectID) -> std::result::Result { + let address = AudioObjectPropertyAddress { + mSelector: kAudioSubDevicePropertyDriftCompensation, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + let mut size = mem::size_of::(); + let mut compensation = u32::max_value(); + let status = unsafe { + AudioObjectGetPropertyData( + id, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + &mut compensation as *mut u32 as *mut c_void, + ) + }; + if status == NO_ERR { + Ok(compensation) + } else { + Err(status) + } +} + +pub fn test_audiounit_scope_is_enabled(unit: AudioUnit, scope: Scope) -> bool { + assert!(!unit.is_null()); + let mut has_io: UInt32 = 0; + let (scope, element) = match scope { + Scope::Input => (kAudioUnitScope_Input, AU_IN_BUS), + Scope::Output => (kAudioUnitScope_Output, AU_OUT_BUS), + }; + assert_eq!( + audio_unit_get_property( + unit, + kAudioOutputUnitProperty_HasIO, + scope, + element, + &mut has_io, + &mut mem::size_of::() + ), + NO_ERR + ); + has_io != 0 +} + +pub fn test_audiounit_get_buffer_frame_size( + unit: AudioUnit, + scope: Scope, + prop_scope: PropertyScope, +) -> std::result::Result { + let element = match scope { + Scope::Input => AU_IN_BUS, + Scope::Output => AU_OUT_BUS, + }; + let prop_scope = match prop_scope { + PropertyScope::Input => kAudioUnitScope_Input, + PropertyScope::Output => kAudioUnitScope_Output, + }; + let mut buffer_frames: u32 = 0; + let mut size = mem::size_of::(); + let status = unsafe { + AudioUnitGetProperty( + unit, + kAudioDevicePropertyBufferFrameSize, + prop_scope, + element, + &mut buffer_frames as *mut u32 as *mut c_void, + &mut size as *mut usize as *mut u32, + ) + }; + if status == NO_ERR { + Ok(buffer_frames) + } else { + Err(status) + } +} + +// Surprisingly it's ok to set +// 1. a unknown device +// 2. a non-input/non-output device +// 3. the current default input/output device +// as the new default input/output device by apple's API. We need to check the above things by ourselves. +pub fn test_set_default_device( + device: AudioObjectID, + scope: Scope, +) -> std::result::Result { + let default = test_get_default_device(scope.clone()); + if default.is_none() { + return Ok(false); + } + let default = default.unwrap(); + if default == device || !test_device_in_scope(device, scope.clone()) { + return Ok(false); + } + let address = AudioObjectPropertyAddress { + mSelector: match scope { + Scope::Input => kAudioHardwarePropertyDefaultInputDevice, + Scope::Output => kAudioHardwarePropertyDefaultOutputDevice, + }, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + let size = mem::size_of::(); + let status = unsafe { + AudioObjectSetPropertyData( + kAudioObjectSystemObject, + &address, + 0, + ptr::null(), + size as u32, + &device as *const AudioObjectID as *const c_void, + ) + }; + if status == NO_ERR { + Ok(true) + } else { + Err(status) + } +} + +pub struct TestDeviceSwitcher { + scope: Scope, + devices: Vec, +} + +impl TestDeviceSwitcher { + pub fn new(scope: Scope) -> Self { + Self { + scope: scope.clone(), + devices: test_get_devices_in_scope(scope), + } + } + + pub fn next(&self) -> std::result::Result { + let current = test_get_default_device(self.scope.clone()).unwrap(); + let mut index = self + .devices + .iter() + .position(|device| *device == current) + .unwrap(); + index = (index + 1) % self.devices.len(); + let next = self.devices[index]; + println!( + "Switch device for {:?}: {} -> {}", + self.scope, current, next + ); + test_set_default_device(next, self.scope.clone()) + } +} + +pub fn test_create_device_change_listener(scope: Scope, listener: F) -> TestPropertyListener +where + F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus, +{ + let address = AudioObjectPropertyAddress { + mSelector: match scope { + Scope::Input => kAudioHardwarePropertyDefaultInputDevice, + Scope::Output => kAudioHardwarePropertyDefaultOutputDevice, + }, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + TestPropertyListener::new(kAudioObjectSystemObject, address, listener) +} + +pub struct TestPropertyListener +where + F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus, +{ + device: AudioObjectID, + property: AudioObjectPropertyAddress, + callback: F, +} + +impl TestPropertyListener +where + F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus, +{ + pub fn new(device: AudioObjectID, property: AudioObjectPropertyAddress, callback: F) -> Self { + Self { + device, + property, + callback, + } + } + + pub fn start(&self) -> std::result::Result<(), OSStatus> { + let status = unsafe { + AudioObjectAddPropertyListener( + self.device, + &self.property, + Some(Self::render), + self as *const Self as *mut c_void, + ) + }; + if status == NO_ERR { + Ok(()) + } else { + Err(status) + } + } + + pub fn stop(&self) -> std::result::Result<(), OSStatus> { + let status = unsafe { + AudioObjectRemovePropertyListener( + self.device, + &self.property, + Some(Self::render), + self as *const Self as *mut c_void, + ) + }; + if status == NO_ERR { + Ok(()) + } else { + Err(status) + } + } + + extern "C" fn render( + id: AudioObjectID, + number_of_addresses: u32, + addresses: *const AudioObjectPropertyAddress, + data: *mut c_void, + ) -> OSStatus { + let listener = unsafe { &*(data as *mut Self) }; + assert_eq!(id, listener.device); + let addrs = unsafe { slice::from_raw_parts(addresses, number_of_addresses as usize) }; + (listener.callback)(addrs) + } +} + +impl Drop for TestPropertyListener +where + F: Fn(&[AudioObjectPropertyAddress]) -> OSStatus, +{ + fn drop(&mut self) { + self.stop(); + } +} + +// TODO: It doesn't work if default input or output is an aggregate device! Probably we need to do +// the same thing as what audiounit_set_aggregate_sub_device_list does. +#[derive(Debug)] +pub struct TestDevicePlugger { + scope: Scope, + plugin_id: AudioObjectID, + device_id: AudioObjectID, +} + +impl TestDevicePlugger { + pub fn new(scope: Scope) -> std::result::Result { + let plugin_id = Self::get_system_plugin_id()?; + Ok(Self { + scope, + plugin_id, + device_id: kAudioObjectUnknown, + }) + } + + pub fn get_device_id(&self) -> AudioObjectID { + self.device_id + } + + pub fn plug(&mut self) -> std::result::Result<(), OSStatus> { + self.device_id = self.create_aggregate_device()?; + Ok(()) + } + + pub fn unplug(&mut self) -> std::result::Result<(), OSStatus> { + self.destroy_aggregate_device() + } + + fn is_plugging(&self) -> bool { + self.device_id != kAudioObjectUnknown + } + + fn destroy_aggregate_device(&mut self) -> std::result::Result<(), OSStatus> { + assert_ne!(self.plugin_id, kAudioObjectUnknown); + assert_ne!(self.device_id, kAudioObjectUnknown); + + let address = AudioObjectPropertyAddress { + mSelector: kAudioPlugInDestroyAggregateDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let mut size: usize = 0; + let status = unsafe { + AudioObjectGetPropertyDataSize( + self.plugin_id, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + ) + }; + if status != NO_ERR { + return Err(status); + } + assert_ne!(size, 0); + + let status = unsafe { + // This call can simulate removing a device. + AudioObjectGetPropertyData( + self.plugin_id, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + &mut self.device_id as *mut AudioDeviceID as *mut c_void, + ) + }; + if status == NO_ERR { + self.device_id = kAudioObjectUnknown; + Ok(()) + } else { + Err(status) + } + } + + fn create_aggregate_device(&self) -> std::result::Result { + use std::time::{SystemTime, UNIX_EPOCH}; + + const TEST_AGGREGATE_DEVICE_NAME: &str = "TestAggregateDevice"; + + assert_ne!(self.plugin_id, kAudioObjectUnknown); + + let sub_devices = Self::get_sub_devices(self.scope.clone()); + if sub_devices.is_none() { + return Err(kAudioCodecUnspecifiedError as OSStatus); + } + let sub_devices = sub_devices.unwrap(); + + let address = AudioObjectPropertyAddress { + mSelector: kAudioPlugInCreateAggregateDevice, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let mut size: usize = 0; + let status = unsafe { + AudioObjectGetPropertyDataSize( + self.plugin_id, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + ) + }; + if status != NO_ERR { + return Err(status); + } + assert_ne!(size, 0); + + let sys_time = SystemTime::now(); + let time_id = sys_time.duration_since(UNIX_EPOCH).unwrap().as_nanos(); + let device_name = format!("{}_{}", TEST_AGGREGATE_DEVICE_NAME, time_id); + let device_uid = format!("org.mozilla.{}", device_name); + + let mut device_id = kAudioObjectUnknown; + let status = unsafe { + let device_dict = CFDictionaryCreateMutable( + kCFAllocatorDefault, + 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ); + + // Set the name of this device. + let device_name = cfstringref_from_string(&device_name); + CFDictionaryAddValue( + device_dict, + cfstringref_from_static_string(AGGREGATE_DEVICE_NAME_KEY) as *const c_void, + device_name as *const c_void, + ); + CFRelease(device_name as *const c_void); + + // Set the uid of this device. + let device_uid = cfstringref_from_string(&device_uid); + CFDictionaryAddValue( + device_dict, + cfstringref_from_static_string(AGGREGATE_DEVICE_UID_KEY) as *const c_void, + device_uid as *const c_void, + ); + CFRelease(device_uid as *const c_void); + + // This device is private to the process creating it. + let private_value: i32 = 1; + let device_private_key = CFNumberCreate( + kCFAllocatorDefault, + i64::from(kCFNumberIntType), + &private_value as *const i32 as *const c_void, + ); + CFDictionaryAddValue( + device_dict, + cfstringref_from_static_string(AGGREGATE_DEVICE_PRIVATE_KEY) as *const c_void, + device_private_key as *const c_void, + ); + CFRelease(device_private_key as *const c_void); + + // Set this device to be a stacked aggregate (i.e. multi-output device). + let stacked_value: i32 = 0; // 1 for normal aggregate device. + let device_stacked_key = CFNumberCreate( + kCFAllocatorDefault, + i64::from(kCFNumberIntType), + &stacked_value as *const i32 as *const c_void, + ); + CFDictionaryAddValue( + device_dict, + cfstringref_from_static_string(AGGREGATE_DEVICE_STACKED_KEY) as *const c_void, + device_stacked_key as *const c_void, + ); + CFRelease(device_stacked_key as *const c_void); + + // Set sub devices for this device. + CFDictionaryAddValue( + device_dict, + cfstringref_from_static_string(AGGREGATE_DEVICE_SUB_DEVICE_LIST_KEY) + as *const c_void, + sub_devices as *const c_void, + ); + CFRelease(sub_devices as *const c_void); + + // This call can simulate adding a device. + let status = AudioObjectGetPropertyData( + self.plugin_id, + &address, + mem::size_of_val(&device_dict) as u32, + &device_dict as *const CFMutableDictionaryRef as *const c_void, + &mut size as *mut usize as *mut u32, + &mut device_id as *mut AudioDeviceID as *mut c_void, + ); + CFRelease(device_dict as *const c_void); + status + }; + if status == NO_ERR { + assert_ne!(device_id, kAudioObjectUnknown); + Ok(device_id) + } else { + Err(status) + } + } + + fn get_system_plugin_id() -> std::result::Result { + let address = AudioObjectPropertyAddress { + mSelector: kAudioHardwarePropertyPlugInForBundleID, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let mut size: usize = 0; + let status = unsafe { + AudioObjectGetPropertyDataSize( + kAudioObjectSystemObject, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + ) + }; + if status != NO_ERR { + return Err(status); + } + assert_ne!(size, 0); + + let mut plugin_id = kAudioObjectUnknown; + let mut in_bundle_ref = cfstringref_from_static_string("com.apple.audio.CoreAudio"); + let mut translation_value = AudioValueTranslation { + mInputData: &mut in_bundle_ref as *mut CFStringRef as *mut c_void, + mInputDataSize: mem::size_of::() as u32, + mOutputData: &mut plugin_id as *mut AudioObjectID as *mut c_void, + mOutputDataSize: mem::size_of::() as u32, + }; + assert_eq!(size, mem::size_of_val(&translation_value)); + + let status = unsafe { + let status = AudioObjectGetPropertyData( + kAudioObjectSystemObject, + &address, + 0, + ptr::null(), + &mut size as *mut usize as *mut u32, + &mut translation_value as *mut AudioValueTranslation as *mut c_void, + ); + CFRelease(in_bundle_ref as *const c_void); + status + }; + if status == NO_ERR { + assert_ne!(plugin_id, kAudioObjectUnknown); + Ok(plugin_id) + } else { + Err(status) + } + } + + // TODO: This doesn't work as what we expect when the default deivce in the scope is an + // aggregate device. We should get the list of all the active sub devices and put + // them into the array, if the device is an aggregate device. See the code in + // AggregateDevice::get_sub_devices and audiounit_set_aggregate_sub_device_list. + fn get_sub_devices(scope: Scope) -> Option { + let device = test_get_default_device(scope); + if device.is_none() { + return None; + } + let device = device.unwrap(); + let uid = get_device_global_uid(device); + if uid.is_err() { + return None; + } + let uid = uid.unwrap(); + unsafe { + let list = CFArrayCreateMutable(ptr::null(), 0, &kCFTypeArrayCallBacks); + let sub_device_dict = CFDictionaryCreateMutable( + ptr::null(), + 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ); + CFDictionaryAddValue( + sub_device_dict, + cfstringref_from_static_string(SUB_DEVICE_UID_KEY) as *const c_void, + uid.get_raw() as *const c_void, + ); + CFArrayAppendValue(list, sub_device_dict as *const c_void); + CFRelease(sub_device_dict as *const c_void); + Some(list) + } + } +} + +impl Drop for TestDevicePlugger { + fn drop(&mut self) { + if self.is_plugging() { + self.unplug(); + } + } +} + +// Test Templates +// ------------------------------------------------------------------------------------------------ +pub fn test_ops_context_operation(name: &'static str, operation: F) +where + F: FnOnce(*mut ffi::cubeb), +{ + let name_c_string = CString::new(name).expect("Failed to create context name"); + let mut context = ptr::null_mut::(); + assert_eq!( + unsafe { OPS.init.unwrap()(&mut context, name_c_string.as_ptr()) }, + ffi::CUBEB_OK + ); + assert!(!context.is_null()); + operation(context); + unsafe { OPS.destroy.unwrap()(context) } +} + +// Note: The in-out stream initializeed with different device will create an aggregate_device and +// result in firing device-collection-changed callbacks. Run in-out streams with tests +// capturing device-collection-changed callbacks may cause troubles. See more details in the +// comments for test_create_blank_aggregate_device. +pub fn test_ops_stream_operation( + name: &'static str, + input_device: ffi::cubeb_devid, + input_stream_params: *mut ffi::cubeb_stream_params, + output_device: ffi::cubeb_devid, + output_stream_params: *mut ffi::cubeb_stream_params, + latency_frames: u32, + data_callback: ffi::cubeb_data_callback, + state_callback: ffi::cubeb_state_callback, + user_ptr: *mut c_void, + operation: F, +) where + F: FnOnce(*mut ffi::cubeb_stream), +{ + test_ops_context_operation("context: stream operation", |context_ptr| { + // Do nothing if there is no input/output device to perform input/output tests. + if !input_stream_params.is_null() && test_get_default_device(Scope::Input).is_none() { + println!("No input device to perform input tests for \"{}\".", name); + return; + } + + if !output_stream_params.is_null() && test_get_default_device(Scope::Output).is_none() { + println!("No output device to perform output tests for \"{}\".", name); + return; + } + + let mut stream: *mut ffi::cubeb_stream = ptr::null_mut(); + let stream_name = CString::new(name).expect("Failed to create stream name"); + assert_eq!( + unsafe { + OPS.stream_init.unwrap()( + context_ptr, + &mut stream, + stream_name.as_ptr(), + input_device, + input_stream_params, + output_device, + output_stream_params, + latency_frames, + data_callback, + state_callback, + user_ptr, + ) + }, + ffi::CUBEB_OK + ); + assert!(!stream.is_null()); + operation(stream); + unsafe { + OPS.stream_destroy.unwrap()(stream); + } + }); +} + +pub fn test_get_raw_context(operation: F) +where + F: FnOnce(&mut AudioUnitContext), +{ + let mut context = AudioUnitContext::new(); + operation(&mut context); +} + +pub fn test_get_default_raw_stream(operation: F) +where + F: FnOnce(&mut AudioUnitStream), +{ + test_get_raw_stream(ptr::null_mut(), None, None, 0, operation); +} + +fn test_get_raw_stream( + user_ptr: *mut c_void, + data_callback: ffi::cubeb_data_callback, + state_callback: ffi::cubeb_state_callback, + latency_frames: u32, + operation: F, +) where + F: FnOnce(&mut AudioUnitStream), +{ + let mut context = AudioUnitContext::new(); + + // Add a stream to the context since we are about to create one. + // AudioUnitStream::drop() will check the context has at least one stream. + let global_latency_frames = context.update_latency_by_adding_stream(latency_frames); + + let mut stream = AudioUnitStream::new( + &mut context, + user_ptr, + data_callback, + state_callback, + global_latency_frames.unwrap(), + ); + stream.core_stream_data = CoreStreamData::new(&stream, None, None); + + operation(&mut stream); +} diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/backend/utils.rs b/third_party/rust/cubeb-coreaudio/src/backend/utils.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/src/backend/utils.rs rename to third_party/rust/cubeb-coreaudio/src/backend/utils.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/capi.rs b/third_party/rust/cubeb-coreaudio/src/capi.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/src/capi.rs rename to third_party/rust/cubeb-coreaudio/src/capi.rs diff --git a/media/libcubeb/cubeb-coreaudio-rs/src/lib.rs b/third_party/rust/cubeb-coreaudio/src/lib.rs similarity index 100% rename from media/libcubeb/cubeb-coreaudio-rs/src/lib.rs rename to third_party/rust/cubeb-coreaudio/src/lib.rs diff --git a/third_party/rust/cubeb-coreaudio/todo.md b/third_party/rust/cubeb-coreaudio/todo.md new file mode 100644 index 000000000000..4da7d54f3e5c --- /dev/null +++ b/third_party/rust/cubeb-coreaudio/todo.md @@ -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 listener’s callback +- Use `Option` rather than `AggregateDevice` for `aggregate_device` in `CoreStreamData` + +### Type of stream +- Use `Option` rather than `device_info` for `{input, output}_device` in `CoreStreamData` +- Use `Option` 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. diff --git a/toolkit/library/rust/shared/Cargo.toml b/toolkit/library/rust/shared/Cargo.toml index 2f44f58e38b0..2b5a3d9c6492 100644 --- a/toolkit/library/rust/shared/Cargo.toml +++ b/toolkit/library/rust/shared/Cargo.toml @@ -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" }