init
This commit is contained in:
@@ -5,3 +5,10 @@ This template should help get you started developing with Tauri + Vue 3 in Vite.
|
|||||||
## Recommended IDE Setup
|
## 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)
|
- [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.
|
||||||
79
src-tauri/Cargo.lock
generated
79
src-tauri/Cargo.lock
generated
@@ -2280,6 +2280,15 @@ dependencies = [
|
|||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2297,6 +2306,15 @@ dependencies = [
|
|||||||
"syn 2.0.104",
|
"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]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -2651,6 +2669,7 @@ dependencies = [
|
|||||||
"moka",
|
"moka",
|
||||||
"opus",
|
"opus",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"rubato",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum",
|
"strum",
|
||||||
@@ -2967,6 +2986,15 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
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]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@@ -3145,6 +3173,15 @@ version = "0.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "realfft"
|
||||||
|
version = "3.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677"
|
||||||
|
dependencies = [
|
||||||
|
"rustfft",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.13"
|
version = "0.5.13"
|
||||||
@@ -3264,6 +3301,18 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.25"
|
version = "0.1.25"
|
||||||
@@ -3279,6 +3328,20 @@ dependencies = [
|
|||||||
"semver",
|
"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]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -3688,6 +3751,12 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strength_reduce"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "string_cache"
|
name = "string_cache"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -4446,6 +4515,16 @@ dependencies = [
|
|||||||
"tracing-log",
|
"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]]
|
[[package]]
|
||||||
name = "tray-icon"
|
name = "tray-icon"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
|
|||||||
@@ -35,3 +35,4 @@ moka = {version = "0.12", features = ["future"] }
|
|||||||
arc-swap = "1.7"
|
arc-swap = "1.7"
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
kanal = "0.1"
|
kanal = "0.1"
|
||||||
|
rubato = "0.16.2"
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ impl OxSpeakApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Démarrer la connexion réseau
|
// Démarrer la connexion réseau
|
||||||
println!("Connecting to UDP server at 127.0.0.1:5000");
|
println!("Connecting to UDP server at 82.64.205.121:5000");
|
||||||
self.udp_session.connect("127.0.0.1:5000").await;
|
self.udp_session.connect("82.64.205.121:5000").await;
|
||||||
|
|
||||||
// Démarrer l'audio-capture
|
// Démarrer l'audio-capture
|
||||||
println!("Starting audio capture");
|
println!("Starting audio capture");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::thread;
|
|||||||
use std::thread::JoinHandle;
|
use std::thread::JoinHandle;
|
||||||
use cpal::{default_host, BufferSize, Device, SampleRate, Stream, StreamConfig, SupportedStreamConfig};
|
use cpal::{default_host, BufferSize, Device, SampleRate, Stream, StreamConfig, SupportedStreamConfig};
|
||||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
|
use rubato::{FftFixedInOut, Resampler};
|
||||||
use crate::core::opus::AudioOpus;
|
use crate::core::opus::AudioOpus;
|
||||||
use crate::domain::event::{Event, EventBus};
|
use crate::domain::event::{Event, EventBus};
|
||||||
use crate::utils::ringbuf::RingBuffer;
|
use crate::utils::ringbuf::RingBuffer;
|
||||||
@@ -17,7 +18,7 @@ pub struct AudioCapture {
|
|||||||
event_bus: EventBus,
|
event_bus: EventBus,
|
||||||
microphone: Microphone,
|
microphone: Microphone,
|
||||||
running: Arc<AtomicBool>,
|
running: Arc<AtomicBool>,
|
||||||
ring_buffer: RingBuffer<i16>,
|
ring_buffer: RingBuffer<f32>,
|
||||||
steam: Option<Stream>,
|
steam: Option<Stream>,
|
||||||
worker: Option<JoinHandle<()>>,
|
worker: Option<JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
@@ -42,17 +43,25 @@ impl Microphone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_stream_config(&self) -> StreamConfig {
|
pub fn get_stream_config(&self) -> StreamConfig {
|
||||||
let config = self.get_input_config();
|
let supported_channels = self.get_supported_channels();
|
||||||
let mut stream_config: StreamConfig = config.into();
|
|
||||||
stream_config.channels = 1;
|
// Priorité : mono si supporté, sinon prend le premier disponible
|
||||||
stream_config.sample_rate = SampleRate(48000);
|
let channels = if supported_channels.contains(&1) {
|
||||||
stream_config.buffer_size = BufferSize::Fixed(960);
|
1 // ✅ Mono préféré
|
||||||
stream_config
|
} else {
|
||||||
|
supported_channels.first().copied().unwrap_or(2) // Fallback
|
||||||
|
};
|
||||||
|
|
||||||
|
StreamConfig {
|
||||||
|
channels,
|
||||||
|
sample_rate: SampleRate(48_000),
|
||||||
|
buffer_size: BufferSize::Default,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_stream<F>(&self, callback: F) -> Stream
|
pub fn build_stream<F>(&self, callback: F) -> Stream
|
||||||
where
|
where
|
||||||
F: FnMut(&[i16], &cpal::InputCallbackInfo) + Send + 'static,
|
F: FnMut(&[f32], &cpal::InputCallbackInfo) + Send + 'static,
|
||||||
{
|
{
|
||||||
let config = self.get_stream_config();
|
let config = self.get_stream_config();
|
||||||
|
|
||||||
@@ -63,6 +72,13 @@ impl Microphone {
|
|||||||
None
|
None
|
||||||
).unwrap()
|
).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
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 {
|
impl AudioCapture {
|
||||||
@@ -131,33 +147,53 @@ impl AudioCapture {
|
|||||||
let worker_running = self.running.clone();
|
let worker_running = self.running.clone();
|
||||||
let event_bus = self.event_bus.clone();
|
let event_bus = self.event_bus.clone();
|
||||||
let input_config = self.microphone.get_input_config();
|
let input_config = self.microphone.get_input_config();
|
||||||
println!("Audio input config: sample rate: {}, channels: {}", input_config.sample_rate().0, input_config.channels());
|
println!("Audio input config: sample rate: {}, channels: {}",
|
||||||
let opus = AudioOpus::new(input_config.sample_rate().0, input_config.channels(), "voip");
|
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();
|
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();
|
let reader = self.ring_buffer.reader();
|
||||||
|
|
||||||
|
// Démarrage du worker
|
||||||
println!("Spawning audio processing thread");
|
println!("Spawning audio processing thread");
|
||||||
|
let stream_config = self.microphone.get_stream_config(); // ✅ Clone la config
|
||||||
self.worker = Some(thread::spawn(move || {
|
self.worker = Some(thread::spawn(move || {
|
||||||
println!("Audio processing thread started");
|
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) {
|
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){
|
if !worker_running.load(Ordering::Relaxed){
|
||||||
println!("Audio processing thread stopping");
|
println!("Audio processing thread stopping");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame_count += 1;
|
// Resampling : 441→480, 48000→480, 96000→480, etc.
|
||||||
if frame_count % 100 == 0 {
|
let processed_audio = Self::process_audio_frame(
|
||||||
println!("Processed {} audio frames", frame_count);
|
&stream_config, // ✅ Utilise stream_config
|
||||||
}
|
resampler.as_mut(),
|
||||||
|
&raw_buffer
|
||||||
|
);
|
||||||
|
// processed_audio est TOUJOURS 480 f32 (mono/48kHz)
|
||||||
|
|
||||||
let raw_data = frame.to_vec();
|
// Events
|
||||||
event_bus.emit_sync(Event::AudioIn(raw_data));
|
event_bus.emit_sync(Event::AudioIn(processed_audio.clone()));
|
||||||
|
|
||||||
match encoder.encode(&frame){
|
match encoder.encode(&processed_audio){
|
||||||
Ok(encoded_data) => {
|
Ok(encoded_data) => {
|
||||||
event_bus.emit_sync(Event::AudioEncoded(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);
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Standardise un buffer en mono float32 échantillonné à 48kHz.
|
||||||
|
fn process_audio_frame(
|
||||||
|
config: &StreamConfig,
|
||||||
|
resampler: Option<&mut FftFixedInOut<f32>>,
|
||||||
|
samples: &[f32],
|
||||||
|
) -> Vec<f32> {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioCapture {
|
fn sample_to_mono(input_channels: usize, samples: &[f32]) -> Vec<f32> {
|
||||||
fn audio_processing(){
|
if input_channels == 1 {
|
||||||
|
samples.to_vec()
|
||||||
|
}else{
|
||||||
|
samples
|
||||||
|
.chunks_exact(input_channels)
|
||||||
|
.map(|frame| frame.iter().copied().sum::<f32>() / input_channels as f32)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Méthode corrigée avec le bon nom
|
||||||
|
fn create_resampler(source_rate: usize) -> FftFixedInOut<f32> {
|
||||||
|
println!("Creating resampler: {}Hz -> 48kHz", source_rate);
|
||||||
|
FftFixedInOut::<f32>::new(
|
||||||
|
source_rate,
|
||||||
|
48_000,
|
||||||
|
512, // chunk size
|
||||||
|
1, // mono après conversion
|
||||||
|
).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 std::sync::Arc;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use crate::domain::audio_client::AudioClientManager;
|
use crate::domain::audio_client::AudioClientManager;
|
||||||
|
use crate::utils::ringbuf::{RingBufReader, RingBufWriter, RingBuffer};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AudioMixer {
|
pub struct AudioMixer {
|
||||||
audio_client_manager: AudioClientManager,
|
audio_client_manager: AudioClientManager,
|
||||||
buffer: Arc<ArcSwap<Vec<i16>>>
|
buffer_writer: Arc<RingBufWriter<i16>>,
|
||||||
|
buffer_reader: Arc<RingBufReader<i16>>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioMixer {
|
impl AudioMixer {
|
||||||
pub fn new(audio_client_manager: AudioClientManager) -> Self {
|
pub fn new(audio_client_manager: AudioClientManager) -> Self {
|
||||||
|
let (buffer_writer, buffer_reader) = RingBuffer::new(2048).split();
|
||||||
Self {
|
Self {
|
||||||
audio_client_manager,
|
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) {
|
pub fn mix_next_frame(&self, size: usize) {
|
||||||
@@ -34,14 +38,18 @@ impl AudioMixer {
|
|||||||
Self::mix_frames(&frames, size)
|
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<i16> {
|
pub fn read(&self, size: usize) -> Vec<i16> {
|
||||||
let mut data = (**self.buffer.load()).clone();
|
let mut data = vec![0i16; size];
|
||||||
data.resize(size, 0);
|
// 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
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioMixer {
|
impl AudioMixer {
|
||||||
|
|||||||
78
src-tauri/src/core/mixer_arcswap.rs
Normal file
78
src-tauri/src/core/mixer_arcswap.rs
Normal 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()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<RingBufWriter<i16>>,
|
|
||||||
// reader: Arc<RingBufReader<i16>>,
|
|
||||||
pre_buffer: Arc<PreBuffer>,
|
|
||||||
worker_sender: Option<mpsc::UnboundedSender<Vec<i16>>>,
|
|
||||||
buffer: Arc<ArcSwap<[i16; 1920]>>, // 1920 frames
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mixer {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let (writer, reader) = RingBuffer::<i16>::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::<Vec<i16>>();
|
|
||||||
|
|
||||||
// let (writer, reader) = RingBuffer::<Bytes>::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<i16>){
|
|
||||||
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<i16> (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<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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mixer plusieurs trames
|
|
||||||
fn mix_frames(frames: &[Vec<i16>]) -> [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<kanal::AsyncSender<Vec<i16>>>,
|
|
||||||
receiver: Arc<kanal::AsyncReceiver<Vec<i16>>>,
|
|
||||||
is_being_read: Arc<AtomicBool>,
|
|
||||||
read_done_notify: Arc<Notify>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PreBuffer {
|
|
||||||
fn new() -> Self {
|
|
||||||
let (sender, reader) = kanal::unbounded_async::<Vec<i16>>();
|
|
||||||
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<i16>) {
|
|
||||||
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<Vec<i16>> {
|
|
||||||
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<Vec<i16>>,
|
|
||||||
// // 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<Notify>,
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// 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<i16>) {
|
|
||||||
// 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<Vec<i16>> {
|
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
@@ -65,7 +65,8 @@ impl AudioOpusEncoder {
|
|||||||
Ok(Self{audio_opus, encoder})
|
Ok(Self{audio_opus, encoder})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn encode(&mut self, frames: &[i16]) -> Result<Vec<u8>, String> {
|
// old version i16 en input, on garde au cas ou ...
|
||||||
|
pub fn encode_i16(&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 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())
|
let len = self.encoder.encode(frames, output.as_mut_slice())
|
||||||
.map_err(|e| format!("Erreur encodage: {:?}", e))?;
|
.map_err(|e| format!("Erreur encodage: {:?}", e))?;
|
||||||
@@ -73,6 +74,21 @@ impl AudioOpusEncoder {
|
|||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn encode(&mut self, frames: &[f32]) -> Result<Vec<u8>, String> {
|
||||||
|
// Conversion f32 -> i16 seulement ici
|
||||||
|
let frames_i16: Vec<i16> = 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)
|
// 🔄 Approche avec buffer réutilisable (encore plus optimal)
|
||||||
fn encode_reuse(&mut self, frames: &[i16], output: &mut Vec<u8>) -> Result<usize, String> {
|
fn encode_reuse(&mut self, frames: &[i16], output: &mut Vec<u8>) -> Result<usize, String> {
|
||||||
output.clear();
|
output.clear();
|
||||||
@@ -101,10 +117,24 @@ impl AudioOpusDecoder {
|
|||||||
Ok(Self{audio_opus, decoder})
|
Ok(Self{audio_opus, decoder})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decode(&mut self, frames: &[u8]) -> Result<Vec<i16>, String> {
|
pub fn decode_i16(&mut self, frames: &[u8]) -> Result<Vec<i16>, String> {
|
||||||
let mut output = vec![0i16; 5760];
|
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))?;
|
let len = self.decoder.decode(frames, output.as_mut_slice(), false).map_err(|e| format!("Erreur décodage: {:?}", e))?;
|
||||||
output.truncate(len);
|
output.truncate(len);
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn decode(&mut self, frames: &[u8]) -> Result<Vec<f32>, 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<f32> = output_i16[..len].iter()
|
||||||
|
.map(|&sample| sample as f32 / i16::MAX as f32)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(output_f32)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -43,8 +43,8 @@ impl Speaker {
|
|||||||
let config = self.get_output_config();
|
let config = self.get_output_config();
|
||||||
let mut stream_config: StreamConfig = config.into();
|
let mut stream_config: StreamConfig = config.into();
|
||||||
stream_config.channels = 2;
|
stream_config.channels = 2;
|
||||||
stream_config.sample_rate = SampleRate(48000);
|
stream_config.sample_rate = SampleRate(44100);
|
||||||
stream_config.buffer_size = BufferSize::Fixed(960);
|
// stream_config.buffer_size = BufferSize::Fixed(960);
|
||||||
stream_config
|
stream_config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ pub enum Event {
|
|||||||
AppStarted,
|
AppStarted,
|
||||||
AppStopped,
|
AppStopped,
|
||||||
|
|
||||||
AudioIn(Vec<i16>),
|
AudioIn(Vec<f32>),
|
||||||
AudioEncoded(Vec<u8>),
|
AudioEncoded(Vec<u8>),
|
||||||
|
|
||||||
PlaybackTick(usize),
|
PlaybackTick(usize),
|
||||||
|
|||||||
Reference in New Issue
Block a user