Compare commits

...

6 Commits

Author SHA1 Message Date
f2b7e6e689 f32 instead of i16 2025-07-22 02:51:37 +02:00
43f8d38cb2 pre-passage f32 2025-07-22 00:22:39 +02:00
044f9781de pre-passage f32 2025-07-21 16:32:27 +02:00
db8e876c1c init 2025-07-21 01:20:30 +02:00
d7a7af9eb4 init 2025-07-20 04:17:44 +02:00
a1f829e2e6 init 2025-07-19 03:45:41 +02:00
15 changed files with 1183 additions and 132 deletions

View File

@@ -5,3 +5,10 @@ This template should help get you started developing with Tauri + Vue 3 in Vite.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
## todo :
- rendre les échantillonage d'entré et de sorti audio
- conversion une fois le paquet reçu en entré en mono/48000hz
- à la réception, adapter à l'échantillonnage de la sortie audio.

90
src-tauri/Cargo.lock generated
View File

@@ -1945,6 +1945,16 @@ dependencies = [
"serde_json",
]
[[package]]
name = "kanal"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e3953adf0cd667798b396c2fa13552d6d9b3269d7dd1154c4c416442d1ff574"
dependencies = [
"futures-core",
"lock_api",
]
[[package]]
name = "keyboard-types"
version = "0.7.0"
@@ -2270,6 +2280,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@@ -2287,6 +2306,15 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -2637,9 +2665,11 @@ dependencies = [
"cpal",
"crossbeam-channel",
"event-listener",
"kanal",
"moka",
"opus",
"parking_lot",
"rubato",
"serde",
"serde_json",
"strum",
@@ -2956,6 +2986,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "primal-check"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08"
dependencies = [
"num-integer",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -3134,6 +3173,15 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "realfft"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677"
dependencies = [
"rustfft",
]
[[package]]
name = "redox_syscall"
version = "0.5.13"
@@ -3253,6 +3301,18 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rubato"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5258099699851cfd0082aeb645feb9c084d9a5e1f1b8d5372086b989fc5e56a1"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"realfft",
]
[[package]]
name = "rustc-demangle"
version = "0.1.25"
@@ -3268,6 +3328,20 @@ dependencies = [
"semver",
]
[[package]]
name = "rustfft"
version = "6.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f140db74548f7c9d7cce60912c9ac414e74df5e718dc947d514b051b42f3f4"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"primal-check",
"strength_reduce",
"transpose",
]
[[package]]
name = "rustix"
version = "1.0.7"
@@ -3677,6 +3751,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strength_reduce"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
[[package]]
name = "string_cache"
version = "0.8.9"
@@ -4435,6 +4515,16 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "transpose"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
dependencies = [
"num-integer",
"strength_reduce",
]
[[package]]
name = "tray-icon"
version = "0.21.0"

View File

@@ -33,4 +33,6 @@ event-listener = "5.4"
bytes = "1.10"
moka = {version = "0.12", features = ["future"] }
arc-swap = "1.7"
crossbeam-channel = "0.5"
crossbeam-channel = "0.5"
kanal = "0.1"
rubato = "0.16"

View File

