init
This commit is contained in:
178
src-tauri/src/core/capture.rs
Normal file
178
src-tauri/src/core/capture.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use std::thread::JoinHandle;
|
||||
use cpal::{default_host, BufferSize, Device, SampleRate, Stream, StreamConfig, SupportedStreamConfig};
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use crate::core::opus::AudioOpus;
|
||||
use crate::domain::event::{Event, EventBus};
|
||||
use crate::utils::ringbuf::RingBuffer;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Microphone {
|
||||
device: Device,
|
||||
}
|
||||
|
||||
pub struct AudioCapture {
|
||||
event_bus: EventBus,
|
||||
microphone: Microphone,
|
||||
running: Arc<AtomicBool>,
|
||||
ring_buffer: RingBuffer<i16>,
|
||||
steam: Option<Stream>,
|
||||
worker: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Microphone {
|
||||
pub fn new(device: Device) -> Self {
|
||||
println!("Initializing microphone with device: {}", device.name().unwrap_or_else(|_| "Unknown".to_string()));
|
||||
Self {
|
||||
device
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default() -> Self {
|
||||
println!("Creating default microphone");
|
||||
let host = default_host();
|
||||
let device = host.default_input_device().unwrap();
|
||||
Self::new(device)
|
||||
}
|
||||
|
||||
pub fn get_input_config(&self) -> SupportedStreamConfig {
|
||||
self.device.default_input_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 = 1;
|
||||
stream_config.sample_rate = SampleRate(48000);
|
||||
stream_config.buffer_size = BufferSize::Fixed(960);
|
||||
stream_config
|
||||
}
|
||||
|
||||
pub fn build_stream<F>(&self, callback: F) -> Stream
|
||||
where
|
||||
F: FnMut(&[i16], &cpal::InputCallbackInfo) + Send + 'static,
|
||||
{
|
||||
let config = self.get_stream_config();
|
||||
|
||||
self.device.build_input_stream(
|
||||
&config,
|
||||
callback,
|
||||
|err| println!("Error input stream: {err}"),
|
||||
None
|
||||
).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioCapture {
|
||||
pub fn new(event_bus: EventBus, microphone: Microphone) -> Self {
|
||||
println!("Creating new AudioCapture instance");
|
||||
Self {
|
||||
event_bus,
|
||||
microphone,
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
ring_buffer: RingBuffer::new(4096),
|
||||
steam: None,
|
||||
worker: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default(event_bus: EventBus) -> Self {
|
||||
println!("Creating default AudioCapture");
|
||||
Self::new(event_bus, Microphone::default())
|
||||
}
|
||||
|
||||
pub async fn start(&mut self) {
|
||||
println!("Starting audio capture");
|
||||
self.running.store(true, Ordering::Relaxed);
|
||||
|
||||
// stream cpal
|
||||
println!("Setting up audio stream");
|
||||
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;
|
||||
}
|
||||
writer.push_slice_overwrite(data);
|
||||
});
|
||||
stream.play().unwrap();
|
||||
self.steam = Some(stream);
|
||||
println!("Audio stream started");
|
||||
|
||||
// Audio processing worker
|
||||
println!("Starting audio processing worker");
|
||||
self.run_processing_worker();
|
||||
println!("Audio capture fully initialized");
|
||||
}
|
||||
|
||||
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){
|
||||
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 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;
|
||||
|
||||
while worker_running.load(Ordering::Relaxed) {
|
||||
let _ = reader.pop_slice_blocking(&mut frame);
|
||||
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 raw_data = frame.to_vec();
|
||||
event_bus.emit_sync(Event::AudioIn(raw_data));
|
||||
|
||||
match encoder.encode(&frame){
|
||||
Ok(encoded_data) => {
|
||||
event_bus.emit_sync(Event::AudioEncoded(encoded_data))
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Error encoding: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("Audio processing thread finished after processing {} frames", frame_count);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioCapture {
|
||||
fn audio_processing(){
|
||||
|
||||
}
|
||||
}
|
||||
1
src-tauri/src/core/mixer.rs
Normal file
1
src-tauri/src/core/mixer.rs
Normal file
@@ -0,0 +1 @@
|
||||
// aller pick l'audio des clients
|
||||
6
src-tauri/src/core/mod.rs
Normal file
6
src-tauri/src/core/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod capture;
|
||||
pub mod mixer;
|
||||
pub mod opus;
|
||||
pub mod playback;
|
||||
pub mod rms;
|
||||
pub mod stats;
|
||||
110
src-tauri/src/core/opus.rs
Normal file
110
src-tauri/src/core/opus.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use opus::{Application, Channels, Decoder, Encoder};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AudioOpus{
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
application: Application
|
||||
}
|
||||
|
||||
impl AudioOpus {
|
||||
pub fn new(sample_rate: u32, channels: u16, application: &str) -> Self {
|
||||
let application = match application {
|
||||
"voip" => Application::Voip,
|
||||
"audio" => Application::Audio,
|
||||
"lowdelay" => Application::LowDelay,
|
||||
_ => Application::Voip,
|
||||
};
|
||||
Self{sample_rate, channels, application}
|
||||
}
|
||||
|
||||
pub fn create_encoder(&self) -> Result<AudioOpusEncoder, String> {
|
||||
AudioOpusEncoder::new(self.clone())
|
||||
}
|
||||
|
||||
pub fn create_decoder(&self) -> Result<AudioOpusDecoder, String> {
|
||||
AudioOpusDecoder::new(self.clone())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub struct AudioOpusEncoder{
|
||||
audio_opus: AudioOpus,
|
||||
encoder: opus::Encoder,
|
||||
}
|
||||
|
||||
impl AudioOpusEncoder {
|
||||
fn new(audio_opus: AudioOpus) -> Result<Self, String> {
|
||||
let opus_channel = match audio_opus.channels {
|
||||
1 => Channels::Mono,
|
||||
2 => Channels::Stereo,
|
||||
_ => Channels::Mono,
|
||||
};
|
||||
let mut encoder = Encoder::new(audio_opus.sample_rate, opus_channel, audio_opus.application)
|
||||
.map_err(|e| format!("Échec de création de l'encodeur: {:?}", e))?;
|
||||
|
||||
match audio_opus.application {
|
||||
Application::Voip => {
|
||||
// Paramètres optimaux pour VoIP: bonne qualité vocale, CPU modéré
|
||||
let _ = encoder.set_bitrate(opus::Bitrate::Bits(24000)); // 24kbps est bon pour la voix
|
||||
let _ = encoder.set_vbr(true); // Variable bitrate économise du CPU
|
||||
let _ = encoder.set_vbr_constraint(false); // Sans contrainte stricte de débit
|
||||
// Pas de set_complexity (non supporté par la crate)
|
||||
},
|
||||
Application::Audio => {
|
||||
// Musique: priorité à la qualité
|
||||
let _ = encoder.set_bitrate(opus::Bitrate::Bits(64000));
|
||||
let _ = encoder.set_vbr(true);
|
||||
},
|
||||
Application::LowDelay => {
|
||||
// Priorité à la latence et l'efficacité CPU
|
||||
let _ = encoder.set_bitrate(opus::Bitrate::Bits(18000));
|
||||
let _ = encoder.set_vbr(true);
|
||||
},
|
||||
}
|
||||
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())
|
||||
.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> {
|
||||
output.clear();
|
||||
output.resize(1276, 0);
|
||||
let len = self.encoder.encode(frames, output.as_mut_slice()).unwrap();
|
||||
output.truncate(len);
|
||||
Ok(len)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AudioOpusDecoder{
|
||||
audio_opus: AudioOpus,
|
||||
decoder: opus::Decoder,
|
||||
}
|
||||
|
||||
impl AudioOpusDecoder {
|
||||
fn new(audio_opus: AudioOpus) -> Result<Self, String> {
|
||||
let opus_channel = match audio_opus.channels {
|
||||
1 => Channels::Mono,
|
||||
2 => Channels::Stereo,
|
||||
_ => Channels::Mono,
|
||||
};
|
||||
|
||||
let decoder = Decoder::new(audio_opus.sample_rate, opus_channel)
|
||||
.map_err(|e| format!("Échec de création du décodeur: {:?}", e))?;
|
||||
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);
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
100
src-tauri/src/core/playback.rs
Normal file
100
src-tauri/src/core/playback.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread::JoinHandle;
|
||||
use cpal::{default_host, BufferSize, Device, SampleRate, Stream, StreamConfig, SupportedStreamConfig};
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use crate::domain::event::EventBus;
|
||||
use crate::utils::real_time_event::RealTimeEvent;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Speaker {
|
||||
device: Device
|
||||
}
|
||||
|
||||
pub struct AudioPlayback {
|
||||
event_bus: EventBus,
|
||||
speaker: Speaker,
|
||||
running: Arc<AtomicBool>,
|
||||
stream: Option<Stream>,
|
||||
worker: Option<JoinHandle<()>>,
|
||||
next_tick: RealTimeEvent
|
||||
}
|
||||
|
||||
impl Speaker {
|
||||
pub fn new(device: Device) -> Self {
|
||||
Speaker {
|
||||
device
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default() -> Self {
|
||||
let host = default_host();
|
||||
let device = host.default_output_device().unwrap();
|
||||
Speaker::new(device)
|
||||
}
|
||||
|
||||
pub fn get_input_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
|
||||
}
|
||||
|
||||
pub fn build_stream<F>(&self, callback: F) -> Stream
|
||||
where
|
||||
F: FnMut(&mut [i16], &cpal::OutputCallbackInfo) + Send + 'static,
|
||||
{
|
||||
let config = self.get_stream_config();
|
||||
|
||||
self.device.build_output_stream(
|
||||
&config,
|
||||
callback,
|
||||
|err| println!("Error output stream: {err}"),
|
||||
None
|
||||
).unwrap()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioPlayback {
|
||||
pub fn new(event_bus: EventBus, speaker: Speaker) -> Self {
|
||||
Self {
|
||||
event_bus,
|
||||
speaker,
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
stream: None,
|
||||
worker: None,
|
||||
next_tick: RealTimeEvent::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default(event_bus: EventBus) -> Self {
|
||||
let speaker = Speaker::default();
|
||||
AudioPlayback::new(event_bus, speaker)
|
||||
}
|
||||
|
||||
pub async fn start(&mut self) {
|
||||
|
||||
}
|
||||
|
||||
pub async fn stop(&mut self) {
|
||||
self.running.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
|
||||
// stream cpal
|
||||
println!("Setting up audio playback stream...");
|
||||
let stream_running = self.running.clone();
|
||||
let stream = self.speaker.build_stream(move |data, _| {
|
||||
if !stream_running.load(Ordering::Relaxed){
|
||||
return;
|
||||
}
|
||||
// aller récupérer 1920 sur un buffer
|
||||
// écrire le contenu dans data
|
||||
});
|
||||
}
|
||||
}
|
||||
0
src-tauri/src/core/rms.rs
Normal file
0
src-tauri/src/core/rms.rs
Normal file
0
src-tauri/src/core/stats.rs
Normal file
0
src-tauri/src/core/stats.rs
Normal file
Reference in New Issue
Block a user