14 KiB
PMOAudio - Extensions Multiroom et Contrôle de Volume
Vue d'ensemble
Ce document décrit les extensions apportées au système PMOAudio pour supporter :
- Contrôle de volume dynamique avec synchronisation master/secondaire
- Nouveaux types de sinks : DiskSink, ChromecastSink, MpdSink
- Système d'événements générique pour la communication inter-nodes
- Champ gain dans AudioChunk pour le contrôle du volume en pipeline
1. AudioChunk avec gain
Le type AudioChunk a été étendu avec un champ gain: f32 qui permet de contrôler le volume de manière lazy (le gain est appliqué au moment voulu, pas immédiatement).
Nouvelles méthodes
// Créer un chunk avec gain spécifique
let chunk = AudioChunk::with_gain(0, left, right, 48000, 0.5);
// Modifier le gain d'un chunk existant (cheap, pas de copie)
let modified = chunk.with_modified_gain(0.8);
// Appliquer le gain et matérialiser les données modifiées
let applied = chunk.apply_gain();
Comportement
- Le gain par défaut est
1.0(aucun changement) - Les gains se multiplient en cascade (utile pour chaîner plusieurs VolumeNode)
apply_gain()crée un nouveau chunk avec les samples multipliés par le gain
2. Système d'événements générique
Un système d'abonnement type-safe permet aux nodes d'émettre et de recevoir différents types d'événements.
Types d'événements disponibles
// Événement de changement de volume
VolumeChangeEvent {
volume: f32,
source_node_id: String,
}
// Événement de mise à jour du nom de source
SourceNameUpdateEvent {
source_name: String,
device_name: Option<String>,
}
// Événement de données audio (pour référence)
AudioDataEvent {
chunk: Arc<AudioChunk>,
}
Utilisation
// Créer un publisher
let mut volume_publisher = EventPublisher::<VolumeChangeEvent>::new();
// S'abonner
let (tx, mut rx) = mpsc::channel(10);
volume_publisher.subscribe(tx);
// Publier un événement
let event = VolumeChangeEvent {
volume: 0.7,
source_node_id: "master".to_string(),
};
volume_publisher.publish(event).await;
// Recevoir
let received = rx.recv().await;
3. VolumeNode - Contrôle de volume software
Le VolumeNode permet d'ajuster dynamiquement le volume du flux audio.
Caractéristiques
- Thread-safe : le volume peut être modifié pendant l'exécution
- Notification : émet des événements lors des changements
- Master/Slave : peut s'abonner à un volume master
- Lazy application : modifie le champ
gaindu chunk, pas les données
Exemple de base
// Créer un VolumeNode avec volume initial 0.8
let (mut volume_node, volume_tx) = VolumeNode::new(
"room1".to_string(),
0.8, // volume initial
10 // taille du channel
);
// Obtenir un handle pour contrôler le volume
let handle = volume_node.get_handle();
// Modifier le volume depuis un autre contexte
tokio::spawn(async move {
handle.set_volume(0.5).await;
});
// Lancer le node
tokio::spawn(async move {
volume_node.run().await.unwrap()
});
Configuration Master/Slave
// Créer le master
let (mut master, master_tx) = VolumeNode::new("master".to_string(), 1.0, 10);
let (master_event_tx, master_event_rx) = mpsc::channel(10);
master.subscribe_volume_events(master_event_tx);
let master_handle = master.get_handle();
// Créer le slave
let (mut slave, slave_tx) = VolumeNode::new("slave".to_string(), 0.8, 10);
slave.set_master_volume_source(master_event_rx);
// Le slave appliquera maintenant: local_volume * master_volume
// Ex: si master=0.5 et local=0.8, le gain final sera 0.4
4. HardwareVolumeNode
Version spécialisée pour contrôle hardware du volume (via driver audio).
Note : L'implémentation actuelle est identique à VolumeNode. Dans une vraie implémentation, elle communiquerait avec le driver système (ALSA, CoreAudio, WASAPI, etc.).
let (hw_volume, hw_tx) = HardwareVolumeNode::new(
"hardware".to_string(),
0.8,
10
);
let handle = hw_volume.get_handle();
handle.set_volume(0.9).await; // Ajusterait le volume matériel
5. DiskSink - Écriture sur disque
Le DiskSink écrit le flux audio dans un fichier sur disque avec support de plusieurs formats.
Caractéristiques
- Dérivation automatique du nom : peut utiliser le nom de la source
- Formats supportés : WAV, FLAC (mock), PCM brut
- Application du gain : applique automatiquement le gain avant l'écriture
- Écriture asynchrone avec buffer
Configuration
let config = DiskSinkConfig {
output_dir: PathBuf::from("/tmp/audio"),
filename: Some("output.wav".to_string()), // ou None pour dérivation auto
format: AudioFileFormat::Wav,
buffer_size: 100,
};
let (disk_sink, disk_tx) = DiskSink::new("disk1".to_string(), config, 10);
Dérivation du nom de fichier
Si filename est None, le DiskSink peut écouter les événements SourceNameUpdateEvent pour dériver automatiquement le nom :
let (source_name_tx, source_name_rx) = mpsc::channel(10);
disk_sink.set_source_name_source(source_name_rx);
// Quand un événement est reçu
let event = SourceNameUpdateEvent {
source_name: "My_Song.mp3".to_string(),
device_name: None,
};
source_name_tx.send(event).await;
// Le fichier sera créé comme: /tmp/audio/My_Song_mp3.wav
Formats supportés
// WAV (16-bit PCM stéréo)
AudioFileFormat::Wav
// FLAC (nécessite bibliothèque externe - actuellement utilise WAV)
AudioFileFormat::Flac
// PCM brut (pas d'en-tête)
AudioFileFormat::Raw
6. ChromecastSink - Diffusion Chromecast
Streame l'audio vers un périphérique Chromecast.
Note : Implémentation mock. Une vraie implémentation nécessiterait une bibliothèque comme rust-cast.
Configuration
let config = ChromecastConfig {
device_address: "192.168.1.100".to_string(),
device_name: "Living Room".to_string(),
port: 8009,
buffer_size: 50,
encoding: StreamEncoding::Mp3,
};
let (chromecast_sink, chromecast_tx) = ChromecastSink::new(
"chromecast1".to_string(),
config,
10
);
Encodages supportés
StreamEncoding::Mp3 // Compatible avec la plupart des Chromecasts
StreamEncoding::Aac // Haute qualité
StreamEncoding::Opus // Faible latence
StreamEncoding::Pcm // Non compressé (haute bande passante)
7. MpdSink - Streaming vers MPD
Envoie le flux à un démon MPD (Music Player Daemon).
Note : Implémentation mock. Une vraie implémentation nécessiterait le protocole MPD complet.
Configuration
let config = MpdConfig {
host: "localhost".to_string(),
port: 6600,
password: Some("secret".to_string()),
output_name: Some("ALSA".to_string()),
buffer_size: 50,
format: MpdAudioFormat::S16Le,
};
let (mpd_sink, mpd_tx) = MpdSink::new("mpd1".to_string(), config, 10);
Contrôle MPD
Le MpdSink fournit un handle pour contrôler la lecture :
let handle = mpd_sink.get_handle();
handle.play().await;
handle.pause().await;
handle.set_volume(75).await; // 0-100
handle.stop().await;
Formats audio MPD
MpdAudioFormat::S16Le // 16-bit signed
MpdAudioFormat::S24Le // 24-bit signed
MpdAudioFormat::S32Le // 32-bit signed
MpdAudioFormat::F32 // Float 32-bit
8. Pipeline Multiroom Complet
Voici un exemple complet d'utilisation de toutes les fonctionnalités :
use pmoaudio::{
SourceNode, VolumeNode, ChromecastSink, DiskSink,
ChromecastConfig, DiskSinkConfig,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Source audio
let mut source = SourceNode::new();
// 2. Volume master
let (mut master_volume, master_tx) = VolumeNode::new("master".to_string(), 1.0, 50);
let master_handle = master_volume.get_handle();
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. Branche Chromecast avec volume secondaire
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);
// 4. Branche DiskSink avec volume secondaire
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("audio"),
filename: Some("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);
// 5. Lancer tous les nodes
tokio::spawn(async move { master_volume.run().await.unwrap() });
tokio::spawn(async move { chromecast_volume.run().await.unwrap() });
tokio::spawn(async move { disk_volume.run().await.unwrap() });
let chromecast_handle = tokio::spawn(async move {
chromecast_sink.run().await.unwrap()
});
let disk_handle = tokio::spawn(async move {
disk_sink.run().await.unwrap()
});
// 6. Contrôler le volume dynamiquement
tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
master_handle.set_volume(0.7).await;
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
master_handle.set_volume(0.4).await;
});
// 7. Générer et streamer l'audio
tokio::spawn(async move {
source.generate_chunks(50, 4800, 48000, 440.0).await.unwrap();
});
// 8. Attendre la fin
chromecast_handle.await?;
disk_handle.await?;
Ok(())
}
Architecture du pipeline multiroom
┌──────────────┐
│ SourceNode │
└──────┬───────┘
│
▼
┌──────────────┐
│ MasterVolume │ ───────► VolumeChangeEvent
└──────┬───────┘ │
│ │
├──────────────────────┼────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ │
│ChromecastVolume │ │ DiskVolume │ │
│ (0.8 local) │ │ (0.9 local) │ │
└────────┬────────┘ └──────┬───────┘ │
│ │ │
│ gain=master×local │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ ...
│ ChromecastSink │ │ DiskSink │
│ Living Room │ │ output.wav │
└─────────────────┘ └──────────────┘
Flux des données
- SourceNode génère des chunks audio avec
gain = 1.0 - MasterVolume modifie le gain :
chunk.gain *= master_volume - Chaque branche secondaire :
- Reçoit le chunk du master
- Applique son volume local :
chunk.gain *= local_volume - Envoie au sink
- Les sinks appliquent le gain final avant l'output
Optimisations
Zero-copy jusqu'au bout
- Les chunks audio (
Arc<AudioChunk>) sont partagés entre branches - Seule la structure est clonée (cheap), pas les données audio
- Le gain est stocké dans le chunk, pas appliqué immédiatement
Application lazy du gain
// Modification du gain : O(1), pas de copie
let modified = chunk.with_modified_gain(0.5);
// Application : O(n), copie et multiplie les samples
let applied = chunk.apply_gain();
Thread-safety
VolumeHandleutiliseArc<RwLock<f32>>pour partager le volume- Changements de volume thread-safe et non-bloquants
EventPublisherutilisetry_sendpour éviter les blocages
Tests
Tous les composants incluent des tests unitaires :
cargo test --lib
Tests disponibles
test_volume_node_basic: test de base du VolumeNodetest_volume_handle: modification du volume via handletest_volume_events: publication d'événementstest_master_slave_volume: synchronisation master/slavetest_disk_sink_basic: écriture sur disquetest_chromecast_sink_basic: simulation Chromecasttest_mpd_sink_basic: simulation MPD
Exemples
Deux exemples complets sont fournis :
1. Volume Control Demo
Démontre le contrôle dynamique du volume :
cargo run --example volume_control_demo
2. Multiroom Volume Demo
Démontre un pipeline complet avec deux branches et synchronisation master/slave :
cargo run --example multiroom_volume_demo
Évolutions futures
Implémentations réelles des sinks
- ChromecastSink : intégrer
rust-castou équivalent - MpdSink : implémenter le protocole MPD complet
- DiskSink FLAC : intégrer
flacousymphonia
Nouveaux sinks possibles
AirPlaySink: diffusion vers AirPlay/AirPlay 2PulseAudioSink: sortie vers PulseAudioAlsaSink: sortie directe ALSA (Linux)CoreAudioSink: sortie CoreAudio (macOS)WasapiSink: sortie WASAPI (Windows)HttpStreamSink: serveur HTTP pour streamingRtpSink: streaming RTP/UDP
Fonctionnalités avancées
- Égaliseur :
EqualizerNodeavec bandes paramétriques - Compresseur/Limiteur :
DynamicsNode - Crossfade : transition entre sources
- Room correction : correction acoustique par pièce
- Synchronisation multi-device : timing précis avec NTP/PTP
Licence
Ce code fait partie du projet PMOMusic.