@@ -4,6 +4,9 @@ use tauri::{AppHandle, Emitter, Listener};
use tokio;
use tokio::sync::mpsc;
use crate::core::capture::AudioCapture;
use crate::core::mixer::AudioMixer;
use crate::core::playback::{AudioPlayback, Speaker};
use crate::domain::audio_client::AudioClientManager;
use crate::domain::event::{Event, EventBus};
use crate::network::udp::UdpSession;
use crate::runtime::dispatcher::Dispatcher;
@@ -12,13 +15,18 @@ pub struct OxSpeakApp {
// Communication inter-thread
event_bus: EventBus,
dispatcher: Dispatcher,
event_rx: Option<mpsc::Receiver<Event>>,
event_rx: kanal::AsyncReceiver<Event>,
// Network
udp_session: UdpSession,
// audio
audio_capture: AudioCapture,
audio_playback: AudioPlayback,
audio_mixer: AudioMixer,
// audio_client
audio_client_manager: AudioClientManager,
// Tauri handle
tauri_handle: AppHandle
@@ -36,48 +44,61 @@ impl OxSpeakApp {
// todo : pour le moment, paramètre par défaut, on verra plus tard pour dynamiser ça
println!("Initializing audio capture");
let audio_capture = AudioCapture::default(event_bus.clone());
println!("Initializing audio client");
let audio_client_manager = AudioClientManager::new();
// todo : pas idéal (la récup du sample_rate), car le mieux serais de récupérer ça dynamiquement. Peut être charger le mixer depuis audio_playback ?
let stream_config = Speaker::default().get_stream_config();
let audio_mixer = AudioMixer::new(stream_config.sample_rate.0 as usize, stream_config.channels as usize, audio_client_manager.clone());
let audio_playback = AudioPlayback::default(event_bus.clone(), audio_mixer.clone());
// UdpSession
println!("Initializing UDP session");
let udp_session = UdpSession::new(event_bus.clone());
// UdpClient
// Dispatcher - Communication inter-components
println!("Initializing event dispatcher");
let mut dispatcher = Dispatcher::new(event_bus.clone(), udp_session.clone(), tauri_handle.clone());
let mut dispatcher = Dispatcher::new(event_bus.clone(), udp_session.clone(), audio_client_manager.clone(), audio_mixer.clone(),tauri_handle.clone());
println!("OxSpeakApp initialization complete");
Self {
event_bus,
dispatcher,
event_rx: Some(event_rx),
event_rx,
udp_session,
audio_capture,
audio_client_manager,
audio_playback,
audio_mixer,
tauri_handle,
}
}
pub async fn start(&mut self) {
println!("Starting OxSpeakApp");
// dispatcher - lancement du process pour la communication inter-process
println!("Starting event dispatcher");
let mut dispatcher = self.dispatcher.clone();
// Prendre l'ownership du receiver (event_rx)
if let Some(event_rx) = self.event_rx.take() {
println!("Starting 4 instances of event dispatcher");
for _ in 0..4 {
let dispatcher = self.dispatcher.clone();
let event_rx = self.event_rx.clone();
tokio::spawn(async move {
dispatcher.start(event_rx).await
});
}
// Démarrer la connexion réseau
println!("Connecting to UDP server at 127.0.0.1:5000");
self.udp_session.connect("127.0.0.1:5000").await;
println!("Connecting to UDP server at 82.64.205.121:5000");
self.udp_session.connect("82.64.205.121:5000").await;
// Démarrer l'audio-capture
println!("Starting audio capture");
self.audio_capture.start().await;
self.audio_playback.start().await;
println!("OxSpeakApp started successfully");
let _ = self.tick_tasks().await;

View File

@@ -4,8 +4,10 @@ use std::thread;
use std::thread::JoinHandle;
use cpal::{default_host, BufferSize, Device, SampleRate, Stream, StreamConfig, SupportedStreamConfig};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
// ✅ Supprimé rubato complètement !
use crate::core::opus::AudioOpus;
use crate::domain::event::{Event, EventBus};
use crate::utils::audio_utils::AudioTools;
use crate::utils::ringbuf::RingBuffer;
#[derive(Clone)]
@@ -17,7 +19,7 @@ pub struct AudioCapture {
event_bus: EventBus,
microphone: Microphone,
running: Arc<AtomicBool>,
ring_buffer: RingBuffer<i16>,
ring_buffer: RingBuffer<f32>,
steam: Option<Stream>,
worker: Option<JoinHandle<()>>,
}
@@ -25,9 +27,7 @@ pub struct AudioCapture {
impl Microphone {
pub fn new(device: Device) -> Self {
println!("Initializing microphone with device: {}", device.name().unwrap_or_else(|_| "Unknown".to_string()));
Self {
device
}
Self { device }
}
pub fn default() -> Self {
@@ -42,20 +42,14 @@ impl Microphone {
}
pub fn get_stream_config(&self) -> StreamConfig {
let config = self.get_input_config();
let mut stream_config: StreamConfig = config.into();
stream_config.channels = 1;
stream_config.sample_rate = SampleRate(48000);
stream_config.buffer_size = BufferSize::Fixed(960);
stream_config
self.get_input_config().into()
}
pub fn build_stream<F>(&self, callback: F) -> Stream
where
F: FnMut(&[i16], &cpal::InputCallbackInfo) + Send + 'static,
F: FnMut(&[f32], &cpal::InputCallbackInfo) + Send + 'static,
{
let config = self.get_stream_config();
self.device.build_input_stream(
&config,
callback,
@@ -63,6 +57,12 @@ impl Microphone {
None
).unwrap()
}
pub fn get_supported_channels(&self) -> Vec<u16> {
self.device.supported_input_configs()
.map(|configs| configs.map(|c| c.channels()).collect())
.unwrap_or(vec![1])
}
}
impl AudioCapture {
@@ -92,14 +92,12 @@ impl AudioCapture {
let writer = self.ring_buffer.writer();
let stream_running = self.running.clone();
let stream = self.microphone.build_stream(move |data, _| {
if !stream_running.load(Ordering::Relaxed){
return;
}
if !stream_running.load(Ordering::Relaxed) { return; }
writer.push_slice_overwrite(data);
});
stream.play().unwrap();
self.steam = Some(stream);
println!("Audio stream started");
println!("Audio capture stream started");
// Audio processing worker
println!("Starting audio processing worker");
@@ -110,54 +108,52 @@ impl AudioCapture {
pub async fn stop(&mut self) {
println!("Stopping audio capture");
self.running.store(false, Ordering::Relaxed);
println!("Releasing audio stream");
self.steam = None;
self.ring_buffer.force_wake_up();
// code possiblement bloquant, wrap vers un thread tokio bloquant
if let Some(worker) = self.worker.take() {
println!("Waiting for audio processing worker to finish");
tokio::task::spawn_blocking(move || {
worker.join().unwrap();
}).await.unwrap();
}
println!("Clearing ring buffer");
self.ring_buffer.clear();
println!("Audio capture stopped");
}
fn run_processing_worker(&mut self){
fn run_processing_worker(&mut self) {
println!("Configuring audio processing worker");
let worker_running = self.running.clone();
let event_bus = self.event_bus.clone();
let input_config = self.microphone.get_input_config();
println!("Audio input config: sample rate: {}, channels: {}", input_config.sample_rate().0, input_config.channels());
let opus = AudioOpus::new(input_config.sample_rate().0, input_config.channels(), "voip");
let stream_config = self.microphone.get_stream_config();
let sample_rate: usize = stream_config.sample_rate.0 as usize;
let channels: usize = stream_config.channels as usize;
println!("Audio config: {} channels @ {}Hz",
stream_config.channels, stream_config.sample_rate.0);
let opus = AudioOpus::new(48_000, 1, "voip");
let mut encoder = opus.create_encoder().unwrap();
let reader = self.ring_buffer.reader();
println!("Spawning audio processing thread");
self.worker = Some(thread::spawn(move || {
println!("Audio processing thread started");
let mut frame = [0i16; 960];
let mut frame_count = 0;
let frame_size = sample_rate * 10 / 1000; // ✅ 10ms = 480 samples @ 48kHz / 441 @ 44.1khz
let mut raw_buffer = vec![0f32; frame_size];
while worker_running.load(Ordering::Relaxed) {
let _ = reader.pop_slice_blocking(&mut frame);
if !worker_running.load(Ordering::Relaxed){
let _read_count = reader.pop_slice_blocking(&mut raw_buffer);
if !worker_running.load(Ordering::Relaxed) {
println!("Audio processing thread stopping");
break;
}
frame_count += 1;
if frame_count % 100 == 0 {
println!("Processed {} audio frames", frame_count);
}
let sample = AudioTools::sample_to_mono(&raw_buffer, channels);
// todo : voir si il est nécessaire d'intégrer un resampling avec AudioResampler
let raw_data = frame.to_vec();
event_bus.emit_sync(Event::AudioIn(raw_data));
match encoder.encode(&frame){
match encoder.encode(&sample) {
Ok(encoded_data) => {
event_bus.emit_sync(Event::AudioEncoded(encoded_data))
}
@@ -165,14 +161,28 @@ impl AudioCapture {
println!("Error encoding: {e}");
}
}
// Events
event_bus.emit_sync(Event::AudioIn(sample));
}
println!("Audio processing thread finished after processing {} frames", frame_count);
}));
}
}
impl AudioCapture {
fn audio_processing(){
// ✅ Super simple : juste conversion mono
fn process_audio_frame(config: &StreamConfig, samples: &[i16]) -> Vec<i16> {
Self::sample_to_mono(config.channels as usize, samples)
}
}
fn sample_to_mono(input_channels: usize, samples: &[i16]) -> Vec<i16> {
if input_channels == 1 {
samples.to_vec()
} else {
samples
.chunks_exact(input_channels)
.map(|frame| {
let sum: i32 = frame.iter().map(|&s| s as i32).sum();
(sum / input_channels as i32) as i16
})
.collect()
}
}
}

View File

@@ -1 +1,90 @@
// aller pick l'audio des clients
// version pull et ringbuf based - buffer de 2048 ou 4096 (donc 1/2 ou 3/4 trames de retard)
use std::sync::Arc;
use arc_swap::ArcSwap;
use cpal::SampleRate;
use crate::domain::audio_client::AudioClientManager;
use crate::utils::audio_utils::{AudioResampler, AudioTools};
use crate::utils::ringbuf::{RingBufReader, RingBufWriter, RingBuffer};
#[derive(Clone)]
pub struct AudioMixer {
audio_client_manager: AudioClientManager,
buffer_writer: Arc<RingBufWriter<f32>>,
buffer_reader: Arc<RingBufReader<f32>>,
internal_sample_rate: usize,
output_channels: usize,
output_sample_rate: usize,
resampler: AudioResampler,
}
impl AudioMixer {
pub fn new(output_sample_rate: usize, output_channels: usize, audio_client_manager: AudioClientManager) -> Self {
let (buffer_writer, buffer_reader) = RingBuffer::new(2048).split();
Self {
audio_client_manager,
buffer_writer: Arc::new(buffer_writer),
buffer_reader: Arc::new(buffer_reader),
internal_sample_rate: 48000,
output_sample_rate,
output_channels,
resampler: AudioResampler::new(),
}
}
pub fn mix_next_frame(&self, output_size: usize) {
// 1. Calcule combien de samples 48kHz on doit récupérer
let ratio = self.internal_sample_rate as f32 / self.output_sample_rate as f32;
let internal_frames_needed = ((output_size / self.output_channels) as f32 * ratio).ceil() as usize;
// 2. Récupère les données utilisateurs (48kHz mono)
let users_audio = self.audio_client_manager.take_audio_collection(internal_frames_needed);
// 3. Mix en 48kHz mono
let mixed_internal = if users_audio.is_empty() {
vec![0f32; internal_frames_needed]
} else {
AudioTools::mix_frames(&users_audio, internal_frames_needed)
};
// 3. Mix en 48kHz mono (résultat en f32)
let mixed_internal = if users_audio.is_empty() {
vec![0.0f32; internal_frames_needed]
} else {
AudioTools::mix_frames(&users_audio, internal_frames_needed)
};
// 4. Resample 48kHz -> output_rate si nécessaire
let resampled = if self.internal_sample_rate != self.output_sample_rate {
self.resampler.resample(
&mixed_internal,
self.internal_sample_rate,
self.output_sample_rate,
1 // mono
)
} else {
mixed_internal
};
// 5. Convert mono -> output_channels
let final_frame = AudioTools::change_channel_count(
&resampled,
1,
self.output_channels
);
// 6. Écrit dans le ringbuffer (pas besoin de resize exacte)
self.buffer_writer.push_slice_overwrite(&final_frame);
}
pub fn read(&self, size: usize) -> Vec<f32> {
let mut data = vec![0f32; size];
// Essaie de pop autant d'échantillons que possible
let read = self.buffer_reader.pop_slice(&mut data);
// Si on n'a pas tout eu, les éléments restants sont déjà à 0
data
}
}

View File

@@ -0,0 +1,78 @@
// version pull et arcswap based - buffer vraiment faible, 1 trame de retard seulement.
use std::sync::Arc;
use arc_swap::ArcSwap;
use crate::domain::audio_client::AudioClientManager;
#[derive(Clone)]
pub struct AudioMixer {
audio_client_manager: AudioClientManager,
buffer: Arc<ArcSwap<Vec<i16>>>
}
impl AudioMixer {
pub fn new(audio_client_manager: AudioClientManager) -> Self {
Self {
audio_client_manager,
buffer: Arc::new(ArcSwap::from_pointee(Vec::new())),
}
}
pub fn mix_next_frame(&self, size: usize) {
let mut frames = Vec::<Vec<i16>>::new();
// Récupérer les buffers audio des utilisateurs, par défaut, ils sont en mono, donc size / 2
// convertir en stéréo, donc size * 2 frames
let users_audio = self.audio_client_manager.take_audio_collection(size/2).into_iter()
.map(|audio| AudioMixer::mono_to_stereo(audio))
.collect::<Vec<Vec<i16>>>();
frames.extend_from_slice(&users_audio);
// Récupérer tous les sons des notifications (pas encore dev)
let mixed_frame = if frames.is_empty() {
vec![0i16; size]
}else{
Self::mix_frames(&frames, size)
};
self.buffer.store(Arc::new(mixed_frame));
}
pub fn read(&self, size: usize) -> Vec<i16> {
let mut data = (**self.buffer.load()).clone();
data.resize(size, 0);
data
}
}
impl AudioMixer {
fn mono_to_stereo(mono_samples: Vec<i16>) -> Vec<i16> {
let mut stereo_data = Vec::with_capacity(mono_samples.len() * 2);
// Chaque échantillon mono devient deux échantillons stéréo identiques
for sample in mono_samples {
stereo_data.push(sample); // Canal gauche
stereo_data.push(sample); // Canal droit
}
stereo_data
}
fn mix_frames(frames: &[Vec<i16>], size: usize) -> Vec<i16> {
let mut mixed = vec![0i32; size];
for frame in frames {
for (i, &sample) in frame.iter().enumerate() {
if i < size {
mixed[i] += sample as i32;
}
}
}
let count = frames.len().max(1) as i32; // éviter la division par zéro
mixed
.into_iter()
.map(|sample| (sample / count).clamp(i16::MIN as i32, i16::MAX as i32) as i16)
.collect()
}
}

View File

@@ -1,4 +1,5 @@
use opus::{Application, Channels, Decoder, Encoder};
use crate::utils::audio_utils::AudioSample;
#[derive(Clone)]
pub struct AudioOpus{
@@ -65,22 +66,31 @@ impl AudioOpusEncoder {
Ok(Self{audio_opus, encoder})
}
pub fn encode(&mut self, frames: &[i16]) -> Result<Vec<u8>, String> {
let mut output = vec![0u8; 1276]; // 1276 octets (la vraie worst-case recommandée par Opus).
let len = self.encoder.encode(frames, output.as_mut_slice())
// Méthode générique qui accepte i16 ou f32
pub fn encode<T: AudioSample>(&mut self, frames: &[T]) -> Result<Vec<u8>, String> {
// Convertir tous les échantillons vers i16
let i16_frames: Vec<i16> = frames.iter().map(|sample| sample.to_i16()).collect();
// Utiliser la logique d'encodage existante
let mut output = vec![0u8; 1276];
let len = self.encoder.encode(&i16_frames, output.as_mut_slice())
.map_err(|e| format!("Erreur encodage: {:?}", e))?;
output.truncate(len);
Ok(output)
}
// 🔄 Approche avec buffer réutilisable (encore plus optimal)
fn encode_reuse(&mut self, frames: &[i16], output: &mut Vec<u8>) -> Result<usize, String> {
// Version avec buffer réutilisable
pub fn encode_into_slice<T: AudioSample>(&mut self, frames: &[T], output: &mut Vec<u8>) -> Result<usize, String> {
let i16_frames: Vec<i16> = frames.iter().map(|sample| sample.to_i16()).collect();
output.clear();
output.resize(1276, 0);
let len = self.encoder.encode(frames, output.as_mut_slice()).unwrap();
let len = self.encoder.encode(&i16_frames, output.as_mut_slice())
.map_err(|e| format!("Erreur encodage: {:?}", e))?;
output.truncate(len);
Ok(len)
}
}
pub struct AudioOpusDecoder{
@@ -101,10 +111,26 @@ impl AudioOpusDecoder {
Ok(Self{audio_opus, decoder})
}
pub fn decode(&mut self, frames: &[u8]) -> Result<Vec<i16>, String> {
let mut output = vec![0i16; 5760];
let len = self.decoder.decode(frames, output.as_mut_slice(), false).map_err(|e| format!("Erreur décodage: {:?}", e))?;
output.truncate(len);
pub fn decode<T: AudioSample>(&mut self, frames: &[u8]) -> Result<Vec<T>, String> {
let mut i16_output = vec![0i16; 5760];
let len = self.decoder.decode(frames, i16_output.as_mut_slice(), false)
.map_err(|e| format!("Erreur décodage: {:?}", e))?;
i16_output.truncate(len);
let output: Vec<T> = i16_output.iter().map(|&sample| T::from_i16(sample)).collect();
Ok(output)
}
// Décodage avec buffer réutilisable
pub fn decode_into_slice<T: AudioSample>(&mut self, frames: &[u8], output: &mut Vec<T>) -> Result<usize, String> {
let mut i16_buffer = vec![0i16; 5760];
let len = self.decoder.decode(frames, i16_buffer.as_mut_slice(), false)
.map_err(|e| format!("Erreur décodage: {:?}", e))?;
i16_buffer.truncate(len);
output.clear();
output.extend(i16_buffer.iter().map(|&sample| T::from_i16(sample)));
Ok(len)
}
}

View File

@@ -1,9 +1,11 @@
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::JoinHandle;
use std::time::Instant;
use cpal::{default_host, BufferSize, Device, SampleRate, Stream, StreamConfig, SupportedStreamConfig};
use cpal::traits::{DeviceTrait, HostTrait};
use crate::domain::event::EventBus;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use crate::core::mixer::AudioMixer;
use crate::domain::event::{Event, EventBus};
use crate::utils::real_time_event::RealTimeEvent;
#[derive(Clone)]
@@ -17,7 +19,7 @@ pub struct AudioPlayback {
running: Arc<AtomicBool>,
stream: Option<Stream>,
worker: Option<JoinHandle<()>>,
next_tick: RealTimeEvent
mixer: AudioMixer
}
impl Speaker {
@@ -33,22 +35,99 @@ impl Speaker {
Speaker::new(device)
}
pub fn get_input_config(&self) -> SupportedStreamConfig {
pub fn get_output_config(&self) -> SupportedStreamConfig {
self.device.default_output_config().unwrap()
}
pub fn get_stream_config(&self) -> StreamConfig {
let config = self.get_input_config();
let mut stream_config: StreamConfig = config.into();
stream_config.channels = 2;
stream_config.sample_rate = SampleRate(48000);
stream_config.buffer_size = BufferSize::Fixed(1920);
stream_config
// Lister toutes les configurations supportées
self.print_supported_configs();
self.get_output_config().into()
}
pub fn print_supported_configs(&self) {
println!("\n=== CONFIGURATIONS AUDIO DISPONIBLES ===");
// Configuration par défaut
match self.device.default_output_config() {
Ok(config) => {
println!("📌 Configuration par défaut:");
println!(" Canaux: {}", config.channels());
println!(" Sample Rate: {} Hz", config.sample_rate().0);
println!(" Format: {:?}", config.sample_format());
println!(" Buffer Size: {:?}", config.buffer_size());
},
Err(e) => println!("❌ Impossible d'obtenir la config par défaut: {}", e)
}
// Toutes les configurations supportées
println!("\n📋 Toutes les configurations supportées:");
match self.device.supported_output_configs() {
Ok(configs) => {
for (i, config_range) in configs.enumerate() {
println!("\n Config #{}", i + 1);
println!(" Canaux: {}", config_range.channels());
println!(" Sample Rate: {} - {} Hz",
config_range.min_sample_rate().0,
config_range.max_sample_rate().0);
println!(" Format: {:?}", config_range.sample_format());
println!(" Buffer Size: {:?}", config_range.buffer_size());
// Suggestions de sample rates courants
let common_rates = [8000, 11025, 16000, 22050, 44100, 48000, 88200, 96000];
let mut supported_common = Vec::new();
for rate in common_rates {
if rate >= config_range.min_sample_rate().0 && rate <= config_range.max_sample_rate().0 {
supported_common.push(rate);
}
}
if !supported_common.is_empty() {
println!(" Sample rates courants supportés: {:?}", supported_common);
}
}
},
Err(e) => println!("❌ Impossible de lister les configs: {}", e)
}
// Informations sur le device
println!("\n🎧 Informations du device:");
if let Ok(name) = self.device.name() {
println!(" Nom: {}", name);
}
// Test de configurations spécifiques
println!("\n🧪 Test de configurations spécifiques:");
let test_configs = [
(44100, 1, "Mono 44.1kHz"),
(44100, 2, "Stéréo 44.1kHz"),
(48000, 1, "Mono 48kHz"),
(48000, 2, "Stéréo 48kHz"),
(22050, 2, "Stéréo 22.05kHz"),
];
for (sample_rate, channels, description) in test_configs {
let test_config = StreamConfig {
channels,
sample_rate: SampleRate(sample_rate),
buffer_size: BufferSize::Default
};
// Test si cette config est supportée (tentative de création d'un stream fictif)
let dummy_callback = |_: &mut [f32], _: &cpal::OutputCallbackInfo| {};
match self.device.build_output_stream(&test_config, dummy_callback, |_| {}, None) {
Ok(_) => println!("{} - SUPPORTÉ", description),
Err(_) => println!("{} - NON SUPPORTÉ", description),
}
}
println!("\n===========================================\n");
}
pub fn build_stream<F>(&self, callback: F) -> Stream
where
F: FnMut(&mut [i16], &cpal::OutputCallbackInfo) + Send + 'static,
F: FnMut(&mut [f32], &cpal::OutputCallbackInfo) + Send + 'static,
{
let config = self.get_stream_config();
@@ -58,43 +137,64 @@ impl Speaker {
|err| println!("Error output stream: {err}"),
None
).unwrap()
}
}
impl AudioPlayback {
pub fn new(event_bus: EventBus, speaker: Speaker) -> Self {
pub fn new(event_bus: EventBus, speaker: Speaker, mixer: AudioMixer) -> Self {
Self {
event_bus,
speaker,
running: Arc::new(AtomicBool::new(false)),
stream: None,
worker: None,
next_tick: RealTimeEvent::new(),
mixer
}
}
pub fn default(event_bus: EventBus) -> Self {
pub fn default(event_bus: EventBus, mixer: AudioMixer) -> Self {
let speaker = Speaker::default();
AudioPlayback::new(event_bus, speaker)
AudioPlayback::new(event_bus, speaker, mixer)
}
pub async fn start(&mut self) {
}
pub async fn stop(&mut self) {
self.running.store(false, std::sync::atomic::Ordering::SeqCst);
self.running.store(true, Ordering::SeqCst);
let stream_running = self.running.clone();
let event_bus = self.event_bus.clone();
let mixer = self.mixer.clone();
// stream cpal
println!("Setting up audio playback stream...");
let stream_running = self.running.clone();
let stream = self.speaker.build_stream(move |data, _| {
let last_time = Mutex::new(Instant::now());
let stream = self.speaker.build_stream(move |data, info| {
println!(
"CALLBACK : reçu {} samples, type info = {:?}",
data.len(),
info
);
let now = Instant::now();
let mut last = last_time.lock().unwrap();
let dt = now.duration_since(*last);
println!("Callback audio appelée chaque {:?} ms (≈ {:.1} Hz)", dt.as_millis(), 1000.0 / dt.as_millis().max(1) as f32);
*last = now;
if !stream_running.load(Ordering::Relaxed){
return;
}
// aller récupérer 1920 sur un buffer
// écrire le contenu dans data
let audio_mixer = mixer.read(data.len());
data.copy_from_slice(&audio_mixer);
// println!("data content : {:?}", data);
let _ = event_bus.emit_sync(Event::PlaybackTick(data.len()));
});
stream.play().unwrap();
self.stream = Some(stream);
println!("Audio playback stream started");
}
pub async fn stop(&mut self) {
self.running.store(false, Ordering::SeqCst);
}
}

View File

@@ -8,11 +8,11 @@ use crate::core::opus::{AudioOpus, AudioOpusDecoder};
use crate::utils::ringbuf::{RingBufReader, RingBufWriter, RingBuffer};
use crate::utils::shared_store::SharedArcMap;
struct AudioClient {
pub struct AudioClient {
uuid: uuid::Uuid,
decode_sender: mpsc::Sender<DecodeRequest>,
buffer_reader: RingBufReader<Vec<i16>>,
buffer_writer: RingBufWriter<Vec<i16>>
buffer_reader: RingBufReader<f32>,
buffer_writer: RingBufWriter<f32>
}
struct DecodeRequest {
@@ -21,36 +21,38 @@ struct DecodeRequest {
}
#[derive(Clone)]
struct AudioClientManager {
pub struct AudioClientManager {
audio_clients: SharedArcMap<uuid::Uuid, AudioClient>,
}
impl AudioClient {
pub fn new() -> Self {
let (writer, reader) = RingBuffer::<Vec<i16>>::new(1024).split();
let (writer, reader) = RingBuffer::<f32>::new(4096).split();
let (decode_sender, mut decode_reader) = mpsc::channel::<DecodeRequest>(100);
let writer_clone = writer.clone();
let decode_handle = tokio::spawn(async move {
let mut decoder = AudioOpus::new(44800, 1, "voip")
let mut decoder = AudioOpus::new(48000, 1, "voip")
.create_decoder().unwrap();
let mut last_sequence: u16 = 0;
while let Some(request) = decode_reader.recv().await {
// si la séquence est "trop vieille" on la drop. (voir plus tard pour un système de ratrapage si c'est possible)
// si la séquence est "trop vieille" on la drop. (voir plus tard pour un système de rattrapage si c'est possible)
if last_sequence < request.sequence {
// todo : si le décodage est trop long, voir pour le mettre dans un thread
// avec let result = tokio::task::spawn_blocking({
// let data = request.data.clone();
// move || decoder.decode(&data)
// }).await.unwrap();
// todo : si le décodage est trop long, voir pour le mettre dans un thread avec
// let result = tokio::task::spawn_blocking({
// let data = request.data.clone();
// move || decoder.decode(&data)
// }).await.unwrap();
let start = std::time::Instant::now();
let result = decoder.decode(&request.data);
let result = decoder.decode::<f32>(&request.data);
if start.elapsed() > Duration::from_millis(1) {
println!("⚠️ Frame drop possible: {:?}", start.elapsed());
}
match result {
Ok(audio_frame) => {
// Pousser la frame complète dans le buffer
writer.push(audio_frame);
writer_clone.push_slice_overwrite(&audio_frame);
},
Err(e) => {
eprintln!("Erreur de décodage audio : {}", e);
@@ -69,19 +71,66 @@ impl AudioClient {
}
}
pub async fn write_audio(&self, sequence: u16, data: Bytes) {
let _ = self.decode_sender.send(DecodeRequest {
pub fn write_audio(&self, sequence: u16, data: Bytes) {
let _ = self.decode_sender.try_send(DecodeRequest {
data,
sequence
});
}
pub fn read_audio(&self, size: usize) -> Option<Vec<f32>> {
if self.buffer_reader.len() < size {
return None;
}
let mut buffer = vec![0f32; size];
let read_count = self.buffer_reader.pop_slice(&mut buffer);
if read_count == size {
Some(buffer)
}else {
None
}
}
}
impl AudioClientManager {
fn new() -> Self {
pub fn new() -> Self {
Self {
audio_clients: SharedArcMap::new()
}
}
pub fn audio_client_exists(&self, uuid: uuid::Uuid) -> bool {
self.audio_clients.contains_key(&uuid)
}
pub fn get_audio_client(&self, uuid: uuid::Uuid) -> Option<Arc<AudioClient>> {
self.audio_clients.get(&uuid)
}
pub fn add_audio_client(&self, uuid: uuid::Uuid, audio_client: AudioClient) {
self.audio_clients.insert(uuid, audio_client);
}
pub fn remove_audio_client(&self, uuid: uuid::Uuid) {
self.audio_clients.remove(&uuid);
}
pub fn write_audio_to_client(&self, uuid: uuid::Uuid, sequence: u16, data: Bytes) {
let _ = self.audio_clients.get(&uuid).unwrap().write_audio(sequence, data);
}
pub fn take_audio_collection(&self, size: usize) -> Vec<Vec<f32>> {
let mut buffers = Vec::new();
for client in self.audio_clients.values() {
if let Some(buffer) = client.read_audio(size) {
buffers.push(buffer);
}
}
buffers
}
}

View File

@@ -1,46 +1,47 @@
use tokio::sync::mpsc;
use bytes::Bytes;
use crate::network::protocol::{MessageClient, MessageServer};
pub enum Event {
AppStarted,
AppStopped,
AudioIn(Vec<i16>),
AudioIn(Vec<f32>),
AudioEncoded(Vec<u8>),
PlaybackTick(usize),
NetConnected,
NetDisconnected,
NetIn(MessageServer),
NetOut(MessageClient),
UiStarted,
UiStopped,
TaskTick
}
#[derive(Clone)]
pub struct EventBus {
pub sender: mpsc::Sender<Event>
pub sender: kanal::AsyncSender<Event>
}
impl EventBus {
pub fn new() -> (Self, mpsc::Receiver<Event>) {
let (sender, receiver) = mpsc::channel(4096);
pub fn new() -> (Self, kanal::AsyncReceiver<Event>) {
let (sender, receiver) = kanal::bounded_async::<Event>(4096);
(Self { sender }, receiver)
}
pub async fn emit(&self, event: Event) {
// s'utilise de cette façon : bus.emit(Event::AudioIn {Vec[0,1,2,3]}.await;
let _ = self.sender.send(event).await;
}
pub fn emit_sync(&self, event: Event) {
let _ = self.sender.try_send(event);
}
pub fn clone_sender(&self) -> mpsc::Sender<Event> {
pub fn clone_sender(&self) -> kanal::AsyncSender<Event> {
self.sender.clone()
}
}

View File

@@ -8,6 +8,8 @@ use bytes::Bytes;
use parking_lot::RwLock;
use tokio::task::AbortHandle;
use uuid::Uuid;
use crate::core::mixer::AudioMixer;
use crate::domain::audio_client::{AudioClient, AudioClientManager};
use crate::domain::event::{Event, EventBus};
use crate::network::protocol::{MessageClient, MessageServer};
use crate::network::udp::UdpSession;
@@ -20,11 +22,14 @@ struct PingInfo {
#[derive(Clone)]
pub struct Dispatcher {
event_bus: EventBus,
event_bus: Arc<EventBus>,
udp_session: UdpSession,
udp_session: Arc<UdpSession>,
tauri_handle: AppHandle,
tauri_handle: Arc<AppHandle>,
audio_client_manager: Arc<AudioClientManager>,
audio_mixer: Arc<AudioMixer>,
// todo : temporaire, le temps d'avoir un handler
@@ -51,23 +56,31 @@ impl PingInfo {
}
impl Dispatcher {
pub fn new(event_bus: EventBus, udp_session: UdpSession, tauri_handle: AppHandle) -> Self {
pub fn new(event_bus: EventBus,
udp_session: UdpSession,
audio_client_manager: AudioClientManager,
audio_mixer: AudioMixer,
tauri_handle: AppHandle
) -> Self {
Self {
event_bus,
udp_session,
event_bus: Arc::new(event_bus),
udp_session: Arc::new(udp_session),
sequence_counter: Arc::new(AtomicU16::new(0)),
tauri_handle,
tauri_handle: Arc::new(tauri_handle),
can_send_audio: Arc::new(AtomicBool::new(false)),
ping_tracker: Arc::new(RwLock::new(HashMap::new())),
audio_client_manager: Arc::new(audio_client_manager),
audio_mixer: Arc::new(audio_mixer),
}
}
pub async fn start(&mut self, mut receiver: mpsc::Receiver<Event>) {
pub async fn start(&self, receiver: kanal::AsyncReceiver<Event>) {
let (_udp_in_abort_handle, udp_in_sender) = self.udp_in_handler().await;
let udp_session = self.udp_session.clone();
let sequence_counter = self.sequence_counter.clone();
let audio_mixer = self.audio_mixer.clone();
while let Some(event) = receiver.recv().await {
while let Ok(event) = receiver.recv().await {
match event {
Event::AudioIn(sample) => {
@@ -84,8 +97,11 @@ impl Dispatcher {
sequence_counter.fetch_add(1, Ordering::Relaxed);
}
Event::PlaybackTick(paquet_size) => {
audio_mixer.mix_next_frame(paquet_size);
}
Event::NetIn(message_event) => {
println!("NetIn: {:?}", message_event);
// println!("NetIn: {:?}", message_event);
let _ = udp_in_sender.send(message_event).await;
}
Event::NetOut(message_call) => {
@@ -117,6 +133,7 @@ impl Dispatcher {
pub async fn udp_in_handler(&self) -> (AbortHandle, mpsc::Sender<MessageServer>) {
let (sender, mut consumer) = mpsc::channel::<MessageServer>(1024);
let ping_tracker = Arc::clone(&self.ping_tracker);
let audio_client_manager = self.audio_client_manager.clone();
let task = tokio::spawn(async move {
while let Some(message) = consumer.recv().await {
@@ -135,7 +152,13 @@ impl Dispatcher {
}
}
MessageServer::Audio {user, sequence, data} => {
// Audio reçu
// println!("Audio received from {}: {} -> {:?}", user, sequence, data);
if audio_client_manager.audio_client_exists(user) {
audio_client_manager.write_audio_to_client(user, sequence, data);
}else{
let client = AudioClient::new();
audio_client_manager.add_audio_client(user, client);
}
}
}
}

View File

@@ -1,4 +1,4 @@
use tauri::Manager;
use tauri::{Manager, WindowEvent};
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::app::ox_speak_app::OxSpeakApp;
@@ -45,6 +45,14 @@ pub async fn run() {
Ok(())
})
// .invoke_handler(tauri::generate_handler![greet])
.on_window_event(|window, event| match event {
WindowEvent::CloseRequested {api, ..} => {
println!("Closing window");
}
_ => {
}
})
.run(context)
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,546 @@
use std::fmt::Debug;
use rubato::{Resampler, SincFixedIn, SincInterpolationType, SincInterpolationParameters, WindowFunction};
use parking_lot::Mutex;
use std::sync::Arc;
/// Trait pour les échantillons audio avec conversion automatique
pub trait AudioSample: Copy + Clone + Debug + Send + Sync + 'static {
fn to_i16(&self) -> i16;
fn to_f32(&self) -> f32;
fn from_i16(value: i16) -> Self;
fn from_f32(value: f32) -> Self;
fn zero() -> Self;
fn clamp_audio(&self) -> Self;
}
impl AudioSample for i16 {
fn to_i16(&self) -> i16 {
*self
}
fn to_f32(&self) -> f32 {
*self as f32 / i16::MAX as f32
}
fn from_i16(value: i16) -> Self {
value
}
fn from_f32(value: f32) -> Self {
(value.clamp(-1.0, 1.0) * i16::MAX as f32) as i16
}
fn zero() -> Self {
0i16
}
fn clamp_audio(&self) -> Self {
*self // i16 est déjà dans sa plage valide
}
}
impl AudioSample for f32 {
fn to_i16(&self) -> i16 {
(self.clamp(-1.0, 1.0) * i16::MAX as f32) as i16
}
fn to_f32(&self) -> f32 {
*self
}
fn from_i16(value: i16) -> Self {
value as f32 / i16::MAX as f32
}
fn from_f32(value: f32) -> Self {
value
}
fn zero() -> Self {
0.0f32
}
fn clamp_audio(&self) -> Self {
self.clamp(-1.0, 1.0)
}
}
/// Resampler audio optimisé avec rubato pour temps réel
#[derive(Clone)]
pub struct AudioResampler {
// État du resampler (recréé quand les paramètres changent)
state: Arc<Mutex<ResamplerState>>,
// Buffer de conversion réutilisable (évite allocations)
conversion_buffers: Arc<Mutex<ConversionBuffers>>,
}
struct ResamplerState {
resampler: Option<SincFixedIn<f32>>,
current_from_rate: usize,
current_to_rate: usize,
current_channels: usize,
}
struct ConversionBuffers {
f32_buffer: Vec<f32>,
planar_input: Vec<Vec<f32>>,
planar_output_buffer: Vec<Vec<f32>>, // Ajouté pour éviter des allocs
}
impl AudioResampler {
pub fn new() -> Self {
Self {
state: Arc::new(Mutex::new(ResamplerState {
resampler: None,
current_from_rate: 0,
current_to_rate: 0,
current_channels: 0,
})),
conversion_buffers: Arc::new(Mutex::new(ConversionBuffers {
f32_buffer: Vec::with_capacity(8192),
planar_input: Vec::new(),
planar_output_buffer: Vec::new(),
})),
}
}
/// Resample audio générique - fonctionne avec i16 et f32
pub fn resample<T: AudioSample>(
&self,
input: &[T],
from_sample_rate: usize,
to_sample_rate: usize,
channels: usize,
) -> Vec<T> {
// ✅ Pas de conversion si même sample rate
if from_sample_rate == to_sample_rate || input.is_empty() {
return input.to_vec();
}
let mut state = self.state.lock();
let mut buffers = self.conversion_buffers.lock();
// 🔄 Recrée le resampler si configuration changée
if state.resampler.is_none()
|| state.current_from_rate != from_sample_rate
|| state.current_to_rate != to_sample_rate
|| state.current_channels != channels {
match Self::create_resampler(from_sample_rate, to_sample_rate, channels) {
Ok(new_resampler) => {
state.resampler = Some(new_resampler);
state.current_from_rate = from_sample_rate;
state.current_to_rate = to_sample_rate;
state.current_channels = channels;
println!("🔧 Resampler reconfiguré: {}Hz → {}Hz, {} canaux",
from_sample_rate, to_sample_rate, channels);
}
Err(e) => {
eprintln!("❌ Erreur création resampler: {}", e);
return input.to_vec();
}
}
}
// 🚀 Processing avec le resampler
if let Some(ref mut resampler) = state.resampler {
match Self::process_with_resampler_generic(resampler, input, channels, &mut buffers) {
Ok(output) => output,
Err(e) => {
eprintln!("❌ Erreur resampling: {}", e);
input.to_vec()
}
}
} else {
input.to_vec()
}
}
/// Crée un resampler optimisé pour votre cas d'usage
fn create_resampler(
from_rate: usize,
to_rate: usize,
channels: usize,
) -> Result<SincFixedIn<f32>, Box<dyn std::error::Error>> {
let ratio = to_rate as f64 / from_rate as f64;
// 🎯 Paramètres optimisés pour audio temps réel de qualité
let params = SincInterpolationParameters {
sinc_len: 256, // Bon compromis qualité/performance
f_cutoff: 0.95, // Anti-aliasing fort
interpolation: SincInterpolationType::Linear, // Plus rapide que Cubic
oversampling_factor: 256,
window: WindowFunction::BlackmanHarris2,
};
// Chunk size optimisé pour vos frames audio
let chunk_size = 1024; // Compatible avec vos frames de 960-1024 samples
Ok(SincFixedIn::<f32>::new(
ratio,
2.0, // Max ratio change pour stabilité
params,
chunk_size,
channels,
)?)
}
/// Process audio générique avec buffers réutilisables
fn process_with_resampler_generic<T: AudioSample>(
resampler: &mut SincFixedIn<f32>,
input: &[T],
channels: usize,
buffers: &mut ConversionBuffers,
) -> Result<Vec<T>, Box<dyn std::error::Error>> {
let frames = input.len() / channels;
// 🔄 1. Conversion vers f32 (utilise AudioSample trait)
buffers.f32_buffer.clear();
buffers.f32_buffer.extend(input.iter().map(|sample| sample.to_f32()));
// 🔄 2. Conversion interleaved → planar
buffers.planar_input.clear();
buffers.planar_input.resize(channels, Vec::with_capacity(frames));
for ch in 0..channels {
buffers.planar_input[ch].clear();
}
for frame in buffers.f32_buffer.chunks_exact(channels) {
for (ch, &sample) in frame.iter().enumerate() {
buffers.planar_input[ch].push(sample);
}
}
// 🎯 3. Resampling magique ! (Rubato travaille directement en f32)
let output_planar = resampler.process(&buffers.planar_input, None)?;
// 🔄 4. Conversion planar → interleaved avec type générique
let output_frames = output_planar[0].len();
let mut output = Vec::with_capacity(output_frames * channels);
for frame_idx in 0..output_frames {
for ch in 0..channels {
let f32_sample = output_planar[ch][frame_idx];
// Utilise AudioSample pour reconvertir au format d'origine
let converted_sample = T::from_f32(f32_sample).clamp_audio();
output.push(converted_sample);
}
}
Ok(output)
}
/// Version spécialisée pour f32 (plus efficace, évite conversions inutiles)
pub fn resample_f32(
&self,
input: &[f32],
from_sample_rate: usize,
to_sample_rate: usize,
channels: usize,
) -> Vec<f32> {
// ✅ Pas de conversion si même sample rate
if from_sample_rate == to_sample_rate || input.is_empty() {
return input.to_vec();
}
let mut state = self.state.lock();
let mut buffers = self.conversion_buffers.lock();
// 🔄 Recrée le resampler si configuration changée
if state.resampler.is_none()
|| state.current_from_rate != from_sample_rate
|| state.current_to_rate != to_sample_rate
|| state.current_channels != channels {
match Self::create_resampler(from_sample_rate, to_sample_rate, channels) {
Ok(new_resampler) => {
state.resampler = Some(new_resampler);
state.current_from_rate = from_sample_rate;
state.current_to_rate = to_sample_rate;
state.current_channels = channels;
}
Err(e) => {
eprintln!("❌ Erreur création resampler: {}", e);
return input.to_vec();
}
}
}
// 🚀 Processing optimisé pour f32 (pas de conversion)
if let Some(ref mut resampler) = state.resampler {
match Self::process_f32_direct(resampler, input, channels, &mut buffers) {
Ok(output) => output,
Err(e) => {
eprintln!("❌ Erreur resampling f32: {}", e);
input.to_vec()
}
}
} else {
input.to_vec()
}
}
/// Processing optimisé pour f32 natif (pas de conversions)
fn process_f32_direct(
resampler: &mut SincFixedIn<f32>,
input: &[f32],
channels: usize,
buffers: &mut ConversionBuffers,
) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
let frames = input.len() / channels;
// 🔄 1. Conversion interleaved → planar (pas de conversion de format!)
buffers.planar_input.clear();
buffers.planar_input.resize(channels, Vec::with_capacity(frames));
for ch in 0..channels {
buffers.planar_input[ch].clear();
}
for frame in input.chunks_exact(channels) {
for (ch, &sample) in frame.iter().enumerate() {
buffers.planar_input[ch].push(sample);
}
}
// 🎯 2. Resampling direct !
let output_planar = resampler.process(&buffers.planar_input, None)?;
// 🔄 3. Conversion planar → interleaved (pas de conversion de format!)
let output_frames = output_planar[0].len();
let mut output = Vec::with_capacity(output_frames * channels);
for frame_idx in 0..output_frames {
for ch in 0..channels {
output.push(output_planar[ch][frame_idx]);
}
}
Ok(output)
}
/// Réinitialise l'état interne (pour éviter glitches lors de changements)
pub fn reset(&self) {
let mut state = self.state.lock();
if let Some(ref mut resampler) = state.resampler {
let _ = resampler.reset();
}
println!("🔄 Resampler reset");
}
/// Version oneshot générique sans état (pour tests)
pub fn resample_oneshot<T: AudioSample>(
input: &[T],
from_rate: usize,
to_rate: usize,
channels: usize,
) -> Result<Vec<T>, Box<dyn std::error::Error>> {
let resampler = AudioResampler::new();
Ok(resampler.resample(input, from_rate, to_rate, channels))
}
}
pub struct AudioTools;
impl AudioTools {
/// Convertit un échantillon audio multi-canaux en mono (générique)
///
/// # Arguments
/// * `samples` - Les échantillons audio (format interleaved)
/// * `channels` - Le nombre de canaux d'entrée
///
/// # Returns
/// Un Vec<T> contenant les échantillons mono
///
/// # Example
/// ```ignore
/// // Avec i16
///
/// let stereo_i16 = vec![100i16, 200i16, 150i16, 250i16];
/// let mono_i16 = AudioTools::sample_to_mono(&stereo_i16, 2);
///
/// // Avec f32
/// let stereo_f32 = vec![0.5f32, -0.3f32, 0.2f32, 0.8f32];
/// let mono_f32 = AudioTools::sample_to_mono(&stereo_f32, 2);
/// ```
pub fn sample_to_mono<T: AudioSample>(samples: &[T], channels: usize) -> Vec<T> {
// Si déjà mono, retourne une copie
if channels <= 1 {
return samples.to_vec();
}
// Calcule le nombre de frames
let frame_count = samples.len() / channels;
let mut mono_samples = Vec::with_capacity(frame_count);
// Pour chaque frame, calcule la moyenne des canaux
for frame_start in (0..samples.len()).step_by(channels) {
let frame_end = (frame_start + channels).min(samples.len());
let frame = &samples[frame_start..frame_end];
// Calcule la moyenne en utilisant f32 comme format intermédiaire
let sum: f32 = frame.iter().map(|sample| sample.to_f32()).sum();
let average = sum / channels as f32;
// Convertit de retour au format original et clamp
let mono_sample = T::from_f32(average).clamp_audio();
mono_samples.push(mono_sample);
}
mono_samples
}
/// Version avec stratégies de mixage (générique)
pub fn sample_to_mono_with_strategy<T: AudioSample>(
samples: &[T],
channels: usize,
strategy: MonoMixStrategy
) -> Vec<T> {
if channels <= 1 {
return samples.to_vec();
}
let frame_count = samples.len() / channels;
let mut mono_samples = Vec::with_capacity(frame_count);
for frame_start in (0..samples.len()).step_by(channels) {
let frame_end = (frame_start + channels).min(samples.len());
let frame = &samples[frame_start..frame_end];
let mono_sample = match strategy {
MonoMixStrategy::Average => {
let sum: f32 = frame.iter().map(|s| s.to_f32()).sum();
let average = sum / channels as f32;
T::from_f32(average).clamp_audio()
}
MonoMixStrategy::LeftChannel => frame[0],
MonoMixStrategy::RightChannel => {
if channels > 1 { frame[1] } else { frame[0] }
}
MonoMixStrategy::Max => {
let max_sample = frame.iter()
.max_by(|a, b| {
a.to_f32().abs().partial_cmp(&b.to_f32().abs())
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(&frame[0]);
*max_sample
}
MonoMixStrategy::Rms => {
// Root Mean Square
let sum_squares: f32 = frame.iter()
.map(|s| {
let val = s.to_f32();
val * val
})
.sum();
let rms = (sum_squares / channels as f32).sqrt();
// Préserve le signe général
let sum_sign: f32 = frame.iter().map(|s| s.to_f32()).sum();
let final_value = if sum_sign >= 0.0 { rms } else { -rms };
T::from_f32(final_value).clamp_audio()
}
};
mono_samples.push(mono_sample);
}
mono_samples
}
/// Conversion mono vers stéréo (générique)
pub fn mono_to_stereo<T: AudioSample>(mono_samples: &[T]) -> Vec<T> {
let mut stereo_data = Vec::with_capacity(mono_samples.len() * 2);
for &sample in mono_samples {
stereo_data.push(sample); // Canal gauche
stereo_data.push(sample); // Canal droit
}
stereo_data
}
/// Conversion entre formats d'échantillons
pub fn convert_format<From: AudioSample, To: AudioSample>(samples: &[From]) -> Vec<To> {
samples.iter()
.map(|&sample| To::from_f32(sample.to_f32()))
.collect()
}
/// Mix plusieurs frames audio ensemble (générique)
pub fn mix_frames<T: AudioSample>(frames: &[Vec<T>], target_size: usize) -> Vec<T> {
if frames.is_empty() {
return vec![T::zero(); target_size];
}
let mut mixed = vec![0.0f32; target_size];
// Mix tous les frames en f32 pour éviter les débordements
for frame in frames {
for (i, &sample) in frame.iter().enumerate() {
if i < target_size {
mixed[i] += sample.to_f32();
}
}
}
let count = frames.len().max(1) as f32;
// Convertit de retour au format cible avec normalisation
mixed.into_iter()
.map(|sample| T::from_f32(sample / count).clamp_audio())
.collect()
}
/// Utilitaire pour changer le nombre de canaux
pub fn change_channel_count<T: AudioSample>(
samples: &[T],
from_channels: usize,
to_channels: usize,
) -> Vec<T> {
match (from_channels, to_channels) {
(1, 2) => Self::mono_to_stereo(samples),
(2, 1) => Self::sample_to_mono(samples, 2),
(from, to) if from == to => samples.to_vec(),
(from, 1) => Self::sample_to_mono(samples, from),
(1, to) => {
// Mono vers multi-canaux : duplique sur tous les canaux
let mut result = Vec::with_capacity(samples.len() * to);
for &sample in samples {
for _ in 0..to {
result.push(sample);
}
}
result
}
(from, to) => {
// Cas complexe : passe par mono puis étend
let mono = Self::sample_to_mono(samples, from);
Self::change_channel_count(&mono, 1, to)
}
}
}
}
/// Stratégies de conversion multi-canaux vers mono
#[derive(Debug, Clone, Copy)]
pub enum MonoMixStrategy {
/// Moyenne de tous les canaux (par défaut)
Average,
/// Utilise seulement le canal gauche
LeftChannel,
/// Utilise seulement le canal droit
RightChannel,
/// Prend l'échantillon avec l'amplitude maximale
Max,
/// Root Mean Square - plus précis énergétiquement
Rms,
}
impl Default for MonoMixStrategy {
fn default() -> Self {
Self::Average
}
}

View File

@@ -1,3 +1,4 @@
pub mod ringbuf;
pub mod real_time_event;
pub mod shared_store;
pub mod shared_store;
pub mod audio_utils;