first commit

This commit is contained in:
2025-06-13 18:10:27 +02:00
commit e2f8bba35e
21 changed files with 2773 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

8
.idea/.gitignore generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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"

0
readme.MD Normal file
View File

105
readme_audio.md Normal file
View 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** laudio 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 sattend à des données audio **continues et naturelles**.
---
## ⏱️ Pas de timer à 20ms
- Ne jamais baser lenvoi des paquets audio sur une **horloge ou un cycle fixe**.
- Lencodage doit être déclenché **uniquement** par la disponibilité dune 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 dallocation après init,
- Idéal pour de l'audio pro mais limité à 1 producer / 1 consumer.
---
## 📊 Latence et performance
- Encodage Opus dune 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 dor
- 🎧 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
View 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 dun état ou dune 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
View 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));
}
}

View File

110
src/modules/audio_opus.rs Normal file
View 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)
}
}

View 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 quil 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());
}
}
}
}

View 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 quil 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
View 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
View 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
View 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
View 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
View 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
View 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()
}
}