diff --git a/apps/hello/.gitignore b/apps/hello/.gitignore deleted file mode 100644 index 87c6086..0000000 --- a/apps/hello/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.mp3 -*.wav \ No newline at end of file diff --git a/apps/hello/Cargo.lock b/apps/hello/Cargo.lock deleted file mode 100644 index c3c2b98..0000000 --- a/apps/hello/Cargo.lock +++ /dev/null @@ -1,733 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[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 = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "atomic-polyfill" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" -dependencies = [ - "critical-section", -] - -[[package]] -name = "audiopus_sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62314a1546a2064e033665d658e88c620a62904be945f8147e6b16c3db9f8651" -dependencies = [ - "cmake", - "log", - "pkg-config", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" - -[[package]] -name = "cc" -version = "1.2.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "extended" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" - -[[package]] -name = "find-msvc-tools" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" - -[[package]] -name = "hash32" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" -dependencies = [ - "byteorder", -] - -[[package]] -name = "heapless" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" -dependencies = [ - "atomic-polyfill", - "hash32", - "rustc_version", - "serde", - "spin", - "stable_deref_trait", -] - -[[package]] -name = "hello" -version = "0.1.0" -dependencies = [ - "alsa", - "anyhow", - "opus", - "postcard", - "serde", - "symphonia", - "tokio", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.178" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "opus" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6526409b274a7e98e55ff59d96aafd38e6cd34d46b7dbbc32ce126dffcd75e8e" -dependencies = [ - "audiopus_sys", - "libc", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "heapless", - "serde", -] - -[[package]] -name = "proc-macro2" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[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 = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "symphonia" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" -dependencies = [ - "lazy_static", - "symphonia-bundle-flac", - "symphonia-bundle-mp3", - "symphonia-codec-adpcm", - "symphonia-codec-pcm", - "symphonia-codec-vorbis", - "symphonia-core", - "symphonia-format-mkv", - "symphonia-format-ogg", - "symphonia-format-riff", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-bundle-flac" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-bundle-mp3" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" -dependencies = [ - "lazy_static", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-codec-adpcm" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f" -dependencies = [ - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-codec-pcm" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" -dependencies = [ - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-codec-vorbis" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" -dependencies = [ - "log", - "symphonia-core", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-core" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" -dependencies = [ - "arrayvec", - "bitflags 1.3.2", - "bytemuck", - "lazy_static", - "log", -] - -[[package]] -name = "symphonia-format-mkv" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0" -dependencies = [ - "lazy_static", - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-format-ogg" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-format-riff" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" -dependencies = [ - "extended", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-metadata" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" -dependencies = [ - "encoding_rs", - "lazy_static", - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-utils-xiph" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" -dependencies = [ - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio" -version = "1.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets", -] - -[[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.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" diff --git a/apps/hello/Cargo.toml b/apps/hello/Cargo.toml deleted file mode 100644 index 4df68e0..0000000 --- a/apps/hello/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "hello" -version = "0.1.0" -edition = "2024" - -[profile.release] -lto = true -opt-level = "s" -codegen-units = 1 -panic = "abort" -strip = true -debug = false - -[dependencies] -opus = "0.3" -anyhow = "1.0" -tokio = { version = "1.48", features = ["full"] } -serde = { version = "1.0", features = ["derive"] } -postcard = { version = "1.0", features = ["alloc", "use-std"] } - -[target.'cfg(not(target_os = "linux"))'.dependencies] -symphonia = { version = "0.5", features = ["mp3", "wav", "pcm"] } - -[target.'cfg(target_os = "linux")'.dependencies] -alsa = "0.11" diff --git a/apps/hello/Makefile b/apps/hello/Makefile deleted file mode 100644 index 3c2952c..0000000 --- a/apps/hello/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -BIN ?= server - -build-stereo: - $(MAKE) build-arm32 BIN=stereo - -build-client: - $(MAKE) build-arm32 BIN=client - -build-server: - cargo build --release --bin server - -run: - ./target/release/$(BIN) - -build-arm32: - docker run --rm -v $(shell pwd):/app open-xiaoai-runtime \ - cargo build --target armv7-unknown-linux-gnueabihf --release --bin $(BIN) - -build-arm64: - cargo build --release --bin $(BIN) \ No newline at end of file diff --git a/apps/hello/build.sh b/apps/hello/build.sh deleted file mode 100755 index 400ab43..0000000 --- a/apps/hello/build.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -set -e - -SSH_PASSWORD=open-xiaoai - -# # 1. 编译 client -# echo "Compiling client..." -# docker run --rm -v $(pwd):/app/hello open-xiaoai-runtime \ -# cargo build --manifest-path hello/Cargo.toml --target armv7-unknown-linux-gnueabihf --bin client --release - -# 2. 上传 client 到小爱音箱 -function upload_to_xiaoai() { - local binary_name=$1 - local ip=$2 - dd if=target/armv7-unknown-linux-gnueabihf/release/$binary_name \ - | sshpass -p $SSH_PASSWORD ssh -o HostKeyAlgorithms=+ssh-rsa root@$ip "dd of=/data/$binary_name" \ - && echo "✅ Uploaded $binary_name to $ip" -} - -upload_to_xiaoai stereo 192.168.31.153 # left -upload_to_xiaoai stereo 192.168.31.235 # right - -# # 3. 编译 server -# cargo build --bin server --release -# ./target/release/server - -# # dd if=test.wav | sshpass -p open-xiaoai ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.235 "dd of=/tmp/test.wav" - - -# /data/stereo slave left - -# /data/stereo master right - - -# ubus call mediaplayer player_play_url '{"url":"/data/test.wav","type":1}' - -/etc/init.d/pns restart >/dev/null 2>&1 -# /etc/init.d/mediaplayer restart >/dev/null 2>&1 -# /etc/init.d/bluetooth restart >/dev/null 2>&1 -# /etc/init.d/miio restart >/dev/null 2>&1 -# /etc/init.d/mico_aivs_lab restart >/dev/null 2>&1 -# /etc/init.d/miplay restart >/dev/null 2>&1 \ No newline at end of file diff --git a/apps/hello/src/audio/codec.rs b/apps/hello/src/audio/codec.rs deleted file mode 100644 index 5367ee2..0000000 --- a/apps/hello/src/audio/codec.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::config::{AudioConfig, AudioScene}; -use anyhow::{Context, Result}; -use opus::{Application, Bitrate, Channels, Decoder, Encoder}; - -pub struct OpusCodec { - encoder: Encoder, - decoder: Decoder, -} - -impl OpusCodec { - pub fn new(config: &AudioConfig) -> Result { - let channels = match config.channels { - 1 => Channels::Mono, - 2 => Channels::Stereo, - _ => return Err(anyhow::anyhow!("Invalid channels: {}", config.channels)), - }; - let mode = match config.audio_scene { - AudioScene::Music => Application::Audio, - AudioScene::Voice => Application::Voip, - }; - let bitrate = match config.bitrate { - -1 => Bitrate::Max, - 0 => Bitrate::Auto, - _ => Bitrate::Bits(config.bitrate), - }; - - let mut encoder = Encoder::new(config.sample_rate, channels, mode) - .context("Failed to create Opus encoder")?; - - encoder.set_bitrate(bitrate)?; - if config.vbr { - encoder.set_vbr(true)?; - } - if config.fec { - encoder.set_inband_fec(true)?; // 内联前向纠错 - encoder.set_packet_loss_perc(20)?; // 预期丢包率20% - } - - let decoder = - Decoder::new(config.sample_rate, channels).context("Failed to create Opus decoder")?; - - Ok(Self { encoder, decoder }) - } - - pub fn encode(&mut self, pcm: &[i16], out: &mut [u8]) -> Result { - self.encoder - .encode(pcm, out) - .context("Opus encoding failed") - } - - pub fn decode(&mut self, opus: &[u8], out: &mut [i16]) -> Result { - self.decoder - .decode(opus, out, false) - .context("Opus decoding failed") - } - - /// 前向纠错(FEC) - pub fn decode_fec(&mut self, opus: &[u8], out: &mut [i16]) -> Result { - self.decoder - .decode(opus, out, true) - .context("Opus FEC decoding failed") - } - - /// 丢包补偿(PLC) - pub fn decode_loss(&mut self, out: &mut [i16]) -> Result { - self.decoder - .decode(&[], out, false) - .context("Opus PLC (decode_loss) failed") - } -} diff --git a/apps/hello/src/audio/mod.rs b/apps/hello/src/audio/mod.rs deleted file mode 100644 index 01141bb..0000000 --- a/apps/hello/src/audio/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub mod codec; -pub mod player; -pub mod reader; -pub mod recorder; - -pub use codec::*; -#[cfg(target_os = "linux")] -pub use player::*; -#[cfg(not(target_os = "linux"))] -pub use reader::*; -#[cfg(target_os = "linux")] -pub use recorder::*; diff --git a/apps/hello/src/audio/player.rs b/apps/hello/src/audio/player.rs deleted file mode 100644 index 277c395..0000000 --- a/apps/hello/src/audio/player.rs +++ /dev/null @@ -1,69 +0,0 @@ -#![cfg(target_os = "linux")] -use crate::config::AudioConfig; -use alsa::Direction; -use alsa::pcm::{Access, Format, HwParams, PCM}; -use anyhow::{Context, Result}; - -pub struct AudioPlayer { - pcm: PCM, -} - -impl AudioPlayer { - pub fn new(config: &AudioConfig) -> Result { - let pcm = PCM::new(&config.playback_device, Direction::Playback, false) - .context("Failed to open playback PCM device")?; - - setup_pcm(&pcm, config.sample_rate, config.channels)?; - Ok(Self { pcm }) - } - - pub fn write(&self, buffer: &[i16]) -> Result { - let res = self.pcm.io_i16()?.writei(buffer); - - match res { - Ok(written) => Ok(written), - Err(e) => { - // Buffer Underrun,即播放缓冲区的数据被耗尽,导致音频流中断 - if e.errno() == 32 { - // 恢复音频流状态 - self.pcm.prepare()?; - // 重新获取 IO 对象并尝试写入数据 - self.pcm - .io_i16()? - .writei(buffer) - .context("Failed to write to playback device after recovery") - } else { - Err(e).context("Failed to write to playback device") - } - } - } - } - - pub fn prepare(&self) -> Result<()> { - self.pcm.prepare().context("Failed to prepare PCM") - } -} - -fn setup_pcm(pcm: &PCM, sample_rate: u32, channels: u16) -> Result<()> { - let hwp = HwParams::any(pcm).context("Failed to get HwParams")?; - hwp.set_access(Access::RWInterleaved)?; - hwp.set_format(Format::s16())?; - hwp.set_rate(sample_rate, alsa::ValueOr::Nearest)?; - hwp.set_channels(channels as u32)?; - - // 设置较大的缓冲区以减少由于调度抖动导致的断音 - // 48000Hz * 0.2s = 9600 samples - let buffer_size = (sample_rate as f64 * 0.2) as u32; - let period_size = buffer_size / 4; - hwp.set_buffer_size_near(buffer_size as alsa::pcm::Frames)?; - hwp.set_period_size_near(period_size as alsa::pcm::Frames, alsa::ValueOr::Nearest)?; - - pcm.hw_params(&hwp).context("Failed to set HwParams")?; - - let swp = pcm.sw_params_current()?; - // 设置 start_threshold 为 buffer_size 的一半,确保缓冲区有足够数据再开始播放 - swp.set_start_threshold(buffer_size as alsa::pcm::Frames / 2)?; - pcm.sw_params(&swp)?; - pcm.prepare()?; - Ok(()) -} diff --git a/apps/hello/src/audio/reader.rs b/apps/hello/src/audio/reader.rs deleted file mode 100644 index f9c9de8..0000000 --- a/apps/hello/src/audio/reader.rs +++ /dev/null @@ -1,140 +0,0 @@ -#![cfg(not(target_os = "linux"))] -use anyhow::{Context, Result}; -use std::fs::File; -use std::path::Path; -use symphonia::core::audio::SampleBuffer; -use symphonia::core::codecs::{Decoder, DecoderOptions}; -use symphonia::core::formats::{FormatOptions, FormatReader}; -use symphonia::core::io::MediaSourceStream; -use symphonia::core::meta::MetadataOptions; -use symphonia::core::probe::Hint; - -pub struct AudioReader { - format: Box, - decoder: Box, - track_id: u32, - sample_buf: Option>, - channels: usize, - left_buffer: Vec, - right_buffer: Vec, -} - -impl AudioReader { - pub fn new(path: &str) -> Result { - let src = - File::open(Path::new(path)).context(format!("Failed to open audio file: {}", path))?; - let mss = MediaSourceStream::new(Box::new(src), Default::default()); - - let mut hint = Hint::new(); - if path.ends_with(".wav") { - hint.with_extension("wav"); - } else if path.ends_with(".mp3") { - hint.with_extension("mp3"); - } - - let probed = symphonia::default::get_probe() - .format( - &hint, - mss, - &FormatOptions::default(), - &MetadataOptions::default(), - ) - .context("Failed to probe audio format")?; - - let format = probed.format; - let track = format - .tracks() - .iter() - .find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL) - .context("No supported audio track found")?; - - let track_id = track.id; - let decoder = symphonia::default::get_codecs() - .make(&track.codec_params, &DecoderOptions::default()) - .context("Failed to create decoder")?; - - let channels = track.codec_params.channels.map(|c| c.count()).unwrap_or(1); - - Ok(Self { - format, - decoder, - track_id, - sample_buf: None, - channels, - left_buffer: Vec::new(), - right_buffer: Vec::new(), - }) - } - - pub fn read_chunk(&mut self, chunk_size: usize) -> Result, Vec)>> { - while self.left_buffer.len() < chunk_size { - if let Some((l, r)) = self.read_frame_internal()? { - self.left_buffer.extend(l); - self.right_buffer.extend(r); - } else { - break; - } - } - - if self.left_buffer.is_empty() { - return Ok(None); - } - - let actual_size = std::cmp::min(chunk_size, self.left_buffer.len()); - let left = self.left_buffer.drain(0..actual_size).collect(); - let right = self.right_buffer.drain(0..actual_size).collect(); - - Ok(Some((left, right))) - } - - fn read_frame_internal(&mut self) -> Result, Vec)>> { - loop { - let packet = match self.format.next_packet() { - Ok(packet) => packet, - Err(symphonia::core::errors::Error::IoError(ref e)) - if e.kind() == std::io::ErrorKind::UnexpectedEof => - { - return Ok(None); - } - Err(e) => return Err(e.into()), - }; - - if packet.track_id() != self.track_id { - continue; - } - - let decoded = self - .decoder - .decode(&packet) - .context("Failed to decode packet")?; - - if self.sample_buf.is_none() { - self.sample_buf = Some(SampleBuffer::::new( - decoded.capacity() as u64, - *decoded.spec(), - )); - } - - if let Some(buf) = self.sample_buf.as_mut() { - buf.copy_interleaved_ref(decoded); - let samples = buf.samples(); - - let mut left = Vec::with_capacity(samples.len() / self.channels); - let mut right = Vec::with_capacity(samples.len() / self.channels); - - if self.channels == 2 { - for i in (0..samples.len()).step_by(2) { - left.push(samples[i]); - right.push(samples[i + 1]); - } - } else { - for &s in samples { - left.push(s); - right.push(s); - } - } - return Ok(Some((left, right))); - } - } - } -} diff --git a/apps/hello/src/audio/recorder.rs b/apps/hello/src/audio/recorder.rs deleted file mode 100644 index d3c4c89..0000000 --- a/apps/hello/src/audio/recorder.rs +++ /dev/null @@ -1,38 +0,0 @@ -#![cfg(target_os = "linux")] -use alsa::pcm::{PCM, HwParams, Format, Access}; -use alsa::Direction; -use anyhow::{Result, Context}; -use crate::config::AudioConfig; - -pub struct AudioRecorder { - pcm: PCM, -} - -impl AudioRecorder { - pub fn new(config: &AudioConfig) -> Result { - let pcm = PCM::new(&config.capture_device, Direction::Capture, false) - .context("Failed to open capture PCM device")?; - - setup_pcm(&pcm, config.sample_rate, config.channels)?; - Ok(Self { pcm }) - } - - pub fn read(&self, buffer: &mut [i16]) -> Result { - self.pcm.io_i16()?.readi(buffer).context("Failed to read from capture device") - } -} - -fn setup_pcm(pcm: &PCM, sample_rate: u32, channels: u16) -> Result<()> { - let hwp = HwParams::any(pcm).context("Failed to get HwParams")?; - hwp.set_access(Access::RWInterleaved)?; - hwp.set_format(Format::s16())?; - hwp.set_rate(sample_rate, alsa::ValueOr::Nearest)?; - hwp.set_channels(channels as u32)?; - pcm.hw_params(&hwp).context("Failed to set HwParams")?; - - let swp = pcm.sw_params_current()?; - pcm.sw_params(&swp)?; - pcm.prepare()?; - Ok(()) -} - diff --git a/apps/hello/src/bin/stereo.rs b/apps/hello/src/bin/stereo.rs deleted file mode 100644 index 4c80c51..0000000 --- a/apps/hello/src/bin/stereo.rs +++ /dev/null @@ -1,29 +0,0 @@ -#![cfg(target_os = "linux")] - -use anyhow::Result; -use hello::stereo_core::master::run_master; -use hello::stereo_core::protocol::ChannelRole; -use hello::stereo_core::slave::run_slave; -use std::env; - -#[tokio::main] -async fn main() -> Result<()> { - let args: Vec = env::args().collect(); - if args.len() < 3 { - eprintln!("用法: {} [master|slave] [left|right]", args[0]); - return Ok(()); - } - - let mode = &args[1]; - let role = if args[2].to_lowercase() == "left" { - ChannelRole::Left - } else { - ChannelRole::Right - }; - - if mode == "master" { - run_master(role).await - } else { - run_slave(role).await - } -} diff --git a/apps/hello/src/config.rs b/apps/hello/src/config.rs deleted file mode 100644 index 1c982ef..0000000 --- a/apps/hello/src/config.rs +++ /dev/null @@ -1,61 +0,0 @@ -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum AudioScene { - Music, - Voice, -} - -#[derive(Debug, Clone)] -pub struct AudioConfig { - // ALSA 设备参数,用于录音和播放 - pub capture_device: String, - pub playback_device: String, - pub sample_rate: u32, - pub channels: u16, - pub frame_size: usize, // 帧大小,单位为采样点 - - // Opus 编解码参数,用于音频传输 - pub audio_scene: AudioScene, - pub bitrate: i32, - pub vbr: bool, // 是否启用 VBR(动态比特率) - pub fec: bool, // 是否启用 FEC(内联前向纠错) -} - -impl AudioConfig { - pub fn music() -> Self { - Self { - audio_scene: AudioScene::Music, - sample_rate: 48_000, // 48kHz - channels: 2, - frame_size: 960, // 20ms at 48kHz - bitrate: 320_000, // 320 kbps - ..Default::default() - } - } - - pub fn voice() -> Self { - Self { - audio_scene: AudioScene::Voice, - sample_rate: 16_000, // 16kHz - channels: 1, - frame_size: 320, // 20ms at 16kHz - bitrate: 32_000, // 32 kbps - ..Default::default() - } - } -} - -impl Default for AudioConfig { - fn default() -> Self { - Self { - audio_scene: AudioScene::Voice, - capture_device: "plug:Capture".to_string(), - playback_device: "default".to_string(), - sample_rate: 16_000, - channels: 1, - frame_size: 320, // 20ms at 16kHz - bitrate: 32_000, - vbr: false, - fec: false, - } - } -} diff --git a/apps/hello/src/config/asound.lx06.conf b/apps/hello/src/config/asound.lx06.conf deleted file mode 100644 index fa80069..0000000 --- a/apps/hello/src/config/asound.lx06.conf +++ /dev/null @@ -1,140 +0,0 @@ -pcm.!default { - type plug - slave { - pcm { - type softvol - slave.pcm dtsaudio - control{ - name "mysoftvol" - card 0 - } - min_dB -51.0 - max_dB 0.0 - } - channels 2 - format S16_LE - rate 48000 - } -} - -pcm.bluetooth { - type plug - slave { - pcm { - type softvol - slave.pcm dmixer - control{ - name "bluetooth" - card 0 - } - min_dB -51.0 - max_dB 0.0 - } - channels 2 - format S16_LE - rate 48000 - } -} - -pcm.notify { - type plug - slave { - pcm { - type softvol - slave.pcm dmixer - control{ - name "notifyvol" - card 0 - } - min_dB -51.0 - max_dB 0.0 - } - channels 2 - format S16_LE - rate 48000 - } -} - -pcm.dtsaudio { - type plug - slave.pcm "dtsladspa" -} - -pcm.dtsladspa { - type ladspa - slave.pcm plug:dmixer - channels 2 - path "/usr/lib/" - playback_plugins [{ - label dts_process - input { - controls [4] - } - }] -} - -pcm.dmixer { - type dmix - ipc_key 1024 - slave { - pcm "hw:0,2" - format S16_LE - period_size 1920 - buffer_size 7680 - rate 48000 - } - bindings { - 0 0 - 1 1 - } -} - -ctl.dmixer { - type hw - card 0 - device 1 -} - -pcm.dsp { - type plug - slave.pcm "dmixer" # use our new PCM here -} - -ctl.mixer { - type hw - card 0 -} - -pcm.dis { - type plug - slave.pcm noop -} - -pcm.mico_record { - type plug - slave.pcm Capture -} - -pcm.noop { - type plug - slave.pcm Capture -} - -pcm.Capture { - type plug - slave.pcm { - type dsnoop - ipc_key 1024 - ipc_perm 0666 - slave { - pcm "hw:0,3" - rate 48000 - format S32_LE - channels 8 - period_size 384 - buffer_size 6144 - } - } -} - -defaults.pcm.rate_converter "speexrate_medium" \ No newline at end of file diff --git a/apps/hello/src/config/asound.oh2p.conf b/apps/hello/src/config/asound.oh2p.conf deleted file mode 100644 index e37cd0f..0000000 --- a/apps/hello/src/config/asound.oh2p.conf +++ /dev/null @@ -1,147 +0,0 @@ -pcm.!default { - type plug - slave { - pcm "vis" - format S16_LE - rate 48000 - } -} - -pcm.vis { - type file - slave.pcm "tocopy" - file "|safe_fifo /tmp/vis_audio.fifo /tmp/mis_audio.fifo" -} - -pcm.tocopy { - type copy - slave { - pcm "Playback" - } -} - -pcm.Playback { - type plug - slave.pcm { - type softvol - slave.pcm "dmixer" - control { - name "mysoftvol" - card 0 - } - min_dB -51.0 - max_dB 0.0 - } -} - -pcm.usb_up { - type plug - slave { - pcm "hw:UAC2Gadget" - rate 48000 - format S16_LE - channels 2 - } -} - -pcm.CaptureUsbDown { - type plug - slave.pcm { - type dsnoop - ipc_key 3333 - ipc_perm 0666 - slave { - pcm "hw:UAC2Gadget" - rate 48000 - format S16_LE - channels 2 - period_size 480 - periods 8 - } - } -} - -pcm.notify { - type plug - slave { - pcm { - type softvol - slave.pcm dmixer - control { - name "notifyvol" - card 0 - } - min_dB -51.0 - max_dB 0.0 - } - channels 2 - format S16_LE - rate 48000 - } -} - -pcm.dmixer { - type dmix - ipc_key 1024 - slave { - pcm "hw:0,2" - format S16_LE - period_size 480 - buffer_size 4800 - rate 48000 - } - bindings { - 0 0 - 1 1 - } -} - -ctl.dmixer { - type hw - card 0 - device 1 -} - -pcm.dsp { - type plug - slave.pcm "dmixer" # use our new PCM here -} - -ctl.mixer { - type hw - card 0 -} - -pcm.dis { - type plug - slave.pcm noop -} - -pcm.mico_record { - type plug - slave.pcm Capture -} - -pcm.noop { - type plug - slave.pcm Capture -} - -pcm.Capture { - type plug - slave.pcm { - type dsnoop - ipc_key 1024 - ipc_perm 0666 - slave { - pcm "hw:0,3" - rate 48000 - format S32_LE - channels 4 - period_size 384 - buffer_size 6144 - } - } -} - -defaults.pcm.rate_converter "speexrate_medium" \ No newline at end of file diff --git a/apps/hello/src/lib.rs b/apps/hello/src/lib.rs deleted file mode 100644 index 147dfb3..0000000 --- a/apps/hello/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod audio; -pub mod config; -pub mod net; -pub mod stereo_core; -pub mod sync; diff --git a/apps/hello/src/net/discovery.rs b/apps/hello/src/net/discovery.rs deleted file mode 100644 index 2847197..0000000 --- a/apps/hello/src/net/discovery.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::net::SocketAddr; -use tokio::net::UdpSocket; -use std::time::Duration; -use crate::net::protocol::ControlPacket; -use anyhow::Result; - -pub const DISCOVERY_PORT: u16 = 8888; -pub const SERVER_PORT: u16 = 8889; - -pub struct Discovery; - -impl Discovery { - pub async fn start_server_beacon() -> Result<()> { - let socket = UdpSocket::bind("0.0.0.0:0").await?; - socket.set_broadcast(true)?; - - let target_addr: SocketAddr = format!("255.255.255.255:{}", DISCOVERY_PORT).parse()?; - let hello = ControlPacket::ServerHello { port: SERVER_PORT }; - let msg = postcard::to_allocvec(&hello)?; - - tokio::spawn(async move { - loop { - let _ = socket.send_to(&msg, target_addr).await; - tokio::time::sleep(Duration::from_secs(1)).await; - } - }); - - Ok(()) - } - - pub async fn client_discover_server() -> Result { - let socket = UdpSocket::bind(format!("0.0.0.0:{}", DISCOVERY_PORT)).await?; - let mut buf = [0u8; 1024]; - - loop { - let (len, addr) = socket.recv_from(&mut buf).await?; - if let Ok(ControlPacket::ServerHello { port }) = postcard::from_bytes::(&buf[..len]) { - let mut server_addr = addr; - server_addr.set_port(port); - return Ok(server_addr); - } - } - } -} - diff --git a/apps/hello/src/net/mod.rs b/apps/hello/src/net/mod.rs deleted file mode 100644 index 05297c9..0000000 --- a/apps/hello/src/net/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod protocol; -pub mod discovery; - -pub use protocol::*; -pub use discovery::*; - diff --git a/apps/hello/src/net/protocol.rs b/apps/hello/src/net/protocol.rs deleted file mode 100644 index bddab45..0000000 --- a/apps/hello/src/net/protocol.rs +++ /dev/null @@ -1,37 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -pub enum ChannelRole { - Left, - Right, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum ControlPacket { - // Discovery - ServerHello { - port: u16, - }, - // Handshake - ClientIdentify { - role: ChannelRole, - }, - // Time Sync - Ping { - client_ts: u128, - }, - Pong { - client_ts: u128, - server_ts: u128, - }, - // Stream control - StartStreaming { - start_time: u128, // Server time when streaming starts - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct AudioPacket { - pub timestamp: u128, // Target server time for playback - pub data: Vec, // Opus encoded data -} diff --git a/apps/hello/src/stereo_core/alsa.rs b/apps/hello/src/stereo_core/alsa.rs deleted file mode 100644 index 54a7607..0000000 --- a/apps/hello/src/stereo_core/alsa.rs +++ /dev/null @@ -1,78 +0,0 @@ -#![cfg(target_os = "linux")] -use anyhow::{Context, Result}; -use std::fs; -use std::process::Command; - -const FIFO_PATH: &str = "/tmp/stereo_out.fifo"; -const REAL_ASOUND_CONF: &str = "/etc/asound.conf"; -const TEMP_ASOUND_CONF: &str = "/tmp/asound.stereo.conf"; - -/// ALSA 音频重定向器,用于拦截系统音频输出到 FIFO 管道 -pub struct AlsaRedirector; - -impl AlsaRedirector { - pub fn new() -> Result { - Self::cleanup(); // 确保环境干净 - - let original_conf = fs::read_to_string(REAL_ASOUND_CONF).unwrap_or_default(); - - if !original_conf.contains("pcm.original_default") { - // 重命名原有的 default 逻辑,插入拦截器 - let mut new_conf = original_conf.replace("pcm.!default", "pcm.original_default"); - new_conf.push_str(&format!( - "\npcm.!default {{ type plug slave {{ pcm \"stereo_interceptor\" format S16_LE rate 48000 channels 2 }} }}\n\ - pcm.stereo_interceptor {{ type file slave.pcm \"null\" file \"{}\" format \"raw\" }}\n", - FIFO_PATH - )); - - fs::write(TEMP_ASOUND_CONF, new_conf)?; - - // 挂载覆盖 /etc/asound.conf - let status = Command::new("mount") - .arg("--bind") - .arg(TEMP_ASOUND_CONF) - .arg(REAL_ASOUND_CONF) - .status() - .context("执行 mount 命令失败")?; - - if !status.success() { - return Err(anyhow::anyhow!("挂载 asound.conf 失败")); - } - - Self::restart_applications(); - } - - // 创建 FIFO 管道 - let _ = Command::new("mkfifo").arg(FIFO_PATH).status(); - let _ = Command::new("chmod").arg("666").arg(FIFO_PATH).status(); - - Ok(Self) - } - - pub fn cleanup() { - let _ = Command::new("sh") - .arg("-c") - .arg(format!("umount -l {} >/dev/null 2>&1", REAL_ASOUND_CONF)) - .status(); - let _ = fs::remove_file(TEMP_ASOUND_CONF); - let _ = fs::remove_file(FIFO_PATH); - Self::restart_applications(); - } - - pub fn fifo_path() -> &'static str { - FIFO_PATH - } - - pub fn restart_applications() { - // 重启媒体播放器 - let _ = Command::new("sh") - .arg("-c") - .arg("/etc/init.d/mediaplayer restart >/dev/null 2>&1") - .status(); - // 重启蓝牙 - let _ = Command::new("sh") - .arg("-c") - .arg("/etc/init.d/bluetooth restart >/dev/null 2>&1") - .status(); - } -} diff --git a/apps/hello/src/stereo_core/discovery.rs b/apps/hello/src/stereo_core/discovery.rs deleted file mode 100644 index 2f7b1d5..0000000 --- a/apps/hello/src/stereo_core/discovery.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::stereo_core::protocol::ControlPacket; -use anyhow::Result; -use std::net::{IpAddr, SocketAddr}; -use std::time::Duration; -use tokio::net::UdpSocket; - -pub const DISCOVERY_PORT: u16 = 53530; - -/// 服务发现模块,用于主从节点的自动发现 -pub struct Discovery; - -impl Discovery { - /// 主节点:启动广播,告知从节点自己的 TCP 端口 - pub async fn start_broadcast(tcp_port: u16) -> Result<()> { - let socket = UdpSocket::bind("0.0.0.0:0").await?; - socket.set_broadcast(true)?; - - let target: SocketAddr = format!("255.255.255.255:{}", DISCOVERY_PORT).parse()?; - let msg = postcard::to_allocvec(&ControlPacket::ServerHello { udp_port: tcp_port })?; - - tokio::spawn(async move { - loop { - let _ = socket.send_to(&msg, target).await; - tokio::time::sleep(Duration::from_secs(1)).await; - } - }); - - Ok(()) - } - - /// 从节点:监听广播,发现主节点的 IP 和 TCP 端口 - pub async fn discover_master() -> Result<(IpAddr, u16)> { - let socket = UdpSocket::bind(format!("0.0.0.0:{}", DISCOVERY_PORT)).await?; - let mut buf = [0u8; 1024]; - - loop { - let (len, addr) = socket.recv_from(&mut buf).await?; - if let Ok(ControlPacket::ServerHello { udp_port }) = - postcard::from_bytes::(&buf[..len]) - { - return Ok((addr.ip(), udp_port)); - } - } - } -} diff --git a/apps/hello/src/stereo_core/jitter_buffer.rs b/apps/hello/src/stereo_core/jitter_buffer.rs deleted file mode 100644 index 9e02e14..0000000 --- a/apps/hello/src/stereo_core/jitter_buffer.rs +++ /dev/null @@ -1,67 +0,0 @@ -use super::protocol::AudioPacket; -use std::collections::BinaryHeap; -use std::cmp::Ordering; - -#[derive(Debug)] -struct OrderedPacket(AudioPacket); - -impl PartialEq for OrderedPacket { - fn eq(&self, other: &Self) -> bool { - self.0.seq == other.0.seq - } -} -impl Eq for OrderedPacket {} -impl PartialOrd for OrderedPacket { - fn partial_cmp(&self, other: &Self) -> Option { - Some(other.0.seq.cmp(&self.0.seq)) - } -} -impl Ord for OrderedPacket { - fn cmp(&self, other: &Self) -> Ordering { - other.0.seq.cmp(&self.0.seq) - } -} - -/// 抖动缓冲区,用于处理网络延迟和乱序 -pub struct JitterBuffer { - buffer: BinaryHeap, - last_played_seq: Option, - pub target_delay_us: u128, -} - -impl JitterBuffer { - pub fn new(target_delay_us: u128) -> Self { - Self { - buffer: BinaryHeap::new(), - last_played_seq: None, - target_delay_us, - } - } - - pub fn push(&mut self, packet: AudioPacket) { - // 丢弃已经播放过的数据包 - if let Some(last) = self.last_played_seq { - if packet.seq <= last { - return; - } - } - self.buffer.push(OrderedPacket(packet)); - } - - /// 根据当前时间弹出待播放的帧 - pub fn pop_frame(&mut self, current_time: u128) -> Option<(u32, Vec)> { - if let Some(OrderedPacket(pkt)) = self.buffer.peek() { - if current_time >= pkt.timestamp { - let pkt = self.buffer.pop().unwrap().0; - self.last_played_seq = Some(pkt.seq); - return Some((pkt.seq, pkt.data)); - } - } - None - } - - pub fn clear(&mut self) { - self.buffer.clear(); - self.last_played_seq = None; - } -} diff --git a/apps/hello/src/stereo_core/master.rs b/apps/hello/src/stereo_core/master.rs deleted file mode 100644 index 2c6bc1c..0000000 --- a/apps/hello/src/stereo_core/master.rs +++ /dev/null @@ -1,362 +0,0 @@ -#![cfg(target_os = "linux")] - -use crate::audio::{AudioPlayer, OpusCodec}; -use crate::config::AudioConfig; -use crate::stereo_core::alsa::AlsaRedirector; -use crate::stereo_core::discovery::Discovery; -use crate::stereo_core::network::{ControlConnection, MasterNetwork}; -use crate::stereo_core::protocol::{AudioPacket, ChannelRole, ControlPacket}; -use crate::stereo_core::sync::now_us; -use anyhow::{Result, anyhow}; -use std::net::SocketAddr; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::Duration; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::signal::unix::{SignalKind, signal}; -use tokio::sync::Mutex; - -pub const SERVER_TCP_PORT: u16 = 53531; - -#[derive(Clone)] -struct SlaveSession { - udp_addr: SocketAddr, - role: ChannelRole, -} - -pub async fn run_master(master_role: ChannelRole) -> Result<()> { - println!("--- 主节点模式 ({}) ---", master_role.to_string()); - - // 0. 设置 ALSA 重定向 - println!("🔥 启动中,请稍等..."); - let _alsa_guard = AlsaRedirector::new()?; - - // 1. 设置网络 (UDP + TCP) - let network = MasterNetwork::setup(SERVER_TCP_PORT).await?; - let audio_socket = network.audio_socket().clone_inner(); - - // 2. 启动服务发现广播 - Discovery::start_broadcast(SERVER_TCP_PORT).await?; - - println!("✅ 服务已启动,等待连接..."); - - let shutdown_flag = Arc::new(AtomicBool::new(false)); - let slaves = Arc::new(Mutex::new(Vec::::new())); - - // 3. 启动连接监听任务 - let slaves_clone = slaves.clone(); - let audio_socket_clone = audio_socket.clone(); - tokio::spawn(async move { - loop { - match network.accept().await { - Ok((control_conn, client_addr)) => { - let slaves_for_session = slaves_clone.clone(); - let audio_socket_for_session = audio_socket_clone.clone(); - tokio::spawn(async move { - if let Err(e) = handle_master_session( - control_conn, - audio_socket_for_session, - slaves_for_session, - client_addr.to_string(), - ) - .await - { - eprintln!("❌ 会话错误: {:?}", e); - } - }); - } - Err(e) => { - eprintln!("❌ Accept 错误: {:?}", e); - } - } - } - }); - - // 4. 音频处理主循环 - let config = AudioConfig::music(); - let encode_config = AudioConfig { - channels: 1, - vbr: true, - ..AudioConfig::music() - }; - let player = AudioPlayer::new(&AudioConfig { - channels: 2, - playback_device: "plug:original_default".into(), - ..config.clone() - })?; - - let mut raw_buf = vec![0u8; config.frame_size * 2 * 2]; - let mut pcm_out = vec![0i16; config.frame_size * 2]; - let mut left_pcm = vec![0i16; config.frame_size]; - let mut right_pcm = vec![0i16; config.frame_size]; - let mut opus_out = vec![0u8; 1500]; - let mut seq = 0u32; - - let delay_us = 200_000; - let frame_duration_us = - (config.frame_size as f64 / config.sample_rate as f64 * 1_000_000.0) as u128; - - let mut stream_start_ts = 0; - let mut stream_start_seq = 0; - - let shutdown_flag_clone = shutdown_flag.clone(); - let audio_loop = async move { - loop { - if shutdown_flag_clone.load(Ordering::Relaxed) { - break; - } - - // 打开 FIFO - let mut fifo = match tokio::fs::File::open(AlsaRedirector::fifo_path()).await { - Ok(f) => f, - Err(_) => { - if shutdown_flag_clone.load(Ordering::Relaxed) { - break; - } - tokio::time::sleep(Duration::from_secs(1)).await; - continue; - } - }; - - // 每个新流开始时,重置编码器状态以避免残留音频导致爆音 - let mut left_encoder = OpusCodec::new(&encode_config)?; - let mut right_encoder = OpusCodec::new(&encode_config)?; - - loop { - if shutdown_flag_clone.load(Ordering::Relaxed) { - break; - } - - // 从 FIFO 读取 - if let Err(_) = fifo.read_exact(&mut raw_buf).await { - break; // FIFO 关闭,重新打开 - } - - let active_slaves = { - let s = slaves.lock().await; - if s.is_empty() { None } else { Some(s.clone()) } - }; - - let now = now_us(); - if stream_start_ts == 0 { - stream_start_ts = now; - stream_start_seq = seq; - } - - // 计算该帧应当播放的基准时间(相对于流开始) - let target_ts = stream_start_ts - + ((seq - stream_start_seq) as u128 * frame_duration_us) - + delay_us; - - // 提取 PCM 数据 - for i in 0..config.frame_size { - left_pcm[i] = i16::from_le_bytes([raw_buf[i * 4], raw_buf[i * 4 + 1]]); - right_pcm[i] = i16::from_le_bytes([raw_buf[i * 4 + 2], raw_buf[i * 4 + 3]]); - } - - if let Some(slaves_list) = active_slaves { - // 情况 1: 有从节点,进行网络传输,并本地构造静音声道回放 - - // 1. 检查各声道是否有从节点需要 - let needs_left = slaves_list.iter().any(|s| s.role == ChannelRole::Left); - let needs_right = slaves_list.iter().any(|s| s.role == ChannelRole::Right); - - // 2. 编码需要的声道 - let mut left_bytes = None; - let mut right_bytes = None; - - if needs_left { - let len = left_encoder.encode(&left_pcm, &mut opus_out)?; - let packet = AudioPacket { - seq, - timestamp: target_ts, - data: opus_out[..len].to_vec(), - }; - left_bytes = Some(postcard::to_allocvec(&packet)?); - } - - if needs_right { - let len = right_encoder.encode(&right_pcm, &mut opus_out)?; - let packet = AudioPacket { - seq, - timestamp: target_ts, - data: opus_out[..len].to_vec(), - }; - right_bytes = Some(postcard::to_allocvec(&packet)?); - } - - // 3. 发送给对应的从节点 - for slave in &slaves_list { - let bytes = match slave.role { - ChannelRole::Left => left_bytes.as_ref(), - ChannelRole::Right => right_bytes.as_ref(), - }; - if let Some(b) = bytes { - let _ = audio_socket.send_to(b, slave.udp_addr).await; - } - } - - // 4. 将非本节点的声道置为静音 - for i in 0..config.frame_size { - match master_role { - ChannelRole::Left => { - pcm_out[i * 2] = left_pcm[i]; - pcm_out[i * 2 + 1] = 0; - } - ChannelRole::Right => { - pcm_out[i * 2] = 0; - pcm_out[i * 2 + 1] = right_pcm[i]; - } - } - } - - // 5. 等待播放 - let now = now_us(); - if now < target_ts { - let wait = target_ts - now; - if wait > 1000 { - tokio::time::sleep(Duration::from_micros(wait as u64)).await; - } - } - } else { - // 情况 2: 没有从节点,本地立体声播放 - for i in 0..config.frame_size { - pcm_out[i * 2] = left_pcm[i]; - pcm_out[i * 2 + 1] = right_pcm[i]; - } - } - - // 统一写入播放器 (始终是立体声) - if let Err(_) = player.write(&pcm_out) { - if shutdown_flag_clone.load(Ordering::Relaxed) { - break; - } - } - - seq += 1; - } - - // 重置流计时 - stream_start_ts = 0; - } - - Ok::<(), anyhow::Error>(()) - }; - - tokio::select! { - res = audio_loop => { - if let Err(e) = res { - eprintln!("❌ 音频循环错误: {:?}", e); - } - }, - _ = shutdown_signal() => { - // 设置退出标志,通知音频循环停止 - shutdown_flag.store(true, Ordering::Relaxed); - }, - } - - // 显式清理 - println!("👋 正在退出..."); - AlsaRedirector::cleanup(); - - // 强制退出 - std::process::exit(0); -} - -/// 监听系统退出信号 (SIGINT, SIGTERM, SIGQUIT) -async fn shutdown_signal() { - let mut sigint = signal(SignalKind::interrupt()).expect("无法注册 SIGINT 处理器"); - let mut sigterm = signal(SignalKind::terminate()).expect("无法注册 SIGTERM 处理器"); - let mut sigquit = signal(SignalKind::quit()).expect("无法注册 SIGQUIT 处理器"); - - tokio::select! { - _ = sigint.recv() => {}, - _ = sigterm.recv() => {}, - _ = sigquit.recv() => {}, - } -} - -/// 处理主节点与从节点的会话 -async fn handle_master_session( - mut control: ControlConnection, - audio_socket: Arc, - slaves: Arc>>, - client_tcp_addr: String, -) -> Result<()> { - let mut buf = [0u8; 1024]; - - // 握手 - let pkt = control.recv_packet(&mut buf).await?; - let slave_role = match pkt { - ControlPacket::ClientIdentify { role } => role, - _ => return Err(anyhow!("无效的握手协议")), - }; - - let hello = ControlPacket::ServerHello { - udp_port: audio_socket.local_addr()?.port(), - }; - control.send_packet(&hello).await?; - - // 等待 UDP 打洞/确认 - let mut buf = [0u8; 128]; - let (_, client_udp_addr) = audio_socket.recv_from(&mut buf).await?; - - println!( - "✅ 从节点已连接: {} {}", - client_tcp_addr, - slave_role.to_string(), - ); - - // 添加到从节点列表 - let session = SlaveSession { - udp_addr: client_udp_addr, - role: slave_role, - }; - { - let mut s = slaves.lock().await; - s.push(session.clone()); - } - - // 分离 TCP 读写,处理控制消息和心跳 - let (mut tcp_rx, mut tcp_tx) = control.split(); - - let mut buf = [0u8; 1024]; - loop { - match tcp_rx.read(&mut buf).await { - Ok(0) | Err(_) => { - break; - } - Ok(n) => { - if let Ok(ControlPacket::Ping { client_ts, seq }) = postcard::from_bytes(&buf[..n]) - { - let pong = ControlPacket::Pong { - client_ts, - server_ts: now_us(), - seq, - }; - if tcp_tx - .write_all(&postcard::to_allocvec(&pong).unwrap()) - .await - .is_err() - { - break; - } - } - } - } - } - - println!( - "❌ 从节点已断开: {} {}", - client_tcp_addr, - slave_role.to_string(), - ); - - // 从列表中移除 - { - let mut s = slaves.lock().await; - s.retain(|x| x.udp_addr != client_udp_addr); - } - - Ok(()) -} diff --git a/apps/hello/src/stereo_core/mod.rs b/apps/hello/src/stereo_core/mod.rs deleted file mode 100644 index 768f74d..0000000 --- a/apps/hello/src/stereo_core/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod alsa; -pub mod discovery; -pub mod jitter_buffer; -pub mod master; -pub mod network; -pub mod protocol; -pub mod slave; -pub mod sync; diff --git a/apps/hello/src/stereo_core/network.rs b/apps/hello/src/stereo_core/network.rs deleted file mode 100644 index 133e441..0000000 --- a/apps/hello/src/stereo_core/network.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::stereo_core::protocol::{AudioPacket, ControlPacket}; -use anyhow::{Context, Result}; -use std::net::SocketAddr; -use std::sync::Arc; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::{TcpListener, TcpStream, UdpSocket}; - -/// UDP 音频传输 -pub struct AudioSocket { - socket: Arc, -} - -impl AudioSocket { - pub async fn bind() -> Result { - let socket = UdpSocket::bind("0.0.0.0:0").await?; - Ok(Self { - socket: Arc::new(socket), - }) - } - - pub fn local_port(&self) -> Result { - Ok(self.socket.local_addr()?.port()) - } - - pub async fn send_packet(&self, packet: &AudioPacket, target: SocketAddr) -> Result<()> { - let bytes = postcard::to_allocvec(packet)?; - self.socket.send_to(&bytes, target).await?; - Ok(()) - } - - pub async fn recv_packet(&self, buf: &mut [u8]) -> Result<(AudioPacket, SocketAddr)> { - let (len, addr) = self.socket.recv_from(buf).await?; - let packet = postcard::from_bytes(&buf[..len])?; - Ok((packet, addr)) - } - - pub async fn punch(&self, target: SocketAddr) -> Result<()> { - self.socket.send_to(&[0u8; 1], target).await?; - Ok(()) - } - - pub fn clone_inner(&self) -> Arc { - self.socket.clone() - } -} - -/// TCP 控制连接 -pub struct ControlConnection { - stream: TcpStream, -} - -impl ControlConnection { - pub fn new(stream: TcpStream) -> Self { - Self { stream } - } - - pub async fn send_packet(&mut self, packet: &ControlPacket) -> Result<()> { - let bytes = postcard::to_allocvec(packet)?; - self.stream.write_all(&bytes).await?; - Ok(()) - } - - pub async fn recv_packet(&mut self, buf: &mut [u8]) -> Result { - let len = self.stream.read(buf).await?; - if len == 0 { - return Err(anyhow::anyhow!("连接已关闭")); - } - let packet = postcard::from_bytes(&buf[..len])?; - Ok(packet) - } - - pub fn split( - self, - ) -> ( - tokio::net::tcp::OwnedReadHalf, - tokio::net::tcp::OwnedWriteHalf, - ) { - self.stream.into_split() - } -} - -/// 主节点网络管理器 -pub struct MasterNetwork { - listener: TcpListener, - audio: AudioSocket, -} - -impl MasterNetwork { - pub async fn setup(port: u16) -> Result { - let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?; - let audio = AudioSocket::bind().await?; - Ok(Self { listener, audio }) - } - - pub async fn accept(&self) -> Result<(ControlConnection, SocketAddr)> { - let (stream, addr) = self.listener.accept().await?; - Ok((ControlConnection::new(stream), addr)) - } - - pub fn audio_socket(&self) -> &AudioSocket { - &self.audio - } -} - -/// 从节点网络管理器 -pub struct SlaveNetwork { - control: ControlConnection, - audio: AudioSocket, -} - -impl SlaveNetwork { - pub async fn connect(master_addr: SocketAddr) -> Result { - let stream = TcpStream::connect(master_addr) - .await - .context(format!("无法连接到主节点 TCP 地址: {}", master_addr))?; - let audio = AudioSocket::bind().await?; - Ok(Self { - control: ControlConnection::new(stream), - audio, - }) - } - - pub fn split(self) -> (ControlConnection, AudioSocket) { - (self.control, self.audio) - } -} diff --git a/apps/hello/src/stereo_core/protocol.rs b/apps/hello/src/stereo_core/protocol.rs deleted file mode 100644 index c699019..0000000 --- a/apps/hello/src/stereo_core/protocol.rs +++ /dev/null @@ -1,45 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy)] -pub enum ChannelRole { - Left, - Right, -} - -impl ChannelRole { - pub fn to_string(&self) -> String { - match self { - ChannelRole::Left => "左声道".to_string(), - ChannelRole::Right => "右声道".to_string(), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum ControlPacket { - // 发现协议 - ServerHello { - udp_port: u16, // UDP 音频流端口 - }, - // 握手协议 - ClientIdentify { - role: ChannelRole, - }, - // 时间同步 (持续进行) - Ping { - client_ts: u128, - seq: u32, - }, - Pong { - client_ts: u128, - server_ts: u128, - seq: u32, - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct AudioPacket { - pub seq: u32, // 序列号,用于丢包检测 - pub timestamp: u128, // 目标播放时间 (主节点时间) - pub data: Vec, // Opus 编码数据 -} diff --git a/apps/hello/src/stereo_core/slave.rs b/apps/hello/src/stereo_core/slave.rs deleted file mode 100644 index a95122f..0000000 --- a/apps/hello/src/stereo_core/slave.rs +++ /dev/null @@ -1,190 +0,0 @@ -#![cfg(target_os = "linux")] - -use crate::audio::{AudioPlayer, OpusCodec}; -use crate::config::AudioConfig; -use crate::stereo_core::discovery::Discovery; -use crate::stereo_core::jitter_buffer::JitterBuffer; -use crate::stereo_core::network::SlaveNetwork; -use crate::stereo_core::protocol::{AudioPacket, ChannelRole, ControlPacket}; -use crate::stereo_core::sync::{ClockSync, now_us}; -use anyhow::{Result, anyhow}; -use std::sync::Arc; -use std::time::Duration; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::sync::{Mutex, mpsc}; - -/// 运行从节点模式 -pub async fn run_slave(role: ChannelRole) -> Result<()> { - println!("--- 从节点模式 ({}) ---", role.to_string()); - - loop { - match handle_connection(role.clone()).await { - Err(e) => { - eprintln!("❌ {:?}", e); - tokio::time::sleep(Duration::from_secs(3)).await; - } - Ok(_) => {} - } - } -} - -async fn handle_connection(role: ChannelRole) -> Result<()> { - // 1. 发现主节点 - println!("🔍 正在扫描主节点..."); - let (master_ip, master_tcp_port) = Discovery::discover_master().await?; - let master_tcp_addr = format!("{}:{}", master_ip, master_tcp_port); - - // 2. 建立 TCP 连接 - println!("🔥 发现主节点: {}", master_tcp_addr); - let network = SlaveNetwork::connect(master_tcp_addr.parse()?).await?; - let (mut control, audio) = network.split(); - - // 3. 身份认证 - control - .send_packet(&ControlPacket::ClientIdentify { role: role.clone() }) - .await?; - - let mut buf = [0u8; 1024]; - let pkt = control.recv_packet(&mut buf).await?; - let server_udp_port = match pkt { - ControlPacket::ServerHello { udp_port } => udp_port, - _ => return Err(anyhow!("身份认证应答异常")), - }; - - // 4. UDP 打洞 - audio - .punch(format!("{}:{}", master_ip, server_udp_port).parse()?) - .await?; - - // 5. 初始化音频与同步组件 - let config = AudioConfig { - channels: 1, - ..AudioConfig::music() - }; - let player = AudioPlayer::new(&config)?; - let mut codec = OpusCodec::new(&config)?; - let mut jitter = JitterBuffer::new(50_000); - let clock = Arc::new(Mutex::new(ClockSync::new(100))); - - // 用于通知主循环 TCP 已断开的消息通道 - let (disconnect_tx, mut disconnect_rx) = mpsc::channel::<()>(1); - - // 6. 分离 TCP 读写 - let (mut tcp_rx, mut tcp_tx) = control.split(); - let clock_updater = clock.clone(); - let d_tx_ping = disconnect_tx.clone(); - let d_tx_pong = disconnect_tx.clone(); - - // 定时发送 Ping (心跳 & 时间同步) - let _sync_handle = tokio::spawn(async move { - let mut seq = 0; - loop { - let t1 = now_us(); - let msg = ControlPacket::Ping { client_ts: t1, seq }; - let data = postcard::to_allocvec(&msg).unwrap(); - if tcp_tx.write_all(&data).await.is_err() { - let _ = d_tx_ping.send(()).await; // 通知主线程 TCP 失败 - break; - } - tokio::time::sleep(Duration::from_secs(1)).await; - seq += 1; - } - }); - - // 接收 Pong - tokio::spawn(async move { - let mut buf = [0u8; 1024]; - loop { - match tcp_rx.read(&mut buf).await { - Ok(n) if n > 0 => { - if let Ok(ControlPacket::Pong { - client_ts, - server_ts, - .. - }) = postcard::from_bytes(&buf[..n]) - { - let t4 = now_us(); - clock_updater.lock().await.update(client_ts, server_ts, t4); - } - } - _ => { - let _ = d_tx_pong.send(()).await; // TCP 断开 - break; - } - } - } - }); - - // 7. 接收音频数据包 (UDP) - let (audio_tx, mut audio_rx) = mpsc::channel(100); - let audio_socket = audio.clone_inner(); - tokio::spawn(async move { - let mut buf = [0u8; 2048]; - loop { - if let Ok((len, _)) = audio_socket.recv_from(&mut buf).await { - if let Ok(packet) = postcard::from_bytes::(&buf[..len]) { - if audio_tx.send(packet).await.is_err() { - break; - } - } - } - } - }); - - // 8. 播放主循环 - println!("✅ 主节点已连接,音频串流中..."); - let mut pcm_buf = vec![0i16; config.frame_size]; - let mut last_seq: Option = None; - let mut last_packet_time = now_us(); - - loop { - // 检查 TCP 是否已断开 - if let Ok(_) = disconnect_rx.try_recv() { - return Err(anyhow!("主节点已断开: {}", master_tcp_addr)); - } - - // 填充 Jitter Buffer - while let Ok(pkt) = audio_rx.try_recv() { - let now = now_us(); - // 如果超过 500ms 没有收到包,认为是新流开始,重置状态 - if now - last_packet_time > 500_000 { - jitter.clear(); - last_seq = None; - codec = OpusCodec::new(&config)?; - let _ = player.prepare(); - } - last_packet_time = now; - jitter.push(pkt); - } - - let now = now_us(); - let current_server_time = clock.lock().await.to_server_time(now); - - if let Some((seq, data)) = jitter.pop_frame(current_server_time) { - if let Some(last) = last_seq { - let loss_count = seq.wrapping_sub(last) as i32 - 1; - if loss_count > 0 { - // 1. 优先尝试 FEC 恢复最近丢失的那一帧 - // Opus 的 FEC 数据存储在当前包(data)中,用于恢复“前一帧” - if let Ok(len) = codec.decode_fec(&data, &mut pcm_buf) { - let _ = player.write(&pcm_buf[..len]); - } - - // 2. 如果丢包超过 1 帧,剩下的帧只能靠丢包补偿(PLC) - for _ in 0..(loss_count - 1) { - if let Ok(len) = codec.decode_loss(&mut pcm_buf) { - let _ = player.write(&pcm_buf[..len]); - } - } - } - } - last_seq = Some(seq); - - // 3. 正常解码当前帧 - let len = codec.decode(&data, &mut pcm_buf)?; - player.write(&pcm_buf[..len])?; - } else { - tokio::time::sleep(Duration::from_millis(10)).await; - } - } -} diff --git a/apps/hello/src/stereo_core/sync.rs b/apps/hello/src/stereo_core/sync.rs deleted file mode 100644 index beac2f3..0000000 --- a/apps/hello/src/stereo_core/sync.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::collections::VecDeque; -use std::time::{SystemTime, UNIX_EPOCH}; - -/// 获取当前微秒级时间戳 -pub fn now_us() -> u128 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("时间倒流") - .as_micros() -} - -/// 时钟同步管理器,用于计算主从节点间的时钟偏移 -pub struct ClockSync { - offsets: VecDeque, - pub current_offset: i128, - window_size: usize, -} - -impl ClockSync { - pub fn new(window_size: usize) -> Self { - Self { - offsets: VecDeque::with_capacity(window_size), - current_offset: 0, - window_size, - } - } - - /// 更新时钟偏移估计 - pub fn update(&mut self, client_send_ts: u128, server_ts: u128, client_recv_ts: u128) { - let rtt = (client_recv_ts - client_send_ts) as i128; - // 基础过滤:如果 RTT 过大则忽略 (例如局域网内 > 100ms) - if rtt > 100_000 { - return; - } - - // 时钟偏移 = 主节点时间 - 从节点时间 - // 假设主节点收到 Ping 的时间点在 (发送时间 + 接收时间) / 2 - let estimated_server_time = server_ts as i128 + rtt / 2; - let offset = estimated_server_time - client_recv_ts as i128; - - self.offsets.push_back(offset); - if self.offsets.len() > self.window_size { - self.offsets.pop_front(); - } - - // 计算中位数偏移,以增强抗干扰能力 - let mut sorted: Vec = self.offsets.iter().cloned().collect(); - sorted.sort_unstable(); - if !sorted.is_empty() { - self.current_offset = sorted[sorted.len() / 2]; - } - } - - /// 将本地时间转换为服务器(主节点)时间 - pub fn to_server_time(&self, client_time: u128) -> u128 { - (client_time as i128 + self.current_offset) as u128 - } - - /// 将服务器(主节点)时间转换为本地时间 - pub fn to_client_time(&self, server_time: u128) -> u128 { - (server_time as i128 - self.current_offset) as u128 - } -} diff --git a/apps/hello/src/sync.rs b/apps/hello/src/sync.rs deleted file mode 100644 index 0717c43..0000000 --- a/apps/hello/src/sync.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -pub fn now_us() -> u128 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_micros() -} - -pub struct ClockSync { - pub offset: i128, // server_time - client_time -} - -impl ClockSync { - pub fn new() -> Self { - Self { offset: 0 } - } - - pub fn update(&mut self, client_send_ts: u128, server_ts: u128, client_recv_ts: u128) { - let rtt = (client_recv_ts - client_send_ts) as i128; - // Estimated server time when client_recv_ts occurred: server_ts + rtt/2 - let estimated_server_now = server_ts as i128 + rtt / 2; - self.offset = estimated_server_now - client_recv_ts as i128; - } - - pub fn to_server_time(&self, client_time: u128) -> u128 { - (client_time as i128 + self.offset) as u128 - } - - pub fn to_client_time(&self, server_time: u128) -> u128 { - (server_time as i128 - self.offset) as u128 - } -} -