Restructuration de pmoaudio avec ajout des messages de synchro
This commit is contained in:
245
old_code/pmoaudio/nodes/buffer_node.rs
Normal file
245
old_code/pmoaudio/nodes/buffer_node.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
use crate::{
|
||||
nodes::{AudioError, MultiSubscriberNode},
|
||||
AudioSegment,
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
|
||||
/// Subscriber avec son propre offset dans le buffer
|
||||
struct BufferSubscriber {
|
||||
tx: mpsc::Sender<Arc<AudioSegment>>,
|
||||
offset: usize, // Position dans le buffer circulaire
|
||||
}
|
||||
|
||||
/// BufferNode avec buffer circulaire pour support multiroom
|
||||
///
|
||||
/// Ce node maintient un buffer circulaire de chunks et permet à plusieurs
|
||||
/// abonnés de lire avec des offsets différents, ce qui est idéal pour des
|
||||
/// configurations multiroom où différentes pièces peuvent avoir un léger
|
||||
/// délai de synchronisation.
|
||||
///
|
||||
/// # Fonctionnement
|
||||
///
|
||||
/// - Le buffer est implémenté avec un `VecDeque` de taille fixe
|
||||
/// - Chaque abonné peut avoir un offset indépendant (en nombre de chunks)
|
||||
/// - Utilise `try_send` pour éviter de bloquer si un abonné est saturé
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::{BufferNode, SinkNode};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let (buffer, buffer_tx) = BufferNode::new(50, 10);
|
||||
///
|
||||
/// let (sink1, sink1_tx) = SinkNode::new("Room 1".to_string(), 10);
|
||||
/// let (sink2, sink2_tx) = SinkNode::new("Room 2".to_string(), 10);
|
||||
///
|
||||
/// // Room 1 sans délai
|
||||
/// buffer.add_subscriber_with_offset(sink1_tx, 0).await;
|
||||
///
|
||||
/// // Room 2 avec 5 chunks de retard
|
||||
/// buffer.add_subscriber_with_offset(sink2_tx, 5).await;
|
||||
///
|
||||
/// tokio::spawn(async move { buffer.run().await.unwrap() });
|
||||
/// // ... spawn sinks et source
|
||||
/// }
|
||||
/// ```
|
||||
pub struct BufferNode {
|
||||
buffer: Arc<RwLock<VecDeque<Arc<AudioSegment>>>>,
|
||||
subscribers: Arc<RwLock<Vec<BufferSubscriber>>>,
|
||||
buffer_size: usize,
|
||||
rx: mpsc::Receiver<Arc<AudioSegment>>,
|
||||
next_subscribers: MultiSubscriberNode, // Pour passer au node suivant
|
||||
}
|
||||
|
||||
impl BufferNode {
|
||||
/// Crée un nouveau BufferNode
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `buffer_size` - Taille maximale du buffer circulaire
|
||||
/// * `channel_size` - Taille du channel bounded pour backpressure
|
||||
pub fn new(buffer_size: usize, channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let node = Self {
|
||||
buffer: Arc::new(RwLock::new(VecDeque::with_capacity(buffer_size))),
|
||||
subscribers: Arc::new(RwLock::new(Vec::new())),
|
||||
buffer_size,
|
||||
rx,
|
||||
next_subscribers: MultiSubscriberNode::new(),
|
||||
};
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
/// Ajoute un abonné avec un offset spécifique (pour multiroom)
|
||||
pub async fn add_subscriber_with_offset(
|
||||
&self,
|
||||
tx: mpsc::Sender<Arc<AudioSegment>>,
|
||||
offset: usize,
|
||||
) {
|
||||
let mut subs = self.subscribers.write().await;
|
||||
subs.push(BufferSubscriber { tx, offset });
|
||||
}
|
||||
|
||||
/// Ajoute un abonné sans offset (commence au chunk courant)
|
||||
pub async fn add_subscriber(&self, tx: mpsc::Sender<Arc<AudioSegment>>) {
|
||||
self.add_subscriber_with_offset(tx, 0).await;
|
||||
}
|
||||
|
||||
/// Ajoute un abonné pour le node suivant (sans buffer)
|
||||
pub fn add_next_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
|
||||
self.next_subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Démarre la boucle de traitement du BufferNode
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
let mut chunk_index = 0usize;
|
||||
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
// Ajouter au buffer circulaire
|
||||
{
|
||||
let mut buffer = self.buffer.write().await;
|
||||
if buffer.len() >= self.buffer_size {
|
||||
buffer.pop_front();
|
||||
}
|
||||
buffer.push_back(chunk.clone());
|
||||
}
|
||||
|
||||
// Envoyer aux abonnés avec offset
|
||||
{
|
||||
let buffer = self.buffer.read().await;
|
||||
let mut subs = self.subscribers.write().await;
|
||||
|
||||
for sub in subs.iter_mut() {
|
||||
// Calculer l'index dans le buffer en fonction de l'offset
|
||||
let target_index = if chunk_index >= sub.offset {
|
||||
chunk_index - sub.offset
|
||||
} else {
|
||||
continue; // Pas encore assez de données
|
||||
};
|
||||
|
||||
// Vérifier si le chunk est disponible dans le buffer
|
||||
let buffer_age = chunk_index - target_index;
|
||||
if buffer_age < buffer.len() {
|
||||
let chunk_to_send = &buffer[buffer.len() - buffer_age - 1];
|
||||
// try_send non-bloquant pour éviter de bloquer la source
|
||||
let _ = sub.tx.try_send(chunk_to_send.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push vers les nodes suivants sans buffer
|
||||
self.next_subscribers.try_push(chunk).await?;
|
||||
|
||||
chunk_index += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Version avec push synchrone au lieu de try_push
|
||||
pub async fn run_blocking(mut self) -> Result<(), AudioError> {
|
||||
let mut chunk_index = 0usize;
|
||||
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
// Ajouter au buffer circulaire
|
||||
{
|
||||
let mut buffer = self.buffer.write().await;
|
||||
if buffer.len() >= self.buffer_size {
|
||||
buffer.pop_front();
|
||||
}
|
||||
buffer.push_back(chunk.clone());
|
||||
}
|
||||
|
||||
// Envoyer aux abonnés avec offset
|
||||
{
|
||||
let buffer = self.buffer.read().await;
|
||||
let subs = self.subscribers.read().await;
|
||||
|
||||
for sub in subs.iter() {
|
||||
let target_index = if chunk_index >= sub.offset {
|
||||
chunk_index - sub.offset
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let buffer_age = chunk_index - target_index;
|
||||
if buffer_age < buffer.len() {
|
||||
let chunk_to_send = &buffer[buffer.len() - buffer_age - 1];
|
||||
let _ = sub.tx.send(chunk_to_send.clone()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push vers les nodes suivants
|
||||
for _ in 0..self.next_subscribers.subscribers.len() {
|
||||
self.next_subscribers.push(chunk.clone()).await?;
|
||||
}
|
||||
|
||||
chunk_index += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::BitDepth;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_buffer_node_basic() {
|
||||
let (mut node, tx) = BufferNode::new(10, 5);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(5);
|
||||
|
||||
node.add_next_subscriber(out_tx);
|
||||
|
||||
// Spawn le node
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer des chunks
|
||||
for i in 0..3 {
|
||||
let chunk = AudioSegment::AudioChunk(AudioChunk::new(i, vec![[0i32; 2]; 100], 48000, BitDepth::B24));
|
||||
tx.send(chunk).await.unwrap();
|
||||
}
|
||||
|
||||
// Recevoir les chunks
|
||||
for i in 0..3 {
|
||||
let chunk = out_rx.recv().await.unwrap();
|
||||
assert_eq!(chunk.order(), i);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_buffer_node_with_offset() {
|
||||
let (node, tx) = BufferNode::new(10, 10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
// Ajouter un abonné avec offset de 2 chunks
|
||||
node.add_subscriber_with_offset(out_tx, 2).await;
|
||||
|
||||
// Spawn le node
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer 5 chunks
|
||||
for i in 0..5 {
|
||||
let chunk = AudioSegment::new(i, vec![[0i32; 2]; 100], 48000, BitDepth::B24);
|
||||
tx.send(chunk).await.unwrap();
|
||||
}
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
// L'abonné devrait recevoir les chunks 0, 1, 2 (avec 2 chunks de retard)
|
||||
let chunk = out_rx.try_recv().unwrap();
|
||||
assert_eq!(chunk.order(), 0);
|
||||
}
|
||||
}
|
||||
290
old_code/pmoaudio/nodes/chromecast_sink.rs
Normal file
290
old_code/pmoaudio/nodes/chromecast_sink.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
//! ChromecastSink - Diffuse le flux audio vers un périphérique Chromecast
|
||||
//!
|
||||
//! Ce module fournit un sink qui envoie le flux audio à un Chromecast.
|
||||
//! Note: Cette implémentation est une version mock/skeleton. Une vraie implémentation
|
||||
//! nécessiterait une bibliothèque comme `rust-cast` ou similaire.
|
||||
|
||||
use crate::{nodes::AudioError, AudioChunk};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Configuration pour le ChromecastSink
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChromecastConfig {
|
||||
/// Nom ou adresse IP du Chromecast
|
||||
pub device_address: String,
|
||||
|
||||
/// Nom amical du device
|
||||
pub device_name: String,
|
||||
|
||||
/// Port de communication (défaut: 8009)
|
||||
pub port: u16,
|
||||
|
||||
/// Taille du buffer de streaming
|
||||
pub buffer_size: usize,
|
||||
|
||||
/// Format d'encodage pour le streaming
|
||||
pub encoding: StreamEncoding,
|
||||
}
|
||||
|
||||
impl Default for ChromecastConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_address: "192.168.1.100".to_string(),
|
||||
device_name: "Living Room".to_string(),
|
||||
port: 8009,
|
||||
buffer_size: 50,
|
||||
encoding: StreamEncoding::Mp3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats d'encodage supportés pour le streaming
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum StreamEncoding {
|
||||
/// MP3 (compatible avec la plupart des Chromecasts)
|
||||
Mp3,
|
||||
/// AAC
|
||||
Aac,
|
||||
/// Opus
|
||||
Opus,
|
||||
/// PCM non compressé (haute qualité, bande passante élevée)
|
||||
Pcm,
|
||||
}
|
||||
|
||||
/// ChromecastSink - Diffuse vers un périphérique Chromecast
|
||||
///
|
||||
/// Ce sink encode le flux audio et le streame vers un Chromecast.
|
||||
/// La connexion est établie lors de l'initialisation et maintenue pendant toute la durée.
|
||||
///
|
||||
/// # Implémentation actuelle
|
||||
///
|
||||
/// Cette version est un mock qui simule l'envoi au Chromecast.
|
||||
/// Pour une vraie implémentation, il faudrait:
|
||||
/// - Utiliser une bibliothèque comme `rust-cast`
|
||||
/// - Établir une connexion TLS avec le device
|
||||
/// - Lancer une application de récepteur sur le Chromecast
|
||||
/// - Encoder l'audio dans le format approprié
|
||||
/// - Streamer via HTTP ou WebSocket
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::{ChromecastSink, ChromecastConfig};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let config = ChromecastConfig {
|
||||
/// device_address: "192.168.1.100".to_string(),
|
||||
/// device_name: "Living Room".to_string(),
|
||||
/// ..Default::default()
|
||||
/// };
|
||||
///
|
||||
/// let (sink, sink_tx) = ChromecastSink::new("chromecast1".to_string(), config, 10);
|
||||
///
|
||||
/// tokio::spawn(async move {
|
||||
/// sink.run().await.unwrap()
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
pub struct ChromecastSink {
|
||||
/// Identifiant du sink
|
||||
node_id: String,
|
||||
|
||||
/// Channel pour recevoir les chunks audio
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
|
||||
/// Configuration
|
||||
config: ChromecastConfig,
|
||||
|
||||
/// État de la connexion (mock)
|
||||
connected: bool,
|
||||
}
|
||||
|
||||
impl ChromecastSink {
|
||||
/// Crée un nouveau ChromecastSink
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `node_id` - Identifiant unique du sink
|
||||
/// * `config` - Configuration du Chromecast
|
||||
/// * `channel_size` - Taille du buffer du channel
|
||||
pub fn new(
|
||||
node_id: String,
|
||||
config: ChromecastConfig,
|
||||
channel_size: usize,
|
||||
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let sink = Self {
|
||||
node_id,
|
||||
rx,
|
||||
config,
|
||||
connected: false,
|
||||
};
|
||||
|
||||
(sink, tx)
|
||||
}
|
||||
|
||||
/// Établit la connexion avec le Chromecast (mock)
|
||||
async fn connect(&mut self) -> Result<(), AudioError> {
|
||||
println!(
|
||||
"[{}] Connecting to Chromecast '{}' at {}:{}...",
|
||||
self.node_id, self.config.device_name, self.config.device_address, self.config.port
|
||||
);
|
||||
|
||||
// Simuler une connexion
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
self.connected = true;
|
||||
|
||||
println!(
|
||||
"[{}] Connected to Chromecast '{}' successfully",
|
||||
self.node_id, self.config.device_name
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Envoie un chunk au Chromecast (mock)
|
||||
async fn send_chunk(&self, _chunk: &AudioChunk) -> Result<(), AudioError> {
|
||||
if !self.connected {
|
||||
return Err(AudioError::ProcessingError(
|
||||
"Not connected to Chromecast".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Dans une vraie implémentation:
|
||||
// 1. Appliquer le gain
|
||||
// 2. Encoder dans le format approprié (MP3, AAC, etc.)
|
||||
// 3. Envoyer via le protocole Chromecast
|
||||
|
||||
// Pour l'instant, simplement simuler un délai d'envoi
|
||||
tokio::time::sleep(tokio::time::Duration::from_micros(50)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Déconnecte proprement du Chromecast (mock)
|
||||
async fn disconnect(&mut self) -> Result<(), AudioError> {
|
||||
if self.connected {
|
||||
println!(
|
||||
"[{}] Disconnecting from Chromecast '{}'...",
|
||||
self.node_id, self.config.device_name
|
||||
);
|
||||
|
||||
// Simuler la déconnexion
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||
|
||||
self.connected = false;
|
||||
|
||||
println!("[{}] Disconnected successfully", self.node_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Démarre la boucle de traitement du ChromecastSink
|
||||
pub async fn run(mut self) -> Result<ChromecastStats, AudioError> {
|
||||
// Établir la connexion
|
||||
self.connect().await?;
|
||||
|
||||
let mut stats = ChromecastStats::new(self.node_id.clone(), self.config.device_name.clone());
|
||||
|
||||
// Boucle principale
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
// Appliquer le gain si nécessaire
|
||||
let chunk_to_send = if chunk.gain_db().abs() > f64::EPSILON {
|
||||
Arc::clone(&chunk).apply_gain()
|
||||
} else {
|
||||
Arc::clone(&chunk)
|
||||
};
|
||||
|
||||
// Envoyer au Chromecast
|
||||
self.send_chunk(&chunk_to_send).await?;
|
||||
|
||||
stats.record_chunk(&chunk_to_send);
|
||||
}
|
||||
|
||||
// Déconnexion propre
|
||||
self.disconnect().await?;
|
||||
|
||||
stats.finalize();
|
||||
Ok(stats)
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques du ChromecastSink
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChromecastStats {
|
||||
pub node_id: String,
|
||||
pub device_name: String,
|
||||
pub chunks_sent: u64,
|
||||
pub total_samples: u64,
|
||||
pub total_duration_sec: f64,
|
||||
}
|
||||
|
||||
impl ChromecastStats {
|
||||
pub fn new(node_id: String, device_name: String) -> Self {
|
||||
Self {
|
||||
node_id,
|
||||
device_name,
|
||||
chunks_sent: 0,
|
||||
total_samples: 0,
|
||||
total_duration_sec: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_chunk(&mut self, chunk: &AudioChunk) {
|
||||
self.chunks_sent += 1;
|
||||
self.total_samples += chunk.len() as u64;
|
||||
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
|
||||
}
|
||||
|
||||
pub fn finalize(&mut self) {
|
||||
// Calculs finaux si nécessaire
|
||||
}
|
||||
|
||||
pub fn display(&self) {
|
||||
println!("\n=== Chromecast Statistics: {} ===", self.node_id);
|
||||
println!("Device: {}", self.device_name);
|
||||
println!("Chunks sent: {}", self.chunks_sent);
|
||||
println!("Total samples: {}", self.total_samples);
|
||||
println!("Total duration: {:.3} sec", self.total_duration_sec);
|
||||
println!("==================================\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::i32;
|
||||
|
||||
use super::*;
|
||||
use crate::BitDepth;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_chromecast_sink_basic() {
|
||||
let config = ChromecastConfig {
|
||||
device_address: "127.0.0.1".to_string(),
|
||||
device_name: "Test Device".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (sink, tx) = ChromecastSink::new("test".to_string(), config, 10);
|
||||
|
||||
let handle = tokio::spawn(async move { sink.run().await });
|
||||
|
||||
// Envoyer quelques chunks
|
||||
for i in 0..5 {
|
||||
let stereo = vec![[i32::MAX / 2; 2]; 1000];
|
||||
let chunk = AudioChunk::new(i, stereo, 48000, BitDepth::B24);
|
||||
tx.send(chunk).await.unwrap();
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
|
||||
let stats = handle.await.unwrap().unwrap();
|
||||
assert_eq!(stats.chunks_sent, 5);
|
||||
assert_eq!(stats.device_name, "Test Device");
|
||||
}
|
||||
}
|
||||
162
old_code/pmoaudio/nodes/decoder_node.rs
Normal file
162
old_code/pmoaudio/nodes/decoder_node.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use crate::{
|
||||
nodes::{AudioError, MultiSubscriberNode},
|
||||
AudioChunk,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// DecoderNode - Décode des chunks audio
|
||||
///
|
||||
/// Version mock qui passe simplement les chunks (ou simule un décodage simple)
|
||||
pub struct DecoderNode {
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
impl DecoderNode {
|
||||
pub fn new(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
};
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Mode passthrough - passe les chunks sans modification
|
||||
pub async fn run_passthrough(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
self.subscribers.push(chunk).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mode mock décodage - simule un changement de sample rate
|
||||
pub async fn run_with_resampling(mut self, target_sample_rate: u32) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
if chunk.sample_rate() == target_sample_rate {
|
||||
// Pas besoin de resampling
|
||||
self.subscribers.push(chunk).await?;
|
||||
} else {
|
||||
// Simuler un resampling (mock simple)
|
||||
let ratio = target_sample_rate as f64 / chunk.sample_rate() as f64;
|
||||
let new_len = (chunk.len() as f64 * ratio) as usize;
|
||||
|
||||
let pairs = chunk.to_pairs_f32();
|
||||
let mut resampled = Vec::with_capacity(new_len);
|
||||
|
||||
// Resampling linéaire simple (mock)
|
||||
for i in 0..new_len {
|
||||
let src_pos = i as f64 / ratio;
|
||||
let src_idx = src_pos as usize;
|
||||
|
||||
if src_idx + 1 < pairs.len() {
|
||||
let frac = src_pos - src_idx as f64;
|
||||
let alpha = (1.0 - frac) as f32;
|
||||
let beta = frac as f32;
|
||||
let left_sample = pairs[src_idx][0] * alpha + pairs[src_idx + 1][0] * beta;
|
||||
let right_sample = pairs[src_idx][1] * alpha + pairs[src_idx + 1][1] * beta;
|
||||
|
||||
resampled.push([left_sample, right_sample]);
|
||||
} else if src_idx < pairs.len() {
|
||||
resampled.push(pairs[src_idx]);
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_chunk = AudioChunk::from_pairs_f32(
|
||||
chunk.order(),
|
||||
resampled,
|
||||
target_sample_rate,
|
||||
chunk.bit_depth(),
|
||||
);
|
||||
if chunk.gain_db().abs() > f64::EPSILON {
|
||||
new_chunk = new_chunk.set_gain_db(chunk.gain_db());
|
||||
}
|
||||
self.subscribers.push(new_chunk).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::BitDepth;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_decoder_passthrough() {
|
||||
let (mut node, tx) = DecoderNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run_passthrough().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk
|
||||
let chunk = AudioChunk::from_channels_f32(
|
||||
0,
|
||||
vec![1.0, 2.0, 3.0],
|
||||
vec![4.0, 5.0, 6.0],
|
||||
48000,
|
||||
BitDepth::B24,
|
||||
);
|
||||
tx.send(chunk.clone()).await.unwrap();
|
||||
|
||||
// Recevoir le chunk
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
assert!(Arc::ptr_eq(&chunk, &received));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_decoder_resampling() {
|
||||
let (mut node, tx) = DecoderNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run_with_resampling(96000).await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk à 48000 Hz
|
||||
let chunk =
|
||||
AudioChunk::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
|
||||
tx.send(chunk).await.unwrap();
|
||||
|
||||
// Recevoir le chunk resampleé
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
assert_eq!(received.sample_rate(), 96000);
|
||||
// Le chunk devrait être environ 2x plus grand
|
||||
assert!(received.len() > 150 && received.len() < 250);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_decoder_no_resampling_needed() {
|
||||
let (mut node, tx) = DecoderNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run_with_resampling(48000).await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk déjà au bon sample rate
|
||||
let chunk =
|
||||
AudioChunk::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
|
||||
tx.send(chunk.clone()).await.unwrap();
|
||||
|
||||
// Le chunk devrait être passé sans modification
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
assert!(Arc::ptr_eq(&chunk, &received));
|
||||
}
|
||||
}
|
||||
500
old_code/pmoaudio/nodes/disk_sink.rs
Normal file
500
old_code/pmoaudio/nodes/disk_sink.rs
Normal file
@@ -0,0 +1,500 @@
|
||||
//! DiskSink - Écrit le flux audio dans un fichier
|
||||
//!
|
||||
//! 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 std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
|
||||
/// Configuration pour le DiskSink
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiskSinkConfig {
|
||||
/// Chemin racine où écrire les fichiers
|
||||
pub output_dir: PathBuf,
|
||||
|
||||
/// Nom de fichier explicite (optionnel)
|
||||
/// Si None, sera dérivé du nom de la source
|
||||
pub filename: Option<String>,
|
||||
|
||||
/// Format d'écriture
|
||||
pub format: AudioFileFormat,
|
||||
|
||||
/// Taille du buffer d'écriture (en chunks)
|
||||
pub buffer_size: usize,
|
||||
}
|
||||
|
||||
impl Default for DiskSinkConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
output_dir: PathBuf::from("."),
|
||||
filename: None,
|
||||
format: AudioFileFormat::Wav,
|
||||
buffer_size: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats de fichiers audio supportés
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum AudioFileFormat {
|
||||
/// Format WAV (non compressé)
|
||||
Wav,
|
||||
/// Format FLAC (compressé sans perte)
|
||||
Flac,
|
||||
/// Format brut PCM
|
||||
Raw,
|
||||
}
|
||||
|
||||
impl AudioFileFormat {
|
||||
/// Retourne l'extension de fichier appropriée
|
||||
pub fn extension(&self) -> &str {
|
||||
match self {
|
||||
AudioFileFormat::Wav => "wav",
|
||||
AudioFileFormat::Flac => "flac",
|
||||
AudioFileFormat::Raw => "pcm",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DiskSink - Écrit le flux audio dans un fichier sur disque
|
||||
///
|
||||
/// Ce sink consomme les chunks audio et les écrit dans un fichier.
|
||||
/// Le nom du fichier peut être dérivé automatiquement du nom de la source
|
||||
/// via les événements `SourceNameUpdateEvent`.
|
||||
///
|
||||
/// # Caractéristiques
|
||||
///
|
||||
/// - Écriture asynchrone avec buffer
|
||||
/// - Dérivation automatique du nom de fichier depuis la source
|
||||
/// - Support de plusieurs formats (WAV, FLAC, PCM brut)
|
||||
/// - Gestion du gain : applique le gain avant l'écriture
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::{DiskSink, DiskSinkConfig};
|
||||
/// use std::path::PathBuf;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let config = DiskSinkConfig {
|
||||
/// output_dir: PathBuf::from("/tmp/audio"),
|
||||
/// filename: Some("output.wav".to_string()),
|
||||
/// ..Default::default()
|
||||
/// };
|
||||
///
|
||||
/// let (sink, sink_tx) = DiskSink::new("disk1".to_string(), config, 10);
|
||||
///
|
||||
/// tokio::spawn(async move {
|
||||
/// sink.run().await.unwrap()
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
pub struct DiskSink {
|
||||
/// Identifiant du sink
|
||||
node_id: String,
|
||||
|
||||
/// Channel pour recevoir les chunks audio
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
|
||||
/// Configuration
|
||||
config: DiskSinkConfig,
|
||||
|
||||
/// Nom de fichier résolu (partagé)
|
||||
resolved_filename: Arc<RwLock<Option<PathBuf>>>,
|
||||
|
||||
/// Receiver pour les événements de nom de source (optionnel)
|
||||
source_name_rx: Option<mpsc::Receiver<SourceNameUpdateEvent>>,
|
||||
|
||||
/// Writer pour le fichier
|
||||
writer: Option<AudioFileWriter>,
|
||||
}
|
||||
|
||||
impl DiskSink {
|
||||
/// Crée un nouveau DiskSink
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `node_id` - Identifiant unique du sink
|
||||
/// * `config` - Configuration du sink
|
||||
/// * `channel_size` - Taille du buffer du channel
|
||||
pub fn new(
|
||||
node_id: String,
|
||||
config: DiskSinkConfig,
|
||||
channel_size: usize,
|
||||
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let sink = Self {
|
||||
node_id,
|
||||
rx,
|
||||
config,
|
||||
resolved_filename: Arc::new(RwLock::new(None)),
|
||||
source_name_rx: None,
|
||||
writer: None,
|
||||
};
|
||||
|
||||
(sink, tx)
|
||||
}
|
||||
|
||||
/// Configure la source des événements de nom de source
|
||||
pub fn set_source_name_source(&mut self, rx: mpsc::Receiver<SourceNameUpdateEvent>) {
|
||||
self.source_name_rx = Some(rx);
|
||||
}
|
||||
|
||||
/// Résout le nom du fichier de sortie
|
||||
///
|
||||
/// Si un filename explicite est fourni dans la config, l'utilise.
|
||||
/// Sinon, utilise le source_name avec l'extension appropriée.
|
||||
fn resolve_filename(&self, source_name: Option<&str>) -> PathBuf {
|
||||
let filename = if let Some(ref explicit_name) = self.config.filename {
|
||||
explicit_name.clone()
|
||||
} else if let Some(name) = source_name {
|
||||
// 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 {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
|
||||
format!("{}.{}", clean_name, self.config.format.extension())
|
||||
} else {
|
||||
// Fallback sur un nom par défaut
|
||||
format!("{}.{}", self.node_id, self.config.format.extension())
|
||||
};
|
||||
|
||||
self.config.output_dir.join(filename)
|
||||
}
|
||||
|
||||
/// Initialise le writer pour le fichier de sortie
|
||||
async fn initialize_writer(&mut self, source_name: Option<&str>) -> Result<(), AudioError> {
|
||||
let path = self.resolve_filename(source_name);
|
||||
*self.resolved_filename.write().await = Some(path.clone());
|
||||
|
||||
// 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))
|
||||
})?;
|
||||
}
|
||||
|
||||
// Créer le writer approprié selon le format
|
||||
let writer = match self.config.format {
|
||||
AudioFileFormat::Wav => AudioFileWriter::new_wav(path).await?,
|
||||
AudioFileFormat::Flac => {
|
||||
// FLAC nécessiterait une bibliothèque externe, pour l'instant utiliser WAV
|
||||
AudioFileWriter::new_wav(path).await?
|
||||
}
|
||||
AudioFileFormat::Raw => AudioFileWriter::new_raw(path).await?,
|
||||
};
|
||||
|
||||
self.writer = Some(writer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Démarre la boucle de traitement du DiskSink
|
||||
pub async fn run(mut self) -> Result<DiskSinkStats, AudioError> {
|
||||
let mut stats = DiskSinkStats::new(self.node_id.clone());
|
||||
let mut source_name: Option<String> = None;
|
||||
let mut initialized = false;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Recevoir les chunks audio
|
||||
chunk_opt = self.rx.recv() => {
|
||||
match chunk_opt {
|
||||
Some(chunk) => {
|
||||
// Initialiser le writer à la réception du premier chunk
|
||||
if !initialized {
|
||||
self.initialize_writer(source_name.as_deref()).await?;
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
// Appliquer le gain avant l'écriture
|
||||
let chunk_with_gain = if chunk.gain_db().abs() > f64::EPSILON {
|
||||
Arc::clone(&chunk).apply_gain()
|
||||
} else {
|
||||
Arc::clone(&chunk)
|
||||
};
|
||||
|
||||
// Écrire le chunk
|
||||
if let Some(ref mut writer) = self.writer {
|
||||
writer.write_chunk(&chunk_with_gain).await?;
|
||||
stats.record_chunk(&chunk_with_gain);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Channel fermé, terminer
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recevoir les mises à jour du nom de source
|
||||
source_event_opt = async {
|
||||
if let Some(ref mut rx) = self.source_name_rx {
|
||||
rx.recv().await
|
||||
} else {
|
||||
std::future::pending().await
|
||||
}
|
||||
} => {
|
||||
if let Some(event) = source_event_opt {
|
||||
source_name = Some(event.source_name.clone());
|
||||
|
||||
// Si on n'a pas encore initialisé, le nom sera utilisé plus tard
|
||||
// Sinon, on pourrait décider de fermer le fichier actuel et d'en créer un nouveau
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fermer le fichier proprement
|
||||
if let Some(writer) = self.writer {
|
||||
writer.close().await?;
|
||||
}
|
||||
|
||||
stats.finalize();
|
||||
Ok(stats)
|
||||
}
|
||||
}
|
||||
|
||||
/// Writer pour fichiers audio
|
||||
struct AudioFileWriter {
|
||||
file: File,
|
||||
format: AudioFileFormat,
|
||||
sample_rate: Option<u32>,
|
||||
total_samples: usize,
|
||||
}
|
||||
|
||||
impl AudioFileWriter {
|
||||
/// Crée un writer WAV
|
||||
async fn new_wav(path: PathBuf) -> Result<Self, AudioError> {
|
||||
let file = File::create(path)
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Failed to create file: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
file,
|
||||
format: AudioFileFormat::Wav,
|
||||
sample_rate: None,
|
||||
total_samples: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Crée un writer pour PCM brut
|
||||
async fn new_raw(path: PathBuf) -> Result<Self, AudioError> {
|
||||
let file = File::create(path)
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Failed to create file: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
file,
|
||||
format: AudioFileFormat::Raw,
|
||||
sample_rate: None,
|
||||
total_samples: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Écrit un chunk audio
|
||||
async fn write_chunk(&mut self, chunk: &AudioChunk) -> Result<(), AudioError> {
|
||||
// Enregistrer le sample rate du premier chunk
|
||||
if self.sample_rate.is_none() {
|
||||
let sr = chunk.sample_rate();
|
||||
self.sample_rate = Some(sr);
|
||||
|
||||
// Pour WAV, écrire l'en-tête (simplifié)
|
||||
if matches!(self.format, AudioFileFormat::Wav) {
|
||||
self.write_wav_header(sr).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Convertir en bytes (little-endian 16-bit PCM)
|
||||
let mut bytes = Vec::with_capacity(chunk.len() * 4);
|
||||
let max_val = chunk.bit_depth().max_value();
|
||||
for frame in chunk.frames() {
|
||||
let left = (frame[0] as f32 / max_val).clamp(-1.0, 1.0);
|
||||
let right = (frame[1] as f32 / max_val).clamp(-1.0, 1.0);
|
||||
let sample_i16 = (left * 32767.0) as i16;
|
||||
bytes.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
let sample_r16 = (right * 32767.0) as i16;
|
||||
bytes.extend_from_slice(&sample_r16.to_le_bytes());
|
||||
}
|
||||
|
||||
self.file.write_all(&bytes).await.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("Failed to write audio data: {}", e))
|
||||
})?;
|
||||
|
||||
self.total_samples += chunk.len();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Écrit un en-tête WAV simplifié
|
||||
async fn write_wav_header(&mut self, sample_rate: u32) -> Result<(), AudioError> {
|
||||
// En-tête WAV basique (sera mis à jour à la fermeture)
|
||||
let mut header = Vec::new();
|
||||
|
||||
// RIFF chunk
|
||||
header.extend_from_slice(b"RIFF");
|
||||
header.extend_from_slice(&0u32.to_le_bytes()); // Taille (à mettre à jour)
|
||||
header.extend_from_slice(b"WAVE");
|
||||
|
||||
// fmt chunk
|
||||
header.extend_from_slice(b"fmt ");
|
||||
header.extend_from_slice(&16u32.to_le_bytes()); // Taille du fmt chunk
|
||||
header.extend_from_slice(&1u16.to_le_bytes()); // Format PCM
|
||||
header.extend_from_slice(&2u16.to_le_bytes()); // 2 canaux (stéréo)
|
||||
header.extend_from_slice(&sample_rate.to_le_bytes());
|
||||
header.extend_from_slice(&(sample_rate * 4).to_le_bytes()); // Byte rate
|
||||
header.extend_from_slice(&4u16.to_le_bytes()); // Block align
|
||||
header.extend_from_slice(&16u16.to_le_bytes()); // Bits per sample
|
||||
|
||||
// data chunk header
|
||||
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))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ferme le fichier et met à jour l'en-tête si nécessaire
|
||||
async fn close(mut self) -> Result<(), AudioError> {
|
||||
if matches!(self.format, AudioFileFormat::Wav) {
|
||||
// Mettre à jour les tailles dans l'en-tête WAV
|
||||
let data_size = (self.total_samples * 4) as u32; // 2 bytes per sample * 2 channels
|
||||
let file_size = data_size + 36;
|
||||
|
||||
// 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(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)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques du DiskSink
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DiskSinkStats {
|
||||
pub node_id: String,
|
||||
pub chunks_written: u64,
|
||||
pub total_samples: u64,
|
||||
pub total_duration_sec: f64,
|
||||
}
|
||||
|
||||
impl DiskSinkStats {
|
||||
pub fn new(node_id: String) -> Self {
|
||||
Self {
|
||||
node_id,
|
||||
chunks_written: 0,
|
||||
total_samples: 0,
|
||||
total_duration_sec: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_chunk(&mut self, chunk: &AudioChunk) {
|
||||
self.chunks_written += 1;
|
||||
self.total_samples += chunk.len() as u64;
|
||||
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
|
||||
}
|
||||
|
||||
pub fn finalize(&mut self) {
|
||||
// Pourrait effectuer des calculs finaux ici
|
||||
}
|
||||
|
||||
pub fn display(&self) {
|
||||
println!("\n=== DiskSink Statistics: {} ===", self.node_id);
|
||||
println!("Chunks written: {}", self.chunks_written);
|
||||
println!("Total samples: {}", self.total_samples);
|
||||
println!("Total duration: {:.3} sec", self.total_duration_sec);
|
||||
println!("============================\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::BitDepth;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_disk_sink_basic() {
|
||||
let temp_dir = std::env::temp_dir().join("pmoaudio_test");
|
||||
tokio::fs::create_dir_all(&temp_dir).await.unwrap();
|
||||
|
||||
let config = DiskSinkConfig {
|
||||
output_dir: temp_dir.clone(),
|
||||
filename: Some("test_output.wav".to_string()),
|
||||
format: AudioFileFormat::Wav,
|
||||
buffer_size: 10,
|
||||
};
|
||||
|
||||
let (sink, tx) = DiskSink::new("test".to_string(), config, 10);
|
||||
|
||||
let handle = tokio::spawn(async move { sink.run().await });
|
||||
|
||||
// Envoyer quelques chunks
|
||||
for i in 0..5 {
|
||||
let chunk = AudioChunk::from_channels_f32(
|
||||
i,
|
||||
vec![0.5; 1000],
|
||||
vec![0.5; 1000],
|
||||
48000,
|
||||
BitDepth::B24,
|
||||
);
|
||||
tx.send(chunk).await.unwrap();
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
|
||||
let stats = handle.await.unwrap().unwrap();
|
||||
assert_eq!(stats.chunks_written, 5);
|
||||
|
||||
// Vérifier que le fichier existe
|
||||
let output_path = temp_dir.join("test_output.wav");
|
||||
assert!(output_path.exists());
|
||||
|
||||
// Nettoyage
|
||||
tokio::fs::remove_file(output_path).await.ok();
|
||||
tokio::fs::remove_dir(temp_dir).await.ok();
|
||||
}
|
||||
}
|
||||
254
old_code/pmoaudio/nodes/dsp_node.rs
Normal file
254
old_code/pmoaudio/nodes/dsp_node.rs
Normal file
@@ -0,0 +1,254 @@
|
||||
use crate::{
|
||||
nodes::{AudioError, MultiSubscriberNode},
|
||||
AudioChunk,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// DspNode - Applique des transformations DSP aux chunks audio
|
||||
///
|
||||
/// Clone les données uniquement si elles doivent être modifiées
|
||||
pub struct DspNode {
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
gain_db: f32,
|
||||
}
|
||||
|
||||
impl DspNode {
|
||||
pub fn new(channel_size: usize, gain_db: f32) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
gain_db,
|
||||
};
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Applique le gain aux chunks
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
if self.gain_db.abs() < f32::EPSILON {
|
||||
// Gain = 0 dB, pas de transformation nécessaire
|
||||
self.subscribers.push(chunk).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let gain_linear = AudioChunk::gain_linear_from_db(self.gain_db as f64) as f32;
|
||||
let mut pairs = chunk.to_pairs_f32();
|
||||
for frame in &mut pairs {
|
||||
frame[0] *= gain_linear;
|
||||
frame[1] *= gain_linear;
|
||||
}
|
||||
|
||||
let mut new_chunk = AudioChunk::from_pairs_f32(
|
||||
chunk.order(),
|
||||
pairs,
|
||||
chunk.sample_rate(),
|
||||
chunk.bit_depth(),
|
||||
);
|
||||
if chunk.gain_db().abs() > f64::EPSILON {
|
||||
new_chunk = new_chunk.set_gain_db(chunk.gain_db());
|
||||
}
|
||||
self.subscribers.push(new_chunk).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Met à jour le gain dynamiquement (nécessite un `Arc<RwLock<f32>>` dans une version réelle)
|
||||
pub fn set_gain_db(&mut self, gain_db: f32) {
|
||||
self.gain_db = gain_db;
|
||||
}
|
||||
}
|
||||
|
||||
/// DspNode avec filtre passe-bas simple (mock)
|
||||
#[allow(dead_code)]
|
||||
pub struct LowPassDspNode {
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
alpha: f32, // Coefficient du filtre
|
||||
prev_left: f32,
|
||||
prev_right: f32,
|
||||
}
|
||||
|
||||
impl LowPassDspNode {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(channel_size: usize, cutoff_ratio: f32) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
// Filtre RC simple: alpha = dt / (RC + dt)
|
||||
// cutoff_ratio entre 0 (tout couper) et 1 (tout passer)
|
||||
let alpha = cutoff_ratio.clamp(0.0, 1.0);
|
||||
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
alpha,
|
||||
prev_left: 0.0,
|
||||
prev_right: 0.0,
|
||||
};
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
let pairs = chunk.to_pairs_f32();
|
||||
let mut filtered = Vec::with_capacity(pairs.len());
|
||||
|
||||
for sample in pairs.iter() {
|
||||
self.prev_left = self.prev_left + self.alpha * (sample[0] - self.prev_left);
|
||||
self.prev_right = self.prev_right + self.alpha * (sample[1] - self.prev_right);
|
||||
filtered.push([self.prev_left, self.prev_right]);
|
||||
}
|
||||
|
||||
let mut new_chunk = AudioChunk::from_pairs_f32(
|
||||
chunk.order(),
|
||||
filtered,
|
||||
chunk.sample_rate(),
|
||||
chunk.bit_depth(),
|
||||
);
|
||||
if chunk.gain_db().abs() > f64::EPSILON {
|
||||
new_chunk = new_chunk.set_gain_db(chunk.gain_db());
|
||||
}
|
||||
|
||||
self.subscribers.push(new_chunk).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::BitDepth;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dsp_node_unity_gain() {
|
||||
let (mut node, tx) = DspNode::new(10, 0.0);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk
|
||||
let chunk = AudioChunk::from_channels_f32(
|
||||
0,
|
||||
vec![0.25, 0.5, 0.75],
|
||||
vec![0.1, 0.2, 0.3],
|
||||
48000,
|
||||
BitDepth::B24,
|
||||
);
|
||||
tx.send(chunk.clone()).await.unwrap();
|
||||
|
||||
// Avec gain = 1.0, le chunk ne devrait pas être cloné
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
assert!(Arc::ptr_eq(&chunk, &received));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dsp_node_gain() {
|
||||
let gain_db = AudioChunk::gain_db_from_linear(2.0) as f32;
|
||||
let (mut node, tx) = DspNode::new(10, gain_db);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk
|
||||
let chunk = AudioChunk::from_channels_f32(
|
||||
0,
|
||||
vec![0.25, 0.5, 0.75],
|
||||
vec![0.1, 0.2, 0.3],
|
||||
48000,
|
||||
BitDepth::B24,
|
||||
);
|
||||
tx.send(chunk).await.unwrap();
|
||||
|
||||
// Vérifier que le gain a été appliqué
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
let frames = received.to_pairs_f32();
|
||||
const EPS: f32 = 1e-3;
|
||||
assert!((frames[0][0] - 0.5).abs() < EPS);
|
||||
assert!((frames[1][0] - 1.0).abs() < EPS);
|
||||
assert!((frames[2][0] - 1.0).abs() < EPS); // Clamp at full scale
|
||||
assert!((frames[0][1] - 0.2).abs() < EPS);
|
||||
assert!((frames[1][1] - 0.4).abs() < EPS);
|
||||
assert!((frames[2][1] - 0.6).abs() < EPS);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lowpass_dsp_node() {
|
||||
let (mut node, tx) = LowPassDspNode::new(10, 0.5);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk avec un signal carré
|
||||
let chunk = AudioChunk::from_channels_f32(
|
||||
0,
|
||||
vec![1.0, 1.0, 1.0, -1.0, -1.0, -1.0],
|
||||
vec![1.0, 1.0, 1.0, -1.0, -1.0, -1.0],
|
||||
48000,
|
||||
BitDepth::B24,
|
||||
);
|
||||
tx.send(chunk).await.unwrap();
|
||||
|
||||
// Le filtre devrait lisser le signal
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
let frames = received.to_pairs_f32();
|
||||
assert!(frames[0][0].abs() < 1.0); // Premier échantillon lissé
|
||||
assert!(frames[2][0].abs() < 1.0); // Signal ne devrait pas atteindre 1.0 immédiatement
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dsp_node_multiple_subscribers() {
|
||||
let gain_db = AudioChunk::gain_db_from_linear(0.5) as f32;
|
||||
let (mut node, tx) = DspNode::new(10, gain_db);
|
||||
let (out_tx1, mut out_rx1) = mpsc::channel(10);
|
||||
let (out_tx2, mut out_rx2) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx1);
|
||||
node.add_subscriber(out_tx2);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
let chunk =
|
||||
AudioChunk::from_channels_f32(0, vec![0.8, 0.4], vec![0.8, 0.4], 48000, BitDepth::B24);
|
||||
tx.send(chunk).await.unwrap();
|
||||
|
||||
// Les deux abonnés devraient recevoir le même Arc
|
||||
let received1 = out_rx1.recv().await.unwrap();
|
||||
let received2 = out_rx2.recv().await.unwrap();
|
||||
|
||||
assert!(Arc::ptr_eq(&received1, &received2));
|
||||
let frames = received1.to_pairs_f32();
|
||||
const EPS: f32 = 1e-3;
|
||||
assert!((frames[0][0] - 0.4).abs() < EPS); // 0.8 * 0.5
|
||||
assert!((frames[1][0] - 0.2).abs() < EPS); // 0.4 * 0.5
|
||||
}
|
||||
}
|
||||
244
old_code/pmoaudio/nodes/file_source.rs
Normal file
244
old_code/pmoaudio/nodes/file_source.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use crate::{
|
||||
nodes::{AudioError, MultiSubscriberNode},
|
||||
AudioChunk, BitDepth,
|
||||
};
|
||||
use pmoflac::{decode_audio_stream, StreamInfo};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use tokio::{fs::File, io::AsyncReadExt, sync::mpsc};
|
||||
|
||||
/// FileSource - Lit un fichier audio et publie des `AudioChunk`
|
||||
///
|
||||
/// Cette source utilise `pmoflac` pour décoder le fichier (FLAC/MP3/OGG/WAV/AIFF)
|
||||
/// puis transforme les échantillons PCM en `AudioChunk` stéréo.
|
||||
pub struct FileSource {
|
||||
path: PathBuf,
|
||||
chunk_frames: usize,
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
impl FileSource {
|
||||
/// Crée une nouvelle source de fichier.
|
||||
///
|
||||
/// * `path` - chemin du fichier audio à lire
|
||||
/// * `chunk_frames` - nombre d'échantillons par canal par chunk
|
||||
pub fn new<P: Into<PathBuf>>(path: P, chunk_frames: usize) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
chunk_frames: chunk_frames.max(1),
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ajoute un abonné qui recevra les chunks décodés.
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Lance la lecture du fichier et diffuse les chunks.
|
||||
pub async fn run(self) -> Result<(), AudioError> {
|
||||
let file = File::open(&self.path).await.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("Failed to open {:?}: {}", self.path, e))
|
||||
})?;
|
||||
|
||||
let mut stream = decode_audio_stream(file)
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Decode error: {}", e)))?;
|
||||
let stream_info = stream.info().clone();
|
||||
|
||||
validate_stream(&stream_info)?;
|
||||
|
||||
let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize;
|
||||
let chunk_byte_len = self.chunk_frames * frame_bytes;
|
||||
let mut pending = Vec::new();
|
||||
let mut read_buf = vec![0u8; frame_bytes * 512.max(self.chunk_frames)];
|
||||
let mut chunk_index = 0u64;
|
||||
|
||||
loop {
|
||||
if pending.len() < chunk_byte_len {
|
||||
let read = stream.read(&mut read_buf).await.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("I/O error while decoding: {}", e))
|
||||
})?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
pending.extend_from_slice(&read_buf[..read]);
|
||||
}
|
||||
|
||||
if pending.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
let frames_in_pending = pending.len() / frame_bytes;
|
||||
let frames_to_emit = frames_in_pending.min(self.chunk_frames);
|
||||
let take_bytes = frames_to_emit * frame_bytes;
|
||||
let chunk_bytes = pending.drain(..take_bytes).collect::<Vec<u8>>();
|
||||
|
||||
let chunk = bytes_to_chunk(&chunk_bytes, &stream_info, frames_to_emit, chunk_index)?;
|
||||
self.subscribers.push(chunk).await?;
|
||||
chunk_index += 1;
|
||||
}
|
||||
|
||||
// Reste éventuel (moins qu'un chunk complet)
|
||||
if !pending.is_empty() {
|
||||
let frames = pending.len() / frame_bytes;
|
||||
if frames > 0 {
|
||||
let chunk = bytes_to_chunk(&pending, &stream_info, frames, chunk_index)?;
|
||||
self.subscribers.push(chunk).await?;
|
||||
}
|
||||
}
|
||||
|
||||
stream
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_stream(info: &StreamInfo) -> Result<(), AudioError> {
|
||||
if !(1..=2).contains(&info.channels) {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"Unsupported channel count: {}",
|
||||
info.channels
|
||||
)));
|
||||
}
|
||||
match info.bits_per_sample {
|
||||
8 | 16 | 24 | 32 => Ok(()),
|
||||
other => Err(AudioError::ProcessingError(format!(
|
||||
"Unsupported bit depth: {}",
|
||||
other
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_to_chunk(
|
||||
chunk_bytes: &[u8],
|
||||
info: &StreamInfo,
|
||||
frames: usize,
|
||||
order: u64,
|
||||
) -> Result<Arc<AudioChunk>, AudioError> {
|
||||
let bytes_per_sample = info.bytes_per_sample();
|
||||
let channels = info.channels as usize;
|
||||
let frame_bytes = bytes_per_sample * channels;
|
||||
|
||||
let mut left = Vec::with_capacity(frames);
|
||||
let mut right = Vec::with_capacity(frames);
|
||||
|
||||
for frame_idx in 0..frames {
|
||||
let base = frame_idx * frame_bytes;
|
||||
let l = sample_to_f32(
|
||||
&chunk_bytes[base..base + bytes_per_sample],
|
||||
info.bits_per_sample,
|
||||
)?;
|
||||
let r = if channels == 1 {
|
||||
l
|
||||
} else {
|
||||
sample_to_f32(
|
||||
&chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample],
|
||||
info.bits_per_sample,
|
||||
)?
|
||||
};
|
||||
|
||||
left.push(l);
|
||||
right.push(r);
|
||||
}
|
||||
|
||||
let bit_depth = BitDepth::from_u32_strict(info.bits_per_sample as u32);
|
||||
Ok(AudioChunk::from_channels_f32(
|
||||
order,
|
||||
left,
|
||||
right,
|
||||
info.sample_rate,
|
||||
bit_depth,
|
||||
))
|
||||
}
|
||||
|
||||
fn sample_to_f32(sample_bytes: &[u8], bits: u8) -> Result<f32, AudioError> {
|
||||
let sample = match bits {
|
||||
8 => i8::from_le_bytes([sample_bytes[0]]) as i32,
|
||||
16 => i16::from_le_bytes(sample_bytes.try_into().unwrap()) as i32,
|
||||
24 => {
|
||||
let mut buf = [0u8; 4];
|
||||
buf[..3].copy_from_slice(sample_bytes);
|
||||
// Sign extend manually
|
||||
if sample_bytes[2] & 0x80 != 0 {
|
||||
buf[3] = 0xFF;
|
||||
}
|
||||
i32::from_le_bytes(buf)
|
||||
}
|
||||
32 => i32::from_le_bytes(sample_bytes.try_into().unwrap()),
|
||||
other => {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"Unsupported bit depth: {}",
|
||||
other
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let max = ((1i64 << (bits as i64 - 1)).saturating_sub(1)) as f32;
|
||||
Ok((sample as f32) / max)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
|
||||
use std::io::Cursor;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_file_source_decodes_flac() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let flac_path = temp_dir.path().join("test.flac");
|
||||
|
||||
let sample_rate = 48_000;
|
||||
let frames = 256;
|
||||
let mut pcm = Vec::with_capacity(frames * 4);
|
||||
for i in 0..frames {
|
||||
let sample = ((i % 32) as f32 / 31.0 * 2.0 - 1.0) * 0.5; // simple ramp
|
||||
let sample_i16 = (sample * 32767.0) as i16;
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
pcm.extend_from_slice(&sample_i16.to_le_bytes());
|
||||
}
|
||||
|
||||
let format = PcmFormat {
|
||||
sample_rate,
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
};
|
||||
|
||||
let mut flac_stream =
|
||||
encode_flac_stream(Cursor::new(pcm.clone()), format, EncoderOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut file = File::create(&flac_path).await.expect("create flac file");
|
||||
tokio::io::copy(&mut flac_stream, &mut file)
|
||||
.await
|
||||
.expect("write flac");
|
||||
file.flush().await.expect("flush file");
|
||||
flac_stream.wait().await.unwrap();
|
||||
|
||||
let mut source = FileSource::new(&flac_path, 64);
|
||||
let (tx, mut rx) = mpsc::channel(4);
|
||||
source.add_subscriber(tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
source.run().await.unwrap();
|
||||
});
|
||||
|
||||
let mut received = 0usize;
|
||||
while let Some(chunk) = rx.recv().await {
|
||||
received += chunk.len();
|
||||
assert_eq!(chunk.sample_rate(), sample_rate);
|
||||
let scale = 1.0 / chunk.bit_depth().max_value();
|
||||
if let Some(frame) = chunk.frames().first() {
|
||||
assert!(((frame[0] as f32) * scale).abs() <= 1.0); // sample range sanity
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(received, frames);
|
||||
}
|
||||
}
|
||||
300
old_code/pmoaudio/nodes/flac_file_sink.rs
Normal file
300
old_code/pmoaudio/nodes/flac_file_sink.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
use crate::{nodes::AudioError, AudioChunk};
|
||||
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
path::PathBuf,
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{self, AsyncRead, AsyncWriteExt, ReadBuf},
|
||||
sync::mpsc,
|
||||
};
|
||||
|
||||
/// Sink qui encode les `AudioChunk` reçus au format FLAC.
|
||||
pub struct FlacFileSink {
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
path: PathBuf,
|
||||
encoder_options: EncoderOptions,
|
||||
pcm_buffer_capacity: usize,
|
||||
}
|
||||
|
||||
impl FlacFileSink {
|
||||
/// Crée un sink FLAC avec les options par défaut (compression 5).
|
||||
pub fn new<P: Into<PathBuf>>(
|
||||
path: P,
|
||||
channel_size: usize,
|
||||
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
Self::with_options(path, channel_size, EncoderOptions::default())
|
||||
}
|
||||
|
||||
/// Crée un sink FLAC avec des options explicites.
|
||||
pub fn with_options<P: Into<PathBuf>>(
|
||||
path: P,
|
||||
channel_size: usize,
|
||||
encoder_options: EncoderOptions,
|
||||
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
let sink = Self {
|
||||
rx,
|
||||
path: path.into(),
|
||||
encoder_options,
|
||||
pcm_buffer_capacity: 8,
|
||||
};
|
||||
(sink, tx)
|
||||
}
|
||||
|
||||
/// Lance l'encodage vers le fichier cible.
|
||||
pub async fn run(self) -> Result<FlacFileSinkStats, AudioError> {
|
||||
let FlacFileSink {
|
||||
mut rx,
|
||||
path,
|
||||
encoder_options,
|
||||
pcm_buffer_capacity,
|
||||
} = self;
|
||||
|
||||
let first_chunk = rx.recv().await.ok_or_else(|| {
|
||||
AudioError::ProcessingError("FlacFileSink: no audio data received".into())
|
||||
})?;
|
||||
|
||||
if first_chunk.len() == 0 {
|
||||
return Err(AudioError::ProcessingError(
|
||||
"FlacFileSink: received empty chunk".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let format = PcmFormat {
|
||||
sample_rate: first_chunk.sample_rate(),
|
||||
channels: 2,
|
||||
bits_per_sample: 16,
|
||||
};
|
||||
if let Err(err) = format.validate() {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"Invalid PCM format: {}",
|
||||
err
|
||||
)));
|
||||
}
|
||||
|
||||
let (pcm_tx, pcm_rx) = mpsc::channel::<Vec<u8>>(pcm_buffer_capacity);
|
||||
let pump_handle = tokio::spawn(pump_chunks(first_chunk, rx, pcm_tx));
|
||||
|
||||
let reader = ByteStreamReader::new(pcm_rx);
|
||||
let mut flac_stream = encode_flac_stream(reader, format, encoder_options)
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("FLAC encode init failed: {}", e)))?;
|
||||
|
||||
let mut output = File::create(&path).await.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("Failed to create {:?}: {}", path, e))
|
||||
})?;
|
||||
|
||||
tokio::io::copy(&mut flac_stream, &mut output)
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("FLAC write failed: {}", e)))?;
|
||||
output.flush().await.map_err(|e| {
|
||||
AudioError::ProcessingError(format!("Failed to flush {:?}: {}", path, e))
|
||||
})?;
|
||||
|
||||
flac_stream
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("FLAC encoder task failed: {}", e)))?;
|
||||
|
||||
let pump_stats = pump_handle
|
||||
.await
|
||||
.map_err(|e| AudioError::ProcessingError(format!("Pump task panicked: {}", e)))??;
|
||||
|
||||
Ok(FlacFileSinkStats {
|
||||
path,
|
||||
chunks_received: pump_stats.chunks,
|
||||
total_samples: pump_stats.samples,
|
||||
total_duration_sec: pump_stats.duration_sec,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct PumpStats {
|
||||
chunks: u64,
|
||||
samples: u64,
|
||||
duration_sec: f64,
|
||||
}
|
||||
|
||||
async fn pump_chunks(
|
||||
first_chunk: Arc<AudioChunk>,
|
||||
mut rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
pcm_tx: mpsc::Sender<Vec<u8>>,
|
||||
) -> Result<PumpStats, AudioError> {
|
||||
let mut chunks = 0u64;
|
||||
let mut samples = 0u64;
|
||||
let mut duration_sec = 0.0f64;
|
||||
let expected_rate = first_chunk.sample_rate();
|
||||
|
||||
let mut current = Some(first_chunk);
|
||||
|
||||
loop {
|
||||
let chunk_opt = if let Some(ch) = current.take() {
|
||||
Some(ch)
|
||||
} else {
|
||||
rx.recv().await
|
||||
};
|
||||
|
||||
let chunk = match chunk_opt {
|
||||
Some(ch) => ch,
|
||||
None => break,
|
||||
};
|
||||
|
||||
if chunk.sample_rate() != expected_rate {
|
||||
return Err(AudioError::ProcessingError(format!(
|
||||
"FlacFileSink: inconsistent sample rate ({} vs {})",
|
||||
chunk.sample_rate(),
|
||||
expected_rate
|
||||
)));
|
||||
}
|
||||
|
||||
let pcm_bytes = chunk_to_pcm_bytes(&chunk);
|
||||
if pcm_bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
pcm_tx
|
||||
.send(pcm_bytes)
|
||||
.await
|
||||
.map_err(|_| AudioError::SendError)?;
|
||||
|
||||
chunks += 1;
|
||||
samples += chunk.len() as u64;
|
||||
duration_sec += chunk.len() as f64 / expected_rate as f64;
|
||||
}
|
||||
|
||||
Ok(PumpStats {
|
||||
chunks,
|
||||
samples,
|
||||
duration_sec,
|
||||
})
|
||||
}
|
||||
|
||||
fn chunk_to_pcm_bytes(chunk: &AudioChunk) -> Vec<u8> {
|
||||
let len = chunk.len();
|
||||
let mut bytes = Vec::with_capacity(len * 4);
|
||||
let gain = chunk.gain_linear() as f32;
|
||||
let scale = 1.0f32 / chunk.bit_depth().max_value();
|
||||
for frame in chunk.frames() {
|
||||
let left = (frame[0] as f32 * scale * gain).clamp(-1.0, 1.0);
|
||||
let right = (frame[1] as f32 * scale * gain).clamp(-1.0, 1.0);
|
||||
let left_i16 = (left * 32767.0) as i16;
|
||||
let right_i16 = (right * 32767.0) as i16;
|
||||
bytes.extend_from_slice(&left_i16.to_le_bytes());
|
||||
bytes.extend_from_slice(&right_i16.to_le_bytes());
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
struct ByteStreamReader {
|
||||
rx: mpsc::Receiver<Vec<u8>>,
|
||||
buffer: VecDeque<u8>,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl ByteStreamReader {
|
||||
fn new(rx: mpsc::Receiver<Vec<u8>>) -> Self {
|
||||
Self {
|
||||
rx,
|
||||
buffer: VecDeque::new(),
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for ByteStreamReader {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
loop {
|
||||
if !self.buffer.is_empty() {
|
||||
let to_copy = self.buffer.len().min(buf.remaining());
|
||||
if to_copy == 0 {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
// VecDeque::make_contiguous pour copier efficacement
|
||||
let slice = self.buffer.make_contiguous();
|
||||
buf.put_slice(&slice[..to_copy]);
|
||||
self.buffer.drain(..to_copy);
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
if self.finished {
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
|
||||
match Pin::new(&mut self.rx).poll_recv(cx) {
|
||||
Poll::Ready(Some(bytes)) => {
|
||||
if bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
self.buffer.extend(bytes);
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
self.finished = true;
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
Poll::Pending => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques produites par le `FlacFileSink`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FlacFileSinkStats {
|
||||
pub path: PathBuf,
|
||||
pub chunks_received: u64,
|
||||
pub total_samples: u64,
|
||||
pub total_duration_sec: f64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::BitDepth;
|
||||
use pmoflac::decode_flac_stream;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_flac_file_sink_writes_audio() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let output_path = temp_dir.path().join("output.flac");
|
||||
|
||||
let (sink, tx) = FlacFileSink::new(&output_path, 8);
|
||||
let handle = tokio::spawn(async move { sink.run().await.unwrap() });
|
||||
|
||||
let chunk = AudioChunk::from_channels_f32(
|
||||
0,
|
||||
vec![0.25; 256],
|
||||
vec![0.5; 256],
|
||||
44_100,
|
||||
BitDepth::B24,
|
||||
);
|
||||
tx.send(chunk).await.unwrap();
|
||||
|
||||
drop(tx);
|
||||
|
||||
let stats = handle.await.unwrap();
|
||||
assert_eq!(stats.chunks_received, 1);
|
||||
assert_eq!(stats.total_samples, 256);
|
||||
|
||||
let file = File::open(&output_path).await.unwrap();
|
||||
let mut stream = decode_flac_stream(file).await.unwrap();
|
||||
let info = stream.info().clone();
|
||||
assert_eq!(info.channels, 2);
|
||||
assert_eq!(info.sample_rate, 44_100);
|
||||
|
||||
let mut decoded = Vec::new();
|
||||
stream.read_to_end(&mut decoded).await.unwrap();
|
||||
stream.wait().await.unwrap();
|
||||
assert_eq!(decoded.len(), 256 * 4); // 256 frames * 2 channels * 2 bytes
|
||||
}
|
||||
}
|
||||
150
old_code/pmoaudio/nodes/mod.rs
Normal file
150
old_code/pmoaudio/nodes/mod.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
//! Nodes du pipeline audio
|
||||
//!
|
||||
//! Ce module contient tous les types de nodes disponibles pour construire
|
||||
//! un pipeline audio, ainsi que les traits et structures de support.
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::AudioSegment;
|
||||
|
||||
pub mod buffer_node;
|
||||
pub mod chromecast_sink;
|
||||
pub mod decoder_node;
|
||||
pub mod disk_sink;
|
||||
pub mod dsp_node;
|
||||
pub mod file_source;
|
||||
pub mod flac_file_sink;
|
||||
pub mod mpd_sink;
|
||||
pub mod sink_node;
|
||||
pub mod source_node;
|
||||
pub mod timer_node;
|
||||
pub mod volume_node;
|
||||
|
||||
/// Trait de base pour tous les nodes audio
|
||||
///
|
||||
/// Tous les nodes du pipeline implémentent ce trait pour permettre
|
||||
/// une interface uniforme de traitement des chunks audio.
|
||||
#[async_trait::async_trait]
|
||||
pub trait AudioNode: Send + Sync {
|
||||
/// Push un chunk vers ce node
|
||||
///
|
||||
/// # Erreurs
|
||||
///
|
||||
/// Retourne `AudioError::SendError` si l'envoi échoue
|
||||
async fn push(&mut self, chunk: Arc<AudioSegment>) -> Result<(), AudioError>;
|
||||
|
||||
/// Ferme le node proprement
|
||||
async fn close(&mut self);
|
||||
}
|
||||
|
||||
/// Node avec un seul abonné (pas de clone inutile)
|
||||
///
|
||||
/// Optimisé pour les cas où un node n'a qu'un seul destinataire.
|
||||
/// Le Arc du chunk est simplement transféré sans clonage supplémentaire.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoaudio::SingleSubscriberNode;
|
||||
/// use tokio::sync::mpsc;
|
||||
///
|
||||
/// let (tx, rx) = mpsc::channel(10);
|
||||
/// let node = SingleSubscriberNode::new(tx);
|
||||
/// ```
|
||||
pub struct SingleSubscriberNode {
|
||||
tx: mpsc::Sender<Arc<AudioSegment>>,
|
||||
}
|
||||
|
||||
impl SingleSubscriberNode {
|
||||
pub fn new(tx: mpsc::Sender<Arc<AudioSegment>>) -> Self {
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
pub async fn push(&self, chunk: Arc<AudioSegment>) -> Result<(), AudioError> {
|
||||
self.tx.send(chunk).await.map_err(|_| AudioError::SendError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Node avec plusieurs abonnés (partage le même Arc)
|
||||
///
|
||||
/// Permet de broadcaster un chunk à plusieurs destinations.
|
||||
/// Tous les abonnés reçoivent le même `Arc<AudioSegment>`, donc pas de copie
|
||||
/// des données audio - seul le compteur de référence Arc est incrémenté.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoaudio::MultiSubscriberNode;
|
||||
/// use tokio::sync::mpsc;
|
||||
///
|
||||
/// let mut node = MultiSubscriberNode::new();
|
||||
/// let (tx1, rx1) = mpsc::channel(10);
|
||||
/// let (tx2, rx2) = mpsc::channel(10);
|
||||
///
|
||||
/// node.add_subscriber(tx1);
|
||||
/// node.add_subscriber(tx2);
|
||||
/// // Les deux abonnés recevront les mêmes chunks
|
||||
/// ```
|
||||
pub struct MultiSubscriberNode {
|
||||
subscribers: Vec<mpsc::Sender<Arc<AudioSegment>>>,
|
||||
}
|
||||
|
||||
impl MultiSubscriberNode {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
|
||||
self.subscribers.push(tx);
|
||||
}
|
||||
|
||||
pub async fn push(&self, chunk: Arc<AudioSegment>) -> Result<(), AudioError> {
|
||||
for tx in &self.subscribers {
|
||||
// On partage le même Arc avec tous les abonnés
|
||||
tx.send(chunk.clone())
|
||||
.await
|
||||
.map_err(|_| AudioError::SendError)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn try_push(&self, chunk: Arc<AudioSegment>) -> Result<(), AudioError> {
|
||||
for tx in &self.subscribers {
|
||||
// try_send non-bloquant, ignore si saturé
|
||||
let _ = tx.try_send(chunk.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MultiSubscriberNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Erreurs possibles dans le pipeline audio
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AudioError {
|
||||
/// Échec d'envoi d'un chunk à travers un channel
|
||||
SendError,
|
||||
/// Échec de réception d'un chunk depuis un channel
|
||||
ReceiveError,
|
||||
/// Erreur de traitement avec message descriptif
|
||||
ProcessingError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AudioError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AudioError::SendError => write!(f, "Failed to send audio chunk"),
|
||||
AudioError::ReceiveError => write!(f, "Failed to receive audio chunk"),
|
||||
AudioError::ProcessingError(msg) => write!(f, "Processing error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AudioError {}
|
||||
398
old_code/pmoaudio/nodes/mpd_sink.rs
Normal file
398
old_code/pmoaudio/nodes/mpd_sink.rs
Normal file
@@ -0,0 +1,398 @@
|
||||
//! MpdSink - Envoie le flux audio à un démon MPD (Music Player Daemon)
|
||||
//!
|
||||
//! Ce module fournit un sink qui streame l'audio vers un démon MPD distant ou local.
|
||||
//! Note: Cette implémentation est une version mock/skeleton. Une vraie implémentation
|
||||
//! nécessiterait le protocole MPD complet et l'utilisation de bibliothèques comme `mpd`.
|
||||
|
||||
use crate::{nodes::AudioError, AudioChunk};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Configuration pour le MpdSink
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MpdConfig {
|
||||
/// Adresse du serveur MPD
|
||||
pub host: String,
|
||||
|
||||
/// Port du serveur MPD (défaut: 6600)
|
||||
pub port: u16,
|
||||
|
||||
/// Mot de passe optionnel
|
||||
pub password: Option<String>,
|
||||
|
||||
/// Nom de l'output MPD à utiliser (optionnel)
|
||||
pub output_name: Option<String>,
|
||||
|
||||
/// Taille du buffer
|
||||
pub buffer_size: usize,
|
||||
|
||||
/// Format d'envoi
|
||||
pub format: MpdAudioFormat,
|
||||
}
|
||||
|
||||
impl Default for MpdConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: "localhost".to_string(),
|
||||
port: 6600,
|
||||
password: None,
|
||||
output_name: None,
|
||||
buffer_size: 50,
|
||||
format: MpdAudioFormat::S16Le,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats audio supportés par MPD
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MpdAudioFormat {
|
||||
/// Signed 16-bit Little Endian
|
||||
S16Le,
|
||||
/// Signed 24-bit Little Endian
|
||||
S24Le,
|
||||
/// Signed 32-bit Little Endian
|
||||
S32Le,
|
||||
/// Float 32-bit
|
||||
F32,
|
||||
}
|
||||
|
||||
impl MpdAudioFormat {
|
||||
/// Retourne le nom du format pour le protocole MPD
|
||||
pub fn as_mpd_string(&self) -> &str {
|
||||
match self {
|
||||
MpdAudioFormat::S16Le => "16:16:2",
|
||||
MpdAudioFormat::S24Le => "24:24:2",
|
||||
MpdAudioFormat::S32Le => "32:32:2",
|
||||
MpdAudioFormat::F32 => "f:32:2",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// MpdSink - Streame vers un démon MPD
|
||||
///
|
||||
/// Ce sink se connecte à un serveur MPD et lui envoie le flux audio.
|
||||
/// MPD peut ensuite router l'audio vers différents outputs (ALSA, PulseAudio, HTTP, etc.).
|
||||
///
|
||||
/// # Implémentation actuelle
|
||||
///
|
||||
/// Cette version est un mock qui simule la communication avec MPD.
|
||||
/// Pour une vraie implémentation, il faudrait:
|
||||
/// - Implémenter le protocole MPD (commandes textuelles sur TCP)
|
||||
/// - S'authentifier si nécessaire
|
||||
/// - Configurer le format audio
|
||||
/// - Envoyer les données PCM via le protocole approprié
|
||||
/// - Gérer les commandes de contrôle (play, pause, stop)
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::{MpdSink, MpdConfig};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let config = MpdConfig {
|
||||
/// host: "localhost".to_string(),
|
||||
/// port: 6600,
|
||||
/// password: None,
|
||||
/// ..Default::default()
|
||||
/// };
|
||||
///
|
||||
/// let (sink, sink_tx) = MpdSink::new("mpd1".to_string(), config, 10);
|
||||
///
|
||||
/// tokio::spawn(async move {
|
||||
/// sink.run().await.unwrap()
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
pub struct MpdSink {
|
||||
/// Identifiant du sink
|
||||
node_id: String,
|
||||
|
||||
/// Channel pour recevoir les chunks audio
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
|
||||
/// Configuration
|
||||
config: MpdConfig,
|
||||
|
||||
/// État de la connexion (mock)
|
||||
connected: bool,
|
||||
|
||||
/// Version du serveur MPD (mock)
|
||||
mpd_version: Option<String>,
|
||||
}
|
||||
|
||||
impl MpdSink {
|
||||
/// Crée un nouveau MpdSink
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `node_id` - Identifiant unique du sink
|
||||
/// * `config` - Configuration MPD
|
||||
/// * `channel_size` - Taille du buffer du channel
|
||||
pub fn new(
|
||||
node_id: String,
|
||||
config: MpdConfig,
|
||||
channel_size: usize,
|
||||
) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let sink = Self {
|
||||
node_id,
|
||||
rx,
|
||||
config,
|
||||
connected: false,
|
||||
mpd_version: None,
|
||||
};
|
||||
|
||||
(sink, tx)
|
||||
}
|
||||
|
||||
/// Établit la connexion avec le serveur MPD (mock)
|
||||
async fn connect(&mut self) -> Result<(), AudioError> {
|
||||
println!(
|
||||
"[{}] Connecting to MPD at {}:{}...",
|
||||
self.node_id, self.config.host, self.config.port
|
||||
);
|
||||
|
||||
// Simuler une connexion TCP
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
|
||||
|
||||
// Dans une vraie implémentation:
|
||||
// 1. Établir connexion TCP
|
||||
// 2. Lire la bannière de version
|
||||
// 3. S'authentifier si password fourni
|
||||
// 4. Configurer le format audio
|
||||
|
||||
self.mpd_version = Some("0.23.0".to_string());
|
||||
self.connected = true;
|
||||
|
||||
println!(
|
||||
"[{}] Connected to MPD v{} successfully",
|
||||
self.node_id,
|
||||
self.mpd_version.as_ref().unwrap()
|
||||
);
|
||||
|
||||
// Configurer le format audio
|
||||
self.configure_audio_format().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configure le format audio sur MPD (mock)
|
||||
async fn configure_audio_format(&self) -> Result<(), AudioError> {
|
||||
println!(
|
||||
"[{}] Configuring audio format: {}",
|
||||
self.node_id,
|
||||
self.config.format.as_mpd_string()
|
||||
);
|
||||
|
||||
// Dans une vraie implémentation:
|
||||
// Envoyer une commande MPD pour configurer le format
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
));
|
||||
}
|
||||
|
||||
// Dans une vraie implémentation:
|
||||
// 1. Appliquer le gain
|
||||
// 2. Convertir dans le format approprié (S16LE, etc.)
|
||||
// 3. Envoyer via le protocole MPD (probablement via une commande `sendmessage` ou pipe)
|
||||
|
||||
// Simuler un délai d'envoi
|
||||
tokio::time::sleep(tokio::time::Duration::from_micros(50)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Déconnecte proprement du serveur MPD (mock)
|
||||
async fn disconnect(&mut self) -> Result<(), AudioError> {
|
||||
if self.connected {
|
||||
println!("[{}] Disconnecting from MPD...", self.node_id);
|
||||
|
||||
// Dans une vraie implémentation:
|
||||
// Envoyer la commande "close"
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
self.connected = false;
|
||||
|
||||
println!("[{}] Disconnected successfully", self.node_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Démarre la boucle de traitement du MpdSink
|
||||
pub async fn run(mut self) -> Result<MpdStats, AudioError> {
|
||||
// Établir la connexion
|
||||
self.connect().await?;
|
||||
|
||||
let mut stats = MpdStats::new(
|
||||
self.node_id.clone(),
|
||||
format!("{}:{}", self.config.host, self.config.port),
|
||||
);
|
||||
|
||||
// Boucle principale
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
// Appliquer le gain si nécessaire
|
||||
let chunk_to_send = if chunk.gain_db().abs() > f64::EPSILON {
|
||||
Arc::clone(&chunk).apply_gain()
|
||||
} else {
|
||||
Arc::clone(&chunk)
|
||||
};
|
||||
|
||||
// Envoyer au serveur MPD
|
||||
self.send_chunk(&chunk_to_send).await?;
|
||||
|
||||
stats.record_chunk(&chunk_to_send);
|
||||
}
|
||||
|
||||
// Déconnexion propre
|
||||
self.disconnect().await?;
|
||||
|
||||
stats.finalize();
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// Retourne un handle pour contrôler le sink (mock)
|
||||
pub fn get_handle(&self) -> MpdHandle {
|
||||
MpdHandle {
|
||||
node_id: self.node_id.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle pour contrôler le MpdSink
|
||||
///
|
||||
/// Permet d'envoyer des commandes de contrôle au serveur MPD
|
||||
#[derive(Clone)]
|
||||
pub struct MpdHandle {
|
||||
node_id: String,
|
||||
}
|
||||
|
||||
impl MpdHandle {
|
||||
/// Commande play (mock)
|
||||
pub async fn play(&self) -> Result<(), AudioError> {
|
||||
println!("[{}] MPD command: play", self.node_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Commande pause (mock)
|
||||
pub async fn pause(&self) -> Result<(), AudioError> {
|
||||
println!("[{}] MPD command: pause", self.node_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Commande stop (mock)
|
||||
pub async fn stop(&self) -> Result<(), AudioError> {
|
||||
println!("[{}] MPD command: stop", self.node_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Change le volume MPD (0-100) (mock)
|
||||
pub async fn set_volume(&self, volume: u8) -> Result<(), AudioError> {
|
||||
let clamped = volume.min(100);
|
||||
println!("[{}] MPD command: setvol {}", self.node_id, clamped);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques du MpdSink
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MpdStats {
|
||||
pub node_id: String,
|
||||
pub server_address: String,
|
||||
pub chunks_sent: u64,
|
||||
pub total_samples: u64,
|
||||
pub total_duration_sec: f64,
|
||||
}
|
||||
|
||||
impl MpdStats {
|
||||
pub fn new(node_id: String, server_address: String) -> Self {
|
||||
Self {
|
||||
node_id,
|
||||
server_address,
|
||||
chunks_sent: 0,
|
||||
total_samples: 0,
|
||||
total_duration_sec: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_chunk(&mut self, chunk: &AudioChunk) {
|
||||
self.chunks_sent += 1;
|
||||
self.total_samples += chunk.len() as u64;
|
||||
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
|
||||
}
|
||||
|
||||
pub fn finalize(&mut self) {
|
||||
// Calculs finaux si nécessaire
|
||||
}
|
||||
|
||||
pub fn display(&self) {
|
||||
println!("\n=== MPD Sink Statistics: {} ===", self.node_id);
|
||||
println!("Server: {}", self.server_address);
|
||||
println!("Chunks sent: {}", self.chunks_sent);
|
||||
println!("Total samples: {}", self.total_samples);
|
||||
println!("Total duration: {:.3} sec", self.total_duration_sec);
|
||||
println!("===============================\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::BitDepth;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mpd_sink_basic() {
|
||||
let config = MpdConfig {
|
||||
host: "localhost".to_string(),
|
||||
port: 6600,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (sink, tx) = MpdSink::new("test".to_string(), config, 10);
|
||||
|
||||
let handle = tokio::spawn(async move { sink.run().await });
|
||||
|
||||
// Envoyer quelques chunks
|
||||
for i in 0..5 {
|
||||
let chunk = AudioChunk::from_channels_f32(
|
||||
i,
|
||||
vec![0.5; 1000],
|
||||
vec![0.5; 1000],
|
||||
48000,
|
||||
BitDepth::B24,
|
||||
);
|
||||
tx.send(chunk).await.unwrap();
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
|
||||
let stats = handle.await.unwrap().unwrap();
|
||||
assert_eq!(stats.chunks_sent, 5);
|
||||
assert_eq!(stats.server_address, "localhost:6600");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mpd_handle() {
|
||||
let config = MpdConfig::default();
|
||||
let (sink, _tx) = MpdSink::new("test".to_string(), config, 10);
|
||||
|
||||
let handle = sink.get_handle();
|
||||
|
||||
// Tester les commandes (mock)
|
||||
handle.play().await.unwrap();
|
||||
handle.pause().await.unwrap();
|
||||
handle.set_volume(75).await.unwrap();
|
||||
handle.stop().await.unwrap();
|
||||
}
|
||||
}
|
||||
212
old_code/pmoaudio/nodes/sink_node.rs
Normal file
212
old_code/pmoaudio/nodes/sink_node.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use crate::{nodes::AudioError, AudioChunk};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// SinkNode - Node terminal qui consomme les chunks audio
|
||||
///
|
||||
/// Version mock pour tests et logging
|
||||
pub struct SinkNode {
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl SinkNode {
|
||||
pub fn new(name: String, channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let node = Self { rx, name };
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
/// Version silencieuse - consomme les chunks sans action
|
||||
pub async fn run_silent(mut self) -> Result<(), AudioError> {
|
||||
while let Some(_chunk) = self.rx.recv().await {
|
||||
// Ne rien faire, juste consommer
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Version avec logging
|
||||
pub async fn run_with_logging(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
println!(
|
||||
"[{}] Received chunk #{} - {} samples @ {} Hz",
|
||||
self.name,
|
||||
chunk.order(),
|
||||
chunk.len(),
|
||||
chunk.sample_rate()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Version avec statistiques
|
||||
pub async fn run_with_stats(mut self) -> Result<SinkStats, AudioError> {
|
||||
let mut stats = SinkStats::new(self.name.clone());
|
||||
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
stats.process_chunk(&chunk);
|
||||
}
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// Version mock pour écriture dans un fichier (simule l'écriture)
|
||||
pub async fn run_mock_file_writer(mut self) -> Result<usize, AudioError> {
|
||||
let mut total_samples = 0;
|
||||
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
total_samples += chunk.len();
|
||||
// Simuler l'écriture avec un petit délai
|
||||
tokio::time::sleep(tokio::time::Duration::from_micros(10)).await;
|
||||
}
|
||||
|
||||
Ok(total_samples)
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques collectées par un SinkNode
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SinkStats {
|
||||
pub name: String,
|
||||
pub chunks_received: u64,
|
||||
pub total_samples: u64,
|
||||
pub total_duration_sec: f64,
|
||||
pub peak_left: f32,
|
||||
pub peak_right: f32,
|
||||
pub rms_left: f64,
|
||||
pub rms_right: f64,
|
||||
}
|
||||
|
||||
impl SinkStats {
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
chunks_received: 0,
|
||||
total_samples: 0,
|
||||
total_duration_sec: 0.0,
|
||||
peak_left: 0.0,
|
||||
peak_right: 0.0,
|
||||
rms_left: 0.0,
|
||||
rms_right: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_chunk(&mut self, chunk: &AudioChunk) {
|
||||
self.chunks_received += 1;
|
||||
let len = chunk.len() as u64;
|
||||
self.total_samples += len;
|
||||
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate() as f64;
|
||||
|
||||
let inv_max = 1.0f32 / chunk.bit_depth().max_value();
|
||||
let mut peak_left = self.peak_left;
|
||||
let mut peak_right = self.peak_right;
|
||||
let mut sum_squares_left = 0.0f64;
|
||||
let mut sum_squares_right = 0.0f64;
|
||||
|
||||
for frame in chunk.frames() {
|
||||
let left = frame[0] as f32 * inv_max;
|
||||
let right = frame[1] as f32 * inv_max;
|
||||
let left_abs = left.abs();
|
||||
let right_abs = right.abs();
|
||||
if left_abs > peak_left {
|
||||
peak_left = left_abs;
|
||||
}
|
||||
if right_abs > peak_right {
|
||||
peak_right = right_abs;
|
||||
}
|
||||
let l64 = left as f64;
|
||||
let r64 = right as f64;
|
||||
sum_squares_left += l64 * l64;
|
||||
sum_squares_right += r64 * r64;
|
||||
}
|
||||
|
||||
self.peak_left = peak_left;
|
||||
self.peak_right = peak_right;
|
||||
|
||||
let prev_samples = self.total_samples - len;
|
||||
self.rms_left = ((self.rms_left.powi(2) * prev_samples as f64 + sum_squares_left)
|
||||
/ self.total_samples as f64)
|
||||
.sqrt();
|
||||
self.rms_right = ((self.rms_right.powi(2) * prev_samples as f64 + sum_squares_right)
|
||||
/ self.total_samples as f64)
|
||||
.sqrt();
|
||||
}
|
||||
|
||||
pub fn display(&self) {
|
||||
println!("\n=== Sink Statistics: {} ===", self.name);
|
||||
println!("Chunks received: {}", self.chunks_received);
|
||||
println!("Total samples: {}", self.total_samples);
|
||||
println!("Total duration: {:.3} sec", self.total_duration_sec);
|
||||
println!("Peak L/R: {:.3} / {:.3}", self.peak_left, self.peak_right);
|
||||
println!("RMS L/R: {:.3} / {:.3}", self.rms_left, self.rms_right);
|
||||
println!("========================\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::BitDepth;
|
||||
|
||||
const BD: BitDepth = BitDepth::B24;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sink_node_silent() {
|
||||
let (node, tx) = SinkNode::new("test".to_string(), 10);
|
||||
|
||||
let handle = tokio::spawn(async move { node.run_silent().await });
|
||||
|
||||
// Envoyer quelques chunks
|
||||
for i in 0..3 {
|
||||
let chunk = AudioChunk::from_channels_f32(i, vec![0.0; 100], vec![0.0; 100], 48000, BD);
|
||||
tx.send(chunk).await.unwrap();
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
handle.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sink_node_stats() {
|
||||
let (node, tx) = SinkNode::new("test".to_string(), 10);
|
||||
|
||||
let handle = tokio::spawn(async move { node.run_with_stats().await });
|
||||
|
||||
// Envoyer des chunks avec signal connu
|
||||
for i in 0..3 {
|
||||
let chunk =
|
||||
AudioChunk::from_channels_f32(i, vec![1.0; 1000], vec![0.5; 1000], 48000, BD);
|
||||
tx.send(chunk).await.unwrap();
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
let stats = handle.await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(stats.chunks_received, 3);
|
||||
assert_eq!(stats.total_samples, 3000);
|
||||
assert!((stats.peak_left - 1.0).abs() < 1e-6);
|
||||
assert!((stats.peak_right - 0.5).abs() < 1e-6);
|
||||
assert!((stats.rms_left - 1.0).abs() < 0.001);
|
||||
assert!((stats.rms_right - 0.5).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sink_node_file_writer() {
|
||||
let (node, tx) = SinkNode::new("writer".to_string(), 10);
|
||||
|
||||
let handle = tokio::spawn(async move { node.run_mock_file_writer().await });
|
||||
|
||||
// Envoyer des chunks
|
||||
for i in 0..5 {
|
||||
let chunk = AudioChunk::from_channels_f32(i, vec![0.0; 100], vec![0.0; 100], 48000, BD);
|
||||
tx.send(chunk).await.unwrap();
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
let total_samples = handle.await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(total_samples, 500);
|
||||
}
|
||||
}
|
||||
171
old_code/pmoaudio/nodes/source_node.rs
Normal file
171
old_code/pmoaudio/nodes/source_node.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use crate::{
|
||||
nodes::{AudioError, MultiSubscriberNode},
|
||||
AudioChunk, BitDepth,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// SourceNode - Génère ou lit des chunks audio depuis une source
|
||||
///
|
||||
/// Ce node est la source du pipeline. Version mock pour tests.
|
||||
pub struct SourceNode {
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
const DEFAULT_BIT_DEPTH: BitDepth = BitDepth::B24;
|
||||
|
||||
impl SourceNode {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Génère un chunk de test avec une forme d'onde sinusoïdale
|
||||
pub fn generate_test_chunk(
|
||||
order: u64,
|
||||
size: usize,
|
||||
sample_rate: u32,
|
||||
frequency: f32,
|
||||
) -> Arc<AudioChunk> {
|
||||
let mut left = Vec::with_capacity(size);
|
||||
let mut right = Vec::with_capacity(size);
|
||||
|
||||
for i in 0..size {
|
||||
let t = (order * size as u64 + i as u64) as f32 / sample_rate as f32;
|
||||
let sample = (2.0 * std::f32::consts::PI * frequency * t).sin();
|
||||
left.push(sample);
|
||||
right.push(sample * 0.8); // Légèrement différent pour la stéréo
|
||||
}
|
||||
|
||||
AudioChunk::from_channels_f32(order, left, right, sample_rate, DEFAULT_BIT_DEPTH)
|
||||
}
|
||||
|
||||
/// Génère et envoie des chunks de test
|
||||
pub async fn generate_chunks(
|
||||
&self,
|
||||
count: u64,
|
||||
chunk_size: usize,
|
||||
sample_rate: u32,
|
||||
frequency: f32,
|
||||
) -> Result<(), AudioError> {
|
||||
for i in 0..count {
|
||||
let chunk = Self::generate_test_chunk(i, chunk_size, sample_rate, frequency);
|
||||
self.subscribers.push(chunk).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Génère des chunks silencieux
|
||||
pub async fn generate_silence(
|
||||
&self,
|
||||
count: u64,
|
||||
chunk_size: usize,
|
||||
sample_rate: u32,
|
||||
) -> Result<(), AudioError> {
|
||||
for i in 0..count {
|
||||
let stereo = vec![[0i32; 2]; chunk_size];
|
||||
let chunk = AudioChunk::new(i, stereo, sample_rate, DEFAULT_BIT_DEPTH);
|
||||
self.subscribers.push(chunk).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Version streaming : génère des chunks continuellement avec délai
|
||||
pub async fn stream_chunks(
|
||||
&self,
|
||||
chunk_size: usize,
|
||||
sample_rate: u32,
|
||||
frequency: f32,
|
||||
duration_ms: u64,
|
||||
) -> Result<(), AudioError> {
|
||||
let chunk_duration_ms = (chunk_size as f64 / sample_rate as f64 * 1000.0) as u64;
|
||||
let mut order = 0u64;
|
||||
|
||||
let start = tokio::time::Instant::now();
|
||||
let duration = tokio::time::Duration::from_millis(duration_ms);
|
||||
|
||||
while start.elapsed() < duration {
|
||||
let chunk = Self::generate_test_chunk(order, chunk_size, sample_rate, frequency);
|
||||
self.subscribers.push(chunk).await?;
|
||||
|
||||
order += 1;
|
||||
|
||||
// Attendre pour simuler le timing réel
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(chunk_duration_ms)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SourceNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_source_node_generation() {
|
||||
let mut source = SourceNode::new();
|
||||
let (tx, mut rx) = mpsc::channel(10);
|
||||
|
||||
source.add_subscriber(tx);
|
||||
|
||||
// Générer 3 chunks
|
||||
source.generate_chunks(3, 100, 48000, 440.0).await.unwrap();
|
||||
|
||||
// Vérifier la réception
|
||||
for i in 0..3 {
|
||||
let chunk = rx.recv().await.unwrap();
|
||||
assert_eq!(chunk.order(), i);
|
||||
assert_eq!(chunk.len(), 100);
|
||||
assert_eq!(chunk.sample_rate(), 48000);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sine_wave_generation() {
|
||||
let chunk = SourceNode::generate_test_chunk(0, 48000, 48000, 440.0);
|
||||
|
||||
// Vérifier qu'on a bien une sinusoïde
|
||||
// À 440 Hz avec 48000 samples/s, on devrait avoir 440 cycles
|
||||
let pairs = chunk.to_pairs_f32();
|
||||
let left: Vec<f32> = pairs.iter().map(|frame| frame[0]).collect();
|
||||
|
||||
// Trouver les passages par zéro
|
||||
let mut zero_crossings = 0;
|
||||
for i in 1..left.len() {
|
||||
if (left[i - 1] < 0.0 && left[i] >= 0.0) || (left[i - 1] >= 0.0 && left[i] < 0.0) {
|
||||
zero_crossings += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 440 cycles = 880 passages par zéro (approximativement)
|
||||
assert!(zero_crossings > 850 && zero_crossings < 910);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_source_node_silence() {
|
||||
let mut source = SourceNode::new();
|
||||
let (tx, mut rx) = mpsc::channel(10);
|
||||
|
||||
source.add_subscriber(tx);
|
||||
|
||||
source.generate_silence(2, 100, 48000).await.unwrap();
|
||||
|
||||
for _ in 0..2 {
|
||||
let chunk = rx.recv().await.unwrap();
|
||||
assert!(chunk.frames().iter().all(|frame| frame[0] == 0));
|
||||
assert!(chunk.frames().iter().all(|frame| frame[1] == 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
291
old_code/pmoaudio/nodes/timer_node.rs
Normal file
291
old_code/pmoaudio/nodes/timer_node.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
use crate::{
|
||||
nodes::{AudioError, MultiSubscriberNode},
|
||||
AudioChunk,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
|
||||
/// TimerNode - Node passthrough qui calcule la position temporelle
|
||||
///
|
||||
/// Ce node ne modifie pas les données audio, il les passe directement
|
||||
/// aux abonnés tout en maintenant un compteur de samples pour calculer
|
||||
/// la position en secondes.
|
||||
///
|
||||
/// # Fonctionnement
|
||||
///
|
||||
/// Pour chaque chunk reçu:
|
||||
/// 1. Incrémente `elapsed_samples += chunk.len()`
|
||||
/// 2. Calcule `position_sec = elapsed_samples / sample_rate`
|
||||
/// 3. Push le chunk (sans modification) vers les abonnés
|
||||
///
|
||||
/// # Utilisation
|
||||
///
|
||||
/// Le TimerNode fournit un [`TimerHandle`] qui permet de lire la position
|
||||
/// depuis d'autres threads/tasks sans bloquer le pipeline.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::TimerNode;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let (mut timer, timer_tx) = TimerNode::new(10);
|
||||
/// let handle = timer.get_position_handle();
|
||||
///
|
||||
/// tokio::spawn(async move {
|
||||
/// timer.run().await.unwrap();
|
||||
/// });
|
||||
///
|
||||
/// // Lire la position depuis un autre thread
|
||||
/// let position = handle.position_sec().await;
|
||||
/// println!("Position: {:.2} sec", position);
|
||||
/// }
|
||||
/// ```
|
||||
pub struct TimerNode {
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
elapsed_samples: Arc<RwLock<u64>>,
|
||||
current_sample_rate: Arc<RwLock<u32>>,
|
||||
}
|
||||
|
||||
impl TimerNode {
|
||||
/// Crée un nouveau TimerNode
|
||||
pub fn new(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
elapsed_samples: Arc::new(RwLock::new(0)),
|
||||
current_sample_rate: Arc::new(RwLock::new(48000)), // Default
|
||||
};
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
/// Ajoute un abonné
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Retourne la position actuelle en secondes
|
||||
pub async fn position_sec(&self) -> f64 {
|
||||
let elapsed = *self.elapsed_samples.read().await;
|
||||
let sample_rate = *self.current_sample_rate.read().await;
|
||||
elapsed as f64 / sample_rate as f64
|
||||
}
|
||||
|
||||
/// Retourne le nombre total d'échantillons écoulés
|
||||
pub async fn elapsed_samples(&self) -> u64 {
|
||||
*self.elapsed_samples.read().await
|
||||
}
|
||||
|
||||
/// Reset le compteur
|
||||
pub async fn reset(&self) {
|
||||
let mut elapsed = self.elapsed_samples.write().await;
|
||||
*elapsed = 0;
|
||||
}
|
||||
|
||||
/// Démarre la boucle de traitement du TimerNode
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
// Mettre à jour le sample rate si nécessaire
|
||||
{
|
||||
let mut sr = self.current_sample_rate.write().await;
|
||||
if *sr != chunk.sample_rate() {
|
||||
*sr = chunk.sample_rate();
|
||||
}
|
||||
}
|
||||
|
||||
// Incrémenter le compteur d'échantillons
|
||||
{
|
||||
let mut elapsed = self.elapsed_samples.write().await;
|
||||
*elapsed += chunk.len() as u64;
|
||||
}
|
||||
|
||||
// Push immédiatement le même chunk vers les abonnés (passthrough)
|
||||
self.subscribers.push(chunk).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Version non-bloquante avec try_push
|
||||
pub async fn run_nonblocking(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
{
|
||||
let mut sr = self.current_sample_rate.write().await;
|
||||
if *sr != chunk.sample_rate() {
|
||||
*sr = chunk.sample_rate();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut elapsed = self.elapsed_samples.write().await;
|
||||
*elapsed += chunk.len() as u64;
|
||||
}
|
||||
|
||||
self.subscribers.try_push(chunk).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retourne un handle pour lire la position depuis d'autres threads
|
||||
pub fn get_position_handle(&self) -> TimerHandle {
|
||||
TimerHandle {
|
||||
elapsed_samples: self.elapsed_samples.clone(),
|
||||
current_sample_rate: self.current_sample_rate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle pour lire la position du TimerNode depuis d'autres threads
|
||||
///
|
||||
/// Ce handle peut être cloné et utilisé depuis plusieurs threads/tasks
|
||||
/// pour monitorer la position de lecture sans bloquer le pipeline.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::TimerNode;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let (mut timer, _tx) = TimerNode::new(10);
|
||||
/// let handle = timer.get_position_handle();
|
||||
/// let handle_clone = handle.clone();
|
||||
///
|
||||
/// // Utiliser depuis plusieurs tasks
|
||||
/// tokio::spawn(async move {
|
||||
/// loop {
|
||||
/// let pos = handle_clone.position_sec().await;
|
||||
/// println!("Position: {:.2}s", pos);
|
||||
/// tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
/// }
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct TimerHandle {
|
||||
elapsed_samples: Arc<RwLock<u64>>,
|
||||
current_sample_rate: Arc<RwLock<u32>>,
|
||||
}
|
||||
|
||||
impl TimerHandle {
|
||||
/// Retourne la position actuelle en secondes
|
||||
pub async fn position_sec(&self) -> f64 {
|
||||
let elapsed = *self.elapsed_samples.read().await;
|
||||
let sample_rate = *self.current_sample_rate.read().await;
|
||||
elapsed as f64 / sample_rate as f64
|
||||
}
|
||||
|
||||
/// Retourne le nombre total d'échantillons écoulés
|
||||
pub async fn elapsed_samples(&self) -> u64 {
|
||||
*self.elapsed_samples.read().await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::BitDepth;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_timer_node_position_calculation() {
|
||||
let (mut node, tx) = TimerNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
let handle = node.get_position_handle();
|
||||
|
||||
// Spawn le node
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer 3 chunks de 1000 samples à 48000 Hz
|
||||
for i in 0..3 {
|
||||
let stereo = vec![[0i32; 2]; 1000];
|
||||
let chunk = AudioChunk::new(i, stereo, 48000, BitDepth::B24);
|
||||
tx.send(chunk).await.unwrap();
|
||||
}
|
||||
|
||||
// Attendre que les chunks soient traités
|
||||
for _ in 0..3 {
|
||||
out_rx.recv().await.unwrap();
|
||||
}
|
||||
|
||||
// Vérifier la position
|
||||
let position = handle.position_sec().await;
|
||||
let expected = 3000.0 / 48000.0; // 3 chunks * 1000 samples / 48000 Hz
|
||||
assert!((position - expected).abs() < 0.0001);
|
||||
|
||||
let elapsed = handle.elapsed_samples().await;
|
||||
assert_eq!(elapsed, 3000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_timer_node_passthrough() {
|
||||
let (mut node, tx) = TimerNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk
|
||||
let chunk = AudioChunk::from_channels_i32(
|
||||
42,
|
||||
vec![100, 200, 300],
|
||||
vec![400, 500, 600],
|
||||
48000,
|
||||
BitDepth::B24,
|
||||
);
|
||||
tx.send(chunk.clone()).await.unwrap();
|
||||
|
||||
// Recevoir le chunk
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
|
||||
// Vérifier que c'est le même Arc (pas de clone des données)
|
||||
assert!(Arc::ptr_eq(&chunk, &received));
|
||||
assert_eq!(received.order(), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_timer_node_sample_rate_change() {
|
||||
let (mut node, tx) = TimerNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
let handle = node.get_position_handle();
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Chunk à 48000 Hz
|
||||
let chunk1 = AudioChunk::new(0, vec![[0i32; 2]; 48000], 48000, BitDepth::B24);
|
||||
tx.send(chunk1).await.unwrap();
|
||||
out_rx.recv().await.unwrap();
|
||||
|
||||
// Après 48000 samples à 48000 Hz = 1 seconde
|
||||
let pos1 = handle.position_sec().await;
|
||||
assert!((pos1 - 1.0).abs() < 0.0001);
|
||||
|
||||
// Chunk à 96000 Hz
|
||||
let chunk2 = AudioChunk::new(1, vec![[0i32; 2]; 96000], 96000, BitDepth::B24);
|
||||
tx.send(chunk2).await.unwrap();
|
||||
out_rx.recv().await.unwrap();
|
||||
|
||||
// Position calculée avec le nouveau sample rate
|
||||
let pos2 = handle.position_sec().await;
|
||||
let expected = (48000.0 + 96000.0) / 96000.0;
|
||||
assert!((pos2 - expected).abs() < 0.0001);
|
||||
}
|
||||
}
|
||||
363
old_code/pmoaudio/nodes/volume_node.rs
Normal file
363
old_code/pmoaudio/nodes/volume_node.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
//! 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).max(0.0);
|
||||
|
||||
// Créer un nouveau chunk avec le gain modifié (conversion vers dB)
|
||||
let modified_chunk =
|
||||
chunk.with_modified_gain_linear(total_volume as f64);
|
||||
|
||||
// Envoyer aux subscribers
|
||||
self.subscribers.push(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::*;
|
||||
use crate::BitDepth;
|
||||
|
||||
#[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::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
|
||||
tx.send(chunk).await.unwrap();
|
||||
|
||||
// Recevoir le chunk modifié
|
||||
let modified = out_rx.recv().await.unwrap();
|
||||
assert!((modified.gain_linear() - 0.5).abs() < 1e-6);
|
||||
|
||||
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::from_channels_f32(0, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
|
||||
slave_tx.send(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::from_channels_f32(1, vec![1.0; 100], vec![1.0; 100], 48000, BitDepth::B24);
|
||||
slave_tx.send(chunk2).await.unwrap();
|
||||
|
||||
// Le deuxième chunk devrait avoir un gain de 0.8 * 0.5 = 0.4 (≈ -7.96 dB)
|
||||
let _first = out_rx.recv().await.unwrap(); // gain ≈ 0.8
|
||||
let second = out_rx.recv().await.unwrap(); // gain ≈ 0.4
|
||||
|
||||
assert!((second.gain_linear() - 0.4).abs() < 0.01);
|
||||
|
||||
drop(master_tx);
|
||||
drop(slave_tx);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user