amélioration de la webapp

This commit is contained in:
2025-10-19 13:42:29 +02:00
parent 756ae7f82c
commit 208fe8be76
179 changed files with 2426 additions and 1685 deletions

View File

@@ -49,7 +49,10 @@ async fn main() {
tokio::spawn(async move {
let mut source = SourceNode::new();
source.add_subscriber(buffer_tx);
source.generate_chunks(30, 4800, 48000, 440.0).await.unwrap();
source
.generate_chunks(30, 4800, 48000, 440.0)
.await
.unwrap();
});
println!("Waiting for all rooms to finish...\n");

View File

@@ -7,8 +7,7 @@
//! - Système d'événements pour la communication entre nodes
use pmoaudio::{
ChromecastConfig, ChromecastSink, DiskSink, DiskSinkConfig, SourceNode,
VolumeNode,
ChromecastConfig, ChromecastSink, DiskSink, DiskSinkConfig, SourceNode, VolumeNode,
};
use tokio::sync::mpsc;
@@ -146,8 +145,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("\n=== Demo completed successfully! ===");
println!("\nSummary:");
println!("- Generated {} chunks of {} samples each", num_chunks, chunk_size);
println!("- Total duration: {:.2} seconds", (num_chunks as usize * chunk_size) as f32 / sample_rate as f32);
println!(
"- Generated {} chunks of {} samples each",
num_chunks, chunk_size
);
println!(
"- Total duration: {:.2} seconds",
(num_chunks as usize * chunk_size) as f32 / sample_rate as f32
);
println!("- Output to Chromecast: Living Room (192.168.1.100)");
println!(
"- Output to file: {}",

View File

@@ -73,10 +73,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 7. Générer l'audio (10 chunks de 4800 samples à 48kHz = ~1 seconde)
source
.generate_chunks(
10, // nombre de chunks
4800, // samples par chunk (100ms @ 48kHz)
48000, // sample rate
440.0, // fréquence (La 440 Hz)
10, // nombre de chunks
4800, // samples par chunk (100ms @ 48kHz)
48000, // sample rate
440.0, // fréquence (La 440 Hz)
)
.await?;

View File

@@ -90,7 +90,13 @@ impl AudioChunk {
}
/// Crée un nouveau chunk audio avec un gain spécifique
pub fn with_gain(order: u64, left: Vec<f32>, right: Vec<f32>, sample_rate: u32, gain: f32) -> Self {
pub fn with_gain(
order: u64,
left: Vec<f32>,
right: Vec<f32>,
sample_rate: u32,
gain: f32,
) -> Self {
Self {
order,
left: Arc::new(left),

View File

@@ -76,13 +76,13 @@
//! - **RwLock** : Pour partage concurrent du compteur [`TimerNode`]
mod audio_chunk;
mod nodes;
pub mod events;
mod nodes;
pub use audio_chunk::AudioChunk;
pub use events::{
AudioDataEvent, EventPublisher, EventReceiver, NodeEvent, NodeListener,
SourceNameUpdateEvent, VolumeChangeEvent,
AudioDataEvent, EventPublisher, EventReceiver, NodeEvent, NodeListener, SourceNameUpdateEvent,
VolumeChangeEvent,
};
pub use nodes::{
buffer_node::BufferNode,

View File

@@ -1,4 +1,7 @@
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}};
use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
use std::collections::VecDeque;
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};

View File

@@ -189,10 +189,7 @@ impl ChromecastSink {
// Établir la connexion
self.connect().await?;
let mut stats = ChromecastStats::new(
self.node_id.clone(),
self.config.device_name.clone(),
);
let mut stats = ChromecastStats::new(self.node_id.clone(), self.config.device_name.clone());
// Boucle principale
while let Some(chunk) = self.rx.recv().await {

View File

@@ -1,4 +1,7 @@
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}};
use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
use std::sync::Arc;
use tokio::sync::mpsc;
@@ -56,10 +59,10 @@ impl DecoderNode {
if src_idx < left_data.len() - 1 {
let frac = src_pos - src_idx as f64;
let left_sample =
left_data[src_idx] * (1.0 - frac as f32) + left_data[src_idx + 1] * frac as f32;
let right_sample =
right_data[src_idx] * (1.0 - frac as f32) + right_data[src_idx + 1] * frac as f32;
let left_sample = left_data[src_idx] * (1.0 - frac as f32)
+ left_data[src_idx + 1] * frac as f32;
let right_sample = right_data[src_idx] * (1.0 - frac as f32)
+ right_data[src_idx + 1] * frac as f32;
new_left.push(left_sample);
new_right.push(right_sample);
@@ -69,7 +72,8 @@ impl DecoderNode {
}
}
let new_chunk = AudioChunk::new(chunk.order, new_left, new_right, target_sample_rate);
let new_chunk =
AudioChunk::new(chunk.order, new_left, new_right, target_sample_rate);
self.subscribers.push(Arc::new(new_chunk)).await?;
}
}

View File

@@ -3,11 +3,7 @@
//! Ce module fournit un sink qui écrit les chunks audio sur disque,
//! avec support de la dérivation automatique du nom de fichier depuis la source.
use crate::{
events::SourceNameUpdateEvent,
nodes::AudioError,
AudioChunk,
};
use crate::{events::SourceNameUpdateEvent, nodes::AudioError, AudioChunk};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs::File;
@@ -161,7 +157,13 @@ impl DiskSink {
// Nettoyer le nom de la source pour en faire un nom de fichier valide
let clean_name = name
.chars()
.map(|c| if c.is_alphanumeric() || c == '_' || c == '-' { c } else { '_' })
.map(|c| {
if c.is_alphanumeric() || c == '_' || c == '-' {
c
} else {
'_'
}
})
.collect::<String>();
format!("{}.{}", clean_name, self.config.format.extension())
@@ -180,9 +182,9 @@ impl DiskSink {
// Créer le répertoire parent si nécessaire
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| AudioError::ProcessingError(format!("Failed to create directory: {}", e)))?;
tokio::fs::create_dir_all(parent).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to create directory: {}", e))
})?;
}
// Créer le writer approprié selon le format
@@ -328,10 +330,9 @@ impl AudioFileWriter {
bytes.extend_from_slice(&sample_i16.to_le_bytes());
}
self.file
.write_all(&bytes)
.await
.map_err(|e| AudioError::ProcessingError(format!("Failed to write audio data: {}", e)))?;
self.file.write_all(&bytes).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to write audio data: {}", e))
})?;
self.total_samples += chunk.len();
Ok(())
@@ -361,10 +362,9 @@ impl AudioFileWriter {
header.extend_from_slice(b"data");
header.extend_from_slice(&0u32.to_le_bytes()); // Taille des données (à mettre à jour)
self.file
.write_all(&header)
.await
.map_err(|e| AudioError::ProcessingError(format!("Failed to write WAV header: {}", e)))?;
self.file.write_all(&header).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to write WAV header: {}", e))
})?;
Ok(())
}
@@ -378,24 +378,37 @@ impl AudioFileWriter {
// Positionner au début et réécrire les tailles
use tokio::io::AsyncSeekExt;
self.file.seek(std::io::SeekFrom::Start(4)).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to seek in file: {}", e))
})?;
self.file.write_all(&file_size.to_le_bytes()).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to update file size: {}", e))
})?;
self.file
.seek(std::io::SeekFrom::Start(4))
.await
.map_err(|e| {
AudioError::ProcessingError(format!("Failed to seek in file: {}", e))
})?;
self.file
.write_all(&file_size.to_le_bytes())
.await
.map_err(|e| {
AudioError::ProcessingError(format!("Failed to update file size: {}", e))
})?;
self.file.seek(std::io::SeekFrom::Start(40)).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to seek in file: {}", e))
})?;
self.file.write_all(&data_size.to_le_bytes()).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to update data size: {}", e))
})?;
self.file
.seek(std::io::SeekFrom::Start(40))
.await
.map_err(|e| {
AudioError::ProcessingError(format!("Failed to seek in file: {}", e))
})?;
self.file
.write_all(&data_size.to_le_bytes())
.await
.map_err(|e| {
AudioError::ProcessingError(format!("Failed to update data size: {}", e))
})?;
}
self.file.flush().await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to flush file: {}", e))
})?;
self.file
.flush()
.await
.map_err(|e| AudioError::ProcessingError(format!("Failed to flush file: {}", e)))?;
Ok(())
}

