diff --git a/README.md b/README.md index ec8226e..637ff3f 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 723ff4d..a10cd08 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2280,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" @@ -2297,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" @@ -2651,6 +2669,7 @@ dependencies = [ "moka", "opus", "parking_lot", + "rubato", "serde", "serde_json", "strum", @@ -2967,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" @@ -3145,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" @@ -3264,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" @@ -3279,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" @@ -3688,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" @@ -4446,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" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cd14e99..2d0032a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,3 +35,4 @@ moka = {version = "0.12", features = ["future"] } arc-swap = "1.7" crossbeam-channel = "0.5" kanal = "0.1" +rubato = "0.16.2" diff --git a/src-tauri/src/app/ox_speak_app.rs b/src-tauri/src/app/ox_speak_app.rs index d00a78b..97612b3 100644 --- a/src-tauri/src/app/ox_speak_app.rs +++ b/src-tauri/src/app/ox_speak_app.rs @@ -88,8 +88,8 @@ impl OxSpeakApp { } // 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"); diff --git a/src-tauri/src/core/capture.rs b/src-tauri/src/core/capture.rs index 618b2aa..5e9dfe4 100644 --- a/src-tauri/src/core/capture.rs +++ b/src-tauri/src/core/capture.rs @@ -4,6 +4,7 @@ use std::thread; use std::thread::JoinHandle; use cpal::{default_host, BufferSize, Device, SampleRate, Stream, StreamConfig, SupportedStreamConfig}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use rubato::{FftFixedInOut, Resampler}; use crate::core::opus::AudioOpus; use crate::domain::event::{Event, EventBus}; use crate::utils::ringbuf::RingBuffer; @@ -17,7 +18,7 @@ pub struct AudioCapture { event_bus: EventBus, microphone: Microphone, running: Arc, - ring_buffer: RingBuffer, + ring_buffer: RingBuffer, steam: Option, worker: Option>, } @@ -25,8 +26,8 @@ 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 } } @@ -42,17 +43,25 @@ 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 + let supported_channels = self.get_supported_channels(); + + // Priorité : mono si supporté, sinon prend le premier disponible + let channels = if supported_channels.contains(&1) { + 1 // ✅ Mono préféré + } else { + supported_channels.first().copied().unwrap_or(2) // Fallback + }; + + StreamConfig { + channels, + sample_rate: SampleRate(48_000), + buffer_size: BufferSize::Default, + } } pub fn build_stream(&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(); @@ -63,6 +72,13 @@ impl Microphone { None ).unwrap() } + + // helpers + pub fn get_supported_channels(&self) -> Vec { + self.device.supported_input_configs() + .map(|configs| configs.map(|c| c.channels()).collect()) + .unwrap_or(vec![1]) + } } impl AudioCapture { @@ -131,33 +147,53 @@ impl AudioCapture { 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"); + println!("Audio input config: sample rate: {}, channels: {}", + input_config.sample_rate().0, input_config.channels()); + + // création du worker opus + let opus = AudioOpus::new(48_000, 1, "voip"); let mut encoder = opus.create_encoder().unwrap(); + + // création du worker resampler + let source_rate = input_config.sample_rate().0 as usize; + let mut resampler = if source_rate != 48_000 { + Some(Self::create_resampler(source_rate)) // ✅ Corrigé + } else { + None + }; + + // création du ringbuffer let reader = self.ring_buffer.reader(); + // Démarrage du worker println!("Spawning audio processing thread"); + let stream_config = self.microphone.get_stream_config(); // ✅ Clone la config self.worker = Some(thread::spawn(move || { println!("Audio processing thread started"); - let mut frame = [0i16; 960]; - let mut frame_count = 0; + + let source_rate = stream_config.sample_rate.0 as usize; // ✅ Utilise stream_config + let frame_size = source_rate * 10 / 1000; // 10ms en échantillons + let mut raw_buffer = vec![0.0f32; frame_size]; while worker_running.load(Ordering::Relaxed) { - let _ = reader.pop_slice_blocking(&mut frame); + 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); - } + // Resampling : 441→480, 48000→480, 96000→480, etc. + let processed_audio = Self::process_audio_frame( + &stream_config, // ✅ Utilise stream_config + resampler.as_mut(), + &raw_buffer + ); + // processed_audio est TOUJOURS 480 f32 (mono/48kHz) - let raw_data = frame.to_vec(); - event_bus.emit_sync(Event::AudioIn(raw_data)); + // Events + event_bus.emit_sync(Event::AudioIn(processed_audio.clone())); - match encoder.encode(&frame){ + match encoder.encode(&processed_audio){ Ok(encoded_data) => { event_bus.emit_sync(Event::AudioEncoded(encoded_data)) } @@ -166,13 +202,52 @@ impl AudioCapture { } } } - println!("Audio processing thread finished after processing {} frames", frame_count); })); } -} -impl AudioCapture { - fn audio_processing(){ + /// Standardise un buffer en mono float32 échantillonné à 48kHz. + fn process_audio_frame( + config: &StreamConfig, + resampler: Option<&mut FftFixedInOut>, + samples: &[f32], + ) -> Vec { + // 1. Conversion mono d'abord + let mono = Self::sample_to_mono(config.channels as usize, samples); + // 2. Resampling si nécessaire + if let Some(resampler) = resampler { + let input: Vec<&[f32]> = vec![&mono]; + match resampler.process(&input, None) { + Ok(mut output) => output.remove(0), + Err(e) => { + println!("Erreur de resampling: {e}"); + mono // Fallback + } + } + } else { + mono + } } -} + + fn sample_to_mono(input_channels: usize, samples: &[f32]) -> Vec { + if input_channels == 1 { + samples.to_vec() + }else{ + samples + .chunks_exact(input_channels) + .map(|frame| frame.iter().copied().sum::() / input_channels as f32) + .collect() + } + } + + // ✅ Méthode corrigée avec le bon nom + fn create_resampler(source_rate: usize) -> FftFixedInOut { + println!("Creating resampler: {}Hz -> 48kHz", source_rate); + FftFixedInOut::::new( + source_rate, + 48_000, + 512, // chunk size + 1, // mono après conversion + ).unwrap() + } +} \ No newline at end of file diff --git a/src-tauri/src/core/mixer.rs b/src-tauri/src/core/mixer.rs index ca4db7b..389c40c 100644 --- a/src-tauri/src/core/mixer.rs +++ b/src-tauri/src/core/mixer.rs @@ -1,20 +1,24 @@ -// version pull-based +// 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 crate::domain::audio_client::AudioClientManager; +use crate::utils::ringbuf::{RingBufReader, RingBufWriter, RingBuffer}; #[derive(Clone)] pub struct AudioMixer { audio_client_manager: AudioClientManager, - buffer: Arc>> + buffer_writer: Arc>, + buffer_reader: Arc> } impl AudioMixer { pub fn new(audio_client_manager: AudioClientManager) -> Self { + let (buffer_writer, buffer_reader) = RingBuffer::new(2048).split(); Self { audio_client_manager, - buffer: Arc::new(ArcSwap::from_pointee(Vec::new())), + buffer_writer: Arc::new(buffer_writer), + buffer_reader: Arc::new(buffer_reader) } } pub fn mix_next_frame(&self, size: usize) { @@ -34,14 +38,18 @@ impl AudioMixer { Self::mix_frames(&frames, size) }; - self.buffer.store(Arc::new(mixed_frame)); + self.buffer_writer.push_slice_overwrite(&mixed_frame); } pub fn read(&self, size: usize) -> Vec { - let mut data = (**self.buffer.load()).clone(); - data.resize(size, 0); + let mut data = vec![0i16; 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 } + + } impl AudioMixer { diff --git a/src-tauri/src/core/mixer_arcswap.rs b/src-tauri/src/core/mixer_arcswap.rs new file mode 100644 index 0000000..4aa00b6 --- /dev/null +++ b/src-tauri/src/core/mixer_arcswap.rs @@ -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>> +} + +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::>::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::>>(); + 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 { + let mut data = (**self.buffer.load()).clone(); + data.resize(size, 0); + data + } +} + +impl AudioMixer { + fn mono_to_stereo(mono_samples: Vec) -> Vec { + 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], size: usize) -> Vec { + 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() + + } +} \ No newline at end of file diff --git a/src-tauri/src/core/mixer_push.rs b/src-tauri/src/core/mixer_push.rs deleted file mode 100644 index a1950aa..0000000 --- a/src-tauri/src/core/mixer_push.rs +++ /dev/null @@ -1,229 +0,0 @@ -// version non utilisé, c'était un prototype "push-based", jamais testé - -use std::sync::{atomic, Arc}; -use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicUsize}; -use arc_swap::ArcSwap; -use bytes::Bytes; -use tokio::sync::{mpsc, Notify}; -use crate::utils::ringbuf::{RingBufReader, RingBufWriter, RingBuffer}; - -pub struct Mixer { - // writer: Arc>, - // reader: Arc>, - pre_buffer: Arc, - worker_sender: Option>>, - buffer: Arc>, // 1920 frames -} - -impl Mixer { - pub fn new() -> Self { - let (writer, reader) = RingBuffer::::new(1024).split(); - Self { - // writer: Arc::new(writer), - // reader: Arc::new(reader), - pre_buffer: Arc::new(PreBuffer::new()), - worker_sender: None, - buffer: Arc::new(ArcSwap::from_pointee([0i16; 1920])), - } - } - - // Démarrer le worker de pré-traitement - pub async fn start(&mut self){ - let (sender, mut receiver) = mpsc::unbounded_channel::>(); - - // let (writer, reader) = RingBuffer::::new(1024).split(); - self.worker_sender = Some(sender); - let prebuffer = self.pre_buffer.clone(); - // worker de pré-traitement - tokio::spawn(async move { - while let Some(data) = receiver.recv().await { - // data doit exactement faire 960 (mono) ou 1920 (stéréo), 20ms - // si il fait 960, on converti en stéréo - // on écrit dans un buffer de pré-traitement - // si data rempli pas les condition, on ignore - - // on vérifie la taille des données - match data.len() { - 960 => { - // Mono 20ms @ 48kHz - convertir en stéréo - let stereo_data = Self::mono_to_stereo(data); - // push dans un buffer de pré-traitement - prebuffer.push(stereo_data).await; - }, - 1920 => { - // push dans un buffer de pré-traitement - prebuffer.push(data).await; - } - _ => { - println!("⚠️ Données audio ignorées - taille incorrecte: {} bytes", data.len()); - } - } - } - }); - } - - // Envoyer des données au pré-traitement - pub async fn write(&self, data: Vec){ - if let Some(sender) = self.worker_sender.as_ref() { - let _ = sender.send(data); - } - } - - // S'occupe de générer la trame qui sera lu dans 20ms - pub async fn mix(&self){ - // récupérer le buffer de pré-traitement, qui sera tableau de Vec (le lock ? pour éviter que le worker de pré-traitement continue d'alimenter) - // le mixer (sum de chaque trame ?) - // le mettre dans un buffer, qui sera accessible par AudioPlayback - // écraser le buffer de pré-traitement - // (libérer le lock) - let frames = self.pre_buffer.read_all().await; - - let mixed_frame = if frames.is_empty() { - [0i16; 1920] // Silence - } else { - Self::mix_frames(&frames) - }; - - // ✅ Swap direct - aucune conversion ! - self.buffer.store(Arc::new(mixed_frame)); - } - - // Récupérer la trame présente qui est déjà pré-généré par mix - pub fn read(&self) -> [i16; 1920] { - // vider le buffer - **self.buffer.load() - } -} - -impl Mixer { - // Functions helpers - fn mono_to_stereo(mono_samples: Vec) -> Vec { - 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 - } - - // Mixer plusieurs trames - fn mix_frames(frames: &[Vec]) -> [i16; 1920] { - let mut mixed = [0i32; 1920]; - - for frame in frames { - for (i, &sample) in frame.iter().enumerate() { - if i < 1920 { - mixed[i] += sample as i32; - } - } - } - - let mut result = [0i16; 1920]; - let count = frames.len() as i32; - for (i, &sample) in mixed.iter().enumerate() { - result[i] = (sample / count).clamp(i16::MIN as i32, i16::MAX as i32) as i16; - } - - result - } - -} - - -/// Pre buffer -#[derive(Clone)] -struct PreBuffer { - sender: Arc>>, - receiver: Arc>>, - is_being_read: Arc, - read_done_notify: Arc, -} - -impl PreBuffer { - fn new() -> Self { - let (sender, reader) = kanal::unbounded_async::>(); - Self { - sender: Arc::new(sender), - receiver: Arc::new(reader), - is_being_read: Arc::new(AtomicBool::new(false)), - read_done_notify: Arc::new(Notify::new()), - } - } - - async fn push(&self, frame: Vec) { - if self.is_being_read.load(atomic::Ordering::Acquire) { - self.read_done_notify.notified().await; - } - - let _ = self.sender.send(frame); - } - - async fn read_all(&self) -> Vec> { - self.is_being_read.store(true, atomic::Ordering::Release); - - let mut frames = Vec::new(); - while let Ok(frame) = self.receiver.recv().await { - frames.push(frame); - } - - // Libérer et notifier les writers en attente - self.is_being_read.store(false, atomic::Ordering::Release); - self.read_done_notify.notify_waiters(); - - frames - } -} -// struct PreBuffer { -// // Vec dynamique pour stocker les trames -// frames: Vec>, -// // Compteur atomique pour le nombre de trames disponibles -// frame_count: AtomicUsize, -// // Flag atomique pour indiquer si le buffer est en cours de lecture -// is_being_read: AtomicBool, -// read_done_notify: Arc, -// } -// -// impl PreBuffer { -// fn new() -> Self { -// Self { -// frames: Vec::new(), -// frame_count: AtomicUsize::new(0), -// is_being_read: AtomicBool::new(false), -// read_done_notify: Arc::new(Notify::new()), -// } -// } -// -// async fn push(&mut self, frame: Vec) { -// if self.is_being_read.load(atomic::Ordering::Acquire) { -// self.read_done_notify.notified().await; -// } -// -// self.frames.push(frame); -// self.frame_count.fetch_add(1, atomic::Ordering::Release); -// } -// -// fn reset(&mut self) { -// self.frame_count.store(0, atomic::Ordering::Release); -// } -// -// fn len(&self) -> usize { -// self.frame_count.load(atomic::Ordering::Acquire) -// } -// -// fn read_all(&mut self) -> Vec> { -// self.is_being_read.store(true, atomic::Ordering::Release); -// -// let frames = std::mem::take(&mut self.frames); -// self.frame_count.store(0, atomic::Ordering::Release); -// -// // Libérer et notifier les writers en attente -// self.is_being_read.store(false, atomic::Ordering::Release); -// self.read_done_notify.notify_waiters(); -// -// frames -// } -// } -// diff --git a/src-tauri/src/core/opus.rs b/src-tauri/src/core/opus.rs index 6452a9c..0f53339 100644 --- a/src-tauri/src/core/opus.rs +++ b/src-tauri/src/core/opus.rs @@ -65,7 +65,8 @@ impl AudioOpusEncoder { Ok(Self{audio_opus, encoder}) } - pub fn encode(&mut self, frames: &[i16]) -> Result, String> { + // old version i16 en input, on garde au cas ou ... + pub fn encode_i16(&mut self, frames: &[i16]) -> Result, String> { let mut output = vec![0u8; 1276]; // 1276 octets (la vraie worst-case recommandée par Opus). let len = self.encoder.encode(frames, output.as_mut_slice()) .map_err(|e| format!("Erreur encodage: {:?}", e))?; @@ -73,6 +74,21 @@ impl AudioOpusEncoder { Ok(output) } + pub fn encode(&mut self, frames: &[f32]) -> Result, String> { + // Conversion f32 -> i16 seulement ici + let frames_i16: Vec = frames.iter() + .map(|&sample| (sample * i16::MAX as f32) + .clamp(i16::MIN as f32, i16::MAX as f32) as i16) + .collect(); + + let mut output = vec![0u8; 1276]; + let len = self.encoder.encode(&frames_i16, output.as_mut_slice()) + .map_err(|e| format!("Erreur encodage: {:?}", e))?; + output.truncate(len); + Ok(output) + } + + // 🔄 Approche avec buffer réutilisable (encore plus optimal) fn encode_reuse(&mut self, frames: &[i16], output: &mut Vec) -> Result { output.clear(); @@ -101,10 +117,24 @@ impl AudioOpusDecoder { Ok(Self{audio_opus, decoder}) } - pub fn decode(&mut self, frames: &[u8]) -> Result, String> { + pub fn decode_i16(&mut self, frames: &[u8]) -> Result, String> { let mut output = vec![0i16; 5760]; let len = self.decoder.decode(frames, output.as_mut_slice(), false).map_err(|e| format!("Erreur décodage: {:?}", e))?; output.truncate(len); Ok(output) } + + pub fn decode(&mut self, frames: &[u8]) -> Result, String> { + let mut output_i16 = vec![0i16; 5760]; + let len = self.decoder.decode(frames, &mut output_i16, false) + .map_err(|e| format!("Erreur décodage: {:?}", e))?; + + // Conversion i16 -> f32 + let output_f32: Vec = output_i16[..len].iter() + .map(|&sample| sample as f32 / i16::MAX as f32) + .collect(); + + Ok(output_f32) + } + } \ No newline at end of file diff --git a/src-tauri/src/core/playback.rs b/src-tauri/src/core/playback.rs index 1ef3f7b..0d58db9 100644 --- a/src-tauri/src/core/playback.rs +++ b/src-tauri/src/core/playback.rs @@ -43,8 +43,8 @@ impl Speaker { let config = self.get_output_config(); let mut stream_config: StreamConfig = config.into(); stream_config.channels = 2; - stream_config.sample_rate = SampleRate(48000); - stream_config.buffer_size = BufferSize::Fixed(960); + stream_config.sample_rate = SampleRate(44100); + // stream_config.buffer_size = BufferSize::Fixed(960); stream_config } diff --git a/src-tauri/src/domain/event.rs b/src-tauri/src/domain/event.rs index 74f1fee..712561d 100644 --- a/src-tauri/src/domain/event.rs +++ b/src-tauri/src/domain/event.rs @@ -6,7 +6,7 @@ pub enum Event { AppStarted, AppStopped, - AudioIn(Vec), + AudioIn(Vec), AudioEncoded(Vec), PlaybackTick(usize),