commit 9d86f322f2926321e8dc499aec8d92e098684a07 Author: Nell Date: Fri Jul 4 17:02:28 2025 +0200 init sync 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..75de2ce --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/ox_speak.iml b/.idea/ox_speak.iml new file mode 100644 index 0000000..cf84ae4 --- /dev/null +++ b/.idea/ox_speak.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..427ceb6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1147 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[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.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[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.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[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" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[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 = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[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 = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +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 = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[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" +version = "0.1.0" +dependencies = [ + "cpal", + "crossbeam", + "event-listener", + "opus", + "parking_lot", + "strum", + "tokio", + "uuid", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[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 = "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 = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "redox_syscall" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" + +[[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 = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +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 = "tokio" +version = "1.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4" +dependencies = [ + "backtrace", + "io-uring", + "libc", + "mio", + "pin-project-lite", + "slab", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +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 = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[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 = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..537d562 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ox_speak" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "ox_speak" +path = "src/main.rs" + +[lib] +name = "ox_speak" +path = "src/lib.rs" + +[dependencies] +cpal = "0.16" +opus = "0.3" +crossbeam = "0.8" +parking_lot = "0.12" +tokio = "1.45" +strum = {version = "0.27", features = ["derive"]} +uuid = {version = "1.17", features = ["v4", "serde"]} +event-listener = "5.4" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e137807 --- /dev/null +++ b/README.md @@ -0,0 +1,112 @@ +# ox_speak – Architecture Audio Temps Réel en Rust + +## 📦 Structure actuelle du projet + +```text +src/ +├── main.rs # Point d'entrée, instancie App et démarre l'orchestration +├── lib.rs # Déclarations des modules principaux +│ +├── app/ # Initialisation, gestion de haut niveau de l'application +│ ├── mod.rs +│ └── app.rs # Struct App : contient les stacks (audio, net) et le EventBus +│ +├── core/ # Traitements audio bas-niveau (CPAL, Opus, stats...) +│ ├── mod.rs +│ ├── capture.rs # Capture micro avec CPAL +│ ├── playback.rs # Playback via CPAL +│ ├── mixer.rs # Mixage audio +│ ├── opus.rs # Encodage/Décodage Opus +│ ├── stats.rs # Statistiques audio +│ └── rms.rs # Détection de silence via RMS +│ +├── domain/ # Types globaux et événements partagés +│ ├── mod.rs +│ └── event.rs # EventBus (struct + enum Event) pour routing interne +│ +├── network/ # Communication réseau (UDP, protocole) +│ ├── mod.rs +│ ├── udp.rs # Client UDP (envoi/réception) +│ └── protocol.rs # Types et parsing de messages réseau +│ +├── runtime/ # Logique d'exécution centrale (orchestration) +│ ├── mod.rs +│ └── dispatcher.rs # Dispatcher central (consomme les Event) +``` + +--- + +## 🛠️ Tâches à faire dans l’ordre + +1. **Compléter `domain/event.rs`** + - Définir les `enum Event` utilisés dans ton application (ex : `AudioIn`, `EncodedFrame`, etc.) + - Structurer `EventBus` avec `crossbeam_channel` + +2. **Écrire un `dispatcher` simple dans `runtime/dispatcher.rs`** + - Boucle bloquante `while let Ok(event) = rx.recv()` + - Match sur chaque type d’événement (→ faire une "god function" au début) + - Appeler directement les fonctions concernées (ex : `udp.send()`, `playback.play()`…) + +3. **Initialiser `App` dans `app/app.rs`** + - Créer une struct `App` contenant : + - le `EventBus` + - les instances des modules nécessaires (`capture`, `udp`, etc.) + - Méthode `run()` qui spawn les threads d’entrée (`audio`, `network`), et lance le `dispatcher` + +4. **Compléter les modules `core/` et `network/`** + - Rendre chaque module autonome, avec une méthode `start()` ou `run()` prenant un `Sender` + - Exemple : `capture.run(tx)` capture le micro et envoie des `AudioIn(Vec)` + +5. **Dans `main.rs`** + - Créer `App`, puis appeler `app.run()`. + +6. (optionnel) **Plus tard : découpler le dispatcher en handlers** + - Créer un `trait EventHandler` pour déléguer proprement la logique métier + - Injecter les handlers dans le dispatcher pour garder `dispatcher.rs` léger + +--- + +## 🔄 Orchestration globale (comment tout communique) + +```text +THREADS : +- capture_thread : génère AudioIn +- opus_encode_thread: encode AudioIn → EncodedFrame +- udp_send_thread : reçoit EncodedFrame → envoie réseau +- udp_recv_thread : reçoit NetIn → decode → AudioDecoded +- playback_thread : lit AudioDecoded + +TOUS utilisent : + Sender (cloné) + ↑ + EventBus central (domain/event.rs) + +RECEIVER : + dispatcher (runtime/dispatcher.rs) + → lit les events + → décide quoi faire : encode, envoyer, lire... +``` + +--- + +## 📝 Remarques + +- Tu peux rester **mono-thread** dans un premier temps pour simplifier le flow. +- L’approche actuelle (dispatcher = "god function") est très bien pour commencer **et lisible**. +- Tu peux logguer chaque événement dans le dispatcher pour debugger le flux. +- `EventBus` est cloné et injecté dans chaque module. **Aucun module ne s'appelle entre eux.** + +--- + +## 🧠 Notes de conception + +- Tu as choisi de garder la génération de la trame encodée **dans le thread de capture**, ce qui est parfaitement logique ici : tu as 20 ms de temps CPU garanti dans le callback CPAL pour encoder et publier un `Event::EncodedFrame`. +- Cette approche évite la surcharge d’un thread supplémentaire et permet une architecture **performante et simple** tant que tu restes dans les contraintes temps réel. + +--- + +## ❓Question + +> Pour le moment, elle ne se pose pas, car en réalité je vais garder l'ancienne logique du "c'est capture qui va générer la frame encodée", vu que le thread a 20 ms pour bosser entre chaque callback cpal, il a largement le temps de le faire. + +Donc aucun doute à ce stade. Si tu changes d’avis ou que la logique devient trop lourde dans `capture`, tu pourras refactorer vers un modèle où `AudioIn` est un event brut et l'encodage est fait côté dispatcher ou dans un worker dédié. diff --git a/ox_speak.zip b/ox_speak.zip new file mode 100644 index 0000000..157f2b7 Binary files /dev/null and b/ox_speak.zip differ diff --git a/src/app/app.rs b/src/app/app.rs new file mode 100644 index 0000000..767531e --- /dev/null +++ b/src/app/app.rs @@ -0,0 +1,41 @@ +use std::thread; +use crossbeam::channel::Receiver; +use crate::core::capture::AudioCapture; +use crate::domain::event::{Event, EventBus}; +use crate::runtime::dispatcher::Dispatcher; + +pub struct App { + // Communication inter-thread + event_bus: EventBus, + event_rx: Receiver, + + // audio + audio_capture: AudioCapture +} + +impl App { + pub fn new() -> Self { + // Event_bus - communication inter-components + let (event_bus, event_rx) = EventBus::new(); + + // Audio + let audio_capture = AudioCapture::default(event_bus.clone()); + + Self { + event_bus, + event_rx, + audio_capture + } + } + + pub fn start(&mut self) { + // Dispatcher - lecture et transmission des signaux inter-components + let mut dispatcher = Dispatcher::new(self.event_bus.clone()); + let event_rx = self.event_rx.clone(); + thread::spawn(move || { + dispatcher.run(event_rx) + }); + + self.audio_capture.start() + } +} \ No newline at end of file diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..02c0277 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1 @@ +pub mod app; \ No newline at end of file diff --git a/src/core/capture.rs b/src/core/capture.rs new file mode 100644 index 0000000..48a08bc --- /dev/null +++ b/src/core/capture.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread; +use std::thread::JoinHandle; +use cpal::{default_host, BufferSize, Device, SampleRate, Stream, StreamConfig, SupportedStreamConfig}; +use cpal::traits::{DeviceTrait, HostTrait}; +use crate::core::opus::AudioOpus; +use crate::domain::event::{Event, EventBus}; +use crate::utils::ringbuf::RingBuffer; + +#[derive(Clone)] +struct Microphone { + device: Device, +} + +pub struct AudioCapture { + event_bus: EventBus, + microphone: Microphone, + running: Arc, + ring_buffer: RingBuffer, + steam: Option, + worker: Option>, +} + +impl Microphone { + pub fn new(device: Device) -> Self { + Self { + device + } + } + + pub fn default() -> Self { + let host = default_host(); + let device = host.default_input_device().unwrap(); + Self::new(device) + } + + pub fn get_input_config(&self) -> SupportedStreamConfig { + self.device.default_input_config().unwrap() + } + + pub fn get_stream_config(&self) -> StreamConfig { + let config = self.get_input_config(); + let mut stream_config: StreamConfig = config.into(); + stream_config.channels = 1; + stream_config.sample_rate = SampleRate(48000); + stream_config.buffer_size = BufferSize::Fixed(960); + stream_config + } + + pub fn build_stream(&self, callback: F) -> Stream + where + F: FnMut(&[i16], &cpal::InputCallbackInfo) + Send + 'static, + { + let config = self.get_stream_config(); + + self.device.build_input_stream( + &config, + callback, + |err| println!("Error input stream: {err}"), + None + ).unwrap() + } +} + +impl AudioCapture { + pub fn new(event_bus: EventBus, microphone: Microphone) -> Self { + Self { + event_bus, + microphone, + running: Arc::new(AtomicBool::new(false)), + ring_buffer: RingBuffer::new(1024), + steam: None, + worker: None, + } + } + + pub fn default(event_bus: EventBus) -> Self { + Self::new(event_bus, Microphone::default()) + } + + pub fn start(&mut self) { + self.running.store(true, Ordering::Relaxed); + + // stream cpal + let writer = self.ring_buffer.writer(); + let stream_running = self.running.clone(); + let stream = self.microphone.build_stream(move |data, _| { + if !stream_running.load(Ordering::Relaxed){ + return; + } + writer.push_slice_overwrite(data); + }); + self.steam = Some(stream); + + // Audio processing worker + self.run_processing_worker(); + + } + + pub fn stop(&mut self) { + self.running.store(false, Ordering::Relaxed); + self.steam = None; + self.ring_buffer.force_wake_up(); + if let Some(worker) = self.worker.take() { + worker.join().unwrap(); + } + self.ring_buffer.clear(); + } + + fn run_processing_worker(&mut self){ + let worker_running = self.running.clone(); + let event_bus = self.event_bus.clone(); + let input_config = self.microphone.get_input_config(); + let opus = AudioOpus::new(input_config.sample_rate().0, input_config.channels(), "voip"); + let mut encoder = opus.create_encoder().unwrap(); + let reader = self.ring_buffer.reader(); + + self.worker = Some(thread::spawn(move || { + let mut frame = [0i16; 960]; + + while worker_running.load(Ordering::Relaxed) { + let _ = reader.pop_slice_blocking(&mut frame); + if !worker_running.load(Ordering::Relaxed){ + break; + } + + let raw_data = frame.to_vec(); + event_bus.emit(Event::AudioIn(raw_data)); + + match encoder.encode(&frame){ + Ok(encoded_data) => { + event_bus.emit(Event::AudioEncoded(encoded_data)) + } + Err(e) => { + println!("Error encoding: {e}"); + } + } + } + })); + } +} + +impl AudioCapture { + fn audio_processing(){ + + } +} \ No newline at end of file diff --git a/src/core/mixer.rs b/src/core/mixer.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..ee51f19 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,6 @@ +pub mod capture; +pub mod mixer; +pub mod opus; +pub mod playback; +pub mod rms; +pub mod stats; \ No newline at end of file diff --git a/src/core/opus.rs b/src/core/opus.rs new file mode 100644 index 0000000..8212815 --- /dev/null +++ b/src/core/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/core/playback.rs b/src/core/playback.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/core/rms.rs b/src/core/rms.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/core/stats.rs b/src/core/stats.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/domain/event.rs b/src/domain/event.rs new file mode 100644 index 0000000..0cea32f --- /dev/null +++ b/src/domain/event.rs @@ -0,0 +1,29 @@ +use crossbeam::channel::{unbounded, Sender, Receiver}; + +pub enum Event { + AudioIn(Vec), + AudioEncoded(Vec), + NetIn(Vec), + NetOut(Vec), +} + +#[derive(Clone)] +pub struct EventBus { + pub sender: Sender +} + +impl EventBus { + pub fn new() -> (Self, Receiver) { + let (sender, receiver) = unbounded(); + (Self { sender }, receiver) + } + + pub fn emit(&self, event: Event) { + // s'utilise de cette façon : bus.emit(Event::AudioIn {Vec[0,1,2,3]} + let _ = self.sender.send(event); + } + + pub fn clone_sender(&self) -> Sender { + self.sender.clone() + } +} \ No newline at end of file diff --git a/src/domain/mod.rs b/src/domain/mod.rs new file mode 100644 index 0000000..c47b0f4 --- /dev/null +++ b/src/domain/mod.rs @@ -0,0 +1 @@ +pub mod event; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ad3d89c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod app; +pub mod core; +pub mod domain; +pub mod network; +pub mod runtime; +pub mod utils; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9c6fb8f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ +// + +fn main() { + println!("Hello, world!"); +} diff --git a/src/network/mod.rs b/src/network/mod.rs new file mode 100644 index 0000000..054820a --- /dev/null +++ b/src/network/mod.rs @@ -0,0 +1,2 @@ +pub mod protocol; +pub mod udp; \ No newline at end of file diff --git a/src/network/protocol.rs b/src/network/protocol.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/network/udp.rs b/src/network/udp.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/runtime/dispatcher.rs b/src/runtime/dispatcher.rs new file mode 100644 index 0000000..cf0e40d --- /dev/null +++ b/src/runtime/dispatcher.rs @@ -0,0 +1,38 @@ +use crossbeam::channel::Receiver; + +use crate::domain::event::{Event, EventBus}; + +pub struct Dispatcher { + event_bus: EventBus +} + +impl Dispatcher { + pub fn new(event_bus: EventBus) -> Self { + Self { + event_bus + } + } + + pub fn run(&mut self, receiver: Receiver) { + while let Ok(event) = receiver.recv() { + match event { + Event::AudioIn(sample) => { + + } + Event::AudioEncoded(sample_encoded) => { + + } + Event::NetIn(sample_encoded) => { + + } + Event::NetOut(sample_encoded) => { + + } + _ => { + println!("Event non prit en charge !") + } + } + } + } +} + diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs new file mode 100644 index 0000000..ebffa3f --- /dev/null +++ b/src/runtime/mod.rs @@ -0,0 +1 @@ +pub mod dispatcher; \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..24e6965 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod ringbuf; +pub mod real_time_event; \ No newline at end of file diff --git a/src/utils/real_time_event.rs b/src/utils/real_time_event.rs new file mode 100644 index 0000000..e0ce113 --- /dev/null +++ b/src/utils/real_time_event.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() + } +} diff --git a/src/utils/ringbuf.rs b/src/utils/ringbuf.rs new file mode 100644 index 0000000..11e792e --- /dev/null +++ b/src/utils/ringbuf.rs @@ -0,0 +1,772 @@ +// Optimisé pour performance audio temps réel avec overwrite automatique +// Version améliorée avec batch processing et gestion intelligente de l'overwrite +// todo : Code généré par IA, je le comprend pas trop trop encore, à peaufiner quand je maitriserais un peu mieux Rust. + +use std::cell::UnsafeCell; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + +use crate::utils::real_time_event::RealTimeEvent; + +// ============================================================================ +// STRUCTURES PRINCIPALES +// ============================================================================ + +/// Celui qui écrit dans le buffer (producteur) +pub struct RingBufWriter { + inner: Arc>, +} + +/// Celui qui lit depuis le buffer (consommateur) +pub struct RingBufReader { + inner: Arc>, +} + +/// Le buffer circulaire interne partagé entre writer et reader +struct InnerRingBuf { + // Le buffer qui contient nos données + buffer: Vec>, + + // Position où on écrit les nouvelles données + tail: AtomicUsize, + + // Position où on lit les données + head: AtomicUsize, + + // Pour réveiller le reader quand il y a des nouvelles données + notify: RealTimeEvent, + + // Taille du buffer + cap: usize, + + // Masque pour optimiser les calculs (cap - 1) + // Au lieu de faire "index % cap", on fait "index & mask" (plus rapide) + mask: usize, +} + +// On dit à Rust que c'est safe de partager entre threads +unsafe impl Send for InnerRingBuf {} +unsafe impl Sync for InnerRingBuf {} + +// ============================================================================ +// FONCTION DE CRÉATION +// ============================================================================ + +/// Crée un nouveau ring buffer +/// IMPORTANT: cap DOIT être une puissance de 2 (2, 4, 8, 16, 32, 64, 128...) +/// Pourquoi ? Pour l'optimisation avec le masque binaire +pub fn ringbuf(cap: usize) -> (RingBufWriter, RingBufReader) { + let buffer = RingBuffer::new(cap); + buffer.split() +} + +#[derive(Clone)] +pub struct RingBuffer { + inner: Arc>, +} + +impl RingBuffer { + pub fn new(cap: usize) -> Self { + // Vérifications de sécurité + assert!(cap > 0, "La capacité doit être > 0"); + assert!(cap.is_power_of_two(), "La capacité doit être une puissance de 2 (ex: 8, 16, 32...)"); + + // Crée le buffer avec des cases vides + let mut buffer = Vec::with_capacity(cap); + for _ in 0..cap { + // UnsafeCell permet de modifier même quand c'est partagé entre threads + // On met des valeurs "poubelle" au début + buffer.push(UnsafeCell::new(unsafe { std::mem::zeroed() })); + } + + // Crée la structure interne + let inner = Arc::new(InnerRingBuf { + buffer, + tail: AtomicUsize::new(0), // On commence à écrire à l'index 0 + head: AtomicUsize::new(0), // On commence à lire à l'index 0 + notify: RealTimeEvent::new(), + cap, + mask: cap - 1, // Si cap=8, mask=7. 7 en binaire = 0111 + }); + + Self { + inner + } + } + + pub fn writer(&self) -> RingBufWriter { + RingBufWriter { inner: self.inner.clone() } + } + + pub fn reader(&self) -> RingBufReader { + RingBufReader { inner: self.inner.clone() } + } + + /// Récupère writer et reader en gardant l'accès au buffer original. + /// Utile pour : struct fields, monitoring, accès multiples. + pub fn both(&self) -> (RingBufWriter, RingBufReader) { + ( + RingBufWriter { inner: self.inner.clone() }, + RingBufReader { inner: self.inner.clone() } + ) + } + + /// Consomme le buffer et retourne writer/reader (optimisé). + /// Plus efficace que both() - évite 1 clone. + /// Utile pour : setup initial, factory functions. + pub fn split(self) -> (RingBufWriter, RingBufReader) { + ( + RingBufWriter { inner: self.inner.clone() }, + RingBufReader { inner: self.inner } // Move optimisé + ) + } + + /// 📊 Méthodes utilitaires directement sur le buffer + pub fn len(&self) -> usize { + let head = self.inner.head.load(Ordering::Relaxed); + let tail = self.inner.tail.load(Ordering::Relaxed); + (tail.wrapping_sub(head)) & self.inner.mask + } + + pub fn is_empty(&self) -> bool { + let head = self.inner.head.load(Ordering::Relaxed); + let tail = self.inner.tail.load(Ordering::Relaxed); + head == tail + } + + pub fn capacity(&self) -> usize { + self.inner.cap + } + + pub fn clear(&self) { + let tail = self.inner.tail.load(Ordering::Acquire); + self.inner.head.store(tail, Ordering::Release); + } + + pub fn force_wake_up(&self) { + self.inner.notify.notify() + } + +} + + +// ============================================================================ +// IMPLÉMENTATION DU WRITER (celui qui écrit) - VERSION OPTIMISÉE +// ============================================================================ + +impl RingBufWriter { + + /// Ajoute un élément dans le buffer + /// Si le buffer est plein, écrase les anciens éléments + pub fn push(&self, value: T) { + // 1. Récupère la position actuelle d'écriture + let tail = self.inner.tail.load(Ordering::Relaxed); + + // 2. Calcule la prochaine position (avec le masque pour optimiser) + let next_tail = (tail + 1) & self.inner.mask; + + // 3. Vérifie si on va rattraper le lecteur + let head = self.inner.head.load(Ordering::Acquire); + if next_tail == head { + // Buffer plein ! On fait avancer le head pour écraser + let new_head = (head + 1) & self.inner.mask; + self.inner.head.store(new_head, Ordering::Release); + } + + // 4. Écrit la donnée dans le buffer + unsafe { + // On écrit directement dans la case mémoire + std::ptr::write(self.inner.buffer[tail].get(), value); + } + + // 5. Met à jour la position d'écriture + self.inner.tail.store(next_tail, Ordering::Release); + + // 6. Réveille le reader s'il attend + self.inner.notify.notify(); + } + + /// ⚡ VERSION OPTIMISÉE : Ajoute plusieurs éléments d'un coup avec overwrite automatique + /// C'est LA méthode à utiliser pour l'audio temps réel ! + pub fn push_slice_overwrite(&self, data: &[T]) -> usize { + let len = data.len(); + if len == 0 { + return 0; + } + + let mask = self.inner.mask; + let tail = self.inner.tail.load(Ordering::Relaxed); + let head = self.inner.head.load(Ordering::Acquire); + + // Calcul de l'espace disponible + let current_used = (tail.wrapping_sub(head)) & mask; + let available = self.inner.cap - current_used; + + if len <= available { + // ✅ Assez de place : écriture normale batch (cas le plus fréquent) + self.push_slice_internal(data, tail) + } else { + // ⚡ Pas assez de place : OVERWRITE automatique + + // 1. Calculer combien d'éléments anciens on doit écraser + let needed_space = len - available; + + // 2. Avancer le head pour libérer exactement l'espace nécessaire + let new_head = (head + needed_space) & mask; + self.inner.head.store(new_head, Ordering::Release); + + // 3. Maintenant on a la place, écrire les nouvelles données + self.push_slice_internal(data, tail) + } + } + + /// 🚀 Méthode interne optimisée pour l'écriture batch + #[inline] + fn push_slice_internal(&self, data: &[T], tail: usize) -> usize { + let mask = self.inner.mask; + let buffer = &self.inner.buffer; + let len = data.len(); + + // Optimisation : gestion des cas où on wrap autour du buffer + let tail_pos = tail & mask; + let space_to_end = self.inner.cap - tail_pos; + + if len <= space_to_end { + // ✅ Cas simple : tout tient avant la fin du buffer + unsafe { + for (i, &item) in data.iter().enumerate() { + let pos = tail_pos + i; + std::ptr::write(buffer[pos].get(), item); + } + } + } else { + // 🔄 Cas wrap : on doit couper en deux parties + unsafe { + // Première partie : jusqu'à la fin du buffer + for (i, &item) in data[..space_to_end].iter().enumerate() { + let pos = tail_pos + i; + std::ptr::write(buffer[pos].get(), item); + } + + // Deuxième partie : depuis le début du buffer + for (i, &item) in data[space_to_end..].iter().enumerate() { + std::ptr::write(buffer[i].get(), item); + } + } + } + + // Mettre à jour tail en une seule fois (atomique) + let new_tail = (tail + len) & mask; + self.inner.tail.store(new_tail, Ordering::Release); + + // Notifier les readers + self.inner.notify.notify(); + + len + } + + /// Version classique pour compatibilité (utilise push_slice_overwrite en interne) + pub fn push_slice(&self, data: &[T]) -> usize { + self.push_slice_overwrite(data) + } + + /// Version spécialisée pour vos frames audio de 960 échantillons + /// Retourne toujours true car overwrite automatique + pub fn push_audio_frame(&self, samples: &[T]) -> bool { + self.push_slice_overwrite(samples); + true // Toujours réussi grâce à l'overwrite + } + + /// 📊 Nombre d'éléments qu'on peut écrire sans overwrite + pub fn available_space(&self) -> usize { + let head = self.inner.head.load(Ordering::Relaxed); + let tail = self.inner.tail.load(Ordering::Relaxed); + let used = (tail.wrapping_sub(head)) & self.inner.mask; + self.inner.cap - used + } + + /// 📏 Capacité totale du buffer + pub fn capacity(&self) -> usize { + self.inner.cap + } +} + +// ============================================================================ +// IMPLÉMENTATION DU READER (celui qui lit) - VERSION OPTIMISÉE +// ============================================================================ + +impl RingBufReader { + + /// Lit un élément en attendant s'il n'y en a pas (BLOQUANT) + pub fn pop_blocking(&self) -> T { + // D'abord on essaie plusieurs fois rapidement (spin) + for _ in 0..100 { + if let Some(val) = self.try_pop() { + return val; + } + // Petite pause pour ne pas surcharger le CPU + std::hint::spin_loop(); + } + + // Si toujours rien, on attend qu'on nous réveille + loop { + if let Some(val) = self.try_pop() { + return val; + } + // On attend que le writer nous réveille + self.inner.notify.wait(); + } + } + + /// Essaie de lire un élément (NON-BLOQUANT) + /// Retourne None s'il n'y a rien + pub fn try_pop(&self) -> Option { + // 1. Récupère les positions actuelles + let head = self.inner.head.load(Ordering::Relaxed); + let tail = self.inner.tail.load(Ordering::Acquire); + + // 2. Vérifie s'il y a quelque chose à lire + if head == tail { + return None; // Buffer vide + } + + // 3. Lit la donnée + let value = unsafe { + std::ptr::read(self.inner.buffer[head & self.inner.mask].get()) + }; + + // 4. Avance la position de lecture + let next_head = (head + 1) & self.inner.mask; + self.inner.head.store(next_head, Ordering::Release); + + Some(value) + } + + /// 🚀 VERSION OPTIMISÉE : Lit plusieurs éléments d'un coup dans un buffer + pub fn pop_slice(&self, output: &mut [T]) -> usize { + let head = self.inner.head.load(Ordering::Relaxed); + let tail = self.inner.tail.load(Ordering::Acquire); + + if head == tail { + return 0; // Buffer vide + } + + // Calcule combien d'éléments on peut lire + let available = (tail.wrapping_sub(head)) & self.inner.mask; + let to_read = std::cmp::min(available, output.len()); + + if to_read == 0 { + return 0; + } + + let mask = self.inner.mask; + let buffer = &self.inner.buffer; + let head_pos = head & mask; + let space_to_end = self.inner.cap - head_pos; + + if to_read <= space_to_end { + // ✅ Cas simple : tout tient avant la fin du buffer + unsafe { + for i in 0..to_read { + let pos = head_pos + i; + output[i] = std::ptr::read(buffer[pos].get()); + } + } + } else { + // 🔄 Cas wrap : on doit lire en deux parties + unsafe { + // Première partie : jusqu'à la fin du buffer + for i in 0..space_to_end { + let pos = head_pos + i; + output[i] = std::ptr::read(buffer[pos].get()); + } + + // Deuxième partie : depuis le début du buffer + let remaining = to_read - space_to_end; + for i in 0..remaining { + output[space_to_end + i] = std::ptr::read(buffer[i].get()); + } + } + } + + // Mettre à jour head en une fois + let new_head = (head + to_read) & mask; + self.inner.head.store(new_head, Ordering::Release); + + to_read + } + + /// Version bloquante pour lire exactement N éléments + pub fn pop_slice_blocking(&self, output: &mut [T]) -> usize { + let mut total_read = 0; + + while total_read < output.len() { + let read = self.pop_slice(&mut output[total_read..]); + total_read += read; + + if total_read < output.len() { + // Pas assez d'éléments, on attend + self.inner.notify.wait(); + } + } + + total_read + } + + /// Récupère les données disponibles, bloque uniquement si buffer vide + /// Combine la puissance de pop_slice (flexible) avec l'attente automatique + pub fn pop_slice_wait(&self, output: &mut [T]) -> usize { + // ⚡ Tentative non-bloquante d'abord + let read = self.pop_slice(output); + + if read > 0 { + return read; // ✅ Données disponibles + } + + // 🔔 Buffer vide - attend signal du producteur + self.inner.notify.wait(); + + // ⚡ Récupère ce qui est maintenant disponible + self.pop_slice(output) + } + + /// Vide complètement le buffer + pub fn clear(&self) { + let tail = self.inner.tail.load(Ordering::Acquire); + self.inner.head.store(tail, Ordering::Release); + } + + /// Nombre approximatif d'éléments dans le buffer + pub fn len(&self) -> usize { + let head = self.inner.head.load(Ordering::Relaxed); + let tail = self.inner.tail.load(Ordering::Relaxed); + (tail.wrapping_sub(head)) & self.inner.mask + } + + /// Le buffer est-il vide ? + pub fn is_empty(&self) -> bool { + let head = self.inner.head.load(Ordering::Relaxed); + let tail = self.inner.tail.load(Ordering::Relaxed); + head == tail + } + + /// 📏 Capacité totale du buffer + pub fn capacity(&self) -> usize { + self.inner.cap + } +} + +// ============================================================================ +// IMPLÉMENTATIONS CLONABLES (pour partager entre threads) +// ============================================================================ + +impl Clone for RingBufWriter { + fn clone(&self) -> Self { + RingBufWriter { + inner: self.inner.clone(), + } + } +} + +impl Clone for RingBufReader { + fn clone(&self) -> Self { + RingBufReader { + inner: self.inner.clone(), + } + } +} + +// ============================================================================ +// RINGBUFFER AUDIO TEMPS RÉEL - GUIDE COMPLET DES CAS D'USAGE +// ============================================================================ +/* + +CRÉATION ET CONFIGURATION : +======================== + +// Création basique (taille DOIT être puissance de 2) +let (writer, reader) = ringbuf::(1024); // Buffer basique +let (writer, reader) = ringbuf::(32768); // Audio haute qualité (~0.7s à 48kHz) +let (writer, reader) = ringbuf::(8192); // Données binaires + +// Distribution multi-threads +let buffer = RingBuffer::::new(16384); +let capture_buffer = buffer.clone(); // Pour thread capture +let encoder_buffer = buffer.clone(); // Pour thread encodage +let stats_buffer = buffer.clone(); // Pour thread statistiques + +// Récupération des endpoints +let (writer, reader) = buffer.split(); // Consomme le buffer +let writer = buffer.writer(); // Endpoint writer seul +let reader = buffer.reader(); // Endpoint reader seul + + +MÉTHODES D'ÉCRITURE (Writer) : +============================= + +// Écriture unitaire +writer.push(sample); // Bloque si buffer plein +writer.try_push(sample)?; // Non-bloquant, erreur si plein + +// Écriture batch - Mode sécurisé +let written = writer.push_slice(&samples); // Écrit tous ou aucun +writer.try_push_slice(&samples)?; // Non-bloquant + +// ⚡ Écriture batch - Mode temps réel (RECOMMANDÉ pour audio) +writer.push_slice_overwrite(&samples); // Jamais bloque, écrase les anciennes données + +// Cas d'usage par contexte : +// - Callback audio temps réel +move |audio_data: &[i16], _info| { + writer.push_slice_overwrite(audio_data); // ✅ Performance garantie +} + +// - Thread de capture manuel +loop { + let samples = microphone.read_samples()?; + writer.push_slice_overwrite(&samples); // ✅ Jamais de blocage +} + +// - Écriture conditionnelle +if buffer.available_write() >= samples.len() { + writer.push_slice(&samples); // Mode sécurisé +} else { + writer.push_slice_overwrite(&samples); // Force l'écriture +} + + +MÉTHODES DE LECTURE (Reader) : +============================= + +// Lecture unitaire +let sample = reader.pop(); // Bloque jusqu'à avoir un élément +let sample = reader.try_pop()?; // Non-bloquant, erreur si vide + +// ⚡ Lecture batch - Mode flexible (RECOMMANDÉ) +let mut buffer = vec![0i16; 960]; +let read = reader.pop_slice(&mut buffer); // Prend ce qui est dispo (0 à 960) +if read > 0 { + process_audio(&buffer[..read]); // Traite la taille réelle +} + +// Lecture batch - Mode blocking (frame exact requis) +let mut buffer = vec![0i16; 960]; +let read = reader.pop_slice_blocking(&mut buffer); // Remplit EXACTEMENT le buffer +assert_eq!(read, buffer.len()); // Toujours vrai +encode_fixed_frame(&buffer); // Encodeur exigeant 960 samples + +// ⭐ Lecture batch - Mode wait (MEILLEUR DES DEUX) +let mut buffer = vec![0i16; 960]; +let read = reader.pop_slice_wait(&mut buffer); // Prend dispo, bloque SEULEMENT si vide +if read > 0 { + process_flexible_audio(&buffer[..read]); // Taille variable OK +} + +// Lecture avec timeout +let read = reader.pop_slice_wait_timeout(&mut buffer, Duration::from_millis(10)); +match read { + 0 => println!("Timeout - pas de données"), + n => process_audio(&buffer[..n]), +} + + +CAS D'USAGE PAR DOMAINE : +======================== + +🎵 AUDIO TEMPS RÉEL : +------------------- + +// Thread capture (producteur temps réel) +move |data: &[i16], _info| { + writer.push_slice_overwrite(data); // ✅ Jamais bloque +} + +// Thread encodage (consommateur temps réel) +loop { + let mut frame = vec![0i16; 960]; // 20ms frame + let samples = reader.pop_slice_wait(&mut frame); // ✅ Prend dispo, attend si vide + + if samples >= 480 { // Au moins 10ms + encode_opus(&frame[..samples]); + } else if samples > 0 { + frame[samples..].fill(0); // Padding silence + encode_opus(&frame); + } +} + +// Thread playback (deadline critique) +move |output: &mut [i16], _info| { + let read = reader.pop_slice(output); // ✅ Non-bloquant + if read < output.len() { + output[read..].fill(0); // Underrun -> silence + } +} + +📊 TRAITEMENT BATCH NON-CRITIQUE : +--------------------------------- + +// Thread analyse (peut bloquer) +loop { + let mut chunk = vec![0i16; 4800]; // 100ms de données + let read = reader.pop_slice_blocking(&mut chunk); // ✅ OK de bloquer + analyze_frequency_spectrum(&chunk[..read]); // Traitement lourd +} + +💾 SAUVEGARDE FICHIER : +---------------------- + +let mut file = File::create("recording.raw")?; +loop { + let mut buffer = vec![0i16; 8192]; + let read = reader.pop_slice_blocking(&mut buffer); + if read == 0 { break; } // EOF + + let bytes = bytemuck::cast_slice(&buffer[..read]); + file.write_all(bytes)?; // Écrit séquentiellement +} + +🌐 RÉSEAU AVEC BUFFERISATION : +----------------------------- + +// Thread envoi réseau +loop { + let mut packet = vec![0u8; 1400]; // MTU Ethernet + let read = reader.pop_slice_wait(&mut packet); + if read > 0 { + udp_socket.send_to(&packet[..read], addr)?; + } +} + +// Thread réception réseau +loop { + let mut buffer = [0u8; 1500]; + let (size, _addr) = udp_socket.recv_from(&mut buffer)?; + writer.push_slice_overwrite(&buffer[..size]); // Peut perdre paquets +} + + +PATTERNS AVANCÉS : +================= + +🔄 MULTI-PRODUCTEUR, MULTI-CONSOMMATEUR : +---------------------------------------- + +let buffer = RingBuffer::::new(32768); + +// Plusieurs producteurs (ex: micros) +let mic1_writer = buffer.clone().writer(); +let mic2_writer = buffer.clone().writer(); + +// Plusieurs consommateurs (ex: encodage + stats) +let encoder_reader = buffer.clone().reader(); +let stats_reader = buffer.clone().reader(); + +🏭 PIPELINE AUDIO COMPLEXE : +--------------------------- + +// Capture -> Filtre -> Encodage -> Réseau +let raw_buffer = RingBuffer::::new(16384); +let filtered_buffer = RingBuffer::::new(16384); + +// Thread 1: Capture +std::thread::spawn({ + let writer = raw_buffer.writer(); + move || { + loop { + let samples = capture_audio(); + writer.push_slice_overwrite(&samples); + } + } +}); + +// Thread 2: Filtrage +std::thread::spawn({ + let reader = raw_buffer.reader(); + let writer = filtered_buffer.writer(); + move || { + let mut buffer = vec![0i16; 480]; + loop { + let read = reader.pop_slice_wait(&mut buffer); + let filtered = apply_noise_reduction(&buffer[..read]); + writer.push_slice_overwrite(&filtered); + } + } +}); + +// Thread 3: Encodage + Réseau +std::thread::spawn({ + let reader = filtered_buffer.reader(); + move || { + let mut buffer = vec![0i16; 960]; + loop { + let read = reader.pop_slice_wait(&mut buffer); + let encoded = encode_opus(&buffer[..read]); + send_to_network(&encoded); + } + } +}); + + +OPTIMISATIONS ET BONNES PRATIQUES : +================================== + +📏 SIZING DU BUFFER : +-------------------- + +// Calcul pour audio 48kHz, latence 100ms +const SAMPLE_RATE: usize = 48000; +const LATENCY_MS: usize = 100; +let buffer_size = (SAMPLE_RATE * LATENCY_MS / 1000).next_power_of_two(); +let (writer, reader) = ringbuf::(buffer_size); + +💾 GESTION MÉMOIRE : +------------------- + +// ✅ Réutiliser les buffers +let mut reusable_buffer = vec![0i16; 960]; +loop { + let read = reader.pop_slice(&mut reusable_buffer); + if read > 0 { + process_audio(&reusable_buffer[..read]); // Pas d'allocation + } +} + +// ❌ Éviter allocations répétées +loop { + let read = reader.pop_slice_wait(&mut vec![0i16; 960]); // ❌ Alloc à chaque tour +} + +📊 MONITORING ET SANTÉ : +----------------------- + +// Surveillance utilisation buffer +let usage = buffer.len() as f32 / buffer.capacity() as f32; +match usage { + x if x > 0.9 => println!("⚠️ Buffer presque plein: {:.1}%", x * 100.0), + x if x < 0.1 => println!("ℹ️ Buffer presque vide: {:.1}%", x * 100.0), + _ => {} // OK +} + +// Qualité adaptative selon charge +let quality = match usage { + x if x > 0.8 => AudioQuality::Low, // Réduire latence + x if x < 0.2 => AudioQuality::High, // Augmenter qualité + _ => AudioQuality::Medium, +}; + + +TABLEAU RÉCAPITULATIF DES MÉTHODES : +=================================== + +| CONTEXTE | ÉCRITURE | LECTURE | RAISON | +|-----------------------|-----------------------|-----------------------|---------------------------| +| Audio temps réel | push_slice_overwrite | pop_slice_wait | Performance + réactivité | +| Callback critique | push_slice_overwrite | pop_slice | Jamais bloquer | +| Traitement batch | push_slice | pop_slice_blocking | Garantie complétude | +| Réseau | push_slice_overwrite | pop_slice_wait | Robustesse + efficacité | +| Sauvegarde fichier | push_slice | pop_slice_blocking | Intégrité données | +| Pipeline flexibile | push_slice_overwrite | pop_slice_wait | Optimal général | + +🏆 VOTRE RINGBUFFER = PUISSANCE DES CHANNELS + PERFORMANCE ZERO-COPY ! 🚀 + +*/