Ajout de la gestion des couvertures d'albums par le FlacCacheSink

This commit is contained in:
2025-11-03 14:52:04 +01:00
parent 40950164e9
commit 83d7520840
17 changed files with 93 additions and 35 deletions

View File

@@ -0,0 +1,80 @@
//! Exemple de configuration multiroom avec BufferNode
//!
//! Démontre l'utilisation du buffer circulaire pour synchroniser
//! plusieurs sorties avec des délais différents
use pmoaudio::{BufferNode, SinkNode, SourceNode};
#[tokio::main]
async fn main() {
println!("=== Multiroom Demo ===\n");
// Buffer avec capacité pour gérer les délais
let (buffer, buffer_tx) = BufferNode::new(50, 10);
// Créer 3 sorties avec délais différents
let (sink1, sink1_tx) = SinkNode::new("Room 1 (no delay)".to_string(), 10);
let (sink2, sink2_tx) = SinkNode::new("Room 2 (5 chunks delay)".to_string(), 10);
let (sink3, sink3_tx) = SinkNode::new("Room 3 (10 chunks delay)".to_string(), 10);
buffer.add_subscriber_with_offset(sink1_tx, 0).await;
buffer.add_subscriber_with_offset(sink2_tx, 5).await;
buffer.add_subscriber_with_offset(sink3_tx, 10).await;
// Spawn buffer et sinks
tokio::spawn(async move {
buffer.run().await.unwrap();
});
let sink1_handle = tokio::spawn(async move {
let stats = sink1.run_with_stats().await.unwrap();
stats.display();
stats
});
let sink2_handle = tokio::spawn(async move {
let stats = sink2.run_with_stats().await.unwrap();
stats.display();
stats
});
let sink3_handle = tokio::spawn(async move {
let stats = sink3.run_with_stats().await.unwrap();
stats.display();
stats
});
// Générer de l'audio dans une tâche séparée
println!("Generating audio for multiroom playback...\n");
tokio::spawn(async move {
let mut source = SourceNode::new();
source.add_subscriber(buffer_tx);
source
.generate_chunks(30, 4800, 48000, 440.0)
.await
.unwrap();
});
println!("Waiting for all rooms to finish...\n");
// Attendre toutes les sorties
let stats1 = sink1_handle.await.unwrap();
let stats2 = sink2_handle.await.unwrap();
let stats3 = sink3_handle.await.unwrap();
println!("\n=== Multiroom Summary ===");
println!(
"{}: {} chunks received",
stats1.name, stats1.chunks_received
);
println!(
"{}: {} chunks received",
stats2.name, stats2.chunks_received
);
println!(
"{}: {} chunks received",
stats3.name, stats3.chunks_received
);
println!("\nNote: Delayed rooms receive fewer chunks due to the offset");
}

View File

