first commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/ox_speak_rs.iml" filepath="$PROJECT_DIR$/.idea/ox_speak_rs.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
11
.idea/ox_speak_rs.iml
generated
Normal file
11
.idea/ox_speak_rs.iml
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
900
Cargo.lock
generated
Normal file
900
Cargo.lock
generated
Normal file
@@ -0,0 +1,900 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "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 = "autocfg"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cacheguard"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dbbe48daefc2575b7dfdc8227f4724582080ae48b0482191f6eb91e4cd2f405"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kanal"
|
||||
version = "0.1.1"
|
||||
source = "git+https://github.com/fereidani/kanal.git#6e5fa16f94d0bf12ef416797cd2455dc5bfc1159"
|
||||
dependencies = [
|
||||
"cacheguard",
|
||||
"futures-core",
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.6.0+11769913"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
|
||||
dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
|
||||
dependencies = [
|
||||
"objc2-encode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-audio-toolbox"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-audio"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-audio-types"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"dispatch2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||
|
||||
[[package]]
|
||||
name = "objc2-foundation"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[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 = "ox_speak_rs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"event-listener",
|
||||
"kanal",
|
||||
"num_enum",
|
||||
"opus",
|
||||
"ringbuf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[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 = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ringbuf"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "ox_speak_rs"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
opus = "0.3"
|
||||
cpal = "0.16"
|
||||
ringbuf = "0.4"
|
||||
#crossbeam = "0.8"
|
||||
#kanal = "0.1" # attente du fix sur le recv_timeout qui fait burn le cpu
|
||||
kanal = { git = "https://github.com/fereidani/kanal.git" }
|
||||
event-listener = "5.4"
|
||||
num_enum = "0.7"
|
||||
105
readme_audio.md
Normal file
105
readme_audio.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Audio Capture & Processing — Architecture Notes
|
||||
|
||||
Ce document résume les décisions techniques et bonnes pratiques concernant la capture, l'encodage, et la transmission de la voix en temps réel dans un logiciel de type Discord/TeamSpeak, basé sur `cpal` (Rust) et `Opus`.
|
||||
|
||||
---
|
||||
|
||||
## 🎤 Capture audio avec `cpal`
|
||||
|
||||
### ✅ Bonnes pratiques
|
||||
|
||||
- Utiliser `cpal` en **mode callback** pour une capture audio fiable et efficace.
|
||||
- **Ne jamais traiter** l’audio directement dans le callback.
|
||||
- Le callback doit être **minimaliste** : uniquement pousser les échantillons dans un buffer partagé/thread-safe.
|
||||
|
||||
### ❌ Mauvaises pratiques
|
||||
|
||||
- Encoder dans le callback → risque de **décrochage audio**.
|
||||
- Loguer ou dormir dans le callback → **clics, perte de données**.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Taille des frames : 960 *samples*, pas 960 *ms*
|
||||
|
||||
- Pour un encodage **Opus en 20 ms à 48 kHz mono**, il faut **960 samples par frame**.
|
||||
- Les chunks reçus de CPAL ne garantissent **jamais** d’être exactement de cette taille.
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Bufferisation : découpage rigide
|
||||
|
||||
- Mettre en place un **buffer circulaire** (ex : `VecDeque<i16>`) pour accumuler les samples reçus.
|
||||
- Dès que `buffer.len() >= 960` :
|
||||
- Extraire les 960 premiers samples,
|
||||
- Encoder via Opus,
|
||||
- Transmettre immédiatement.
|
||||
|
||||
### Pourquoi ne **pas** compléter avec des zéros ?
|
||||
- Cela introduit des **artefacts audio**,
|
||||
- L'encodeur Opus s’attend à des données audio **continues et naturelles**.
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ Pas de timer à 20ms
|
||||
|
||||
- Ne jamais baser l’envoi des paquets audio sur une **horloge ou un cycle fixe**.
|
||||
- L’encodage doit être déclenché **uniquement** par la disponibilité d’une frame complète.
|
||||
|
||||
---
|
||||
|
||||
## 🧵 Architecture multithread recommandée
|
||||
|
||||
```
|
||||
[ CPAL Callback ]
|
||||
↓
|
||||
[ Channel / Ring Buffer ]
|
||||
↓
|
||||
[ Encode Thread (Opus) ]
|
||||
↓
|
||||
[ Network Sender (UDP) ]
|
||||
```
|
||||
|
||||
- Le callback ne fait que `send(samples)` dans un channel rapide.
|
||||
- L'encodage Opus est fait dans un thread séparé dès qu'une frame de 960 samples est disponible.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Communication entre threads
|
||||
|
||||
### Option 1 – Crossbeam channel (recommandé au départ)
|
||||
```rust
|
||||
use crossbeam_channel::{bounded, Receiver, Sender};
|
||||
```
|
||||
- Solide, rapide, facile à intégrer.
|
||||
- Peut être remplacé plus tard si besoin de plus de performance.
|
||||
|
||||
### Option 2 – `ringbuf` (optimisé audio)
|
||||
- Ultra rapide, pas d’allocation après init,
|
||||
- Idéal pour de l'audio pro mais limité à 1 producer / 1 consumer.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Latence et performance
|
||||
|
||||
- Encodage Opus d’une frame 960 mono ≈ **0.1 à 0.5 ms** sur PC moderne.
|
||||
- Aucun besoin d'ajouter de délai artificiel.
|
||||
- **Pas de risque de “fractionner la voix”** si le buffer est bien géré.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Récapitulatif des règles d’or
|
||||
|
||||
- 🎧 Ne pas encoder dans le callback CPAL.
|
||||
- 🔁 Accumuler les samples dans un buffer jusqu'à 960.
|
||||
- ✂️ Ne jamais compléter une frame incomplète avec des zéros.
|
||||
- 🚫 Ne pas timer les envois → laisser le flux audio dicter le tempo.
|
||||
- 🧵 Utiliser des channels ou buffers inter-threads pour le découplage.
|
||||
|
||||
---
|
||||
|
||||
## 📌 TODO futur
|
||||
|
||||
- [ ] Comparer performance `crossbeam_channel` vs `ringbuf` dans ton usage réel.
|
||||
- [ ] Implémenter détection de silence (VAD) pour ne pas encoder à vide.
|
||||
- [ ] Ajouter sequence number + timestamp (RTP-like) dans les paquets réseau.
|
||||
- [ ] Implémenter jitter buffer côté client (récepteur).
|
||||
10
readme_reseau.MD
Normal file
10
readme_reseau.MD
Normal file
@@ -0,0 +1,10 @@
|
||||
### Message type :
|
||||
| txt | Enum | Usage | Description |
|
||||
|-----------|------|-------------------|----------------------------------------------------------|
|
||||
| keepalive | 0 | Client-only | Signal périodique pour maintenir la session active |
|
||||
| hello | 1 | Client-only | Message initial envoyé lors de la connexion |
|
||||
| bye | 2 | Client-only | Indique que le client ferme la session |
|
||||
| command | 3 | Client → Server | Requête d'action envoyée au serveur |
|
||||
| status | 4 | Server → Client | Envoi d’un état ou d’une réponse à une commande |
|
||||
| audio | 9 | Client-only | Flux audio en temps réel envoyé par le client |
|
||||
| error | 99 | Server-only | Signale une erreur côté serveur (souvent en réponse) |
|
||||
46
src/main.rs
Normal file
46
src/main.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
mod modules;
|
||||
|
||||
use std::sync::Arc;
|
||||
use modules::audio_processor_in::{Microphone, AudioCapture};
|
||||
use modules::client::UdpClient;
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
let microphone = Microphone::new(None).unwrap();
|
||||
let mut capture = AudioCapture::new(microphone).unwrap();
|
||||
let mut udpclient = UdpClient::new("127.0.0.1:5000".to_string()).unwrap();
|
||||
|
||||
// ✅ Subscribe avant start
|
||||
let raw_receiver = capture.event_bus.subscribe_raw().unwrap();
|
||||
let encoded_receiver = capture.event_bus.subscribe_encoded().unwrap();
|
||||
|
||||
capture.start_capture().unwrap();
|
||||
udpclient.start().unwrap();
|
||||
|
||||
// Créer le handle audio (zero-copy, thread-safe)
|
||||
let audio_handle = udpclient.create_audio_handle().unwrap();
|
||||
|
||||
// Thread pour traiter raw
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(data) = raw_receiver.recv() {
|
||||
// println!("📊 Raw: {} échantillons", data.len());
|
||||
}
|
||||
});
|
||||
|
||||
// Thread pour traiter encoded
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(encoded_data) = encoded_receiver.recv() {
|
||||
if let Err(e) = audio_handle.send_audio_frame((*encoded_data).clone()) {
|
||||
eprintln!("⚠️ {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
0
src/modules/audio_client.rs
Normal file
0
src/modules/audio_client.rs
Normal file
110
src/modules/audio_opus.rs
Normal file
110
src/modules/audio_opus.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use opus::{Application, Channels, Decoder, Encoder};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AudioOpus{
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
application: Application
|
||||
}
|
||||
|
||||
impl AudioOpus {
|
||||
pub fn new(sample_rate: u32, channels: u16, application: &str) -> Self {
|
||||
let application = match application {
|
||||
"voip" => Application::Voip,
|
||||
"audio" => Application::Audio,
|
||||
"lowdelay" => Application::LowDelay,
|
||||
_ => Application::Voip,
|
||||
};
|
||||
Self{sample_rate, channels, application}
|
||||
}
|
||||
|
||||
pub fn create_encoder(&self) -> Result<AudioOpusEncoder, String> {
|
||||
AudioOpusEncoder::new(self.clone())
|
||||
}
|
||||
|
||||
pub fn create_decoder(&self) -> Result<AudioOpusDecoder, String> {
|
||||
AudioOpusDecoder::new(self.clone())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub struct AudioOpusEncoder{
|
||||
audio_opus: AudioOpus,
|
||||
encoder: opus::Encoder,
|
||||
}
|
||||
|
||||
impl AudioOpusEncoder {
|
||||
fn new(audio_opus: AudioOpus) -> Result<Self, String> {
|
||||
let opus_channel = match audio_opus.channels {
|
||||
1 => Channels::Mono,
|
||||
2 => Channels::Stereo,
|
||||
_ => Channels::Mono,
|
||||
};
|
||||
let mut encoder = Encoder::new(audio_opus.sample_rate, opus_channel, audio_opus.application)
|
||||
.map_err(|e| format!("Échec de création de l'encodeur: {:?}", e))?;
|
||||
|
||||
match audio_opus.application {
|
||||
Application::Voip => {
|
||||
// Paramètres optimaux pour VoIP: bonne qualité vocale, CPU modéré
|
||||
let _ = encoder.set_bitrate(opus::Bitrate::Bits(24000)); // 24kbps est bon pour la voix
|
||||
let _ = encoder.set_vbr(true); // Variable bitrate économise du CPU
|
||||
let _ = encoder.set_vbr_constraint(false); // Sans contrainte stricte de débit
|
||||
// Pas de set_complexity (non supporté par la crate)
|
||||
},
|
||||
Application::Audio => {
|
||||
// Musique: priorité à la qualité
|
||||
let _ = encoder.set_bitrate(opus::Bitrate::Bits(64000));
|
||||
let _ = encoder.set_vbr(true);
|
||||
},
|
||||
Application::LowDelay => {
|
||||
// Priorité à la latence et l'efficacité CPU
|
||||
let _ = encoder.set_bitrate(opus::Bitrate::Bits(18000));
|
||||
let _ = encoder.set_vbr(true);
|
||||
},
|
||||
}
|
||||
Ok(Self{audio_opus, encoder})
|
||||
}
|
||||
|
||||
pub fn encode(&mut self, frames: &[i16]) -> Result<Vec<u8>, String> {
|
||||
let mut output = vec![0u8; 1276]; // 1276 octets (la vraie worst-case recommandée par Opus).
|
||||
let len = self.encoder.encode(frames, output.as_mut_slice())
|
||||
.map_err(|e| format!("Erreur encodage: {:?}", e))?;
|
||||
output.truncate(len);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
// 🔄 Approche avec buffer réutilisable (encore plus optimal)
|
||||
fn encode_reuse(&mut self, frames: &[i16], output: &mut Vec<u8>) -> Result<usize, String> {
|
||||
output.clear();
|
||||
output.resize(1276, 0);
|
||||
let len = self.encoder.encode(frames, output.as_mut_slice()).unwrap();
|
||||
output.truncate(len);
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioOpusDecoder{
|
||||
audio_opus: AudioOpus,
|
||||
decoder: opus::Decoder,
|
||||
}
|
||||
|
||||
impl AudioOpusDecoder {
|
||||
fn new(audio_opus: AudioOpus) -> Result<Self, String> {
|
||||
let opus_channel = match audio_opus.channels {
|
||||
1 => Channels::Mono,
|
||||
2 => Channels::Stereo,
|
||||
_ => Channels::Mono,
|
||||
};
|
||||
|
||||
let decoder = Decoder::new(audio_opus.sample_rate, opus_channel)
|
||||
.map_err(|e| format!("Échec de création du décodeur: {:?}", e))?;;
|
||||
Ok(Self{audio_opus, decoder})
|
||||
}
|
||||
|
||||
pub fn decode(&mut self, frames: &[u8]) -> Result<Vec<i16>, String> {
|
||||
let mut output = vec![0i16; 5760];
|
||||
let len = self.decoder.decode(frames, output.as_mut_slice(), false).map_err(|e| format!("Erreur décodage: {:?}", e))?;
|
||||
output.truncate(len);
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
384
src/modules/audio_processor.back.rs
Normal file
384
src/modules/audio_processor.back.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
use std::sync::atomic::{AtomicUsize, AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use cpal::{BufferSize, StreamConfig};
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use kanal::{unbounded, Receiver, Sender, };
|
||||
use ringbuf::{CachingCons, CachingProd, HeapRb};
|
||||
use ringbuf::traits::{Split, Producer, Consumer};
|
||||
use crate::modules::audio_opus::AudioOpus;
|
||||
use crate::modules::audio_stats::{AudioBufferWithStats, GlobalAudioStats};
|
||||
use crate::modules::utils::RealTimeEvent;
|
||||
|
||||
// todo : faciliter le système de config
|
||||
// faire en sorte que ça prenne la config par défaut de la carte audio histoire de simplifier le code.
|
||||
pub struct Microphone {
|
||||
host: cpal::Host,
|
||||
device: cpal::Device,
|
||||
stream_config: StreamConfig,
|
||||
stream: Option<cpal::Stream>,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl Microphone {
|
||||
pub fn new(device_opt: Option<cpal::Device>) -> Result<Self, String> {
|
||||
let host = cpal::default_host();
|
||||
|
||||
let device = match device_opt {
|
||||
Some(dev) => dev,
|
||||
None => host.default_input_device().ok_or_else(|| "Aucun périphérique d'entrée disponible".to_string())?
|
||||
};
|
||||
|
||||
// Obtenir la configuration par défaut du périphérique
|
||||
let input_config = device.default_input_config()
|
||||
.map_err(|e| format!("Erreur de configuration: {}", e))?;
|
||||
|
||||
let mut stream_config: StreamConfig = input_config.into();
|
||||
stream_config.channels = 1 as cpal::ChannelCount; // 1 pour mono, 2 pour stereo ; voir pour mettre en variable ?
|
||||
stream_config.sample_rate = cpal::SampleRate(48000);
|
||||
stream_config.buffer_size = BufferSize::Fixed(960);
|
||||
|
||||
|
||||
Ok(Self{host, device, stream_config, stream: None, active: false})
|
||||
}
|
||||
|
||||
pub fn start<F>(&mut self, data_callback: F) -> Result<(), String>
|
||||
where
|
||||
F: FnMut(&[i16], &cpal::InputCallbackInfo) + Send + 'static
|
||||
{
|
||||
if self.active {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let stream = self.device.build_input_stream(
|
||||
&self.stream_config,
|
||||
data_callback,
|
||||
|err| eprintln!("Erreur dans le stream audio: {}", err),
|
||||
None
|
||||
).map_err(|e| format!("Erreur lors de la création du stream: {}", e))?;
|
||||
|
||||
stream.play().map_err(|e| format!("Erreur lors du lancement du stream: {}", e))?;
|
||||
|
||||
self.stream = Some(stream);
|
||||
self.active = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.stream = None;
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
fn get_config(&self) -> (u32, u16){
|
||||
(
|
||||
self.stream_config.sample_rate.0,
|
||||
self.stream_config.channels as u16,
|
||||
)
|
||||
}
|
||||
|
||||
// Changer de périphérique
|
||||
pub fn change_device(&mut self, device_opt: Option<cpal::Device>) -> Result<(), String> {
|
||||
// Stocker l'état actuel
|
||||
let was_active = self.active;
|
||||
|
||||
// Arrêter le stream actuel
|
||||
self.stop();
|
||||
|
||||
// Sélectionner le nouveau périphérique
|
||||
self.device = match device_opt {
|
||||
Some(dev) => dev,
|
||||
None => self.host.default_input_device().ok_or_else(|| "Aucun périphérique d'entrée disponible".to_string())?
|
||||
};
|
||||
|
||||
// Mettre à jour la configuration
|
||||
let input_config = self.device.default_input_config()
|
||||
.map_err(|e| format!("Erreur de configuration: {}", e))?;
|
||||
|
||||
|
||||
// todo : Redémarrer si c'était actif
|
||||
// Note: nécessiterait de stocker le callback, ce qui complique l'implémentation
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// associated function
|
||||
fn list_available_devices() -> Vec<cpal::Device> {
|
||||
let host = cpal::default_host();
|
||||
host.input_devices()
|
||||
.map(|devices| devices.collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub struct AudioCapture {
|
||||
microphone: Microphone,
|
||||
|
||||
silence_min_square: f64,
|
||||
pub event_bus: AudioEventBus,
|
||||
}
|
||||
|
||||
impl AudioCapture {
|
||||
pub fn new(microphone: Microphone) -> Result<Self, String> {
|
||||
// Vérifier la compatibilité de configuration
|
||||
let mut silence_percent = 1.0; // 1.0%
|
||||
silence_percent = (silence_percent / 100.0);
|
||||
|
||||
let ring_buffer = HeapRb::<i16>::new(8192);
|
||||
let (mut producer, mut consumer) = ring_buffer.split();
|
||||
|
||||
Ok(Self{
|
||||
microphone,
|
||||
silence_min_square: (silence_percent * i16::MAX as f64).powi(2),
|
||||
event_bus: AudioEventBus::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_capture(&mut self) -> Result<(), String> {
|
||||
// variables nécessaires pour fonctionner
|
||||
let (sample_rate, channels) = self.microphone.get_config();
|
||||
let silence_min_square = self.silence_min_square;
|
||||
|
||||
// Buffer circulaire
|
||||
// todo : Voir pour utiliser kanal ?
|
||||
let ring_buffer = HeapRb::<i16>::new(8192);
|
||||
let (mut producer, mut consumer) = ring_buffer.split();
|
||||
|
||||
// Clone les senders pour le thread audio
|
||||
let raw_subscribers = Arc::clone(&self.event_bus.raw_subscribers);
|
||||
let encoded_subscribers = Arc::clone(&self.event_bus.encoded_subscribers);
|
||||
|
||||
// Signal partagé entre callback et worker
|
||||
let notify = RealTimeEvent::new();
|
||||
let notify_clone = notify.clone();
|
||||
|
||||
// création de l'encoder
|
||||
let audio_opus = AudioOpus::new(sample_rate, channels, "voip");
|
||||
let mut encoder = audio_opus.create_encoder()?;
|
||||
|
||||
// stats
|
||||
let global_stats = GlobalAudioStats::new();
|
||||
let stats_clone = Arc::clone(&global_stats);
|
||||
let stats_display = Arc::clone(&global_stats);
|
||||
|
||||
// fonction qui sera appelé par le callback CPAL.
|
||||
let callback = move | data: &[i16], _: &cpal::InputCallbackInfo| {
|
||||
let written = producer.push_slice(data);
|
||||
|
||||
if written > 0 {
|
||||
notify_clone.notify();
|
||||
}
|
||||
};
|
||||
|
||||
// worker qui va bosser sur les frames
|
||||
let worker = move || {
|
||||
// création du buffer
|
||||
let mut temp_buf = [0i16; 960];
|
||||
let mut read = 0;
|
||||
|
||||
loop {
|
||||
while read < 960 {
|
||||
// Essaye de lire ce qu’il peut
|
||||
let n = consumer.pop_slice(&mut temp_buf[read..]);
|
||||
if n == 0 {break;} // Plus rien à lire !
|
||||
read += n;
|
||||
}
|
||||
|
||||
if read < 960 {
|
||||
// Pas assez de données ET rien à lire maintenant → on dort
|
||||
notify.wait();
|
||||
continue; // on relance au début du loop !
|
||||
}
|
||||
// notify broadcaster
|
||||
let raw_data = Arc::new(temp_buf.to_vec());
|
||||
for subscriber in raw_subscribers.iter() {
|
||||
if subscriber.active.load(Ordering::Relaxed) {
|
||||
let _ = subscriber.sender.try_send(Arc::clone(&raw_data));
|
||||
}
|
||||
}
|
||||
|
||||
// stats
|
||||
let audio_frame = AudioBufferWithStats::new_frame(&temp_buf, Arc::clone(&stats_clone));
|
||||
|
||||
// Traitement audio ici
|
||||
if Self::is_silence(&temp_buf, silence_min_square) {
|
||||
// println!("silence !");
|
||||
}else {
|
||||
// println!("audio : {:?}", &temp_buf);
|
||||
match encoder.encode(&temp_buf){
|
||||
Ok(encoded_data) => {
|
||||
let encoded_arc = Arc::new(encoded_data);
|
||||
for subscriber in encoded_subscribers.iter() {
|
||||
if subscriber.active.load(Ordering::Relaxed) {
|
||||
let _ = subscriber.sender.try_send(Arc::clone(&encoded_arc));
|
||||
}
|
||||
}
|
||||
println!("Frame encodé: {:?} ", encoded_arc.len());
|
||||
}
|
||||
Err(e) => {
|
||||
// println!("Erreur lors de l'encodage: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
read = 0;
|
||||
}
|
||||
};
|
||||
|
||||
let stats = move || {
|
||||
loop {
|
||||
// Récupérer les stats actuelles
|
||||
let stats = stats_display.get_live_stats();
|
||||
|
||||
// Clear l'écran (optionnel)
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
|
||||
// Affichage principal
|
||||
println!("🎤 AUDIO CAPTURE - LIVE STATS");
|
||||
println!("══════════════════════════════════════════════════════");
|
||||
|
||||
// Barre de volume avec timing
|
||||
println!("{}", stats.volume_bar_with_timing(50));
|
||||
|
||||
// Analyse du timing
|
||||
println!("{}", stats.timing_analysis());
|
||||
|
||||
// Stats générales
|
||||
println!("📊 Frame #{} | Échantillons totaux: {}",
|
||||
stats.frame_count,
|
||||
stats.total_samples);
|
||||
|
||||
// Calcul du FPS audio en temps réel
|
||||
if stats.average_interval_ms > 0.0 {
|
||||
let fps = 1000.0 / stats.average_interval_ms;
|
||||
println!("🎯 FPS Audio: {:.1} | Attendu: 50.0", fps);
|
||||
}
|
||||
|
||||
// Indicateurs de qualité
|
||||
if stats.current_rms > 0.1 {
|
||||
println!("🔊 🟢 Signal fort - Capture OK");
|
||||
} else if stats.current_rms > 0.01 {
|
||||
println!("🔉 🟡 Signal faible");
|
||||
} else {
|
||||
println!("🔇 🔴 Silence ou bruit de fond");
|
||||
}
|
||||
|
||||
println!("──────────────────────────────────────────────────────");
|
||||
println!("Appuyez sur Ctrl+C pour arrêter");
|
||||
|
||||
// Mise à jour toutes les 100ms (10 FPS d'affichage)
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
thread::spawn(worker);
|
||||
// thread::spawn(stats);
|
||||
self.microphone.start(callback)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Détecte si un buffer audio contient uniquement du silence (bruit de fond).
|
||||
/// Utilise le calcul RMS (Root Mean Square) pour mesurer l'intensité sonore.
|
||||
fn is_silence(buffer: &[i16], silence_min_square: f64) -> bool {
|
||||
// 1️⃣ Calcule la somme des carrés de tous les échantillons
|
||||
let sum_squares: f64 = buffer.iter()
|
||||
.map(|&sample| (sample as f64).powi(2))
|
||||
.sum();
|
||||
|
||||
// 2️⃣ Calcule la moyenne des carrés (= puissance moyenne)
|
||||
let mean_square = sum_squares / buffer.len() as f64;
|
||||
|
||||
// 3️⃣ Compare à notre seuil précalculé
|
||||
mean_square < silence_min_square // true = silence
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Subscriber<T> {
|
||||
pub sender: Sender<T>,
|
||||
pub receiver: Receiver<T>,
|
||||
pub active: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
pub struct AudioEventBus {
|
||||
raw_subscribers: Arc<Vec<Subscriber<Arc<Vec<i16>>>>>,
|
||||
encoded_subscribers: Arc<Vec<Subscriber<Arc<Vec<u8>>>>>,
|
||||
next_raw: AtomicUsize,
|
||||
next_encoded: AtomicUsize,
|
||||
}
|
||||
|
||||
impl AudioEventBus {
|
||||
pub fn new() -> Self {
|
||||
Self::new_with_capacity(10)
|
||||
}
|
||||
|
||||
pub fn new_with_capacity(capacity: usize) -> Self {
|
||||
let mut raw_subscribers = Vec::with_capacity(capacity);
|
||||
for _ in 0..capacity {
|
||||
let (s, r) = unbounded();
|
||||
raw_subscribers.push(Subscriber {
|
||||
sender: s,
|
||||
receiver: r,
|
||||
active: Arc::new(AtomicBool::new(false)),
|
||||
});
|
||||
}
|
||||
|
||||
let mut encoded_subscribers = Vec::with_capacity(capacity);
|
||||
for _ in 0..capacity {
|
||||
let (s, r) = kanal::unbounded();
|
||||
encoded_subscribers.push(Subscriber {
|
||||
sender: s,
|
||||
receiver: r,
|
||||
active: Arc::new(AtomicBool::new(false)),
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
raw_subscribers: Arc::new(raw_subscribers),
|
||||
encoded_subscribers: Arc::new(encoded_subscribers),
|
||||
next_raw: AtomicUsize::new(0),
|
||||
next_encoded: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe à raw_data (retourne un Receiver)
|
||||
pub fn subscribe_raw(&self) -> Option<Receiver<Arc<Vec<i16>>>> {
|
||||
let idx = self.next_raw.fetch_add(1, Ordering::Relaxed);
|
||||
self.raw_subscribers.get(idx).map(|sub| {
|
||||
sub.active.store(true, Ordering::Relaxed);
|
||||
sub.receiver.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// Subscribe à encoded_data
|
||||
pub fn subscribe_encoded(&self) -> Option<Receiver<Arc<Vec<u8>>>> {
|
||||
let idx = self.next_encoded.fetch_add(1, Ordering::Relaxed);
|
||||
self.encoded_subscribers.get(idx).map(|sub| {
|
||||
sub.active.store(true, Ordering::Relaxed);
|
||||
sub.receiver.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// Broadcast raw_data à tous les consumers
|
||||
pub fn notify_raw(&self, data: Arc<Vec<i16>>) {
|
||||
for sub in self.raw_subscribers.iter() {
|
||||
if sub.active.load(Ordering::Relaxed) {
|
||||
let _ = sub.sender.try_send(data.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Broadcast encoded_data à tous les consumers
|
||||
pub fn notify_encoded(&self, data: Arc<Vec<u8>>) {
|
||||
for sub in self.encoded_subscribers.iter() {
|
||||
if sub.active.load(Ordering::Relaxed) {
|
||||
let _ = sub.sender.try_send(data.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
385
src/modules/audio_processor_in.rs
Normal file
385
src/modules/audio_processor_in.rs
Normal file
@@ -0,0 +1,385 @@
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
use std::sync::atomic::{AtomicUsize, AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use cpal::{BufferSize, StreamConfig};
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use kanal::{unbounded, Receiver, Sender, };
|
||||
use ringbuf::{CachingCons, CachingProd, HeapRb};
|
||||
use ringbuf::traits::{Split, Producer, Consumer};
|
||||
use crate::modules::audio_opus::AudioOpus;
|
||||
use crate::modules::audio_stats::{AudioBufferWithStats, GlobalAudioStats};
|
||||
use crate::modules::utils::RealTimeEvent;
|
||||
|
||||
// todo : faciliter le système de config
|
||||
// faire en sorte que ça prenne la config par défaut de la carte audio histoire de simplifier le code.
|
||||
pub struct Microphone {
|
||||
host: cpal::Host,
|
||||
device: cpal::Device,
|
||||
stream_config: StreamConfig,
|
||||
stream: Option<cpal::Stream>,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl Microphone {
|
||||
pub fn new(device_opt: Option<cpal::Device>) -> Result<Self, String> {
|
||||
let host = cpal::default_host();
|
||||
|
||||
let device = match device_opt {
|
||||
Some(dev) => dev,
|
||||
None => host.default_input_device().ok_or_else(|| "Aucun périphérique d'entrée disponible".to_string())?
|
||||
};
|
||||
|
||||
// Obtenir la configuration par défaut du périphérique
|
||||
let input_config = device.default_input_config()
|
||||
.map_err(|e| format!("Erreur de configuration: {}", e))?;
|
||||
|
||||
let mut stream_config: StreamConfig = input_config.into();
|
||||
stream_config.channels = 1 as cpal::ChannelCount; // 1 pour mono, 2 pour stereo ; voir pour mettre en variable ?
|
||||
stream_config.sample_rate = cpal::SampleRate(48000);
|
||||
stream_config.buffer_size = BufferSize::Fixed(960);
|
||||
|
||||
|
||||
Ok(Self{host, device, stream_config, stream: None, active: false})
|
||||
}
|
||||
|
||||
pub fn start<F>(&mut self, data_callback: F) -> Result<(), String>
|
||||
where
|
||||
F: FnMut(&[i16], &cpal::InputCallbackInfo) + Send + 'static
|
||||
{
|
||||
if self.active {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let stream = self.device.build_input_stream(
|
||||
&self.stream_config,
|
||||
data_callback,
|
||||
|err| eprintln!("Erreur dans le stream audio: {}", err),
|
||||
None
|
||||
).map_err(|e| format!("Erreur lors de la création du stream: {}", e))?;
|
||||
|
||||
stream.play().map_err(|e| format!("Erreur lors du lancement du stream: {}", e))?;
|
||||
|
||||
self.stream = Some(stream);
|
||||
self.active = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.stream = None;
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
fn get_config(&self) -> (u32, u16){
|
||||
(
|
||||
self.stream_config.sample_rate.0,
|
||||
self.stream_config.channels as u16,
|
||||
)
|
||||
}
|
||||
|
||||
// Changer de périphérique
|
||||
pub fn change_device(&mut self, device_opt: Option<cpal::Device>) -> Result<(), String> {
|
||||
// Stocker l'état actuel
|
||||
let was_active = self.active;
|
||||
|
||||
// Arrêter le stream actuel
|
||||
self.stop();
|
||||
|
||||
// Sélectionner le nouveau périphérique
|
||||
self.device = match device_opt {
|
||||
Some(dev) => dev,
|
||||
None => self.host.default_input_device().ok_or_else(|| "Aucun périphérique d'entrée disponible".to_string())?
|
||||
};
|
||||
|
||||
// Mettre à jour la configuration
|
||||
let input_config = self.device.default_input_config()
|
||||
.map_err(|e| format!("Erreur de configuration: {}", e))?;
|
||||
|
||||
|
||||
// todo : Redémarrer si c'était actif
|
||||
// Note: nécessiterait de stocker le callback, ce qui complique l'implémentation
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// associated function
|
||||
fn list_available_devices() -> Vec<cpal::Device> {
|
||||
let host = cpal::default_host();
|
||||
host.input_devices()
|
||||
.map(|devices| devices.collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub struct AudioCapture {
|
||||
microphone: Microphone,
|
||||
|
||||
silence_min_square: f64,
|
||||
pub event_bus: AudioEventBus,
|
||||
}
|
||||
|
||||
impl AudioCapture {
|
||||
pub fn new(microphone: Microphone) -> Result<Self, String> {
|
||||
// Vérifier la compatibilité de configuration
|
||||
let mut silence_percent = 1.0; // 1.0%
|
||||
silence_percent = (silence_percent / 100.0);
|
||||
|
||||
let ring_buffer = HeapRb::<i16>::new(8192);
|
||||
let (mut producer, mut consumer) = ring_buffer.split();
|
||||
|
||||
Ok(Self{
|
||||
microphone,
|
||||
silence_min_square: (silence_percent * i16::MAX as f64).powi(2),
|
||||
event_bus: AudioEventBus::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_capture(&mut self) -> Result<(), String> {
|
||||
// variables nécessaires pour fonctionner
|
||||
let (sample_rate, channels) = self.microphone.get_config();
|
||||
let silence_min_square = self.silence_min_square;
|
||||
|
||||
// Buffer circulaire
|
||||
// todo : Voir pour utiliser kanal ?
|
||||
let ring_buffer = HeapRb::<i16>::new(8192);
|
||||
let (mut producer, mut consumer) = ring_buffer.split();
|
||||
|
||||
// Clone les senders pour le thread audio
|
||||
let raw_subscribers = Arc::clone(&self.event_bus.raw_subscribers);
|
||||
let encoded_subscribers = Arc::clone(&self.event_bus.encoded_subscribers);
|
||||
|
||||
// Signal partagé entre callback et worker
|
||||
let notify = RealTimeEvent::new();
|
||||
let notify_clone = notify.clone();
|
||||
|
||||
// création de l'encoder
|
||||
let audio_opus = AudioOpus::new(sample_rate, channels, "voip");
|
||||
let mut encoder = audio_opus.create_encoder()?;
|
||||
|
||||
// stats
|
||||
let global_stats = GlobalAudioStats::new();
|
||||
let stats_clone = Arc::clone(&global_stats);
|
||||
let stats_display = Arc::clone(&global_stats);
|
||||
|
||||
// fonction qui sera appelé par le callback CPAL.
|
||||
let callback = move | data: &[i16], _: &cpal::InputCallbackInfo| {
|
||||
let written = producer.push_slice(data);
|
||||
|
||||
if written > 0 {
|
||||
notify_clone.notify();
|
||||
}
|
||||
};
|
||||
|
||||
// worker qui va bosser sur les frames
|
||||
let worker = move || {
|
||||
// création du buffer
|
||||
let mut temp_buf = [0i16; 960];
|
||||
let mut read = 0;
|
||||
|
||||
loop {
|
||||
while read < 960 {
|
||||
// Essaye de lire ce qu’il peut
|
||||
let n = consumer.pop_slice(&mut temp_buf[read..]);
|
||||
if n == 0 {break;} // Plus rien à lire !
|
||||
read += n;
|
||||
}
|
||||
|
||||
if read < 960 {
|
||||
// Pas assez de données ET rien à lire maintenant → on dort
|
||||
notify.wait();
|
||||
continue; // on relance au début du loop !
|
||||
}
|
||||
// notify broadcaster
|
||||
let raw_data = Arc::new(temp_buf.to_vec());
|
||||
for subscriber in raw_subscribers.iter() {
|
||||
if subscriber.active.load(Ordering::Relaxed) {
|
||||
let _ = subscriber.sender.try_send(Arc::clone(&raw_data));
|
||||
}
|
||||
}
|
||||
|
||||
// stats
|
||||
let audio_frame = AudioBufferWithStats::new_frame(&temp_buf, Arc::clone(&stats_clone));
|
||||
|
||||
// Traitement audio ici
|
||||
if Self::is_silence(&temp_buf, silence_min_square) {
|
||||
// println!("silence !");
|
||||
}else {
|
||||
// println!("audio : {:?}", &temp_buf);
|
||||
match encoder.encode(&temp_buf){
|
||||
Ok(encoded_data) => {
|
||||
let encoded_arc = Arc::new(encoded_data);
|
||||
for subscriber in encoded_subscribers.iter() {
|
||||
if subscriber.active.load(Ordering::Relaxed) {
|
||||
let _ = subscriber.sender.try_send(Arc::clone(&encoded_arc));
|
||||
}
|
||||
}
|
||||
|
||||
println!("Frame encodé: {:?} ", encoded_arc.len());
|
||||
}
|
||||
Err(e) => {
|
||||
// println!("Erreur lors de l'encodage: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
read = 0;
|
||||
}
|
||||
};
|
||||
|
||||
let stats = move || {
|
||||
loop {
|
||||
// Récupérer les stats actuelles
|
||||
let stats = stats_display.get_live_stats();
|
||||
|
||||
// Clear l'écran (optionnel)
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
|
||||
// Affichage principal
|
||||
println!("🎤 AUDIO CAPTURE - LIVE STATS");
|
||||
println!("══════════════════════════════════════════════════════");
|
||||
|
||||
// Barre de volume avec timing
|
||||
println!("{}", stats.volume_bar_with_timing(50));
|
||||
|
||||
// Analyse du timing
|
||||
println!("{}", stats.timing_analysis());
|
||||
|
||||
// Stats générales
|
||||
println!("📊 Frame #{} | Échantillons totaux: {}",
|
||||
stats.frame_count,
|
||||
stats.total_samples);
|
||||
|
||||
// Calcul du FPS audio en temps réel
|
||||
if stats.average_interval_ms > 0.0 {
|
||||
let fps = 1000.0 / stats.average_interval_ms;
|
||||
println!("🎯 FPS Audio: {:.1} | Attendu: 50.0", fps);
|
||||
}
|
||||
|
||||
// Indicateurs de qualité
|
||||
if stats.current_rms > 0.1 {
|
||||
println!("🔊 🟢 Signal fort - Capture OK");
|
||||
} else if stats.current_rms > 0.01 {
|
||||
println!("🔉 🟡 Signal faible");
|
||||
} else {
|
||||
println!("🔇 🔴 Silence ou bruit de fond");
|
||||
}
|
||||
|
||||
println!("──────────────────────────────────────────────────────");
|
||||
println!("Appuyez sur Ctrl+C pour arrêter");
|
||||
|
||||
// Mise à jour toutes les 100ms (10 FPS d'affichage)
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
thread::spawn(worker);
|
||||
// thread::spawn(stats);
|
||||
self.microphone.start(callback)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Détecte si un buffer audio contient uniquement du silence (bruit de fond).
|
||||
/// Utilise le calcul RMS (Root Mean Square) pour mesurer l'intensité sonore.
|
||||
fn is_silence(buffer: &[i16], silence_min_square: f64) -> bool {
|
||||
// 1️⃣ Calcule la somme des carrés de tous les échantillons
|
||||
let sum_squares: f64 = buffer.iter()
|
||||
.map(|&sample| (sample as f64).powi(2))
|
||||
.sum();
|
||||
|
||||
// 2️⃣ Calcule la moyenne des carrés (= puissance moyenne)
|
||||
let mean_square = sum_squares / buffer.len() as f64;
|
||||
|
||||
// 3️⃣ Compare à notre seuil précalculé
|
||||
mean_square < silence_min_square // true = silence
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Subscriber<T> {
|
||||
pub sender: Sender<T>,
|
||||
pub receiver: Receiver<T>,
|
||||
pub active: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
pub struct AudioEventBus {
|
||||
raw_subscribers: Arc<Vec<Subscriber<Arc<Vec<i16>>>>>,
|
||||
encoded_subscribers: Arc<Vec<Subscriber<Arc<Vec<u8>>>>>,
|
||||
next_raw: AtomicUsize,
|
||||
next_encoded: AtomicUsize,
|
||||
}
|
||||
|
||||
impl AudioEventBus {
|
||||
pub fn new() -> Self {
|
||||
Self::new_with_capacity(10)
|
||||
}
|
||||
|
||||
pub fn new_with_capacity(capacity: usize) -> Self {
|
||||
let mut raw_subscribers = Vec::with_capacity(capacity);
|
||||
for _ in 0..capacity {
|
||||
let (s, r) = unbounded();
|
||||
raw_subscribers.push(Subscriber {
|
||||
sender: s,
|
||||
receiver: r,
|
||||
active: Arc::new(AtomicBool::new(false)),
|
||||
});
|
||||
}
|
||||
|
||||
let mut encoded_subscribers = Vec::with_capacity(capacity);
|
||||
for _ in 0..capacity {
|
||||
let (s, r) = kanal::unbounded();
|
||||
encoded_subscribers.push(Subscriber {
|
||||
sender: s,
|
||||
receiver: r,
|
||||
active: Arc::new(AtomicBool::new(false)),
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
raw_subscribers: Arc::new(raw_subscribers),
|
||||
encoded_subscribers: Arc::new(encoded_subscribers),
|
||||
next_raw: AtomicUsize::new(0),
|
||||
next_encoded: AtomicUsize::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe à raw_data (retourne un Receiver)
|
||||
pub fn subscribe_raw(&self) -> Option<Receiver<Arc<Vec<i16>>>> {
|
||||
let idx = self.next_raw.fetch_add(1, Ordering::Relaxed);
|
||||
self.raw_subscribers.get(idx).map(|sub| {
|
||||
sub.active.store(true, Ordering::Relaxed);
|
||||
sub.receiver.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// Subscribe à encoded_data
|
||||
pub fn subscribe_encoded(&self) -> Option<Receiver<Arc<Vec<u8>>>> {
|
||||
let idx = self.next_encoded.fetch_add(1, Ordering::Relaxed);
|
||||
self.encoded_subscribers.get(idx).map(|sub| {
|
||||
sub.active.store(true, Ordering::Relaxed);
|
||||
sub.receiver.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// Broadcast raw_data à tous les consumers
|
||||
pub fn notify_raw(&self, data: Arc<Vec<i16>>) {
|
||||
for sub in self.raw_subscribers.iter() {
|
||||
if sub.active.load(Ordering::Relaxed) {
|
||||
let _ = sub.sender.try_send(data.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Broadcast encoded_data à tous les consumers
|
||||
pub fn notify_encoded(&self, data: Arc<Vec<u8>>) {
|
||||
for sub in self.encoded_subscribers.iter() {
|
||||
if sub.active.load(Ordering::Relaxed) {
|
||||
let _ = sub.sender.try_send(data.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
200
src/modules/audio_stats.rs
Normal file
200
src/modules/audio_stats.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Stats intégrées directement dans le buffer audio (style OBS)
|
||||
pub struct AudioBufferWithStats {
|
||||
// Buffer audio principal
|
||||
samples: Vec<f32>, // OBS utilise f32, plus efficace que i16 pour les calculs
|
||||
|
||||
// Métadonnées intégrées (comme OBS)
|
||||
timestamp: u64,
|
||||
peak_magnitude: f32,
|
||||
rms_magnitude: f32,
|
||||
|
||||
// Stats globales atomiques
|
||||
global_stats: Arc<GlobalAudioStats>,
|
||||
}
|
||||
|
||||
pub struct GlobalAudioStats {
|
||||
// Compteurs ultra-rapides (même technique qu'OBS)
|
||||
frame_count: AtomicU64,
|
||||
total_samples: AtomicU64,
|
||||
last_update_time: AtomicU64,
|
||||
|
||||
// Peak et RMS en format fixe pour éviter les floats atomiques
|
||||
current_peak_x1000: AtomicU32, // Peak * 1000
|
||||
current_rms_x1000: AtomicU32, // RMS * 1000
|
||||
|
||||
// Timing
|
||||
previous_frame_time: AtomicU64, // Timestamp de la frame précédente
|
||||
total_frame_intervals: AtomicU64, // Somme de tous les intervalles (en nanos)
|
||||
last_interval_nanos: AtomicU64, // Dernier intervalle calculé
|
||||
}
|
||||
|
||||
impl AudioBufferWithStats {
|
||||
pub fn new_frame(samples: &[i16], global_stats: Arc<GlobalAudioStats>) -> Self {
|
||||
let now = Self::now_nanos();
|
||||
|
||||
// Conversion i16 -> f32 (comme OBS)
|
||||
let float_samples: Vec<f32> = samples.iter()
|
||||
.map(|&s| s as f32 / i16::MAX as f32)
|
||||
.collect();
|
||||
|
||||
// Calcul vectorisé du peak/RMS
|
||||
let (peak, rms) = Self::calculate_peak_rms_simd(&float_samples);
|
||||
|
||||
// Calcul de l'intervalle entre frames
|
||||
let previous_time = global_stats.previous_frame_time.swap(now, Ordering::Relaxed);
|
||||
let interval_nanos = now.saturating_sub(previous_time);
|
||||
|
||||
// Mise à jour des stats existantes
|
||||
global_stats.frame_count.fetch_add(1, Ordering::Relaxed);
|
||||
global_stats.total_samples.fetch_add(samples.len() as u64, Ordering::Relaxed);
|
||||
global_stats.current_peak_x1000.store((peak * 1000.0) as u32, Ordering::Relaxed);
|
||||
global_stats.current_rms_x1000.store((rms * 1000.0) as u32, Ordering::Relaxed);
|
||||
global_stats.last_update_time.store(now, Ordering::Relaxed);
|
||||
|
||||
// Mise à jour des stats de timing
|
||||
global_stats.total_frame_intervals.fetch_add(interval_nanos, Ordering::Relaxed);
|
||||
global_stats.last_interval_nanos.store(interval_nanos, Ordering::Relaxed);
|
||||
|
||||
Self {
|
||||
samples: float_samples,
|
||||
timestamp: now,
|
||||
peak_magnitude: peak,
|
||||
rms_magnitude: rms,
|
||||
global_stats,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Calcul optimisé Peak/RMS (inspiré des calculs SIMD d'OBS)
|
||||
fn calculate_peak_rms_simd(samples: &[f32]) -> (f32, f32) {
|
||||
// Version Rust optimisée (le compilateur vectorise automatiquement)
|
||||
let mut peak = 0.0f32;
|
||||
let mut sum_squares = 0.0f32;
|
||||
|
||||
// Le compilateur Rust avec -O3 vectorise cette boucle automatiquement
|
||||
for &sample in samples.iter() {
|
||||
let abs_sample = sample.abs();
|
||||
peak = peak.max(abs_sample);
|
||||
sum_squares += sample * sample;
|
||||
}
|
||||
|
||||
let rms = (sum_squares / samples.len() as f32).sqrt();
|
||||
(peak, rms)
|
||||
}
|
||||
|
||||
fn now_nanos() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap().as_nanos() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl GlobalAudioStats {
|
||||
pub fn new() -> Arc<Self> {
|
||||
let now = Self::now_nanos();
|
||||
Arc::new(Self {
|
||||
frame_count: AtomicU64::new(0),
|
||||
total_samples: AtomicU64::new(0),
|
||||
last_update_time: AtomicU64::new(0),
|
||||
current_peak_x1000: AtomicU32::new(0),
|
||||
current_rms_x1000: AtomicU32::new(0),
|
||||
previous_frame_time: AtomicU64::new(now),
|
||||
total_frame_intervals: AtomicU64::new(0),
|
||||
last_interval_nanos: AtomicU64::new(0),
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
/// Lecture ultra-rapide (juste des atomiques, comme OBS)
|
||||
pub fn get_live_stats(&self) -> LiveAudioStats {
|
||||
let frame_count = self.frame_count.load(Ordering::Relaxed);
|
||||
let total_intervals = self.total_frame_intervals.load(Ordering::Relaxed);
|
||||
|
||||
// Calcul de la moyenne des intervalles
|
||||
let average_interval_nanos = if frame_count > 1 {
|
||||
total_intervals / (frame_count - 1)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
LiveAudioStats {
|
||||
frame_count,
|
||||
total_samples: self.total_samples.load(Ordering::Relaxed),
|
||||
current_peak: self.current_peak_x1000.load(Ordering::Relaxed) as f32 / 1000.0,
|
||||
current_rms: self.current_rms_x1000.load(Ordering::Relaxed) as f32 / 1000.0,
|
||||
last_update: self.last_update_time.load(Ordering::Relaxed),
|
||||
last_interval_ms: self.last_interval_nanos.load(Ordering::Relaxed) as f64 / 1_000_000.0,
|
||||
average_interval_ms: average_interval_nanos as f64 / 1_000_000.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn now_nanos() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap().as_nanos() as u64
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LiveAudioStats {
|
||||
pub frame_count: u64,
|
||||
pub total_samples: u64,
|
||||
pub current_peak: f32,
|
||||
pub current_rms: f32,
|
||||
pub last_update: u64,
|
||||
|
||||
// 🎯 NOUVEAUX: Stats de timing
|
||||
pub last_interval_ms: f64, // Dernier intervalle en millisecondes
|
||||
pub average_interval_ms: f64, // Intervalle moyen en millisecondes
|
||||
}
|
||||
|
||||
impl LiveAudioStats {
|
||||
/// Interface avec stats de timing
|
||||
pub fn volume_bar_with_timing(&self, width: usize) -> String {
|
||||
let filled = (self.current_rms * width as f32) as usize;
|
||||
let peak_pos = (self.current_peak * width as f32) as usize;
|
||||
|
||||
let mut bar = "█".repeat(filled.min(width));
|
||||
|
||||
if peak_pos < width && peak_pos > filled {
|
||||
bar.push_str(&"░".repeat(peak_pos - filled));
|
||||
bar.push('▌');
|
||||
bar.push_str(&"░".repeat(width - peak_pos - 1));
|
||||
} else {
|
||||
bar.push_str(&"░".repeat(width - filled));
|
||||
}
|
||||
|
||||
format!("[{}] RMS:{:.1}% Peak:{:.1}% | Δt:{:.1}ms (avg:{:.1}ms)",
|
||||
bar,
|
||||
self.current_rms * 100.0,
|
||||
self.current_peak * 100.0,
|
||||
self.last_interval_ms,
|
||||
self.average_interval_ms)
|
||||
}
|
||||
|
||||
/// Stats de performance détaillées
|
||||
pub fn timing_analysis(&self) -> String {
|
||||
let expected_interval = 20.0; // 20ms pour 50 FPS audio
|
||||
let jitter = (self.last_interval_ms - expected_interval).abs();
|
||||
let avg_jitter = (self.average_interval_ms - expected_interval).abs();
|
||||
|
||||
let status = if avg_jitter < 1.0 {
|
||||
"🟢 Excellent"
|
||||
} else if avg_jitter < 5.0 {
|
||||
"🟡 Correct"
|
||||
} else {
|
||||
"🔴 Problème"
|
||||
};
|
||||
|
||||
format!("⏱️ Timing: {:.1}ms | Moyenne: {:.1}ms | Jitter: {:.1}ms | {}",
|
||||
self.last_interval_ms,
|
||||
self.average_interval_ms,
|
||||
jitter,
|
||||
status)
|
||||
}
|
||||
}
|
||||
346
src/modules/client.rs
Normal file
346
src/modules/client.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
// client (websocket ou http ou les deux) (tcp) qui sera utilisé pour l'envoie de commande qui nécessite une garantie sur la réception
|
||||
|
||||
// Envoie de commande (http ?)
|
||||
// kick client
|
||||
// ban client
|
||||
// create channel
|
||||
// delete channel
|
||||
// get channel list
|
||||
// ..
|
||||
|
||||
// Reception de status (websocket ?)
|
||||
// client join
|
||||
// client leave
|
||||
// ..
|
||||
|
||||
// Etat personnel (websocket ?)
|
||||
// hello (join channel)
|
||||
// bye (leave channel)
|
||||
// ..
|
||||
|
||||
use std::net::UdpSocket;
|
||||
use std::ops::Add;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
// use crossbeam::channel::{self, Sender, Receiver, RecvTimeoutError};
|
||||
use kanal::{self, Sender, Receiver};
|
||||
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, IntoPrimitive, TryFromPrimitive)]
|
||||
pub enum MessageType {
|
||||
Keepalive = 0,
|
||||
Hello = 1,
|
||||
Bye = 2,
|
||||
Command = 3,
|
||||
Status = 4,
|
||||
Audio = 9,
|
||||
Error = 99,
|
||||
}
|
||||
|
||||
|
||||
pub struct UdpClient {
|
||||
addr: String,
|
||||
socket: Option<UdpSocket>,
|
||||
|
||||
running: bool,
|
||||
send_channel: Option<Sender<Vec<u8>>>,
|
||||
recv_channel: Option<Receiver<Vec<u8>>>,
|
||||
handles: Vec<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl UdpClient {
|
||||
pub fn new(addr: String) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
addr,
|
||||
socket: None,
|
||||
send_channel: None,
|
||||
recv_channel: None,
|
||||
handles: Vec::new(),
|
||||
running: false,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
pub fn send(&mut self, data: &[u8]) -> Result<(), String> {
|
||||
if !self.running {
|
||||
return Err("Client non démarré".to_string());
|
||||
}
|
||||
|
||||
match self.send_channel.as_ref().ok_or("Channel fermé")?.try_send(data.to_vec()){
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("Erreur envoi message : {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_recv(&mut self) -> Option<Vec<u8>> {
|
||||
if !self.running {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.recv_channel.as_ref()?
|
||||
.try_recv().ok()?
|
||||
}
|
||||
|
||||
pub fn start(&mut self) -> Result<(), String> {
|
||||
if self.running {
|
||||
return Err("Client déjà démarré !!".to_string());
|
||||
}
|
||||
|
||||
// Création socket
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")
|
||||
.map_err(|e| format!("Erreur liaison socket: {}", e))?;
|
||||
|
||||
let socket_send = socket.try_clone()
|
||||
.map_err(|e| format!("Erreur clone socket send: {}", e))?;
|
||||
let socket_recv = socket.try_clone()
|
||||
.map_err(|e| format!("Erreur clone socket recv: {}", e))?;
|
||||
let socket_keepalive = socket.try_clone()
|
||||
.map_err(|e| format!("Erreur clone socket recv: {}", e))?;
|
||||
let socket_clone = socket.try_clone()
|
||||
.map_err(|e| format!("Erreur clone socket info: {}", e))?;
|
||||
|
||||
// Channels
|
||||
let (send_tx, send_rx) = kanal::bounded(2000);
|
||||
let (recv_tx, recv_rx) = kanal::bounded(2000);
|
||||
|
||||
// Thread sender (appel de la méthode static)
|
||||
let addr_clone = self.addr.clone();
|
||||
let addr_clone2 = self.addr.clone();
|
||||
let sender_handle = thread::spawn(move || {
|
||||
Self::sender_worker(socket_send, addr_clone, send_rx);
|
||||
});
|
||||
// Thread keepalive (message toutes les 10sec)
|
||||
|
||||
let keepalive_handle = thread::spawn(move || {
|
||||
Self::keepalive_worker(socket_keepalive, addr_clone2);
|
||||
});
|
||||
|
||||
// Thread receiver (appel de la méthode static)
|
||||
let receiver_handle = thread::spawn(move || {
|
||||
Self::receiver_worker(socket_recv, recv_tx);
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Stockage
|
||||
self.socket = Some(socket);
|
||||
self.send_channel = Some(send_tx);
|
||||
self.recv_channel = Some(recv_rx);
|
||||
self.handles.push(sender_handle);
|
||||
self.handles.push(receiver_handle);
|
||||
self.running = true;
|
||||
|
||||
match socket_clone.local_addr() {
|
||||
Ok(local_addr) => println!("🚀 UdpClient démarré vers {} et écoute sur {}", self.addr, local_addr),
|
||||
Err(e) => println!("🚀 UdpClient démarré vers {} (impossible de récupérer l'adresse locale: {})", self.addr, e),
|
||||
}
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
if !self.running {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fermeture des channels
|
||||
self.send_channel = None;
|
||||
self.recv_channel = None;
|
||||
self.socket = None;
|
||||
|
||||
// Attendre que les threads se terminent
|
||||
for handle in self.handles.drain(..) {
|
||||
let _ = handle.join();
|
||||
}
|
||||
|
||||
self.running = false;
|
||||
println!("UdpClient arrêté");
|
||||
}
|
||||
|
||||
fn sender_worker(socket: UdpSocket, addr: String, rx: Receiver<Vec<u8>>) {
|
||||
println!("🚀 UDP sender worker started");
|
||||
|
||||
// Worker principal - SANS timeout (stable)
|
||||
while let Ok(data) = rx.recv() {
|
||||
if let Err(e) = socket.send_to(&data, &addr) {
|
||||
eprintln!("❌ UDP send error: {}", e);
|
||||
} else {
|
||||
println!("📤 Message sent ({} bytes)", data.len());
|
||||
}
|
||||
}
|
||||
|
||||
println!("💀 UDP sender worker stopped");
|
||||
}
|
||||
|
||||
fn receiver_worker(socket: UdpSocket, tx: Sender<Vec<u8>>) {
|
||||
let mut buf = [0u8; 1500]; // mtu 1500
|
||||
|
||||
loop {
|
||||
match socket.recv_from(&mut buf) {
|
||||
Ok((size, _addr)) => {
|
||||
let data = buf[..size].to_vec();
|
||||
// println!("📥 Reçu ({} bytes)", size);
|
||||
|
||||
if tx.try_send(data).is_err() {
|
||||
// Queue pleine, on drop (pas grave pour l'audio)
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("🔌 Receiver fermé");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn keepalive_worker(socket: UdpSocket, addr: String) {
|
||||
println!("💗 Keepalive worker started");
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
|
||||
let keepalive = (MessageType::Keepalive as u16).to_le_bytes();
|
||||
if let Err(e) = socket.send_to(&keepalive, &addr) {
|
||||
eprintln!("❌ Keepalive error: {}", e);
|
||||
break; // Socket fermé
|
||||
} else {
|
||||
println!("💗 Keepalive sent");
|
||||
}
|
||||
}
|
||||
|
||||
println!("💀 Keepalive worker stopped");
|
||||
}
|
||||
|
||||
|
||||
// helper
|
||||
// todo : voir si on peut utiliser ces méthodes autrement ou gérer ça via des interfaces, ou alors quelque chose de plus générique.
|
||||
pub fn get_audio_sender(&self) -> Result<Sender<Vec<u8>>, String> {
|
||||
if !self.running {
|
||||
return Err("Client non démarré".to_string());
|
||||
}
|
||||
|
||||
self.send_channel
|
||||
.as_ref()
|
||||
.ok_or("Channel fermé".to_string())
|
||||
.map(|s| s.clone()) // Clone du sender (gratuit pour les channels)
|
||||
}
|
||||
|
||||
pub fn create_audio_handle(&self) -> Result<AudioHandle, String> {
|
||||
let sender = self.get_audio_sender()?;
|
||||
Ok(AudioHandle {
|
||||
sender,
|
||||
sequence: Arc::new(AtomicU32::new(0)),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
socket: Arc<UdpClient>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(addr: String) -> Result<Self, String> {
|
||||
let mut client = Self {
|
||||
socket: Arc::new(UdpClient::new(addr)?),
|
||||
};
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum MessageCall {
|
||||
KeepAlive, // (0)
|
||||
Hello {user_id: u32}, // à peaufiner
|
||||
Bye,
|
||||
Command {command_id: u32, data: Vec<u8>}, // pas encore défini
|
||||
Audio {sequence: u32, data: Vec<u8>},
|
||||
Error, // pas encore défini
|
||||
}
|
||||
|
||||
/// Messages client -> serveur
|
||||
impl MessageCall {
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
match self {
|
||||
MessageCall::KeepAlive => {
|
||||
(MessageType::Keepalive as u16).to_le_bytes().to_vec()
|
||||
}
|
||||
MessageCall::Hello {user_id} => {
|
||||
buf.extend_from_slice(&(MessageType::Hello as u16).to_le_bytes());
|
||||
buf.extend_from_slice(&user_id.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
MessageCall::Bye => {
|
||||
(MessageType::Bye as u16).to_le_bytes().to_vec()
|
||||
}
|
||||
MessageCall::Command {command_id, data} => {
|
||||
buf.extend_from_slice(&(MessageType::Command as u16).to_le_bytes());
|
||||
buf.extend_from_slice(&command_id.to_le_bytes());
|
||||
buf.extend_from_slice(&data);
|
||||
buf
|
||||
}
|
||||
MessageCall::Audio {sequence, data} => {
|
||||
buf.extend_from_slice(&(MessageType::Audio as u16).to_le_bytes());
|
||||
buf.extend_from_slice(&sequence.to_le_bytes());
|
||||
buf.extend_from_slice(&data);
|
||||
buf
|
||||
}
|
||||
MessageCall::Error => {
|
||||
(MessageType::Error as u16).to_le_bytes().to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helpers de construction
|
||||
pub fn keepalive() -> Self {Self::KeepAlive}
|
||||
pub fn hello(user_id: u32) -> Self {Self::Hello {user_id}}
|
||||
pub fn bye() -> Self {Self::Bye}
|
||||
pub fn command(command_id: u32, data: Vec<u8>) -> Self {Self::Command {command_id, data}}
|
||||
pub fn audio(sequence: u32, data: Vec<u8>) -> Self {Self::Audio {sequence, data}}
|
||||
pub fn error() -> Self {Self::Error}
|
||||
}
|
||||
|
||||
/// Messages serveur -> client
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum MessageEvent {
|
||||
Status {user_id: u32, channel_id: u32, data: Vec<u8>},
|
||||
Audio {user_id: u32, sequence: u32, data: Vec<u8>},
|
||||
Error,
|
||||
}
|
||||
|
||||
impl MessageEvent {
|
||||
|
||||
}
|
||||
|
||||
// todo : voir si on peut utiliser ces méthodes autrement ou gérer ça via des interfaces, ou alors quelque chose de plus générique.
|
||||
pub struct AudioHandle {
|
||||
sender: Sender<Vec<u8>>,
|
||||
sequence: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
impl AudioHandle {
|
||||
pub fn new(sender: Sender<Vec<u8>>) -> Self {
|
||||
Self {
|
||||
sender,
|
||||
sequence: Arc::new(AtomicU32::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_audio_frame(&self, data: Vec<u8>) -> Result<(), String> {
|
||||
let seq = self.sequence.fetch_add(1, Ordering::Relaxed);
|
||||
let message = MessageCall::audio(seq, data);
|
||||
let serialized = message.serialize();
|
||||
|
||||
match self.sender.try_send(serialized) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err("Queue full".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/modules/client_old.rs
Normal file
133
src/modules/client_old.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use std::net::UdpSocket;
|
||||
use std::thread;
|
||||
|
||||
// Le message peut être variable, mais il est certain que le "base" sera à ce format
|
||||
// base : MessageType<2 octets>MessagePayload<n octets>
|
||||
// call hello : MessageType<2 octets>MessageId<4 octets>Token<16octets>
|
||||
// response hello : MessageType<2 octets>MessageId<4octets>Success<1 octet>
|
||||
// call bye : MessageType<2 octets>MessageId<4octets>
|
||||
// response bye : MessageType<2 octets>MessageId<4 octets>Success<1 octet>
|
||||
// call command : MessageType<2 octets>Command<32 octets> (peut-être voir pour être tcp sur cet aspect non nécessiteux de réactivité, à voir comment font ts,discord,mumble...)
|
||||
// response command : MessageType<2 octets>MessageId<4 octets>Success<1 octet>
|
||||
//
|
||||
// audio (no response) : MessageType<2octets>Incremental<4octets>Audio<n octets>
|
||||
// error (Bidirectionnel) : MessageType<2octets>Error<n octets>
|
||||
//
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum MessageType {
|
||||
Keepalive = 0,
|
||||
Hello = 1,
|
||||
Bye = 2,
|
||||
Command = 3,
|
||||
Status = 4,
|
||||
Audio = 9,
|
||||
Error = 99,
|
||||
}
|
||||
|
||||
impl MessageType {
|
||||
pub fn from_u16(value: u16) -> Option<Self> {
|
||||
match value {
|
||||
0 => Some(MessageType::Keepalive),
|
||||
1 => Some(MessageType::Hello),
|
||||
2 => Some(MessageType::Bye),
|
||||
3 => Some(MessageType::Command),
|
||||
4 => Some(MessageType::Audio),
|
||||
99 => Some(MessageType::Error),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum CommandType {
|
||||
Hello = 0,
|
||||
Bye = 1,
|
||||
Command = 2,
|
||||
Audio = 3,
|
||||
}
|
||||
|
||||
// faire en sorte que on_audio soit traité dans un autre thread, les autres peuvent être traité dans un thread plus générale, plus lent.
|
||||
pub struct MessageCallbacks {
|
||||
pub on_audio: Option<Box<dyn FnMut(&[u8]) + Send + 'static>>,
|
||||
pub on_hello: Option<Box<dyn FnMut(&[u8]) + Send + 'static>>,
|
||||
pub on_bye: Option<Box<dyn FnMut(&[u8]) + Send + 'static>>,
|
||||
pub on_command: Option<Box<dyn FnMut(&[u8]) + Send + 'static>>,
|
||||
pub on_error: Option<Box<dyn FnMut(&[u8]) + Send + 'static>>,
|
||||
}
|
||||
|
||||
pub struct UdpClient {
|
||||
socket: UdpSocket,
|
||||
addr: String,
|
||||
running: bool,
|
||||
}
|
||||
|
||||
impl UdpClient {
|
||||
pub fn new(addr: String) -> Result<Self, String> {
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")
|
||||
.map_err(|e| format!("Erreur liaison socket: {}", e))?;
|
||||
|
||||
socket.set_read_timeout(Some(std::time::Duration::from_secs(1))).map_err(|e| format!("Erreur timeout : {}", e))?;
|
||||
|
||||
Ok(Self {
|
||||
socket,
|
||||
addr,
|
||||
running: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn send_data(&self, data: &[u8]) -> Result<(), String> {
|
||||
self.socket.send_to(data, &self.addr).map_err(|e| format!("Erreur envoi : {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn receive_data(&self) -> Result<Vec<u8>, String> {
|
||||
let mut buf = [0; 1024];
|
||||
self.socket.recv_from(&mut buf).map_err(|e| format!("Erreur reception : {}", e))?;
|
||||
Ok(buf.to_vec())
|
||||
}
|
||||
|
||||
fn listener<F>(&mut self, callback: F) -> Result<(), String>
|
||||
where F: FnMut(&Vec<u8>) + Send + 'static {
|
||||
if self.running {
|
||||
return Err("Client déjà en cours d'éxecution".to_string());
|
||||
}
|
||||
|
||||
let socket_clone = self.socket.try_clone().map_err(|e| format!("Erreur clone socket : {}", e))?;
|
||||
|
||||
let worker = move || {
|
||||
let mut buffer = [0u8; 1500];
|
||||
loop {
|
||||
match socket_clone.recv(&mut buffer) {
|
||||
Ok(size) => {
|
||||
if size >= 2 {
|
||||
let message_type_raw = u16::from_le_bytes([buffer[0], buffer[1]]);
|
||||
if let Some(message_type) = MessageType::from_u16(message_type_raw) {
|
||||
match message_type {
|
||||
MessageType::Audio => {
|
||||
},
|
||||
_ => {
|
||||
//default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Erreur reception : {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
thread::spawn(worker);
|
||||
self.running = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
49
src/modules/config.rs
Normal file
49
src/modules/config.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
// #[derive(Clone, Debug)]
|
||||
// pub struct AudioConfig {
|
||||
// pub sample_rate: u32,
|
||||
// pub channels: u8,
|
||||
// pub frame_frequency_ms: u32,
|
||||
// }
|
||||
//
|
||||
// impl Default for AudioConfig {
|
||||
// fn default() -> Self {
|
||||
// Self::new(48000, 1, 20)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl AudioConfig {
|
||||
// pub fn new(sample_rate: u32, channels: u8, frame_frequency_ms: u32) -> Self {
|
||||
// assert!((8000..=96000).contains(&sample_rate),
|
||||
// "Taux d'échantillonnage non supporté: {}. Utilisez entre 8000 et 96000 Hz",
|
||||
// sample_rate);
|
||||
// assert!([1, 2].contains(&channels), "Nombre de canaux non supporté: {}. Utilisez 1 (mono) ou 2 (stéréo)",
|
||||
// channels);
|
||||
// assert!((5..=1000).contains(&frame_frequency_ms), "Taille de frame non supportée: {} ms. Utilisez entre 5 et 1000 ms",
|
||||
// frame_frequency_ms);
|
||||
//
|
||||
// Self {sample_rate, channels, frame_frequency_ms}
|
||||
// }
|
||||
//
|
||||
// // Méthodes de construction avec valeurs par défaut
|
||||
// pub fn with_sample_rate(mut self, sample_rate: u32) -> Self {
|
||||
// assert!((8000..=96000).contains(&sample_rate),
|
||||
// "Taux d'échantillonnage non supporté: {}. Utilisez entre 8000 et 96000 Hz",
|
||||
// sample_rate);
|
||||
// self.sample_rate = sample_rate;
|
||||
// self
|
||||
// }
|
||||
//
|
||||
// pub fn with_channels(mut self, channels: u8) -> Self {
|
||||
// assert!([1, 2].contains(&channels), "Nombre de canaux non supporté: {}. Utilisez 1 (mono) ou 2 (stéréo)",
|
||||
// channels);
|
||||
// self.channels = channels;
|
||||
// self
|
||||
// }
|
||||
//
|
||||
// pub fn with_frame_frequency_ms(mut self, frame_frequency_ms: u32) -> Self {
|
||||
// assert!((5..=1000).contains(&frame_frequency_ms), "Taille de frame non supportée: {} ms. Utilisez entre 5 et 1000 ms",
|
||||
// frame_frequency_ms);
|
||||
// self.frame_frequency_ms = frame_frequency_ms;
|
||||
// self
|
||||
// }
|
||||
// }
|
||||
11
src/modules/mod.rs
Normal file
11
src/modules/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub mod audio_processor_in;
|
||||
pub mod config;
|
||||
pub mod client;
|
||||
mod audio_opus;
|
||||
mod utils;
|
||||
mod audio_stats;
|
||||
// mod client_old;
|
||||
mod audio_client;
|
||||
|
||||
// mod back;
|
||||
|
||||
46
src/modules/utils.rs
Normal file
46
src/modules/utils.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use event_listener::{Event, Listener};
|
||||
|
||||
|
||||
struct RealTimeEventInner{
|
||||
flag: AtomicBool,
|
||||
event: Event,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealTimeEvent {
|
||||
inner: Arc<RealTimeEventInner>,
|
||||
}
|
||||
|
||||
impl RealTimeEvent{
|
||||
pub fn new() -> Self{
|
||||
Self{
|
||||
inner: Arc::new(RealTimeEventInner{
|
||||
flag: AtomicBool::new(false),
|
||||
event: Event::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify(&self){
|
||||
self.inner.flag.store(true, Ordering::Release);
|
||||
self.inner.event.notify(usize::MAX);
|
||||
}
|
||||
|
||||
pub fn wait(&self){
|
||||
loop {
|
||||
let listener = self.inner.event.listen();
|
||||
if self.inner.flag.swap(false, Ordering::Acquire){
|
||||
break
|
||||
}
|
||||
listener.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RealTimeEvent{
|
||||
fn default() -> Self{
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user