Files
pmomusic/old_code/pmoaudio/nodes/mpd_sink.rs

399 lines
11 KiB
Rust
Raw Normal View History

2025-10-11 15:36:06 +02:00
//! 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 {
2025-10-19 13:42:29 +02:00
return Err(AudioError::ProcessingError(
"Not connected to MPD".to_string(),
));
2025-10-11 15:36:06 +02:00
}
// 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()
2025-10-11 15:36:06 +02:00
} else {
Arc::clone(&chunk)
2025-10-11 15:36:06 +02:00
};
// 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;
2025-10-11 15:36:06 +02:00
}
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;
2025-10-11 15:36:06 +02:00
#[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();
2025-10-11 15:36:06 +02:00
}
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();
}
}