feat(stereo): 新增不同型号的小爱音箱组立体声应用
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
*.mp3
|
||||||
|
*.wav
|
||||||
Generated
+547
@@ -0,0 +1,547 @@
|
|||||||
|
# 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",
|
||||||
|
"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 = "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 = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[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 = "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 = "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 = "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",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "stereo"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"alsa",
|
||||||
|
"anyhow",
|
||||||
|
"opus",
|
||||||
|
"postcard",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "stereo"
|
||||||
|
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(target_os = "linux")'.dependencies]
|
||||||
|
alsa = "0.11"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
build:
|
||||||
|
docker run --rm -v $(shell pwd):/app idootop/open-xiaoai-runtime:oh2p \
|
||||||
|
cargo build --target armv7-unknown-linux-gnueabihf --release
|
||||||
|
|
||||||
|
# 部署到小爱音箱(调试用)
|
||||||
|
deploy:
|
||||||
|
dd if=target/armv7-unknown-linux-gnueabihf/release/stereo \
|
||||||
|
| sshpass -p open-xiaoai ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.153 "dd of=/data/stereo"
|
||||||
|
dd if=target/armv7-unknown-linux-gnueabihf/release/stereo \
|
||||||
|
| sshpass -p open-xiaoai ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.235 "dd of=/data/stereo"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Open-XiaoAI Stereo
|
||||||
|
|
||||||
|
不同型号的小爱音箱组立体声。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
完善中,敬请期待。
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#![cfg(target_os = "linux")]
|
||||||
|
|
||||||
|
use crate::app::master::run_master;
|
||||||
|
use crate::app::slave::run_slave;
|
||||||
|
use crate::net::protocol::ChannelRole;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
pub async fn run_stereo() -> Result<()> {
|
||||||
|
let args: Vec<String> = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
#![cfg(target_os = "linux")]
|
||||||
|
|
||||||
|
use crate::audio::codec::OpusCodec;
|
||||||
|
use crate::audio::config::AudioConfig;
|
||||||
|
use crate::audio::player::AudioPlayer;
|
||||||
|
use crate::net::discovery::Discovery;
|
||||||
|
use crate::net::network::{ControlConnection, MasterNetwork};
|
||||||
|
use crate::net::protocol::{AudioPacket, ChannelRole, ControlPacket};
|
||||||
|
use crate::utils::alsa::AlsaRedirector;
|
||||||
|
use crate::utils::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::<SlaveSession>::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<tokio::net::UdpSocket>,
|
||||||
|
slaves: Arc<Mutex<Vec<SlaveSession>>>,
|
||||||
|
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 stereo = ControlPacket::ServerHello {
|
||||||
|
udp_port: audio_socket.local_addr()?.port(),
|
||||||
|
};
|
||||||
|
control.send_packet(&stereo).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(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod entry;
|
||||||
|
pub mod master;
|
||||||
|
pub mod slave;
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
#![cfg(target_os = "linux")]
|
||||||
|
|
||||||
|
use crate::audio::codec::OpusCodec;
|
||||||
|
use crate::audio::config::AudioConfig;
|
||||||
|
use crate::audio::player::AudioPlayer;
|
||||||
|
use crate::net::discovery::Discovery;
|
||||||
|
use crate::net::network::SlaveNetwork;
|
||||||
|
use crate::net::protocol::{AudioPacket, ChannelRole, ControlPacket};
|
||||||
|
use crate::utils::jitter_buffer::JitterBuffer;
|
||||||
|
use crate::utils::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, 3);
|
||||||
|
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::<AudioPacket>(&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<u32> = 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use crate::audio::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<Self> {
|
||||||
|
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<usize> {
|
||||||
|
self.encoder
|
||||||
|
.encode(pcm, out)
|
||||||
|
.context("Opus encoding failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(&mut self, opus: &[u8], out: &mut [i16]) -> Result<usize> {
|
||||||
|
self.decoder
|
||||||
|
.decode(opus, out, false)
|
||||||
|
.context("Opus decoding failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 前向纠错(FEC)
|
||||||
|
pub fn decode_fec(&mut self, opus: &[u8], out: &mut [i16]) -> Result<usize> {
|
||||||
|
self.decoder
|
||||||
|
.decode(opus, out, true)
|
||||||
|
.context("Opus FEC decoding failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 丢包补偿(PLC)
|
||||||
|
pub fn decode_loss(&mut self, out: &mut [i16]) -> Result<usize> {
|
||||||
|
self.decoder
|
||||||
|
.decode(&[], out, false)
|
||||||
|
.context("Opus PLC (decode_loss) failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#[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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod codec;
|
||||||
|
pub mod config;
|
||||||
|
pub mod player;
|
||||||
|
pub mod recorder;
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#![cfg(target_os = "linux")]
|
||||||
|
|
||||||
|
use crate::audio::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<Self> {
|
||||||
|
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<usize> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#![cfg(target_os = "linux")]
|
||||||
|
|
||||||
|
use crate::audio::config::AudioConfig;
|
||||||
|
use alsa::Direction;
|
||||||
|
use alsa::pcm::{Access, Format, HwParams, PCM};
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
|
pub struct AudioRecorder {
|
||||||
|
pcm: PCM,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioRecorder {
|
||||||
|
pub fn new(config: &AudioConfig) -> Result<Self> {
|
||||||
|
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<usize> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
mod app;
|
||||||
|
mod audio;
|
||||||
|
mod net;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
crate::app::entry::run_stereo().await.unwrap();
|
||||||
|
}
|
||||||
|
println!("Only support Linux");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use crate::net::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::<ControlPacket>(&buf[..len])
|
||||||
|
{
|
||||||
|
return Ok((addr.ip(), udp_port));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod discovery;
|
||||||
|
pub mod network;
|
||||||
|
pub mod protocol;
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
use crate::net::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<UdpSocket>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioSocket {
|
||||||
|
pub async fn bind() -> Result<Self> {
|
||||||
|
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||||
|
Ok(Self {
|
||||||
|
socket: Arc::new(socket),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn local_port(&self) -> Result<u16> {
|
||||||
|
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<UdpSocket> {
|
||||||
|
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<ControlPacket> {
|
||||||
|
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<Self> {
|
||||||
|
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<Self> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
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<u8>, // Opus 编码数据
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#![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> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for AlsaRedirector {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
Self::cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::cmp::Reverse;
|
||||||
|
use std::collections::BinaryHeap;
|
||||||
|
|
||||||
|
use crate::net::protocol::AudioPacket;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct OrderedPacket {
|
||||||
|
seq: u32,
|
||||||
|
timestamp: u128,
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅针对序列号进行比较,处理回绕逻辑
|
||||||
|
impl PartialEq for OrderedPacket {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.seq == other.seq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for OrderedPacket {}
|
||||||
|
|
||||||
|
impl PartialOrd for OrderedPacket {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for OrderedPacket {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
// 使用 wrapping_sub 处理 u32 回绕 (Rollover)
|
||||||
|
let diff = self.seq.wrapping_sub(other.seq) as i32;
|
||||||
|
diff.cmp(&0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct JitterBuffer {
|
||||||
|
// 使用 Reverse 将 BinaryHeap 变为小顶堆,避免手动实现大量 Trait
|
||||||
|
buffer: BinaryHeap<Reverse<OrderedPacket>>,
|
||||||
|
last_played_seq: Option<u32>,
|
||||||
|
pub target_delay_us: u128,
|
||||||
|
min_packets: usize, // 最小缓冲包数,防止微小抖动
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JitterBuffer {
|
||||||
|
pub fn new(target_delay_us: u128, min_packets: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
buffer: BinaryHeap::with_capacity(32), // 预分配初始容量
|
||||||
|
last_played_seq: None,
|
||||||
|
target_delay_us,
|
||||||
|
min_packets,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, packet: AudioPacket) {
|
||||||
|
// 1. 处理序列号回绕的丢包逻辑
|
||||||
|
if let Some(last) = self.last_played_seq {
|
||||||
|
let diff = packet.seq.wrapping_sub(last) as i32;
|
||||||
|
if diff <= 0 {
|
||||||
|
return; // 这是一个延迟到达的旧包,直接丢弃
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buffer.push(Reverse(OrderedPacket {
|
||||||
|
seq: packet.seq,
|
||||||
|
timestamp: packet.timestamp,
|
||||||
|
data: packet.data,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop_frame(&mut self, current_time: u128) -> Option<(u32, Vec<u8>)> {
|
||||||
|
// 2. 预缓冲逻辑:如果包量太少,先不播放,等待填充
|
||||||
|
if self.buffer.len() < self.min_packets && self.last_played_seq.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查堆顶元素
|
||||||
|
if let Some(Reverse(pkt)) = self.buffer.peek() {
|
||||||
|
// 判断是否到达播放时间(考虑目标延迟)
|
||||||
|
if current_time >= pkt.timestamp + (self.target_delay_us / 1000) {
|
||||||
|
let Reverse(pkt) = self.buffer.pop().unwrap();
|
||||||
|
self.last_played_seq = Some(pkt.seq);
|
||||||
|
return Some((pkt.seq, pkt.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 如果缓冲区堆积过大,可以主动跳帧以降低延迟
|
||||||
|
pub fn shrink_to_fit_latency(&mut self, max_size: usize) {
|
||||||
|
while self.buffer.len() > max_size {
|
||||||
|
self.buffer.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.buffer.clear();
|
||||||
|
self.last_played_seq = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod alsa;
|
||||||
|
pub mod jitter_buffer;
|
||||||
|
pub mod sync;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
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<i128>,
|
||||||
|
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<i128> = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user