diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 0000000..fa53927 --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..64d456c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "rust-analyzer.cargo.features": [ + "pipewire" + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 044dff6..081f108 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,38 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alsa" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" -dependencies = [ - "alsa-sys", - "bitflags 2.10.0", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "annotate-snippets" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" -dependencies = [ - "unicode-width 0.1.14", - "yansi-term", -] - [[package]] name = "anstream" version = "0.6.21" @@ -95,42 +63,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "annotate-snippets", - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn", -] +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "bindgen" @@ -138,24 +73,18 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.10.0", + "bitflags", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools", "proc-macro2", "quote", "regex", - "rustc-hash 2.1.1", + "rustc-hash", "shlex", "syn", ] -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.10.0" @@ -169,14 +98,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] -name = "cc" -version = "1.2.55" +name = "bytemuck" +version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" -dependencies = [ - "find-msvc-tools", - "shlex", -] +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" [[package]] name = "cexpr" @@ -187,16 +112,6 @@ dependencies = [ "nom", ] -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -235,35 +150,17 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.2", + "unicode-width", "windows-sys", ] -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie-factory" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" -dependencies = [ - "futures", -] - [[package]] name = "coreaudio-rs" -version = "0.13.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ - "bitflags 1.3.2", + "bitflags", "libc", "objc2-audio-toolbox", "objc2-core-audio", @@ -277,14 +174,31 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" dependencies = [ - "bindgen 0.72.1", + "bindgen", +] + +[[package]] +name = "ctor" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e30e509674ef0ec91e21a7735766db37d163d46151b6a361d8b83dd79116bd" +dependencies = [ + "link-section", + "linktime-proc-macro", ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "dialoguer" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +dependencies = [ + "console", + "fuzzy-matcher", + "shell-words", + "tempfile", + "zeroize", +] [[package]] name = "dispatch2" @@ -292,10 +206,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.10.0", + "bitflags", "objc2", ] +[[package]] +name = "dtor" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d738e43aa64edab57c983d56de890d65fea7dc05605490c74451ce721dfd84b" + [[package]] name = "duplicate" version = "2.0.1" @@ -343,122 +263,39 @@ dependencies = [ ] [[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fast-interleave" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a5ab837f0983c0a9210d7847931f64e3be67fcb0ab93d625f7845a0132c825" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fixed-resample" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e548b80c8a294b230847d73ae8e53d1b2d5b407a1a9ec51c9698a761c4610b1" -dependencies = [ - "arrayvec", - "fast-interleave", - "ringbuf", - "rubato", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "libc", + "windows-sys", ] [[package]] -name = "futures-io" -version = "0.3.31" +name = "fastrand" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] -name = "futures-macro" -version = "0.3.31" +name = "fuzzy-matcher" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" dependencies = [ - "proc-macro2", - "quote", - "syn", + "thread_local", ] [[package]] -name = "futures-sink" -version = "0.3.31" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", + "cfg-if", + "libc", + "r-efi", ] [[package]] @@ -467,28 +304,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown", -] - [[package]] name = "indicatif" version = "0.18.3" @@ -497,7 +318,7 @@ checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" dependencies = [ "console", "portable-atomic", - "unicode-width 0.2.2", + "unicode-width", "unit-prefix", "web-time", ] @@ -506,26 +327,53 @@ dependencies = [ name = "interflow" version = "0.1.0" dependencies = [ - "alsa", "anyhow", - "bitflags 2.10.0", "cfg_aliases", - "coreaudio-rs", - "coreaudio-sys", - "duplicate", + "dialoguer", "env_logger", - "fixed-resample", "indicatif", - "libc", - "libspa", - "libspa-sys", + "interflow-core", + "interflow-coreaudio", + "interflow-wasapi", + "log", + "scattered-collect", +] + +[[package]] +name = "interflow-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "bitflags", + "duplicate", + "scattered-collect", + "thiserror", + "zerocopy", +] + +[[package]] +name = "interflow-coreaudio" +version = "0.1.0" +dependencies = [ + "coreaudio-rs", + "coreaudio-sys", + "interflow-core", "log", - "ndarray", - "nix 0.31.1", "oneshot", - "pipewire", - "rtrb", - "thiserror 2.0.18", + "scattered-collect", + "thiserror", +] + +[[package]] +name = "interflow-wasapi" +version = "0.1.0" +dependencies = [ + "bitflags", + "duplicate", + "interflow-core", + "log", + "scattered-collect", + "thiserror", "windows", "zerocopy", ] @@ -536,15 +384,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -588,23 +427,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" -version = "0.2.180" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libloading" @@ -613,52 +440,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.1", + "windows-link", ] [[package]] -name = "libspa" -version = "0.8.0" +name = "link-section" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810" +checksum = "8dc98458dfe90986c5e2f6ddcf68360c7e5c4252600153e06aa4ee8176c0f8d1" dependencies = [ - "bitflags 2.10.0", - "cc", - "convert_case", - "cookie-factory", - "libc", - "libspa-sys", - "nix 0.27.1", - "nom", - "system-deps", + "linktime-proc-macro", ] [[package]] -name = "libspa-sys" -version = "0.8.0" +name = "linktime" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" +checksum = "747e3b77d4686c927ec2052a2dcc3943e734a31b279c8f12b4332357ad9be5c8" dependencies = [ - "bindgen 0.69.5", - "cc", - "system-deps", + "ctor", + "dtor", + "link-section", ] [[package]] -name = "log" -version = "0.4.29" +name = "linktime-proc-macro" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "8c7b0a3383c2a1002d11349c92c85a666a5fb679e96c79d782cf0dbe557fd6ee" [[package]] -name = "matrixmultiply" -version = "0.3.10" +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" -dependencies = [ - "autocfg", - "rawpointer", -] +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" @@ -672,44 +493,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "ndarray" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" -dependencies = [ - "matrixmultiply", - "num-complex", - "num-integer", - "num-traits", - "portable-atomic", - "portable-atomic-util", - "rawpointer", -] - -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "libc", -] - -[[package]] -name = "nix" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -720,33 +503,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "objc2" version = "0.6.3" @@ -762,7 +518,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags 2.10.0", + "bitflags", "libc", "objc2", "objc2-core-audio", @@ -789,7 +545,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.10.0", + "bitflags", "objc2", ] @@ -799,7 +555,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags", "dispatch2", "objc2", ] @@ -833,55 +589,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oneshot" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pipewire" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda" -dependencies = [ - "anyhow", - "bitflags 2.10.0", - "libc", - "libspa", - "libspa-sys", - "nix 0.27.1", - "once_cell", - "pipewire-sys", - "thiserror 1.0.69", -] - -[[package]] -name = "pipewire-sys" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" -dependencies = [ - "bindgen 0.69.5", - "libspa-sys", - "system-deps", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c" [[package]] name = "portable-atomic" @@ -898,15 +608,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "primal-check" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" -dependencies = [ - "num-integer", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -938,19 +639,10 @@ dependencies = [ ] [[package]] -name = "rawpointer" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" - -[[package]] -name = "realfft" -version = "3.5.0" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" -dependencies = [ - "rustfft", -] +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "regex" @@ -981,41 +673,6 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" -[[package]] -name = "ringbuf" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" -dependencies = [ - "crossbeam-utils", - "portable-atomic", - "portable-atomic-util", -] - -[[package]] -name = "rtrb" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba" - -[[package]] -name = "rubato" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "realfft", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -1023,17 +680,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] -name = "rustfft" -version = "6.4.1" +name = "rustix" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "primal-check", - "strength_reduce", - "transpose", + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", ] [[package]] @@ -1043,12 +699,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] -name = "serde" -version = "1.0.228" +name = "safe_arch" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "3a52ec151f024d703f9fd65abb7cbe81e7cdb39f18917a3a37e3014470dc7c59" dependencies = [ - "serde_core", + "bytemuck", +] + +[[package]] +name = "scattered-collect" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bdd12e1c322169b156f50601d3171a34980821744f0281bfb07b90c642c5686" +dependencies = [ + "ctor", + "link-section", + "linktime", + "linktime-proc-macro", + "wide", + "xxhash-rust", ] [[package]] @@ -1072,13 +742,10 @@ dependencies = [ ] [[package]] -name = "serde_spanned" -version = "0.6.9" +name = "shell-words" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" @@ -1086,24 +753,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - [[package]] name = "syn" version = "2.0.114" @@ -1116,31 +765,16 @@ dependencies = [ ] [[package]] -name = "system-deps" -version = "6.2.2" +name = "tempfile" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", ] [[package]] @@ -1149,18 +783,7 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -1175,47 +798,12 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" +name = "thread_local" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", + "cfg-if", ] [[package]] @@ -1224,18 +812,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.2" @@ -1254,12 +830,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - [[package]] name = "version_check" version = "0.9.5" @@ -1322,70 +892,57 @@ dependencies = [ ] [[package]] -name = "winapi" -version = "0.3.9" +name = "wide" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "dfdfe6a32973f2d1b268b8895845a8a96cac2f0191e72c27cc929036060dbf89" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "bytemuck", + "safe_arch", ] -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows" -version = "0.61.3" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link 0.1.3", "windows-numerics", ] [[package]] name = "windows-collections" -version = "0.2.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ "windows-core", ] [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-future" -version = "0.2.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ "windows-core", - "windows-link 0.1.3", + "windows-link", "windows-threading", ] @@ -1411,12 +968,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" @@ -1425,30 +976,30 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ "windows-core", - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -1457,35 +1008,23 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "winnow" -version = "0.7.14" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ - "memchr", + "windows-link", ] [[package]] -name = "yansi-term" -version = "0.1.2" +name = "xxhash-rust" +version = "0.8.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" -dependencies = [ - "winapi", -] +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" [[package]] name = "zerocopy" @@ -1506,3 +1045,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" diff --git a/Cargo.toml b/Cargo.toml index 58fb043..3d60b4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,82 +1,56 @@ -[package] -name = "interflow" +[workspace] +resolver = "3" +members = ["crates/*", "."] +default-members = ["."] + +[workspace.package] version = "0.1.0" edition = "2021" -rust-version = "1.85" +rust-version = "1.91" license = "MIT" -[dependencies] +[workspace.dependencies] +anyhow = "1.0.104" bitflags = "2.10.0" duplicate = "2.0.1" -fixed-resample = "0.9.2" -log = { version = "0.4.29", features = ["kv"] } -ndarray = "0.17.1" -oneshot = "0.1.11" -rtrb = "0.3.2" -thiserror = "2.0.17" -zerocopy = { version = "0.8.39", optional = true } +interflow-core = { path = "crates/interflow-core" } +interflow-coreaudio = { path = "crates/interflow-coreaudio" } +log = "0.4.29" +scattered-collect = "0.21.3" +thiserror = "2.0.18" + +[package] +name = "interflow" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +interflow-core = { workspace = true, features = ["collect"] } +scattered-collect.workspace = true +log.workspace = true [dev-dependencies] -anyhow = "1.0.100" +anyhow.workspace = true env_logger = "0.11.8" +dialoguer = { version = "0.12.0", features = ["completion", "fuzzy-select"] } indicatif = "0.18.3" [build-dependencies] cfg_aliases = "0.2.1" -[features] -pipewire = ["dep:pipewire", "dep:libspa", "dep:libspa-sys", "dep:zerocopy"] - -[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] -alsa = "0.11.0" -libc = "0.2.180" -libspa = { version = "0.8.0", optional = true } -libspa-sys = { version = "0.8.0", optional = true } -nix = "0.31.1" -pipewire = { version = "0.8.0", optional = true, features = ["v0_3_45"] } - [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -coreaudio-rs = "0.13.0" -coreaudio-sys = "0.2.17" - -[target.'cfg(target_os = "windows")'.dependencies] -windows = { version = "0.61.3", features = [ - "Win32_Media_Audio", - "Win32_Foundation", - "Win32_Devices_Properties", - "Win32_Media_KernelStreaming", - "Win32_System_Com_StructuredStorage", - "Win32_System_Threading", - "Win32_Security", - "Win32_System_SystemServices", - "Win32_System_Variant", - "Win32_Media_Multimedia", - "Win32_UI_Shell_PropertiesSystem", -] } - -[[example]] -name = "eject_stream_pipewire" -path = "examples/eject_stream_pipewire.rs" -required-features = ["pipewire"] +interflow-coreaudio = { workspace = true, features = ["collect"] } -[[example]] -name = "enumerate_alsa" -path = "examples/enumerate_alsa.rs" +# [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd"))'.dependencies] +# alsa = "0.11.0" +# libc = "0.2.180" +# libspa = { version = "0.8.0", optional = true } +# libspa-sys = { version = "0.8.0", optional = true } +# nix = "0.31.1" +# pipewire = { version = "0.8.0", optional = true, features = ["v0_3_45"] } -[[example]] -name = "enumerate_coreaudio" -path = "examples/enumerate_coreaudio.rs" - -[[example]] -name = "enumerate_wasapi" -path = "examples/enumerate_wasapi.rs" - -[[example]] -name = "enumerate_pipewire" -path = "examples/enumerate_pipewire.rs" -required-features = ["pipewire"] - -[[example]] -name = "sine_wave_pipewire" -path = "examples/sine_wave_pipewire.rs" -required-features = ["pipewire"] +[target.'cfg(target_os = "windows")'.dependencies] +interflow-wasapi = { path = "crates/interflow-wasapi", features = ["collect"] } diff --git a/README.md b/README.md index 92b5c16..be2d48d 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,63 @@ and format conversion. - [x] WASAPI - [ ] ASIO -- [x] ALSA +- [ ] ALSA - [ ] PulseAudio -- [x] PipeWire +- [ ] PipeWire - [ ] JACK - [x] CoreAudio -## Getting Started +## Getting started + +Add `interflow` to the the list of dependencies in your `Cargo.toml` file: + +```toml +[dependencies] +interflow = { git = "https://github.com/SolarLiner/interflow.git", version = "0.1.0" } +``` + +Then, in your main function, import `interflow::prelude::*` and use the `default_stream` function: + +```rust +use interflow::prelude::*; + +fn main() { + let handle = default_stream(DeviceType::OUTPUT, |context, _input, output| { + let time = context.timestamp.as_seconds(); // stream timestamp + let time = output.timestamp.as_seconds(); // output stream provided timestamp (generally more accurate, also + // available for input) + for i in 0..output.buffer.frames() { + output.buffer.set_mono(0.0); // example: output silence + } + }); + std::thread::sleep(std::time::Duration::from_secs(10)); + let callback = handle.eject().unwrap(); // You can "eject" your callback and retrieve it, so you can reuse it in + // another stream. If you don't eject it, the callback will be dropped + // with the handle itself +} +``` + +It is important to import backends that you want to use, which is done automatically for default backends with `use +interflow::prelude::*;`. Additional third-party backends that participate in automatic registration must be imported +separately. + +The mechanism used is link-time registration through the +[`scattered-collections`](https://docs.rs/scattered-collect/latest/scattered_collect/) crate. It is possible to use +backends directly: + +```rust +use interflow::prelude::*; + +fn main() { + let device = wasapi::platform::Platform.default_device(DeviceType::OUTPUT).unwrap(); + let handle = device.create_default_stream(...); +} +``` + +Take a look at the [examples](./examples) for an overview of the available API, as well +as [the docs](https://solarliner.dev/interflow) for the generated reference documentation. + +## Contributing ### Prerequisites @@ -46,4 +96,4 @@ Ensure you have the following installed on your system: ### Building -`Interflow` uses `cargo` for dependency management and building. +`Interflow` uses `cargo` for dependency management and building. \ No newline at end of file diff --git a/crates/interflow-core/Cargo.toml b/crates/interflow-core/Cargo.toml new file mode 100644 index 0000000..584316e --- /dev/null +++ b/crates/interflow-core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "interflow-core" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +bitflags.workspace = true +duplicate.workspace = true +thiserror.workspace = true + +scattered-collect = { version = "0.21.3", optional = true } +zerocopy = { version = "0.8.39", features = ["alloc"] } + +[features] +collect = ["dep:scattered-collect"] diff --git a/crates/interflow-core/src/buffer.rs b/crates/interflow-core/src/buffer.rs new file mode 100644 index 0000000..30be040 --- /dev/null +++ b/crates/interflow-core/src/buffer.rs @@ -0,0 +1,474 @@ +use std::num::NonZeroUsize; +use std::ops; +use zerocopy::FromZeros; + +/// Audio buffer type. Data is stored in a contiguous array, in non-interleaved format. +#[derive(Clone)] +pub struct AudioBuffer { + data: Box<[T]>, + frames: NonZeroUsize, +} + +impl ops::Index for AudioBuffer { + type Output = [T]; + fn index(&self, index: usize) -> &Self::Output { + self.channel(index) + } +} + +impl ops::IndexMut for AudioBuffer { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.data[index * self.frames.get()..(index + 1) * self.frames.get()] + } +} + +impl ops::Index<(usize, usize)> for AudioBuffer { + type Output = T; + fn index(&self, (index, channel): (usize, usize)) -> &Self::Output { + &self.data[channel * self.frames.get() + index] + } +} + +impl ops::IndexMut<(usize, usize)> for AudioBuffer { + fn index_mut(&mut self, (index, channel): (usize, usize)) -> &mut Self::Output { + &mut self.data[channel * self.frames.get() + index] + } +} + +impl AudioBuffer +where + [T]: FromZeros, +{ + /// Creates a new buffer with the given number of frames and channels. The audio buffer will be zeroed out. + pub fn zeroed(frames: NonZeroUsize, channels: usize) -> Self { + let len = frames.get() * channels; + AudioBuffer { + data: <[T] as FromZeros>::new_box_zeroed_with_elems(len).unwrap(), + frames, + } + } + + pub fn resize_channels(&mut self, channels: NonZeroUsize) { + let mut data = + <[T] as FromZeros>::new_box_zeroed_with_elems(channels.get() * self.frames.get()) + .unwrap(); + for channel in 0..channels.get() { + let old_channel = channel % self.channels(); + let old_data = + &self.data[old_channel * self.frames.get()..(old_channel + 1) * self.frames.get()]; + data[channel * self.frames.get()..(channel + 1) * self.frames.get()] + .copy_from_slice(old_data); + } + self.data = data; + } + + pub fn resize_frames(&mut self, frames: NonZeroUsize) { + let mut data = + <[T] as FromZeros>::new_box_zeroed_with_elems(frames.get() * self.channels()).unwrap(); + let min_frames = self.frames.min(frames).get(); + data[..min_frames * self.channels()] + .copy_from_slice(&self.data[..min_frames * self.channels()]); + self.frames = frames; + self.data = data; + } + + pub fn copy_to_interleaved(&self, out: &mut [T]) { + debug_assert!(out.len() >= self.channels() * self.frames()); + for (i, sample) in out.iter_mut().enumerate() { + let frame = i / self.channels(); + let channel = i % self.channels(); + let i = channel * self.frames.get() + frame; + *sample = self.data[i]; + } + } + + pub fn copy_from_interleaved(&mut self, data: &[T]) { + debug_assert!(data.len() <= self.channels() * self.frames()); + for (i, sample) in data.iter().enumerate() { + let frame = i / self.channels(); + let channel = i % self.channels(); + let i = channel * self.frames.get() + frame; + self.data[i] = *sample; + } + } + + pub fn as_ref(&self) -> AudioRef<'_, T> { + AudioRef { + buffer: self, + frame_slice: (0, self.frames.get()), + } + } + + pub fn as_mut(&mut self) -> AudioMut<'_, T> { + let end = self.frames.get(); + AudioMut { + buffer: self, + frame_slice: (0, end), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum FromDataError { + #[error("Empty buffer")] + Empty, + #[error("Invalid number of channels: {channels} for buffer length {len} (len % channels) == {}", len % channels)] + InvalidChannelCount { len: usize, channels: usize }, + #[error("Invalid number of frames: {frames} for buffer length {len} (len % frames) == {}", len % frames)] + InvalidFrameCount { len: usize, frames: usize }, +} + +impl AudioBuffer { + /// Create an empty buffer that reports the given number of frames. No allocations are done since the slice is + /// 0-sized, as guaranteed by `Box::new`. + pub fn empty(frames: NonZeroUsize) -> Self { + const _NO_ALLOCATIONS: () = { + assert!(0 == size_of::<[f32; 0]>()); + }; + + Self { + data: Box::new([]), + frames, + } + } + pub fn from_fn( + channels: NonZeroUsize, + frames: NonZeroUsize, + f: impl Fn(usize, usize) -> T, + ) -> Self { + let mut data = Vec::with_capacity(channels.get() * frames.get()); + for channel in 0..channels.get() { + for frame in 0..frames.get() { + data.push(f(channel, frame)); + } + } + AudioBuffer { + data: data.into_boxed_slice(), + frames, + } + } + + pub fn from_data_channels(data: Box<[T]>, channels: usize) -> Result { + if data.is_empty() { + return Err(FromDataError::Empty); + } + if data.len() % channels != 0 { + return Err(FromDataError::InvalidChannelCount { + len: data.len(), + channels, + }); + } + + let frames = NonZeroUsize::new(data.len() / channels).unwrap(); + Ok(AudioBuffer { data, frames }) + } + + pub fn from_data_frames(data: Box<[T]>, frames: NonZeroUsize) -> Result { + if data.is_empty() { + return Ok(Self::empty(frames)); + } + + if data.len() % frames.get() != 0 { + return Err(FromDataError::InvalidFrameCount { + len: data.len(), + frames: frames.get(), + }); + } + + Ok(AudioBuffer { data, frames }) + } + + pub fn frames(&self) -> usize { + self.frames.get() + } + + pub fn channels(&self) -> usize { + self.data.len() / self.frames.get() + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + //noinspection ALL + #[duplicate::duplicate_item( + name reference(type) out; + [frame] [&'_ type] [FrameRef::<'_, T>]; + [frame_mut] [&'_ mut type] [FrameMut::<'_, T>]; + )] + pub fn name(self: reference([Self]), frame: usize) -> out { + debug_assert!(frame < self.frames()); + out { + buffer: self, + frame, + } + } + + #[duplicate::duplicate_item( + name reference(type); + [channel] [& type]; + [channel_mut] [&mut type]; + )] + pub fn name(self: reference([Self]), channel: usize) -> reference([[T]]) { + debug_assert!(channel < self.channels()); + reference([self.data[channel * self.frames.get()..(channel + 1) * self.frames.get()]]) + } + + // noinspection ALL + #[duplicate::duplicate_item( + name reference(type) out; + [slice] [& type] [AudioRef::<'_, T>]; + [slice_mut] [&mut type] [AudioMut::<'_, T>]; + )] + pub fn name(self: reference([Self]), index: impl ops::RangeBounds) -> out { + let begin = match index.start_bound() { + ops::Bound::Included(i) => *i, + ops::Bound::Excluded(i) => *i + 1, + ops::Bound::Unbounded => 0, + }; + let end = match index.end_bound() { + ops::Bound::Included(i) => *i + 1, + ops::Bound::Excluded(i) => *i, + ops::Bound::Unbounded => self.frames(), + }; + debug_assert!(begin <= end); + debug_assert!(end <= self.frames()); + out { + buffer: self, + frame_slice: (begin, end), + } + } + + pub fn chunks(&self, size: usize) -> impl Iterator> { + (0..self.frames()) + .step_by(size) + .map(move |frame| self.slice(frame..(frame + size).min(self.frames()))) + } + + pub fn chunks_exact(&self, size: usize) -> impl Iterator> { + (0..self.frames()).step_by(size).filter_map(move |frame| { + let end = frame + size; + if end > self.frames() { + return None; + } + Some(self.slice(frame..end)) + }) + } + + pub fn windows(&self, size: usize) -> impl Iterator> { + (0..self.frames() - size).map(move |frame| self.slice(frame..(frame + size))) + } + + pub fn iter_frames(&self) -> impl Iterator> { + (0..self.frames()).map(move |frame| self.frame(frame)) + } + + pub fn iter_frames_mut(&mut self) -> impl Iterator> { + IterFramesMut { + buffer: self, + frame: 0, + } + } + + pub fn iter_channels(&self) -> impl Iterator { + self.data.chunks(self.frames.get()) + } + + pub fn iter_channels_mut(&mut self) -> impl Iterator { + self.data.chunks_mut(self.frames.get()) + } + + pub fn get_channels(&self, indices: [usize; N]) -> [&[T]; N] { + indices.map(|i| self.channel(i)) + } + + pub fn get_channels_mut(&mut self, indices: [usize; N]) -> [&mut [T]; N] { + self.data + .get_disjoint_mut(indices.map(|i| i * self.frames.get()..(i + 1) * self.frames.get())) + .unwrap() + } + + pub fn set_mono(&mut self, frame: usize, value: T) + where + T: Copy, + { + self.frame_mut(frame).set_mono(value); + } +} + +#[duplicate::duplicate_item( +name reference(lifetime, type) derive; +[FrameRef] [&'lifetime type] [derive(Clone, Copy)]; +[FrameMut] [&'lifetime mut type] [derive()] +)] +#[derive] +pub struct name<'a, T> { + buffer: reference([a], [AudioBuffer]), + frame: usize, +} + +#[duplicate::duplicate_item( +name; +[FrameRef]; +[FrameMut]; +)] +impl name<'_, T> { + pub fn get(&self, channel: usize) -> T { + debug_assert!(channel < self.buffer.channels()); + self.buffer[channel][self.frame] + } + + pub fn get_frame(&self, out: &mut [T]) { + debug_assert!(out.len() >= self.buffer.channels()); + for (channel, value) in out.iter_mut().enumerate() { + *value = self.get(channel); + } + } +} + +impl FrameMut<'_, T> { + pub fn set(&mut self, channel: usize, value: T) { + debug_assert!(channel < self.buffer.channels()); + self.buffer[channel][self.frame] = value; + } + + pub fn set_frame(&mut self, data: &[T]) { + debug_assert!(data.len() >= self.buffer.channels()); + for (channel, value) in data.iter().enumerate() { + self.set(channel, *value); + } + } + + pub fn set_mono(&mut self, value: T) { + for ch in 0..self.buffer.channels() { + self.buffer[ch][self.frame] = value; + } + } +} + +struct IterFramesMut<'a, T> { + buffer: &'a mut AudioBuffer, + frame: usize, +} + +impl<'a, T> Iterator for IterFramesMut<'a, T> { + type Item = FrameMut<'a, T>; + fn next(&mut self) -> Option { + if self.frame < self.buffer.frames() { + let frame = self.frame; + self.frame += 1; + // SAFETY: + // Lifetime of the frame is actually 'a, but the compiler cannot see that + unsafe { + let buffer_ptr = self.buffer as *mut AudioBuffer; + Some((*buffer_ptr).frame_mut(frame)) + } + } else { + None + } + } +} + +#[duplicate::duplicate_item( +name reference(lifetime, type) derive; +[AudioRef] [&'lifetime type] [derive(Clone, Copy)]; +[AudioMut] [&'lifetime mut type] [derive()] +)] +#[derive] +pub struct name<'a, T> { + buffer: reference([a], [AudioBuffer]), + frame_slice: (usize, usize), +} + +#[duplicate::duplicate_item( +name; +[AudioRef]; +[AudioMut]; +)] +impl ops::Index for name<'_, T> { + type Output = [T]; + + fn index(&self, index: usize) -> &Self::Output { + &self.buffer[self.frame_slice.0 + index] + } +} + +impl ops::IndexMut for AudioMut<'_, T> { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.buffer[self.frame_slice.0 + index] + } +} + +#[duplicate::duplicate_item( +name; +[AudioRef]; +[AudioMut]; +)] +impl name<'_, T> { + pub fn frames(&self) -> usize { + self.frame_slice.1 - self.frame_slice.0 + } + + pub fn channels(&self) -> usize { + self.buffer.channels() + } + + pub fn frame(&self, index: usize) -> FrameRef<'_, T> { + self.buffer.frame(self.frame_slice.0 + index) + } + + pub fn channel(&self, channel: usize) -> &[T] { + let slice = self.buffer.channel(channel); + &slice[self.frame_slice.0..self.frame_slice.1] + } +} + +impl AudioMut<'_, T> { + pub fn frame_mut(&mut self, index: usize) -> FrameMut<'_, T> { + let frame = index + self.frame_slice.0; + debug_assert!(frame < self.frame_slice.1); + FrameMut { + buffer: self.buffer, + frame, + } + } + + pub fn channel_mut(&mut self, channel: usize) -> &mut [T] { + let slice = self.buffer.channel_mut(channel); + &mut slice[self.frame_slice.0..self.frame_slice.1] + } + + pub fn set_mono(&mut self, frame: usize, value: T) + where + T: Copy, + { + self.frame_mut(frame).set_mono(value); + } + + pub fn change_amplitude(&mut self, factor: T) + where + T: Copy + ops::MulAssign, + { + let num_channels = self.channels(); + let start = self.frame_slice.0; + let end = self.frame_slice.1; + let hop = self.buffer.frames(); + for ch in 0..num_channels { + let offset = hop * ch; + for frame in &mut self.buffer.data[offset + start..offset + end] { + *frame *= factor; + } + } + } + + pub fn as_ref(&self) -> AudioRef<'_, T> { + AudioRef { + buffer: self.buffer, + frame_slice: self.frame_slice, + } + } +} diff --git a/crates/interflow-core/src/collect.rs b/crates/interflow-core/src/collect.rs new file mode 100644 index 0000000..36fdfcd --- /dev/null +++ b/crates/interflow-core/src/collect.rs @@ -0,0 +1,32 @@ +use crate::proxies::DynPlatform; +use scattered_collect::{gather, ScatteredSlice}; + +pub type PlatformConstructor = fn() -> Option; + +/// Registration type for platforms to be used in `interflow::default_platform`. +pub struct Registration { + pub constructor: PlatformConstructor, + pub priority: i32, +} + +/// Registrar. Do not use directly, instead call [`get_registry`] which returns a sorted list of constructors. +#[gather] +pub static REGISTRAR: ScatteredSlice; + +/// Returns a sorted list of constructors to try in sequence. +pub fn get_registry() -> impl Iterator { + let mut collected = Vec::from_iter(REGISTRAR.iter()); + collected.sort_by_key(|reg| -reg.priority); + collected.into_iter().map(|reg| reg.constructor) +} + +pub fn construct_platform() -> Option { + for registration in get_registry() { + if let Some(platform) = registration() { + return Some(platform); + } + } + None +} + +pub const DEFAULT_PLATFORM_PRIORITY: i32 = 100; diff --git a/crates/interflow-core/src/device.rs b/crates/interflow-core/src/device.rs new file mode 100644 index 0000000..065f3a7 --- /dev/null +++ b/crates/interflow-core/src/device.rs @@ -0,0 +1,148 @@ +use crate::stream::{self, StreamHandle}; +use crate::traits::{Enumerate, Enumerator, ExtensionProvider}; +use crate::{dyn_compatible, DeviceType}; +use std::borrow::Cow; + +/// Configuration for an audio stream. +#[derive(Debug, Clone, PartialEq)] +pub struct StreamConfig { + /// Configured sample rate of the requested stream. The opened stream can have a different + /// sample rate, so don't rely on this parameter being correct at runtime. + pub sample_rate: f64, + /// Number of input channels requested + pub input_channels: usize, + /// Number of output channels requested + pub output_channels: usize, + /// Range of preferential buffer sizes, in units of audio samples per channel. + /// The library will make a best-effort attempt at honoring this setting, and in future versions + /// may provide additional buffering to ensure it, but for now you should not make assumptions + /// on buffer sizes based on this setting. + pub buffer_size_range: (Option, Option), + /// Whether the device should be exclusively held (meaning no other application can open the + /// same device). + pub exclusive: bool, +} + +impl StreamConfig { + /// Returns a [`DeviceType`] that describes this [`StreamConfig`]. Only [`DeviceType::INPUT`] and + /// [`DeviceType::OUTPUT`] are set. + pub fn requested_device_type(&self) -> DeviceType { + let mut ret = DeviceType::empty(); + ret.set(DeviceType::INPUT, self.input_channels > 0); + ret.set(DeviceType::OUTPUT, self.output_channels > 0); + ret + } + + /// Changes the [`StreamConfig`] such that it matches the configuration of a stream created with a device with + /// the given [`DeviceType`]. + /// + /// This method returns a copy of the input [`StreamConfig`]. + pub fn restrict(mut self, requested_type: DeviceType) -> Self { + if !requested_type.is_input() { + self.input_channels = 0; + } + if !requested_type.is_output() { + self.output_channels = 0; + } + self + } +} + +/// Configuration for an audio stream. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct ResolvedStreamConfig { + /// Configured sample rate of the requested stream. The opened stream can have a different + /// sample rate, so don't rely on this parameter being correct at runtime. + pub sample_rate: f64, + /// Number of input channels requested + pub input_channels: usize, + /// Number of output channels requested + pub output_channels: usize, + /// Maximum number of frames the audio callback will receive + pub max_frame_count: usize, +} + +/// Trait for types describing audio devices. Audio devices have zero or more inputs and outputs, +/// and depending on the driver, can be duplex devices which can provide both of them at the same +/// time natively. +pub trait Device: ExtensionProvider { + type Error: Send + Sync + 'static + std::error::Error; + type StreamHandle: StreamHandle< + Callback, + Error: Into, + >; + + fn name(&self) -> Cow<'_, str>; + + fn device_type(&self) -> DeviceType; + + /// Default configuration for this device. If [`Ok`], should return a [`StreamConfig`] that is supported (i.e., + /// returns `true` when passed to [`Self::is_config_supported`]). + fn default_config(&self) -> Result; + + /// Returns the supported I/O buffer size range for the device. + fn buffer_size_range(&self) -> Result<(Option, Option), Self::Error> { + Ok((None, None)) + } + + /// Not all configuration values make sense for a particular device, and this method tests a + /// configuration to see if it can be used in an audio stream. + fn is_config_supported(&self, config: &StreamConfig) -> bool; + + /// Creates an output stream with the provided stream configuration. For this call to be + /// valid, [`AudioDevice::is_config_supported`] should have returned `true` on the provided + /// configuration. + /// + /// An output callback is required to process the audio, whose ownership will be transferred + /// to the audio stream. + fn create_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error>; + + /// Create an output stream using the default configuration as returned by [`Self::default_output_config`]. + /// + /// # Arguments + /// + /// - `callback`: Output callback to generate audio data with. + fn default_stream( + &self, + requested_type: DeviceType, + callback: Callback, + ) -> Result, Self::Error> { + let config = self.default_config()?.restrict(requested_type); + debug_assert!( + self.is_config_supported(&config), + "Default configuration is not supported" + ); + self.create_stream(config, callback) + } +} + +/// Audio channel description. +#[derive(Debug, Clone)] +pub struct Channel<'a> { + /// Index of the channel in the device + pub index: usize, + /// Display the name for the channel, if available, else a generic name like "Channel 1" + pub name: Cow<'a, str>, +} + +pub trait NamedChannels { + fn enumerate_channels(&self) -> Enumerate<&'_ dyn Enumerator>>; +} + +dyn_compatible!(NamedChannels); + +pub trait ConfigurationList { + fn enumerate_configurations(&self) -> Enumerate<&'_ dyn Enumerator>; +} + +dyn_compatible!(ConfigurationList); + +pub trait DeviceState { + fn connected(&self) -> bool; +} + +dyn_compatible!(DeviceState); diff --git a/crates/interflow-core/src/lib.rs b/crates/interflow-core/src/lib.rs new file mode 100644 index 0000000..68e4eb7 --- /dev/null +++ b/crates/interflow-core/src/lib.rs @@ -0,0 +1,91 @@ +use bitflags::bitflags; + +pub mod buffer; +#[cfg(feature = "collect")] +pub mod collect; +pub mod device; +pub mod platform; +pub mod proxies; +pub mod stream; +pub mod timing; +pub mod traits; + +pub mod prelude { + pub use super::DeviceType; + pub use crate::buffer::{AudioBuffer, AudioMut, AudioRef}; + pub use crate::device::{self, Device as _}; + pub use crate::platform; + pub use crate::proxies::{self, CreateStreamExt}; + pub use crate::stream::{ + self, AudioInput, AudioOutput, ChannelFlags, StreamHandle, StreamLatency, StreamProxy, + }; + pub use crate::timing::{self, *}; + pub use crate::traits::{self, *}; +} + +bitflags! { + /// Represents the types/capabilities of an audio device. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct DeviceType: u32 { + /// Device supports audio input. + const INPUT = 1 << 0; + + /// Device supports audio output. + const OUTPUT = 1 << 1; + + /// Physical audio device (hardware). + const PHYSICAL = 1 << 2; + + /// Virtual/software application device. + const APPLICATION = 1 << 3; + + /// This device is set as default + const DEFAULT = 1 << 4; + + /// Device that supports both input and output. + const DUPLEX = Self::INPUT.bits() | Self::OUTPUT.bits(); + } +} + +impl DeviceType { + /// Returns true if this device type has the input capability. + pub fn is_input(&self) -> bool { + self.contains(Self::INPUT) + } + + /// Returns true if this device type has the output capability. + pub fn is_output(&self) -> bool { + self.contains(Self::OUTPUT) + } + + /// Returns true if this device type is a physical device. + pub fn is_physical(&self) -> bool { + self.contains(Self::PHYSICAL) + } + + /// Returns true if this device type is an application/virtual device. + pub fn is_application(&self) -> bool { + self.contains(Self::APPLICATION) + } + + /// Returns true if this device is set as default + pub fn is_default(&self) -> bool { + self.contains(Self::DEFAULT) + } + + /// Returns true if this device type supports both input and output. + pub fn is_duplex(&self) -> bool { + self.contains(Self::DUPLEX) + } +} + +/// Adds compile-time checks that the given trait is dyn-safe. +#[macro_export] +macro_rules! dyn_compatible { + ($(<$($generic:ident),+>)? $trait_:ident $(<$($(:$generic_name:ident =)?$generic_type:ident),+>)?) => { + const _: () = { + #[expect(unused)] + const fn typeable$(<$($generic),*>)?(_: &dyn $trait_ $(<$($($generic_name =)? $generic_type),*>)?) {} + }; + }; +} diff --git a/crates/interflow-core/src/platform.rs b/crates/interflow-core/src/platform.rs new file mode 100644 index 0000000..83f8711 --- /dev/null +++ b/crates/interflow-core/src/platform.rs @@ -0,0 +1,19 @@ +use crate::device::Device; +use crate::traits::ExtensionProvider; +use crate::DeviceType; +use std::borrow::Cow; + +/// Trait for platforms which provide audio devices. +pub trait Platform: ExtensionProvider { + type Error: Send + Sync + std::error::Error; + type Device: Device>; + const NAME: &'static str; + + fn default_device(&self, device_type: DeviceType) -> Result; + + fn list_devices(&self) -> Result, Self::Error>; +} + +pub trait ServerInfo { + fn version(&self) -> Cow<'_, str>; +} diff --git a/crates/interflow-core/src/proxies.rs b/crates/interflow-core/src/proxies.rs new file mode 100644 index 0000000..4ea628f --- /dev/null +++ b/crates/interflow-core/src/proxies.rs @@ -0,0 +1,275 @@ +use crate::device::{Device, StreamConfig}; +use crate::platform::Platform; +use crate::stream::{AudioInput, AudioOutput, CallbackContext}; +use crate::traits::ExtensionProvider; +use crate::{stream, DeviceType}; +use anyhow::{Context, Result}; +use std::any::{Any, TypeId}; +use std::borrow::Cow; +use std::marker::PhantomData; +use std::ptr::NonNull; +use std::rc::Rc; + +/// Type alias for a reference-counted platform proxy. +pub type DynPlatform = Rc; + +/// Type alias for a reference-counted device proxy. +pub type DynDevice = Rc; + +/// Trait for platform proxies that provides access to audio devices. +pub trait PlatformProxy: ExtensionProvider { + fn name(&self) -> Cow<'static, str>; + fn list_devices(&self) -> Result>; + fn list_devices_matching(&self, device_type: DeviceType) -> Result>; + fn default_device(&self, device_type: DeviceType) -> Result; +} + +impl PlatformProxy for P +where + <<

::Device as Device>::StreamHandle as stream::StreamHandle< + DynCallback, + >>::Error: Sync, +{ + fn name(&self) -> Cow<'static, str> { + Cow::Borrowed(P::NAME) + } + + fn list_devices(&self) -> Result> { + Ok(Vec::from_iter( + Platform::list_devices(self)? + .into_iter() + .map(|dev| Rc::new(dev) as DynDevice), + )) + } + + fn list_devices_matching(&self, device_type: DeviceType) -> Result> { + Ok(Vec::from_iter( + Platform::list_devices(self)?.into_iter().filter_map(|dev| { + Device::device_type(&dev) + .contains(device_type) + .then(|| Rc::new(dev) as DynDevice) + }), + )) + } + + fn default_device(&self, device_type: DeviceType) -> Result { + let device = Platform::default_device(self, device_type)?; + Ok(Rc::new(device) as DynDevice) + } +} + +/// Trait for device proxies that describes audio device capabilities. +pub trait DeviceProxy: ExtensionProvider { + fn name(&self) -> Cow<'_, str>; + fn device_type(&self) -> DeviceType; + fn default_config(&self) -> Result; + fn is_config_supported(&self, config: &StreamConfig) -> bool; + fn buffer_size_range(&self) -> Result<(Option, Option)>; + fn create_stream_raw( + &self, + config: StreamConfig, + callback: DynCallback, + ) -> Result; + + fn create_default_stream_raw( + &self, + requested_type: DeviceType, + callback: DynCallback, + ) -> Result; +} + +impl DeviceProxy for D +where + as stream::StreamHandle>::Error: Sync, +{ + #[inline] + fn name(&self) -> Cow<'_, str> { + Device::name(self) + } + + fn device_type(&self) -> DeviceType { + Device::device_type(self) + } + + fn default_config(&self) -> Result { + Ok(Device::default_config(self)?) + } + + fn is_config_supported(&self, config: &StreamConfig) -> bool { + Device::is_config_supported(self, config) + } + + fn buffer_size_range(&self) -> Result<(Option, Option)> { + Ok(Device::buffer_size_range(self)?) + } + + fn create_stream_raw( + &self, + config: StreamConfig, + callback: DynCallback, + ) -> Result { + let handle = Device::create_stream(self, config, callback).context("Cannot open stream")?; + Ok(RawStreamHandle::from_handle(handle)) + } + + fn create_default_stream_raw( + &self, + requested_type: DeviceType, + callback: DynCallback, + ) -> Result { + let handle = + Device::default_stream(self, requested_type, callback).context("Cannot open stream")?; + Ok(RawStreamHandle::from_handle(handle)) + } +} + +pub struct DynCallback { + type_id: TypeId, + handle: NonNull<()>, + prepare: unsafe fn(NonNull<()>, CallbackContext), + process_audio: unsafe fn(NonNull<()>, CallbackContext, &AudioInput, &mut AudioOutput), +} + +unsafe impl Send for DynCallback {} + +impl DynCallback { + pub fn from_callback(callback: Callback) -> Self { + Self { + type_id: TypeId::of::(), + handle: unsafe { create_nonnull(callback).cast() }, + prepare: |handle, context| { + unsafe { handle.cast::().as_mut() }.prepare(context) + }, + process_audio: |handle, context, input, output| { + unsafe { handle.cast::().as_mut() }.process_audio(context, input, output) + }, + } + } + + pub fn into_inner(self) -> Option> { + if self.type_id != TypeId::of::() { + return None; + } + Some(unsafe { consume_nonnull(self.handle.cast::()) }) + } +} + +impl stream::Callback for DynCallback { + fn prepare(&mut self, context: CallbackContext) { + unsafe { (self.prepare)(self.handle, context) } + } + + fn process_audio( + &mut self, + context: CallbackContext, + input: &AudioInput, + output: &mut AudioOutput, + ) { + unsafe { (self.process_audio)(self.handle, context, input, output) } + } +} + +pub struct RawStreamHandle { + handle: Option>, + eject: unsafe fn(NonNull<()>) -> Result, + drop: unsafe fn(NonNull<()>), +} + +impl RawStreamHandle { + pub fn from_handle>( + handle: Handle, + ) -> Self { + let handle = Box::into_raw(Box::new(handle)); + let handle = Some(unsafe { NonNull::new_unchecked(handle).cast() }); + Self { + handle, + eject: |ptr| { + let handle = unsafe { Box::from_raw(ptr.cast::().as_ptr()) }; + Ok(handle.eject()?) + }, + drop: |ptr| { + let _ = unsafe { Box::from_raw(ptr.cast::().as_ptr()) }; + }, + } + } + + pub fn eject(mut self) -> Result { + let Some(handle) = self.handle.take() else { + anyhow::bail!("Stream already ejected"); + }; + Ok(unsafe { (self.eject)(handle)? }) + } +} + +impl Drop for RawStreamHandle { + fn drop(&mut self) { + let Some(handle) = self.handle.take() else { + return; + }; + unsafe { (self.drop)(handle) }; + } +} + +pub trait CreateStreamExt: DeviceProxy { + fn create_stream( + &self, + config: StreamConfig, + callback: Callback, + ) -> Result> { + let callback = DynCallback::from_callback(callback); + let handle = self.create_stream_raw(config, callback)?; + Ok(unsafe { StreamHandle::from_raw(handle) }) + } + + fn default_stream( + &self, + requested_type: DeviceType, + callback: Callback, + ) -> Result> { + let callback = DynCallback::from_callback(callback); + let handle = self.create_default_stream_raw(requested_type, callback)?; + Ok(unsafe { StreamHandle::from_raw(handle) }) + } +} + +impl CreateStreamExt for C {} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct AnyError(anyhow::Error); + +pub struct StreamHandle { + __callback: PhantomData, + raw: RawStreamHandle, +} + +impl StreamHandle { + unsafe fn from_raw(raw: RawStreamHandle) -> Self { + Self { + __callback: PhantomData, + raw, + } + } +} + +impl stream::StreamHandle> + for StreamHandle +{ + type Error = AnyError; + + fn eject(self) -> std::result::Result, Self::Error> { + (|| -> Result> { + let raw_callback = self.raw.eject()?; + Ok(raw_callback.into_inner().unwrap()) + })() + .map_err(AnyError) + } +} + +unsafe fn create_nonnull(value: T) -> NonNull { + NonNull::new_unchecked(Box::into_raw(Box::new(value))) +} + +unsafe fn consume_nonnull(handle: NonNull) -> Box { + Box::from_raw(handle.as_ptr()) +} diff --git a/crates/interflow-core/src/stream.rs b/crates/interflow-core/src/stream.rs new file mode 100644 index 0000000..727cb59 --- /dev/null +++ b/crates/interflow-core/src/stream.rs @@ -0,0 +1,111 @@ +use crate::buffer::{AudioMut, AudioRef}; +use crate::device::ResolvedStreamConfig; +use crate::stream; +use crate::timing::Timestamp; +use crate::traits::ExtensionProvider; +use bitflags::bitflags; + +pub trait StreamProxy: Send + Sync + ExtensionProvider {} + +bitflags! { + pub struct ChannelFlags: u32 { + const INACTIVE = 0x0000_0001; + const UNDERFLOW = 0x0000_0002; + const OVERFLOW = 0x0000_0004; + } +} + +pub trait StreamLatency { + fn input_latency(&self, channel: usize) -> Option; + fn output_latency(&self, channel: usize) -> Option; +} + +#[duplicate::duplicate_item( + name bufty derive; + [AudioInput] [AudioRef < 'a, T >] [derive(Copy, Clone)]; + [AudioOutput] [AudioMut < 'a, T >] [derive()]; +)] +/// Plain-old-data object holding references to the audio buffer and the associated time-keeping +/// [`Timestamp`]. This timestamp is associated with the stream, and in the cases where the +/// driver provides timing information, it is used instead of relying on sample-counting. +#[derive] +pub struct name<'a, T> { + /// Associated time stamp for this callback. The time represents the duration for which the + /// stream has been opened, and is either provided by the driver if available, or is kept up + /// manually by the library. + pub timestamp: Timestamp, + /// Audio buffer data. + pub buffer: bufty, + pub channel_flags: &'a [ChannelFlags], +} + +/// Plain-old-data object holding the passed-in stream configuration, as well as a general +/// callback timestamp, which can be different from the input and output streams in case of +/// cross-stream latencies; differences in timing can indicate desync. +#[derive(Copy, Clone)] +pub struct CallbackContext<'a> { + /// Passed-in stream configuration. Values have been updated where necessary to correspond to + /// the actual stream properties. + pub stream_config: &'a ResolvedStreamConfig, + /// Callback-wide timestamp. + pub timestamp: Timestamp, + pub stream_proxy: &'a dyn StreamProxy, +} + +/// Trait for types which handles an audio stream (input or output). +pub trait StreamHandle { + /// Type of errors which have caused the stream to fail. + type Error: Send + std::error::Error; + + /// Eject the stream, returning ownership of the callback. + /// + /// An error can occur when an irrecoverable error has occured and ownership has been lost + /// already. + fn eject(self) -> Result; +} + +/// Trait of types which process audio data. This is the trait that users will want to +/// implement when processing audio from a device. +pub trait Callback: Send { + /// Prepare the audio callback to process audio. This function is *not* real-time safe (i.e., allocations can be + /// performed), in preparation for processing the stream with [`Self::process_audio`]. + fn prepare(&mut self, context: CallbackContext); + + /// Callback called when audio data can be processed. + fn process_audio( + &mut self, + context: CallbackContext, + input: &AudioInput, + output: &mut AudioOutput, + ); +} + +impl Callback for Box { + fn prepare(&mut self, context: CallbackContext) { + Callback::prepare(&mut **self, context) + } + + fn process_audio( + &mut self, + context: CallbackContext, + input: &AudioInput, + output: &mut AudioOutput, + ) { + Callback::process_audio(&mut **self, context, input, output) + } +} + +impl, &mut AudioOutput)> Callback + for &mut F +{ + fn prepare(&mut self, _: CallbackContext) {} + + fn process_audio( + &mut self, + context: CallbackContext, + input: &AudioInput, + output: &mut AudioOutput, + ) { + (self)(context, input, output); + } +} diff --git a/src/timestamp.rs b/crates/interflow-core/src/timing.rs similarity index 68% rename from src/timestamp.rs rename to crates/interflow-core/src/timing.rs index 3831dc0..7e378de 100644 --- a/src/timestamp.rs +++ b/crates/interflow-core/src/timing.rs @@ -1,31 +1,6 @@ -//! Module for handling timestamp and duration calculations in audio processing. -//! -//! This module provides the [`Timestamp`] type, which manages time-related operations -//! for audio streams by tracking sample counts and their corresponding durations based -//! on a specified sample rate. It supports basic arithmetic operations for sample -//! counting and duration calculations, making it useful for audio stream synchronization -//! and timing operations. -//! -//! # Examples -//! -//! ```rust -//! use std::time::Duration; -//! use interflow::timestamp::Timestamp; -//! -//! // Create a timestamp for 48 kHz audio -//! let mut ts = Timestamp::new(48000.); -//! -//! // Add 48 samples (1ms at 48kHz) -//! ts += 48; -//! assert_eq!(ts.as_duration(), Duration::from_millis(1)); -//! -//! // Convert a duration to samples -//! let ts2 = Timestamp::from_duration(48000., Duration::from_millis(100)); -//! assert_eq!(ts2.counter, 4800); -//! ``` - use std::ops; use std::ops::AddAssign; +use std::sync::atomic::AtomicU64; use std::time::Duration; /// Timestamp value, which computes duration information from a provided samplerate and a running @@ -140,13 +115,68 @@ impl Timestamp { } } + /// Compute the number of seconds represented in this [`Timestamp`]. + pub fn as_seconds(&self) -> f64 { + self.counter as f64 / self.samplerate + } + /// Compute the duration represented by this [`Timestamp`]. pub fn as_duration(&self) -> Duration { Duration::from_secs_f64(self.as_seconds()) } +} - /// Compute the number of seconds represented in this [`Timestamp`]. - pub fn as_seconds(&self) -> f64 { - self.counter as f64 / self.samplerate +/// Atomic version of [`Timestamp`] to be shared between threads. Mainly used by the [`crate::duplex`] module, but +/// may be useful in user code as well. +pub struct AtomicTimestamp { + samplerate: AtomicU64, + counter: AtomicU64, +} + +impl AtomicTimestamp { + /// Create a new [`AtomicTimestamp`] with zeroed values. + pub fn zeroed() -> Self { + Self { + samplerate: AtomicU64::new(0), + counter: AtomicU64::new(0), + } + } + /// Update the contents with the provided [`Timestamp`]. + pub fn update(&self, ts: Timestamp) { + self.samplerate.store( + ts.samplerate.to_bits(), + std::sync::atomic::Ordering::Relaxed, + ); + self.counter + .store(ts.counter, std::sync::atomic::Ordering::Relaxed); + } + + /// Load values and return them as a [`Timestamp`]. + pub fn as_timestamp(&self) -> Timestamp { + Timestamp { + samplerate: f64::from_bits(self.samplerate.load(std::sync::atomic::Ordering::Relaxed)), + counter: self.counter.load(std::sync::atomic::Ordering::Relaxed), + } + } + + /// Add the provided number of frames to this. + pub fn add_frames(&self, frames: u64) { + self.counter + .fetch_add(frames, std::sync::atomic::Ordering::Relaxed); + } +} + +impl From for AtomicTimestamp { + fn from(value: Timestamp) -> Self { + Self { + samplerate: AtomicU64::new(value.samplerate.to_bits()), + counter: AtomicU64::new(value.counter), + } + } +} + +impl From for Timestamp { + fn from(value: AtomicTimestamp) -> Self { + value.as_timestamp() } } diff --git a/crates/interflow-core/src/traits.rs b/crates/interflow-core/src/traits.rs new file mode 100644 index 0000000..7b866c3 --- /dev/null +++ b/crates/interflow-core/src/traits.rs @@ -0,0 +1,206 @@ +use crate::dyn_compatible; +use std::any::TypeId; +use std::marker::PhantomData; +use std::mem::MaybeUninit; + +/// A fully type-erased pointer that can work with both thin and fat pointers. +/// Copied from . +#[derive(Copy, Clone)] +struct ErasedPtr { + value: MaybeUninit<[usize; 2]>, +} + +impl ErasedPtr { + /// Erase `ptr`. + fn new(ptr: *const T) -> Self { + let mut res = ErasedPtr { + value: MaybeUninit::zeroed(), + }; + + let len = size_of::<*const T>(); + assert!(len <= size_of::<[usize; 2]>()); + + let ptr_val = (&ptr) as *const *const T as *const u8; + let target = res.value.as_mut_ptr() as *mut u8; + // SAFETY: The target is valid for at least `len` bytes, and has no + // requirements on the value. + unsafe { + core::ptr::copy_nonoverlapping(ptr_val, target, len); + } + + res + } + + /// Erase `ptr`. + fn new_mut(ptr: *mut T) -> Self { + let mut res = ErasedPtr { + value: MaybeUninit::zeroed(), + }; + + let len = size_of::<*const T>(); + assert!(len <= size_of::<[usize; 2]>()); + + let ptr_val = (&ptr) as *const *mut T as *mut u8; + let target = res.value.as_mut_ptr() as *mut u8; + // SAFETY: The target is valid for at least `len` bytes, and has no + // requirements on the value. + unsafe { + core::ptr::copy_nonoverlapping(ptr_val, target, len); + } + + res + } + + /// Convert the type erased pointer back into a pointer. + /// + /// # Safety + /// + /// The type `T` must be the same type as the one used with `new` or `new_mut`. + unsafe fn as_ptr(&self) -> *const T { + // SAFETY: The constructor ensures that the first `size_of::()` + // bytes of `&self.value` are a valid `*const T` pointer. + unsafe { core::mem::transmute_copy(&self.value) } + } + + /// Convert the type erased pointer back into a pointer. + /// + /// # Safety + /// + /// This [`ErasedPtr`] must have been constructed from [`Selector::new_mut`], using the same `T` type used in + /// this function. + unsafe fn as_mut_ptr(&self) -> *mut T { + // SAFETY: The constructor ensures that the first `size_of::()` + // bytes of `&self.value` are a valid `*const T` pointer. + // The caller ensures that the original pointer was mutable. + unsafe { core::mem::transmute_copy(&self.value) } + } +} + +/// Type that can dynamically retrieve a type registered by an [`ExtensionProvider`] object. +/// Types are queried on-demand, every time an extension is requested. +/// +/// Consumers of [`ExtensionProvider`] should instead use [`ExtensionExt::lookup`]. +pub struct Selector<'a> { + __lifetime: PhantomData<&'a ()>, + target: TypeId, + found: Option, + found_mut: Option, +} + +impl<'a> Selector<'a> { + pub(crate) const fn new() -> Self { + Self { + __lifetime: PhantomData, + target: TypeId::of::(), + found: None, + found_mut: None, + } + } + + pub fn register(&mut self, value: &T) -> &mut Self { + if self.target == TypeId::of::() { + self.found = Some(ErasedPtr::new(value)); + } + self + } + + pub fn register_mut(&mut self, value: &mut T) -> &mut Self { + if self.target == TypeId::of::() { + self.found_mut = Some(ErasedPtr::new_mut(value)); + } + self + } + + pub(crate) fn finish(self) -> Option<&'a I> { + assert_eq!(self.target, TypeId::of::()); + let target = self.found.or(self.found_mut)?; + Some(unsafe { &*target.as_ptr() }) + } + + pub(crate) fn finish_mut(self) -> Option<&'a mut I> { + assert_eq!(self.target, TypeId::of::()); + Some(unsafe { &mut *self.found_mut?.as_mut_ptr() }) + } +} + +/// Trait for types that have optional data available on-demand. +pub trait ExtensionProvider: 'static { + /// Register on-demand types. Note that the implementation details around registering extensions mean that this + /// function will be called for every request. Runtime checks are expected, but this function should remain as fast + /// as possible. + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a>; + fn register_mut<'a, 'sel>( + &'a mut self, + selector: &'sel mut Selector<'a>, + ) -> &'sel mut Selector<'a> { + self.register(selector) + } +} + +dyn_compatible!(ExtensionProvider); + +pub trait ExtensionProviderExt: ExtensionProvider { + /// Look up [`T`] from the extension if it is registered. Returns an immutable reference. + fn lookup(&self) -> Option<&T>; + + /// Look up [`T`] from the extension if it is registered. Returns as mutable reference. + fn lookup_mut(&mut self) -> Option<&mut T>; +} + +impl ExtensionProviderExt for E { + fn lookup(&self) -> Option<&T> { + let mut selector = Selector::new::(); + selector.register::(self); + self.register(&mut selector); + selector.finish::() + } + + fn lookup_mut(&mut self) -> Option<&mut T> { + let mut selector = Selector::new::(); + selector.register_mut::(self); + self.register_mut(&mut selector); + selector.finish_mut::() + } +} + +pub trait Enumerator { + type Item; + fn len(&self) -> usize; + fn at(&self, index: usize) -> Option; +} + +impl<'a, T> Enumerator for &'a dyn Enumerator { + type Item = T; + + fn len(&self) -> usize { + (*self).len() + } + + fn at(&self, index: usize) -> Option { + (*self).at(index) + } +} + +pub struct Enumerate { + enumerator: E, + i: usize, +} + +impl Iterator for Enumerate { + type Item = E::Item; + + fn next(&mut self) -> Option { + let result = self.enumerator.at(self.i); + self.i += 1; + result + } + + fn size_hint(&self) -> (usize, Option) { + let len = self.enumerator.len(); + (len, Some(len)) + } +} + +pub fn enumerate(enumerator: &dyn Enumerator) -> impl use<'_, T> + Iterator { + Enumerate { enumerator, i: 0 } +} diff --git a/crates/interflow-coreaudio/Cargo.toml b/crates/interflow-coreaudio/Cargo.toml new file mode 100644 index 0000000..125a0d7 --- /dev/null +++ b/crates/interflow-coreaudio/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "interflow-coreaudio" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[dependencies] +interflow-core.workspace = true + +coreaudio-rs = "0.14.1" +coreaudio-sys = "0.2.17" +log.workspace = true +thiserror.workspace = true +oneshot = { version = "0.2.1", features = ["std"] } +scattered-collect = { workspace = true, optional = true } + +[features] +collect = ["interflow-core/collect", "dep:scattered-collect"] diff --git a/crates/interflow-coreaudio/src/device.rs b/crates/interflow-coreaudio/src/device.rs new file mode 100644 index 0000000..5cf71ee --- /dev/null +++ b/crates/interflow-coreaudio/src/device.rs @@ -0,0 +1,316 @@ +//! CoreAudio device implementation +use crate::stream::Handle; +use crate::{utils, Error}; +use coreaudio::audio_unit::macos_helpers::{ + get_audio_device_supports_scope, get_default_device_id, get_device_name, +}; +use coreaudio::audio_unit::{AudioUnit, Element, IOType, Scope}; +use coreaudio_sys::{ + kAudioDevicePropertyBufferFrameSizeRange, kAudioHardwarePropertyDefaultInputDevice, + kAudioHardwarePropertyDefaultOutputDevice, kAudioHardwarePropertyDefaultSystemOutputDevice, + kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeGlobal, + kAudioObjectPropertyScopeInput, kAudioObjectPropertyScopeOutput, kAudioObjectSystemObject, + kAudioOutputUnitProperty_CurrentDevice, kAudioOutputUnitProperty_EnableIO, AudioDeviceID, + AudioObjectGetPropertyData, AudioObjectID, AudioObjectPropertyAddress, AudioValueRange, +}; +use interflow_core::device::StreamConfig; +use interflow_core::traits::{ExtensionProvider, Selector}; +use interflow_core::{device, stream, DeviceType}; +use std::borrow::Cow; +use std::cell::OnceCell; +use std::ffi::c_uint; +use std::ptr::null; + +/// Audio device request type. CoreAudio has separate code paths for getting the default device, and getting a +/// specific ID. Furthermore, "default device"s are automatically re-routed when the user changes the default device. +#[derive(Debug, Default, Copy, Clone)] +pub enum DeviceRequest { + /// Automatically connects to the default output device, generic stream. + #[default] + DefaultOutput, + /// Automatically connects to the default output device, notification stream. + SystemOutput, + /// Automatically connects to the default input device. + DefaultInput, + /// Automatically connects to the default voice input device, using the system's voice enhancement processing if + /// enabled. + VoiceInput, + /// Connect to this specific output. + Specific(AudioDeviceID), +} + +impl DeviceRequest { + pub(crate) fn to_audio_unit(&self) -> Result { + match self { + Self::DefaultOutput => Ok(AudioUnit::new(IOType::DefaultOutput)?), + Self::SystemOutput => Ok(AudioUnit::new(IOType::SystemOutput)?), + Self::VoiceInput => Ok(AudioUnit::new(IOType::VoiceProcessingIO)?), + Self::DefaultInput => { + let mut unit = AudioUnit::new_uninitialized(IOType::HalOutput)?; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Input, + Element::Input, + Some(&1u32), + )?; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Output, + Element::Output, + Some(&0u32), + )?; + let input_device_id = get_default_device_id(true).unwrap_or(0); + unit.set_property( + kAudioOutputUnitProperty_CurrentDevice, + Scope::Global, + Element::Output, + Some(&input_device_id), + )?; + unit.initialize()?; + Ok(unit) + } + &Self::Specific(id) => { + let mut unit = AudioUnit::new_uninitialized(IOType::HalOutput)?; + let device_type = self.device_type(); + let value = if device_type.is_input() { 1u32 } else { 0 }; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Input, + Element::Input, + Some(&value), + )?; + let value = if device_type.is_output() { 1u32 } else { 0 }; + unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Output, + Element::Output, + Some(&value), + )?; + unit.set_property( + kAudioOutputUnitProperty_CurrentDevice, + Scope::Global, + Element::Output, + Some(&id), + )?; + unit.initialize()?; + Ok(unit) + } + } + } + + /// Return the name associated with this [`DeviceRequest`]. This is a descriptive name, and does not uniquely + /// identify the device. + pub fn name(&self) -> Cow<'_, str> { + let (selector, fallback) = match self { + Self::DefaultOutput => (kAudioHardwarePropertyDefaultOutputDevice, "Default Output"), + Self::DefaultInput => (kAudioHardwarePropertyDefaultInputDevice, "Default Input"), + Self::SystemOutput => ( + kAudioHardwarePropertyDefaultSystemOutputDevice, + "Notifications output", + ), + Self::VoiceInput => (c_uint::MAX, "Voice Input"), + &Self::Specific(id) => { + return match get_device_name(id) { + Ok(s) => Cow::Owned(s), + Err(err) => { + log::error!("Cannot get device name for ID: {}: {err}", id); + Cow::Borrowed("") + } + } + } + }; + + (|| { + let property_address = AudioObjectPropertyAddress { + mSelector: selector, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + + let mut audio_device_id: AudioDeviceID = 0; + let mut data_size = size_of::() as u32; + let status = unsafe { + AudioObjectGetPropertyData( + kAudioObjectSystemObject as AudioObjectID, + &property_address, + 0, + null(), + &mut data_size, + (&raw mut audio_device_id).cast(), + ) + }; + coreaudio::Error::from_os_status(status)?; + let name = get_device_name(audio_device_id)?; + let name = Cow::Owned(format!("{fallback} ({name})")); + Ok(name) + })() + .unwrap_or_else(|err: Error| { + log::error!("Cannot get device name: {err}"); + Cow::Borrowed(fallback) + }) + } + + /// Returns the [`DeviceType`] that best describes this [`DeviceRequest`]. + pub fn device_type(&self) -> DeviceType { + let log_error = |result: Result| { + result + .inspect_err(|err| { + log::error!("Cannot get device type for {:?}: {err}", self.name()); + }) + .unwrap_or(false) + }; + let supports_scope = |scope| match self { + Self::DefaultOutput | Self::SystemOutput if matches!(scope, Scope::Output) => true, + Self::DefaultInput | Self::VoiceInput if matches!(scope, Scope::Input) => true, + &Self::Specific(id) => log_error(get_audio_device_supports_scope(id, scope)), + _ => false, + }; + let is_default = match self { + Self::DefaultOutput | Self::SystemOutput | Self::DefaultInput | Self::VoiceInput => { + true + } + &Self::Specific(id) => { + get_default_device_id(true) == Some(id) || get_default_device_id(false) == Some(id) + } + }; + + let mut type_ = DeviceType::PHYSICAL; + type_.set(DeviceType::INPUT, supports_scope(Scope::Input)); + type_.set(DeviceType::OUTPUT, supports_scope(Scope::Output)); + type_.set(DeviceType::DEFAULT, is_default); + type_ + } + + /// Queries the accepted range of buffer sizes that the device accepts. + pub fn buffer_size_range(&self) -> Result<(Option, Option), Error> { + match self { + Self::DefaultOutput | Self::SystemOutput | Self::DefaultInput | Self::VoiceInput => { + Ok((None, None)) + } + &Self::Specific(id) => { + let address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyBufferFrameSizeRange, + mScope: if self.device_type().is_input() { + kAudioObjectPropertyScopeInput + } else { + kAudioObjectPropertyScopeOutput + }, + mElement: kAudioObjectPropertyElementMaster, + }; + let range = utils::get_device_property::(id, address)?; + Ok((Some(range.mMinimum as usize), Some(range.mMaximum as usize))) + } + } + } +} + +/// CoreAudio device type. Contains the requested device (including default output/notification stream) and +/// associated audio unit. +pub struct Device { + pub(crate) request: DeviceRequest, + audio_unit: OnceCell, +} + +impl Device { + /// Create a new device from a [`DeviceRequest`]. + pub fn new(request: DeviceRequest) -> Self { + Self { + request, + audio_unit: OnceCell::new(), + } + } + + /// Create a device from a specific device ID. + pub fn from_id(id: AudioDeviceID) -> Self { + Self::new(DeviceRequest::Specific(id)) + } + + /// Create a device following the default output. + pub fn default_output() -> Self { + Self::new(DeviceRequest::DefaultOutput) + } + + /// Create a device following the default input. + pub fn default_input() -> Self { + Self::new(DeviceRequest::DefaultInput) + } + + /// Consumes this device object, returning the associated Audio Unit. + pub fn into_audio_unit(mut self) -> Result { + match self.audio_unit.take() { + Some(unit) => Ok(unit), + None => self.request.to_audio_unit(), + } + } +} + +impl ExtensionProvider for Device { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector.register::(self) + } +} + +impl device::Device for Device { + type Error = Error; + + type StreamHandle = Handle; + + fn name(&self) -> Cow<'_, str> { + self.request.name() + } + + fn device_type(&self) -> DeviceType { + self.request.device_type() + } + + fn default_config(&self) -> Result { + let au = self.get_audio_unit()?; + let input_channels = au + .input_stream_format() + .map(|fmt| fmt.channels as usize) + .unwrap_or(0); + let output_channels = au + .output_stream_format() + .map(|fmt| fmt.channels as usize) + .unwrap_or(0); + let buffer_size_range = self.request.buffer_size_range()?; + Ok(StreamConfig { + sample_rate: au.sample_rate()?, + input_channels, + output_channels, + buffer_size_range, + exclusive: false, + }) + } + + fn is_config_supported(&self, _config: &StreamConfig) -> bool { + true + } + + fn create_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + Handle::new(self, stream_config, callback) + } +} + +/// Extension trait for [`Device`]. Either use directly, or lookup the trait with `device.lookup::()`. +pub trait CoreAudioDeviceExt { + /// Return the audio unit associated with this device. Creates it at most once; subsequent calls return the same + /// object. + fn get_audio_unit(&self) -> Result<&AudioUnit, Error>; +} + +impl CoreAudioDeviceExt for Device { + fn get_audio_unit(&self) -> Result<&AudioUnit, Error> { + if let Some(unit) = self.audio_unit.get() { + return Ok(unit); + } + let unit = self.request.to_audio_unit()?; + let result = self.audio_unit.set(unit); + assert!(result.is_ok()); + Ok(self.audio_unit.get().unwrap()) + } +} diff --git a/crates/interflow-coreaudio/src/lib.rs b/crates/interflow-coreaudio/src/lib.rs new file mode 100644 index 0000000..53dac73 --- /dev/null +++ b/crates/interflow-coreaudio/src/lib.rs @@ -0,0 +1,43 @@ +//! # Interflow CoreAudio +//! +//! Interflow backend using CoreAudio for macOS and iOS applications +#![warn(missing_docs)] +use coreaudio::audio_unit; +use interflow_core::DeviceType; +use std::convert::Infallible; + +pub mod device; +pub mod platform; +pub mod stream; +mod utils; + +/// Prelude module. Import all with `interflow_coreaudio::prelude::*`. +pub mod prelude { + pub use crate::device::{Device, DeviceRequest}; + pub use crate::platform::Platform; + pub use crate::stream::Handle; +} + +/// Type of errors from the CoreAudio backend +#[derive(Debug, thiserror::Error)] +#[error("CoreAudio error: ")] +pub enum Error { + /// Error originating from CoreAudio + #[error(transparent)] + Backend(#[from] coreaudio::Error), + /// The scope given to an audio device is invalid. + #[error("Invalid scope {0:?}")] + InvalidScope(audio_unit::Scope), + /// No matching devices for the given type. + #[error("No matching devices for type: {0:?}")] + NoMatchingDevices(DeviceType), + /// User has requested a duplex (both input and output) stream, which is not supported by CoreAudio. + #[error("Duplex devices are not supported")] + DuplexUnavailable, +} + +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() + } +} diff --git a/crates/interflow-coreaudio/src/platform.rs b/crates/interflow-coreaudio/src/platform.rs new file mode 100644 index 0000000..28e960d --- /dev/null +++ b/crates/interflow-coreaudio/src/platform.rs @@ -0,0 +1,45 @@ +//! CoreAudio platform implementation +use crate::device::Device; +use crate::Error; +use coreaudio::audio_unit::macos_helpers::{get_audio_device_ids, get_default_device_id}; +use interflow_core::traits::{ExtensionProvider, Selector}; +use interflow_core::{collect, platform, DeviceType}; +use std::rc::Rc; + +/// The CoreAudio driver. +pub struct Platform; + +impl ExtensionProvider for Platform { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl platform::Platform for Platform { + type Error = Error; + + type Device = Device; + + const NAME: &'static str = "CoreAudio"; + + fn default_device(&self, device_type: DeviceType) -> Result { + if device_type.is_output() || !device_type.is_input() { + Ok(Device::default_output()) + } else { + Ok(Device::default_input()) + } + } + + fn list_devices(&self) -> Result, Self::Error> { + Ok([Device::default_output(), Device::default_input()] + .into_iter() + .chain(get_audio_device_ids()?.into_iter().map(Device::from_id))) + } +} + +#[cfg(feature = "collect")] +#[scattered_collect::scatter(collect::REGISTRAR)] +static COREAUDIO_PLATFORM_REGISTRATION: collect::Registration = collect::Registration { + constructor: || Some(Rc::new(Platform)), + priority: collect::DEFAULT_PLATFORM_PRIORITY, +}; diff --git a/crates/interflow-coreaudio/src/stream.rs b/crates/interflow-coreaudio/src/stream.rs new file mode 100644 index 0000000..07b3e80 --- /dev/null +++ b/crates/interflow-coreaudio/src/stream.rs @@ -0,0 +1,270 @@ +//! CoreAudio stream implementation. +use crate::device::Device; +use crate::Error; +use coreaudio::audio_unit::audio_format::LinearPcmFlags; +use coreaudio::audio_unit::render_callback::{data, Args}; +use coreaudio::audio_unit::{AudioUnit, Element, SampleFormat, Scope, StreamFormat}; +use coreaudio_sys::{kAudioDevicePropertyBufferFrameSize, kAudioUnitProperty_StreamFormat}; +use interflow_core::buffer::AudioBuffer; +use interflow_core::device::{Device as _, ResolvedStreamConfig, StreamConfig}; +use interflow_core::stream; +use interflow_core::stream::{AudioInput, AudioOutput, CallbackContext, StreamProxy}; +use interflow_core::timing::Timestamp; +use interflow_core::traits::{ExtensionProvider, Selector}; +use std::convert::Infallible; +use std::num::NonZeroUsize; + +/// Stream type created by opening up a stream on a [`Device`]. +pub struct Handle { + audio_unit: AudioUnit, + callback_retrieve: oneshot::Sender>, +} + +impl stream::StreamHandle for Handle { + type Error = Infallible; + + fn eject(mut self) -> Result { + let (tx, rx) = oneshot::channel(); + self.callback_retrieve + .send(tx) + .expect("Callback receiver cannot have been dropped yet"); + let callback = rx.recv().expect("Oneshot receiver must be used"); + self.audio_unit.free_input_callback(); + self.audio_unit.free_render_callback(); + Ok(callback) + } +} + +fn input_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { + StreamFormat { + sample_rate, + sample_format: SampleFormat::I16, + flags: LinearPcmFlags::IS_SIGNED_INTEGER, + channels: channel_count as _, + } +} + +fn output_stream_format(sample_rate: f64, channel_count: usize) -> StreamFormat { + StreamFormat { + sample_rate, + sample_format: SampleFormat::F32, + flags: LinearPcmFlags::IS_NON_INTERLEAVED | LinearPcmFlags::IS_FLOAT, + channels: channel_count as _, + } +} + +struct DummyStreamProxy; + +impl ExtensionProvider for DummyStreamProxy { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl StreamProxy for DummyStreamProxy {} + +static STREAM_PROXY: DummyStreamProxy = DummyStreamProxy; + +impl Handle { + pub(crate) fn new( + device: &Device, + stream_config: StreamConfig, + callback: Callback, + ) -> Result { + let requested_type = stream_config.requested_device_type(); + if requested_type.is_duplex() { + return Err(Error::DuplexUnavailable); + } + + let unsupported = device.device_type() & !requested_type; + if !unsupported.is_empty() { + log::warn!( + "Cannot request {unsupported:?} for {device}, ignoring", + device = device.name() + ); + } + let unit = device.request.to_audio_unit()?; + if requested_type.is_input() { + Self::new_input(unit, stream_config, callback) + } else { + Self::new_output(unit, stream_config, callback) + } + } + + fn new_input( + mut audio_unit: AudioUnit, + stream_config: StreamConfig, + mut callback: Callback, + ) -> Result { + let asbd = + input_stream_format(stream_config.sample_rate, stream_config.input_channels).to_asbd(); + audio_unit.set_property( + kAudioUnitProperty_StreamFormat, + Scope::Output, + Element::Input, + Some(&asbd), + )?; + let frame_count: u32 = audio_unit.get_property( + kAudioDevicePropertyBufferFrameSize, + Scope::Global, + Element::Output, + )?; + let resolved_config = ResolvedStreamConfig { + sample_rate: asbd.mSampleRate, + input_channels: asbd.mChannelsPerFrame as _, + output_channels: 0, + max_frame_count: frame_count as _, + }; + let mut buffer = AudioBuffer::zeroed( + NonZeroUsize::new(frame_count as _).unwrap(), + asbd.mChannelsPerFrame as _, + ); + + let (tx, rx) = oneshot::channel::>(); + callback.prepare(CallbackContext { + stream_config: &resolved_config, + timestamp: Timestamp::new(asbd.mSampleRate), + stream_proxy: &STREAM_PROXY, + }); + let mut callback = Some(callback); + + audio_unit.set_input_callback(move |args: Args>| { + if let Ok(sender) = rx.try_recv() { + sender.send(callback.take().unwrap()).unwrap(); + return Err(()); + } + let num_frames = args.num_frames; + let channels = asbd.mChannelsPerFrame as usize; + let input_data = args.data.buffer; + + for frame_idx in 0..num_frames { + for ch in 0..channels { + let sample = input_data[frame_idx * channels + ch]; + buffer + .frame_mut(frame_idx) + .set(ch, (sample as f32) / (i16::MAX as f32)); + } + } + + let timestamp = Timestamp::from_count( + resolved_config.sample_rate, + args.time_stamp.mSampleTime as _, + ); + + let input = AudioInput { + buffer: buffer.as_ref(), + timestamp, + channel_flags: &[], + }; + + let mut dummy_buf = AudioBuffer::zeroed(NonZeroUsize::new(1).unwrap(), channels as _); + let mut dummy_output = AudioOutput { + buffer: dummy_buf.as_mut(), + timestamp: Timestamp::new(asbd.mSampleRate), + channel_flags: &[], + }; + + if let Some(callback) = &mut callback { + callback.process_audio( + CallbackContext { + stream_config: &resolved_config, + timestamp, + stream_proxy: &STREAM_PROXY, + }, + &input, + &mut dummy_output, + ); + } + Ok(()) + })?; + audio_unit.start()?; + Ok(Self { + audio_unit, + callback_retrieve: tx, + }) + } + + fn new_output( + mut audio_unit: AudioUnit, + stream_config: StreamConfig, + mut callback: Callback, + ) -> Result { + let asbd = output_stream_format(stream_config.sample_rate, stream_config.output_channels) + .to_asbd(); + audio_unit.set_property( + kAudioUnitProperty_StreamFormat, + Scope::Input, + Element::Output, + Some(&asbd), + )?; + let frame_size: u32 = audio_unit.get_property( + kAudioDevicePropertyBufferFrameSize, + Scope::Output, + Element::Output, + )?; + let resolved_config = ResolvedStreamConfig { + sample_rate: asbd.mSampleRate, + input_channels: 0, + output_channels: asbd.mChannelsPerFrame as _, + max_frame_count: frame_size as _, + }; + let mut buffer = AudioBuffer::zeroed( + NonZeroUsize::new(frame_size as _).unwrap(), + asbd.mChannelsPerFrame as _, + ); + + let (tx, rx) = oneshot::channel::>(); + callback.prepare(CallbackContext { + stream_config: &resolved_config, + timestamp: Timestamp::new(resolved_config.sample_rate), + stream_proxy: &STREAM_PROXY, + }); + let mut callback = Some(callback); + + audio_unit.set_render_callback(move |mut args: Args>| { + if let Ok(sender) = rx.try_recv() { + sender.send(callback.take().unwrap()).unwrap(); + return Err(()); + } + let timestamp = Timestamp::from_count( + resolved_config.sample_rate, + args.time_stamp.mSampleTime as _, + ); + + let dummy_buf = + AudioBuffer::zeroed(NonZeroUsize::new(1).unwrap(), asbd.mChannelsPerFrame as _); + let dummy_input = AudioInput { + buffer: dummy_buf.as_ref(), + timestamp: Timestamp::new(resolved_config.sample_rate), + channel_flags: &[], + }; + + let mut output = AudioOutput { + buffer: buffer.as_mut(), + timestamp, + channel_flags: &[], + }; + + if let Some(callback) = &mut callback { + callback.process_audio( + CallbackContext { + stream_config: &resolved_config, + timestamp, + stream_proxy: &STREAM_PROXY, + }, + &dummy_input, + &mut output, + ); + for (out_ch, in_ch) in args.data.channels_mut().zip(buffer.iter_channels()) { + out_ch.copy_from_slice(in_ch); + } + } + Ok(()) + })?; + audio_unit.start()?; + Ok(Self { + audio_unit, + callback_retrieve: tx, + }) + } +} diff --git a/crates/interflow-coreaudio/src/utils.rs b/crates/interflow-coreaudio/src/utils.rs new file mode 100644 index 0000000..7eaccae --- /dev/null +++ b/crates/interflow-coreaudio/src/utils.rs @@ -0,0 +1,41 @@ +use coreaudio_sys::{AudioDeviceID, AudioObjectGetPropertyData, AudioObjectPropertyAddress}; + +pub(crate) fn get_device_property( + device_id: AudioDeviceID, + address: AudioObjectPropertyAddress, +) -> Result { + let mut data = std::mem::MaybeUninit::::uninit(); + let mut size = size_of::() as u32; + let status = unsafe { + AudioObjectGetPropertyData( + device_id, + &address, + 0, + std::ptr::null(), + &mut size, + data.as_mut_ptr() as *mut _, + ) + }; + coreaudio::Error::from_os_status(status)?; + Ok(unsafe { data.assume_init() }) +} + +#[expect(unused)] +pub(crate) fn set_device_property( + device_id: AudioDeviceID, + address: AudioObjectPropertyAddress, + data: &T, +) -> Result<(), coreaudio::Error> { + let size = size_of::() as u32; + let status = unsafe { + coreaudio_sys::AudioObjectSetPropertyData( + device_id, + &address, + 0, + std::ptr::null(), + size, + data as *const T as *const _, + ) + }; + coreaudio::Error::from_os_status(status) +} diff --git a/crates/interflow-wasapi/Cargo.toml b/crates/interflow-wasapi/Cargo.toml new file mode 100644 index 0000000..c289fa2 --- /dev/null +++ b/crates/interflow-wasapi/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "interflow-wasapi" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true + +[features] +collect = ["interflow-core/collect", "dep:scattered-collect"] + +[dependencies] +bitflags.workspace = true +duplicate.workspace = true +interflow-core.workspace = true +scattered-collect = { workspace = true, optional = true } +thiserror.workspace = true +log.workspace = true +windows = { version = "0.62.2", features = [ + "Win32_Media_Audio", + "Win32_Foundation", + "Win32_Devices_Properties", + "Win32_Media_KernelStreaming", + "Win32_System_Com_StructuredStorage", + "Win32_System_Threading", + "Win32_Security", + "Win32_System_SystemServices", + "Win32_System_Variant", + "Win32_Media_Multimedia", + "Win32_UI_Shell_PropertiesSystem", +] } +zerocopy = { version = "0.8.39", features = ["std"] } diff --git a/crates/interflow-wasapi/src/device.rs b/crates/interflow-wasapi/src/device.rs new file mode 100644 index 0000000..fcdbc6b --- /dev/null +++ b/crates/interflow-wasapi/src/device.rs @@ -0,0 +1,205 @@ +use crate::util::MMDevice; +use crate::Error; +use interflow_core::device::StreamConfig; +use interflow_core::traits::{ExtensionProvider, Selector}; +use interflow_core::{device, stream, DeviceType}; +use std::borrow::Cow; +use std::ptr::NonNull; +use windows::Win32::Media::Audio; +use windows::Win32::Media::Audio::{IAudioClient, IAudioClient3}; +use windows::Win32::Media::Multimedia; + +#[derive(Debug, Clone)] +pub struct Device { + pub(crate) handle: MMDevice, + pub(crate) device_type: DeviceType, +} + +impl ExtensionProvider for Device { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl device::Device for Device { + type Error = Error; + type StreamHandle = crate::stream::Handle; + + fn name(&self) -> Cow<'_, str> { + Cow::Owned(self.handle.name()) + } + + fn device_type(&self) -> DeviceType { + self.device_type + } + + fn default_config(&self) -> Result { + self.get_mix_format_iac3() + .or_else(|_| self.get_mix_format()) + } + + fn is_config_supported(&self, config: &StreamConfig) -> bool { + self.check_format(config).unwrap_or(false) + } + + fn create_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + crate::stream::Handle::new(self, stream_config, callback) + } +} + +impl Device { + pub(crate) fn activate_audio_client(&self) -> Result { + self.handle.activate::() + } + + fn get_mix_format(&self) -> Result { + let client = self.activate_audio_client()?; + let mix_format = unsafe { client.GetMixFormat() }?; + let format = unsafe { mix_format.read_unaligned() }; + let channels = format.nChannels as usize; + let input_channels = if self.device_type.is_input() { + channels + } else { + 0 + }; + let output_channels = if self.device_type.is_output() { + channels + } else { + 0 + }; + Ok(StreamConfig { + sample_rate: format.nSamplesPerSec as _, + input_channels, + output_channels, + buffer_size_range: (None, None), + exclusive: false, + }) + } + + fn get_mix_format_iac3(&self) -> Result { + let client = self.handle.activate::()?; + let mut period_default = 0u32; + let mut period_min = 0u32; + let mut period_max = 0u32; + let format = unsafe { client.GetMixFormat() }?; + unsafe { + let mut _fundamental_period = 0u32; + client.GetSharedModeEnginePeriod( + format.cast_const(), + &mut period_default, + &mut _fundamental_period, + &mut period_min, + &mut period_max, + )?; + } + let format = unsafe { format.read_unaligned() }; + let channels = format.nChannels as usize; + let input_channels = if self.device_type.is_input() { + channels + } else { + 0 + }; + let output_channels = if self.device_type.is_output() { + channels + } else { + 0 + }; + Ok(StreamConfig { + sample_rate: format.nSamplesPerSec as _, + input_channels, + output_channels, + buffer_size_range: (Some(period_min as usize), Some(period_max as usize)), + exclusive: false, + }) + } + + fn check_format(&self, config: &StreamConfig) -> Result { + unsafe { + let audio_client = self.activate_audio_client()?; + let sharemode = if config.exclusive { + Audio::AUDCLNT_SHAREMODE_EXCLUSIVE + } else { + Audio::AUDCLNT_SHAREMODE_SHARED + }; + let format = Self::build_format(config); + if config.exclusive { + audio_client + .IsFormatSupported(sharemode, &format, None) + .ok()?; + return Ok(true); + } + let mut closest_ptr: *mut Audio::WAVEFORMATEX = std::ptr::null_mut(); + let result = audio_client + .IsFormatSupported(sharemode, &format, Some(&mut closest_ptr)); + let hr = result.0; + if hr == 0 { + return Ok(true); + } + if hr > 0 && !closest_ptr.is_null() { + let closest = + crate::util::CoTask::new(NonNull::new_unchecked(closest_ptr)); + let closest_format = closest.as_ptr().read_unaligned(); + return Ok(closest_format.nSamplesPerSec == config.sample_rate as u32 + && closest_format.nChannels as usize + == config.output_channels.max(config.input_channels)); + } + Ok(false) + } + } + + pub(crate) fn build_format(config: &StreamConfig) -> Audio::WAVEFORMATEX { + let channels = (config.input_channels.max(config.output_channels)) as u16; + let sample_rate = config.sample_rate as u32; + let sample_bytes = size_of::() as u16; + let avg_bytes_per_sec = channels as u32 * sample_rate * sample_bytes as u32; + let block_align = channels * sample_bytes; + Audio::WAVEFORMATEX { + wFormatTag: Multimedia::WAVE_FORMAT_IEEE_FLOAT as u16, + nChannels: channels, + nSamplesPerSec: sample_rate, + nAvgBytesPerSec: avg_bytes_per_sec, + nBlockAlign: block_align, + wBitsPerSample: 8 * sample_bytes, + cbSize: 0, + } + } +} + +pub(crate) struct DeviceList { + pub(crate) collection: Audio::IMMDeviceCollection, + pub(crate) total_count: u32, + pub(crate) next_item: u32, + pub(crate) device_type: DeviceType, +} + +unsafe impl Send for DeviceList {} +unsafe impl Sync for DeviceList {} + +impl Iterator for DeviceList { + type Item = Device; + + fn next(&mut self) -> Option { + if self.next_item >= self.total_count { + return None; + } + unsafe { + let device = self.collection.Item(self.next_item).ok()?; + self.next_item += 1; + Some(Device { + handle: MMDevice::new(device), + device_type: self.device_type, + }) + } + } + + fn size_hint(&self) -> (usize, Option) { + let rest = (self.total_count - self.next_item) as usize; + (rest, Some(rest)) + } +} + +impl ExactSizeIterator for DeviceList {} diff --git a/crates/interflow-wasapi/src/lib.rs b/crates/interflow-wasapi/src/lib.rs new file mode 100644 index 0000000..b176865 --- /dev/null +++ b/crates/interflow-wasapi/src/lib.rs @@ -0,0 +1,30 @@ +pub mod device; +pub mod platform; +pub mod stream; +mod util; + +pub mod prelude { + pub use crate::device::Device; + pub use crate::platform::Platform; + pub use crate::stream::Handle; +} + +use std::convert::Infallible; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{} (code {})", .0.message(), .0.code())] + BackendError(#[from] windows::core::Error), + #[error("Configuration not available")] + ConfigurationNotAvailable, + #[error("Win32 error: {0}")] + FoundationError(String), + #[error("Unsupported duplex stream requested")] + DuplexStreamRequested, +} + +impl From for Error { + fn from(_: Infallible) -> Self { + unreachable!() + } +} diff --git a/crates/interflow-wasapi/src/platform.rs b/crates/interflow-wasapi/src/platform.rs new file mode 100644 index 0000000..0a907a7 --- /dev/null +++ b/crates/interflow-wasapi/src/platform.rs @@ -0,0 +1,151 @@ +use crate::device::{Device, DeviceList}; +use crate::util::MMDevice; +use crate::Error; +use bitflags::bitflags_match; +#[cfg(feature = "collect")] +use interflow_core::collect; +use interflow_core::platform; +use interflow_core::traits::{ExtensionProvider, Selector}; +use interflow_core::DeviceType; +#[cfg(feature = "collect")] +use std::rc::Rc; +use std::sync::OnceLock; +use windows::Win32::Media::Audio; +use windows::Win32::Media::Audio::{EDataFlow, ERole}; +use windows::Win32::System::Com; +use interflow_core::proxies::PlatformProxy; + +pub struct Platform; + +impl ExtensionProvider for Platform { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + .register::(self) + } +} + +impl platform::Platform for Platform { + type Error = Error; + type Device = Device; + const NAME: &'static str = "WASAPI"; + + fn default_device(&self, device_type: DeviceType) -> Result { + let Some(device) = audio_device_enumerator().get_default_device(device_type)? else { + return Err(Error::ConfigurationNotAvailable); + }; + Ok(device) + } + + fn list_devices(&self) -> Result, Self::Error> { + audio_device_enumerator().get_device_list() + } +} + +pub trait DefaultByRole { + fn default_by_role(&self, flow: Audio::EDataFlow, role: Audio::ERole) -> Result; +} + +impl DefaultByRole for Platform { + fn default_by_role(&self, flow: Audio::EDataFlow, role: Audio::ERole) -> Result { + audio_device_enumerator().get_default_device_with_role(flow, role) + } +} + +fn audio_device_enumerator() -> &'static AudioDeviceEnumerator { + static ENUMERATOR: OnceLock = OnceLock::new(); + ENUMERATOR.get_or_init(|| { + let com = crate::util::com().unwrap(); + unsafe { + let enumerator = com + .create_instance::<_, Audio::IMMDeviceEnumerator>( + &Audio::MMDeviceEnumerator, + None, + Com::CLSCTX_ALL, + ) + .unwrap(); + AudioDeviceEnumerator(enumerator) + } + }) +} + +struct AudioDeviceEnumerator(Audio::IMMDeviceEnumerator); + +unsafe impl Send for AudioDeviceEnumerator {} +unsafe impl Sync for AudioDeviceEnumerator {} + +impl AudioDeviceEnumerator { + fn get_default_device(&self, device_type: DeviceType) -> Result, Error> { + let Some(flow) = bitflags_match!(device_type, { + DeviceType::INPUT => Some(Audio::eCapture), + DeviceType::OUTPUT => Some(Audio::eRender), + _ => None, + }) else { + return Ok(None); + }; + self.get_default_device_with_role(flow, Audio::eConsole) + .map(Some) + } + + fn get_default_device_with_role( + &self, + flow: Audio::EDataFlow, + role: Audio::ERole, + ) -> Result { + unsafe { + let device = self.0.GetDefaultAudioEndpoint(flow, role)?; + let device_type = match flow { + Audio::eRender => DeviceType::OUTPUT, + _ => DeviceType::INPUT, + }; + Ok(Device { + handle: MMDevice::new(device), + device_type: DeviceType::PHYSICAL | device_type, + }) + } + } + + fn get_device_list(&self) -> Result, Error> { + unsafe { + let output_collection = self + .0 + .EnumAudioEndpoints(Audio::eRender, Audio::DEVICE_STATE_ACTIVE)?; + let count = output_collection.GetCount()?; + let output_device_list = DeviceList { + collection: output_collection, + total_count: count, + next_item: 0, + device_type: DeviceType::OUTPUT, + }; + + let input_collection = self + .0 + .EnumAudioEndpoints(Audio::eCapture, Audio::DEVICE_STATE_ACTIVE)?; + let count = input_collection.GetCount()?; + let input_device_list = DeviceList { + collection: input_collection, + total_count: count, + next_item: 0, + device_type: DeviceType::INPUT, + }; + + Ok(output_device_list.chain(input_device_list)) + } + } +} + +pub trait DefaultForRole: PlatformProxy { + fn default_for_role(&self, flow: Audio::EDataFlow, role: Audio::ERole) -> Result; +} + +impl DefaultForRole for Platform { + fn default_for_role(&self, flow: EDataFlow, role: ERole) -> Result { + Ok(audio_device_enumerator().get_default_device_with_role(flow, role)?) + } +} + +#[cfg(feature = "collect")] +#[scattered_collect::scatter(collect::REGISTRAR)] +static WASAPI_PLATFORM_REGISTRATION: collect::Registration = collect::Registration { + constructor: || Some(Rc::new(Platform)), + priority: collect::DEFAULT_PLATFORM_PRIORITY, +}; diff --git a/crates/interflow-wasapi/src/stream.rs b/crates/interflow-wasapi/src/stream.rs new file mode 100644 index 0000000..7dd3967 --- /dev/null +++ b/crates/interflow-wasapi/src/stream.rs @@ -0,0 +1,366 @@ +use crate::device::Device; +use crate::Error; +use interflow_core::buffer::AudioBuffer; +use interflow_core::device::{ResolvedStreamConfig, StreamConfig}; +use interflow_core::stream; +use interflow_core::stream::{AudioInput, AudioOutput, CallbackContext, StreamProxy}; +use interflow_core::timing::Timestamp; +use interflow_core::traits::{ExtensionProvider, Selector}; +use std::num::NonZeroUsize; +use std::ptr; +use std::slice; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::JoinHandle; +use std::time::Duration; +use windows::Win32::Foundation::{CloseHandle, HANDLE, WAIT_FAILED}; +use windows::Win32::Media::Audio; +use windows::Win32::System::Threading; + +type EjectSignal = Arc; + +pub struct Handle { + join_handle: JoinHandle>, + eject_signal: EjectSignal, +} + +impl stream::StreamHandle for Handle { + type Error = Error; + + fn eject(self) -> Result { + self.eject_signal.store(true, Ordering::Relaxed); + self.join_handle.join().expect("Audio thread panicked") + } +} + +impl Handle { + pub(crate) fn new( + device: &Device, + stream_config: StreamConfig, + callback: Callback, + ) -> Result { + let requested = stream_config.requested_device_type(); + if requested.is_duplex() { + return Err(Error::DuplexStreamRequested); + } + + let eject_signal = EjectSignal::default(); + let device = device.clone(); + let thread_name = if requested.is_input() { + "interflow_wasapi_input_stream" + } else { + "interflow_wasapi_output_stream" + }; + + let join_handle = std::thread::Builder::new() + .name(thread_name.to_string()) + .spawn({ + let eject = eject_signal.clone(); + move || { + set_thread_priority(); + if requested.is_input() { + run_input(device, stream_config, callback, eject) + } else { + run_output(device, stream_config, callback, eject) + } + } + }) + .expect("Cannot spawn audio thread"); + + Ok(Self { + join_handle, + eject_signal, + }) + } +} + +fn run_output( + device: Device, + stream_config: StreamConfig, + mut callback: C, + eject_signal: EjectSignal, +) -> Result { + unsafe { + let audio_client = device.activate_audio_client()?; + + let format = Device::build_format(&stream_config); + let buffer_duration = stream_config + .buffer_size_range + .0 + .or(stream_config.buffer_size_range.1) + .map(|frames| buffer_size_to_duration(frames, stream_config.sample_rate as u32)) + .unwrap_or(0); + + audio_client.Initialize( + Audio::AUDCLNT_SHAREMODE_SHARED, + Audio::AUDCLNT_STREAMFLAGS_EVENTCALLBACK | Audio::AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, + buffer_duration, + 0, + &format, + None, + )?; + + let frame_count = audio_client.GetBufferSize()? as usize; + let resolved_config = ResolvedStreamConfig { + sample_rate: stream_config.sample_rate, + input_channels: 0, + output_channels: stream_config.output_channels, + max_frame_count: frame_count, + }; + + let event_handle = + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))?; + audio_client.SetEventHandle(event_handle)?; + + let render_client = audio_client.GetService::()?; + let audio_clock = audio_client.GetService::()?; + + let frame_size = NonZeroUsize::new(frame_count.max(1)).unwrap(); + let mut audio_output = AudioBuffer::zeroed(frame_size, stream_config.output_channels); + let audio_input = AudioBuffer::empty(frame_size); + + let context = CallbackContext { + stream_config: &resolved_config, + timestamp: Timestamp::new(resolved_config.sample_rate), + stream_proxy: &STREAM_PROXY, + }; + callback.prepare(context); + + audio_client.Start()?; + let clock_start = stream_instant(&audio_clock)?; + + loop { + if eject_signal.load(Ordering::Relaxed) { + break; + } + let result = Threading::WaitForSingleObject(event_handle, Threading::INFINITE); + if result == WAIT_FAILED { + let err = foundation_get_last_error(); + return Err(Error::FoundationError(format!( + "WaitForSingleObject failed: {:?}", + err + ))); + } + + let padding = audio_client.GetCurrentPadding()? as usize; + let frames_available = frame_count.saturating_sub(padding); + if frames_available == 0 { + continue; + } + + let timestamp = + output_timestamp(&audio_clock, clock_start, resolved_config.sample_rate)?; + + let dummy_input = AudioInput { + timestamp, + buffer: audio_input.slice(..0), + channel_flags: &[], + }; + + let mut output = AudioOutput { + timestamp, + buffer: audio_output.slice_mut(..frames_available), + channel_flags: &[], + }; + + callback.process_audio( + CallbackContext { + stream_config: &resolved_config, + timestamp, + stream_proxy: &STREAM_PROXY, + }, + &dummy_input, + &mut output, + ); + + let wasapi_buf = render_client.GetBuffer(frames_available as _)?; + let wasapi_slice = slice::from_raw_parts_mut( + wasapi_buf as *mut f32, + frames_available * stream_config.output_channels, + ); + for frame in 0..frames_available { + for ch in 0..stream_config.output_channels { + wasapi_slice[frame * stream_config.output_channels + ch] = + audio_output[ch][frame]; + } + } + render_client.ReleaseBuffer(frames_available as _, 0)?; + } + + audio_client.Stop()?; + CloseHandle(event_handle)?; + Ok(callback) + } +} + +fn run_input( + device: Device, + stream_config: StreamConfig, + mut callback: C, + eject_signal: EjectSignal, +) -> Result { + unsafe { + let audio_client = device.activate_audio_client()?; + + let format = Device::build_format(&stream_config); + let buffer_duration = stream_config + .buffer_size_range + .0 + .or(stream_config.buffer_size_range.1) + .map(|frames| buffer_size_to_duration(frames, stream_config.sample_rate as u32)) + .unwrap_or(0); + + audio_client.Initialize( + Audio::AUDCLNT_SHAREMODE_SHARED, + Audio::AUDCLNT_STREAMFLAGS_EVENTCALLBACK | Audio::AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, + buffer_duration, + 0, + &format, + None, + )?; + + let frame_count = audio_client.GetBufferSize()? as usize; + let resolved_config = ResolvedStreamConfig { + sample_rate: stream_config.sample_rate, + input_channels: stream_config.input_channels, + output_channels: 0, + max_frame_count: frame_count, + }; + + let event_handle = + Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))?; + audio_client.SetEventHandle(event_handle)?; + + let capture_client = audio_client.GetService::()?; + let audio_clock = audio_client.GetService::()?; + + let frame_size = NonZeroUsize::new(frame_count.max(1)).unwrap(); + let mut audio_input = AudioBuffer::zeroed(frame_size, stream_config.input_channels); + let mut audio_output = AudioBuffer::empty(frame_size); + + let context = CallbackContext { + stream_config: &resolved_config, + timestamp: Timestamp::new(resolved_config.sample_rate), + stream_proxy: &STREAM_PROXY, + }; + callback.prepare(context); + + audio_client.Start()?; + let clock_start = stream_instant(&audio_clock)?; + + loop { + if eject_signal.load(Ordering::Relaxed) { + break; + } + let result = Threading::WaitForSingleObject(event_handle, Threading::INFINITE); + if result == WAIT_FAILED { + let err = foundation_get_last_error(); + return Err(Error::FoundationError(format!( + "WaitForSingleObject failed: {:?}", + err + ))); + } + + let mut buf_ptr = ptr::null_mut(); + let mut frames_available = 0u32; + let mut flags = 0u32; + capture_client.GetBuffer( + &mut buf_ptr, + &mut frames_available, + &mut flags, + None, + None, + )?; + + if frames_available == 0 { + continue; + } + + let wasapi_slice = slice::from_raw_parts( + buf_ptr as *const f32, + frames_available as usize * stream_config.input_channels, + ); + audio_input.copy_from_interleaved(wasapi_slice); + capture_client.ReleaseBuffer(frames_available)?; + + let timestamp = + output_timestamp(&audio_clock, clock_start, resolved_config.sample_rate)?; + + let input = AudioInput { + timestamp, + buffer: audio_input.slice(..frames_available as usize), + channel_flags: &[], + }; + + let mut dummy_output = AudioOutput { + timestamp, + buffer: audio_output.slice_mut(..0), + channel_flags: &[], + }; + + callback.process_audio( + CallbackContext { + stream_config: &resolved_config, + timestamp, + stream_proxy: &STREAM_PROXY, + }, + &input, + &mut dummy_output, + ); + } + + audio_client.Stop()?; + CloseHandle(event_handle)?; + Ok(callback) + } +} + +fn stream_instant(audio_clock: &Audio::IAudioClock) -> Result { + let mut position: u64 = 0; + let mut qpc_position: u64 = 0; + unsafe { + audio_clock.GetPosition(&mut position, Some(&mut qpc_position))?; + } + let qpc_nanos = qpc_position * 100; + Ok(Duration::from_nanos(qpc_nanos)) +} + +fn output_timestamp( + audio_clock: &Audio::IAudioClock, + clock_start: Duration, + sample_rate: f64, +) -> Result { + let clock = stream_instant(audio_clock)?; + let diff = clock - clock_start; + Ok(Timestamp::from_duration(sample_rate, diff)) +} + +fn buffer_size_to_duration(buffer_size: usize, sample_rate: u32) -> i64 { + (buffer_size as i64 * 10_000_000) / sample_rate as i64 +} + +fn set_thread_priority() { + unsafe { + let thread_id = Threading::GetCurrentThreadId(); + let _ = Threading::SetThreadPriority( + HANDLE(thread_id as isize as _), + Threading::THREAD_PRIORITY_TIME_CRITICAL, + ); + } +} + +fn foundation_get_last_error() -> windows::core::Error { + unsafe { windows::Win32::Foundation::GetLastError().into() } +} + +struct WASAPIStreamProxy; + +impl ExtensionProvider for WASAPIStreamProxy { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl StreamProxy for WASAPIStreamProxy {} + +static STREAM_PROXY: WASAPIStreamProxy = WASAPIStreamProxy; diff --git a/crates/interflow-wasapi/src/util.rs b/crates/interflow-wasapi/src/util.rs new file mode 100644 index 0000000..84e32fd --- /dev/null +++ b/crates/interflow-wasapi/src/util.rs @@ -0,0 +1,151 @@ +use std::ffi::OsString; +use std::marker::PhantomData; +use std::ops; +use std::os::windows::ffi::OsStringExt; +use std::ptr::NonNull; +use std::sync::OnceLock; +use windows::core::Interface; +use windows::Win32::Devices::Properties; +use windows::Win32::Media::Audio; +use windows::Win32::System::Com::StructuredStorage; +use windows::Win32::System::Com::{ + self, CoInitializeEx, CoTaskMemFree, CoUninitialize, CLSCTX, COINIT_MULTITHREADED, STGM_READ, +}; +use windows::Win32::System::Variant::VT_LPWSTR; + +pub(crate) struct ComponentObjectModel(PhantomData<()>); + +unsafe impl Send for ComponentObjectModel {} +unsafe impl Sync for ComponentObjectModel {} + +impl ComponentObjectModel { + pub unsafe fn create_instance< + P1: windows::core::Param, + T: Interface, + >( + &self, + guid: *const windows::core::GUID, + param1: P1, + class_context: CLSCTX, + ) -> windows::core::Result { + Com::CoCreateInstance(guid, param1, class_context) + } +} + +impl Drop for ComponentObjectModel { + fn drop(&mut self) { + unsafe { CoUninitialize() }; + } +} + +pub fn com() -> windows::core::Result<&'static ComponentObjectModel> { + static VALUE: OnceLock = OnceLock::new(); + let Some(value) = VALUE.get() else { + unsafe { + CoInitializeEx(None, COINIT_MULTITHREADED).ok()?; + } + let _ = VALUE.set(ComponentObjectModel(PhantomData)); + return Ok(VALUE.get().unwrap()); + }; + Ok(value) +} + +#[derive(Debug, Clone)] +pub struct MMDevice(Audio::IMMDevice); + +unsafe impl Send for MMDevice {} + +impl MMDevice { + pub(crate) fn new(device: Audio::IMMDevice) -> Self { + Self(device) + } + + pub(crate) fn activate(&self) -> Result { + unsafe { + self.0 + .Activate::(Com::CLSCTX_ALL, None) + .map_err(crate::Error::BackendError) + } + } + + pub(crate) fn name(&self) -> String { + get_device_name(&self.0) + } +} + +fn get_device_name(device: &Audio::IMMDevice) -> String { + unsafe { + let property_store = device + .OpenPropertyStore(STGM_READ) + .expect("could not open property store"); + + let mut property_value = property_store + .GetValue(&Properties::DEVPKEY_Device_FriendlyName as *const _ as *const _) + .or(property_store.GetValue( + &Properties::DEVPKEY_DeviceInterface_FriendlyName as *const _ as *const _, + )) + .or(property_store + .GetValue(&Properties::DEVPKEY_Device_DeviceDesc as *const _ as *const _)) + .unwrap(); + + let prop_variant = &property_value.Anonymous.Anonymous; + assert_eq!(VT_LPWSTR, prop_variant.vt); + + let ptr_utf16 = *(&prop_variant.Anonymous as *const _ as *const *const u16); + + let mut len = 0; + while *ptr_utf16.offset(len) != 0 { + len += 1; + } + + let name_slice = std::slice::from_raw_parts(ptr_utf16, len as usize); + let name_os_string: OsString = OsStringExt::from_wide(name_slice); + let name = name_os_string + .into_string() + .unwrap_or_else(|os_string| os_string.to_string_lossy().into()); + + StructuredStorage::PropVariantClear(&mut property_value).unwrap(); + + name + } +} + +#[repr(transparent)] +pub(super) struct CoTask { + ptr: NonNull, +} + +impl ops::Deref for CoTask { + type Target = NonNull; + fn deref(&self) -> &Self::Target { + &self.ptr + } +} + +impl ops::DerefMut for CoTask { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ptr + } +} + +impl Drop for CoTask { + fn drop(&mut self) { + unsafe { + CoTaskMemFree(Some(self.ptr.as_ptr().cast())); + } + } +} + +impl CoTask { + pub(super) const unsafe fn new(ptr: NonNull) -> Self { + Self { ptr } + } + + pub(super) unsafe fn construct(func: impl FnOnce(*mut *mut T) -> bool) -> Option { + let mut ptr = std::ptr::null_mut(); + if !func(&mut ptr) { + return None; + } + NonNull::new(ptr).map(|ptr| Self { ptr }) + } +} diff --git a/examples/duplex.rs b/examples/duplex.rs deleted file mode 100644 index 4e007c1..0000000 --- a/examples/duplex.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::util::sine::SineWave; -use anyhow::Result; -use interflow::duplex::AudioDuplexCallback; -use interflow::prelude::*; - -mod util; - -fn main() -> Result<()> { - let input = default_input_device(); - let output = default_output_device(); - let mut input_config = input.default_input_config().unwrap(); - input_config.buffer_size_range = (Some(128), Some(512)); - let mut output_config = output.default_output_config().unwrap(); - output_config.buffer_size_range = (Some(128), Some(512)); - let duplex_config = DuplexStreamConfig::new(input_config, output_config); - let stream = - duplex::create_duplex_stream(input, output, RingMod::new(), duplex_config).unwrap(); - println!("Press Enter to stop"); - std::io::stdin().read_line(&mut String::new())?; - stream.eject().unwrap(); - Ok(()) -} - -struct RingMod { - carrier: SineWave, -} - -impl RingMod { - fn new() -> Self { - Self { - carrier: SineWave::new(440.0), - } - } -} - -impl AudioDuplexCallback for RingMod { - fn on_audio_data( - &mut self, - context: AudioCallbackContext, - input: AudioInput, - mut output: AudioOutput, - ) { - let sr = context.stream_config.samplerate as f32; - for i in 0..output.buffer.num_samples() { - let inp = input.buffer.get_frame(i)[0]; - let c = self.carrier.next_sample(sr); - output.buffer.set_mono(i, inp * c); - } - } -} diff --git a/examples/eject_stream_pipewire.rs b/examples/eject_stream_pipewire.rs deleted file mode 100644 index 3241bf2..0000000 --- a/examples/eject_stream_pipewire.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! This example showcases the problem with ejecting stopped pipewire streams described in -//! https://github.com/SolarLiner/interflow/issues/105 -//! -//! It would be best as an integration test, but it has nontrivial prerequisites on the environment: -//! - running PipeWire daemon -//! - at least one PipeWire audio output device -//! - the `pw-link` program installed (bundled with pipewire) - -use interflow::prelude::*; -use std::ops::Deref; -use std::thread; -use util::sine::SineWave; - -mod util; - -#[cfg(all(os_pipewire, feature = "pipewire"))] -fn main() -> Result<(), Box> { - use interflow::prelude::pipewire::driver::PipewireDriver; - use std::{process, time::Duration}; - - env_logger::init(); - - let driver = PipewireDriver::new()?; - - // Select the highest-priority output device. Use this rather than `driver.default_device()` - // because we need its real name for disconnecting it below. - let devices = driver.list_devices()?; - let mut device = devices - .into_iter() - .filter(|d| d.device_type().is_output()) - .max_by_key(device_session_priority) - .expect("No output PipeWire devices?"); - println!("Using device {}", device.name()); - - let config = device.default_output_config()?; - device.with_stream_name("Interflow eject test 1"); - let stream_1 = device.create_output_stream(config, SineWave::new(440.0))?; - - println!("Playing sine wave for 1 second, then ejecting"); - thread::sleep(Duration::from_secs(1)); - let callback = stream_1.eject().unwrap(); - - println!("Playing sine wave for another second in a new stream but old callback"); - let stream_2 = device.create_output_stream(config, callback)?; - thread::sleep(Duration::from_secs(1)); - - // Disconnect our node from the device node. Call external program, doing this programmatically - // using pipewire-rs would be much more involved. - let mut command = process::Command::new("pw-link"); - command - .arg("--disconnect") - .arg("eject_stream_pipewire") - .arg(device.name().deref()); - println!("Disconnecting playback pipewire node from its device using {command:?}"); - let status = command.status()?; - assert!(status.success()); - - println!("Ejecting the callback from the new stream"); - // The hang occurred right in this call - stream_2.eject()?; - - println!("Exiting cleanly"); - Ok(()) -} - -#[cfg(all(os_pipewire, feature = "pipewire"))] -fn device_session_priority(device: &pipewire::device::PipewireDevice) -> Option { - let properties = device - .properties() - .expect("Cannot get pipewire device properties")?; - - let priority_property = properties.get("priority.session")?; - let priority = priority_property - .parse() - .expect("Cannot parse priority.session as i32"); - Some(priority) -} diff --git a/examples/enumerate.rs b/examples/enumerate.rs new file mode 100644 index 0000000..2d1ea71 --- /dev/null +++ b/examples/enumerate.rs @@ -0,0 +1,29 @@ +use interflow::core::proxies::PlatformProxy; +use interflow::prelude::*; + +pub fn enumerate_devices(platform: &dyn PlatformProxy) -> anyhow::Result<()> { + eprintln!("Driver name : {}", platform.name()); + eprintln!("Default device"); + for (s, device_type) in [("Input", DeviceType::INPUT), ("Output", DeviceType::OUTPUT)] { + let device_type = device_type | DeviceType::PHYSICAL; + eprint!("\t{s}:\t"); + let Ok(device) = platform.default_device(device_type) else { + println!("no default device"); + continue; + }; + println!("{}", device.name()); + } + + eprintln!("\nAll devices"); + for device in platform.list_devices()? { + eprintln!("\t{} ({:?})", device.name(), device.device_type()); + } + Ok(()) +} + +fn main() -> anyhow::Result<()> { + env_logger::init(); + let platform = default_platform(); + enumerate_devices(&*platform)?; + Ok(()) +} diff --git a/examples/enumerate_alsa.rs b/examples/enumerate_alsa.rs deleted file mode 100644 index 2c38560..0000000 --- a/examples/enumerate_alsa.rs +++ /dev/null @@ -1,16 +0,0 @@ -mod util; - -#[cfg(os_alsa)] -fn main() -> Result<(), Box> { - use crate::util::enumerate::enumerate_devices; - use interflow::backends::alsa::AlsaDriver; - - env_logger::init(); - - enumerate_devices(AlsaDriver) -} - -#[cfg(not(os_alsa))] -fn main() { - println!("ALSA driver is not available on this platform"); -} diff --git a/examples/enumerate_coreaudio.rs b/examples/enumerate_coreaudio.rs deleted file mode 100644 index 030883d..0000000 --- a/examples/enumerate_coreaudio.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod util; - -#[cfg(os_coreaudio)] -fn main() -> Result<(), Box> { - use crate::util::enumerate::enumerate_devices; - use interflow::backends::coreaudio::CoreAudioDriver; - - enumerate_devices(CoreAudioDriver) -} - -#[cfg(not(os_coreaudio))] -fn main() { - println!("CoreAudio is not available on this platform"); -} diff --git a/examples/enumerate_pipewire.rs b/examples/enumerate_pipewire.rs deleted file mode 100644 index 6d2e6d3..0000000 --- a/examples/enumerate_pipewire.rs +++ /dev/null @@ -1,40 +0,0 @@ -use interflow::{prelude::pipewire::driver::PipewireDriver, AudioDevice, AudioDriver}; - -mod util; - -type Result = std::result::Result<(), Box>; - -#[cfg(all(os_pipewire, feature = "pipewire"))] -fn main() -> Result { - use crate::util::enumerate::enumerate_devices; - use interflow::backends::pipewire::driver::PipewireDriver; - env_logger::init(); - let driver = PipewireDriver::new()?; - enumerate_properties(&driver)?; - enumerate_devices(driver)?; - Ok(()) -} - -fn enumerate_properties(driver: &PipewireDriver) -> Result { - eprintln!("Properties:"); - - for device in driver.list_devices()? { - eprintln!("\t{}", device.name()); - - let Some(properties) = device.properties()? else { - eprintln!("\tNo properties found"); - continue; - }; - - for (key, value) in properties.dict().iter() { - eprintln!("\t\t{key}: {value}") - } - } - - Ok(()) -} - -#[cfg(not(all(os_pipewire, feature = "pipewire")))] -fn main() { - println!("Pipewire feature is not enabled"); -} diff --git a/examples/enumerate_wasapi.rs b/examples/enumerate_wasapi.rs deleted file mode 100644 index 05c658c..0000000 --- a/examples/enumerate_wasapi.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod util; - -#[cfg(os_wasapi)] -fn main() -> Result<(), Box> { - use crate::util::enumerate::enumerate_devices; - use interflow::backends::wasapi::WasapiDriver; - enumerate_devices(WasapiDriver) -} - -#[cfg(not(os_wasapi))] -fn main() { - println!("WASAPI driver is not available on this platform"); -} diff --git a/examples/ext-provider-lookup.rs b/examples/ext-provider-lookup.rs new file mode 100644 index 0000000..bdabc96 --- /dev/null +++ b/examples/ext-provider-lookup.rs @@ -0,0 +1,47 @@ +//! An example of using the [`ExtensionProvider`] trait through [`ExtensionProviderExt::lookup`] to dynamically +//! lookup types and traits registered by the backend types. +use interflow::prelude::*; + +fn main() -> anyhow::Result<()> { + let device = default_device(DeviceType::OUTPUT)?; + + println!("Selected device name: {:?}", device.name()); + + // Assuming the above device originates from platform-independent code, we can query for the concrete backend + // type. + #[cfg(any(target_os = "macos", target_os = "ios"))] + if let Some(device) = device.lookup::() { + let audio_unit = device.get_audio_unit()?; + let stream_format = audio_unit.output_stream_format()?; + println!( + "CoreAudio output stream format for {:?}: {stream_format:#?}", + device.name() + ); + } + + // There are traits that can be implemented by several backends, they can also be queried for and used if they + // are registered by the backend type. + if let Some(ext) = device.lookup::() { + println!("Device is connected: {}", ext.connected()); + } + + // A concrete example is listing configurations: not all platforms support this feature, and instead of + // "(un)graceful degradation", we instead only implement and register this trait when it makes sense. + if let Some(enumerator) = device.lookup::() { + for config in enumerator.enumerate_configurations() { + println!("\tConfiguration: {config:?}"); + } + } else { + println!("Device does not support listing configurations"); + } + + // Another useful extension allows iterating over named channels of a device. + if let Some(enumerator) = device.lookup::() { + for channel in enumerator.enumerate_channels() { + println!("\tChannel {:3}: {}", channel.index, channel.name); + } + } else { + println!("Device does not have named channels"); + } + Ok(()) +} diff --git a/examples/input.rs b/examples/input.rs index d1a72b7..e9287ec 100644 --- a/examples/input.rs +++ b/examples/input.rs @@ -1,43 +1,12 @@ -use crate::util::meter::PeakMeter; -use crate::util::AtomicF32; -use anyhow::Result; +use crate::util::noop::Noop; use interflow::prelude::*; -use std::sync::Arc; mod util; -fn main() -> Result<()> { - env_logger::init(); - - let device = default_input_device(); - let value = Arc::new(AtomicF32::new(0.)); - let stream = device - .default_input_stream(RmsMeter::new(value.clone())) - .unwrap(); - util::display_peakmeter(value)?; - stream.eject().unwrap(); +fn main() -> anyhow::Result<()> { + let (callback, display) = util::prepare_display(Noop); + let handle = default_stream(DeviceType::INPUT, callback)?; + display()?; + let _ = handle.eject()?; Ok(()) } - -struct RmsMeter { - value: Arc, - meter: Option, -} - -impl RmsMeter { - fn new(value: Arc) -> Self { - Self { meter: None, value } - } -} - -impl AudioInputCallback for RmsMeter { - fn on_input_data(&mut self, context: AudioCallbackContext, input: AudioInput) { - let meter = self - .meter - .get_or_insert_with(|| PeakMeter::new(context.stream_config.samplerate as f32, 15.0)); - meter.set_samplerate(context.stream_config.samplerate as f32); - meter.process_buffer(input.buffer.as_ref()); - self.value - .store(meter.value(), std::sync::atomic::Ordering::Relaxed); - } -} diff --git a/examples/list-platforms.rs b/examples/list-platforms.rs new file mode 100644 index 0000000..9371041 --- /dev/null +++ b/examples/list-platforms.rs @@ -0,0 +1,18 @@ +use interflow::prelude::*; +use interflow_core::collect::get_registry; + +fn main() -> anyhow::Result<()> { + env_logger::init(); + + for ctor in get_registry() { + let Some(platform) = ctor() else { continue }; + println!("Platform: {}", platform.name()); + if let Ok(device) = platform.default_device(DeviceType::INPUT) { + println!("\tDefault input device: {}", device.name()); + } + if let Ok(device) = platform.default_device(DeviceType::OUTPUT) { + println!("\tDefault output device: {}", device.name()); + } + } + Ok(()) +} \ No newline at end of file diff --git a/examples/loopback.rs b/examples/loopback.rs deleted file mode 100644 index db72372..0000000 --- a/examples/loopback.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::util::meter::PeakMeter; -use crate::util::AtomicF32; -use anyhow::Result; -use interflow::prelude::*; -use std::sync::Arc; -mod util; - -fn main() -> Result<()> { - env_logger::init(); - - let input = default_input_device(); - let output = default_output_device(); - let mut input_config = input.default_input_config().unwrap(); - input_config.buffer_size_range = (Some(128), Some(512)); - let mut output_config = output.default_output_config().unwrap(); - output_config.buffer_size_range = (Some(128), Some(512)); - input_config.channels = 0b01; - output_config.channels = 0b11; - let value = Arc::new(AtomicF32::new(0.)); - let config = DuplexStreamConfig::new(input_config, output_config); - let stream = - create_duplex_stream(input, output, Loopback::new(44100., value.clone()), config).unwrap(); - util::display_peakmeter(value)?; - stream.eject().unwrap(); - Ok(()) -} - -struct Loopback { - meter: PeakMeter, - value: Arc, -} - -impl Loopback { - fn new(samplerate: f32, value: Arc) -> Self { - Self { - meter: PeakMeter::new(samplerate, 15.0), - value, - } - } -} - -impl AudioDuplexCallback for Loopback { - fn on_audio_data( - &mut self, - context: AudioCallbackContext, - input: AudioInput, - mut output: AudioOutput, - ) { - self.meter - .set_samplerate(context.stream_config.samplerate as f32); - let rms = self.meter.process_buffer(input.buffer.as_ref()); - self.value.store(rms, std::sync::atomic::Ordering::Relaxed); - output.buffer.as_interleaved_mut().fill(0.0); - output.buffer.mix(input.buffer.as_ref(), 1.0); - } -} diff --git a/examples/set_buffer_size.rs b/examples/set_buffer_size.rs deleted file mode 100644 index 7e86ecd..0000000 --- a/examples/set_buffer_size.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! This example demonstrates how to request a specific buffer size from the CoreAudio backend. -//! Probably only works on macOS. - -mod util; - -#[cfg(os_coreaudio)] -fn main() -> anyhow::Result<()> { - use interflow::backends::coreaudio::CoreAudioDriver; - use interflow::channel_map::{ChannelMap32, CreateBitset}; - use interflow::prelude::*; - use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }; - use util::sine::SineWave; - - struct MyCallback { - first_callback: Arc, - sine_wave: SineWave, - } - - impl AudioOutputCallback for MyCallback { - fn on_output_data(&mut self, context: AudioCallbackContext, mut output: AudioOutput) { - if self.first_callback.swap(false, Ordering::SeqCst) { - println!( - "Actual buffer size granted by OS: {}", - output.buffer.num_samples() - ); - } - - for mut frame in output.buffer.as_interleaved_mut().rows_mut() { - let sample = self - .sine_wave - .next_sample(context.stream_config.samplerate as f32); - for channel_sample in &mut frame { - *channel_sample = sample; - } - } - } - } - - env_logger::init(); - - let driver = CoreAudioDriver; - let device = driver - .default_device(DeviceType::OUTPUT) - .expect("Failed to query for default output device") - .expect("No default output device found on this system"); - - println!("Using device: {}", device.name()); - - if let Ok((min, max)) = device.buffer_size_range() { - println!( - "Supported buffer size range: min={}, max={}", - min.map_or_else(|| "N/A".to_string(), |v| v.to_string()), - max.map_or_else(|| "N/A".to_string(), |v| v.to_string()) - ); - } - - let requested_buffer_size = 256; - println!("Requesting buffer size: {}", requested_buffer_size); - - let stream_config = StreamConfig { - samplerate: 48000.0, - channels: ChannelMap32::from_indices([0, 1]), - buffer_size_range: (Some(requested_buffer_size), Some(requested_buffer_size)), - exclusive: false, - }; - - let callback = MyCallback { - first_callback: Arc::new(AtomicBool::new(true)), - sine_wave: SineWave::new(440.0), - }; - - let stream = device.create_output_stream(stream_config, callback)?; - - println!("Playing sine wave... Press enter to stop."); - std::io::stdin().read_line(&mut String::new())?; - - stream.eject()?; - Ok(()) -} - -#[cfg(not(os_coreaudio))] -fn main() { - println!("This example is only available on platforms that support CoreAudio (e.g. macOS)."); -} diff --git a/examples/sine-wave.rs b/examples/sine-wave.rs new file mode 100644 index 0000000..293dadb --- /dev/null +++ b/examples/sine-wave.rs @@ -0,0 +1,16 @@ +use anyhow::Result; +use interflow::prelude::*; +use util::sine::SineWave; + +mod util; + +fn main() -> Result<()> { + env_logger::init(); + let (callback, display) = util::prepare_display(SineWave::new(440.0)); + let stream = default_stream(DeviceType::OUTPUT | DeviceType::PHYSICAL, callback)?; + + display()?; + + let _ = stream.eject()?; + Ok(()) +} diff --git a/examples/sine_wave.rs b/examples/sine_wave.rs deleted file mode 100644 index 6a0b14e..0000000 --- a/examples/sine_wave.rs +++ /dev/null @@ -1,17 +0,0 @@ -use anyhow::Result; -use interflow::prelude::*; -use util::sine::SineWave; - -mod util; - -fn main() -> Result<()> { - env_logger::init(); - - let device = default_output_device(); - println!("Using device {}", device.name()); - let stream = device.default_output_stream(SineWave::new(440.0)).unwrap(); - println!("Press Enter to stop"); - std::io::stdin().read_line(&mut String::new())?; - stream.eject().unwrap(); - Ok(()) -} diff --git a/examples/sine_wave_pipewire.rs b/examples/sine_wave_pipewire.rs deleted file mode 100644 index 550aa17..0000000 --- a/examples/sine_wave_pipewire.rs +++ /dev/null @@ -1,26 +0,0 @@ -use interflow::prelude::*; -use util::sine::SineWave; - -mod util; - -#[cfg(all(os_pipewire, feature = "pipewire"))] -fn main() -> Result<(), Box> { - use interflow::prelude::pipewire::driver::PipewireDriver; - - env_logger::init(); - - let driver = PipewireDriver::new()?; - let mut device = driver.default_device(DeviceType::OUTPUT)?.unwrap(); - println!("Using device {}", device.name()); - - let config = device.default_output_config()?; - device.with_stream_name("Interflow sine wave"); - let properties = [("node.custom-property".into(), "interflow".into())].into(); - device.with_stream_properties(properties); - let stream = device.create_output_stream(config, SineWave::new(440.0))?; - - println!("Press Enter to stop"); - std::io::stdin().read_line(&mut String::new())?; - stream.eject().unwrap(); - Ok(()) -} diff --git a/examples/specific-device-input.rs b/examples/specific-device-input.rs new file mode 100644 index 0000000..a37d90b --- /dev/null +++ b/examples/specific-device-input.rs @@ -0,0 +1,34 @@ +use crate::util::noop::Noop; +use anyhow::Context; +use dialoguer::console::Term; +use dialoguer::FuzzySelect; +use interflow::prelude::*; + +mod util; + +fn main() -> anyhow::Result<()> { + env_logger::init(); + + let term = Term::stdout(); + let platform = default_platform(); + let devices = platform + .list_devices_matching(DeviceType::INPUT) + .context("Cannot list devices")?; + let Some(index) = FuzzySelect::new() + .default(0) + .items(devices.iter().map(|dev| dev.name())) + .interact_on_opt(&term) + .context("Cannot display list")? + else { + return Ok(()); + }; + + let (callback, display) = util::prepare_display(Noop); + + let handle = devices[index] + .default_stream(DeviceType::INPUT, callback) + .context("Cannot create stream")?; + display()?; + let _ = handle.eject()?; + Ok(()) +} diff --git a/examples/specific-device.rs b/examples/specific-device.rs new file mode 100644 index 0000000..01e6969 --- /dev/null +++ b/examples/specific-device.rs @@ -0,0 +1,34 @@ +use crate::util::sine::SineWave; +use anyhow::Context; +use dialoguer::console::Term; +use dialoguer::FuzzySelect; +use interflow::prelude::*; + +mod util; + +fn main() -> anyhow::Result<()> { + env_logger::init(); + + let term = Term::stdout(); + let platform = default_platform(); + let devices = platform + .list_devices_matching(DeviceType::OUTPUT) + .context("Cannot list devices")?; + let Some(index) = FuzzySelect::new() + .default(0) + .items(devices.iter().map(|dev| dev.name())) + .interact_on_opt(&term) + .context("Cannot display list")? + else { + return Ok(()); + }; + + let (callback, display) = util::prepare_display(SineWave::new(440.0)); + + let handle = devices[index] + .default_stream(DeviceType::OUTPUT, callback) + .context("Cannot create stream")?; + display()?; + let _ = handle.eject()?; + Ok(()) +} diff --git a/examples/util/enumerate.rs b/examples/util/enumerate.rs deleted file mode 100644 index c01032f..0000000 --- a/examples/util/enumerate.rs +++ /dev/null @@ -1,26 +0,0 @@ -use interflow::{AudioDevice, AudioDriver, DeviceType}; -use std::error::Error; - -pub fn enumerate_devices(driver: Driver) -> Result<(), Box> -where - ::Error: 'static, -{ - eprintln!("Driver name : {}", Driver::DISPLAY_NAME); - eprintln!("Driver version: {}", driver.version()?); - eprintln!("Default device"); - for (s, device_type) in [("Input", DeviceType::INPUT), ("Output", DeviceType::OUTPUT)] { - let device_type = device_type | DeviceType::PHYSICAL; - eprint!("\t{s}:\t"); - if let Some(device) = driver.default_device(device_type)? { - eprintln!("{}", device.name()); - } else { - eprintln!("None"); - } - } - - eprintln!("All devices"); - for device in driver.list_devices()? { - eprintln!("\t{} ({:?})", device.name(), device.device_type()); - } - Ok(()) -} diff --git a/examples/util/meter.rs b/examples/util/meter.rs index 36acee6..3b332fd 100644 --- a/examples/util/meter.rs +++ b/examples/util/meter.rs @@ -1,21 +1,33 @@ -use interflow::audio_buffer::AudioRef; +use crate::util::AtomicF32; +use interflow::core::buffer::AudioRef; +use interflow_core::prelude::{AtomicTimestamp, AudioInput, AudioOutput}; +use interflow_core::stream; +use interflow_core::stream::CallbackContext; +use std::sync::atomic::Ordering; +use std::sync::Arc; -#[derive(Debug, Copy, Clone)] +#[derive(Clone)] pub struct PeakMeter { + output: Arc, last_out: f32, decay: f32, dt: f32, } impl PeakMeter { - pub fn new(samplerate: f32, decay: f32) -> Self { + pub fn new(decay: f32) -> Self { Self { + output: Arc::new(AtomicF32::new(0.)), last_out: 0., - decay, - dt: 1. / samplerate, + decay: decay.max(1e-6), + dt: 0.0, } } + pub fn output(&self) -> Arc { + self.output.clone() + } + pub fn samplerate(&self) -> f32 { 1. / self.dt } @@ -32,23 +44,76 @@ impl PeakMeter { self.decay = decay; } - pub fn value(&self) -> f32 { - self.last_out - } pub fn process(&mut self, sample: f32) -> f32 { let k = f32::exp(-self.decay * self.dt); self.last_out = (k * sample).max(self.last_out); + self.output.store(self.last_out, Ordering::Relaxed); self.last_out } pub fn process_buffer(&mut self, buffer: AudioRef) -> f32 { - let buffer_duration = buffer.num_samples() as f32 * self.dt; - let peak_lin = buffer - .channels() - .flat_map(|ch| ch.iter().copied().max_by(f32::total_cmp)) + let buffer_duration = buffer.frames() as f32 * self.dt; + let peak_lin = (0..buffer.channels()) + .flat_map(|ch| buffer[ch].iter().copied().max_by(f32::total_cmp)) .max_by(f32::total_cmp) .unwrap_or(0.); - self.last_out = peak_lin.max(self.last_out * f32::exp(-self.decay * buffer_duration)); + self.last_out = peak_lin.max(self.last_out * f32::exp(-buffer_duration / self.decay)); + self.output.store(self.last_out, Ordering::Relaxed); self.last_out } } + +#[derive(Clone)] +pub struct Metered { + pub inner: C, + input: PeakMeter, + output: PeakMeter, + timestamp: Arc, +} + +impl Metered { + pub fn new(inner: C, meter_decay: f32) -> Self { + Self { + inner, + input: PeakMeter::new(meter_decay), + output: PeakMeter::new(meter_decay), + timestamp: Arc::new(AtomicTimestamp::zeroed()), + } + } + + pub fn shared(&self) -> MeteredShared { + MeteredShared { + input: self.input.output.clone(), + output: self.output.output.clone(), + timestamp: self.timestamp.clone(), + } + } +} + +impl stream::Callback for Metered { + fn prepare(&mut self, context: CallbackContext) { + self.input + .set_samplerate(context.stream_config.sample_rate as _); + self.output + .set_samplerate(context.stream_config.sample_rate as _); + self.inner.prepare(context); + } + + fn process_audio( + &mut self, + context: CallbackContext, + input: &AudioInput, + output: &mut AudioOutput, + ) { + self.timestamp.update(context.timestamp); + self.input.process_buffer(input.buffer); + self.inner.process_audio(context, input, output); + self.output.process_buffer(output.buffer.as_ref()); + } +} + +pub struct MeteredShared { + pub input: Arc, + pub output: Arc, + pub timestamp: Arc, +} diff --git a/examples/util/mod.rs b/examples/util/mod.rs index 531feb7..f48e253 100644 --- a/examples/util/mod.rs +++ b/examples/util/mod.rs @@ -4,13 +4,17 @@ // Unfortunate consequence is that we lose the genuine unused warnings. #![allow(unused)] +use crate::util::meter::Metered; +use dialoguer::console::{Style, Term}; use indicatif::{ProgressBar, ProgressStyle}; +use interflow_core::stream; +use std::fmt::Write; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; -use std::thread; +use std::{iter, thread}; -pub mod enumerate; pub mod meter; +pub mod noop; pub mod sine; #[derive(Debug)] @@ -69,3 +73,66 @@ pub fn normalize(min: f32, max: f32, value: f32) -> f32 { let range = max - min; (value - min) / range } + +pub fn prepare_display( + callback: C, +) -> (meter::Metered, impl Fn() -> anyhow::Result<()>) { + let meter = Metered::new(callback, 100e-3); + let shared = meter.shared(); + let meter_display = make_meter_display(-60.0, 6.0, 10); + let display = move || { + let term = Term::stdout(); + loop { + let elapsed = shared.timestamp.as_timestamp().as_seconds(); + + let inp = shared.input.load(Ordering::Relaxed); + let inp = 20.0 * inp.log10(); + let inp_display = meter_display(inp); + + let out = shared.output.load(Ordering::Relaxed); + let out = 20.0 * out.log10(); + let out_display = meter_display(out); + + term.write_str(&format!( + "Elapsed: {elapsed:3.2} s\tInput: {inp_display} {inp:2.1} dB\tOutput: {out_display} {out:2.1} dB" + ))?; + std::thread::sleep(std::time::Duration::from_millis(50)); + term.clear_line()?; + } + }; + (meter, display) +} + +fn make_meter_display(min: f32, max: f32, initial_size: usize) -> impl Fn(f32) -> String { + const WIDTH: usize = 2; + let size = WIDTH * initial_size; + let fsize = size as f32; + let position = + move |v: f32| f32::round(fsize * normalize(min, max, v).clamp(0.0, 1.0)) as usize; + let fullscale_pos = position(0.0); + let style_below = Style::new().green(); + let style_above = Style::new().red(); + let style_background = Style::new().black().bright(); + + move |v: f32| { + let pos = position(v); + let mut s = String::new(); + for i in 0..initial_size { + let start = WIDTH * i; + let end = start + WIDTH; + let style = if end < fullscale_pos { + &style_below + } else { + &style_above + }; + if end < pos { + write!(s, "{}", style.apply_to('⣿')); + } else if start + 1 == pos { + write!(s, "{}", style.apply_to('⡇')); + } else { + write!(s, "{}", style_background.apply_to(' ')); + } + } + s + } +} diff --git a/examples/util/noop.rs b/examples/util/noop.rs new file mode 100644 index 0000000..61f5d4d --- /dev/null +++ b/examples/util/noop.rs @@ -0,0 +1,16 @@ +use interflow::prelude::*; +use interflow_core::stream::CallbackContext; + +pub struct Noop; + +impl stream::Callback for Noop { + fn prepare(&mut self, context: CallbackContext) {} + + fn process_audio( + &mut self, + context: CallbackContext, + input: &AudioInput, + output: &mut AudioOutput, + ) { + } +} diff --git a/examples/util/sine.rs b/examples/util/sine.rs index f780329..0540fdc 100644 --- a/examples/util/sine.rs +++ b/examples/util/sine.rs @@ -1,20 +1,25 @@ -use interflow::{AudioCallbackContext, AudioOutput, AudioOutputCallback}; +use interflow_core::stream::{AudioInput, AudioOutput, Callback, CallbackContext}; use std::f32::consts::TAU; pub struct SineWave { pub frequency: f32, pub phase: f32, + step_frequency_scaling: f32, } -impl AudioOutputCallback for SineWave { - fn on_output_data(&mut self, context: AudioCallbackContext, mut output: AudioOutput) { - eprintln!( - "Callback called, timestamp: {:2.3} s", - context.timestamp.as_seconds() - ); +impl Callback for SineWave { + fn prepare(&mut self, context: CallbackContext) { + self.step_frequency_scaling = context.stream_config.sample_rate.recip() as f32; + } + fn process_audio( + &mut self, + context: CallbackContext, + input: &AudioInput, + output: &mut AudioOutput, + ) { let sr = context.timestamp.samplerate as f32; - for i in 0..output.buffer.num_samples() { - output.buffer.set_mono(i, self.next_sample(sr)); + for i in 0..output.buffer.frames() { + output.buffer.set_mono(i, self.next_sample()); } // Reduce amplitude to not blow up speakers and ears output.buffer.change_amplitude(0.125); @@ -26,11 +31,12 @@ impl SineWave { Self { frequency, phase: 0.0, + step_frequency_scaling: 0.0, } } - pub fn next_sample(&mut self, samplerate: f32) -> f32 { - let step = samplerate.recip() * self.frequency; + pub fn next_sample(&mut self) -> f32 { + let step = self.step_frequency_scaling * self.frequency; let y = (TAU * self.phase).sin(); self.phase += step; if self.phase > 1. { diff --git a/src/audio_buffer.rs b/src/audio_buffer.rs deleted file mode 100644 index 3d4031b..0000000 --- a/src/audio_buffer.rs +++ /dev/null @@ -1,699 +0,0 @@ -//! Audio buffer types and traits for audio data manipulation. -//! -//! This module provides different types of audio buffers optimized for various use cases: -//! -//! - [`AudioBuffer`]: Owned buffer type for standard audio processing -//! - [`AudioRef`]: Immutable reference buffer for reading audio data -//! - [`AudioMut`]: Mutable reference buffer for modifying audio data -//! - [`AudioShared`]: Arc-backed shared buffer for multithreaded access -//! - [`AudioCow`]: Copy-on-write buffer (avoid in audio callbacks) -//! -//! The buffers support both interleaved and non-interleaved data formats and provide -//! convenient methods for: -//! -//! - Accessing individual channels and frames -//! - Slicing and subsetting audio data -//! - Computing audio metrics like RMS -//! - Mixing and amplitude adjustments -//! - Converting between different sample formats -//! -//! The buffers are built on top of ndarray for efficient multidimensional array operations. - -use ndarray::{ - s, Array0, ArrayBase, ArrayView1, ArrayView2, ArrayViewMut1, ArrayViewMut2, AsArray, CowRepr, - Data, DataMut, DataOwned, Ix1, Ix2, OwnedArcRepr, OwnedRepr, RawData, RawDataClone, ViewRepr, -}; -use std::collections::Bound; -use std::fmt; -use std::fmt::Formatter; -use std::ops::{AddAssign, RangeBounds}; - -/// Owned audio buffer type. -pub type AudioBuffer = AudioBufferBase>; -/// Immutably referenced audio buffer type. -pub type AudioRef<'a, T> = AudioBufferBase>; -/// Mutably referenced audio buffer type. -pub type AudioMut<'a, T> = AudioBufferBase>; -/// Arc-backed shared audio buffer type. -pub type AudioShared = AudioBufferBase>; -/// Copy-on-write audio buffer type. Should not be used within audio callbacks, as the copy will -/// introduce allocations. -pub type AudioCow<'a, T> = AudioBufferBase>; - -type Storage = ArrayBase; - -/// Audio buffer type, which backs all audio data interfacing with user code. -/// -/// This type is made to make manipulation of audio data easier, and is agnostic in its storage -/// representation, meaning that it can work with both interleaved and non-interleaved data. -/// -/// Audio is stored as "row-per-channel" -pub struct AudioBufferBase { - storage: Storage, -} - -impl fmt::Debug for AudioBufferBase { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_struct("AudioBufferBase") - .field( - "storage", - &format!( - "[{}x{} buffer of {}]", - self.storage.nrows(), - self.storage.ncols(), - std::any::type_name::() - ), - ) - .finish_non_exhaustive() - } -} - -impl Clone for AudioBufferBase { - fn clone(&self) -> Self { - Self { - storage: self.storage.clone(), - } - } -} - -impl Copy for AudioBufferBase {} - -impl Default for AudioBufferBase { - fn default() -> Self { - Self { - storage: ArrayBase::from_shape_fn((0, 0), |(_, _)| unreachable!()), - } - } -} - -impl> PartialEq> for AudioBufferBase -where - S::Elem: PartialEq, -{ - fn eq(&self, other: &AudioBufferBase) -> bool { - self.storage.shape() == other.storage.shape() - && self.storage.iter().eq(other.storage.iter()) - } - - // Explicitely implementing `fn ne` may yield better performance with the shortcircuiting of the or operator - #[allow(clippy::partialeq_ne_impl)] - fn ne(&self, other: &AudioBufferBase) -> bool { - self.storage.shape() != other.storage.shape() - || self.storage.iter().ne(other.storage.iter()) - } -} - -impl Eq for AudioBufferBase -where - Self: PartialEq, - S::Elem: Eq, -{ -} - -impl AudioBufferBase { - /// Number of samples present in this buffer. - pub fn num_samples(&self) -> usize { - self.storage.ncols() - } - - /// Number of channels present in this buffer. - pub fn num_channels(&self) -> usize { - self.storage.nrows() - } -} - -impl AudioBufferBase { - /// Return an immutable audio buffer view, sharing the data with this buffer. - pub fn as_ref(&self) -> AudioRef<'_, S::Elem> { - AudioRef { - storage: self.storage.view(), - } - } - - /// Slice the contents of this audio buffer, returning an immutable view of this buffer - /// containing only the audio samples at indices within the provided range. - pub fn slice(&self, range: impl RangeBounds) -> AudioRef<'_, S::Elem> { - let start = match range.start_bound() { - Bound::Included(i) => *i, - Bound::Excluded(i) => *i + 1, - Bound::Unbounded => 0, - }; - let end = match range.end_bound() { - Bound::Included(i) => *i - 1, - Bound::Excluded(i) => *i, - Bound::Unbounded => self.num_samples(), - }; - let storage = self.storage.slice(s![.., start..end]); - AudioRef { storage } - } - - /// Iterate over non-overlapping chunks of this audio buffer. - pub fn chunks(&self, size: usize) -> impl Iterator> { - let mut i = 0; - std::iter::from_fn(move || { - if i >= self.num_samples() { - return None; - } - let range = i..(i + size).min(self.num_samples()); - i += size; - Some(self.slice(range)) - }) - } - - /// Iterate over non-overlapping chunks of this audio buffer. If the last chunk has a smaller length than the - /// requested size, it will not be yielded. - pub fn chunks_exact(&self, size: usize) -> impl Iterator> { - let mut i = 0; - std::iter::from_fn(move || { - if i + size >= self.num_samples() { - return None; - } - let range = i..i + size; - i += size; - Some(self.slice(range)) - }) - } - - /// Iterate over overlapping windows of this audio buffer. - /// - /// # Arguments - /// - /// - `size`: Size of the window - pub fn windows(&self, size: usize) -> impl Iterator> { - let mut i = 0; - std::iter::from_fn(move || { - if i + size >= self.num_samples() { - return None; - } - let range = i..(i + size).min(self.num_samples()); - i += 1; - Some(self.slice(range)) - }) - } - - /// Return an immutable view of a single channel. Panics when the requested channel does not - /// exist. - pub fn get_channel(&self, channel: usize) -> ArrayView1<'_, S::Elem> { - self.storage.row(channel) - } - - /// Return an iterator of immutable views of the channels present in this audio buffer. - pub fn channels(&self) -> impl '_ + Iterator> { - self.storage.rows().into_iter() - } - - /// Get a single frame, that is all channels at the specified sample index. Panics when the - /// sample is out of range. - pub fn get_frame(&self, sample: usize) -> ArrayView1<'_, S::Elem> { - self.storage.column(sample) - } - - /// Return an immutable interleaved 2-D array view, where samples are in rows and channels are - /// in columns. - pub fn as_interleaved(&self) -> ArrayView2<'_, S::Elem> { - self.storage.t() - } - - /// Copies this audio buffer to another, giving you a unique owned buffer in the end. - /// - /// Not realtime-safe. - pub fn to_owned(&self) -> AudioBuffer - where - S::Elem: Clone, - { - AudioBuffer { - storage: self.storage.to_owned(), - } - } - - /// Copies audio data in this buffer to the provided interleaved buffer. The `output` buffer - /// must represent an interleaved buffer with the same number of channels and same number of - /// samples. - #[must_use] - pub fn copy_into_interleaved(&self, output: &mut [S::Elem]) -> bool - where - S::Elem: Copy, - { - if output.len() != self.storage.len() { - return false; - } - - for (inp, out) in self.as_interleaved().iter().zip(output.iter_mut()) { - *out = *inp; - } - true - } -} - -impl AudioBufferBase { - /// Return a mutable audio buffer view. - pub fn as_mut(&mut self) -> AudioMut<'_, S::Elem> { - AudioMut { - storage: self.storage.view_mut(), - } - } - - /// Slice the contents of this audio buffer, returning a mutable view of this buffer - /// containing only the audio samples at indices within the provided range. - pub fn slice_mut(&mut self, range: impl RangeBounds) -> AudioMut<'_, S::Elem> { - let start = match range.start_bound() { - Bound::Included(i) => *i, - Bound::Excluded(i) => *i + 1, - Bound::Unbounded => 0, - }; - let end = match range.end_bound() { - Bound::Included(i) => *i - 1, - Bound::Excluded(i) => *i, - Bound::Unbounded => self.num_samples(), - }; - let storage = self.storage.slice_mut(s![.., start..end]); - AudioMut { storage } - } - - /// Return a mutable view of a single channel. Panics when the requested channel does not - /// exist. - pub fn get_channel_mut(&mut self, channel: usize) -> ArrayViewMut1<'_, S::Elem> { - self.storage.row_mut(channel) - } - - /// Return an iterator of mutable views of the channels present in this audio buffer. - pub fn channels_mut(&mut self) -> impl '_ + Iterator> { - self.storage.rows_mut().into_iter() - } - /// Return a mutable interleaved 2-D array view, where samples are in rows and channels are in - /// columns. - pub fn as_interleaved_mut(&mut self) -> ArrayViewMut2<'_, S::Elem> { - self.storage.view_mut().reversed_axes() - } - - /// Copies audio data into this buffer from the provided interleaved audio buffer. The `output` buffer - /// must represent an interleaved buffer with the same number of channels and same number of - /// samples. - #[must_use] - pub fn copy_from_interleaved(&mut self, input: &[S::Elem]) -> bool - where - S::Elem: Copy, - { - if input.len() != self.storage.len() { - return false; - } - - for (out, inp) in self.as_interleaved_mut().iter_mut().zip(input.iter()) { - *out = *inp; - } - true - } -} - -impl AudioBufferBase { - /// Create a new audio buffer with the provided number of channels and sample size, filling - /// it with the provided fill function. - /// - /// Not realtime-safe. - pub fn fill_with( - channels: usize, - sample_size: usize, - fill: impl Fn(usize, usize) -> S::Elem, - ) -> Self { - let storage = Storage::from_shape_fn((channels, sample_size), |(ch, i)| fill(ch, i)); - Self { storage } - } - - /// Create a new audio buffer with the provided number of channels and sample size, filling - /// it with the provided value. - pub fn fill(channels: usize, sample_size: usize, value: S::Elem) -> Self - where - S::Elem: Copy, - { - Self::fill_with(channels, sample_size, |_, _| value) - } - - /// Create a new audio buffer with the provided number of channels and sample size, filling - /// it with the [`Default`] value of that type. - pub fn defaulted(channels: usize, sample_size: usize) -> Self - where - S::Elem: Default, - { - Self::fill_with(channels, sample_size, |_, _| S::Elem::default()) - } -} - -impl<'a, T: 'a> AudioRef<'a, T> -where - ViewRepr<&'a T>: Sized, -{ - /// Create an audio buffer reference from interleaved data. This does *not* copy the data, - /// but creates a view over it, so that it can be accessed as any other audio buffer. - pub fn from_interleaved(data: &'a [T], channels: usize) -> Option { - let buffer_size = data.len() / channels; - let raw = ArrayView2::from_shape((buffer_size, channels), data).ok()?; - let storage = raw.reversed_axes(); - Some(Self { storage }) - } - - /// Create an audio buffer reference from non-interleaved data. This does *not* copy the data, - /// but creates a view over it, so that it can be accessed as any other audio buffer. - pub fn from_noninterleaved(data: &'a [T], channels: usize) -> Option { - let buffer_size = data.len() / channels; - let storage = ArrayView2::from_shape((channels, buffer_size), data).ok()?; - Some(Self { storage }) - } -} - -impl<'a, T: 'a> AudioMut<'a, T> { - /// Create an audio buffer mutable reference from interleaved data. This does *not* copy the - /// data, but creates a view over it, so that it can be accessed as any other audio buffer. - /// - /// Writes to the resulting buffer directly map to the provided slice, and asking an - /// interleaved view out of the resulting buffer (with [`AudioBufferBase::as_interleaved`]) - /// means the same slice is returned. This makes for efficient copying between different - /// interleaved buffers, even though a non-interleaved interface. - pub fn from_interleaved_mut(data: &'a mut [T], channels: usize) -> Option { - let buffer_size = data.len() / channels; - let raw = ArrayViewMut2::from_shape((buffer_size, channels), data).ok()?; - let storage = raw.reversed_axes(); - Some(Self { storage }) - } - - /// Create an audio buffer mutable reference from interleaved data. This does *not* copy the - /// data, but creates a view over it, so that it can be accessed as any other audio buffer. - /// - /// Writes to the resulting buffer directly map to the provided slice, and asking an - /// interleaved view out of the resulting buffer (with [`AudioBufferBase::as_interleaved`]) - /// means the same slice is returned. - pub fn from_noninterleaved_mut(data: &'a mut [T], channels: usize) -> Option { - let buffer_size = data.len() / channels; - let storage = ArrayViewMut2::from_shape((channels, buffer_size), data).ok()?; - Some(Self { storage }) - } -} - -impl AudioBufferBase -where - S::Elem: Clone, -{ - /// Returns a mutable view over each channel of the frame at the given index. - /// - /// # Arguments - /// - /// * `sample`: Sample index for the frame to return. - /// - /// # Panics - /// - /// Panics if the sample index is out of range. - /// - /// returns: ArrayBase::Elem>, Dim<[usize; 1]>> - pub fn get_frame_mut(&mut self, sample: usize) -> ArrayViewMut1<'_, S::Elem> { - self.storage.column_mut(sample) - } - - /// Sets audio data of a single frame, that is all channels at the specified sample index. - /// Panics when the sample is out of range. - pub fn set_frame<'a>(&mut self, sample: usize, data: impl AsArray<'a, S::Elem, Ix1>) - where - S::Elem: 'a, - { - let column = self.storage.column_mut(sample); - data.into().assign_to(column); - } - - /// Sets audio data of a single sample, copying the provided value to each channel at that - /// sample index. Panics when the sample index is out of range. - pub fn set_mono(&mut self, i: usize, value: S::Elem) { - Array0::from_elem([], value) - .broadcast((self.num_channels(),)) - .unwrap() - .assign_to(self.storage.column_mut(i)) - } -} - -/// Trait for sample types. Typical sample types can be `i32`, `f32`, etc. but more can be -/// implemented downstream. -pub trait Sample: Copy { - /// Floating-point type which can fit all or a big majority of this type's values. - /// This type is the type used in float conversions, as well as the type of the amplitude in - /// buffer amplitude operations. - type Float: Copy; - /// Zero value for this sample. This is *not specifically* the numerical zero of the type, - /// but the value for which the amplitude of the stream is zero. Unsigned types are an - /// example for which the two are different. - const ZERO: Self; - - /// Construct a sample of this type from the corresponding float signal value. - fn from_float(f: Self::Float) -> Self; - - /// Compute the RMS value out of an iterator of this type. - fn rms(it: impl Iterator) -> Self::Float; - - /// Convert this value into its floating point equivalent. - fn into_float(self) -> Self::Float; - - /// Change the "amplitude" of this value, ie. absolute values less than one will bring the - /// value closer to [`Self::ZERO`], whereas absolute values above one will move the value - /// further away. - fn change_amplitude(&mut self, amp: Self::Float); -} - -#[duplicate::duplicate_item( - ty fty; - [i8] [f32]; - [i16] [f32]; - [i32] [f32]; - [i64] [f64]; -)] -impl Sample for ty { - type Float = fty; - const ZERO: Self = 0; - - fn from_float(f: Self::Float) -> Self { - (f * ty::MAX as fty) as ty - } - fn rms(it: impl Iterator) -> Self::Float { - let mut i = 0.0; - it.map(|t| t.into_float().powi(2)) - .reduce(|a, b| { - let res = a * i / (i + 1.0) + b / (i + 1.0); - i += 1.0; - res - }) - .unwrap_or(0.0) - .sqrt() - } - - fn into_float(self) -> Self::Float { - self as fty / ty::MAX as fty - } - fn change_amplitude(&mut self, amp: Self::Float) { - *self = ((*self as fty) * amp) as Self; - } -} - -#[duplicate::duplicate_item( - ty fty; - [u8] [f32]; - [u16] [f32]; - [u32] [f32]; - [u64] [f64]; -)] -impl Sample for ty { - type Float = fty; - const ZERO: Self = 1 + Self::MAX / 2; - - fn from_float(f: Self::Float) -> Self { - ((f * 0.5 + 0.5) * (Self::MAX as Self::Float + 1.0)) as Self - } - - fn rms(it: impl Iterator) -> Self::Float { - let mut i = 0.0; - it.map(|t| t.into_float().powi(2)) - .reduce(|a, b| { - let res = a * i / (i + 1.0) + b / (i + 1.0); - i += 1.0; - res - }) - .unwrap_or(0.0) - .sqrt() - } - - fn into_float(self) -> Self::Float { - let t = self as Self::Float / (Self::MAX as Self::Float + 1.0); - t * 2.0 - 1.0 - } - - fn change_amplitude(&mut self, amp: Self::Float) { - let f = Self::into_float(*self) * amp; - *self = Self::from_float(f) - } -} - -#[duplicate::duplicate_item( - ty; - [f32]; - [f64]; -)] -impl Sample for ty { - type Float = Self; - const ZERO: Self = 0.0; - - fn from_float(f: Self::Float) -> Self { - f - } - - fn rms(it: impl Iterator) -> Self::Float { - let mut i = 0.0; - it.map(|t| t.powi(2)) - .reduce(|a, b| { - let res = a * i / (i + 1.0) + b / (i + 1.0); - i += 1.0; - res - }) - .unwrap_or(0.0) - .sqrt() - } - - fn into_float(self) -> Self::Float { - self - } - - fn change_amplitude(&mut self, amp: Self::Float) { - *self *= amp; - } -} - -impl AudioBuffer { - /// Construct a zeroed buffer with the provided channels and sample size. - /// - /// Not realtime-safe. - pub fn zeroed(channels: usize, sample_size: usize) -> Self { - Self::fill(channels, sample_size, T::ZERO) - } -} - -impl AudioBufferBase -where - S::Elem: Sample, -{ - /// Compute the RMS (Root Mean Square) value of this entire buffer, all channels considered - /// equally. The result is given in terms of linear amplitude values, as a float determined by - /// [`S::Float`]. - /// - /// You can convert the result to decibels with the formula `20. * rms.log10()`. - pub fn rms(&self) -> ::Float { - S::Elem::rms(self.storage.iter().copied()) - } - - /// Compute the RMS (Root Mean Square) value of this entire buffer for a single channel. The - /// result is given in terms of linear amplitude values, as a float determined by [`S::Float`]. - /// - /// You can convert the result to decibels with the formula `20. * rms.log10()`. - pub fn channel_rms(&self, channel: usize) -> ::Float { - S::Elem::rms(self.storage.column(channel).iter().copied()) - } -} - -impl> AudioBufferBase { - /// Change the amplitude of this buffer by the provided amplitude. - /// - /// See [`Sample::change_amplitude`] for more details. - pub fn change_amplitude(&mut self, amplitude: ::Float) { - for s in self.storage.iter_mut() { - s.change_amplitude(amplitude); - } - } - - /// Mix a buffer into this buffer at the specified amplitude. The audio will be mixed into - /// this buffer as a result, and the other buffer's amplitude will be changed similarly to - /// applying [`Self::change_amplitude`] first. - pub fn mix(&mut self, other: AudioRef, other_amplitude: ::Float) - where - S::Elem: AddAssign, - { - for (mut ch_a, ch_b) in self.channels_mut().zip(other.channels()) { - for (a, b) in ch_a.iter_mut().zip(ch_b) { - let mut b = *b; - b.change_amplitude(other_amplitude); - *a += b; - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ndarray::ArrayView1; - - fn create_test_buffer() -> AudioBuffer { - AudioBuffer::fill_with(2, 4, |ch, i| (ch * 4 + i) as f32) - } - - #[test] - fn test_buffer_creation() { - let buf = create_test_buffer(); - assert_eq!(buf.num_channels(), 2); - assert_eq!(buf.num_samples(), 4); - - // Verify sample values - assert_eq!(buf.get_channel(0).to_vec(), vec![0.0, 1.0, 2.0, 3.0]); - assert_eq!(buf.get_channel(1).to_vec(), vec![4.0, 5.0, 6.0, 7.0]); - } - - #[test] - fn test_buffer_views() { - let mut buf = create_test_buffer(); - - // Test immutable slice - let slice = buf.slice(1..3); - assert_eq!(slice.num_samples(), 2); - assert_eq!(slice.get_channel(0).to_vec(), vec![1.0, 2.0]); - - // Test mutable slice - let mut slice = buf.slice_mut(1..3); - slice.get_channel_mut(0).fill(10.0); - assert_eq!(buf.get_channel(0).to_vec(), vec![0.0, 10.0, 10.0, 3.0]); - } - - #[test] - fn test_sample_conversions() { - // Test i16 <-> f32 conversion - assert_eq!(i16::from_float(0.5), 16383); - assert!((i16::MAX.into_float() - 1.0).abs() < f32::EPSILON); - - // Test u8 <-> f32 conversion - assert_eq!(u8::from_float(0.0), 128); - assert!((u8::ZERO.into_float()).abs() < f32::EPSILON); - } - - #[test] - fn test_rms() { - let mut buf = AudioBuffer::::zeroed(1, 4); - buf.get_channel_mut(0) - .assign(&ArrayView1::from(&[1.0, -1.0, 1.0, -1.0])); - let rms = buf.rms(); - assert!((rms - 1.0).abs() < f32::EPSILON, "RMS is incorrect: {rms}"); - } - - #[test] - fn test_mixing() { - let mut buf1 = AudioBuffer::::fill(1, 4, 1.0); - let buf2 = AudioBuffer::::fill(1, 4, 0.5); - - buf1.mix(buf2.as_ref(), 2.0); - assert_eq!(buf1.get_channel(0).to_vec(), vec![2.0, 2.0, 2.0, 2.0]); - } - - #[test] - fn test_interleaved() { - let data = vec![1.0f32, 2.0, 3.0, 4.0]; - let buf = AudioRef::from_interleaved(&data, 2).unwrap(); - - assert_eq!(buf.num_channels(), 2); - assert_eq!(buf.num_samples(), 2); - assert_eq!(buf.get_channel(0).to_vec(), vec![1.0, 3.0]); - assert_eq!(buf.get_channel(1).to_vec(), vec![2.0, 4.0]); - - let mut out = vec![0.0f32; 4]; - assert!(buf.copy_into_interleaved(&mut out)); - assert_eq!(out, data); - } -} diff --git a/src/backends/alsa/device.rs b/src/backends/alsa/device.rs index 7ee4d4a..17b915a 100644 --- a/src/backends/alsa/device.rs +++ b/src/backends/alsa/device.rs @@ -1,13 +1,11 @@ use crate::backends::alsa::stream::AlsaStream; use crate::backends::alsa::AlsaError; -use crate::{ - AudioDevice, AudioInputCallback, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, - Channel, DeviceType, StreamConfig, -}; -use alsa::{pcm, PCM}; +use crate::{AudioCallback, AudioDevice, Channel, DeviceType, StreamConfig}; +use alsa::{pcm, Direction, PCM}; use std::borrow::Cow; use std::fmt; use std::rc::Rc; +use std::sync::Arc; /// Type of ALSA devices. #[derive(Clone)] @@ -27,6 +25,7 @@ impl fmt::Debug for AlsaDevice { } impl AudioDevice for AlsaDevice { + type StreamHandle = AlsaStream; type Error = AlsaError; fn name(&self) -> Cow<'_, str> { @@ -44,6 +43,36 @@ impl AudioDevice for AlsaDevice { [] } + fn default_config(&self) -> Result { + let params = self.pcm.hw_params_current()?; + let min = params + .get_buffer_size_min() + .map(|x| x as usize) + .inspect_err(|err| log::warn!("Cannot get buffer size: {err}")) + .ok(); + let max = params + .get_buffer_size_max() + .map(|x| x as usize) + .inspect_err(|err| log::warn!("Cannot get buffer size: {err}")) + .ok(); + + let channels = params.get_channels()? as usize; + let (input_channels, output_channels) = + if matches!(self.direction, alsa::Direction::Capture) { + (channels, 0) + } else { + (0, channels) + }; + + Ok(StreamConfig { + sample_rate: params.get_rate()? as _, + buffer_size_range: (min, max), + exclusive: false, + input_channels, + output_channels, + }) + } + fn is_config_supported(&self, config: &StreamConfig) -> bool { self.get_hwp(config) .inspect_err(|err| { @@ -57,49 +86,39 @@ impl AudioDevice for AlsaDevice { log::info!("TODO: enumerate configurations"); None::<[StreamConfig; 0]> } -} - -impl AudioInputDevice for AlsaDevice { - type StreamHandle = AlsaStream; - - fn default_input_config(&self) -> Result { - self.default_config() - } - - fn create_input_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - AlsaStream::new_input(self.name.clone(), stream_config, callback) - } -} - -impl AudioOutputDevice for AlsaDevice { - type StreamHandle = AlsaStream; - - fn default_output_config(&self) -> Result { - self.default_config() - } - fn create_output_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, ) -> Result, Self::Error> { - AlsaStream::new_output(self.name.clone(), stream_config, callback) + let name = Arc::::from(self.name.to_owned()); + let direction = self.direction; + let input_device = { + let name = name.clone(); + move || match direction { + alsa::Direction::Capture => Ok(Some(AlsaDevice::new( + &name.clone(), + alsa::Direction::Capture, + )?)), + alsa::Direction::Playback => Ok(None), + } + }; + let output_device = move || match direction { + alsa::Direction::Capture => Ok(None), + alsa::Direction::Playback => Ok(Some(AlsaDevice::new( + &name.clone(), + alsa::Direction::Playback, + )?)), + }; + AlsaStream::new(input_device, output_device, stream_config, callback) } } impl AlsaDevice { /// Shortcut constructor for getting ALSA devices directly. - pub fn default_device(direction: alsa::Direction) -> Result, alsa::Error> { - let pcm = Rc::new(PCM::new("default", direction, true)?); - Ok(Some(Self { - pcm, - direction, - name: "default".to_string(), - })) + pub fn default_device(direction: alsa::Direction) -> Result { + Self::new("default", direction) } pub(super) fn new(name: &str, direction: alsa::Direction) -> Result { @@ -112,21 +131,6 @@ impl AlsaDevice { }) } - fn get_hwp(&self, config: &StreamConfig) -> Result, alsa::Error> { - let hwp = pcm::HwParams::any(&self.pcm)?; - hwp.set_channels(config.channels as _)?; - hwp.set_rate(config.samplerate as _, alsa::ValueOr::Nearest)?; - if let Some(min) = config.buffer_size_range.0 { - hwp.set_buffer_size_min(min as _)?; - } - if let Some(max) = config.buffer_size_range.1 { - hwp.set_buffer_size_max(max as _)?; - } - hwp.set_format(pcm::Format::float())?; - hwp.set_access(pcm::Access::RWInterleaved)?; - Ok(hwp) - } - pub(super) fn apply_config( &self, config: &StreamConfig, @@ -139,6 +143,12 @@ impl AlsaDevice { log::debug!("Apply config: hwp {hwp:#?}"); + if matches!(self.direction, alsa::Direction::Playback) { + hwp.set_channels(config.output_channels as _)?; + } else { + hwp.set_channels(config.input_channels as _)?; + } + swp.set_start_threshold(hwp.get_buffer_size()?)?; self.pcm.sw_params(&swp)?; log::debug!("Apply config: swp {swp:#?}"); @@ -146,15 +156,94 @@ impl AlsaDevice { Ok((hwp, swp, io)) } - fn default_config(&self) -> Result { - let samplerate = 48e3; // Default ALSA sample rate - let channel_count = 2; // Stereo stream - let channels = (1 << channel_count) - 1; + fn get_hwp(&self, config: &StreamConfig) -> Result { + let hwp = pcm::HwParams::any(&self.pcm)?; + hwp.set_channels(config.output_channels as _)?; + hwp.set_rate(config.sample_rate as _, alsa::ValueOr::Nearest)?; + if let Some(min) = config.buffer_size_range.0 { + hwp.set_buffer_size_min(min as _)?; + } + if let Some(max) = config.buffer_size_range.1 { + hwp.set_buffer_size_max(max as _)?; + } + hwp.set_format(pcm::Format::float())?; + hwp.set_access(pcm::Access::RWInterleaved)?; + Ok(hwp) + } +} + +#[derive(Debug, Clone)] +pub struct AlsaDuplex { + pub capture: AlsaDevice, + pub playback: AlsaDevice, +} + +impl AudioDevice for AlsaDuplex { + type StreamHandle = AlsaStream; + + type Error = AlsaError; + + fn name(&self) -> Cow { + Cow::Owned(format!("{} / {}", &self.capture.name, &self.playback.name)) + } + + fn device_type(&self) -> DeviceType { + DeviceType::PHYSICAL | DeviceType::DUPLEX + } + + fn channel_map(&self) -> impl IntoIterator { + [] + } + + fn default_config(&self) -> Result { + let hwp_inp = self.capture.pcm.hw_params_current()?; + let hwp_out = self.playback.pcm.hw_params_current()?; + let sample_rate = hwp_out.get_rate()? as f64; + let input_channels = hwp_inp.get_channels()? as usize; + let output_channels = hwp_out.get_channels()? as usize; + let min_size = { + let inp_min = hwp_inp.get_buffer_size_min().unwrap_or(0); + let out_min = hwp_out.get_buffer_size_min().unwrap_or(0); + inp_min.max(out_min) + }; + let max_size = { + let inp_max = hwp_inp.get_buffer_size_max().unwrap_or(0); + let out_max = hwp_out.get_buffer_size_max().unwrap_or(0); + inp_max.min(out_max) + }; Ok(StreamConfig { - samplerate: samplerate as _, - channels, - buffer_size_range: (None, None), + sample_rate, + input_channels, + output_channels, + buffer_size_range: ( + (min_size == 0).then_some(min_size as _), + (max_size == 0).then_some(max_size as _), + ), exclusive: false, }) } + + fn is_config_supported(&self, config: &StreamConfig) -> bool { + self.capture.is_config_supported(config) && self.playback.is_config_supported(config) + } + + fn enumerate_configurations(&self) -> Option> { + None::<[StreamConfig; 0]> + } + + fn create_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + let input_device = { + let name = self.capture.name.to_owned(); + move || AlsaDevice::new(&name, alsa::Direction::Capture).map(Some) + }; + let output_device = { + let name = self.playback.name.to_owned(); + move || AlsaDevice::new(&name, alsa::Direction::Playback).map(Some) + }; + AlsaStream::new(input_device, output_device, stream_config, callback) + } } diff --git a/src/backends/alsa/input.rs b/src/backends/alsa/input.rs deleted file mode 100644 index 19ec8b9..0000000 --- a/src/backends/alsa/input.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::audio_buffer::AudioRef; -use crate::backends::alsa::stream::AlsaStream; -use crate::backends::alsa::AlsaError; -use crate::prelude::alsa::device::AlsaDevice; -use crate::{AudioCallbackContext, AudioInput, AudioInputCallback, StreamConfig}; - -impl AlsaStream { - pub(super) fn new_input( - name: String, - stream_config: StreamConfig, - callback: Callback, - ) -> Result { - Self::new_generic( - stream_config, - move || AlsaDevice::new(&name, alsa::Direction::Capture), - callback, - move |ctx, recover| { - if let Err(err) = ctx.io.readi(&mut ctx.buffer[..]) { - recover(err)?; - } - let buffer = AudioRef::from_interleaved(ctx.buffer, ctx.num_channels).unwrap(); - let context = AudioCallbackContext { - stream_config: *ctx.config, - timestamp: *ctx.timestamp, - }; - let input = AudioInput { - buffer, - timestamp: *ctx.timestamp, - }; - ctx.callback.on_input_data(context, input); - *ctx.timestamp += ctx.num_frames as u64; - Ok(()) - }, - ) - } -} diff --git a/src/backends/alsa/mod.rs b/src/backends/alsa/mod.rs index 4a6e952..74f5b7f 100644 --- a/src/backends/alsa/mod.rs +++ b/src/backends/alsa/mod.rs @@ -12,8 +12,6 @@ use std::borrow::Cow; use thiserror::Error; mod device; -mod input; -mod output; mod stream; mod triggerfd; @@ -27,6 +25,8 @@ pub enum AlsaError { /// Error originates from I/O operations. #[error("I/O error: {0}")] IoError(#[from] nix::Error), + #[error("No channels have been opened")] + NoChannelsOpened, } /// ALSA driver type. ALSA is statically available without client configuration, therefore this type @@ -50,7 +50,7 @@ impl AudioDriver for AlsaDriver { _ if device_type.is_output() => alsa::Direction::Playback, _ => return Ok(None), }; - Ok(AlsaDevice::default_device(direction)?) + Ok(Some(AlsaDevice::default_device(direction)?)) } fn list_devices(&self) -> Result, Self::Error> { diff --git a/src/backends/alsa/output.rs b/src/backends/alsa/output.rs deleted file mode 100644 index 029f95e..0000000 --- a/src/backends/alsa/output.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::audio_buffer::AudioMut; -use crate::backends::alsa::stream::AlsaStream; -use crate::backends::alsa::AlsaError; -use crate::prelude::alsa::device::AlsaDevice; -use crate::{AudioCallbackContext, AudioOutput, AudioOutputCallback, StreamConfig}; - -impl AlsaStream { - pub(super) fn new_output( - name: String, - stream_config: StreamConfig, - callback: Callback, - ) -> Result { - Self::new_generic( - stream_config, - move || AlsaDevice::new(&name, alsa::Direction::Playback), - callback, - move |ctx, recover| { - let context = AudioCallbackContext { - stream_config, - timestamp: *ctx.timestamp, - }; - let input = AudioOutput { - buffer: AudioMut::from_interleaved_mut(&mut ctx.buffer[..], ctx.num_channels) - .unwrap(), - timestamp: *ctx.timestamp, - }; - ctx.callback.on_output_data(context, input); - *ctx.timestamp += ctx.num_frames as u64; - if let Err(err) = ctx.io.writei(&ctx.buffer[..]) { - recover(err)?; - } - Ok(()) - }, - ) - } -} diff --git a/src/backends/alsa/stream.rs b/src/backends/alsa/stream.rs index 663114d..a827a6f 100644 --- a/src/backends/alsa/stream.rs +++ b/src/backends/alsa/stream.rs @@ -1,13 +1,227 @@ +use crate::audio_buffer::{AudioMut, AudioRef}; use crate::backends::alsa::device::AlsaDevice; use crate::backends::alsa::{triggerfd, AlsaError}; -use crate::channel_map::{Bitset, ChannelMap32}; use crate::timestamp::Timestamp; -use crate::{AudioStreamHandle, StreamConfig}; +use crate::{ + AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, AudioStreamHandle, + ResolvedStreamConfig, StreamConfig, +}; use alsa::pcm; use alsa::PollDescriptors; +use std::rc::{Rc, Weak}; use std::sync::Arc; use std::thread::JoinHandle; -use std::time::Duration; + +struct AlsaStreamData<'io> { + pcm: Weak, + io: Option>, + buffer: Box<[f32]>, + timestamp: Timestamp, + sample_rate: f64, + channels: usize, + max_frame_count: usize, + latency: f64, +} + +impl<'io> AlsaStreamData<'io> { + fn new(device: &'io AlsaDevice, config: StreamConfig) -> Result { + let (hwp, _, io) = device.apply_config(&config)?; + let (_, period_size) = device.pcm.get_params()?; + let period_size = period_size as usize; + log::info!("[{}] Period size : {period_size}", &device.name); + let channels = hwp.get_channels()? as usize; + log::info!("[{}] channels: {channels}", &device.name); + let sample_rate = hwp.get_rate()? as f64; + log::info!("[{}] Sample rate : {sample_rate}", &device.name); + + let buffer = std::iter::repeat_n(0.0, period_size * channels).collect(); + let timestamp = Timestamp::new(sample_rate); + let latency = period_size as f64 / sample_rate; + + Ok(Self { + pcm: Rc::downgrade(&device.pcm), + io: Some(io), + buffer, + timestamp, + sample_rate, + channels, + max_frame_count: period_size, + latency, + }) + } + + fn new_empty() -> Self { + Self { + pcm: Weak::new(), + io: None, + buffer: Box::new([]), + timestamp: Timestamp::new(0.0), + sample_rate: 0.0, + channels: 0, + max_frame_count: 0, + latency: 0.0, + } + } + + fn descriptor_count(&self) -> usize { + let Some(pcm) = self.pcm.upgrade() else { + return 0; + }; + return pcm.count(); + } + + fn available(&self) -> Result { + let Some(pcm) = self.pcm.upgrade() else { + return Ok(0); + }; + Ok(pcm.avail_update()? as usize) + } + + fn read(&mut self, num_frames: usize) -> Result, AlsaError> { + let Some(io) = self.io.as_mut() else { + return Ok(self.get_buffer(0)); + }; + let len = num_frames * self.channels; + let buffer = &mut self.buffer[..len]; + if let Err(err) = io.readi(buffer) { + self.pcm.upgrade().unwrap().try_recover(err, true)?; + } + Ok(AudioRef::from_interleaved(buffer, self.channels).unwrap()) + } + + fn read_input(&mut self, num_frames: usize) -> Result, AlsaError> { + Ok(AudioInput { + timestamp: self.timestamp, + buffer: self.read(num_frames)?, + }) + } + + fn get_buffer(&self, num_frames: usize) -> AudioRef { + let len = num_frames * self.channels; + AudioRef::from_interleaved(&self.buffer[..len], self.channels).unwrap() + } + + fn get_buffer_mut(&mut self, num_frames: usize) -> AudioMut { + let len = num_frames * self.channels; + AudioMut::from_interleaved_mut(&mut self.buffer[..len], self.channels).unwrap() + } + + fn write(&mut self, num_frames: usize) -> Result<(), AlsaError> { + let Some(io) = self.io.as_mut() else { + return Ok(()); + }; + let len = num_frames * self.channels; + let scratch = &self.buffer[..len]; + if let Err(err) = io.writei(scratch) { + self.pcm.upgrade().unwrap().try_recover(err, true)?; + } + Ok(()) + } + + fn provide_output(&mut self, num_frames: usize) -> AudioOutput { + AudioOutput { + timestamp: self.timestamp, + buffer: self.get_buffer_mut(num_frames), + } + } + + fn tick_timestamp(&mut self, samples: u64) { + self.timestamp += samples; + } +} + +struct AlsaThread { + callback: Callback, + eject_trigger: triggerfd::Receiver, + stream_config: StreamConfig, + input_device: Option, + output_device: Option, +} + +impl AlsaThread { + fn new( + callback: Callback, + eject_trigger: triggerfd::Receiver, + stream_config: StreamConfig, + input_device: Option, + output_device: Option, + ) -> Self { + Self { + callback, + eject_trigger, + stream_config, + input_device, + output_device, + } + } + + fn thread_loop(mut self) -> Result { + let mut input_stream = self + .input_device + .as_ref() + .map(|d| AlsaStreamData::new(d, self.stream_config)) + .transpose()? + .unwrap_or_else(AlsaStreamData::new_empty); + let mut output_stream = self + .output_device + .as_ref() + .map(|d| AlsaStreamData::new(d, self.stream_config)) + .transpose()? + .unwrap_or_else(AlsaStreamData::new_empty); + + let stream_config = ResolvedStreamConfig { + sample_rate: output_stream.sample_rate, + input_channels: input_stream.channels, + output_channels: output_stream.channels, + max_frame_count: output_stream + .max_frame_count + .max(input_stream.max_frame_count), + }; + let mut poll_descriptors = { + let mut buf = vec![self.eject_trigger.as_pollfd()]; + let num_descriptors = + input_stream.descriptor_count() + output_stream.descriptor_count(); + buf.extend( + std::iter::repeat(libc::pollfd { + fd: 0, + events: 0, + revents: 0, + }) + .take(num_descriptors), + ); + buf + }; + self.callback.prepare(AudioCallbackContext { + stream_config, + timestamp: Timestamp::new(stream_config.sample_rate), + }); + loop { + let out_frames = output_stream.available()?; + let in_frames = input_stream.available()?; + + if out_frames == 0 && in_frames == 0 { + let latency = input_stream.latency.round() as i32; + if alsa::poll::poll(&mut poll_descriptors, latency)? > 0 { + log::debug!("Eject requested, returning ownership of callback"); + break Ok(self.callback); + } + continue; + } + + log::debug!("Frames available: out {out_frames}, in {in_frames}"); + let context = AudioCallbackContext { + timestamp: output_stream.timestamp, + stream_config, + }; + let input = input_stream.read_input(in_frames)?; + let output = output_stream.provide_output(out_frames); + self.callback.process_audio(context, input, output); + input_stream.tick_timestamp(in_frames as u64); + output_stream.tick_timestamp(out_frames as u64); + output_stream.write(out_frames)?; + } + } +} /// Type of ALSA streams. /// @@ -29,101 +243,23 @@ impl AudioStreamHandle for AlsaStream { } } -impl AlsaStream { - pub(super) fn new_generic( +impl AlsaStream { + pub(super) fn new( + input_device: impl 'static + Send + FnOnce() -> Result, alsa::Error>, + output_device: impl 'static + Send + FnOnce() -> Result, alsa::Error>, stream_config: StreamConfig, - device: impl 'static + Send + FnOnce() -> Result, - mut callback: Callback, - loop_callback: impl 'static - + Send - + Fn( - StreamContext, - &dyn Fn(alsa::Error) -> Result<(), alsa::Error>, - ) -> Result<(), alsa::Error>, + callback: Callback, ) -> Result { let (tx, rx) = triggerfd::trigger()?; - let join_handle = std::thread::spawn({ - move || { - let device = device()?; - let recover = |err| device.pcm.try_recover(err, true); - let mut poll_descriptors = { - let mut buf = vec![rx.as_pollfd()]; - let num_descriptors = device.pcm.count(); - buf.extend(std::iter::repeat_n( - libc::pollfd { - fd: 0, - events: 0, - revents: 0, - }, - num_descriptors, - )); - buf - }; - let (hwp, _, io) = device.apply_config(&stream_config)?; - let (_, period_size) = device.pcm.get_params()?; - let period_size = period_size as usize; - log::info!("Period size : {period_size}"); - let num_channels = hwp.get_channels()? as usize; - log::info!("Num channels: {num_channels}"); - let samplerate = hwp.get_rate()? as f64; - log::info!("Sample rate : {samplerate}"); - let stream_config = StreamConfig { - samplerate, - channels: ChannelMap32::default() - .with_indices(std::iter::repeat_n(1, num_channels)), - buffer_size_range: (Some(period_size), Some(period_size)), - exclusive: false, - }; - let mut timestamp = Timestamp::new(samplerate); - let mut buffer = vec![0f32; period_size * num_channels]; - let latency = period_size as f64 / samplerate; - device.pcm.prepare()?; - if device.pcm.state() != pcm::State::Running { - log::info!("Device not already started, starting now"); - device.pcm.start()?; - } - let _try = || loop { - let frames = device.pcm.avail_update()? as usize; - if frames == 0 { - let latency = latency.round() as i32; - if alsa::poll::poll(&mut poll_descriptors, latency)? > 0 { - log::debug!("Eject requested, returning ownership of callback"); - break Ok(callback); - } - continue; - } - - log::debug!("Frames available: {frames}"); - let frames = std::cmp::min(frames, period_size); - let len = frames * num_channels; - - loop_callback( - StreamContext { - config: &stream_config, - timestamp: &mut timestamp, - io: &io, - num_channels, - num_frames: frames, - buffer: &mut buffer[..len], - callback: &mut callback, - }, - &recover, - )?; - - match device.pcm.state() { - pcm::State::Suspended => { - if hwp.can_resume() { - device.pcm.resume()?; - } else { - device.pcm.prepare()?; - } - } - pcm::State::Paused => std::thread::sleep(Duration::from_secs(1)), - _ => {} - } - }; - _try() - } + let join_handle = std::thread::spawn(move || { + let worker = AlsaThread::new( + callback, + rx, + stream_config, + input_device()?, + output_device()?, + ); + worker.thread_loop() }); Ok(Self { eject_trigger: Arc::new(tx), @@ -131,13 +267,3 @@ impl AlsaStream { }) } } - -pub(super) struct StreamContext<'a, Callback: 'a> { - pub(super) config: &'a StreamConfig, - pub(super) timestamp: &'a mut Timestamp, - pub(super) io: &'a pcm::IO<'a, f32>, - pub(super) num_channels: usize, - pub(super) num_frames: usize, - pub(super) buffer: &'a mut [f32], - pub(super) callback: &'a mut Callback, -} diff --git a/src/backends/coreaudio.rs b/src/backends/coreaudio.rs deleted file mode 100644 index 93dd7a6..0000000 --- a/src/backends/coreaudio.rs +++ /dev/null @@ -1,497 +0,0 @@ -//! # CoreAudio backend -//! -//! CoreAudio is the audio backend for macOS and iOS devices. - -use std::borrow::Cow; -use std::convert::Infallible; - -use coreaudio::audio_unit::audio_format::LinearPcmFlags; -use coreaudio::audio_unit::macos_helpers::{ - audio_unit_from_device_id, get_audio_device_ids_for_scope, get_default_device_id, - get_device_name, get_supported_physical_stream_formats, -}; -use coreaudio::audio_unit::render_callback::{data, Args}; -use coreaudio::audio_unit::{AudioUnit, Element, SampleFormat, Scope, StreamFormat}; -use coreaudio_sys::{ - kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange, - kAudioObjectPropertyElementMaster, kAudioObjectPropertyScopeInput, - kAudioObjectPropertyScopeOutput, kAudioUnitProperty_MaximumFramesPerSlice, - kAudioUnitProperty_SampleRate, kAudioUnitProperty_StreamFormat, AudioDeviceID, - AudioObjectGetPropertyData, AudioObjectPropertyAddress, AudioValueRange, -}; - -fn get_device_property( - device_id: AudioDeviceID, - address: AudioObjectPropertyAddress, -) -> Result { - let mut data = std::mem::MaybeUninit::::uninit(); - let mut size = std::mem::size_of::() as u32; - let status = unsafe { - AudioObjectGetPropertyData( - device_id, - &address, - 0, - std::ptr::null(), - &mut size, - data.as_mut_ptr() as *mut _, - ) - }; - coreaudio::Error::from_os_status(status)?; - Ok(unsafe { data.assume_init() }) -} - -fn set_device_property( - device_id: AudioDeviceID, - address: AudioObjectPropertyAddress, - data: &T, -) -> Result<(), coreaudio::Error> { - let size = std::mem::size_of::() as u32; - let status = unsafe { - coreaudio_sys::AudioObjectSetPropertyData( - device_id, - &address, - 0, - std::ptr::null(), - size, - data as *const T as *const _, - ) - }; - coreaudio::Error::from_os_status(status) -} -use thiserror::Error; - -use crate::audio_buffer::{AudioBuffer, Sample}; -use crate::channel_map::Bitset; -use crate::prelude::ChannelMap32; -use crate::timestamp::Timestamp; -use crate::{ - AudioCallbackContext, AudioDevice, AudioDriver, AudioInput, AudioInputCallback, - AudioInputDevice, AudioOutput, AudioOutputCallback, AudioOutputDevice, AudioStreamHandle, - Channel, DeviceType, SendEverywhereButOnWeb, StreamConfig, -}; - -/// Type of errors from the CoreAudio backend -#[derive(Debug, Error)] -#[error("CoreAudio error:")] -pub enum CoreAudioError { - /// Error originating from CoreAudio - #[error("{0}")] - BackendError(#[from] coreaudio::Error), - /// The scope given to an audio device is invalid. - #[error("Invalid scope {0:?}")] - InvalidScope(Scope), -} - -/// The CoreAudio driver. -#[derive(Debug, Copy, Clone)] -pub struct CoreAudioDriver; - -impl AudioDriver for CoreAudioDriver { - type Error = CoreAudioError; - type Device = CoreAudioDevice; - const DISPLAY_NAME: &'static str = "CoreAudio"; - - fn version(&self) -> Result, Self::Error> { - Ok(Cow::Borrowed("unknown")) - } - - fn default_device(&self, device_type: DeviceType) -> Result, Self::Error> { - let Some(device_id) = get_default_device_id(device_type.is_input()) else { - return Ok(None); - }; - Ok(Some(CoreAudioDevice::from_id( - device_id, - device_type.is_input(), - )?)) - } - - fn list_devices(&self) -> Result, Self::Error> { - let per_scope = [Scope::Input, Scope::Output] - .into_iter() - .map(|scope| { - let audio_ids = get_audio_device_ids_for_scope(scope)?; - audio_ids - .into_iter() - .map(|id| CoreAudioDevice::from_id(id, matches!(scope, Scope::Input))) - .collect::, _>>() - }) - .collect::, _>>()?; - Ok(per_scope.into_iter().flatten()) - } -} - -/// Type of devices available from the CoreAudio driver. -#[derive(Debug, Clone, Copy)] -pub struct CoreAudioDevice { - device_id: AudioDeviceID, - device_type: DeviceType, -} - -impl CoreAudioDevice { - fn from_id(device_id: AudioDeviceID, is_input: bool) -> Result { - let is_output = !is_input; // TODO: Interact with CoreAudio directly to be able to work with duplex devices - let is_default = get_default_device_id(true) == Some(device_id) - || get_default_device_id(false) == Some(device_id); - let mut device_type = DeviceType::empty(); - device_type.set(DeviceType::INPUT, is_input); - device_type.set(DeviceType::OUTPUT, is_output); - device_type.set(DeviceType::DEFAULT, is_default); - Ok(Self { - device_id, - device_type, - }) - } - - /// Sets the device's buffer size if requested in the `StreamConfig`. - /// This must be done before creating the AudioUnit. - fn set_buffer_size_from_config( - &self, - stream_config: &StreamConfig, - ) -> Result<(), CoreAudioError> { - if let (Some(min), Some(max)) = stream_config.buffer_size_range { - if min == max { - let property_address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyBufferFrameSize, - mScope: if self.device_type.is_input() { - kAudioObjectPropertyScopeInput - } else { - kAudioObjectPropertyScopeOutput - }, - mElement: kAudioObjectPropertyElementMaster, - }; - set_device_property(self.device_id, property_address, &(min as u32))?; - } - } - Ok(()) - } -} - -impl AudioDevice for CoreAudioDevice { - type Error = CoreAudioError; - - fn name(&self) -> Cow { - match get_device_name(self.device_id) { - Ok(std) => Cow::Owned(std), - Err(err) => { - eprintln!("Cannot get audio device name: {err}"); - Cow::Borrowed("") - } - } - } - - fn device_type(&self) -> DeviceType { - self.device_type - } - - fn channel_map(&self) -> impl IntoIterator { - let is_input = matches!(self.device_type, DeviceType::INPUT); - let channels = match audio_unit_from_device_id(self.device_id, is_input) { - Err(err) => { - eprintln!("CoreAudio error getting audio unit: {err}"); - 0 - } - Ok(audio_unit) => { - let stream_format = if is_input { - audio_unit.input_stream_format().unwrap() - } else { - audio_unit.output_stream_format().unwrap() - }; - stream_format.channels as usize - } - }; - (0..channels).map(|ch| Channel { - index: ch, - name: Cow::Owned(format!("Channel {}", ch)), - }) - } - - fn is_config_supported(&self, _config: &StreamConfig) -> bool { - true - } - - /// Returns the supported I/O buffer size range for the device. - fn buffer_size_range(&self) -> Result<(Option, Option), CoreAudioError> { - let property_address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyBufferFrameSizeRange, - mScope: if self.device_type.is_input() { - kAudioObjectPropertyScopeInput - } else { - kAudioObjectPropertyScopeOutput - }, - mElement: kAudioObjectPropertyElementMaster, - }; - - let range: AudioValueRange = get_device_property(self.device_id, property_address)?; - - Ok((Some(range.mMinimum as usize), Some(range.mMaximum as usize))) - } - - fn enumerate_configurations(&self) -> Option> { - const TYPICAL_SAMPLERATES: [f64; 5] = [44100., 48000., 96000., 128000., 192000.]; - let supported_list = get_supported_physical_stream_formats(self.device_id) - .inspect_err(|err| eprintln!("Error getting stream formats: {err}")) - .ok()?; - let buffer_size_range = self.buffer_size_range().unwrap_or((None, None)); - Some(supported_list.into_iter().flat_map(move |asbd| { - let samplerate_range = asbd.mSampleRateRange.mMinimum..asbd.mSampleRateRange.mMaximum; - TYPICAL_SAMPLERATES - .iter() - .copied() - .filter(move |sr| samplerate_range.contains(sr)) - .flat_map(move |sr| { - [false, true] - .into_iter() - .map(move |exclusive| (sr, exclusive)) - }) - .map(move |(samplerate, exclusive)| { - let channels = 1 << (asbd.mFormat.mChannelsPerFrame - 1); - StreamConfig { - samplerate, - channels, - buffer_size_range, - exclusive, - } - }) - })) - } -} - -fn input_stream_format(sample_rate: f64, channels: ChannelMap32) -> StreamFormat { - StreamFormat { - sample_rate, - sample_format: SampleFormat::I16, - flags: LinearPcmFlags::IS_SIGNED_INTEGER, - channels: channels.count() as _, - } -} - -impl AudioInputDevice for CoreAudioDevice { - type StreamHandle = CoreAudioStream; - - fn default_input_config(&self) -> Result { - let audio_unit = audio_unit_from_device_id(self.device_id, true)?; - let samplerate = audio_unit.get_property::( - kAudioUnitProperty_SampleRate, - Scope::Input, - Element::Input, - )?; - Ok(StreamConfig { - channels: 0b11, - samplerate, - buffer_size_range: self.buffer_size_range()?, - exclusive: false, - }) - } - - fn create_input_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - let mut device = *self; - device.device_type = DeviceType::INPUT; - device.set_buffer_size_from_config(&stream_config)?; - CoreAudioStream::new_input(self.device_id, stream_config, callback) - } -} - -fn output_stream_format(sample_rate: f64, channels: ChannelMap32) -> StreamFormat { - StreamFormat { - sample_rate, - sample_format: SampleFormat::F32, - flags: LinearPcmFlags::IS_NON_INTERLEAVED | LinearPcmFlags::IS_FLOAT, - channels, - } -} - -impl AudioOutputDevice for CoreAudioDevice { - type StreamHandle = CoreAudioStream; - - fn default_output_config(&self) -> Result { - let audio_unit = audio_unit_from_device_id(self.device_id, false)?; - let samplerate = audio_unit.sample_rate()?; - Ok(StreamConfig { - samplerate, - buffer_size_range: self.buffer_size_range()?, - channels: 0b11, - exclusive: false, - }) - } - - fn create_output_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - self.set_buffer_size_from_config(&stream_config)?; - CoreAudioStream::new_output(self.device_id, stream_config, callback) - } -} - -/// Stream type created by opening up a stream on a [`CoreAudioDevice`]. -pub struct CoreAudioStream { - audio_unit: AudioUnit, - callback_retrieve: oneshot::Sender>, -} - -impl AudioStreamHandle for CoreAudioStream { - type Error = Infallible; - - fn eject(mut self) -> Result { - let (tx, rx) = oneshot::channel(); - self.callback_retrieve.send(tx).unwrap(); - let callback = rx.recv().unwrap(); - self.audio_unit.free_input_callback(); - self.audio_unit.free_render_callback(); - Ok(callback) - } -} - -impl CoreAudioStream { - fn new_input( - device_id: AudioDeviceID, - stream_config: StreamConfig, - callback: Callback, - ) -> Result { - let mut audio_unit = audio_unit_from_device_id(device_id, true)?; - let asbd = input_stream_format(stream_config.samplerate, stream_config.channels).to_asbd(); - audio_unit.set_property( - kAudioUnitProperty_StreamFormat, - Scope::Output, - Element::Input, - Some(&asbd), - )?; - let max_frames: u32 = audio_unit.get_property( - kAudioUnitProperty_MaximumFramesPerSlice, - Scope::Global, - Element::Output, - )?; - let mut buffer = AudioBuffer::zeroed(stream_config.channels.count(), max_frames as usize); - - // Set up the callback retrieval process, without needing to make the callback `Sync` - let (tx, rx) = oneshot::channel::>(); - let mut callback = Some(callback); - audio_unit.set_input_callback(move |args: Args>| { - if let Ok(sender) = rx.try_recv() { - sender.send(callback.take().unwrap()).unwrap(); - return Err(()); - } - let mut buffer = buffer.slice_mut(..args.num_frames); - for (out, inp) in buffer - .as_interleaved_mut() - .iter_mut() - .zip(args.data.buffer.iter()) - { - *out = inp.into_float(); - } - let timestamp = - Timestamp::from_count(stream_config.samplerate, args.time_stamp.mSampleTime as _); - let input = AudioInput { - buffer: buffer.as_ref(), - timestamp, - }; - if let Some(callback) = &mut callback { - callback.on_input_data( - AudioCallbackContext { - stream_config, - timestamp, - }, - input, - ); - } - Ok(()) - })?; - audio_unit.start()?; - Ok(Self { - audio_unit, - callback_retrieve: tx, - }) - } -} - -impl CoreAudioStream { - fn new_output( - device_id: AudioDeviceID, - stream_config: StreamConfig, - callback: Callback, - ) -> Result { - let mut audio_unit = audio_unit_from_device_id(device_id, false)?; - let asbd = output_stream_format(stream_config.samplerate, stream_config.channels).to_asbd(); - audio_unit.set_property( - kAudioUnitProperty_StreamFormat, - Scope::Input, - Element::Output, - Some(&asbd), - )?; - let max_frames: u32 = audio_unit.get_property( - kAudioUnitProperty_MaximumFramesPerSlice, - Scope::Global, - Element::Output, - )?; - let mut buffer = AudioBuffer::zeroed(stream_config.channels.count(), max_frames as usize); - - // Set up the callback retrieval process, without needing to make the callback `Sync` - let (tx, rx) = oneshot::channel::>(); - let mut callback = Some(callback); - audio_unit.set_render_callback(move |mut args: Args>| { - if let Ok(sender) = rx.try_recv() { - sender.send(callback.take().unwrap()).unwrap(); - return Err(()); - } - let mut buffer = buffer.slice_mut(..args.num_frames); - let timestamp = - Timestamp::from_count(stream_config.samplerate, args.time_stamp.mSampleTime as _); - let output = AudioOutput { - buffer: buffer.as_mut(), - timestamp, - }; - if let Some(callback) = &mut callback { - callback.on_output_data( - AudioCallbackContext { - stream_config, - timestamp, - }, - output, - ); - for (output, inner) in args.data.channels_mut().zip(buffer.channels()) { - output.copy_from_slice(inner.as_slice().unwrap()); - } - } - Ok(()) - })?; - audio_unit.start()?; - Ok(Self { - audio_unit, - callback_retrieve: tx, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_set_device_buffersize() { - let driver = CoreAudioDriver; - if let Ok(Some(device)) = driver.default_device(DeviceType::OUTPUT) { - let buffer_size = 256; - - // Set the buffer size on the device. - let property_address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyBufferFrameSize, - mScope: kAudioObjectPropertyScopeOutput, - mElement: kAudioObjectPropertyElementMaster, - }; - set_device_property(device.device_id, property_address, &buffer_size).unwrap(); - - // Read it back to confirm. - let actual_buffer_size: u32 = - get_device_property(device.device_id, property_address).unwrap(); - - assert_eq!(buffer_size, actual_buffer_size); - } else { - println!("Skipping test: No default output device found."); - } - } -} diff --git a/src/backends/mod.rs b/src/backends/mod.rs index 6e8289d..3d28778 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -5,109 +5,10 @@ //! Each backend is provided in its own submodule. Types should be public so that the user isn't //! limited to going through the main API if they want to choose a specific backend. -use crate::{AudioDriver, AudioInputDevice, AudioOutputDevice, DeviceType}; +pub mod null; -#[cfg(unsupported)] -compile_error!("Unsupported platform (supports ALSA, CoreAudio, and WASAPI)"); +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub use interflow_coreaudio as coreaudio; -#[cfg(os_alsa)] -pub mod alsa; - -#[cfg(os_coreaudio)] -pub mod coreaudio; - -#[cfg(os_wasapi)] -pub mod wasapi; - -#[cfg(all(os_pipewire, feature = "pipewire"))] -pub mod pipewire; - -/// Returns the default driver. -/// -/// "Default" here means that it is a supported driver that is available on the platform. -/// -/// The signature makes it unfortunately impossible to do runtime selection, and could change in -/// the future to make it possible. Until now, the "default" driver is the lowest common -/// denominator. -/// -/// Selects the following driver depending on platform: -/// -/// | **Platform** | **Driver** | -/// |:------------:|:---------------------------:| -/// | Linux | Pipewire (if enabled), ALSA | -/// | macOS | CoreAudio | -/// | Windows | WASAPI | -#[cfg(any(os_alsa, os_coreaudio, os_wasapi))] -#[allow(clippy::needless_return)] -pub fn default_driver() -> impl AudioDriver { - #[cfg(all(os_pipewire, feature = "pipewire"))] - return pipewire::driver::PipewireDriver::new().unwrap(); - #[cfg(all(not(all(os_pipewire, feature = "pipewire")), os_alsa))] - return alsa::AlsaDriver; - #[cfg(os_coreaudio)] - return coreaudio::CoreAudioDriver; - #[cfg(os_wasapi)] - return wasapi::WasapiDriver; -} - -/// Returns the default input device for the given audio driver. -/// -/// The default device is usually the one the user has selected in its system settings. -pub fn default_input_device_from(driver: &Driver) -> Driver::Device -where - Driver::Device: AudioInputDevice, -{ - driver - .default_device(DeviceType::PHYSICAL | DeviceType::INPUT) - .expect("Audio driver error") - .expect("No default device found") -} - -/// Default input device from the default driver for this platform. -/// -/// "Default" here means both in terms of platform support but also can include runtime selection. -/// Therefore, it is better to use this method directly rather than first getting the default -/// driver from [`default_driver`]. -#[cfg(any(feature = "pipewire", os_alsa, os_coreaudio, os_wasapi))] -#[allow(clippy::needless_return)] -pub fn default_input_device() -> impl AudioInputDevice { - #[cfg(all(os_pipewire, feature = "pipewire"))] - return default_input_device_from(&pipewire::driver::PipewireDriver::new().unwrap()); - #[cfg(all(not(all(os_pipewire, feature = "pipewire")), os_alsa))] - return default_input_device_from(&alsa::AlsaDriver); - #[cfg(os_coreaudio)] - return default_input_device_from(&coreaudio::CoreAudioDriver); - #[cfg(os_wasapi)] - return default_input_device_from(&wasapi::WasapiDriver); -} - -/// Returns the default input device for the given audio driver. -/// -/// The default device is usually the one the user has selected in its system settings. -pub fn default_output_device_from(driver: &Driver) -> Driver::Device -where - Driver::Device: AudioOutputDevice, -{ - driver - .default_device(DeviceType::PHYSICAL | DeviceType::OUTPUT) - .expect("Audio driver error") - .expect("No default device found") -} - -/// Default output device from the default driver for this platform. -/// -/// "Default" here means both in terms of platform support but also can include runtime selection. -/// Therefore, it is better to use this method directly rather than first getting the default -/// driver from [`default_driver`]. -#[cfg(any(os_alsa, os_coreaudio, os_wasapi, feature = "pipewire"))] -#[allow(clippy::needless_return)] -pub fn default_output_device() -> impl AudioOutputDevice { - #[cfg(all(os_pipewire, feature = "pipewire"))] - return default_output_device_from(&pipewire::driver::PipewireDriver::new().unwrap()); - #[cfg(all(not(all(os_pipewire, feature = "pipewire")), os_alsa))] - return default_output_device_from(&alsa::AlsaDriver); - #[cfg(os_coreaudio)] - return default_output_device_from(&coreaudio::CoreAudioDriver); - #[cfg(os_wasapi)] - return default_output_device_from(&wasapi::WasapiDriver); -} +#[cfg(target_os = "windows")] +pub use interflow_wasapi as wasapi; diff --git a/src/backends/null/mod.rs b/src/backends/null/mod.rs new file mode 100644 index 0000000..397b7d8 --- /dev/null +++ b/src/backends/null/mod.rs @@ -0,0 +1,181 @@ +//! Null backend. Accepts any stream configuration, does nothing. + +use crate::prelude::*; +use interflow_core::collect; +use interflow_core::device::{ResolvedStreamConfig, StreamConfig}; +use interflow_core::stream::CallbackContext; +use std::borrow::Cow; +use std::convert::Infallible; +use std::num::NonZeroUsize; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread::JoinHandle; +use std::time::Instant; + +/// Platform type for the null backend. +pub struct Platform; + +impl ExtensionProvider for Platform { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl platform::Platform for Platform { + type Error = Infallible; + type Device = Device; + const NAME: &'static str = "Null"; + + fn default_device(&self, device_type: DeviceType) -> Result { + Ok(Device(device_type)) + } + + fn list_devices(&self) -> Result, Self::Error> { + Ok([Device(DeviceType::OUTPUT), Device(DeviceType::INPUT)]) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Device(pub DeviceType); + +impl ExtensionProvider for Device { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl device::Device for Device { + type Error = Infallible; + type StreamHandle = Stream; + + fn name(&self) -> Cow<'_, str> { + Cow::Borrowed("Null device") + } + + fn device_type(&self) -> DeviceType { + self.0 + } + + fn default_config(&self) -> Result { + Ok(StreamConfig { + exclusive: true, + sample_rate: 48000.0, + input_channels: 2, + output_channels: 2, + buffer_size_range: (None, None), + }) + } + + fn is_config_supported(&self, _: &StreamConfig) -> bool { + true + } + + fn create_stream( + &self, + stream_config: StreamConfig, + callback: Callback, + ) -> Result, Self::Error> { + Ok(Stream::new(stream_config, callback)) + } +} + +pub struct Stream { + thread_handle: JoinHandle, + abort_signal: Arc, +} + +impl ExtensionProvider for Stream { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl StreamHandle for Stream { + type Error = Infallible; + + fn eject(self) -> Result { + self.abort_signal.store(true, Ordering::Relaxed); + Ok(self.thread_handle.join().unwrap()) + } +} + +impl Stream { + fn new(stream_config: StreamConfig, mut callback: Callback) -> Self { + let abort_signal = Arc::new(AtomicBool::new(false)); + let thread_handle = std::thread::spawn({ + let abort_signal = abort_signal.clone(); + let frame_count = NonZeroUsize::new( + stream_config + .buffer_size_range + .1 + .or(stream_config.buffer_size_range.0) + .unwrap_or(512) + .max(1), + ) + .unwrap(); + let input_buffers = AudioBuffer::zeroed(frame_count, stream_config.input_channels); + let mut output_buffers = + AudioBuffer::zeroed(frame_count, stream_config.output_channels); + move || { + let stream_config = ResolvedStreamConfig { + sample_rate: stream_config.sample_rate, + input_channels: stream_config.input_channels, + output_channels: stream_config.output_channels, + max_frame_count: frame_count.get(), + }; + let mut context = CallbackContext { + stream_config: &stream_config, + timestamp: Timestamp::new(stream_config.sample_rate), + stream_proxy: &NullStreamProxy, + }; + callback.prepare(context); + let start = Instant::now(); + loop { + if abort_signal.load(Ordering::Relaxed) { + break callback; + } + + let audio_input = AudioInput { + timestamp: context.timestamp, + buffer: input_buffers.as_ref(), + channel_flags: &[], + }; + let mut audio_output = AudioOutput { + timestamp: context.timestamp, + buffer: output_buffers.as_mut(), + channel_flags: &[], + }; + callback.process_audio(context, &audio_input, &mut audio_output); + context.timestamp += stream_config.max_frame_count as u64; + while context.timestamp.as_duration() > start.elapsed() { + std::thread::yield_now(); + } + } + } + }); + Self { + thread_handle, + abort_signal, + } + } +} + +struct NullStreamProxy; + +impl ExtensionProvider for NullStreamProxy { + fn register<'a, 'sel>(&'a self, selector: &'sel mut Selector<'a>) -> &'sel mut Selector<'a> { + selector + } +} + +impl StreamProxy for NullStreamProxy {} + +#[scattered_collect::scatter(collect::REGISTRAR)] +static NULL_PLATFORM_REGISTRATION: collect::Registration = collect::Registration { + constructor: || { + log::warn!("Using null backend (you may have no sound when using `default_stream`)"); + Some(Rc::new(Platform)) + }, + priority: i32::MIN + 1, +}; diff --git a/src/backends/pipewire/device.rs b/src/backends/pipewire/device.rs index d9ef53b..762f2f5 100644 --- a/src/backends/pipewire/device.rs +++ b/src/backends/pipewire/device.rs @@ -2,10 +2,7 @@ use super::stream::StreamHandle; use crate::backends::pipewire::error::PipewireError; -use crate::{ - AudioDevice, AudioInputCallback, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, - Channel, DeviceType, SendEverywhereButOnWeb, StreamConfig, -}; +use crate::{AudioCallback, AudioDevice, Channel, DeviceType, StreamConfig}; use pipewire::context::Context; use pipewire::main_loop::MainLoop; use pipewire::properties::Properties; @@ -39,6 +36,8 @@ impl PipewireDevice { } impl AudioDevice for PipewireDevice { + type StreamHandle = StreamHandle; + type Error = PipewireError; fn name(&self) -> Cow<'_, str> { @@ -73,61 +72,33 @@ impl AudioDevice for PipewireDevice { } fn enumerate_configurations(&self) -> Option> { - Some([]) + None::<[StreamConfig; 0]> } -} - -impl AudioInputDevice for PipewireDevice { - type StreamHandle = StreamHandle; - fn default_input_config(&self) -> Result { + fn default_config(&self) -> Result { + let input_channels = if self.device_type.is_input() { 2 } else { 0 }; + let output_channels = if self.device_type.is_output() { 2 } else { 0 }; Ok(StreamConfig { - samplerate: 48000.0, - channels: 0b11, - exclusive: false, + sample_rate: 48000.0, + input_channels, + output_channels, buffer_size_range: (None, None), - }) - } - - fn create_input_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - StreamHandle::new_input( - self.object_serial.clone(), - &self.stream_name, - stream_config, - self.stream_properties.clone(), - callback, - ) - } -} - -impl AudioOutputDevice for PipewireDevice { - type StreamHandle = StreamHandle; - - fn default_output_config(&self) -> Result { - Ok(StreamConfig { - samplerate: 48000.0, - channels: 0b11, exclusive: false, - buffer_size_range: (None, None), }) } - fn create_output_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, ) -> Result, Self::Error> { - StreamHandle::new_output( - self.object_serial.clone(), - &self.stream_name, - stream_config, - self.stream_properties.clone(), - callback, - ) + if self.device_type.is_input() && !self.device_type.is_output() { + StreamHandle::new_input(&self.stream_name, stream_config, callback) + } else if self.device_type.is_output() { + StreamHandle::new_output(&self.stream_name, stream_config, callback) + } else { + Err(PipewireError::InvalidDeviceType) + } } } diff --git a/src/backends/pipewire/driver.rs b/src/backends/pipewire/driver.rs index 38f8410..94f56f6 100644 --- a/src/backends/pipewire/driver.rs +++ b/src/backends/pipewire/driver.rs @@ -10,6 +10,7 @@ use std::marker::PhantomData; /// The Interflow PipeWire backend driver. pub struct PipewireDriver { + // Needed to make this type unable to be constructed directly __init: PhantomData<()>, } diff --git a/src/backends/pipewire/error.rs b/src/backends/pipewire/error.rs index c46c197..4c64e15 100644 --- a/src/backends/pipewire/error.rs +++ b/src/backends/pipewire/error.rs @@ -11,4 +11,6 @@ pub enum PipewireError { /// Error creating a pipewire stream (SPA pod serialization problem). #[error("Cannot create Pipewire stream: {0}")] GenError(#[from] libspa::pod::serialize::GenError), + #[error("Device has invalid type")] + InvalidDeviceType, } diff --git a/src/backends/pipewire/filter.rs b/src/backends/pipewire/filter.rs new file mode 100644 index 0000000..8d3b0f3 --- /dev/null +++ b/src/backends/pipewire/filter.rs @@ -0,0 +1,18 @@ +use std::ptr::NonNull; + +use pipewire::properties::Properties; +use pipewire::{core::Core, sys::pw_filter}; + +pub struct Filter { + data: NonNull, +} + +impl Filter { + pub fn new(core: &Core, name: impl AsRef<[u8]>, properties: Properties) -> Self { + let core = core.as_raw(); + let name = { + let s = CString::new(name.as_ref()); + }; + let filter = unsafe { pw_filter_new(core) }; + } +} diff --git a/src/backends/pipewire/mod.rs b/src/backends/pipewire/mod.rs index 12d0c5b..e6e372f 100644 --- a/src/backends/pipewire/mod.rs +++ b/src/backends/pipewire/mod.rs @@ -8,5 +8,6 @@ pub mod device; pub mod driver; pub mod error; +mod filter; pub mod stream; mod utils; diff --git a/src/backends/pipewire/stream.rs b/src/backends/pipewire/stream.rs index 85e2d0b..e66dc0e 100644 --- a/src/backends/pipewire/stream.rs +++ b/src/backends/pipewire/stream.rs @@ -6,15 +6,17 @@ use crate::channel_map::Bitset; use crate::prelude::pipewire::utils::{BlackHole, CallbackHolder}; use crate::timestamp::Timestamp; use crate::{ - AudioCallbackContext, AudioInput, AudioInputCallback, AudioOutput, AudioOutputCallback, - AudioStreamHandle, StreamConfig, + audio_buffer::{AudioMut, AudioRef}, + ResolvedStreamConfig, +}; +use crate::{ + AudioCallback, AudioCallbackContext, AudioInput, AudioOutput, AudioStreamHandle, StreamConfig, }; use libspa::buffer::Data; use libspa::param::audio::{AudioFormat, AudioInfoRaw}; use libspa::pod::Pod; use libspa::utils::Direction; use libspa_sys::{SPA_PARAM_EnumFormat, SPA_TYPE_OBJECT_Format}; -use pipewire::context::Context; use pipewire::keys; use pipewire::main_loop::MainLoop; use pipewire::properties::Properties; @@ -41,42 +43,43 @@ impl fmt::Debug for StreamCommands { struct StreamInner { scratch_buffer: Box<[f32]>, callback: Weak>, - config: StreamConfig, + config: ResolvedStreamConfig, timestamp: Timestamp, } -impl StreamInner { - fn process_output(&mut self, channels: usize, buffer_size: usize) -> usize { - let buffer = AudioMut::from_interleaved_mut( - &mut self.scratch_buffer[..channels * buffer_size], +impl StreamInner { + fn process_output(&mut self, channels: usize, frames: usize) -> usize { + let buffer = AudioMut::from_noninterleaved_mut( + &mut self.scratch_buffer[..channels * frames], channels, ) .unwrap(); - if let Some(mut callback) = self.callback.upgrade() { - let context = AudioCallbackContext { - stream_config: self.config, - timestamp: self.timestamp, - }; - let num_frames = buffer.num_samples(); - let output = AudioOutput { - buffer, - timestamp: self.timestamp, - }; - - // SAFETY: there is max one other owner of the callback Arc, and it never dereferences - // it thanks to `BlackHole`, fulfilling safety requirements of `arc_get_mut_unchecked()`. - let callback = unsafe { arc_get_mut_unchecked(&mut callback) }; - callback.on_output_data(context, output); - - self.timestamp += num_frames as u64; - num_frames - } else { - 0 - } + let Some(mut callback) = self.callback.upgrade() else { + return 0; + }; + let context = AudioCallbackContext { + stream_config: self.config, + timestamp: self.timestamp, + }; + let num_frames = buffer.num_frames(); + let dummy_input = AudioInput { + timestamp: Timestamp::new(self.config.sample_rate), + buffer: AudioRef::empty(), + }; + let output = AudioOutput { + buffer, + timestamp: self.timestamp, + }; + // SAFETY: there is max one other owner of the callback Arc, and it never dereferences + // it thanks to `BlackHole`, fulfilling safety requirements of `arc_get_mut_unchecked()`. + let callback = unsafe { arc_get_mut_unchecked(&mut callback) }; + callback.process_audio(context, dummy_input, output); + self.timestamp += num_frames as u64; + num_frames } } -impl StreamInner { +impl StreamInner { fn process_input(&mut self, channels: usize, frames: usize) -> usize { let buffer = AudioRef::from_interleaved(&self.scratch_buffer[..channels * frames], channels) @@ -86,7 +89,7 @@ impl StreamInner { stream_config: self.config, timestamp: self.timestamp, }; - let num_frames = buffer.num_samples(); + let num_frames = buffer.num_frames(); let input = AudioInput { buffer, timestamp: self.timestamp, @@ -125,12 +128,27 @@ impl AudioStreamHandle for StreamHandle { } } -impl StreamHandle { +impl StreamHandle { fn create_stream( + name: String, + serial: Option, + config: StreamConfig, + callback: Callback, + ) -> Result { + let handle = std::thread::Builder::new() + .name(format!("{name} audio thread")) + .spawn(move || { + let main_loop = MainLoop::new(None)?; + let context = Context::new(&main_loop)?; + let core = context.connect(None)?; + Ok(todo!()) + }); + } + + fn create_stream_old( device_object_serial: Option, name: String, - mut config: StreamConfig, - user_properties: HashMap, Vec>, + config: StreamConfig, callback: Callback, direction: pipewire::spa::utils::Direction, process_frames: impl Fn(&mut [Data], &mut StreamInner, usize) -> usize @@ -146,7 +164,11 @@ impl StreamHandle { let context = Context::new(&main_loop)?; let core = context.connect(None)?; - let channels = config.channels.count(); + let channels = if direction == pipewire::spa::utils::Direction::Input { + config.input_channels + } else { + config.output_channels + }; let channels_str = channels.to_string(); let buffer_size = stream_buffer_size(config.buffer_size_range); @@ -155,6 +177,24 @@ impl StreamHandle { properties.insert(key, value); } + let input_channels = if direction == pipewire::spa::utils::Direction::Input { + channels + } else { + 0 + }; + let output_channels = if direction == pipewire::spa::utils::Direction::Output { + channels + } else { + 0 + }; + + let config = ResolvedStreamConfig { + sample_rate: config.sample_rate.round(), + input_channels, + output_channels, + max_frame_count, + }; + properties.insert(*keys::MEDIA_TYPE, "Audio"); properties.insert(*keys::MEDIA_ROLE, "Music"); properties.insert(*keys::MEDIA_CATEGORY, get_category(direction)); @@ -170,9 +210,15 @@ impl StreamHandle { let stream_inner = StreamInner { callback: Arc::downgrade(&callback_holder), - scratch_buffer: vec![0.0; MAX_FRAMES * channels].into_boxed_slice(), + scratch_buffer: { + log::debug!( + "StreamInner: allocating {} frames", + max_frame_count * channels + ); + vec![0.0; max_frame_count * channels].into_boxed_slice() + }, config, - timestamp: Timestamp::new(config.samplerate), + timestamp: Timestamp::new(config.sample_rate), }; // SAFETY of StreamInner::process_input(), StreamInner::process_output() depends on us @@ -209,7 +255,7 @@ impl StreamHandle { properties: { let mut info = AudioInfoRaw::new(); info.set_format(AudioFormat::F32LE); - info.set_rate(config.samplerate as u32); + info.set_rate(config.sample_rate as u32); info.set_channels(channels as u32); info.into() }, @@ -271,7 +317,7 @@ impl StreamHandle { } } -impl StreamHandle { +impl StreamHandle { /// Create an input Pipewire stream pub fn new_input( device_object_serial: Option, @@ -280,7 +326,7 @@ impl StreamHandle { properties: HashMap, Vec>, callback: Callback, ) -> Result { - Self::create_stream( + Self::create_stream_old( device_object_serial, name.to_string(), config, @@ -321,7 +367,7 @@ fn get_category(direction: pipewire::spa::utils::Direction) -> &'static str { } } -impl StreamHandle { +impl StreamHandle { /// Create an output Pipewire stream pub fn new_output( device_object_serial: Option, @@ -330,7 +376,7 @@ impl StreamHandle { properties: HashMap, Vec>, callback: Callback, ) -> Result { - Self::create_stream( + Self::create_stream_old( device_object_serial, name.to_string(), config, diff --git a/src/backends/wasapi/device.rs b/src/backends/wasapi/device.rs index ebc8d68..104672d 100644 --- a/src/backends/wasapi/device.rs +++ b/src/backends/wasapi/device.rs @@ -1,11 +1,8 @@ use super::{error, stream}; use crate::backends::wasapi::stream::WasapiStream; -use crate::channel_map::Bitset; +use crate::prelude::wasapi::stream::{FindSupportedConfig, StreamDirection}; use crate::prelude::wasapi::util::WasapiMMDevice; -use crate::{ - AudioDevice, AudioInputCallback, AudioInputDevice, AudioOutputCallback, AudioOutputDevice, - Channel, DeviceType, StreamConfig, -}; +use crate::{AudioCallback, AudioDevice, Channel, DeviceType, StreamConfig}; use std::borrow::Cow; use windows::Win32::Media::Audio; @@ -27,6 +24,7 @@ impl WasapiDevice { impl AudioDevice for WasapiDevice { type Error = error::WasapiError; + type StreamHandle = WasapiStream; fn name(&self) -> Cow { match self.device.name() { @@ -47,69 +45,50 @@ impl AudioDevice for WasapiDevice { } fn is_config_supported(&self, config: &StreamConfig) -> bool { - self.device_type.contains(DeviceType::OUTPUT) - && stream::is_output_config_supported(self.device.clone(), config) + if self.device_type.is_duplex() || !self.device_type.is_physical() { + return false; + } + FindSupportedConfig { + config, + device: &self.device, + is_output: self.device_type.is_output(), + } + .supported_config() + .is_some() } fn enumerate_configurations(&self) -> Option> { None::<[StreamConfig; 0]> } -} - -impl AudioInputDevice for WasapiDevice { - type StreamHandle = WasapiStream; - fn default_input_config(&self) -> Result { + fn default_config(&self) -> Result { let audio_client = self.device.activate::()?; let format = unsafe { audio_client.GetMixFormat()?.read_unaligned() }; let frame_size = unsafe { audio_client.GetBufferSize() } .map(|i| i as usize) .ok(); + let (input_channels, output_channels) = if self.device_type.is_input() { + (format.nChannels as _, 0) + } else { + (0, format.nChannels as _) + }; Ok(StreamConfig { - channels: 0u32.with_indices(0..format.nChannels as _), - exclusive: false, - samplerate: format.nSamplesPerSec as _, + sample_rate: format.nSamplesPerSec as _, + input_channels, + output_channels, buffer_size_range: (frame_size, frame_size), - }) - } - - fn create_input_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error> { - Ok(WasapiStream::new_input( - self.device.clone(), - stream_config, - callback, - )) - } -} - -impl AudioOutputDevice for WasapiDevice { - type StreamHandle = WasapiStream; - - fn default_output_config(&self) -> Result { - let audio_client = self.device.activate::()?; - let format = unsafe { audio_client.GetMixFormat()?.read_unaligned() }; - let frame_size = unsafe { audio_client.GetBufferSize() } - .map(|i| i as usize) - .ok(); - Ok(StreamConfig { - channels: 0u32.with_indices(0..format.nChannels as _), exclusive: false, - samplerate: format.nSamplesPerSec as _, - buffer_size_range: (frame_size, frame_size), }) } - fn create_output_stream( + fn create_stream( &self, stream_config: StreamConfig, callback: Callback, ) -> Result, Self::Error> { - Ok(WasapiStream::new_output( + Ok(WasapiStream::new( self.device.clone(), + StreamDirection::try_from(self.device_type)?, stream_config, callback, )) @@ -117,7 +96,7 @@ impl AudioOutputDevice for WasapiDevice { } /// An iterable collection WASAPI devices. -pub struct WasapiDeviceList { +pub(crate) struct WasapiDeviceList { pub(crate) collection: Audio::IMMDeviceCollection, pub(crate) total_count: u32, pub(crate) next_item: u32, diff --git a/src/backends/wasapi/driver.rs b/src/backends/wasapi/driver.rs index f505d1d..2fce33c 100644 --- a/src/backends/wasapi/driver.rs +++ b/src/backends/wasapi/driver.rs @@ -61,16 +61,34 @@ impl AudioDeviceEnumerator { &self, device_type: DeviceType, ) -> Result, error::WasapiError> { - let data_flow = bitflags_match!(device_type, { + let Some(flow) = bitflags_match!(device_type, { DeviceType::INPUT | DeviceType::PHYSICAL => Some(Audio::eCapture), DeviceType::OUTPUT | DeviceType::PHYSICAL => Some(Audio::eRender), _ => None, - }); + }) else { + return Ok(None); + }; - data_flow.map_or(Ok(None), |flow| unsafe { - let device = self.0.GetDefaultAudioEndpoint(flow, Audio::eConsole)?; - Ok(Some(WasapiDevice::new(device, device_type))) - }) + self.get_default_device_with_role(flow, Audio::eConsole) + .map(Some) + } + + fn get_default_device_with_role( + &self, + flow: Audio::EDataFlow, + role: Audio::ERole, + ) -> Result { + unsafe { + let device = self.0.GetDefaultAudioEndpoint(flow, role)?; + let device_type = match flow { + Audio::eRender => DeviceType::OUTPUT, + _ => DeviceType::INPUT, + }; + Ok(WasapiDevice::new( + device, + DeviceType::PHYSICAL | device_type, + )) + } } // Returns a chained iterator of output and input devices. diff --git a/src/backends/wasapi/error.rs b/src/backends/wasapi/error.rs index 964be9b..fc833a7 100644 --- a/src/backends/wasapi/error.rs +++ b/src/backends/wasapi/error.rs @@ -13,4 +13,7 @@ pub enum WasapiError { /// Windows Foundation error #[error("Win32 error: {0}")] FoundationError(String), + /// Duplex stream requested, unsupported by WASAPI + #[error("Unsupported duplex stream requested")] + DuplexStreamRequested, } diff --git a/src/backends/wasapi/stream.rs b/src/backends/wasapi/stream.rs deleted file mode 100644 index 815851e..0000000 --- a/src/backends/wasapi/stream.rs +++ /dev/null @@ -1,520 +0,0 @@ -use super::error; -use crate::audio_buffer::AudioMut; -use crate::backends::wasapi::util::WasapiMMDevice; -use crate::channel_map::Bitset; -use crate::prelude::{AudioRef, Timestamp}; -use crate::{ - AudioCallbackContext, AudioInput, AudioInputCallback, AudioOutput, AudioOutputCallback, - AudioStreamHandle, StreamConfig, -}; -use duplicate::duplicate_item; -use std::marker::PhantomData; -use std::ptr::NonNull; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::thread::JoinHandle; -use std::time::Duration; -use std::{ops, ptr, slice}; -use windows::core::imp::CoTaskMemFree; -use windows::core::Interface; -use windows::Win32::Foundation; -use windows::Win32::Foundation::{CloseHandle, HANDLE}; -use windows::Win32::Media::{Audio, KernelStreaming, Multimedia}; -use windows::Win32::System::Threading; - -type EjectSignal = Arc; - -#[duplicate_item( -name ty; -[AudioCaptureBuffer] [IAudioCaptureClient]; -[AudioRenderBuffer] [IAudioRenderClient]; -)] -struct name<'a, T> { - interface: &'a Audio::ty, - data: NonNull, - frame_size: usize, - channels: usize, - __type: PhantomData, -} - -#[duplicate_item( -name; -[AudioCaptureBuffer]; -[AudioRenderBuffer]; -)] -impl<'a, T> ops::Deref for name<'a, T> { - type Target = [T]; - - fn deref(&self) -> &Self::Target { - unsafe { slice::from_raw_parts(self.data.cast().as_ptr(), self.channels * self.frame_size) } - } -} - -#[duplicate_item( -name; -[AudioCaptureBuffer]; -[AudioRenderBuffer]; -)] -impl<'a, T> ops::DerefMut for name<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - unsafe { - slice::from_raw_parts_mut(self.data.cast().as_ptr(), self.channels * self.frame_size) - } - } -} - -impl Drop for AudioCaptureBuffer<'_, T> { - fn drop(&mut self) { - unsafe { self.interface.ReleaseBuffer(self.frame_size as _).unwrap() }; - } -} - -impl Drop for AudioRenderBuffer<'_, T> { - fn drop(&mut self) { - unsafe { - self.interface - .ReleaseBuffer(self.frame_size as _, 0) - .unwrap(); - } - } -} - -impl<'a, T> AudioRenderBuffer<'a, T> { - fn from_client( - render_client: &'a Audio::IAudioRenderClient, - channels: usize, - frame_size: usize, - ) -> Result { - let data = NonNull::new(unsafe { render_client.GetBuffer(frame_size as _) }?) - .expect("Audio buffer data is null"); - Ok(Self { - interface: render_client, - data, - frame_size, - channels, - __type: PhantomData, - }) - } -} -impl<'a, T> AudioCaptureBuffer<'a, T> { - fn from_client( - capture_client: &'a Audio::IAudioCaptureClient, - channels: usize, - ) -> Result, error::WasapiError> { - let mut buf_ptr = ptr::null_mut(); - let mut frame_size = 0; - let mut flags = 0; - unsafe { capture_client.GetBuffer(&mut buf_ptr, &mut frame_size, &mut flags, None, None) }?; - let Some(data) = NonNull::new(buf_ptr as _) else { - return Ok(None); - }; - Ok(Some(Self { - interface: capture_client, - data, - frame_size: frame_size as _, - channels, - __type: PhantomData, - })) - } -} - -struct AudioThread { - audio_client: Audio::IAudioClient, - interface: Interface, - audio_clock: Audio::IAudioClock, - stream_config: StreamConfig, - eject_signal: EjectSignal, - frame_size: usize, - callback: Callback, - event_handle: HANDLE, - clock_start: Duration, -} - -impl AudioThread { - fn finalize(self) -> Result { - if !self.event_handle.is_invalid() { - unsafe { CloseHandle(self.event_handle) }?; - } - let _ = unsafe { - self.audio_client - .Stop() - .inspect_err(|err| eprintln!("Cannot stop audio thread: {err}")) - }; - Ok(self.callback) - } -} - -impl AudioThread { - fn new( - device: WasapiMMDevice, - eject_signal: EjectSignal, - mut stream_config: StreamConfig, - callback: Callback, - ) -> Result { - unsafe { - let audio_client: Audio::IAudioClient = device.activate()?; - let sharemode = if stream_config.exclusive { - Audio::AUDCLNT_SHAREMODE_EXCLUSIVE - } else { - Audio::AUDCLNT_SHAREMODE_SHARED - }; - let format = { - let mut format = config_to_waveformatextensible(&stream_config); - let mut actual_format = ptr::null_mut(); - audio_client - .IsFormatSupported( - sharemode, - &format.Format, - (!stream_config.exclusive).then_some(&mut actual_format), - ) - .ok()?; - if !stream_config.exclusive { - assert!(!actual_format.is_null()); - format.Format = actual_format.read_unaligned(); - CoTaskMemFree(actual_format.cast()); - let sample_rate = format.Format.nSamplesPerSec; - stream_config.channels = 0u32.with_indices(0..format.Format.nChannels as _); - stream_config.samplerate = sample_rate as _; - } - format - }; - let frame_size = stream_config - .buffer_size_range - .0 - .or(stream_config.buffer_size_range.1); - let buffer_duration = frame_size - .map(|frame_size| { - buffer_size_to_duration(frame_size, stream_config.samplerate as _) - }) - .unwrap_or(0); - audio_client.Initialize( - sharemode, - Audio::AUDCLNT_STREAMFLAGS_EVENTCALLBACK - | Audio::AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, - buffer_duration, - 0, - &format.Format, - None, - )?; - let buffer_size = audio_client.GetBufferSize()? as usize; - let event_handle = { - let event_handle = - Threading::CreateEventA(None, false, false, windows::core::PCSTR(ptr::null()))?; - audio_client.SetEventHandle(event_handle)?; - event_handle - }; - let interface = audio_client.GetService::()?; - let audio_clock = audio_client.GetService::()?; - let frame_size = buffer_size; - Ok(Self { - audio_client, - interface, - audio_clock, - event_handle, - frame_size, - eject_signal, - stream_config: StreamConfig { - buffer_size_range: (Some(frame_size), Some(frame_size)), - ..stream_config - }, - clock_start: Duration::ZERO, - callback, - }) - } - } - - fn await_frame(&mut self) -> Result<(), error::WasapiError> { - let _ = unsafe { - let result = Threading::WaitForSingleObject(self.event_handle, Threading::INFINITE); - if result == Foundation::WAIT_FAILED { - let err = Foundation::GetLastError(); - let description = format!("Waiting for event handle failed: {:?}", err); - return Err(error::WasapiError::FoundationError(description)); - } - result - }; - Ok(()) - } - - fn output_timestamp(&self) -> Result { - let clock = stream_instant(&self.audio_clock)?; - let diff = clock - self.clock_start; - Ok(Timestamp::from_duration( - self.stream_config.samplerate, - diff, - )) - } -} - -impl AudioThread { - fn run(mut self) -> Result { - set_thread_priority(); - unsafe { - self.audio_client.Start()?; - } - self.clock_start = stream_instant(&self.audio_clock)?; - loop { - if self.eject_signal.load(Ordering::Relaxed) { - break self.finalize(); - } - self.await_frame()?; - self.process()?; - } - .inspect_err(|err| eprintln!("Render thread process error: {err}")) - } - - fn process(&mut self) -> Result<(), error::WasapiError> { - let frames_available = unsafe { self.interface.GetNextPacketSize()? as usize }; - if frames_available == 0 { - return Ok(()); - } - let Some(mut buffer) = AudioCaptureBuffer::::from_client( - &self.interface, - self.stream_config.channels.count(), - )? - else { - eprintln!("Null buffer from WASAPI"); - return Ok(()); - }; - let timestamp = self.output_timestamp()?; - let context = AudioCallbackContext { - stream_config: self.stream_config, - timestamp, - }; - let buffer = - AudioRef::from_interleaved(&mut buffer, self.stream_config.channels.count()).unwrap(); - let output = AudioInput { timestamp, buffer }; - self.callback.on_input_data(context, output); - Ok(()) - } -} - -impl AudioThread { - fn run(mut self) -> Result { - set_thread_priority(); - unsafe { - self.audio_client.Start()?; - } - self.clock_start = stream_instant(&self.audio_clock)?; - loop { - if self.eject_signal.load(Ordering::Relaxed) { - break self.finalize(); - } - self.await_frame()?; - self.process()?; - } - .inspect_err(|err| eprintln!("Render thread process error: {err}")) - } - - fn process(&mut self) -> Result<(), error::WasapiError> { - let frames_available = unsafe { - let padding = self.audio_client.GetCurrentPadding()? as usize; - self.frame_size - padding - }; - if frames_available == 0 { - return Ok(()); - } - let frames_requested = if let Some(max_frames) = self.stream_config.buffer_size_range.1 { - frames_available.min(max_frames) - } else { - frames_available - }; - let mut buffer = AudioRenderBuffer::::from_client( - &self.interface, - self.stream_config.channels.count(), - frames_requested, - )?; - let timestamp = self.output_timestamp()?; - let context = AudioCallbackContext { - stream_config: self.stream_config, - timestamp, - }; - let buffer = - AudioMut::from_interleaved_mut(&mut buffer, self.stream_config.channels.count()) - .unwrap(); - let output = AudioOutput { timestamp, buffer }; - self.callback.on_output_data(context, output); - Ok(()) - } -} - -/// Type representing a WASAPI audio stream. -pub struct WasapiStream { - join_handle: JoinHandle>, - eject_signal: EjectSignal, -} - -impl AudioStreamHandle for WasapiStream { - type Error = error::WasapiError; - - fn eject(self) -> Result { - self.eject_signal.store(true, Ordering::Relaxed); - self.join_handle - .join() - .expect("Audio output thread panicked") - } -} - -impl WasapiStream { - pub(crate) fn new_input( - device: WasapiMMDevice, - stream_config: StreamConfig, - callback: Callback, - ) -> Self { - let eject_signal = EjectSignal::default(); - let join_handle = std::thread::Builder::new() - .name("interflow_wasapi_output_stream".to_string()) - .spawn({ - let eject_signal = eject_signal.clone(); - move || { - let inner: AudioThread = - AudioThread::new(device, eject_signal, stream_config, callback) - .inspect_err(|err| { - eprintln!("Failed to create render thread: {err}") - })?; - inner.run() - } - }) - .expect("Cannot spawn audio output thread"); - Self { - join_handle, - eject_signal, - } - } -} - -impl WasapiStream { - pub(crate) fn new_output( - device: WasapiMMDevice, - stream_config: StreamConfig, - callback: Callback, - ) -> Self { - let eject_signal = EjectSignal::default(); - let join_handle = std::thread::Builder::new() - .name("interflow_wasapi_output_stream".to_string()) - .spawn({ - let eject_signal = eject_signal.clone(); - move || { - let inner: AudioThread = - AudioThread::new(device, eject_signal, stream_config, callback) - .inspect_err(|err| { - eprintln!("Failed to create render thread: {err}") - })?; - inner.run() - } - }) - .expect("Cannot spawn audio output thread"); - Self { - join_handle, - eject_signal, - } - } -} - -fn set_thread_priority() { - unsafe { - let thread_id = Threading::GetCurrentThreadId(); - - let _ = Threading::SetThreadPriority( - HANDLE(thread_id as isize as _), - Threading::THREAD_PRIORITY_TIME_CRITICAL, - ); - } -} - -pub fn buffer_size_to_duration(buffer_size: usize, sample_rate: u32) -> i64 { - (buffer_size as i64 / sample_rate as i64) * (1_000_000_000 / 100) -} - -fn stream_instant(audio_clock: &Audio::IAudioClock) -> Result { - let mut position: u64 = 0; - let mut qpc_position: u64 = 0; - unsafe { - audio_clock.GetPosition(&mut position, Some(&mut qpc_position))?; - }; - // The `qpc_position` is in 100 nanosecond units. Convert it to nanoseconds. - let qpc_nanos = qpc_position * 100; - let instant = Duration::from_nanos(qpc_nanos); - Ok(instant) -} - -pub(crate) fn config_to_waveformatextensible(config: &StreamConfig) -> Audio::WAVEFORMATEXTENSIBLE { - let format_tag = KernelStreaming::WAVE_FORMAT_EXTENSIBLE; - let channels = config.channels as u16; - let sample_rate = config.samplerate as u32; - let sample_bytes = size_of::() as u16; - let avg_bytes_per_sec = u32::from(channels) * sample_rate * u32::from(sample_bytes); - let block_align = channels * sample_bytes; - let bits_per_sample = 8 * sample_bytes; - - let cb_size = { - let extensible_size = size_of::(); - let ex_size = size_of::(); - (extensible_size - ex_size) as u16 - }; - - let waveformatex = Audio::WAVEFORMATEX { - wFormatTag: format_tag as u16, - nChannels: channels, - nSamplesPerSec: sample_rate, - nAvgBytesPerSec: avg_bytes_per_sec, - nBlockAlign: block_align, - wBitsPerSample: bits_per_sample, - cbSize: cb_size, - }; - - let channel_mask = KernelStreaming::KSAUDIO_SPEAKER_DIRECTOUT; - - let sub_format = Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; - - let waveformatextensible = Audio::WAVEFORMATEXTENSIBLE { - Format: waveformatex, - Samples: Audio::WAVEFORMATEXTENSIBLE_0 { - wSamplesPerBlock: bits_per_sample, - }, - dwChannelMask: channel_mask, - SubFormat: sub_format, - }; - - waveformatextensible -} - -pub(crate) fn is_output_config_supported( - device: WasapiMMDevice, - stream_config: &StreamConfig, -) -> bool { - let mut try_ = || unsafe { - let audio_client: Audio::IAudioClient = device.activate()?; - let sharemode = if stream_config.exclusive { - Audio::AUDCLNT_SHAREMODE_EXCLUSIVE - } else { - Audio::AUDCLNT_SHAREMODE_SHARED - }; - let mut format = config_to_waveformatextensible(&stream_config); - let mut actual_format = ptr::null_mut(); - audio_client - .IsFormatSupported( - sharemode, - &format.Format, - (!stream_config.exclusive).then_some(&mut actual_format), - ) - .ok()?; - if !stream_config.exclusive { - assert!(!actual_format.is_null()); - format.Format = actual_format.read_unaligned(); - CoTaskMemFree(actual_format.cast()); - let sample_rate = format.Format.nSamplesPerSec; - let new_channels = 0u32.with_indices(0..format.Format.nChannels as _); - let new_samplerate = sample_rate as f64; - if stream_config.samplerate != new_samplerate - || stream_config.channels.count() != new_channels.count() - { - return Ok(false); - } - } - Ok::<_, error::WasapiError>(true) - }; - try_() - .inspect_err(|err| eprintln!("Error while checking configuration is valid: {err}")) - .unwrap_or(false) -} diff --git a/src/backends/wasapi/util.rs b/src/backends/wasapi/util.rs index a0744fc..64056f9 100644 --- a/src/backends/wasapi/util.rs +++ b/src/backends/wasapi/util.rs @@ -1,12 +1,14 @@ use crate::prelude::wasapi::error; use std::ffi::OsString; use std::marker::PhantomData; +use std::ops; use std::os::windows::ffi::OsStringExt; -use windows::core::Interface; +use std::ptr::{self, NonNull}; +use windows::core::{Interface, HRESULT}; use windows::Win32::Devices::Properties; use windows::Win32::Foundation::RPC_E_CHANGED_MODE; use windows::Win32::Media::Audio; -use windows::Win32::System::Com; +use windows::Win32::System::Com::{self, CoTaskMemFree}; use windows::Win32::System::Com::{ CoInitializeEx, CoUninitialize, StructuredStorage, COINIT_APARTMENTTHREADED, STGM_READ, }; @@ -130,3 +132,43 @@ fn get_device_name(device: &Audio::IMMDevice) -> Option { Some(name) } } + +#[repr(transparent)] +pub(super) struct CoTaskOwned { + ptr: ptr::NonNull, +} + +impl ops::Deref for CoTaskOwned { + type Target = ptr::NonNull; + fn deref(&self) -> &Self::Target { + &self.ptr + } +} + +impl ops::DerefMut for CoTaskOwned { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ptr + } +} + +impl Drop for CoTaskOwned { + fn drop(&mut self) { + unsafe { + CoTaskMemFree(Some(self.ptr.as_ptr().cast())); + } + } +} + +impl CoTaskOwned { + pub(super) const unsafe fn new(ptr: NonNull) -> Self { + Self { ptr } + } + + pub(super) unsafe fn construct(func: impl FnOnce(*mut *mut T) -> bool) -> Option { + let mut ptr = ptr::null_mut(); + if !func(&mut ptr) { + return None; + } + ptr::NonNull::new(ptr).map(|ptr| Self { ptr }) + } +} diff --git a/src/channel_map.rs b/src/channel_map.rs deleted file mode 100644 index 46dfbd2..0000000 --- a/src/channel_map.rs +++ /dev/null @@ -1,223 +0,0 @@ -//! This module provides functionality for working with bitsets and channel mapping. -//! -//! A bitset is a data structure that efficiently stores a set of boolean values using bits. -//! Each bit represents a boolean state (true/false) for a specific index or channel. -//! -//! The module includes: -//! - Generic `Bitset` trait for types that can represent sets of boolean values -//! - `CreateBitset` trait for constructing bitsets from indices -//! - Implementations for standard unsigned integer types (u8, u16, u32, u64, u128) -//! - Slice-based implementation for working with arrays of bitsets -//! - Type aliases for common channel map sizes (32, 64, and 128 bits) -//! -//! # Example -//! -//! ``` -//! use interflow::channel_map::Bitset; -//! -//! let mut map = 0u32; -//! map.set_index(0, true); -//! map.set_index(5, true); -//! assert!(map.get_index(0)); -//! assert!(map.get_index(5)); -//! assert!(!map.get_index(1)); -//! ``` - -use core::panic; - -/// Trait for types which can represent bitsets. -/// -/// A bit set is a type which encodes a boolean value, functioning similarly in principle to a -/// `HashSet`. -pub trait Bitset: Sized { - /// Return the capacity of this bitset, that is, how many indices can be used with this type. - fn capacity(&self) -> usize; - - /// Get the value for a specific index. Implementations should panic when this value is out - /// of range. - fn get_index(&self, index: usize) -> bool; - - /// Sets the value for a specific index. Implementations should panic when this value is out - /// of range. - fn set_index(&mut self, index: usize, value: bool); - - /// Returns an iterator of indices for which the value has been set `true`. - fn indices(&self) -> impl IntoIterator { - (0..self.capacity()).filter(|i| self.get_index(*i)) - } - /// Count the number of `true` elements in this bit set. - fn count(&self) -> usize { - self.indices().into_iter().count() - } - - /// Builder-like method for setting a value at a specific index. - fn with_index(&mut self, index: usize, value: bool) -> &mut Self { - self.set_index(index, value); - self - } - /// Builder-like method for setting all provided indices to `. - fn with_indices(mut self, indices: impl IntoIterator) -> Self { - for ix in indices { - self.set_index(ix, true); - } - self - } -} - -/// Trait for bitsets that can be created from indices -pub trait CreateBitset: Bitset { - /// Create a [`Self`] from the given indices - /// - /// # Arguments - /// - /// - `indices`: [`IntoIterator`] implementation that returns [`usize`] values corresponding to the indices to - /// set in the bitset. - fn from_indices(indices: impl IntoIterator) -> Self; -} - -#[duplicate::duplicate_item( - ty; - [u8]; - [u16]; - [u32]; - [u64]; - [u128]; -)] -impl Bitset for ty { - fn capacity(&self) -> usize { - ty::BITS as usize - } - - fn get_index(&self, index: usize) -> bool { - let mask = 1 << index; - self & mask > 0 - } - - fn set_index(&mut self, index: usize, value: bool) { - let mask = 1 << index; - if value { - *self |= mask; - } else { - *self &= !mask; - } - } - - fn count(&self) -> usize { - self.count_ones() as _ - } -} - -#[duplicate::duplicate_item( - ty; - [u8]; - [u16]; - [u32]; - [u64]; - [u128]; -)] -impl CreateBitset for ty { - fn from_indices(indices: impl IntoIterator) -> Self { - indices - .into_iter() - .inspect(|x| assert!(*x < Self::BITS as usize, "Index out of range")) - .fold(0, |acc, ix| acc | (1 << ix)) - } -} - -fn get_inner_bitset_at(arr: &[T], mut index: usize) -> Option<(usize, usize)> { - arr.iter().enumerate().find_map({ - move |(i, b)| match index.checked_sub(b.capacity()) { - None => Some((i, index)), - Some(v) => { - index = v; - None - } - } - }) -} - -impl Bitset for &mut [T] { - fn capacity(&self) -> usize { - self.iter().map(|b| b.capacity()).sum() - } - - fn get_index(&self, index: usize) -> bool { - let Some((bitset_index, inner_index)) = get_inner_bitset_at(self, index) else { - return false; - }; - self[bitset_index].get_index(inner_index) - } - - fn set_index(&mut self, index: usize, value: bool) { - let Some((bitset_index, inner_index)) = get_inner_bitset_at(self, index) else { - panic!("Index {index} outside of range {}", self.capacity()); - }; - self[bitset_index].set_index(inner_index, value); - } -} - -/// Type alias for a bitset with a capacity of 32 slots. -pub type ChannelMap32 = u32; -/// Type alias for a bitset with a capacity of 64 slots. -pub type ChannelMap64 = u64; -/// Type alias for a bitset with a capacity of 128 slots. -pub type ChannelMap128 = u128; - -#[cfg(test)] -mod test { - use std::collections::HashSet; - use std::hash::RandomState; - - use super::*; - - #[test] - fn test_getset_index() { - let mut bitset = 0u8; - bitset.set_index(0, true); - bitset.set_index(2, true); - bitset.set_index(3, true); - bitset.set_index(2, false); - - assert_eq!(0b1001, bitset); - assert!(bitset.get_index(0)); - assert!(bitset.get_index(3)); - assert!(!bitset.get_index(2)); - } - - #[test] - fn test_from_indices() { - let bitset = u8::from_indices([0, 2, 3]); - assert_eq!(0b1101, bitset); - } - - #[test] - fn test_indices() { - let bitset = 0b10010100u8; - let result = HashSet::<_, RandomState>::from_iter(bitset.indices()); - assert_eq!(HashSet::from_iter([2, 4, 7]), result); - } - - #[test] - fn test_slice_getset() { - let mut storage = [0; 3]; - let mut bitset: &mut [u32] = &mut storage; - - bitset.set_index(0, true); - bitset.set_index(34, true); - bitset.set_index(81, true); - - assert_eq!([0b1, 0b100, 1 << (81 - 64)], bitset); - - assert!(bitset.get_index(0)); - assert!(bitset.get_index(34)); - assert!(bitset.get_index(81)); - } - - #[test] - fn test_slice_indices() { - let mut storage = [0b100101u8, (1 << 6) | (1 << 4), 1]; - let bitrate: &mut [u8] = &mut storage; - let result = HashSet::<_, RandomState>::from_iter(bitrate.indices()); - assert_eq!(HashSet::from_iter([0, 2, 5, 12, 14, 16]), result); - } -} diff --git a/src/duplex.rs b/src/duplex.rs index 027d111..c60bfdd 100644 --- a/src/duplex.rs +++ b/src/duplex.rs @@ -1,37 +1,21 @@ //! Module for simultaneous input/output audio processing //! -//! This module includes a proxy for gathering an input audio stream, and optionally process it to resample it to the +//! This module includes a proxy for gathering an input audio stream and optionally processing it to resample it to the //! output sample rate. use crate::audio_buffer::AudioRef; -use crate::channel_map::Bitset; +use crate::timestamp::{AtomicTimestamp, Timestamp}; use crate::{ - AudioCallbackContext, AudioDevice, AudioInput, AudioInputCallback, AudioInputDevice, - AudioOutput, AudioOutputCallback, AudioOutputDevice, AudioStreamHandle, SendEverywhereButOnWeb, + AudioCallback, AudioCallbackContext, AudioDevice, AudioInput, AudioOutput, AudioStreamHandle, StreamConfig, }; use fixed_resample::{PushStatus, ReadStatus, ResamplingChannelConfig}; -use std::error::Error; use std::num::NonZeroUsize; +use std::ops::IndexMut; +use std::sync::Arc; use thiserror::Error; const MAX_CHANNELS: usize = 64; -/// Trait of types that can process both input and output audio streams at the same time. -pub trait AudioDuplexCallback: 'static + SendEverywhereButOnWeb { - /// Processes audio data in a duplex stream. - /// - /// # Arguments - /// * `context` - The context containing stream configuration and timing information - /// * `input` - The input audio buffer containing captured audio data - /// * `output` - The output audio buffer to be filled with processed audio data - fn on_audio_data( - &mut self, - context: AudioCallbackContext, - input: AudioInput, - output: AudioOutput, - ); -} - /// Type which handles both a duplex stream handle. pub struct DuplexStream { _input_stream: Box>, @@ -41,6 +25,8 @@ pub struct DuplexStream { /// Input proxy for transferring an input signal to a separate output callback to be processed as a duplex stream. pub struct InputProxy { producer: Option>, + timestamp: Arc, + scratch_buffer: Option>, // TODO: switch to non-interleaved processing receive_output_samplerate: rtrb::Consumer, send_consumer: rtrb::Producer>, } @@ -58,60 +44,83 @@ impl InputProxy { ( Self { producer: None, + timestamp: Arc::new(Timestamp::new(0.0).into()), receive_output_samplerate, + scratch_buffer: None, send_consumer, }, produce_output_samplerate, receive_consumer, ) } + + fn change_output_samplerate( + &mut self, + context: AudioCallbackContext, + output_samplerate: u32, + ) -> bool { + let Some(num_channels) = NonZeroUsize::new(context.stream_config.input_channels) else { + log::error!("Input proxy: no input channels given"); + return true; + }; + let input_samplerate = context.stream_config.sample_rate as _; + log::debug!( + "Creating resampling channel ({} Hz) -> ({} Hz) ({} channels)", + input_samplerate, + output_samplerate, + num_channels.get() + ); + let (tx, rx) = fixed_resample::resampling_channel( + num_channels, + input_samplerate, + output_samplerate, + ResamplingChannelConfig { + latency_seconds: 0.01, + quality: fixed_resample::ResampleQuality::Low, + ..Default::default() + }, + ); + self.producer.replace(tx); + match self.send_consumer.push(rx) { + Ok(_) => { + log::debug!( + "Input proxy: resampling channel ({} Hz) sent", + context.stream_config.sample_rate + ); + } + Err(err) => { + log::error!("Input proxy: cannot send resampling channel: {}", err); + } + } + false + } } -impl AudioInputCallback for InputProxy { - /// Processes incoming audio data and stores it in the internal buffer. - /// - /// Handles sample rate conversion between input and output streams. - /// - /// # Arguments - /// * `context` - The context containing stream configuration and timing information - /// * `input` - The input audio buffer containing captured audio data - fn on_input_data(&mut self, context: AudioCallbackContext, input: AudioInput) { - log::trace!(num_samples = input.buffer.num_samples(), num_channels = input.buffer.num_channels(); - "on_input_data"); +impl AudioCallback for InputProxy { + fn prepare(&mut self, context: AudioCallbackContext) { + let len = context.stream_config.input_channels * context.stream_config.max_frame_count; + self.scratch_buffer = Some(Box::from_iter(std::iter::repeat_n(0.0, len))); + self.timestamp + .update(Timestamp::new(context.stream_config.sample_rate)); + } + + fn process_audio( + &mut self, + context: AudioCallbackContext, + input: AudioInput, + output: AudioOutput, + ) { + debug_assert_eq!( + 0, + output.buffer.num_channels(), + "Input proxy should not be receiving audio output data" + ); + log::trace!(num_samples = input.buffer.num_frames(), num_channels = input.buffer.num_channels(); + "InputProxy::process_audio"); + if let Ok(output_samplerate) = self.receive_output_samplerate.pop() { - let Some(num_channels) = NonZeroUsize::new(context.stream_config.channels.count()) - else { - log::error!("Input proxy: no input channels given"); + if self.change_output_samplerate(context, output_samplerate) { return; - }; - let input_samplerate = context.stream_config.samplerate as _; - log::debug!( - "Creating resampling channel ({} Hz) -> ({} Hz) ({} channels)", - input_samplerate, - output_samplerate, - num_channels.get() - ); - let (tx, rx) = fixed_resample::resampling_channel( - num_channels, - input_samplerate, - output_samplerate, - ResamplingChannelConfig { - latency_seconds: 0.01, - quality: fixed_resample::ResampleQuality::Low, - ..Default::default() - }, - ); - self.producer.replace(tx); - match self.send_consumer.push(rx) { - Ok(_) => { - log::debug!( - "Input proxy: resampling channel ({} Hz) sent", - context.stream_config.samplerate - ); - } - Err(err) => { - log::error!("Input proxy: cannot send resampling channel: {}", err); - } } } let Some(producer) = &mut self.producer else { @@ -119,22 +128,25 @@ impl AudioInputCallback for InputProxy { return; }; - let mut scratch = [0f32; 32 * MAX_CHANNELS]; - for slice in input.buffer.chunks(32) { - let len = slice.num_samples() * slice.num_channels(); - debug_assert!( - slice.copy_into_interleaved(&mut scratch[..len]), - "Cannot fail: len is computed from slice itself" - ); - match producer.push_interleaved(&scratch[..len]) { - PushStatus::OverflowOccurred { .. } => { - log::error!("Input proxy: overflow occurred"); - } - PushStatus::UnderflowCorrected { .. } => { - log::error!("Input proxy: underflow corrected"); - } - _ => {} + let scratch = self + .scratch_buffer + .as_mut() + .unwrap() + .index_mut(0..input.buffer.num_frames()); + let len = input.buffer.num_frames() * input.buffer.num_channels(); + debug_assert!( + input.buffer.copy_into_interleaved(scratch), + "Cannot fail: len is computed from slice itself" + ); + self.timestamp.add_frames(input.buffer.num_frames() as _); + match producer.push_interleaved(&scratch[..len]) { + PushStatus::OverflowOccurred { .. } => { + log::error!("Input proxy: overflow occurred"); } + PushStatus::UnderflowCorrected { .. } => { + log::error!("Input proxy: underflow corrected"); + } + _ => {} } } } @@ -150,8 +162,6 @@ pub enum DuplexCallbackError { InputError(InputError), /// An error occurred in the output stream OutputError(OutputError), - /// An error that doesn't fit into other categories - Other(Box), } /// [`AudioOutputCallback`] implementation for which runs the provided [`AudioDuplexCallback`]. @@ -160,8 +170,9 @@ pub struct DuplexCallback { receive_consumer: rtrb::Consumer>, send_samplerate: rtrb::Producer, callback: Callback, - storage_raw: Box<[f32]>, + storage_raw: Option>, current_samplerate: u32, + input_timestamp: Arc, num_input_channels: usize, resample_config: ResamplingChannelConfig, } @@ -171,15 +182,36 @@ impl DuplexCallback { /// /// # Returns /// The wrapped callback instance or an error if extraction fails - pub fn into_inner(self) -> Result> { - Ok(self.callback) + pub fn into_inner(self) -> Callback { + self.callback } } -impl AudioOutputCallback for DuplexCallback { - fn on_output_data(&mut self, context: AudioCallbackContext, output: AudioOutput) { - // If changed, send new output samplerate to input proxy - let samplerate = context.stream_config.samplerate as u32; +impl AudioCallback for DuplexCallback { + fn prepare(&mut self, context: AudioCallbackContext) { + log::debug!("Prepare duplex callback {:#?}", context.stream_config); + let len = context.stream_config.output_channels * context.stream_config.max_frame_count; + log::debug!("Create storage space for {len} elements"); + self.storage_raw = Some(Box::from_iter(std::iter::repeat_n(0.0, len))); + self.callback.prepare(context); + } + + fn process_audio( + &mut self, + context: AudioCallbackContext, + input: AudioInput, + output: AudioOutput, + ) { + debug_assert_eq!( + 0, + input.buffer.num_channels(), + "DuplexCallback should not be receiving audio input data" + ); + log::trace!(num_samples = output.buffer.num_frames(), num_channels = output.buffer.num_channels(); + "DuplexCallback::process_audio"); + + // If changed, send the new output samplerate to input proxy + let samplerate = context.stream_config.sample_rate as u32; if samplerate != self.current_samplerate && self.send_samplerate.push(samplerate).is_ok() { log::debug!("Output samplerate changed to {}", samplerate); self.current_samplerate = samplerate; @@ -196,11 +228,12 @@ impl AudioOutputCallback for DuplexCallback { log::error!("Output resample channel underflow occurred"); @@ -212,15 +245,19 @@ impl AudioOutputCallback for DuplexCallback AudioOutputCallback for DuplexCallback` -/// * `OutputHandle` - The type of the output stream handle, must implement `AudioStreamHandle>` +/// * `InputHandle` - The type of the input stream handle must implement `AudioStreamHandle` +/// * `OutputHandle` - The type of the output stream handle must implement `AudioStreamHandle>` /// /// # Example /// /// ```no_run -/// use interflow::duplex::AudioDuplexCallback; /// use interflow::prelude::*; /// /// let input_device = default_input_device(); /// let output_device = default_output_device(); -/// let input_config = input_device.default_input_config().unwrap(); -/// let output_config = output_device.default_output_config().unwrap(); /// /// struct MyCallback; /// @@ -252,18 +286,20 @@ impl AudioOutputCallback for DuplexCallback Self { Self } /// } /// -/// impl AudioDuplexCallback for MyCallback { -/// fn on_audio_data(&mut self, context: AudioCallbackContext, input: AudioInput, output: AudioOutput) { +/// impl AudioCallback for MyCallback { +/// fn prepare(&mut self, context: AudioCallbackContext) {} +/// fn process_audio(&mut self, context: AudioCallbackContext, input: AudioInput, output: AudioOutput) { /// // Implementation left as an exercise to the reader /// } /// } /// /// // Create and use a duplex stream +/// let config = output_device.default_config().unwrap(); /// let stream_handle = create_duplex_stream( /// input_device, /// output_device, /// MyCallback::new(), -/// DuplexStreamConfig::new(input_config, output_config), +/// DuplexStreamConfig::new(config), /// ).expect("Failed to create duplex stream"); /// /// // Later, stop the stream and retrieve the callback @@ -296,9 +332,7 @@ impl< .output_handle .eject() .map_err(DuplexCallbackError::OutputError)?; - duplex_callback - .into_inner() - .map_err(DuplexCallbackError::Other) + Ok(duplex_callback.into_inner()) } } @@ -306,22 +340,35 @@ impl< #[derive(Debug, Copy, Clone)] pub struct DuplexStreamConfig { /// Input stream configuration - pub input: StreamConfig, - /// Output stream configuration - pub output: StreamConfig, - /// Use high quality resampling. Increases latency and CPU usage. + pub stream_config: StreamConfig, + /// Use high-quality resampling. Increases latency and CPU usage. pub high_quality_resampling: bool, /// Target latency. May be higher if the resampling takes too much latency. pub target_latency_secs: f32, } +impl DuplexStreamConfig { + pub(crate) fn input_config(&self) -> StreamConfig { + StreamConfig { + output_channels: 0, + ..self.stream_config + } + } + + pub(crate) fn output_config(&self) -> StreamConfig { + StreamConfig { + input_channels: 0, + ..self.stream_config + } + } +} + impl DuplexStreamConfig { /// Create a new duplex stream config with the provided input and output stream configuration, and default /// resampler values. - pub fn new(input: StreamConfig, output: StreamConfig) -> Self { + pub fn new(stream_config: StreamConfig) -> Self { Self { - input, - output, + stream_config, high_quality_resampling: false, target_latency_secs: 0.01, } @@ -331,8 +378,8 @@ impl DuplexStreamConfig { /// Type alias of the result of creating a duplex stream. pub type DuplexStreamResult = Result< DuplexStreamHandle< - ::StreamHandle, - ::StreamHandle>, + ::StreamHandle, + ::StreamHandle>, >, DuplexCallbackError<::Error, ::Error>, >; @@ -360,7 +407,6 @@ pub type DuplexStreamResult = Result< /// # Example /// /// ```no_run -/// use interflow::duplex::AudioDuplexCallback; /// use interflow::prelude::*; /// /// struct MyCallback; @@ -371,32 +417,32 @@ pub type DuplexStreamResult = Result< /// } /// } /// -/// impl AudioDuplexCallback for MyCallback { -/// fn on_audio_data(&mut self, context: AudioCallbackContext, input: AudioInput, output: AudioOutput) { -/// // Implementation left as exercise to the reader +/// impl AudioCallback for MyCallback { +/// fn prepare(&mut self, context: AudioCallbackContext) {} +/// fn process_audio(&mut self, context: AudioCallbackContext, input: AudioInput, output: AudioOutput) { +/// // Implementation left as an exercise to the reader /// } /// } /// /// let input_device = default_input_device(); /// let output_device = default_output_device(); -/// let input_config = input_device.default_input_config().unwrap(); -/// let output_config = output_device.default_output_config().unwrap(); /// /// let callback = MyCallback::new(); /// +/// let config = output_device.default_config().unwrap(); /// let duplex_stream = create_duplex_stream( /// input_device, /// output_device, /// callback, -/// DuplexStreamConfig::new(input_config, output_config), +/// DuplexStreamConfig::new(config), /// ).expect("Failed to create duplex stream"); /// /// ``` #[allow(clippy::type_complexity)] // Allowing because moving to a type alias would be just as complex pub fn create_duplex_stream< - InputDevice: AudioInputDevice, - OutputDevice: AudioOutputDevice, - Callback: AudioDuplexCallback, + InputDevice: AudioDevice, + OutputDevice: AudioDevice, + Callback: 'static + AudioCallback, >( input_device: InputDevice, output_device: OutputDevice, @@ -410,20 +456,22 @@ pub fn create_duplex_stream< DuplexCallbackError, > { let (proxy, send_samplerate, receive_consumer) = InputProxy::new(); + let input_timestamp = proxy.timestamp.clone(); let input_handle = input_device - .create_input_stream(config.input, proxy) + .create_stream(config.input_config(), proxy) .map_err(DuplexCallbackError::InputError)?; let output_handle = output_device - .create_output_stream( - config.output, + .create_stream( + config.output_config(), DuplexCallback { input: None, send_samplerate, + input_timestamp, receive_consumer, callback, - storage_raw: vec![0f32; 8192 * MAX_CHANNELS].into_boxed_slice(), + storage_raw: None, current_samplerate: 0, - num_input_channels: config.input.channels.count(), + num_input_channels: config.stream_config.input_channels, resample_config: ResamplingChannelConfig { capacity_seconds: (2.0 * config.target_latency_secs as f64).max(0.5), latency_seconds: config.target_latency_secs as f64, diff --git a/src/lib.rs b/src/lib.rs index 6a07515..91ec842 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,312 +1,46 @@ #![doc = include_str!("../README.md")] #![warn(missing_docs)] -use bitflags::bitflags; -use std::borrow::Cow; +use anyhow::Context; +use core::{stream, DeviceType}; +pub use interflow_core as core; +use interflow_core::proxies::CreateStreamExt; -use crate::audio_buffer::{AudioMut, AudioRef}; -use crate::channel_map::ChannelMap32; -use crate::timestamp::Timestamp; - -pub mod audio_buffer; pub mod backends; -pub mod channel_map; -pub mod duplex; -pub mod prelude; -pub mod timestamp; - -bitflags! { - /// Represents the types/capabilities of an audio device. - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct DeviceType: u32 { - /// Device supports audio input. - const INPUT = 1 << 0; - - /// Device supports audio output. - const OUTPUT = 1 << 1; - - /// Physical audio device (hardware). - const PHYSICAL = 1 << 2; - - /// Virtual/software application device. - const APPLICATION = 1 << 3; - - /// This device is set as default - const DEFAULT = 1 << 4; - - /// Device that supports both input and output. - const DUPLEX = Self::INPUT.bits() | Self::OUTPUT.bits(); - } -} - -/// Audio drivers provide access to the inputs and outputs of devices. -/// Several drivers might provide the same accesses, some sharing it with other applications, -/// while others work in exclusive mode. -pub trait AudioDriver { - /// Type of errors that can happen when using this audio driver. - type Error: std::error::Error; - /// Type of audio devices this driver provides. - type Device: AudioDevice; - - /// Driver display name. - const DISPLAY_NAME: &'static str; - - /// Runtime version of the audio driver. If there is a difference between "client" and - /// "server" versions, then this should reflect the server version. - fn version(&self) -> Result, Self::Error>; - - /// Default device of the given type. This is most often tied to the audio settings at the - /// operating system level. - fn default_device(&self, device_type: DeviceType) -> Result, Self::Error>; - - /// List all devices available through this audio driver. - fn list_devices(&self) -> Result, Self::Error>; -} - -impl DeviceType { - /// Returns true if this device type has the input capability. - pub fn is_input(&self) -> bool { - self.contains(Self::INPUT) - } - - /// Returns true if this device type has the output capability. - pub fn is_output(&self) -> bool { - self.contains(Self::OUTPUT) - } - - /// Returns true if this device type is a physical device. - pub fn is_physical(&self) -> bool { - self.contains(Self::PHYSICAL) - } - - /// Returns true if this device type is an application/virtual device. - pub fn is_application(&self) -> bool { - self.contains(Self::APPLICATION) - } - - /// Returns true if this device is set as default - pub fn is_default(&self) -> bool { - self.contains(Self::DEFAULT) - } - - /// Returns true if this device type supports both input and output. - pub fn is_duplex(&self) -> bool { - self.contains(Self::DUPLEX) - } -} - -/// Configuration for an audio stream. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct StreamConfig { - /// Configured sample rate of the requested stream. The opened stream can have a different - /// sample rate, so don't rely on this parameter being correct at runtime. - pub samplerate: f64, - /// Map of channels requested by the stream. Entries correspond in order to - /// [AudioDevice::channel_map]. - /// - /// Some drivers allow specifying which channels are going to be opened and available through - /// the audio buffers. For other drivers, only the number of requested channels is used, and - /// order does not matter. - pub channels: ChannelMap32, - /// Range of preferential buffer sizes, in units of audio samples per channel. - /// The library will make a best-effort attempt at honoring this setting, and in future versions - /// may provide additional buffering to ensure it, but for now you should not make assumptions - /// on buffer sizes based on this setting. - pub buffer_size_range: (Option, Option), - /// Whether the device should be exclusively held (meaning no other application can open the - /// same device). - pub exclusive: bool, -} - -/// Audio channel description. -#[derive(Debug, Clone)] -pub struct Channel<'a> { - /// Index of the channel in the device - pub index: usize, - /// Display name for the channel, if available, else a generic name like "Channel 1" - pub name: Cow<'a, str>, -} - -/// Trait for types describing audio devices. Audio devices have zero or more inputs and outputs, -/// and depending on the driver, can be duplex devices which can provide both of them at the same -/// time natively. -pub trait AudioDevice { - /// Type of errors that can happen when using this device. - type Error: std::error::Error; - - /// Device display name - fn name(&self) -> Cow<'_, str>; - - /// Device type. Either input, output, or duplex. - fn device_type(&self) -> DeviceType; - - /// Iterator of the available channels in this device. Channel indices are used when - /// specifying which channels to open when creating an audio stream. - fn channel_map(&self) -> impl IntoIterator>; - - /// Not all configuration values make sense for a particular device, and this method tests a - /// configuration to see if it can be used in an audio stream. - fn is_config_supported(&self, config: &StreamConfig) -> bool; - - /// Enumerate all possible configurations this device supports. If that is not provided by - /// the device, and not easily generated manually, this will return `None`. - fn enumerate_configurations(&self) -> Option>; - - /// Returns the supported I/O buffer size range for the device. - fn buffer_size_range(&self) -> Result<(Option, Option), Self::Error> { - Ok((None, None)) - } -} - -/// Marker trait for values which are [Send] everywhere but on the web (as WASM does not yet have -/// web targets. -/// -/// This should only be used to define the traits and should not be relied upon in external code. -/// -/// This definition is selected on non-web platforms, and does require [`Send`]. -#[cfg(not(wasm))] -pub trait SendEverywhereButOnWeb: 'static + Send {} -#[cfg(not(wasm))] -impl SendEverywhereButOnWeb for T {} - -/// Marker trait for values which are [Send] everywhere but on the web (as WASM does not yet have -/// web targets. -/// -/// This should only be used to define the traits and should not be relied upon in external code. -/// -/// This definition is selected on web platforms, and does not require [`Send`]. -#[cfg(wasm)] -pub trait SendEverywhereButOnWeb {} -#[cfg(wasm)] -impl SendEverywhereButOnWeb for T {} - -/// Trait for types which can provide input streams. -/// -/// Input devices require a [`AudioInputCallback`] which receives the audio data from the input -/// device, and processes it. -pub trait AudioInputDevice: AudioDevice { - /// Type of the resulting stream. This stream can be used to control the audio processing - /// externally, or stop it completely and give back ownership of the callback with - /// [`AudioStreamHandle::eject`]. - type StreamHandle: AudioStreamHandle; - - /// Return the default configuration for this device, if there is one. The returned configuration *must* be - /// valid according to [`Self::is_config_supported`]. - fn default_input_config(&self) -> Result; - - /// Creates an input stream with the provided stream configuration. For this call to be - /// valid, [`AudioDevice::is_config_supported`] should have returned `true` on the provided - /// configuration. - /// - /// An input callback is required to process the audio, whose ownership will be transferred - /// to the audio stream. - fn create_input_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error>; - - /// Create an input stream with the default configuration (as returned by [`Self::default_input_config`]). - /// - /// # Arguments - /// - /// - `callback`: Callback to process the audio input - fn default_input_stream( - &self, - callback: Callback, - ) -> Result, Self::Error> { - self.create_input_stream(self.default_input_config()?, callback) - } -} - -/// Trait for types which can provide output streams. -/// -/// Output devices require a [`AudioOutputCallback`] which receives the audio data from the output -/// device, and processes it. -pub trait AudioOutputDevice: AudioDevice { - /// Type of the resulting stream. This stream can be used to control the audio processing - /// externally, or stop it completely and give back ownership of the callback with - /// [`AudioStreamHandle::eject`]. - type StreamHandle: AudioStreamHandle; - - /// Return the default output configuration for this device, if it exists - fn default_output_config(&self) -> Result; - - /// Creates an output stream with the provided stream configuration. For this call to be - /// valid, [`AudioDevice::is_config_supported`] should have returned `true` on the provided - /// configuration. - /// - /// An output callback is required to process the audio, whose ownership will be transferred - /// to the audio stream. - fn create_output_stream( - &self, - stream_config: StreamConfig, - callback: Callback, - ) -> Result, Self::Error>; - - /// Create an output stream using the default configuration as returned by [`Self::default_output_config`]. - /// - /// # Arguments - /// - /// - `callback`: Output callback to generate audio data with. - fn default_output_stream( - &self, - callback: Callback, - ) -> Result, Self::Error> { - self.create_output_stream(self.default_output_config()?, callback) - } -} - -/// Trait for types which handles an audio stream (input or output). -pub trait AudioStreamHandle { - /// Type of errors which have caused the stream to fail. - type Error: std::error::Error; - - /// Eject the stream, returning ownership of the callback. - /// - /// An error can occur when an irrecoverable error has occured and ownership has been lost - /// already. - fn eject(self) -> Result; -} - -#[duplicate::duplicate_item( - name bufty; - [AudioInput] [AudioRef < 'a, T >]; - [AudioOutput] [AudioMut < 'a, T >]; -)] -/// Plain-old-data object holding references to the audio buffer and the associated time-keeping -/// [`Timestamp`]. This timestamp is associated with the stream, and in the cases where the -/// driver provides timing information, it is used instead of relying on sample-counting. -pub struct name<'a, T> { - /// Associated time stamp for this callback. The time represents the duration for which the - /// stream has been opened, and is either provided by the driver if available, or is kept up - /// manually by the library. - pub timestamp: Timestamp, - /// Audio buffer data. - pub buffer: bufty, -} - -/// Plain-old-data object holding the passed-in stream configuration, as well as a general -/// callback timestamp, which can be different from the input and output streams in case of -/// cross-stream latencies; differences in timing can indicate desync. -pub struct AudioCallbackContext { - /// Passed-in stream configuration. Values have been updated where necessary to correspond to - /// the actual stream properties. - pub stream_config: StreamConfig, - /// Callback-wide timestamp. - pub timestamp: Timestamp, -} - -/// Trait of types which process input audio data. This is the trait that users will want to -/// implement when processing an input device. -pub trait AudioInputCallback { - /// Callback called when input data is available to be processed. - fn on_input_data(&mut self, context: AudioCallbackContext, input: AudioInput); -} -/// Trait of types which process output audio data. This is the trait that users will want to -/// implement when processing an output device. -pub trait AudioOutputCallback { - /// Callback called when output data is available to be processed. - fn on_output_data(&mut self, context: AudioCallbackContext, input: AudioOutput); +/// Prelude module. Import all with `interflow::prelude::*`. +pub mod prelude { + pub use super::{default_device, default_platform, default_stream}; + pub use interflow_core::prelude::*; + #[cfg(any(target_os = "macos", target_os = "ios"))] + pub use interflow_coreaudio::prelude as coreaudio; + #[cfg(target_os = "windows")] + pub use interflow_wasapi::prelude as wasapi; +} + +/// Return the default platform. +/// The platform is selected automatically based on your available and enabled backends. +#[allow(unreachable_code)] +pub fn default_platform() -> core::proxies::DynPlatform { + let platform = core::collect::construct_platform().expect("FATAL: no platforms available"); + log::info!("Using platform {}", platform.name()); + platform +} + +/// Return the default device, using the default platform as returned by [`default_platform`]. +pub fn default_device(device_type: DeviceType) -> anyhow::Result { + let device = default_platform() + .default_device(device_type) + .context("Cannot get default device")?; + log::info!("Using device {}", device.name()); + Ok(device) +} + +/// Create a stream using the default device as returned by [`default_device`]. +pub fn default_stream( + device_type: DeviceType, + callback: Callback, +) -> anyhow::Result> { + let device = default_device(device_type)?; + Ok(device.default_stream(device_type, callback)?) } diff --git a/src/prelude.rs b/src/prelude.rs deleted file mode 100644 index 1588298..0000000 --- a/src/prelude.rs +++ /dev/null @@ -1,10 +0,0 @@ -#![allow(unused)] -//! Prelude module for `interflow`. Use as a star-import. - -#[cfg(os_wasapi)] -pub use crate::backends::wasapi::prelude::*; -pub use crate::backends::*; -pub use crate::duplex::{ - create_duplex_stream, AudioDuplexCallback, DuplexStreamConfig, DuplexStreamHandle, -}; -pub use crate::*;