View File

@@ -1,4 +1,7 @@
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}};
use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
use std::sync::Arc;
use tokio::sync::mpsc;
@@ -46,12 +49,8 @@ impl DspNode {
*sample *= self.gain;
}
let new_chunk = AudioChunk::new(
chunk.order,
left_data,
right_data,
chunk.sample_rate,
);
let new_chunk =
AudioChunk::new(chunk.order, left_data, right_data, chunk.sample_rate);
self.subscribers.push(Arc::new(new_chunk)).await?;
}
@@ -118,12 +117,7 @@ impl LowPassDspNode {
new_right.push(self.prev_right);
}
let new_chunk = AudioChunk::new(
chunk.order,
new_left,
new_right,
chunk.sample_rate,
);
let new_chunk = AudioChunk::new(chunk.order, new_left, new_right, chunk.sample_rate);
self.subscribers.push(Arc::new(new_chunk)).await?;
}

View File

@@ -59,10 +59,7 @@ impl SingleSubscriberNode {
}
pub async fn push(&self, chunk: Arc<AudioChunk>) -> Result<(), AudioError> {
self.tx
.send(chunk)
.await
.map_err(|_| AudioError::SendError)
self.tx.send(chunk).await.map_err(|_| AudioError::SendError)
}
}

View File

@@ -197,7 +197,9 @@ impl MpdSink {
/// Envoie un chunk au serveur MPD (mock)
async fn send_chunk(&self, _chunk: &AudioChunk) -> Result<(), AudioError> {
if !self.connected {
return Err(AudioError::ProcessingError("Not connected to MPD".to_string()));
return Err(AudioError::ProcessingError(
"Not connected to MPD".to_string(),
));
}
// Dans une vraie implémentation:

View File

@@ -1,4 +1,4 @@
use crate::{AudioChunk, nodes::AudioError};
use crate::{nodes::AudioError, AudioChunk};
use std::sync::Arc;
use tokio::sync::mpsc;
@@ -115,11 +115,13 @@ impl SinkStats {
let sum_squares_left: f64 = chunk.left.iter().map(|&x| (x * x) as f64).sum();
let sum_squares_right: f64 = chunk.right.iter().map(|&x| (x * x) as f64).sum();
self.rms_left = ((self.rms_left.powi(2) * (self.total_samples - chunk.len() as u64) as f64
self.rms_left = ((self.rms_left.powi(2)
* (self.total_samples - chunk.len() as u64) as f64
+ sum_squares_left)
/ self.total_samples as f64)
.sqrt();
self.rms_right = ((self.rms_right.powi(2) * (self.total_samples - chunk.len() as u64) as f64
self.rms_right = ((self.rms_right.powi(2)
* (self.total_samples - chunk.len() as u64) as f64
+ sum_squares_right)
/ self.total_samples as f64)
.sqrt();

View File

@@ -1,4 +1,7 @@
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}};
use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
use std::sync::Arc;
use tokio::sync::mpsc;
@@ -63,12 +66,8 @@ impl SourceNode {
sample_rate: u32,
) -> Result<(), AudioError> {
for i in 0..count {
let chunk = AudioChunk::new(
i,
vec![0.0; chunk_size],
vec![0.0; chunk_size],
sample_rate,
);
let chunk =
AudioChunk::new(i, vec![0.0; chunk_size], vec![0.0; chunk_size], sample_rate);
self.subscribers.push(Arc::new(chunk)).await?;
}
Ok(())

View File

@@ -1,4 +1,7 @@
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}};
use crate::{
nodes::{AudioError, MultiSubscriberNode},
AudioChunk,
};
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};

View File

@@ -32,7 +32,10 @@ async fn test_complete_pipeline() {
tokio::spawn(async move {
let mut source = SourceNode::new();
source.add_subscriber(decoder_tx);
source.generate_chunks(10, 4800, 48000, 440.0).await.unwrap();
source
.generate_chunks(10, 4800, 48000, 440.0)
.await
.unwrap();
});
// Attendre la fin
@@ -73,7 +76,10 @@ async fn test_multiroom_buffering() {
tokio::spawn(async move {
let mut source = SourceNode::new();
source.add_subscriber(buffer_tx);
source.generate_chunks(20, 1000, 48000, 440.0).await.unwrap();
source
.generate_chunks(20, 1000, 48000, 440.0)
.await
.unwrap();
});
let stats1 = sink1_handle.await.unwrap();
@@ -108,7 +114,10 @@ async fn test_timer_accuracy() {
source.add_subscriber(timer_tx);
// 48000 samples à 48kHz = 1 seconde
source.generate_chunks(1, 48000, 48000, 440.0).await.unwrap();
source
.generate_chunks(1, 48000, 48000, 440.0)
.await
.unwrap();
});
sink_handle.await.unwrap();