359 lines
12 KiB
Rust
359 lines
12 KiB
Rust
|
|
//! Volume nodes - Contrôle du volume audio
|
||
|
|
//!
|
||
|
|
//! Ce module fournit des nodes pour ajuster le volume du flux audio,
|
||
|
|
//! avec support du volume master/secondaire et notification des changements.
|
||
|
|
|
||
|
|
use crate::{
|
||
|
|
events::{EventPublisher, VolumeChangeEvent},
|
||
|
|
nodes::{AudioError, MultiSubscriberNode},
|
||
|
|
AudioChunk,
|
||
|
|
};
|
||
|
|
use std::sync::Arc;
|
||
|
|
use tokio::sync::{mpsc, RwLock};
|
||
|
|
|
||
|
|
/// VolumeNode - Applique un gain au flux audio (contrôle software)
|
||
|
|
///
|
||
|
|
/// Ce node modifie le champ `gain` de chaque `AudioChunk` qui le traverse.
|
||
|
|
/// Le gain est multiplié avec le gain existant du chunk, permettant ainsi
|
||
|
|
/// une chaîne de contrôles de volume.
|
||
|
|
///
|
||
|
|
/// # Caractéristiques
|
||
|
|
///
|
||
|
|
/// - Thread-safe : le volume peut être modifié pendant l'exécution via `set_volume`
|
||
|
|
/// - Notification : émet des événements `VolumeChangeEvent` lors des changements
|
||
|
|
/// - Master/Slave : peut s'abonner à un volume master pour synchronisation
|
||
|
|
///
|
||
|
|
/// # Exemples
|
||
|
|
///
|
||
|
|
/// ```no_run
|
||
|
|
/// use pmoaudio::VolumeNode;
|
||
|
|
///
|
||
|
|
/// #[tokio::main]
|
||
|
|
/// async fn main() {
|
||
|
|
/// let (volume_node, volume_tx) = VolumeNode::new("Room 1".to_string(), 0.8, 10);
|
||
|
|
///
|
||
|
|
/// // Modifier le volume pendant l'exécution
|
||
|
|
/// let handle = volume_node.get_handle();
|
||
|
|
/// tokio::spawn(async move {
|
||
|
|
/// tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||
|
|
/// handle.set_volume(0.5).await;
|
||
|
|
/// });
|
||
|
|
///
|
||
|
|
/// tokio::spawn(async move { volume_node.run().await.unwrap() });
|
||
|
|
/// }
|
||
|
|
/// ```
|
||
|
|
pub struct VolumeNode {
|
||
|
|
/// Channel pour recevoir les chunks audio
|
||
|
|
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||
|
|
|
||
|
|
/// Subscribers pour les chunks modifiés
|
||
|
|
subscribers: MultiSubscriberNode,
|
||
|
|
|
||
|
|
/// Volume courant (partagé via RwLock pour lecture/écriture thread-safe)
|
||
|
|
volume: Arc<RwLock<f32>>,
|
||
|
|
|
||
|
|
/// Publisher pour les événements de changement de volume
|
||
|
|
volume_publisher: EventPublisher<VolumeChangeEvent>,
|
||
|
|
|
||
|
|
/// Identifiant unique du node (pour traçabilité)
|
||
|
|
node_id: String,
|
||
|
|
|
||
|
|
/// Receiver pour les événements de volume master (optionnel)
|
||
|
|
master_volume_rx: Option<mpsc::Receiver<VolumeChangeEvent>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl VolumeNode {
|
||
|
|
/// Crée un nouveau VolumeNode
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `node_id` - Identifiant unique du node
|
||
|
|
/// * `initial_volume` - Volume initial (0.0 à 1.0)
|
||
|
|
/// * `channel_size` - Taille du buffer du channel
|
||
|
|
pub fn new(
|
||
|
|
node_id: String,
|
||
|
|
initial_volume: f32,
|
||
|
|
channel_size: usize,
|
||
|
|
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||
|
|
let (tx, rx) = mpsc::channel(channel_size);
|
||
|
|
|
||
|
|
let node = Self {
|
||
|
|
rx,
|
||
|
|
subscribers: MultiSubscriberNode::new(),
|
||
|
|
volume: Arc::new(RwLock::new(initial_volume)),
|
||
|
|
volume_publisher: EventPublisher::new(),
|
||
|
|
node_id,
|
||
|
|
master_volume_rx: None,
|
||
|
|
};
|
||
|
|
|
||
|
|
(node, tx)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute un subscriber pour recevoir les chunks audio modifiés
|
||
|
|
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||
|
|
self.subscribers.add_subscriber(tx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute un subscriber pour les événements de changement de volume
|
||
|
|
pub fn subscribe_volume_events(&mut self, tx: mpsc::Sender<VolumeChangeEvent>) {
|
||
|
|
self.volume_publisher.subscribe(tx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Configure ce node pour écouter un volume master
|
||
|
|
///
|
||
|
|
/// Le node appliquera à la fois son volume local ET le volume master reçu.
|
||
|
|
pub fn set_master_volume_source(&mut self, rx: mpsc::Receiver<VolumeChangeEvent>) {
|
||
|
|
self.master_volume_rx = Some(rx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Retourne un handle pour contrôler le volume depuis un autre contexte
|
||
|
|
pub fn get_handle(&self) -> VolumeHandle {
|
||
|
|
VolumeHandle {
|
||
|
|
volume: self.volume.clone(),
|
||
|
|
node_id: self.node_id.clone(),
|
||
|
|
publisher: Arc::new(RwLock::new(self.volume_publisher.clone())),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Démarre la boucle de traitement du VolumeNode
|
||
|
|
pub async fn run(mut self) -> Result<(), AudioError> {
|
||
|
|
let mut master_volume = 1.0f32;
|
||
|
|
|
||
|
|
loop {
|
||
|
|
tokio::select! {
|
||
|
|
// Recevoir les chunks audio
|
||
|
|
chunk_opt = self.rx.recv() => {
|
||
|
|
match chunk_opt {
|
||
|
|
Some(chunk) => {
|
||
|
|
let local_volume = *self.volume.read().await;
|
||
|
|
let total_volume = local_volume * master_volume;
|
||
|
|
|
||
|
|
// Créer un nouveau chunk avec le gain modifié
|
||
|
|
let modified_chunk = chunk.with_modified_gain(total_volume);
|
||
|
|
|
||
|
|
// Envoyer aux subscribers
|
||
|
|
self.subscribers.push(Arc::new(modified_chunk)).await?;
|
||
|
|
}
|
||
|
|
None => {
|
||
|
|
// Channel fermé, terminer
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Recevoir les mises à jour du volume master (si configuré)
|
||
|
|
master_event_opt = async {
|
||
|
|
if let Some(ref mut rx) = self.master_volume_rx {
|
||
|
|
rx.recv().await
|
||
|
|
} else {
|
||
|
|
// Bloquer indéfiniment si pas de master
|
||
|
|
std::future::pending().await
|
||
|
|
}
|
||
|
|
} => {
|
||
|
|
if let Some(event) = master_event_opt {
|
||
|
|
master_volume = event.volume;
|
||
|
|
|
||
|
|
// Optionnel : re-publier l'événement combiné
|
||
|
|
let local_volume = *self.volume.read().await;
|
||
|
|
let combined_event = VolumeChangeEvent {
|
||
|
|
volume: local_volume * master_volume,
|
||
|
|
source_node_id: self.node_id.clone(),
|
||
|
|
};
|
||
|
|
self.volume_publisher.publish(combined_event).await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Handle pour contrôler un VolumeNode depuis un autre contexte
|
||
|
|
///
|
||
|
|
/// Ce handle permet de modifier le volume et de notifier les subscribers
|
||
|
|
/// sans avoir accès direct au node.
|
||
|
|
#[derive(Clone)]
|
||
|
|
pub struct VolumeHandle {
|
||
|
|
volume: Arc<RwLock<f32>>,
|
||
|
|
node_id: String,
|
||
|
|
publisher: Arc<RwLock<EventPublisher<VolumeChangeEvent>>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl VolumeHandle {
|
||
|
|
/// Modifie le volume
|
||
|
|
///
|
||
|
|
/// # Arguments
|
||
|
|
///
|
||
|
|
/// * `new_volume` - Nouveau volume (0.0 à 1.0)
|
||
|
|
pub async fn set_volume(&self, new_volume: f32) {
|
||
|
|
let clamped = new_volume.clamp(0.0, 1.0);
|
||
|
|
*self.volume.write().await = clamped;
|
||
|
|
|
||
|
|
// Publier l'événement de changement
|
||
|
|
let event = VolumeChangeEvent {
|
||
|
|
volume: clamped,
|
||
|
|
source_node_id: self.node_id.clone(),
|
||
|
|
};
|
||
|
|
|
||
|
|
self.publisher.read().await.publish(event).await;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Obtient le volume courant
|
||
|
|
pub async fn get_volume(&self) -> f32 {
|
||
|
|
*self.volume.read().await
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Augmente le volume de manière relative
|
||
|
|
pub async fn adjust_volume(&self, delta: f32) {
|
||
|
|
let current = *self.volume.read().await;
|
||
|
|
self.set_volume(current + delta).await;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// HardwareVolumeNode - Contrôle matériel du volume
|
||
|
|
///
|
||
|
|
/// Ce node simule un contrôle hardware du volume. Dans une implémentation réelle,
|
||
|
|
/// il communiquerait avec le driver audio pour ajuster le volume matériel.
|
||
|
|
///
|
||
|
|
/// Pour cette version, il agit de manière similaire à `VolumeNode` mais pourrait
|
||
|
|
/// être étendu pour utiliser des APIs système spécifiques.
|
||
|
|
pub struct HardwareVolumeNode {
|
||
|
|
inner: VolumeNode,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl HardwareVolumeNode {
|
||
|
|
/// Crée un nouveau HardwareVolumeNode
|
||
|
|
pub fn new(
|
||
|
|
node_id: String,
|
||
|
|
initial_volume: f32,
|
||
|
|
channel_size: usize,
|
||
|
|
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||
|
|
let (inner, tx) = VolumeNode::new(node_id, initial_volume, channel_size);
|
||
|
|
|
||
|
|
(Self { inner }, tx)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Ajoute un subscriber
|
||
|
|
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||
|
|
self.inner.add_subscriber(tx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Obtient un handle pour contrôler le volume
|
||
|
|
pub fn get_handle(&self) -> VolumeHandle {
|
||
|
|
self.inner.get_handle()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Démarre la boucle de traitement
|
||
|
|
pub async fn run(self) -> Result<(), AudioError> {
|
||
|
|
// Dans une vraie implémentation, on communiquerait avec le hardware ici
|
||
|
|
// Pour l'instant, délègue au VolumeNode standard
|
||
|
|
self.inner.run().await
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#[cfg(test)]
|
||
|
|
mod tests {
|
||
|
|
use super::*;
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_volume_node_basic() {
|
||
|
|
let (mut node, tx) = VolumeNode::new("test".to_string(), 0.5, 10);
|
||
|
|
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||
|
|
|
||
|
|
node.add_subscriber(out_tx);
|
||
|
|
|
||
|
|
let handle = tokio::spawn(async move { node.run().await });
|
||
|
|
|
||
|
|
// Envoyer un chunk avec gain 1.0
|
||
|
|
let chunk = AudioChunk::with_gain(0, vec![1.0; 100], vec![1.0; 100], 48000, 1.0);
|
||
|
|
tx.send(Arc::new(chunk)).await.unwrap();
|
||
|
|
|
||
|
|
// Recevoir le chunk modifié
|
||
|
|
let modified = out_rx.recv().await.unwrap();
|
||
|
|
assert!((modified.gain - 0.5).abs() < f32::EPSILON);
|
||
|
|
|
||
|
|
drop(tx);
|
||
|
|
handle.await.unwrap().unwrap();
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_volume_handle() {
|
||
|
|
let (node, tx) = VolumeNode::new("test".to_string(), 1.0, 10);
|
||
|
|
let handle = node.get_handle();
|
||
|
|
|
||
|
|
tokio::spawn(async move { node.run().await });
|
||
|
|
|
||
|
|
// Modifier le volume via le handle
|
||
|
|
handle.set_volume(0.3).await;
|
||
|
|
|
||
|
|
let volume = handle.get_volume().await;
|
||
|
|
assert!((volume - 0.3).abs() < f32::EPSILON);
|
||
|
|
|
||
|
|
drop(tx);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_volume_events() {
|
||
|
|
let (mut node, tx) = VolumeNode::new("test".to_string(), 1.0, 10);
|
||
|
|
let (event_tx, mut event_rx) = mpsc::channel(10);
|
||
|
|
|
||
|
|
node.subscribe_volume_events(event_tx);
|
||
|
|
let handle = node.get_handle();
|
||
|
|
|
||
|
|
tokio::spawn(async move { node.run().await });
|
||
|
|
|
||
|
|
// Changer le volume
|
||
|
|
handle.set_volume(0.7).await;
|
||
|
|
|
||
|
|
// Vérifier l'événement
|
||
|
|
let event = event_rx.recv().await.unwrap();
|
||
|
|
assert!((event.volume - 0.7).abs() < f32::EPSILON);
|
||
|
|
assert_eq!(event.source_node_id, "test");
|
||
|
|
|
||
|
|
drop(tx);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn test_master_slave_volume() {
|
||
|
|
// 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);
|
||
|
|
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||
|
|
slave.add_subscriber(out_tx);
|
||
|
|
|
||
|
|
tokio::spawn(async move { master.run().await });
|
||
|
|
tokio::spawn(async move { slave.run().await });
|
||
|
|
|
||
|
|
// Envoyer un chunk au slave
|
||
|
|
let chunk = AudioChunk::with_gain(0, vec![1.0; 100], vec![1.0; 100], 48000, 1.0);
|
||
|
|
slave_tx.send(Arc::new(chunk)).await.unwrap();
|
||
|
|
|
||
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||
|
|
|
||
|
|
// Modifier le volume master
|
||
|
|
master_handle.set_volume(0.5).await;
|
||
|
|
|
||
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
|
||
|
|
|
||
|
|
// Envoyer un autre chunk
|
||
|
|
let chunk2 = AudioChunk::with_gain(1, vec![1.0; 100], vec![1.0; 100], 48000, 1.0);
|
||
|
|
slave_tx.send(Arc::new(chunk2)).await.unwrap();
|
||
|
|
|
||
|
|
// Le deuxième chunk devrait avoir un gain de 0.8 * 0.5 = 0.4
|
||
|
|
let _first = out_rx.recv().await.unwrap(); // gain = 0.8
|
||
|
|
let second = out_rx.recv().await.unwrap(); // gain = 0.4
|
||
|
|
|
||
|
|
assert!((second.gain - 0.4).abs() < 0.01);
|
||
|
|
|
||
|
|
drop(master_tx);
|
||
|
|
drop(slave_tx);
|
||
|
|
}
|
||
|
|
}
|