commit e2f8bba35ebdd87cf07fb04945516e84556051c7 Author: Nell Date: Fri Jun 13 18:10:27 2025 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..68c921f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/ox_speak_rs.iml b/.idea/ox_speak_rs.iml new file mode 100644 index 0000000..cf84ae4 --- /dev/null +++ b/.idea/ox_speak_rs.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..50e86ce --- /dev/null +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..15765fa --- /dev/null +++ b/Cargo.toml @@ -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" \ No newline at end of file diff --git a/readme.MD b/readme.MD new file mode 100644 index 0000000..e69de29 diff --git a/readme_audio.md b/readme_audio.md new file mode 100644 index 0000000..4e7a77f --- /dev/null +++ b/readme_audio.md @@ -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`) 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). diff --git a/readme_reseau.MD b/readme_reseau.MD new file mode 100644 index 0000000..0ca02d5 --- /dev/null +++ b/readme_reseau.MD @@ -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) | \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9a91472 --- /dev/null +++ b/src/main.rs @@ -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)); + } + + +} \ No newline at end of file diff --git a/src/modules/audio_client.rs b/src/modules/audio_client.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/audio_opus.rs b/src/modules/audio_opus.rs new file mode 100644 index 0000000..14bf647 --- /dev/null +++ b/src/modules/audio_opus.rs @@ -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::new(self.clone()) + } + + pub fn create_decoder(&self) -> Result { + AudioOpusDecoder::new(self.clone()) + } + +} + +pub struct AudioOpusEncoder{ + audio_opus: AudioOpus, + encoder: opus::Encoder, +} + +impl AudioOpusEncoder { + fn new(audio_opus: AudioOpus) -> Result { + 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, 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) -> Result { + 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 { + 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, 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) + } +} \ No newline at end of file diff --git a/src/modules/audio_processor.back.rs b/src/modules/audio_processor.back.rs new file mode 100644 index 0000000..bd848ff --- /dev/null +++ b/src/modules/audio_processor.back.rs @@ -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, + active: bool, +} + +impl Microphone { + pub fn new(device_opt: Option) -> Result { + 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(&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) -> 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 { + 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 { + // Vérifier la compatibilité de configuration + let mut silence_percent = 1.0; // 1.0% + silence_percent = (silence_percent / 100.0); + + let ring_buffer = HeapRb::::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::::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 { + pub sender: Sender, + pub receiver: Receiver, + pub active: Arc, +} + +pub struct AudioEventBus { + raw_subscribers: Arc>>>>, + encoded_subscribers: Arc>>>>, + 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>>> { + 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>>> { + 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>) { + 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>) { + for sub in self.encoded_subscribers.iter() { + if sub.active.load(Ordering::Relaxed) { + let _ = sub.sender.try_send(data.clone()); + } + } + } +} + diff --git a/src/modules/audio_processor_in.rs b/src/modules/audio_processor_in.rs new file mode 100644 index 0000000..1c80f8a --- /dev/null +++ b/src/modules/audio_processor_in.rs @@ -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, + active: bool, +} + +impl Microphone { + pub fn new(device_opt: Option) -> Result { + 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(&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) -> 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 { + 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 { + // Vérifier la compatibilité de configuration + let mut silence_percent = 1.0; // 1.0% + silence_percent = (silence_percent / 100.0); + + let ring_buffer = HeapRb::::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::::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 { + pub sender: Sender, + pub receiver: Receiver, + pub active: Arc, +} + +pub struct AudioEventBus { + raw_subscribers: Arc>>>>, + encoded_subscribers: Arc>>>>, + 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>>> { + 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>>> { + 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>) { + 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>) { + for sub in self.encoded_subscribers.iter() { + if sub.active.load(Ordering::Relaxed) { + let _ = sub.sender.try_send(data.clone()); + } + } + } +} + diff --git a/src/modules/audio_stats.rs b/src/modules/audio_stats.rs new file mode 100644 index 0000000..6c060fc --- /dev/null +++ b/src/modules/audio_stats.rs @@ -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, // 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, +} + +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) -> Self { + let now = Self::now_nanos(); + + // Conversion i16 -> f32 (comme OBS) + let float_samples: Vec = 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 { + 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) + } +} diff --git a/src/modules/client.rs b/src/modules/client.rs new file mode 100644 index 0000000..3e054df --- /dev/null +++ b/src/modules/client.rs @@ -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, + + running: bool, + send_channel: Option>>, + recv_channel: Option>>, + handles: Vec>, +} + +impl UdpClient { + pub fn new(addr: String) -> Result { + 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> { + 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>) { + 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>) { + 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>, 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 { + let sender = self.get_audio_sender()?; + Ok(AudioHandle { + sender, + sequence: Arc::new(AtomicU32::new(0)), + }) + } + +} + +#[derive(Clone)] +pub struct Client { + socket: Arc, +} + +impl Client { + pub fn new(addr: String) -> Result { + 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}, // pas encore défini + Audio {sequence: u32, data: Vec}, + Error, // pas encore défini +} + +/// Messages client -> serveur +impl MessageCall { + pub fn serialize(&self) -> Vec { + 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) -> Self {Self::Command {command_id, data}} + pub fn audio(sequence: u32, data: Vec) -> 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}, + Audio {user_id: u32, sequence: u32, data: Vec}, + 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>, + sequence: Arc, +} + +impl AudioHandle { + pub fn new(sender: Sender>) -> Self { + Self { + sender, + sequence: Arc::new(AtomicU32::new(0)), + } + } + + pub fn send_audio_frame(&self, data: Vec) -> 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()) + } + } +} diff --git a/src/modules/client_old.rs b/src/modules/client_old.rs new file mode 100644 index 0000000..4776fc9 --- /dev/null +++ b/src/modules/client_old.rs @@ -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 +// 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 +// error (Bidirectionnel) : MessageType<2octets>Error +// +#[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 { + 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>, + pub on_hello: Option>, + pub on_bye: Option>, + pub on_command: Option>, + pub on_error: Option>, +} + +pub struct UdpClient { + socket: UdpSocket, + addr: String, + running: bool, +} + +impl UdpClient { + pub fn new(addr: String) -> Result { + 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, 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(&mut self, callback: F) -> Result<(), String> + where F: FnMut(&Vec) + 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(()) + } +} + + diff --git a/src/modules/config.rs b/src/modules/config.rs new file mode 100644 index 0000000..b457783 --- /dev/null +++ b/src/modules/config.rs @@ -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 +// } +// } \ No newline at end of file diff --git a/src/modules/mod.rs b/src/modules/mod.rs new file mode 100644 index 0000000..baf75c8 --- /dev/null +++ b/src/modules/mod.rs @@ -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; + diff --git a/src/modules/utils.rs b/src/modules/utils.rs new file mode 100644 index 0000000..e0ce113 --- /dev/null +++ b/src/modules/utils.rs @@ -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, +} + +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() + } +}