@@ -0,0 +1,168 @@
//! Exemple complet de pipeline multiroom avec contrôle de volume
//!
//! Ce programme démontre :
//! - Une source audio unique
//! - Deux branches de sortie : Chromecast et DiskSink
//! - Un volume master avec deux VolumeNodes secondaires synchronisés
//! - Système d'événements pour la communication entre nodes
use pmoaudio::{
ChromecastConfig, ChromecastSink, DiskSink, DiskSinkConfig, SourceNode, VolumeNode,
};
use tokio::sync::mpsc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== PMOAudio Multiroom Volume Demo ===\n");
// Configuration
let sample_rate = 48000u32;
let chunk_size = 4800usize; // 100ms à 48kHz
let num_chunks = 50; // 5 secondes de lecture
let frequency = 440.0; // La 440 Hz
// ===== 1. Créer la source audio =====
println!("1. Creating audio source...");
let mut source = SourceNode::new();
// ===== 2. Créer le volume master =====
println!("2. Creating master volume node...");
let (mut master_volume, master_tx) = VolumeNode::new("master".to_string(), 1.0, 50);
let master_handle = master_volume.get_handle();
// Channel pour les événements du volume master
let (master_event_tx, master_event_rx_chromecast) = mpsc::channel(10);
let (_, master_event_rx_disk) = mpsc::channel(10);
master_volume.subscribe_volume_events(master_event_tx);
source.add_subscriber(master_tx);
// ===== 3. Créer les branches de sortie =====
// Branche 1: Chromecast avec volume secondaire
println!("3a. Creating Chromecast output branch...");
let (mut chromecast_volume, chromecast_volume_tx) =
VolumeNode::new("chromecast_volume".to_string(), 0.8, 50);
chromecast_volume.set_master_volume_source(master_event_rx_chromecast);
let chromecast_config = ChromecastConfig {
device_address: "192.168.1.100".to_string(),
device_name: "Living Room".to_string(),
..Default::default()
};
let (chromecast_sink, chromecast_sink_tx) =
ChromecastSink::new("chromecast1".to_string(), chromecast_config, 50);
chromecast_volume.add_subscriber(chromecast_sink_tx);
master_volume.add_subscriber(chromecast_volume_tx);
// Branche 2: DiskSink avec volume secondaire
println!("3b. Creating DiskSink output branch...");
let (mut disk_volume, disk_volume_tx) = VolumeNode::new("disk_volume".to_string(), 0.9, 50);
disk_volume.set_master_volume_source(master_event_rx_disk);
let disk_config = DiskSinkConfig {
output_dir: std::env::temp_dir().join("pmoaudio_demo"),
filename: Some("multiroom_output.wav".to_string()),
..Default::default()
};
let (disk_sink, disk_sink_tx) = DiskSink::new("disk1".to_string(), disk_config, 50);
disk_volume.add_subscriber(disk_sink_tx);
master_volume.add_subscriber(disk_volume_tx);
// ===== 4. Lancer tous les nodes =====
println!("4. Starting pipeline nodes...\n");
// Spawn master volume
let master_volume_handle = tokio::spawn(async move {
master_volume.run().await.unwrap();
});
// Spawn chromecast branch
let chromecast_volume_handle = tokio::spawn(async move {
chromecast_volume.run().await.unwrap();
});
let chromecast_sink_handle = tokio::spawn(async move {
let stats = chromecast_sink.run().await.unwrap();
stats.display();
});
// Spawn disk branch
let disk_volume_handle = tokio::spawn(async move {
disk_volume.run().await.unwrap();
});
let disk_sink_handle = tokio::spawn(async move {
let stats = disk_sink.run().await.unwrap();
stats.display();
});
// ===== 5. Contrôler le volume pendant la lecture =====
let master_handle_clone = master_handle.clone();
tokio::spawn(async move {
// Attendre un peu, puis diminuer le volume
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
println!("\n>>> Decreasing master volume to 0.7");
master_handle_clone.set_volume(0.7).await;
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
println!(">>> Decreasing master volume to 0.4");
master_handle_clone.set_volume(0.4).await;
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
println!(">>> Increasing master volume back to 1.0");
master_handle_clone.set_volume(1.0).await;
});
// ===== 6. Générer et envoyer les chunks audio =====
println!("5. Generating and streaming audio...");
tokio::spawn(async move {
source
.generate_chunks(num_chunks, chunk_size, sample_rate, frequency)
.await
.unwrap();
println!("\n>>> Audio generation complete!");
});
// ===== 7. Attendre la fin de tous les nodes =====
println!("6. Waiting for all nodes to complete...\n");
// Attendre que les sinks terminent
chromecast_sink_handle.await?;
disk_sink_handle.await?;
// Nettoyer
master_volume_handle.abort();
chromecast_volume_handle.abort();
disk_volume_handle.abort();
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!("- Output to Chromecast: Living Room (192.168.1.100)");
println!(
"- Output to file: {}",
std::env::temp_dir()
.join("pmoaudio_demo")
.join("multiroom_output.wav")
.display()
);
println!("- Master volume control demonstrated with live changes");
println!("\nAll streams received synchronized volume updates!");
Ok(())
}

