use crate::{ nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS}, pipeline::AudioPipelineNode, type_constraints::TypeRequirement, AudioChunk, AudioChunkData, AudioSegment, I24, }; use pmoflac::{decode_audio_stream, AudioFileMetadata, StreamInfo}; use pmometadata::{MemoryTrackMetadata, TrackMetadata}; use std::{path::PathBuf, sync::Arc, time::Duration}; use tokio::{fs::File, io::AsyncReadExt, sync::mpsc}; use tokio_util::sync::CancellationToken; use tracing; /// FileSource - Lit un fichier audio et publie des `AudioSegment` /// /// Cette source utilise `pmoflac` pour décoder le fichier (FLAC/MP3/OGG/WAV/AIFF) /// puis transforme les échantillons PCM en `AudioSegment` stéréo avec le type approprié /// (I16, I24, ou I32) selon la profondeur de bit du fichier source. /// /// Le node émet trois types de syncmarkers : /// - `TopZeroSync` au début du flux /// - `TrackBoundary` avec les métadonnées du fichier /// - `EndOfStream` à la fin du flux pub struct FileSource { path: PathBuf, chunk_frames: usize, child_txs: Vec>>, children: Vec>, } impl FileSource { /// Crée une nouvelle source de fichier avec calcul automatique de la taille des chunks. /// /// La taille des chunks sera calculée automatiquement pour obtenir environ 50ms /// de latence par chunk, en fonction du sample rate du fichier. /// /// * `path` - chemin du fichier audio à lire pub fn new>(path: P) -> Self { Self::with_chunk_size(path, 0) // 0 = auto-calculer } /// Crée une nouvelle source de fichier avec une taille de chunk spécifique. /// /// * `path` - chemin du fichier audio à lire /// * `chunk_frames` - nombre d'échantillons par canal par chunk (0 = auto) pub fn with_chunk_size>(path: P, chunk_frames: usize) -> Self { Self { path: path.into(), chunk_frames, child_txs: Vec::new(), children: Vec::new(), } } } 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 ))), } } /// Convertit des bytes PCM en AudioSegment avec le type approprié fn bytes_to_segment( chunk_bytes: &[u8], info: &StreamInfo, frames: usize, order: u64, timestamp_sec: f64, ) -> Result, AudioError> { let bytes_per_sample = info.bytes_per_sample(); let channels = info.channels as usize; let frame_bytes = bytes_per_sample * channels; // Créer le chunk du bon type selon la profondeur de bit let chunk = match info.bits_per_sample { 16 => { // Type I16 let mut stereo = Vec::with_capacity(frames); for frame_idx in 0..frames { let base = frame_idx * frame_bytes; let l = i16::from_le_bytes( chunk_bytes[base..base + bytes_per_sample] .try_into() .unwrap(), ); let r = if channels == 1 { l } else { i16::from_le_bytes( chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample] .try_into() .unwrap(), ) }; stereo.push([l, r]); } let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0); AudioChunk::I16(chunk_data) } 24 => { // Type I24 let mut stereo = Vec::with_capacity(frames); for frame_idx in 0..frames { let base = frame_idx * frame_bytes; let l_i32 = { let mut buf = [0u8; 4]; buf[..3].copy_from_slice(&chunk_bytes[base..base + 3]); // Sign extend if chunk_bytes[base + 2] & 0x80 != 0 { buf[3] = 0xFF; } i32::from_le_bytes(buf) }; let l = I24::new(l_i32).ok_or_else(|| { AudioError::ProcessingError(format!("Invalid I24 value: {}", l_i32)) })?; let r = if channels == 1 { l } else { let r_i32 = { let mut buf = [0u8; 4]; buf[..3].copy_from_slice( &chunk_bytes[base + bytes_per_sample..base + bytes_per_sample + 3], ); // Sign extend if chunk_bytes[base + bytes_per_sample + 2] & 0x80 != 0 { buf[3] = 0xFF; } i32::from_le_bytes(buf) }; I24::new(r_i32).ok_or_else(|| { AudioError::ProcessingError(format!("Invalid I24 value: {}", r_i32)) })? }; stereo.push([l, r]); } let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0); AudioChunk::I24(chunk_data) } 32 => { // Type I32 let mut stereo = Vec::with_capacity(frames); for frame_idx in 0..frames { let base = frame_idx * frame_bytes; let l = i32::from_le_bytes( chunk_bytes[base..base + bytes_per_sample] .try_into() .unwrap(), ); let r = if channels == 1 { l } else { i32::from_le_bytes( chunk_bytes[base + bytes_per_sample..base + 2 * bytes_per_sample] .try_into() .unwrap(), ) }; stereo.push([l, r]); } let chunk_data = AudioChunkData::new(stereo, info.sample_rate, 0.0); AudioChunk::I32(chunk_data) } other => { return Err(AudioError::ProcessingError(format!( "Unsupported bit depth: {}", other ))) } }; // Créer le segment audio Ok(Arc::new(AudioSegment { order, timestamp_sec, segment: crate::_AudioSegment::Chunk(Arc::new(chunk)), })) } #[async_trait::async_trait] impl AudioPipelineNode for FileSource { fn get_tx(&self) -> Option>> { // FileSource est une source, elle n'a pas d'input None } fn register(&mut self, child: Box) { // Extraire le tx du child avant de le stocker if let Some(tx) = child.get_tx() { self.child_txs.push(tx.clone()); } self.children.push(child); } async fn run( self: Box, stop_token: CancellationToken, ) -> Result<(), AudioError> { let Self { path, chunk_frames, child_txs, children, } = *self; // 1. Spawner tous les enfants AVANT de commencer à lire let mut child_handles = Vec::new(); for child in children { let child_token = stop_token.child_token(); let handle = tokio::spawn(async move { child.run(child_token).await }); child_handles.push(handle); } // 2. Faire le travail de lecture du fichier let work_result: Result<(), AudioError> = async { // Macro helper pour envoyer à tous les enfants macro_rules! send_to_children { ($segment:expr) => { for tx in &child_txs { if tx.send($segment.clone()).await.is_err() { tracing::warn!("FileSource: child died during send"); return Err(AudioError::SendError); } } }; } // Ouvrir le fichier let file = File::open(&path).await.map_err(|e| { AudioError::ProcessingError(format!("Failed to open {:?}: {}", path, e)) })?; // Décoder le flux audio 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)?; // Calculer la taille des chunks si non spécifiée (0 = auto) let chunk_frames = if chunk_frames == 0 { let frames = (stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize; frames.next_power_of_two().max(256) } else { chunk_frames.max(1) }; // Émettre TopZeroSync let top_zero = AudioSegment::new_top_zero_sync(); send_to_children!(top_zero); // Extraire et émettre les métadonnées du fichier if let Ok(file_metadata) = AudioFileMetadata::from_file(&path) { let mut metadata = MemoryTrackMetadata::new(); if let Some(title) = file_metadata.title { let _ = metadata.set_title(Some(title)).await; } if let Some(artist) = file_metadata.artist { let _ = metadata.set_artist(Some(artist)).await; } if let Some(album) = file_metadata.album { let _ = metadata.set_album(Some(album)).await; } if let Some(year) = file_metadata.year { let _ = metadata.set_year(Some(year)).await; } if let Some(duration_secs) = file_metadata.duration_secs { let _ = metadata .set_duration(Some(Duration::from_secs(duration_secs))) .await; } let track_boundary = AudioSegment::new_track_boundary( 0, 0.0, Arc::new(tokio::sync::RwLock::new(metadata)), ); send_to_children!(track_boundary); } // Préparer la lecture des chunks audio let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize; let chunk_byte_len = chunk_frames * frame_bytes; let mut pending = Vec::new(); let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames)]; let mut chunk_index = 0u64; let mut total_frames = 0u64; // Lire et émettre les chunks audio loop { tokio::select! { _ = stop_token.cancelled() => { tracing::info!("FileSource: stop requested"); break; } read_result = stream.read(&mut read_buf) => { // Remplir le buffer if pending.len() < chunk_byte_len { let read = read_result.map_err(|e| { AudioError::ProcessingError(format!("I/O error while decoding: {}", e)) })?; if read == 0 && pending.is_empty() { break; } if read > 0 { pending.extend_from_slice(&read_buf[..read]); } } if pending.is_empty() { break; } // Extraire un chunk let frames_in_pending = pending.len() / frame_bytes; let frames_to_emit = frames_in_pending.min(chunk_frames); if frames_to_emit == 0 { break; } let take_bytes = frames_to_emit * frame_bytes; let chunk_bytes = pending.drain(..take_bytes).collect::>(); // Calculer le timestamp let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64; // Créer et envoyer le segment audio let segment = bytes_to_segment( &chunk_bytes, &stream_info, frames_to_emit, chunk_index, timestamp_sec, )?; send_to_children!(segment); chunk_index += 1; total_frames += frames_to_emit as u64; } } } // Traiter le reste éventuel (moins qu'un chunk complet) if !pending.is_empty() { let frames = pending.len() / frame_bytes; if frames > 0 { let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64; let segment = bytes_to_segment(&pending, &stream_info, frames, chunk_index, timestamp_sec)?; send_to_children!(segment); total_frames += frames as u64; chunk_index += 1; } } // Émettre EndOfStream let final_timestamp = total_frames as f64 / stream_info.sample_rate as f64; let eos = AudioSegment::new_end_of_stream(chunk_index, final_timestamp); send_to_children!(eos); // Attendre la fin du décodage stream .wait() .await .map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?; Ok(()) }.await; // 3. Arrêter les enfants qui tournent encore (descendant uniquement) stop_token.cancel(); // 4. Fermer les channels pour signaler EOF drop(child_txs); // 5. Attendre que TOUS les enfants se terminent for handle in child_handles { match handle.await { Ok(Ok(())) => { // Enfant terminé normalement } Ok(Err(e)) => { // Enfant en erreur → propager tracing::error!("FileSource: child error: {}", e); return Err(e); } Err(e) => { // Panic dans l'enfant tracing::error!("FileSource: child panic: {}", e); return Err(AudioError::ProcessingError(format!("Child panic: {}", e))); } } } // 6. Retourner notre propre résultat (montant vers le parent) work_result } } impl TypedAudioNode for FileSource { fn input_type(&self) -> Option { // FileSource est une source, elle ne consomme pas d'audio None } fn output_type(&self) -> Option { // FileSource peut produire n'importe quel type entier (I16, I24, I32) // selon la profondeur de bit du fichier source Some(TypeRequirement::any_integer()) } } #[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(); // Créer un collecteur simple qui transmet les segments à un channel de test struct TestCollectorNode { tx: mpsc::Sender>, rx: mpsc::Receiver>, test_tx: mpsc::Sender>, } impl TestCollectorNode { fn new(test_tx: mpsc::Sender>) -> Self { let (tx, rx) = mpsc::channel(16); Self { tx, rx, test_tx, } } } #[async_trait::async_trait] impl AudioPipelineNode for TestCollectorNode { fn get_tx(&self) -> Option>> { Some(self.tx.clone()) } fn register(&mut self, _child: Box) { panic!("TestCollectorNode is a terminal node"); } async fn run( mut self: Box, stop_token: CancellationToken, ) -> Result<(), AudioError> { // Transférer tous les segments au test loop { tokio::select! { _ = stop_token.cancelled() => { break; } segment = self.rx.recv() => { match segment { Some(seg) => { if self.test_tx.send(seg).await.is_err() { break; } } None => break, } } } } Ok(()) } } let (test_tx, mut rx) = mpsc::channel(1024); let mut source = FileSource::with_chunk_size(&flac_path, 64); let collector = TestCollectorNode::new(test_tx); source.register(Box::new(collector)); tokio::spawn(async move { let token = CancellationToken::new(); Box::new(source).run(token).await.unwrap(); }); let mut received_frames = 0usize; let mut received_syncmarkers = 0usize; let mut seen_top_zero = false; let mut seen_eos = false; while let Some(segment) = rx.recv().await { if segment.is_audio_chunk() { if let Some(chunk) = segment.as_chunk() { received_frames += chunk.len(); assert_eq!(chunk.sample_rate(), sample_rate); } } else { received_syncmarkers += 1; if let Some(marker) = segment.as_sync_marker() { match **marker { crate::SyncMarker::TopZeroSync => seen_top_zero = true, crate::SyncMarker::EndOfStream => seen_eos = true, crate::SyncMarker::TrackBoundary { .. } => {} _ => {} } } } } // Vérifier que tous les frames ont été reçus assert_eq!(received_frames, frames); // Vérifier qu'on a bien reçu des syncmarkers assert!(received_syncmarkers >= 2); // Au moins TopZeroSync et EndOfStream assert!(seen_top_zero, "Should have received TopZeroSync"); assert!(seen_eos, "Should have received EndOfStream"); } }