# RadioParadiseStreamSource - Documentation Technique ## Vue d'ensemble `RadioParadiseStreamSource` est un nœud source pour `pmoaudio` qui télécharge et décode les blocs FLAC de Radio Paradise en temps réel, avec gestion automatique des transitions entre pistes (TrackBoundary). ## Architecture ### Pattern Node Suit l'architecture séparée logique/pipeline de `pmoaudio` : ``` RadioParadiseStreamSource (wrapper) └── Node └── RadioParadiseStreamSourceLogic (logique métier) ``` ### RadioParadiseStreamSourceLogic Responsabilités : - **File d'attente** : `VecDeque` pour les blocks à télécharger - **Cache anti-redondance** : `VecDeque` pour 10 blocs récents (FIFO) - **Téléchargement** : Fetch bloc FLAC (bitrate=4 uniquement) - **Décodage** : Stream FLAC via `pmoflac::decode_audio_stream` - **Timing** : Calcul précis pour insertion TrackBoundary ## Flux d'exécution ``` ┌─────────────────────────────────────────────────────────┐ │ 1. Attente block ID (timeout 3s) │ │ └─> VecDeque::pop_front() │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 2. Vérification cache │ │ └─> VecDeque::contains(&event_id) │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 3. Téléchargement métadonnées │ │ └─> client.get_block(event_id) │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 4. Téléchargement FLAC (bitrate=4) │ │ └─> client.download_block_file(&block, 4) │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 5. Décodage streaming │ │ └─> pmoflac::decode_audio_stream(reader) │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 6. Découpage en chunks │ │ └─> pcm_to_audio_chunk(pcm, sr, bps) │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 7. Insertion TrackBoundary (timing sample-based) │ │ └─> elapsed_ms = (total_samples * 1000) / sr │ └─────────────────────────────────────────────────────────┘ ``` ## Timing TrackBoundary ### Algorithme ```rust let elapsed_ms = (total_samples * 1000) / sample_rate as u64; if elapsed_ms >= song.elapsed { // Envoyer TrackBoundary AVANT le chunk (même order) send_track_boundary(*order, song, block).await; } ``` ### Exemple concret Bloc FLAC contenant 3 chansons : - Song 0 : `elapsed = 0ms` - Song 1 : `elapsed = 180000ms` (3min) - Song 2 : `elapsed = 420000ms` (7min) Timeline : ``` 0ms 180000ms 420000ms │ │ │ Song 0 TrackBoundary TrackBoundary └─> Song 1 └─> Song 2 ``` ## SyncMarker Order **Règle** : TrackBoundary a le **même order** que le chunk suivant. ```rust // TrackBoundary order = 42 AudioSegment::new_sync(42, SyncMarker::TrackBoundary { ... }) // Chunk suivant order = 42 AudioSegment::new_audio(42, AudioChunk::I16(...)) ``` ## Gestion du cache ### Stratégie FIFO simple ```rust const RECENT_BLOCKS_CACHE_SIZE: usize = 10; fn mark_block_downloaded(&mut self, event_id: EventId) { // Retirer tous les éléments excédentaires (garantit <= CACHE_SIZE) while self.recent_blocks.len() >= RECENT_BLOCKS_CACHE_SIZE { self.recent_blocks.pop_front(); } // Puis ajouter le nouveau bloc self.recent_blocks.push_back(event_id); } ``` **Avantages VecDeque** : - ✅ Ordre FIFO garanti (le plus ancien est toujours retiré) - ✅ Simple et prévisible - ✅ Robuste : `while` garantit exactement 10 éléments max, même en cas d'état anormal - ✅ Ne dépasse jamais la capacité pré-allouée (retire avant d'ajouter) - ✅ Pour 10 éléments, `contains()` en O(n) reste très performant ## Support FLAC ### Formats supportés - **16-bit** : `AudioChunk::I16` - **24-bit** : `AudioChunk::I24` ### Conversion PCM ```rust match bits_per_sample { 16 => { let samples: Vec = pcm_data .chunks_exact(2) .map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]])) .collect(); AudioChunk::I16(...) } 24 => { let samples: Vec = pcm_data .chunks_exact(3) .map(|chunk| { let value = i32::from_le_bytes([chunk[0], chunk[1], chunk[2], 0]) >> 8; I24::from_i32(value) }) .collect(); AudioChunk::I24(...) } } ``` ## Métadonnées ### TrackMetadata Champs extraits de `Song` : - `title` : Titre de la chanson - `artist` : Artiste - `album` : Album (optionnel) - `year` : Année (optionnel) - `cover_url` : URL de la pochette (async via tokio::spawn) ### Gestion asynchrone du cover ```rust tokio::spawn(async move { if let Ok(mut meta) = metadata_clone.write().await { let _ = meta.set_cover_url(Some(cover_url)).await; } }); ``` ## API Publique ### Création ```rust pub fn new(client: RadioParadiseClient, chunk_duration_ms: u32) -> Self ``` ### Configuration ```rust pub fn push_block_id(&mut self, event_id: EventId) ``` Ajoute un block ID à télécharger dans la file d'attente. ### Exécution ```rust async fn run(self: Box, stop_token: CancellationToken) -> Result<(), AudioError> ``` Hérite de `AudioPipelineNode`. ## Exemple d'utilisation Voir `examples/radio_paradise_stream.rs` pour : - Utilisation basique - Intégration avec nowplaying stream - Connexion à un sink ## Constantes ```rust const BLOCK_ID_TIMEOUT_SECS: u64 = 3; // Timeout attente nouveau block const RECENT_BLOCKS_CACHE_SIZE: usize = 10; // Taille cache anti-redondance ``` ## Dépendances - `pmoaudio` : Pipeline audio, types AudioChunk/AudioSegment - `pmoflac` : Décodage FLAC streaming - `pmometadata` : Métadonnées pistes - `futures-util` : StreamExt pour le décodage - `tokio` : Runtime async - `tokio-util` : StreamReader, CancellationToken ## Feature gate ```toml [features] pmoaudio = ["dep:pmoaudio", "dep:pmoflac", "dep:pmometadata", "dep:futures-util"] ``` Activer avec : `cargo build -p pmoparadise --features pmoaudio`