View File

@@ -0,0 +1,126 @@
//! Exemple de pipeline audio stéréo complet avec tous les nodes
//!
//! Pipeline: SourceNode → DecoderNode → DspNode → BufferNode → TimerNode → SinkNode(s)
use pmoaudio::{BufferNode, DecoderNode, DspNode, SinkNode, SourceNode, TimerNode};
#[tokio::main]
async fn main() {
println!("=== PMOAudio Pipeline Demo ===\n");
// Créer le pipeline de nodes
// 2. DecoderNode - passthrough dans cet exemple
let (mut decoder, decoder_tx) = DecoderNode::new(10);
// 3. DspNode - applique un gain de 0.5
let (mut dsp, dsp_tx) = DspNode::new(10, 0.5);
// 4. BufferNode - buffer circulaire pour multiroom
let (mut buffer, buffer_tx) = BufferNode::new(100, 10);
// 5. TimerNode - calcule la position temporelle
let (mut timer, timer_tx) = TimerNode::new(10);
// 6. SinkNodes - deux destinations finales
let (sink1, sink1_tx) = SinkNode::new("Main Output".to_string(), 10);
let (sink2, sink2_tx) = SinkNode::new("Secondary Output".to_string(), 10);
// Ajouter un abonné au BufferNode avec offset (multiroom simulation)
let (sink3, sink3_tx) = SinkNode::new("Delayed Output".to_string(), 10);
buffer.add_subscriber_with_offset(sink3_tx, 5).await; // 5 chunks de retard
// Connecter le pipeline
decoder.add_subscriber(dsp_tx);
dsp.add_subscriber(buffer_tx);
buffer.add_next_subscriber(timer_tx); // BufferNode -> TimerNode
timer.add_subscriber(sink1_tx);
timer.add_subscriber(sink2_tx);
// Obtenir un handle pour lire la position du TimerNode
let timer_handle = timer.get_position_handle();
// Spawn tous les nodes
let decoder_handle = tokio::spawn(async move {
decoder.run_passthrough().await.unwrap();
});
let dsp_handle = tokio::spawn(async move {
dsp.run().await.unwrap();
});
let buffer_handle = tokio::spawn(async move {
buffer.run().await.unwrap();
});
let timer_handle_task = tokio::spawn(async move {
timer.run().await.unwrap();
});
let sink1_handle = tokio::spawn(async move {
let stats = sink1.run_with_stats().await.unwrap();
stats.display();
stats
});
let sink2_handle = tokio::spawn(async move {
sink2.run_silent().await.unwrap();
});
let sink3_handle = tokio::spawn(async move {
let stats = sink3.run_with_stats().await.unwrap();
stats.display();
stats
});
// Spawn une tâche pour afficher la position périodiquement
let position_monitor = tokio::spawn(async move {
for _ in 0..10 {
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let position = timer_handle.position_sec().await;
let samples = timer_handle.elapsed_samples().await;
println!("Position: {:.3} sec ({} samples)", position, samples);
}
});
// Générer des chunks audio
println!("Generating audio chunks...\n");
let chunk_size = 4800; // 100ms à 48kHz
let sample_rate = 48000;
let frequency = 440.0; // La 440Hz
// Source node dans une tâche séparée
tokio::spawn(async move {
let mut source = SourceNode::new();
source.add_subscriber(decoder_tx);
// Générer 50 chunks (environ 5 secondes)
source
.generate_chunks(50, chunk_size, sample_rate, frequency)
.await
.unwrap();
println!("\nChunks sent. Processing...\n");
});
// Attendre que tous les nodes terminent
decoder_handle.await.unwrap();
dsp_handle.await.unwrap();
buffer_handle.await.unwrap();
timer_handle_task.await.unwrap();
let stats1 = sink1_handle.await.unwrap();
sink2_handle.await.unwrap();
let stats3 = sink3_handle.await.unwrap();
position_monitor.await.unwrap();
println!("\n=== Pipeline Demo Complete ===");
println!(
"Main output processed: {} chunks, {:.3} sec",
stats1.chunks_received, stats1.total_duration_sec
);
println!(
"Delayed output processed: {} chunks, {:.3} sec",
stats3.chunks_received, stats3.total_duration_sec
);
}

