refactoring...

This commit is contained in:
2025-06-18 03:15:36 +02:00
parent e2f8bba35e
commit 6597b28e01
26 changed files with 1411 additions and 88 deletions

137
Cargo.lock generated
View File

@@ -2,6 +2,21 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "alsa" name = "alsa"
version = "0.9.1" version = "0.9.1"
@@ -41,6 +56,21 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 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]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -65,12 +95,6 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cacheguard"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dbbe48daefc2575b7dfdc8227f4724582080ae48b0482191f6eb91e4cd2f405"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.25" version = "1.2.25"
@@ -205,6 +229,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.3" version = "0.15.3"
@@ -256,9 +286,9 @@ dependencies = [
[[package]] [[package]]
name = "kanal" name = "kanal"
version = "0.1.1" 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 = [ dependencies = [
"cacheguard",
"futures-core", "futures-core",
"lock_api", "lock_api",
] ]
@@ -300,6 +330,15 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 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]] [[package]]
name = "ndk" name = "ndk"
version = "0.9.0" version = "0.9.0"
@@ -442,6 +481,15 @@ dependencies = [
"objc2", "objc2",
] ]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@@ -467,7 +515,8 @@ dependencies = [
"kanal", "kanal",
"num_enum", "num_enum",
"opus", "opus",
"ringbuf", "parking_lot",
"tokio",
] ]
[[package]] [[package]]
@@ -476,6 +525,29 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 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]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@@ -488,21 +560,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 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]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "3.3.0" version = "3.3.0"
@@ -531,16 +588,20 @@ dependencies = [
] ]
[[package]] [[package]]
name = "ringbuf" name = "redox_syscall"
version = "0.4.8" version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
dependencies = [ dependencies = [
"crossbeam-utils", "bitflags 2.9.1",
"portable-atomic",
"portable-atomic-util",
] ]
[[package]]
name = "rustc-demangle"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.21" version = "1.0.21"
@@ -568,6 +629,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.101" version = "2.0.101"
@@ -599,6 +666,16 @@ dependencies = [
"syn", "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]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.9" version = "0.6.9"

View File

@@ -3,12 +3,23 @@ name = "ox_speak_rs"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[[bin]]
name = "ox_speak_rs"
path = "src/main.rs"
[lib]
name = "ox_speak_rs"
path = "src/lib.rs"
[dependencies] [dependencies]
opus = "0.3" opus = "0.3"
cpal = "0.16" cpal = "0.16"
ringbuf = "0.4" #ringbuf = "0.4"
#crossbeam = "0.8" #crossbeam = "0.8"
#kanal = "0.1" # attente du fix sur le recv_timeout qui fait burn le cpu kanal = "0.1" # attente du fix sur le recv_timeout qui fait burn le cpu
kanal = { git = "https://github.com/fereidani/kanal.git" } #kanal = { git = "https://github.com/fereidani/kanal.git" }
event-listener = "5.4" event-listener = "5.4"
num_enum = "0.7" num_enum = "0.7"
parking_lot = "0.12"
tokio = "1.45"
#rtrb = "0.3"

27
src/app/context.rs Normal file
View File

@@ -0,0 +1,27 @@
// Logique passive de l'application avec les resources globales
use std::sync::Arc;
pub struct Context {
host: Arc<cpal::Host>,
// 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<cpal::Host> {
self.host.clone()
}
}

2
src/app/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod context;
pub mod runtime;

20
src/app/runtime.rs Normal file
View File

@@ -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<Context>
}
impl Runtime {
pub fn new(context: Arc<Context>) -> Runtime {
Self {
context
}
}
pub fn run(&self) {
}
}

147
src/audio/capture.rs Normal file
View File

@@ -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<Self, String> {
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<SupportedStreamConfig, String> {
self.device.default_input_config()
.map_err(|e| format!("Erreur config: {e}"))
}
pub fn get_stream_config(&self) -> Result<StreamConfig, String> {
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<F>(&self, callback: F) -> Result<Stream, String>
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<i16>,
stream: Option<Stream>,
worker: Option<JoinHandle<()>>,
running: Arc<AtomicBool>,
}
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.");
}
}

4
src/audio/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod capture;
pub mod playback;
pub mod opus;
pub mod stats;

0
src/audio/opus.rs Normal file
View File

0
src/audio/playback.rs Normal file
View File

0
src/audio/stats.rs Normal file
View File

4
src/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod app;
pub mod audio;
pub mod net;
pub mod utils;

View File

@@ -1,8 +1,13 @@
mod modules; // mod modules;
// mod utils;
use std::sync::Arc; use std::sync::Arc;
use modules::audio_processor_in::{Microphone, AudioCapture}; use ox_speak_rs::app::context;
use modules::client::UdpClient; 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() { fn main() {
println!("Hello, world!"); println!("Hello, world!");
@@ -36,6 +41,10 @@ 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 { loop {

View File

@@ -9,7 +9,7 @@ use ringbuf::{CachingCons, CachingProd, HeapRb};
use ringbuf::traits::{Split, Producer, Consumer}; use ringbuf::traits::{Split, Producer, Consumer};
use crate::modules::audio_opus::AudioOpus; use crate::modules::audio_opus::AudioOpus;
use crate::modules::audio_stats::{AudioBufferWithStats, GlobalAudioStats}; 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 // 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. // faire en sorte que ça prenne la config par défaut de la carte audio histoire de simplifier le code.

View File

@@ -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<Device>) -> Result<Self, String> {
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
})
}
}

View File

