diff --git a/Cargo.lock b/Cargo.lock index 50e86ce..5bda297 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # 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" @@ -41,6 +56,21 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[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" @@ -65,12 +95,6 @@ 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" @@ -205,6 +229,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[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.3" @@ -256,9 +286,9 @@ dependencies = [ [[package]] name = "kanal" version = "0.1.1" -source = "git+https://github.com/fereidani/kanal.git#6e5fa16f94d0bf12ef416797cd2455dc5bfc1159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3953adf0cd667798b396c2fa13552d6d9b3269d7dd1154c4c416442d1ff574" dependencies = [ - "cacheguard", "futures-core", "lock_api", ] @@ -300,6 +330,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "ndk" version = "0.9.0" @@ -442,6 +481,15 @@ 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" @@ -467,7 +515,8 @@ dependencies = [ "kanal", "num_enum", "opus", - "ringbuf", + "parking_lot", + "tokio", ] [[package]] @@ -476,6 +525,29 @@ 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" @@ -488,21 +560,6 @@ 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" @@ -531,16 +588,20 @@ dependencies = [ ] [[package]] -name = "ringbuf" -version = "0.4.8" +name = "redox_syscall" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ - "crossbeam-utils", - "portable-atomic", - "portable-atomic-util", + "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" @@ -568,6 +629,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "syn" version = "2.0.101" @@ -599,6 +666,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "pin-project-lite", +] + [[package]] name = "toml_datetime" version = "0.6.9" diff --git a/Cargo.toml b/Cargo.toml index 15765fa..a97e7df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,12 +3,23 @@ name = "ox_speak_rs" version = "0.1.0" edition = "2024" +[[bin]] +name = "ox_speak_rs" +path = "src/main.rs" + +[lib] +name = "ox_speak_rs" +path = "src/lib.rs" + [dependencies] opus = "0.3" cpal = "0.16" -ringbuf = "0.4" +#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" } +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 +num_enum = "0.7" +parking_lot = "0.12" +tokio = "1.45" +#rtrb = "0.3" \ No newline at end of file diff --git a/src/app/context.rs b/src/app/context.rs new file mode 100644 index 0000000..cc41e8c --- /dev/null +++ b/src/app/context.rs @@ -0,0 +1,27 @@ +// Logique passive de l'application avec les resources globales + +use std::sync::Arc; + +pub struct Context { + host: Arc, + // todo : mettre tous les contextes de l'app nécessaire au bon fonctionnement de celle ci, udpclient, catpure audio, lecture audio .... + // L'idéal étant que tout soit chargé "statiquement" dans le contexte de l'application pour être "relié" dans le runtime +} + +impl Context { + pub fn new() -> Self { + let host = Arc::new(cpal::default_host()); + + // Arc::new(UdpClient::new()) + // Arc::new(AudioCapture::new()) + // etc ... + + Self { + host + } + } + + pub fn get_host(&self) -> Arc { + self.host.clone() + } +} \ No newline at end of file diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..00f0279 --- /dev/null +++ b/src/app/mod.rs @@ -0,0 +1,2 @@ +pub mod context; +pub mod runtime; \ No newline at end of file diff --git a/src/app/runtime.rs b/src/app/runtime.rs new file mode 100644 index 0000000..bbf21f2 --- /dev/null +++ b/src/app/runtime.rs @@ -0,0 +1,20 @@ +// Logiques actives (connexion des différents composants, création des threads ...) + +use std::sync::Arc; +use crate::app::context::Context; + +pub struct Runtime{ + context: Arc +} + +impl Runtime { + pub fn new(context: Arc) -> Runtime { + Self { + context + } + } + + pub fn run(&self) { + + } +} diff --git a/src/audio/capture.rs b/src/audio/capture.rs new file mode 100644 index 0000000..3f9b0ec --- /dev/null +++ b/src/audio/capture.rs @@ -0,0 +1,147 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread; +use std::thread::JoinHandle; +use cpal::{BufferSize, Device, Host, Stream, StreamConfig, SupportedStreamConfig, SampleRate, InputCallbackInfo}; +use cpal::traits::{DeviceTrait, HostTrait}; +use crate::utils::ringbuf; +use crate::utils::ringbuf::{ringbuf, RingBuffer}; + +pub struct Microphone { + device: Device, +} + +impl Microphone { + pub fn new(device: Device) -> Self { + Self { + device, + } + } + + pub fn default(host: &Host) -> Result { + let device = host.default_input_device() + .ok_or_else(|| "Aucun périphérique d'entrée disponible".to_string())?; + Ok(Self::new(device)) + } + + pub fn get_input_config(&self) -> Result { + self.device.default_input_config() + .map_err(|e| format!("Erreur config: {e}")) + } + + pub fn get_stream_config(&self) -> Result { + 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); + Ok(stream_config) + } + + pub fn build_stream(&self, callback: F) -> Result + where + F: FnMut(&[i16], &cpal::InputCallbackInfo) + Send + 'static, + { + let config = self.get_stream_config()?; + + let stream = self.device.build_input_stream( + &config, + callback, + |err| eprintln!("Erreur stream: {err}"), + None, + ).map_err(|e| format!("Erreur création stream: {e}"))?; + + Ok(stream) + } +} + +struct AudioCapture { + microphone: Microphone, + + // catpure related + ringbuf: RingBuffer, + stream: Option, + worker: Option>, + running: Arc, +} + +impl AudioCapture { + pub fn new(microphone: Microphone) -> Self { + Self { + microphone, + ringbuf: RingBuffer::new(48000), + stream: None, + worker: None, + running: Arc::new(AtomicBool::new(false)), + } + } + + pub fn update_device(&mut self, device: Device) { + let running = self.running.load(Ordering::Relaxed); + self.stop_capture(); + self.microphone = Microphone::new(device); + if running { + self.start_capture(); + } + } + + pub fn start_capture(&mut self) -> Result<(), String> { + self.running.store(true, Ordering::Relaxed); + // todo ajouter les threads dans self.workers (pour le suivi de l'arrêt) + + let (mut writer, mut reader) = self.ringbuf.both(); + + let stream_running = self.running.clone(); + + // Stream audio callback + let stream = self.microphone.build_stream(move |data: &[i16], _: &InputCallbackInfo| { + if !stream_running.load(Ordering::Relaxed) { + return; + } + + writer.push_slice_overwrite(data); + })?; + + let worker_running = self.running.clone(); + let worker = thread::spawn(move || { + let mut frame = [0i16; 960]; // Taille de Frame souhaité + while worker_running.load(Ordering::Relaxed){ + let read = reader.pop_slice_blocking(&mut frame); + // ✅ Check simple après réveil + if !worker_running.load(Ordering::Relaxed) { + println!("🛑 Arrêt demandé"); + break; + } + } + println!("Fermeture du thread de capture.") + }); + + + self.stream = Some(stream); + self.worker = Some(worker); + + Ok(()) + } + + pub fn stop_capture(&mut self){ + println!("🛑 Arrêt en cours..."); + + // ✅ 1. Signal d'arrêt + self.running.store(false, Ordering::Relaxed); + + // ✅ 2. RÉVEIL FORCÉ du worker ! + self.ringbuf.force_wake_up(); + + // ✅ 3. Attente de terminaison (maintenant ça marche !) + if let Some(handle) = self.worker.take() { + handle.join().unwrap(); + } + + self.stream = None; + + // todo ajouter une méthode pour vider le ringbuf + self.ringbuf.clear(); + + println!("🎤 Capture audio arrêtée."); + } +} diff --git a/src/audio/mod.rs b/src/audio/mod.rs new file mode 100644 index 0000000..f0b6b88 --- /dev/null +++ b/src/audio/mod.rs @@ -0,0 +1,4 @@ +pub mod capture; +pub mod playback; +pub mod opus; +pub mod stats; \ No newline at end of file diff --git a/src/audio/opus.rs b/src/audio/opus.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/audio/playback.rs b/src/audio/playback.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/audio/stats.rs b/src/audio/stats.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8c12cc3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod app; +pub mod audio; +pub mod net; +pub mod utils; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 9a91472..7570c89 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,13 @@ -mod modules; +// mod modules; +// mod utils; + use std::sync::Arc; -use modules::audio_processor_in::{Microphone, AudioCapture}; -use modules::client::UdpClient; +use ox_speak_rs::app::context; +use ox_speak_rs::app::context::Context; +use ox_speak_rs::app::runtime::Runtime; +// use modules::audio_processor_in::{Microphone, AudioCapture}; +// use modules::client::UdpClient; fn main() { println!("Hello, world!"); @@ -35,7 +40,11 @@ fn main() { } } }); - + + // todo : delete ce qui a au dessus, je garde le temps de migrer. + let context = Arc::new(Context::new()); + let runtime = Runtime::new(context); + runtime.run(); loop { diff --git a/src/modules/audio_processor_in.rs b/src/modules/audio_processor_in.rs index 1c80f8a..64d3a77 100644 --- a/src/modules/audio_processor_in.rs +++ b/src/modules/audio_processor_in.rs @@ -9,7 +9,7 @@ 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; +use crate::utils::real_time_event::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. diff --git a/src/modules/audio_processor_out.rs b/src/modules/audio_processor_out.rs new file mode 100644 index 0000000..8a54239 --- /dev/null +++ b/src/modules/audio_processor_out.rs @@ -0,0 +1,24 @@ +use cpal::{Device, Host}; +use cpal::traits::HostTrait; + +struct Speaker { + host: Host, + device: cpal::Device, + +} + +impl Speaker { + pub fn new(device_opt: Option) -> Result { + let host = cpal::default_host(); + let device = match device_opt { + Some(dev) => dev, + None => host.default_output_device().ok_or_else(|| "Aucun périphérique de sortie disponible".to_string())? + }; + + + Ok(Self{ + host, + device + }) + } +} \ No newline at end of file diff --git a/src/modules/client.rs b/src/modules/client.rs index 3e054df..a6e3eab 100644 --- a/src/modules/client.rs +++ b/src/modules/client.rs @@ -66,7 +66,7 @@ impl UdpClient { } - pub fn send(&mut self, data: &[u8]) -> Result<(), String> { + pub fn send(&self, data: &[u8]) -> Result<(), String> { if !self.running { return Err("Client non démarré".to_string()); } @@ -184,7 +184,7 @@ impl UdpClient { match socket.recv_from(&mut buf) { Ok((size, _addr)) => { let data = buf[..size].to_vec(); - // println!("📥 Reçu ({} bytes)", size); + println!("📥 Reçu ({} bytes)", size); if tx.try_send(data).is_err() { // Queue pleine, on drop (pas grave pour l'audio) diff --git a/src/modules/mod.rs b/src/modules/mod.rs index baf75c8..3423f2b 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -6,6 +6,6 @@ mod utils; mod audio_stats; // mod client_old; mod audio_client; - +mod audio_processor_out; // mod back; diff --git a/src/modules/utils.rs b/src/modules/utils.rs index e0ce113..e69de29 100644 --- a/src/modules/utils.rs +++ b/src/modules/utils.rs @@ -1,46 +0,0 @@ -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/net/audio.rs b/src/net/audio.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/net/message.rs b/src/net/message.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/net/mod.rs b/src/net/mod.rs new file mode 100644 index 0000000..f82afb2 --- /dev/null +++ b/src/net/mod.rs @@ -0,0 +1,5 @@ +pub mod udp_client; +pub mod message; +pub mod audio; + + diff --git a/src/net/udp_client.rs b/src/net/udp_client.rs new file mode 100644 index 0000000..e69de29 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.md b/src/utils/ringbuf.md new file mode 100644 index 0000000..788c9a0 --- /dev/null +++ b/src/utils/ringbuf.md @@ -0,0 +1,32 @@ +Usage +```rust + +use std::thread; +use std::time::Duration; +mod ringbuf; +use ringbuf::ringbuf; + +fn main() { + let (writer, reader) = ringbuf::(1024); + + // Simule CPAL : producteur ultra rapide + let write_thread = thread::spawn(move || { + for i in 0..10_000 { + writer.push(i as f32); + // Simule fréquence audio + std::thread::sleep(Duration::from_micros(20)); + } + }); + + // Lecteur bloquant + let read_thread = thread::spawn(move || { + for _ in 0..10_000 { + let sample = reader.pop_blocking(); + println!("Got: {sample}"); + } + }); + + write_thread.join().unwrap(); + read_thread.join().unwrap(); +} +``` \ No newline at end of file diff --git a/src/utils/ringbuf.rs b/src/utils/ringbuf.rs new file mode 100644 index 0000000..f706490 --- /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 ! 🚀 + +*/ diff --git a/tree.md b/tree.md new file mode 100644 index 0000000..a90c01f --- /dev/null +++ b/tree.md @@ -0,0 +1,187 @@ +# Projet ox_speak_rs - Structure du Code + +## Structure des Modules + +``` +ox_speak_rs/ +├── src/ +│ ├── main.rs # Point d'entrée de l'application +│ ├── modules/ # Modules fonctionnels +│ │ ├── mod.rs # Exports des modules +│ │ ├── audio_processor_in.rs # Traitement audio d'entrée +│ │ ├── audio_processor_out.rs # Traitement audio de sortie +│ │ ├── audio_opus.rs # Encodage/décodage Opus +│ │ ├── audio_stats.rs # Statistiques audio +│ │ ├── client.rs # Client réseau UDP +│ │ ├── client_old.rs # Ancienne implémentation du client +│ │ ├── config.rs # Configuration (commenté) +│ │ ├── audio_client.rs # Client audio (vide) +│ │ └── utils.rs # Utilitaires pour les modules (vide) +│ └── utils/ # Utilitaires génériques +│ ├── mod.rs # Exports des utilitaires +│ ├── real_time_event.rs # Gestion d'événements temps réel +│ ├── ringbuf.rs # Implémentation de buffer circulaire +│ └── ringbuf.md # Documentation du buffer circulaire +``` + +## Hiérarchie des Structures + +### Module Principal (main.rs) +- Initialise les composants principaux +- Configure les threads de traitement audio + +### Modules Audio + +#### audio_processor_in.rs +- `Microphone` + - Gère l'accès au périphérique d'entrée audio + - Méthodes: + - `new()`: Crée une nouvelle instance + - `start()`: Démarre la capture audio + - `stop()`: Arrête la capture + - `get_config()`: Récupère la configuration + - `change_device()`: Change le périphérique d'entrée + - `list_available_devices()`: Liste les périphériques disponibles + +- `AudioCapture` + - Gère le processus de capture audio + - Utilise `Microphone` pour l'accès au périphérique + - Méthodes: + - `new()`: Crée une nouvelle instance + - `start_capture()`: Démarre la capture + - `is_silence()`: Détecte le silence dans un buffer + +- `Subscriber` + - Structure pour les abonnés aux événements audio + +- `AudioEventBus` + - Bus d'événements pour la distribution des données audio + - Méthodes: + - `new()`: Crée une nouvelle instance + - `new_with_capacity()`: Crée avec une capacité spécifiée + - `subscribe_raw()`: S'abonne aux données brutes + - `subscribe_encoded()`: S'abonne aux données encodées + - `notify_raw()`: Notifie les abonnés avec données brutes + - `notify_encoded()`: Notifie les abonnés avec données encodées + +#### audio_processor_out.rs +- `Speaker` + - Gère l'accès au périphérique de sortie audio + - Méthodes: + - `new()`: Crée une nouvelle instance + +#### audio_opus.rs +- `AudioOpus` + - Configuration de base pour le codec Opus + - Méthodes: + - `new()`: Crée une nouvelle instance + - `create_encoder()`: Crée un encodeur + - `create_decoder()`: Crée un décodeur + +- `AudioOpusEncoder` + - Gère l'encodage audio avec Opus + - Méthodes: + - `new()`: Crée une nouvelle instance + - `encode()`: Encode des données audio + - `encode_reuse()`: Encode avec réutilisation de buffer + +- `AudioOpusDecoder` + - Gère le décodage audio avec Opus + - Méthodes: + - `new()`: Crée une nouvelle instance + - `decode()`: Décode des données audio + +#### audio_stats.rs +- `AudioBufferWithStats` + - Buffer audio avec statistiques + - Méthodes: + - `new_frame()`: Crée un nouveau frame avec stats + - `calculate_peak_rms_simd()`: Calcule pic et RMS avec SIMD + - `now_nanos()`: Horodatage en nanosecondes + +- `GlobalAudioStats` + - Statistiques audio globales + - Méthodes: + - `new()`: Crée une nouvelle instance + - `get_live_stats()`: Récupère les stats en temps réel + - `now_nanos()`: Horodatage en nanosecondes + +- `LiveAudioStats` + - Statistiques audio en temps réel + - Méthodes: + - `volume_bar_with_timing()`: Affiche barre de volume + - `timing_analysis()`: Analyse de timing + +### Modules Réseau + +#### client.rs +- `MessageType` (enum) + - Types de messages réseau + - Variantes: Keepalive, Hello, Bye, Command, Status, Audio, Error + +- `UdpClient` + - Client UDP pour communication réseau + - Méthodes: + - `new()`: Crée une nouvelle instance + - `send()`: Envoie des données + - `try_recv()`: Tente de recevoir des données + - `start()`: Démarre le client + - `stop()`: Arrête le client + - `get_audio_sender()`: Récupère le sender audio + - `create_audio_handle()`: Crée un handle audio + +- `Client` + - Wrapper autour de UdpClient + - Méthodes: + - `new()`: Crée une nouvelle instance + +- `MessageCall` (enum) + - Représente les messages sortants + - Variantes: KeepAlive, Hello, Bye, Command, Audio, Error + - Méthodes: + - `serialize()`: Sérialise le message + - Constructeurs pour chaque type de message + +- `MessageEvent` (enum) + - Représente les messages entrants + - Variantes: Status, Audio, Error + +- `AudioHandle` + - Gère l'envoi de frames audio + - Méthodes: + - `new()`: Crée une nouvelle instance + - `send_audio_frame()`: Envoie un frame audio + +### Utilitaires + +#### real_time_event.rs +- `RealTimeEventInner` + - Implémentation interne pour la gestion d'événements + +- `RealTimeEvent` + - API publique pour notification et attente d'événements + - Méthodes: + - `new()`: Crée une nouvelle instance + - `notify()`: Notifie les listeners + - `wait()`: Attend une notification + +#### ringbuf.rs +- `RingBufWriter` + - Écrit dans un buffer circulaire + - Méthodes: + - `push()`: Ajoute un élément + - `push_slice()`: Ajoute un slice d'éléments + +- `RingBufReader` + - Lit depuis un buffer circulaire + - Méthodes: + - `pop_blocking()`: Récupère un élément (bloquant) + - `try_pop()`: Tente de récupérer un élément + +- `InnerRingBuf` + - Implémentation interne du buffer circulaire + - Méthodes: + - `next()`: Calcule l'index suivant + +- Fonction `ringbuf()` + - Crée une paire (writer, reader) de buffer circulaire \ No newline at end of file