View File

@@ -0,0 +1,96 @@
//! Quick Start - Démonstration rapide des nouvelles fonctionnalités
//!
//! Cet exemple montre l'utilisation des principales nouvelles fonctionnalités :
//! - VolumeNode avec contrôle dynamique
//! - DiskSink pour écriture sur disque
//! - Pipeline simple et efficace
use pmoaudio::{AudioFileFormat, DiskSink, DiskSinkConfig, SourceNode, VolumeNode};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== PMOAudio Quick Start ===\n");
// 1. Créer la source audio (génère un signal de test)
let mut source = SourceNode::new();
// 2. Créer un VolumeNode pour contrôler le volume
let (mut volume, volume_tx) = VolumeNode::new("main".to_string(), 0.8, 10);
let volume_handle = volume.get_handle();
// 3. Créer un DiskSink pour écrire sur disque
let output_dir = std::env::temp_dir().join("pmoaudio_quickstart");
let config = DiskSinkConfig {
output_dir: output_dir.clone(),
filename: Some("quickstart_output.wav".to_string()),
format: AudioFileFormat::Wav,
buffer_size: 50,
};
let (disk_sink, disk_tx) = DiskSink::new("disk".to_string(), config, 10);
// 4. Connecter le pipeline : Source → Volume → DiskSink
source.add_subscriber(volume_tx);
volume.add_subscriber(disk_tx);
println!("Pipeline configured:");
println!(" SourceNode → VolumeNode (vol=0.8) → DiskSink");
println!(" Output: {}/quickstart_output.wav\n", output_dir.display());
// 5. Lancer les nodes
let volume_handle_clone = volume_handle.clone();
tokio::spawn(async move {
volume.run().await.unwrap();
});
let disk_handle = tokio::spawn(async move {
let stats = disk_sink.run().await.unwrap();
println!("\nDiskSink Statistics:");
stats.display();
stats
});
// 6. Démonstration du contrôle de volume pendant la lecture
tokio::spawn(async move {
println!("Generating audio with volume changes...");
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
println!(" → Volume: 0.8 (initial)");
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
volume_handle_clone.set_volume(0.5).await;
println!(" → Volume: 0.5 (decreased)");
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
volume_handle_clone.set_volume(1.0).await;
println!(" → Volume: 1.0 (maximum)");
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
volume_handle_clone.set_volume(0.3).await;
println!(" → Volume: 0.3 (low)");
});
// 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)
)
.await?;
// 8. Attendre la fin du traitement
let stats = disk_handle.await?;
// 9. Résumé
println!("\n=== Summary ===");
println!("✓ Audio file generated successfully");
println!("{} chunks written", stats.chunks_written);
println!("✓ Duration: {:.2} seconds", stats.total_duration_sec);
println!("✓ Volume was dynamically adjusted during playback");
println!("\nYou can play the file with:");
println!(" ffplay {}/quickstart_output.wav", output_dir.display());
Ok(())
}

View File

