//! Sink qui encode les AudioSegment au format FLAC et les stocke dans le cache audio use pmoaudio::{ nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE}, pipeline::{Node, NodeLogic}, type_constraints::TypeRequirement, AudioChunk, AudioPipelineNode, AudioSegment, SyncMarker, _AudioSegment, }; use pmoaudiocache::AudioTrackMetadataExt; use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat}; use std::{ collections::VecDeque, pin::Pin, sync::Arc, task::{Context, Poll}, }; use tokio::{ io::{self, AsyncRead, ReadBuf}, sync::{mpsc, RwLock}, }; use tokio_util::sync::CancellationToken; /// Sink qui encode les `AudioSegment` reçus au format FLAC et les stocke dans le cache audio. /// /// Ce sink : /// - Filtre les chunks audio et ignore les autres syncmarkers (sauf TrackBoundary et EndOfStream) /// - Crée une nouvelle entrée de cache pour chaque TrackBoundary rencontré /// - Adapte automatiquement l'encodage FLAC selon la profondeur de bit du chunk (8/16/24/32-bit) /// - Copie les métadonnées du TrackBoundary dans le cache après ingestion /// - Peut optionnellement ajouter les tracks à une playlist via `register_playlist()` /// - Termine l'encodage proprement quand il reçoit EndOfStream // ═══════════════════════════════════════════════════════════════════════════ // FlacCacheSinkLogic - Logique métier pure // ═══════════════════════════════════════════════════════════════════════════ /// Logique pure d'encodage FLAC vers le cache pub struct FlacCacheSinkLogic { cache: Arc, covers: Arc, collection: Option, encoder_options: EncoderOptions, pcm_buffer_capacity: usize, #[cfg(feature = "playlist")] playlist_handle: Option>, } impl FlacCacheSinkLogic { pub fn new( cache: Arc, covers: Arc, collection: Option, encoder_options: EncoderOptions, pcm_buffer_capacity: usize, ) -> Self { Self { cache, covers, collection, encoder_options, pcm_buffer_capacity, #[cfg(feature = "playlist")] playlist_handle: None, } } #[cfg(feature = "playlist")] pub fn set_playlist_handle(&mut self, handle: Arc) { self.playlist_handle = Some(handle); } } #[async_trait::async_trait] impl NodeLogic for FlacCacheSinkLogic { async fn process( &mut self, input: Option>>, _output: Vec>>, stop_token: CancellationToken, ) -> Result<(), AudioError> { tracing::debug!("FlacCacheSink::process() started"); let mut rx = input.expect("FlacCacheSink must have input"); let mut track_number = 0; // Stocker les métadonnées du prochain TrackBoundary reçu en Phase 3 let mut next_track_metadata: Option>> = None; loop { // Attendre le premier chunk audio pour cette track tracing::debug!( "FlacCacheSink: Waiting for first audio chunk (track_number={})", track_number ); let (first_segment, track_metadata) = if let Some(metadata) = next_track_metadata.take() { // On a déjà reçu le TrackBoundary en Phase 3 de la track précédente tracing::debug!( "FlacCacheSink: Using TrackBoundary metadata from previous track's Phase 3" ); // Attendre juste le premier chunk match wait_for_first_audio_chunk(&mut rx, &stop_token).await { Ok(chunk) => { tracing::debug!("FlacCacheSink: Got first audio chunk"); (chunk, Some(metadata)) } Err(e) => { tracing::debug!("FlacCacheSink: No more audio available: {}", e); return Ok(()); } } } else { // Première track ou pas de TrackBoundary reçu en avance match wait_for_first_audio_chunk_with_metadata(&mut rx, &stop_token).await { Ok(result) => { tracing::debug!("FlacCacheSink: Got first audio chunk"); result } Err(e) => { // Plus d'audio disponible tracing::debug!("FlacCacheSink: No more audio available: {}", e); return Ok(()); } } }; // Extraire les informations du premier chunk let first_chunk = first_segment.as_chunk().unwrap(); let sample_rate = first_chunk.sample_rate(); let bits_per_sample = get_chunk_bit_depth(first_chunk); let format = PcmFormat { sample_rate, channels: 2, bits_per_sample, }; if let Err(err) = format.validate() { return Err(AudioError::ProcessingError(format!( "Invalid PCM format: {}", err ))); } // Créer le pipeline d'encodage pour cette track let (pcm_tx, pcm_rx) = mpsc::channel::>(self.pcm_buffer_capacity); // Préparer les options d'encodage avec les métadonnées du TrackBoundary let mut options_with_metadata = self.encoder_options.clone(); options_with_metadata.metadata = track_metadata.clone(); // Créer l'encoder tracing::debug!("FlacCacheSink: Creating FLAC encoder"); let reader = ByteStreamReader::new(pcm_rx); let flac_stream = encode_flac_stream(reader, format, options_with_metadata) .await .map_err(|e| { AudioError::ProcessingError(format!("FLAC encode init failed: {}", e)) })?; tracing::debug!("FlacCacheSink: FLAC encoder created"); // Ingérer le FLAC progressivement dans le cache // add_from_reader lance l'ingestion en arrière-plan et retourne dès que // le prebuffer (512 KB) est atteint, permettant un streaming progressif // Le cache skip automatiquement le header FLAC (512 octets) pour calculer le pk // à partir du contenu audio, évitant les collisions entre morceaux au même format let collection_ref = self.collection.as_deref(); tracing::debug!("FlacCacheSink: Starting cache ingestion and pump in parallel"); let cache_future = self.cache.add_from_reader( None, flac_stream, None, // Taille inconnue car streaming collection_ref, ); // Créer un channel dédié pour dispatcher les chunks vers ce pump let (track_tx, track_rx) = mpsc::channel::>(16); // Lancer le pump en arrière-plan avec son channel dédié // Cela permet à plusieurs pumps de tourner simultanément (écriture parallèle) let pump_handle = tokio::spawn(pump_track_segments_from_channel( first_segment, track_rx, pcm_tx, bits_per_sample, sample_rate, )); // Dispatcher les segments vers track_tx en parallèle de l'attente du prebuffer // Utiliser tokio::select! pour éviter le deadlock let start = std::time::Instant::now(); tracing::debug!("FlacCacheSink: Starting dispatcher loop with prebuffer wait"); // Pin la future pour pouvoir l'utiliser dans select! tokio::pin!(cache_future); // Phase 1: Dispatcher jusqu'à ce que le prebuffer soit terminé let mut end_of_stream_received = false; let mut early_track_boundary_received = false; let mut track_tx_opt = Some(track_tx); let pk = loop { tokio::select! { // Attendre le prebuffer result = &mut cache_future => { match result { Ok(pk) => { let prebuffer_time = start.elapsed(); tracing::info!("FlacCacheSink: Prebuffer complete with pk {} in {:?}", pk, prebuffer_time); break pk; // Sort de la loop pour faire les métadonnées et le push } Err(e) => { return Err(AudioError::ProcessingError(format!("Failed to add to cache: {}", e))); } } } // Dispatcher les segments depuis rx vers track_tx result = rx.recv() => { match result { Some(segment) => { // Si EndOfStream ou TrackBoundary a été reçu, ignorer tous les segments suivants // et continuer à attendre cache_future if end_of_stream_received || early_track_boundary_received { continue; } match &segment.segment { _AudioSegment::Chunk(_) => { // Dispatcher vers le pump if let Some(ref tx) = track_tx_opt { if tx.send(segment).await.is_err() { // Le pump est mort - erreur fatale tracing::error!("FlacCacheSink: pump died unexpectedly during prebuffer phase"); return Err(AudioError::ProcessingError("Pump task died".to_string())); } } } _AudioSegment::Sync(marker) => match &**marker { SyncMarker::TrackBoundary { metadata, .. } => { // TrackBoundary pendant le prebuffer - track courte (< 512KB) tracing::warn!( "FlacCacheSink: TrackBoundary received before prebuffer complete - track shorter than 512KB, closing pump and waiting for ingestion" ); // Stocker les métadonnées pour la prochaine track next_track_metadata = Some(metadata.clone()); // Fermer le track_tx pour que le pump se termine proprement track_tx_opt = None; // Marquer qu'on a reçu un TrackBoundary précoce early_track_boundary_received = true; // Continuer à attendre cache_future } SyncMarker::EndOfStream => { tracing::debug!("FlacCacheSink: EndOfStream during prebuffer - closing pump and waiting for ingestion to complete"); // Fermer le track_tx pour que le pump se termine proprement track_tx_opt = None; // Marquer qu'on a reçu EndOfStream et continuer à attendre cache_future end_of_stream_received = true; } _ => { // Transmettre les autres syncmarkers au pump if let Some(ref tx) = track_tx_opt { let _ = tx.send(segment).await; } } }, } } None => { // EOF sur rx pendant le prebuffer - attendre que cache_future se termine if !end_of_stream_received && !early_track_boundary_received { tracing::debug!("FlacCacheSink: EOF on rx during prebuffer, waiting for ingestion to complete"); track_tx_opt = None; end_of_stream_received = true; } // Continue à attendre cache_future } } } _ = stop_token.cancelled() => { drop(track_tx_opt); drop(pump_handle); return Ok(()); } } }; if let Some(transform) = self.cache.transform_metadata(&pk).await { tracing::debug!( "FlacCacheSink: Got transform metadata for pk {}: sr={:?}, bps={:?}, ch={:?}, ts={:?}", pk, transform.sample_rate, transform.bits_per_sample, transform.channels, transform.total_samples ); // Persister les métadonnées techniques via l'interface TrackMetadata let track_meta = self.cache.track_metadata(&pk); let mut meta = track_meta.write().await; if let Some(sr) = transform.sample_rate { if let Err(e) = meta.set_sample_rate(Some(sr)).await { tracing::error!( "FlacCacheSink: Failed to set sample_rate for pk {}: {:?}", pk, e ); } else { tracing::debug!("FlacCacheSink: Set sample_rate={} for pk {}", sr, pk); } } if let Some(bps) = transform.bits_per_sample { if let Err(e) = meta.set_bits_per_sample(Some(bps)).await { tracing::error!( "FlacCacheSink: Failed to set bits_per_sample for pk {}: {:?}", pk, e ); } else { tracing::debug!("FlacCacheSink: Set bits_per_sample={} for pk {}", bps, pk); } } if let Some(ch) = transform.channels { if let Err(e) = meta.set_channels(Some(ch)).await { tracing::error!( "FlacCacheSink: Failed to set channels for pk {}: {:?}", pk, e ); } else { tracing::debug!("FlacCacheSink: Set channels={} for pk {}", ch, pk); } } if let Some(ts) = transform.total_samples { if let Err(e) = meta.set_total_samples(Some(ts)).await { tracing::error!( "FlacCacheSink: Failed to set total_samples for pk {}: {:?}", pk, e ); } else { tracing::debug!("FlacCacheSink: Set total_samples={} for pk {}", ts, pk); } // Calculer la durée à partir de total_samples et sample_rate if let Some(sr) = transform.sample_rate { if sr > 0 { use std::time::Duration; let secs = (ts as f64 / sr as f64).round() as u64; if let Err(e) = meta.set_duration(Some(Duration::from_secs(secs))).await { tracing::error!( "FlacCacheSink: Failed to set duration for pk {}: {:?}", pk, e ); } else { tracing::debug!( "FlacCacheSink: Set duration={} secs for pk {}", secs, pk ); } } } } drop(meta); // Libérer le lock explicitement } else { tracing::warn!( "FlacCacheSink: No transform metadata available for pk {}", pk ); } // Phase 2: Prebuffer terminé! Copier les métadonnées et pusher à la playlist // Copier les métadonnées du TrackBoundary dans le cache // IMPORTANT: Faire ceci AVANT d'ajouter à la playlist pour que les métadonnées soient disponibles if let Some(src_metadata) = track_metadata.clone() { let dest_metadata = self.cache.track_metadata(&pk); // Utiliser copy_metadata_into pour copier toutes les métadonnées pmometadata::copy_metadata_into(&src_metadata, &dest_metadata) .await .map_err(|e| { AudioError::ProcessingError(format!( "Failed to copy metadata to cache: {}", e )) })?; let cover_pk_present = match dest_metadata.read().await.get_cover_pk().await { Ok(Some(existing_pk)) => { tracing::debug!( "FlacCacheSink: cover_pk already set for audio asset {} ({})", pk, existing_pk ); true } Ok(None) => false, Err(e) if e.is_transient() => { tracing::debug!( "FlacCacheSink: Transient error getting cover_pk for pk {}: {}", pk, e ); false } Err(e) => { tracing::warn!( "FlacCacheSink: Cannot obtain cover_pk for audio asset {}: {}", pk, e ); false } }; let url = if cover_pk_present { None } else { match dest_metadata.read().await.get_cover_url().await { Ok(url) => { tracing::debug!( "FlacCacheSink: Got cover URL for pk {}: {:?}", pk, url ); url } Err(e) if e.is_transient() => { tracing::debug!( "FlacCacheSink: Transient error getting cover URL for pk {}: {}", pk, e ); None } Err(e) => { tracing::warn!( "FlacCacheSink: Cannot obtain cover URL for audio asset {}: {}", pk, e ); None } } }; if let Some(cover_url) = url { tracing::debug!( "FlacCacheSink: Attempting to cache cover from URL: {}", cover_url ); match self .covers .add_from_url(&cover_url, self.collection.as_deref()) .await { Ok(pk_covers) => { tracing::info!("FlacCacheSink: Successfully cached cover for pk {} with cover pk {}", pk, pk_covers); if let Err(e) = dest_metadata .write() .await .set_cover_pk(Some(pk_covers)) .await { tracing::error!("FlacCacheSink: Failed to set cover_pk for audio asset {}: {:?}", pk, e); } } Err(e) => { tracing::warn!( "FlacCacheSink: Failed to cache cover for audio asset {}: {}", pk, e ); } } } else { tracing::debug!("FlacCacheSink: No cover URL available for pk {}", pk); } } // Push IMMÉDIATEMENT à la playlist (après prebuffer, avant pump complet!) #[cfg(feature = "playlist")] if let Some(ref playlist_handle) = self.playlist_handle { let push_start = std::time::Instant::now(); tracing::debug!("FlacCacheSink: Pushing pk {} to playlist", pk); playlist_handle.push(pk.clone()).await.map_err(|e| { AudioError::ProcessingError(format!("Failed to add to playlist: {}", e)) })?; tracing::info!( "FlacCacheSink: Successfully pushed to playlist in {:?}", push_start.elapsed() ); } // Si EndOfStream a été reçu pendant le prebuffer, on a déjà tout traité // Il faut juste attendre que le pump se termine et retourner if end_of_stream_received { tracing::debug!( "FlacCacheSink: EndOfStream was received during prebuffer, track complete" ); drop(pump_handle); track_number += 1; continue; // Passer à la track suivante (qui n'arrivera pas car EndOfStream) } // Si TrackBoundary précoce a été reçu pendant le prebuffer, passer à la track suivante if early_track_boundary_received { tracing::debug!( "FlacCacheSink: TrackBoundary was received during prebuffer, track complete, moving to next track" ); drop(pump_handle); track_number += 1; continue; // Passer à la track suivante (métadonnées déjà stockées dans next_track_metadata) } // Phase 3: Continuer à dispatcher jusqu'au TrackBoundary tracing::debug!( "FlacCacheSink: Continuing dispatch until TrackBoundary (pump runs in background)" ); let mut track_tx = track_tx_opt; // track_tx_opt contient Some(track_tx) car end_of_stream_received est false let mut pump_handle = Some(pump_handle); let mut pump_closed = false; loop { let segment = tokio::select! { result = rx.recv() => { match result { Some(seg) => seg, None => { // EOF sur rx drop(track_tx); drop(pump_handle); return Ok(()); } } } _ = stop_token.cancelled() => { drop(track_tx); drop(pump_handle); return Ok(()); } }; match &segment.segment { _AudioSegment::Chunk(_) => { // Continuer à dispatcher vers le pump (sauf si déjà fermé) if !pump_closed { if let Some(ref tx) = track_tx { if tx.send(segment).await.is_err() { // Le pump a fermé son channel - cela peut arriver si le fichier // était déjà en cache (add_from_reader retourne immédiatement) tracing::debug!( "FlacCacheSink: pump closed track_tx, checking pump status" ); drop(track_tx.take()); // Attendre que le pump se termine et vérifier le résultat if let Some(handle) = pump_handle.take() { match handle.await { Ok(Ok(_)) => { // Le pump s'est terminé proprement (fichier était en cache) tracing::debug!("FlacCacheSink: pump completed successfully, ignoring remaining chunks until TrackBoundary"); pump_closed = true; } Ok(Err(e)) => { // Le pump a rencontré une erreur tracing::error!( "FlacCacheSink: pump died with error: {}", e ); return Err(e); } Err(e) => { // Le pump task a paniqué tracing::error!( "FlacCacheSink: pump task panicked: {}", e ); return Err(AudioError::ProcessingError( "Pump task panicked".to_string(), )); } } } } } } // Si pump_closed, ignorer silencieusement le chunk } _AudioSegment::Sync(marker) => match &**marker { SyncMarker::TrackBoundary { metadata, .. } => { // Nouveau morceau - fermer le pump si pas déjà fermé tracing::debug!("FlacCacheSink: TrackBoundary received, closing pump and storing metadata for next track"); // Stocker les métadonnées pour la prochaine track next_track_metadata = Some(metadata.clone()); drop(track_tx.take()); drop(pump_handle.take()); track_number += 1; break; // Sort de la Phase 3, retour à la loop externe pour next track } SyncMarker::EndOfStream => { tracing::debug!("FlacCacheSink: EndOfStream received"); drop(track_tx.take()); drop(pump_handle.take()); return Ok(()); } _ => { // Transmettre les autres syncmarkers au pump (sauf si fermé) if !pump_closed { if let Some(ref tx) = track_tx { let _ = tx.send(segment).await; } } } }, } } } } } // ═══════════════════════════════════════════════════════════════════════════ // FlacCacheSink - Wrapper utilisant Node // ═══════════════════════════════════════════════════════════════════════════ pub struct FlacCacheSink { inner: Node, } impl FlacCacheSink { /// Crée un sink FLAC cache avec les options par défaut (compression 5, buffer de 16 segments). /// /// # Arguments /// /// * `cache` - Arc vers le cache audio où stocker les fichiers FLAC encodés pub fn new(cache: Arc, covers: Arc) -> Self { Self::with_channel_size(cache, covers, DEFAULT_CHANNEL_SIZE) } /// Crée un sink FLAC cache avec une taille de buffer MPSC personnalisée. /// /// # Arguments /// /// * `cache` - Arc vers le cache audio /// * `channel_size` - Taille du buffer MPSC (nombre de segments en attente avant backpressure) pub fn with_channel_size( cache: Arc, covers: Arc, channel_size: usize, ) -> Self { Self::with_config(cache, covers, channel_size, EncoderOptions::default(), None) } /// Crée un sink FLAC cache avec une configuration complète. /// /// # Arguments /// /// * `cache` - Arc vers le cache audio /// * `channel_size` - Taille du buffer MPSC /// * `encoder_options` - Options d'encodage FLAC (compression, etc.) /// * `collection` - Collection optionnelle à laquelle appartiennent les fichiers pub fn with_config( cache: Arc, covers: Arc, channel_size: usize, encoder_options: EncoderOptions, collection: Option, ) -> Self { let logic = FlacCacheSinkLogic::new(cache, covers, collection, encoder_options, 256); Self { inner: Node::new_with_input(logic, channel_size), } } /// Enregistre une playlist pour recevoir automatiquement les tracks sauvées dans le cache. /// /// # Arguments /// /// * `handle` - WriteHandle de la playlist qui recevra les pk des tracks #[cfg(feature = "playlist")] pub fn register_playlist(&mut self, handle: pmoplaylist::WriteHandle) { self.inner.logic_mut().set_playlist_handle(Arc::new(handle)); } } /// Attend le premier chunk audio (sans attendre de TrackBoundary) /// Utilisé quand on a déjà reçu le TrackBoundary en Phase 3 de la track précédente async fn wait_for_first_audio_chunk( rx: &mut mpsc::Receiver>, stop_token: &CancellationToken, ) -> Result, AudioError> { loop { let segment = tokio::select! { result = rx.recv() => { result.ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))? } _ = stop_token.cancelled() => { return Err(AudioError::ProcessingError("Cancelled".into())); } }; match &segment.segment { _AudioSegment::Chunk(chunk) => { if chunk.len() == 0 { return Err(AudioError::ProcessingError("Received empty chunk".into())); } return Ok(segment); } _AudioSegment::Sync(marker) => match &**marker { SyncMarker::TrackBoundary { .. } => { // On ne devrait pas recevoir de TrackBoundary ici car on l'a déjà tracing::warn!( "FlacCacheSink: Unexpected TrackBoundary while waiting for first chunk" ); continue; } SyncMarker::EndOfStream => { return Err(AudioError::ProcessingError( "EndOfStream received before any audio".into(), )); } _ => { // Ignorer TopZeroSync, Heartbeat, etc. continue; } }, } } } async fn wait_for_first_audio_chunk_with_metadata( rx: &mut mpsc::Receiver>, stop_token: &CancellationToken, ) -> Result< ( Arc, Option>>, ), AudioError, > { let mut track_metadata: Option>> = None; loop { let segment = tokio::select! { result = rx.recv() => { result.ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))? } _ = stop_token.cancelled() => { return Err(AudioError::ProcessingError("Cancelled".into())); } }; match &segment.segment { _AudioSegment::Chunk(chunk) => { if chunk.len() == 0 { return Err(AudioError::ProcessingError("Received empty chunk".into())); } return Ok((segment, track_metadata)); } _AudioSegment::Sync(marker) => match &**marker { SyncMarker::TrackBoundary { metadata, .. } => { // Capturer les métadonnées du TrackBoundary track_metadata = Some(metadata.clone()); continue; } SyncMarker::EndOfStream => { return Err(AudioError::ProcessingError( "EndOfStream received before any audio".into(), )); } _ => { // Ignorer TopZeroSync, Heartbeat, etc. continue; } }, } } } /// Pompe les segments pour une seule track depuis un channel dédié. /// /// Cette version permet d'avoir plusieurs pumps en parallèle (pour cache progressif), /// car chaque pump a son propre channel et ne bloque pas le traitement des tracks suivantes. async fn pump_track_segments_from_channel( first_segment: Arc, mut track_rx: mpsc::Receiver>, pcm_tx: mpsc::Sender>, bits_per_sample: u8, expected_rate: u32, ) -> Result<(u64, u64, f64), AudioError> { let mut chunks = 0u64; let mut samples = 0u64; let mut duration_sec = 0.0f64; // Traiter le premier segment if let Some(chunk) = first_segment.as_chunk() { let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?; if !pcm_bytes.is_empty() { if pcm_tx.send(pcm_bytes).await.is_err() { drop(pcm_tx); tracing::debug!("pump_track_segments_from_channel: pcm_tx closed on first segment"); return Ok((chunks, samples, duration_sec)); } chunks += 1; samples += chunk.len() as u64; duration_sec += chunk.len() as f64 / expected_rate as f64; } } // Boucle sur les segments depuis le channel dédié loop { let segment = match track_rx.recv().await { Some(seg) => seg, None => { // Channel fermé - la track est terminée (TrackBoundary a été reçu en amont) drop(pcm_tx); tracing::debug!("pump_track_segments_from_channel: channel closed, track finished"); return Ok((chunks, samples, duration_sec)); } }; match &segment.segment { _AudioSegment::Chunk(chunk) => { if chunk.sample_rate() != expected_rate { return Err(AudioError::ProcessingError(format!( "FlacCacheSink: inconsistent sample rate ({} vs {})", chunk.sample_rate(), expected_rate ))); } let pcm_bytes = chunk_to_pcm_bytes(&chunk, bits_per_sample)?; if pcm_bytes.is_empty() { continue; } if pcm_tx.send(pcm_bytes).await.is_err() { // Le cache a fermé le channel (erreur ou déjà en cache) drop(pcm_tx); tracing::debug!("pump_track_segments_from_channel: pcm_tx closed"); return Ok((chunks, samples, duration_sec)); } chunks += 1; samples += chunk.len() as u64; duration_sec += chunk.len() as f64 / expected_rate as f64; } _AudioSegment::Sync(_marker) => { // Ignorer les syncmarkers - le TrackBoundary est géré en amont // Le channel sera fermé quand le TrackBoundary est détecté } } } } /// Détermine la profondeur de bit d'un chunk audio fn get_chunk_bit_depth(chunk: &AudioChunk) -> u8 { match chunk { AudioChunk::I16(_) => 16, AudioChunk::I24(_) => 24, AudioChunk::I32(_) => 32, AudioChunk::F32(_) => 32, // Les flottants seront convertis en 32-bit AudioChunk::F64(_) => 32, // Les flottants seront convertis en 32-bit } } /// Convertit un chunk audio en bytes PCM avec la profondeur de bit spécifiée fn chunk_to_pcm_bytes(chunk: &AudioChunk, bits_per_sample: u8) -> Result, AudioError> { // Vérifier que le chunk est de type entier match chunk { AudioChunk::F32(_) | AudioChunk::F64(_) => { return Err(AudioError::ProcessingError( "FlacCacheSink only supports integer audio chunks (I16, I24, I32)".into(), )); } _ => {} } let len = chunk.len(); let bytes_per_frame = (bits_per_sample / 8) as usize * 2; // 2 channels let mut bytes = Vec::with_capacity(len * bytes_per_frame); // Convertir selon le type du chunk match (chunk, bits_per_sample) { // I16 source (AudioChunk::I16(data), 16) => { for frame in data.get_frames() { bytes.extend_from_slice(&frame[0].to_le_bytes()); bytes.extend_from_slice(&frame[1].to_le_bytes()); } } (AudioChunk::I16(data), 24) => { for frame in data.get_frames() { let left = (frame[0] as i32) << 8; let right = (frame[1] as i32) << 8; bytes.extend_from_slice(&left.to_le_bytes()[..3]); bytes.extend_from_slice(&right.to_le_bytes()[..3]); } } (AudioChunk::I16(data), 32) => { for frame in data.get_frames() { let left = (frame[0] as i32) << 16; let right = (frame[1] as i32) << 16; bytes.extend_from_slice(&left.to_le_bytes()); bytes.extend_from_slice(&right.to_le_bytes()); } } // I24 source (AudioChunk::I24(data), 16) => { for frame in data.get_frames() { let left = (frame[0].as_i32() >> 8) as i16; let right = (frame[1].as_i32() >> 8) as i16; bytes.extend_from_slice(&left.to_le_bytes()); bytes.extend_from_slice(&right.to_le_bytes()); } } (AudioChunk::I24(data), 24) => { for frame in data.get_frames() { bytes.extend_from_slice(&frame[0].as_i32().to_le_bytes()[..3]); bytes.extend_from_slice(&frame[1].as_i32().to_le_bytes()[..3]); } } (AudioChunk::I24(data), 32) => { for frame in data.get_frames() { let left = frame[0].as_i32() << 8; let right = frame[1].as_i32() << 8; bytes.extend_from_slice(&left.to_le_bytes()); bytes.extend_from_slice(&right.to_le_bytes()); } } // I32 source (AudioChunk::I32(data), 16) => { for frame in data.get_frames() { let left = (frame[0] >> 16) as i16; let right = (frame[1] >> 16) as i16; bytes.extend_from_slice(&left.to_le_bytes()); bytes.extend_from_slice(&right.to_le_bytes()); } } (AudioChunk::I32(data), 24) => { for frame in data.get_frames() { let left = frame[0] >> 8; let right = frame[1] >> 8; bytes.extend_from_slice(&left.to_le_bytes()[..3]); bytes.extend_from_slice(&right.to_le_bytes()[..3]); } } (AudioChunk::I32(data), 32) => { for frame in data.get_frames() { bytes.extend_from_slice(&frame[0].to_le_bytes()); bytes.extend_from_slice(&frame[1].to_le_bytes()); } } _ => { return Err(AudioError::ProcessingError(format!( "Unsupported bits_per_sample: {}", bits_per_sample ))); } } Ok(bytes) } struct ByteStreamReader { rx: mpsc::Receiver>, buffer: VecDeque, finished: bool, } impl ByteStreamReader { fn new(rx: mpsc::Receiver>) -> 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> { 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 pour une track individuelle. #[derive(Debug, Clone)] pub struct TrackStats { pub pk: String, pub track_number: usize, pub chunks_received: u64, pub total_samples: u64, pub total_duration_sec: f64, } /// Statistiques produites par le `FlacCacheSink`. #[derive(Debug, Clone)] pub struct FlacCacheSinkStats { pub tracks: Vec, } #[async_trait::async_trait] impl AudioPipelineNode for FlacCacheSink { fn get_tx(&self) -> Option>> { self.inner.get_tx() } fn register(&mut self, _child: Box) { panic!("FlacCacheSink is a terminal sink and cannot have children"); } async fn run(self: Box, stop_token: CancellationToken) -> Result<(), AudioError> { Box::new(self.inner).run(stop_token).await } } impl TypedAudioNode for FlacCacheSink { fn input_type(&self) -> Option { // FlacCacheSink accepte n'importe quel type entier (I16, I24, I32) // mais rejette les chunks flottants Some(TypeRequirement::any_integer()) } fn output_type(&self) -> Option { // FlacCacheSink est un sink, il ne produit pas d'audio None } }