@@ -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 { if !self.running {
return Err("Client non démarré".to_string()); return Err("Client non démarré".to_string());
} }
@@ -184,7 +184,7 @@ impl UdpClient {
match socket.recv_from(&mut buf) { match socket.recv_from(&mut buf) {
Ok((size, _addr)) => { Ok((size, _addr)) => {
let data = buf[..size].to_vec(); let data = buf[..size].to_vec();
// println!("📥 Reçu ({} bytes)", size); println!("📥 Reçu ({} bytes)", size);
if tx.try_send(data).is_err() { if tx.try_send(data).is_err() {
// Queue pleine, on drop (pas grave pour l'audio) // Queue pleine, on drop (pas grave pour l'audio)

View File

@@ -6,6 +6,6 @@ mod utils;
mod audio_stats; mod audio_stats;
// mod client_old; // mod client_old;
mod audio_client; mod audio_client;
mod audio_processor_out;
// mod back; // mod back;

View File

@@ -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<RealTimeEventInner>,
}
impl RealTimeEvent{
pub fn new() -> Self{
Self{
inner: Arc::new(RealTimeEventInner{
flag: AtomicBool::new(false),
event: Event::new(),
})
}
}
pub fn notify(&self){
self.inner.flag.store(true, Ordering::Release);
self.inner.event.notify(usize::MAX);
}
pub fn wait(&self){
loop {
let listener = self.inner.event.listen();
if self.inner.flag.swap(false, Ordering::Acquire){
break
}
listener.wait();
}
}
}
impl Default for RealTimeEvent{
fn default() -> Self{
Self::new()
}
}

0
src/net/audio.rs Normal file
View File

0
src/net/message.rs Normal file
View File

5
src/net/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod udp_client;
pub mod message;
pub mod audio;

0
src/net/udp_client.rs Normal file
View File

2
src/utils/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod ringbuf;
pub mod real_time_event;

View File

@@ -0,0 +1,46 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use event_listener::{Event, Listener};
struct RealTimeEventInner{
flag: AtomicBool,
event: Event,
}
#[derive(Clone)]
pub struct RealTimeEvent {
inner: Arc<RealTimeEventInner>,
}
impl RealTimeEvent{
pub fn new() -> Self{
Self{
inner: Arc::new(RealTimeEventInner{
flag: AtomicBool::new(false),
event: Event::new(),
})
}
}
pub fn notify(&self){
self.inner.flag.store(true, Ordering::Release);
self.inner.event.notify(usize::MAX);
}
pub fn wait(&self){
loop {
let listener = self.inner.event.listen();
if self.inner.flag.swap(false, Ordering::Acquire){
break
}
listener.wait();
}
}
}
impl Default for RealTimeEvent{
fn default() -> Self{
Self::new()
}
}

32
src/utils/ringbuf.md Normal file
View File

@@ -0,0 +1,32 @@
Usage
```rust
use std::thread;
use std::time::Duration;
mod ringbuf;
use ringbuf::ringbuf;
fn main() {
let (writer, reader) = ringbuf::<f32>(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();
}
```

772
src/utils/ringbuf.rs Normal file
View File

@@ -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<T> {
inner: Arc<InnerRingBuf<T>>,
}
/// Celui qui lit depuis le buffer (consommateur)
pub struct RingBufReader<T> {
inner: Arc<InnerRingBuf<T>>,
}
/// Le buffer circulaire interne partagé entre writer et reader
struct InnerRingBuf<T> {
// Le buffer qui contient nos données
buffer: Vec<UnsafeCell<T>>,
// 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<T: Send> Send for InnerRingBuf<T> {}
unsafe impl<T: Send> Sync for InnerRingBuf<T> {}
// ============================================================================
// 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<T>(cap: usize) -> (RingBufWriter<T>, RingBufReader<T>) {
let buffer = RingBuffer::new(cap);
buffer.split()
}
#[derive(Clone)]
pub struct RingBuffer<T> {
inner: Arc<InnerRingBuf<T>>,
}
impl<T> RingBuffer<T> {
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<T> {
RingBufWriter { inner: self.inner.clone() }
}
pub fn reader(&self) -> RingBufReader<T> {
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<T>, RingBufReader<T>) {
(
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<T>, RingBufReader<T>) {
(
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<T: Copy> RingBufWriter<T> {
/// 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<T: Copy> RingBufReader<T> {
/// 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<T> {
// 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<T> Clone for RingBufWriter<T> {
fn clone(&self) -> Self {
RingBufWriter {
inner: self.inner.clone(),
}
}
}
impl<T> Clone for RingBufReader<T> {
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::<i16>(1024); // Buffer basique
let (writer, reader) = ringbuf::<f32>(32768); // Audio haute qualité (~0.7s à 48kHz)
let (writer, reader) = ringbuf::<u8>(8192); // Données binaires
// Distribution multi-threads
let buffer = RingBuffer::<i16>::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::<i16>::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::<i16>::new(16384);
let filtered_buffer = RingBuffer::<i16>::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::<i16>(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 ! 🚀
*/

187
tree.md Normal file
View File

@@ -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<T>`
- Écrit dans un buffer circulaire
- Méthodes:
- `push()`: Ajoute un élément
- `push_slice()`: Ajoute un slice d'éléments
- `RingBufReader<T>`
- 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<T>`
- Implémentation interne du buffer circulaire
- Méthodes:
- `next()`: Calcule l'index suivant
- Fonction `ringbuf<T>()`
- Crée une paire (writer, reader) de buffer circulaire