@@ -0,0 +1,53 @@
//! Exemple simple de pipeline audio : Source → Timer → Sink
//!
//! Démontre l'utilisation basique du pipeline avec calcul de position
use pmoaudio::{SinkNode, SourceNode, TimerNode};
#[tokio::main]
async fn main() {
println!("=== Simple Pipeline Example ===\n");
// Créer les nodes
let (mut timer, timer_tx) = TimerNode::new(10);
let (sink, sink_tx) = SinkNode::new("Output".to_string(), 10);
// Connecter
timer.add_subscriber(sink_tx);
// Handle pour monitorer la position
let timer_handle = timer.get_position_handle();
// Spawn timer et sink
tokio::spawn(async move {
timer.run().await.unwrap();
});
let sink_handle = tokio::spawn(async move {
let stats = sink.run_with_stats().await.unwrap();
stats.display();
stats
});
// Générer quelques secondes d'audio dans une tâche séparée
println!("Generating 440Hz sine wave...\n");
tokio::spawn(async move {
let mut source = SourceNode::new();
source.add_subscriber(timer_tx);
source
.generate_chunks(30, 4800, 48000, 440.0) // ~3 secondes
.await
.unwrap();
// La source est drop ici, fermant le channel
});
// Attendre la fin
let stats = sink_handle.await.unwrap();
let final_position = timer_handle.position_sec().await;
println!("\nFinal position: {:.3} seconds", final_position);
println!("Total duration: {:.3} seconds", stats.total_duration_sec);
}

View File

@@ -0,0 +1,52 @@
//! Exemple de streaming audio en temps réel
//!
//! Démontre l'utilisation du pipeline avec génération de chunks
//! en temps réel avec timing approprié
use pmoaudio::{SinkNode, SourceNode, TimerNode};
#[tokio::main]
async fn main() {
println!("=== Streaming Demo ===\n");
println!("Streaming audio in real-time for 3 seconds...\n");
let mut source = SourceNode::new();
let (mut timer, timer_tx) = TimerNode::new(20);
let (sink, sink_tx) = SinkNode::new("Streaming Output".to_string(), 20);
source.add_subscriber(timer_tx);
timer.add_subscriber(sink_tx);
let timer_handle = timer.get_position_handle();
// Spawn le pipeline
tokio::spawn(async move {
timer.run().await.unwrap();
});
let sink_handle = tokio::spawn(async move {
sink.run_with_logging().await.unwrap();
});
// Monitor la position
let monitor_handle = tokio::spawn(async move {
for _ in 0..15 {
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
let position = timer_handle.position_sec().await;
println!("Playback position: {:.3} sec", position);
}
});
// Stream des chunks avec timing réel
// 100ms par chunk à 48kHz = 4800 samples
source
.stream_chunks(4800, 48000, 440.0, 3000) // 3 secondes
.await
.unwrap();
println!("\nStreaming complete.");
// Attendre la fin
sink_handle.await.unwrap();
monitor_handle.await.unwrap();
}

View File

@@ -0,0 +1,58 @@
//! Exemple simple de contrôle de volume
//!
//! Démontre l'utilisation du VolumeNode avec changements dynamiques
use pmoaudio::{SinkNode, SourceNode, VolumeNode};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Volume Control Demo ===\n");
// Créer la source
let mut source = SourceNode::new();
// Créer le volume node
let (mut volume, volume_tx) = VolumeNode::new("main".to_string(), 1.0, 10);
let volume_handle = volume.get_handle();
// Créer le sink
let (sink, sink_tx) = SinkNode::new("Output".to_string(), 10);
// Connecter le pipeline
source.add_subscriber(volume_tx);
volume.add_subscriber(sink_tx);
// Lancer les nodes
tokio::spawn(async move { volume.run().await.unwrap() });
let sink_handle = tokio::spawn(async move { sink.run_with_stats().await.unwrap() });
// Contrôler le volume pendant la lecture
let volume_control = tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
println!("Setting volume to 0.5");
volume_handle.set_volume(0.5).await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
println!("Setting volume to 0.2");
volume_handle.set_volume(0.2).await;
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
println!("Setting volume to 1.0");
volume_handle.set_volume(1.0).await;
});
// Générer l'audio
source
.generate_chunks(20, 4800, 48000, 440.0)
.await
.unwrap();
volume_control.await?;
let stats = sink_handle.await?;
println!("\nFinal statistics:");
stats.display();
Ok(())
}