diff --git a/pmoaudio/src/nodes/audio_sink.rs b/pmoaudio/src/nodes/audio_sink.rs index 37126896..e71798d1 100644 --- a/pmoaudio/src/nodes/audio_sink.rs +++ b/pmoaudio/src/nodes/audio_sink.rs @@ -172,11 +172,70 @@ fn chunk_to_f32_interleaved(chunk: &AudioChunk) -> Vec { // ═══════════════════════════════════════════════════════════════════════════ /// Logique pure de lecture audio via cpal -pub struct AudioSinkLogic {} +pub struct AudioSinkLogic { + use_null_output: bool, +} impl AudioSinkLogic { pub fn new() -> Self { - Self {} + Self { + use_null_output: false, + } + } + + pub fn with_null_output() -> Self { + Self { + use_null_output: true, + } + } + + /// Version null output - consomme les segments sans les jouer + async fn process_null_output( + mut rx: mpsc::Receiver>, + stop_token: CancellationToken, + ) -> Result<(), AudioError> { + loop { + let segment = tokio::select! { + result = rx.recv() => { + match result { + Some(seg) => seg, + None => { + tracing::debug!("AudioSinkLogic (null): input channel closed"); + return Ok(()); + } + } + } + _ = stop_token.cancelled() => { + tracing::debug!("AudioSinkLogic (null): cancelled"); + return Ok(()); + } + }; + + // Juste logger les segments sans les jouer + match &segment.segment { + crate::_AudioSegment::Chunk(chunk) => { + tracing::trace!( + "AudioSink (null): consumed chunk with {} frames at {}Hz", + chunk.len(), + chunk.sample_rate() + ); + } + crate::_AudioSegment::Sync(marker) => { + match **marker { + SyncMarker::TrackBoundary { .. } => { + tracing::debug!("AudioSink (null): TrackBoundary received"); + } + SyncMarker::EndOfStream => { + tracing::debug!("AudioSink (null): EndOfStream received"); + return Ok(()); + } + _ => { + tracing::trace!("AudioSink (null): sync marker"); + } + } + } + } + } } } @@ -198,6 +257,12 @@ impl NodeLogic for AudioSinkLogic { tracing::debug!("AudioSinkLogic::process started"); + // Si null output, juste consommer les segments sans jouer + if self.use_null_output { + tracing::debug!("Using null audio output (no playback)"); + return Self::process_null_output(rx, stop_token).await; + } + // Créer le buffer partagé let buffer = Arc::new(Mutex::new(SharedBuffer::new())); let buffer_clone = buffer.clone(); @@ -485,6 +550,14 @@ impl AudioSink { inner: Node::new_with_input(AudioSinkLogic::new(), channel_size), } } + + /// Crée un AudioSink avec null output (pour tests sans carte audio) + /// Consomme les segments audio sans les jouer + pub fn with_null_output() -> Self { + Self { + inner: Node::new_with_input(AudioSinkLogic::with_null_output(), DEFAULT_CHANNEL_SIZE), + } + } } impl Default for AudioSink { diff --git a/pmocache/src/cache.rs b/pmocache/src/cache.rs index 938eb165..2fbe7705 100755 --- a/pmocache/src/cache.rs +++ b/pmocache/src/cache.rs @@ -387,10 +387,11 @@ impl Cache { where R: AsyncRead + Send + Unpin + 'static, { - // 1. Lire jusqu'à 1024 octets (ou ce qui est disponible) - let header = crate::download::peek_reader_header(&mut reader, 1024) + // 1. Lire EXACTEMENT 1024 octets (ou EOF si fichier plus petit) + // Utilise read_exact_or_eof qui boucle jusqu'à avoir tous les octets demandés + let header = crate::download::read_exact_or_eof(&mut reader, 1024) .await - .map_err(|e| anyhow!("Failed to peek reader header: {}", e))?; + .map_err(|e| anyhow!("Failed to read header bytes: {}", e))?; // 2. Calculer le pk en utilisant au plus les 512 derniers octets // Ceci évite les collisions pour les fichiers avec headers identiques (ex: FLAC) @@ -399,7 +400,7 @@ impl Cache { explicit } else { let pk_bytes = if header.len() > 512 { - // Fichier >= 512 octets: utiliser les octets 512+ (au plus 512 octets) + // Fichier >= 512 octets: utiliser les octets 512-1024 (contenu audio pour FLAC) &header[512..] } else { // Petit fichier < 512 octets: utiliser tout le contenu diff --git a/pmocache/src/download.rs b/pmocache/src/download.rs index 636e89b6..aaeb12d6 100644 --- a/pmocache/src/download.rs +++ b/pmocache/src/download.rs @@ -600,3 +600,37 @@ where buffer.truncate(n); Ok(buffer) } + +/// Lit exactement `size` octets du reader, ou jusqu'à EOF. +/// +/// Contrairement à `peek_reader_header`, cette fonction boucle jusqu'à avoir lu +/// exactement `size` octets (ou atteindre EOF). Ceci est crucial pour calculer +/// un pk fiable sur un nombre d'octets précis. +/// +/// # Returns +/// +/// Le buffer contenant exactement `size` octets, ou moins si EOF est atteint +pub async fn read_exact_or_eof(reader: &mut R, size: usize) -> Result, String> +where + R: AsyncRead + Unpin, +{ + let mut buffer = vec![0u8; size]; + let mut total_read = 0; + + while total_read < size { + let n = reader + .read(&mut buffer[total_read..]) + .await + .map_err(|e| format!("Failed to read from stream: {}", e))?; + + if n == 0 { + // EOF atteint + buffer.truncate(total_read); + return Ok(buffer); + } + + total_read += n; + } + + Ok(buffer) +} diff --git a/pmoparadise/examples/play_and_cache.rs b/pmoparadise/examples/play_and_cache.rs index e971c843..ae1fd10f 100644 --- a/pmoparadise/examples/play_and_cache.rs +++ b/pmoparadise/examples/play_and_cache.rs @@ -50,8 +50,8 @@ async fn main() -> Result<(), Box> { // Récupérer les arguments let args: Vec = env::args().collect(); - if args.len() != 2 { - eprintln!("Usage: {} ", args[0]); + if args.len() < 2 { + eprintln!("Usage: {} [--null-audio]", args[0]); eprintln!(); eprintln!("Downloads a Radio Paradise block, caches it, and plays it simultaneously."); eprintln!(); @@ -60,6 +60,9 @@ async fn main() -> Result<(), Box> { eprintln!(" 1 - Mellow Mix (smooth, chilled music)"); eprintln!(" 2 - Rock Mix (classic & modern rock)"); eprintln!(" 3 - World/Etc Mix (global sounds)"); + eprintln!(); + eprintln!("Options:"); + eprintln!(" --null-audio Don't play audio (for testing without audio device)"); std::process::exit(1); } @@ -71,7 +74,12 @@ async fn main() -> Result<(), Box> { } }; + let use_null_audio = args.len() > 2 && args[2] == "--null-audio"; + tracing::info!("Channel ID: {}", channel_id); + if use_null_audio { + tracing::info!("Using null audio output (no playback)"); + } // ═══════════════════════════════════════════════════════════════════════════ // Initialiser les caches et le gestionnaire de playlist @@ -188,7 +196,11 @@ async fn main() -> Result<(), Box> { tracing::debug!("PlaylistSource created"); // Créer le sink audio - let audio_sink = AudioSink::new(); + let audio_sink = if use_null_audio { + AudioSink::with_null_output() + } else { + AudioSink::new() + }; tracing::debug!("AudioSink created"); // Connecter playlist → audio