399 lines
11 KiB
Rust
399 lines
11 KiB
Rust
//! 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();
|
|
}
|
|
}
|