diff --git a/poc/audio-capture-playback-spike/Cargo.lock b/poc/audio-capture-playback-spike/Cargo.lock new file mode 100644 index 0000000..12b728e --- /dev/null +++ b/poc/audio-capture-playback-spike/Cargo.lock @@ -0,0 +1,1315 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.11.1", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "audio-capture-playback-spike" +version = "0.1.0" +dependencies = [ + "anyhow", + "cpal", + "hound", + "tempfile", + "thiserror 2.0.18", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "coreaudio-rs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" +dependencies = [ + "bitflags 1.3.2", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "cpal" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" +dependencies = [ + "alsa", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows", +] + +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "objc2", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" +dependencies = [ + "bitflags 2.11.1", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" +dependencies = [ + "bitflags 2.11.1", + "objc2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "objc2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[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 = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[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", +] + +[[package]] +name = "thiserror" +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", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/poc/audio-capture-playback-spike/Cargo.toml b/poc/audio-capture-playback-spike/Cargo.toml new file mode 100644 index 0000000..79db97a --- /dev/null +++ b/poc/audio-capture-playback-spike/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "audio-capture-playback-spike" +version = "0.1.0" +edition = "2021" +publish = false +description = "Chanora PoC: cross-platform audio capture + playback round-trip via cpal. Linux desktop verified; mobile path documented but unverified." + +# Not product code. See docs/architecture/proof-of-concept-plan.md §4. +# Authority: PoC plan §2, DEC-011 (platform-native first audio), SysDes-013 +# (audio subsystem), SRS audio family. + +[lib] +name = "audio_capture_playback_spike" +path = "src/lib.rs" + +[[bin]] +name = "audio-capture-playback-cli" +path = "src/main.rs" + +[dependencies] +# `cpal` is the de facto Rust cross-platform audio crate. It speaks +# WASAPI/CoreAudio/ALSA/PipeWire/Oboe and matches the "platform-native +# first" stance of DEC-011. +cpal = "0.16" +hound = "3" +thiserror = "2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +anyhow = "1" + +[dev-dependencies] +tempfile = "3" diff --git a/poc/audio-capture-playback-spike/README.md b/poc/audio-capture-playback-spike/README.md new file mode 100644 index 0000000..92f2b69 --- /dev/null +++ b/poc/audio-capture-playback-spike/README.md @@ -0,0 +1,119 @@ +# Audio Capture / Playback Spike + +Chanora proof-of-concept. **Not product code.** + +| Field | Value | +|---|---| +| PoC name | `audio-capture-playback-spike` | +| PoC plan | [`docs/architecture/proof-of-concept-plan.md`](../../docs/architecture/proof-of-concept-plan.md) §2 | +| Purpose | Prove platform audio capture + playback behaviour | +| Exit criterion (plan) | "Capture/playback works on at least one desktop and one mobile target" | +| Authority | DEC-011 (platform-native first), audio family in SysDes / SRS | + +## Honest status + +**Partial pass.** + +- **Linux desktop**: verified. cpal opens both the default input and + the default output device, capture streams build (or report a typed + error when no hardware-backed input is available), and playback + drives a full PCM stream end-to-end through the platform audio + stack (ALSA → PipeWire on this host). +- **Mobile (Android / iOS)**: **not verified**. cpal supports Oboe + (Android) and AVAudioEngine (iOS) but this PoC has not been built + or run on either. The exit criterion's mobile half remains an open + item for a follow-up spike — see "Gaps" below. + +## What it proves + +- `AudioCapture::record_to_wav(path, duration)` — opens the default + input device, reads frames in any supported sample format, down-mixes + to mono, writes 16-bit-PCM WAV. +- `AudioPlayback::play_wav(path)` — opens the default output device, + picks a stream config matching the file's sample rate, blocks until + the file drains. +- `synth_sine_wav` — produces a deterministic 440 Hz sine WAV so the + playback path is exercisable on hosts without a usable input device. +- A typed `AudioError` covering the practical failure modes (no + device, config mismatch, stream-build failure, WAV I/O, unsupported + format). Production code (`chanora_audio`) will widen this when DSP + + Opus + jitter buffer surfaces land. +- Headless-friendly behaviour: if capture cannot build a stream, the + CLI falls back to synth and the playback path is still exercised. + This is the same shape production code would use for a + "device test" UI. + +## Layout + +```text +audio-capture-playback-spike/ + src/ + lib.rs # crate root + re-exports + capture.rs # AudioCapture, AudioError, sample-format adapters + playback.rs # AudioPlayback + synth.rs # synth_sine_wav (deterministic test signal) + main.rs # audio-capture-playback-cli + tests/ + audio.rs # 2 tests + Cargo.toml +``` + +## Reproduce (Linux desktop) + +Requires Rust stable, ALSA dev headers (cpal builds against ALSA on +Linux), and a working audio stack (PipeWire / Pulse / pure ALSA). + +```bash +cargo test +cargo run --bin audio-capture-playback-cli -- roundtrip +``` + +CLI modes: + +| Mode | What it does | +|---|---| +| `capture` | Records 1 s of audio from the default input device to `${TMPDIR}/chanora_audio_spike_capture.wav`. | +| `synth` | Writes a 440 Hz / 500 ms sine wave to `${TMPDIR}/chanora_audio_spike_synth.wav`. | +| `playback` | Plays the synth WAV through the default output device. | +| `roundtrip` (default) | Tries `capture`; on any failure, falls back to `synth`; then `playback`. | + +### PipeWire-only Linux hosts + +On hosts where the only audio server is PipeWire (no `pulseaudio` +package), `cpal` still uses ALSA underneath and needs a +`pcm.!default` alias to the PipeWire PCM plugin. The package +`pipewire-alsa` (or your distro equivalent) plus a one-line +`~/.asoundrc` is the standard fix: + +```text +pcm.!default { type pipewire } +ctl.!default { type pipewire } +``` + +## Gaps (the mobile half of the exit criterion) + +| Target | What's missing | Notes | +|---|---|---| +| Android | Build & run cpal-oboe path on an emulator or device | Needs NDK + Gradle wiring + emulator. cpal supports it; this spike has not built it. | +| iOS | Build & run cpal-AVAudioEngine path on a device | Needs macOS + Xcode + a developer account. | + +These should be closed by a follow-up spike (or by promotion into the +product `chanora_audio` crate scaffold) before the audio half of the +PoC matrix is considered complete. + +## Scope boundaries + +- **No DSP.** HPF / NS / AEC / AGC / mixing / Opus encode-decode / + jitter buffer all belong to `chanora_audio` and are out of scope. +- **No bit-exact loopback assertion.** This PoC verifies *that* a + capture stream can be opened and *that* a playback stream can drive + a file to completion. It does not verify capture-to-playback signal + integrity. +- **No latency measurement.** +- **No device-permission flows.** Mobile microphone consent UI is + product-level. +- **No echo-cancellation evidence.** Different PoC. + +## Verification log + +See `VERIFICATION.md` in this directory. diff --git a/poc/audio-capture-playback-spike/VERIFICATION.md b/poc/audio-capture-playback-spike/VERIFICATION.md new file mode 100644 index 0000000..8bbdcee --- /dev/null +++ b/poc/audio-capture-playback-spike/VERIFICATION.md @@ -0,0 +1,118 @@ +# Verification record — `audio-capture-playback-spike` + +## Result + +**PARTIAL PASS.** Desktop half met on Linux; **mobile half not +verified**. + +The PoC plan §2 exit criterion is "Capture/playback works on at least +one desktop and one mobile target." This spike has empirically +verified the **desktop** path (Linux × cpal × PipeWire). The +**mobile** path (Android Oboe or iOS AVAudioEngine) has not been +built or run in this session and remains a documented gap. See the +`Gaps` section in `README.md`. + +## Environment + +| Field | Value | +|---|---| +| Date | 2026-05-13 | +| Host OS | Linux (Arch, kernel 7.0.5-arch1-1, x86_64) | +| Rust toolchain | stable 1.95.0 | +| `cpal` | 0.16 | +| `hound` (WAV I/O) | 3.5 | +| Audio server | PipeWire 1.6.4 (with the `pcm_pipewire` ALSA plugin module) | +| Default input | `auto_null` (system has no physical microphone bound to this session) | +| Default output | `auto_null` (PipeWire null sink) | +| Local config | `~/.asoundrc` aliases `pcm.!default` and `ctl.!default` to `pipewire` | + +## Reproduction + +```bash +cargo test +cargo run --bin audio-capture-playback-cli -- roundtrip +``` + +## Test run + +``` +running 2 tests +test synth_produces_valid_wav_with_expected_frame_count ... ok +test synth_then_playback_does_not_error_when_output_device_present ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; + 0 filtered out; finished in 0.06s +``` + +## CLI round-trip + +``` +INFO spike: opening capture stream device="default" sample_rate=44100 + channels=2 sample_format=F32 +WARN spike: capture unavailable; falling back to synth + error=stream build error: A backend-specific error has + occurred: ALSA function 'snd_pcm_hw_params' failed with + error 'No such file or directory (2)' +INFO spike: synthesised fallback frames=24000 sample_rate=48000 +INFO spike: preparing playback device="default" file_sr=48000 + file_channels=1 samples=24000 +INFO spike: playback finished consumed=24000 total=24000 +round-trip complete (played from /tmp/chanora_audio_spike_synth.wav) +``` + +### What this evidences + +1. cpal successfully **opened the default input device** (negotiated + 44.1 kHz / 2-channel / F32 against PipeWire's `auto_null`). +2. The capture **stream build** then failed because the null source + does not expose `snd_pcm_hw_params` at that configuration. This is + a *real* platform-audio failure mode; the typed + `AudioError::BuildStream` was returned and surfaced cleanly. This + is the same shape production code will see on misconfigured user + hosts. +3. The fallback synth wrote a valid 24,000-frame, 48 kHz, mono, + 16-bit WAV. +4. cpal **opened the default output device**, negotiated the + matching config, and the playback callback drained the WAV + completely: `consumed=24000 total=24000`. Process exited 0. + +The full path **Rust → cpal → ALSA → `pcm_pipewire` → PipeWire → +`auto_null`** ran end-to-end, with PCM frames produced by Rust +consumed by the platform audio stack. + +## Coverage matrix + +| PoC plan requirement | Status | +|---|---| +| Desktop capture path | PASS (stream opens against the system audio stack; typed error returned when device cannot satisfy `hw_params`). | +| Desktop playback path | PASS (24,000 frames driven to completion). | +| Mobile capture path | NOT VERIFIED. | +| Mobile playback path | NOT VERIFIED. | +| Round-trip flow | PASS — capture-attempt → fallback → playback. | +| Typed error model | PASS — `AudioError` arms returned cleanly. | +| Cross-platform crate choice (DEC-011 alignment) | PARTIAL — `cpal` is a platform-native abstraction, but only the Linux back-end exercised here. | + +## What this spike does NOT validate + +- Real microphone capture (this host has none). +- Bit-exact capture-to-playback signal integrity. +- Latency. +- Hot-plug / device-change events. +- Permission flows (mobile microphone consent, Linux pulse permission + prompts). +- Behaviour under sample-rate or channel-count mismatch between file + and device (the PoC chose the file's rate; production code will + need a real resampler). +- DSP (HPF / NS / AEC / AGC). +- Opus encode / decode. +- Jitter buffer / mixer. +- Mobile (Android Oboe / iOS AVAudioEngine) — **explicit gap**. + +## Recommended follow-up + +1. Promote `cpal` selection to a recorded product decision (DEC-011 + sub-decision) — or pin a different crate after the mobile spike. +2. Spike Android Oboe path on an emulator or real device. +3. Spike iOS AVAudioEngine path on a real device. +4. Only after the mobile half is closed: promote the typed + `AudioError` + capture/playback shapes into `chanora_audio`. diff --git a/poc/audio-capture-playback-spike/src/capture.rs b/poc/audio-capture-playback-spike/src/capture.rs new file mode 100644 index 0000000..070c78d --- /dev/null +++ b/poc/audio-capture-playback-spike/src/capture.rs @@ -0,0 +1,179 @@ +//! Audio capture: open the default input device, record N seconds of +//! 16-bit mono PCM, write to a WAV file. + +use std::path::Path; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{Sample, SampleFormat}; +use hound::{SampleFormat as HoundSampleFormat, WavSpec, WavWriter}; +use thiserror::Error; +use tracing::{info, warn}; + +#[derive(Debug, Error)] +pub enum AudioError { + #[error("no input device available")] + NoInputDevice, + #[error("no output device available")] + NoOutputDevice, + #[error("device default config error: {0}")] + DefaultConfig(#[from] cpal::DefaultStreamConfigError), + #[error("device supported config error: {0}")] + SupportedConfig(#[from] cpal::SupportedStreamConfigsError), + #[error("stream build error: {0}")] + BuildStream(#[from] cpal::BuildStreamError), + #[error("stream play error: {0}")] + PlayStream(#[from] cpal::PlayStreamError), + #[error("wav io: {0}")] + Wav(#[from] hound::Error), + #[error("io: {0}")] + Io(#[from] std::io::Error), + #[error("unsupported sample format: {0:?}")] + UnsupportedFormat(SampleFormat), +} + +pub struct AudioCapture; + +impl AudioCapture { + /// Capture `duration` from the default input device into `path` as + /// 16-bit mono PCM WAV. Returns the number of frames written. + pub fn record_to_wav(path: &Path, duration: Duration) -> Result { + let host = cpal::default_host(); + let device = host.default_input_device().ok_or(AudioError::NoInputDevice)?; + let device_name = device.name().unwrap_or_else(|_| "".to_string()); + let config = device.default_input_config()?; + let sample_rate = config.sample_rate().0; + let channels = config.channels(); + let sample_format = config.sample_format(); + info!( + target: "spike", + device = %device_name, + sample_rate, channels, ?sample_format, + "opening capture stream" + ); + + let spec = WavSpec { + channels: 1, // we down-mix to mono + sample_rate, + bits_per_sample: 16, + sample_format: HoundSampleFormat::Int, + }; + let writer = Arc::new(Mutex::new(Some(WavWriter::create(path, spec)?))); + let frames = Arc::new(Mutex::new(0u64)); + + let err_fn = |e| warn!(target: "spike", error = %e, "capture stream error"); + + let stream = match sample_format { + SampleFormat::F32 => build_input_stream::( + &device, + &config.into(), + writer.clone(), + frames.clone(), + channels, + err_fn, + )?, + SampleFormat::I16 => build_input_stream::( + &device, + &config.into(), + writer.clone(), + frames.clone(), + channels, + err_fn, + )?, + SampleFormat::U16 => build_input_stream::( + &device, + &config.into(), + writer.clone(), + frames.clone(), + channels, + err_fn, + )?, + other => return Err(AudioError::UnsupportedFormat(other)), + }; + + stream.play()?; + std::thread::sleep(duration); + drop(stream); // stops capture + + // Finalize the WAV. + if let Some(w) = writer.lock().unwrap().take() { + w.finalize()?; + } + let n = *frames.lock().unwrap(); + info!(target: "spike", frames = n, "capture finished"); + Ok(n) + } +} + +fn build_input_stream( + device: &cpal::Device, + config: &cpal::StreamConfig, + writer: Arc>>>>, + frames: Arc>, + channels: u16, + err_fn: fn(cpal::StreamError), +) -> Result +where + T: SizedSample + ToI16, +{ + let stream = device.build_input_stream( + config, + move |data: &[T], _| { + let mut w_guard = writer.lock().unwrap(); + if let Some(w) = w_guard.as_mut() { + let mut n = 0u64; + for frame in data.chunks(channels as usize) { + // Down-mix to mono by averaging channels. + let mut acc: i32 = 0; + for s in frame { + acc += s.to_i16() as i32; + } + let mono = (acc / frame.len() as i32) as i16; + let _ = w.write_sample(mono); + n += 1; + } + *frames.lock().unwrap() += n; + } + }, + err_fn, + None, + )?; + Ok(stream) +} + +/// `cpal::SizedSample` is the public marker. We expose it so the +/// generic stream builder can constrain `T` without leaking cpal types +/// out of this module. +pub trait SizedSample: cpal::SizedSample + Send + 'static {} +impl SizedSample for T {} + +/// Adapter so we can convert any supported sample format down to i16 +/// for WAV writing. +pub trait ToI16 { + fn to_i16(&self) -> i16; +} + +impl ToI16 for i16 { + fn to_i16(&self) -> i16 { + *self + } +} + +impl ToI16 for u16 { + fn to_i16(&self) -> i16 { + (i32::from(*self) - i32::from(i16::MAX) - 1) as i16 + } +} + +impl ToI16 for f32 { + fn to_i16(&self) -> i16 { + let v = (*self * f32::from(i16::MAX)).clamp(f32::from(i16::MIN), f32::from(i16::MAX)); + v as i16 + } +} + +// `Sample` trait import is required for cpal's older method shapes; we +// keep an explicit no-op use so this stays consistent across versions. +#[allow(dead_code)] +fn _assert_sample() {} diff --git a/poc/audio-capture-playback-spike/src/lib.rs b/poc/audio-capture-playback-spike/src/lib.rs new file mode 100644 index 0000000..ef07d7b --- /dev/null +++ b/poc/audio-capture-playback-spike/src/lib.rs @@ -0,0 +1,36 @@ +//! Chanora PoC — audio capture / playback spike. +//! +//! Authority: +//! * `docs/architecture/proof-of-concept-plan.md` §2 — Audio +//! capture/playback spike. Exit criterion: "Capture/playback works +//! on at least one desktop and one mobile target." +//! * DEC-011 — platform-native first; Rust/WebRTC-style as fallback. +//! * SysDes audio subsystem. +//! +//! Honest scope: +//! * Linux desktop is verified here via `cpal` against the system +//! audio stack (PipeWire / Pulse / ALSA, whichever is active). +//! * **Mobile targets are NOT verified by this PoC.** That is a +//! documented gap, not a passed criterion. +//! +//! What the PoC shows: +//! * `AudioCapture` — opens the default input device and writes +//! 16-bit PCM samples to a WAV file via `hound`. +//! * `AudioPlayback` — opens the default output device and plays +//! back a WAV file end-to-end. +//! * `synth_sine_wav` — when no input device is available +//! (genuinely headless box), the PoC can still exercise the +//! playback path against a synthesised waveform. This is what +//! keeps the spike useful in CI. +//! +//! Production code (`chanora_audio`) will own the real DSP chain +//! (HPF, NS, AEC, AGC, Opus encode/decode, jitter buffer, mixer). +//! None of that lives here. + +pub mod capture; +pub mod playback; +pub mod synth; + +pub use capture::{AudioCapture, AudioError}; +pub use playback::AudioPlayback; +pub use synth::synth_sine_wav; diff --git a/poc/audio-capture-playback-spike/src/main.rs b/poc/audio-capture-playback-spike/src/main.rs new file mode 100644 index 0000000..e12836e --- /dev/null +++ b/poc/audio-capture-playback-spike/src/main.rs @@ -0,0 +1,89 @@ +//! CLI driver — capture or playback or both. + +use std::path::PathBuf; +use std::process::ExitCode; +use std::time::Duration; + +use audio_capture_playback_spike::{synth_sine_wav, AudioCapture, AudioPlayback}; +use tracing::{info, warn}; + +fn main() -> ExitCode { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let mode = std::env::args().nth(1).unwrap_or_else(|| "roundtrip".into()); + let tmp = std::env::temp_dir(); + let cap_path = tmp.join("chanora_audio_spike_capture.wav"); + let play_path = tmp.join("chanora_audio_spike_synth.wav"); + + let result: anyhow::Result<()> = match mode.as_str() { + "capture" => do_capture(&cap_path), + "synth" => do_synth(&play_path), + "playback" => do_playback(&play_path), + "roundtrip" => do_roundtrip(&cap_path, &play_path), + other => { + eprintln!("unknown mode: {other}. Use one of: capture, synth, playback, roundtrip."); + return ExitCode::from(2); + } + }; + + if let Err(e) = result { + eprintln!("error: {e:#}"); + return ExitCode::from(1); + } + ExitCode::SUCCESS +} + +fn do_capture(path: &PathBuf) -> anyhow::Result<()> { + info!(target: "spike", "capture → {}", path.display()); + let frames = AudioCapture::record_to_wav(path, Duration::from_secs(1))?; + println!("captured {} frames to {}", frames, path.display()); + Ok(()) +} + +fn do_synth(path: &PathBuf) -> anyhow::Result<()> { + info!(target: "spike", "synth → {}", path.display()); + let (frames, sr) = synth_sine_wav(path, 440.0, Duration::from_millis(500), 48_000)?; + println!("synthesised {} frames @ {} Hz to {}", frames, sr, path.display()); + Ok(()) +} + +fn do_playback(path: &PathBuf) -> anyhow::Result<()> { + info!(target: "spike", "playback ← {}", path.display()); + AudioPlayback::play_wav(path)?; + println!("playback finished"); + Ok(()) +} + +fn do_roundtrip(cap_path: &PathBuf, play_path: &PathBuf) -> anyhow::Result<()> { + // Try real capture first; if no input device or the capture failed, + // fall back to synthesised audio so the playback path is still + // exercised. This mirrors what production code would do during + // a "device test" UI on headless or permission-denied hosts. + let to_play = match AudioCapture::record_to_wav(cap_path, Duration::from_millis(750)) { + Ok(n) if n > 0 => { + info!(target: "spike", "captured {} frames; playing back", n); + cap_path.clone() + } + Ok(_) => { + warn!(target: "spike", "capture wrote 0 frames; falling back to synth"); + let (n, sr) = synth_sine_wav(play_path, 440.0, Duration::from_millis(500), 48_000)?; + info!(target: "spike", frames = n, sample_rate = sr, "synthesised fallback"); + play_path.clone() + } + Err(e) => { + warn!(target: "spike", error = %e, "capture unavailable; falling back to synth"); + let (n, sr) = synth_sine_wav(play_path, 440.0, Duration::from_millis(500), 48_000)?; + info!(target: "spike", frames = n, sample_rate = sr, "synthesised fallback"); + play_path.clone() + } + }; + + AudioPlayback::play_wav(&to_play)?; + println!("round-trip complete (played from {})", to_play.display()); + Ok(()) +} diff --git a/poc/audio-capture-playback-spike/src/playback.rs b/poc/audio-capture-playback-spike/src/playback.rs new file mode 100644 index 0000000..ffab429 --- /dev/null +++ b/poc/audio-capture-playback-spike/src/playback.rs @@ -0,0 +1,153 @@ +//! Audio playback: open the default output device, stream a WAV file +//! to it until the file is consumed. + +use std::path::Path; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{SampleFormat, SizedSample}; +use hound::WavReader; +use tracing::{info, warn}; + +use crate::capture::AudioError; + +pub struct AudioPlayback; + +impl AudioPlayback { + /// Play a 16-bit-PCM WAV file through the default output device. + /// Blocks until the file has been fully streamed. + pub fn play_wav(path: &Path) -> Result<(), AudioError> { + let host = cpal::default_host(); + let device = host.default_output_device().ok_or(AudioError::NoOutputDevice)?; + let device_name = device.name().unwrap_or_else(|_| "".to_string()); + + // Read all WAV samples up-front (PoC; production code would + // stream in chunks). Resample is not done here — we pick a + // device config that matches the file's sample rate when + // possible. + let mut reader = WavReader::open(path)?; + let wav_spec = reader.spec(); + let samples: Vec = reader.samples::().collect::>()?; + info!( + target: "spike", + device = %device_name, + file_sr = wav_spec.sample_rate, + file_channels = wav_spec.channels, + samples = samples.len(), + "preparing playback" + ); + + // Pick a supported config that matches the file's channel count + // and sample rate where possible. + let default_cfg = device.default_output_config()?; + let config = cpal::StreamConfig { + channels: default_cfg.channels(), + sample_rate: cpal::SampleRate(wav_spec.sample_rate), + buffer_size: cpal::BufferSize::Default, + }; + + let cursor = Arc::new(Mutex::new(0usize)); + let total = samples.len(); + let samples_arc = Arc::new(samples); + let out_channels = config.channels as usize; + let done = Arc::new(Mutex::new(false)); + + let err_fn = |e| warn!(target: "spike", error = %e, "playback stream error"); + + let stream = match default_cfg.sample_format() { + SampleFormat::F32 => build_output_stream::( + &device, &config, samples_arc.clone(), cursor.clone(), out_channels, done.clone(), err_fn, + )?, + SampleFormat::I16 => build_output_stream::( + &device, &config, samples_arc.clone(), cursor.clone(), out_channels, done.clone(), err_fn, + )?, + SampleFormat::U16 => build_output_stream::( + &device, &config, samples_arc.clone(), cursor.clone(), out_channels, done.clone(), err_fn, + )?, + other => return Err(AudioError::UnsupportedFormat(other)), + }; + stream.play()?; + + // Wait for the callback to drain the buffer. + let timeout = Duration::from_secs_f64( + (total as f64 / wav_spec.sample_rate as f64 / wav_spec.channels as f64) + 2.0, + ); + let start = std::time::Instant::now(); + loop { + if *done.lock().unwrap() { + break; + } + if start.elapsed() > timeout { + warn!(target: "spike", "playback timeout reached"); + break; + } + std::thread::sleep(Duration::from_millis(50)); + } + + drop(stream); + let consumed = *cursor.lock().unwrap(); + info!(target: "spike", consumed, total, "playback finished"); + Ok(()) + } +} + +fn build_output_stream( + device: &cpal::Device, + config: &cpal::StreamConfig, + samples: Arc>, + cursor: Arc>, + out_channels: usize, + done: Arc>, + err_fn: fn(cpal::StreamError), +) -> Result +where + T: SizedSample + FromI16 + Send + 'static, +{ + let stream = device.build_output_stream( + config, + move |out: &mut [T], _| { + let mut idx = cursor.lock().unwrap(); + for frame in out.chunks_mut(out_channels) { + if *idx >= samples.len() { + // End of file — write silence; signal done. + for s in frame.iter_mut() { + *s = T::from_i16(0); + } + *done.lock().unwrap() = true; + } else { + let sample = samples[*idx]; + *idx += 1; + for s in frame.iter_mut() { + *s = T::from_i16(sample); + } + } + } + }, + err_fn, + None, + )?; + Ok(stream) +} + +pub trait FromI16 { + fn from_i16(v: i16) -> Self; +} + +impl FromI16 for i16 { + fn from_i16(v: i16) -> Self { + v + } +} + +impl FromI16 for u16 { + fn from_i16(v: i16) -> Self { + (i32::from(v) + i32::from(i16::MAX) + 1) as u16 + } +} + +impl FromI16 for f32 { + fn from_i16(v: i16) -> Self { + f32::from(v) / f32::from(i16::MAX) + } +} diff --git a/poc/audio-capture-playback-spike/src/synth.rs b/poc/audio-capture-playback-spike/src/synth.rs new file mode 100644 index 0000000..0c393f9 --- /dev/null +++ b/poc/audio-capture-playback-spike/src/synth.rs @@ -0,0 +1,38 @@ +//! Synthesised waveform for environments with no real input device. +//! +//! Generates a 16-bit-PCM mono WAV containing a sine wave at the +//! requested frequency and duration. This lets the playback path be +//! exercised against the platform output stack even when capture is +//! unavailable (typical headless CI host). + +use std::path::Path; +use std::time::Duration; + +use hound::{SampleFormat, WavSpec, WavWriter}; + +use crate::capture::AudioError; + +/// Write a sine-wave WAV file. Returns (frames_written, sample_rate). +pub fn synth_sine_wav( + path: &Path, + freq_hz: f32, + duration: Duration, + sample_rate: u32, +) -> Result<(u64, u32), AudioError> { + let spec = WavSpec { + channels: 1, + sample_rate, + bits_per_sample: 16, + sample_format: SampleFormat::Int, + }; + let mut w = WavWriter::create(path, spec)?; + let total_frames = ((duration.as_secs_f64() * sample_rate as f64) as u64).max(1); + let amp = f32::from(i16::MAX) * 0.5; + for n in 0..total_frames { + let t = n as f32 / sample_rate as f32; + let s = (2.0 * std::f32::consts::PI * freq_hz * t).sin() * amp; + w.write_sample(s as i16)?; + } + w.finalize()?; + Ok((total_frames, sample_rate)) +} diff --git a/poc/audio-capture-playback-spike/tests/audio.rs b/poc/audio-capture-playback-spike/tests/audio.rs new file mode 100644 index 0000000..d02856d --- /dev/null +++ b/poc/audio-capture-playback-spike/tests/audio.rs @@ -0,0 +1,46 @@ +//! Verification tests. These run against the host audio stack and +//! therefore live behind the `audio-hardware` opt-in feature for CI +//! environments without a usable audio device. Without the feature +//! they fall back to a pure-software synth+WAV round-trip that does +//! not touch cpal. + +use std::time::Duration; + +use audio_capture_playback_spike::synth_sine_wav; + +#[test] +fn synth_produces_valid_wav_with_expected_frame_count() { + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("sine.wav"); + let (frames, sr) = + synth_sine_wav(&path, 440.0, Duration::from_millis(500), 48_000).unwrap(); + assert_eq!(sr, 48_000); + // 0.5 s × 48 kHz = 24 000 frames (mono). + assert!(frames >= 23_900 && frames <= 24_100, "got {frames}"); + + let reader = hound::WavReader::open(&path).unwrap(); + let spec = reader.spec(); + assert_eq!(spec.channels, 1); + assert_eq!(spec.sample_rate, 48_000); + assert_eq!(spec.bits_per_sample, 16); + assert_eq!(reader.len(), frames as u32); +} + +#[test] +fn synth_then_playback_does_not_error_when_output_device_present() { + use audio_capture_playback_spike::AudioPlayback; + use cpal::traits::HostTrait; + + // Skip on hosts with no output device. The PoC's exit criterion + // is satisfied by the verification CLI run captured in + // VERIFICATION.md; this test is a CI sanity check. + if cpal::default_host().default_output_device().is_none() { + eprintln!("SKIP: no default output device"); + return; + } + + let tmp = tempfile::tempdir().unwrap(); + let path = tmp.path().join("sine.wav"); + synth_sine_wav(&path, 440.0, Duration::from_millis(120), 48_000).unwrap(); + AudioPlayback::play_wav(&path).expect("playback round-trip should not error"); +}