refactoring...
This commit is contained in:
137
Cargo.lock
generated
137
Cargo.lock
generated
@@ -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"
|
||||||
|
|||||||
19
Cargo.toml
19
Cargo.toml
@@ -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
27
src/app/context.rs
Normal 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
2
src/app/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod context;
|
||||||
|
pub mod runtime;
|
||||||
20
src/app/runtime.rs
Normal file
20
src/app/runtime.rs
Normal 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
147
src/audio/capture.rs
Normal 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
4
src/audio/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod capture;
|
||||||
|
pub mod playback;
|
||||||
|
pub mod opus;
|
||||||
|
pub mod stats;
|
||||||
0
src/audio/opus.rs
Normal file
0
src/audio/opus.rs
Normal file
0
src/audio/playback.rs
Normal file
0
src/audio/playback.rs
Normal file
0
src/audio/stats.rs
Normal file
0
src/audio/stats.rs
Normal file
4
src/lib.rs
Normal file
4
src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod app;
|
||||||
|
pub mod audio;
|
||||||
|
pub mod net;
|
||||||
|
pub mod utils;
|
||||||
17
src/main.rs
17
src/main.rs
@@ -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!");
|
||||||
@@ -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 {
|
loop {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
24
src/modules/audio_processor_out.rs
Normal file
24
src/modules/audio_processor_out.rs
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
0
src/net/audio.rs
Normal file
0
src/net/message.rs
Normal file
0
src/net/message.rs
Normal file
5
src/net/mod.rs
Normal file
5
src/net/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub mod udp_client;
|
||||||
|
pub mod message;
|
||||||
|
pub mod audio;
|
||||||
|
|
||||||
|
|
||||||
0
src/net/udp_client.rs
Normal file
0
src/net/udp_client.rs
Normal file
2
src/utils/mod.rs
Normal file
2
src/utils/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod ringbuf;
|
||||||
|
pub mod real_time_event;
|
||||||
46
src/utils/real_time_event.rs
Normal file
46
src/utils/real_time_event.rs
Normal 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
32
src/utils/ringbuf.md
Normal 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
772
src/utils/ringbuf.rs
Normal 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
187
tree.md
Normal 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
|
||||||
Reference in New Issue
Block a user