Ajout d'un noeud puis vers le cache audio

This commit is contained in:
2025-11-03 06:55:45 +01:00
parent 1eff9a57a5
commit 40950164e9
22 changed files with 1507 additions and 1162 deletions

BIN
.DS_Store vendored

Binary file not shown.

713
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ pmosource = { path = "../pmosource", features = ["server"] }
pmoserver = { path = "../pmoserver" } pmoserver = { path = "../pmoserver" }
pmocovers = { path = "../pmocovers", features = ["pmoserver"] } pmocovers = { path = "../pmocovers", features = ["pmoserver"] }
pmoaudiocache = { path = "../pmoaudiocache", features = ["pmoserver"]} pmoaudiocache = { path = "../pmoaudiocache", features = ["pmoserver"]}
pmoaudio-ext = { path = "../pmoaudio-ext", features = ["all"] }
pmoapp = { path = "../pmoapp", features = ["pmoserver"] } pmoapp = { path = "../pmoapp", features = ["pmoserver"] }
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time","signal"] } tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time","signal"] }

View File

@@ -238,8 +238,6 @@
//! - [Vite Documentation](https://vitejs.dev/) //! - [Vite Documentation](https://vitejs.dev/)
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use std::future::Future;
use std::pin::Pin;
/// Structure représentant l'application web embarquée. /// Structure représentant l'application web embarquée.
/// ///

30
pmoaudio-ext/Cargo.toml Normal file
View File

@@ -0,0 +1,30 @@
[package]
name = "pmoaudio-ext"
version = "0.1.0"
edition = "2021"
[dependencies]
# Core audio types
pmoaudio = { path = "../pmoaudio" }
# Optional dependencies for cache-sink feature
pmoaudiocache = { path = "../pmoaudiocache", optional = true }
pmoflac = { path = "../pmoflac", optional = true }
pmometadata = { path = "../pmometadata", optional = true }
# Optional dependency for playlist integration
pmoplaylist = { path = "../pmoplaylist", optional = true }
# Async runtime
tokio = { version = "1.0", features = ["full"] }
tokio-util = { version = "0.7" }
async-trait = "0.1"
# Utilities
tracing = "0.1"
[features]
default = []
cache-sink = ["dep:pmoaudiocache", "dep:pmoflac", "dep:pmometadata"]
playlist = ["dep:pmoplaylist"]
all = ["cache-sink", "playlist"]

30
pmoaudio-ext/src/lib.rs Normal file
View File

@@ -0,0 +1,30 @@
//! Extensions pour pmoaudio
//!
//! Cette crate fournit des nodes d'extension pour pmoaudio qui dépendent
//! d'autres crates du projet. Elle permet d'éviter les dépendances cycliques
//! en plaçant ces extensions en "bout de chaîne" de dépendances.
//!
//! # Features
//!
//! - `cache-sink` : Active le `FlacCacheSink` qui encode l'audio en FLAC et le stocke dans pmoaudiocache
//! - `playlist` : Active l'intégration avec pmoplaylist pour les sinks
//! - `all` : Active toutes les features d'un coup
//!
//! # Architecture
//!
//! Cette crate dépend de :
//! - `pmoaudio` : Types de base (AudioSegment, AudioError, etc.)
//! - `pmoaudiocache` (optionnel) : Cache audio pour le stockage FLAC
//! - `pmoflac` (optionnel) : Encodage FLAC
//! - `pmometadata` (optionnel) : Gestion des métadonnées
//! - `pmoplaylist` (optionnel) : Intégration playlist
//!
//! Aucune des crates ci-dessus ne dépend de `pmoaudio-ext`, évitant ainsi
//! tout cycle de dépendances.
#[cfg(feature = "cache-sink")]
pub mod sinks;
// Re-exports pour faciliter l'utilisation
#[cfg(feature = "cache-sink")]
pub use sinks::*;

View File

@@ -1,11 +1,11 @@
//! Sink qui encode les AudioSegment au format FLAC et les stocke dans le cache audio //! Sink qui encode les AudioSegment au format FLAC et les stocke dans le cache audio
use crate::metadata_ext::AudioTrackMetadataExt;
use pmoaudio::{ use pmoaudio::{
nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE}, nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE},
type_constraints::TypeRequirement, type_constraints::TypeRequirement,
AudioChunk, AudioSegment, SyncMarker, _AudioSegment, AudioChunk, AudioPipelineNode, AudioSegment, SyncMarker, _AudioSegment,
}; };
use pmoaudiocache::AudioTrackMetadataExt;
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat}; use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
@@ -18,6 +18,7 @@ use tokio::{
io::{self, AsyncRead, ReadBuf}, io::{self, AsyncRead, ReadBuf},
sync::{mpsc, RwLock}, 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. /// Sink qui encode les `AudioSegment` reçus au format FLAC et les stocke dans le cache audio.
/// ///
@@ -26,13 +27,17 @@ use tokio::{
/// - Crée une nouvelle entrée de cache pour chaque TrackBoundary rencontré /// - 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) /// - 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 /// - 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 /// - Termine l'encodage proprement quand il reçoit EndOfStream
pub struct FlacCacheSink { pub struct FlacCacheSink {
tx: mpsc::Sender<Arc<AudioSegment>>,
rx: mpsc::Receiver<Arc<AudioSegment>>, rx: mpsc::Receiver<Arc<AudioSegment>>,
cache: Arc<crate::Cache>, cache: Arc<pmoaudiocache::Cache>,
collection: Option<String>, collection: Option<String>,
encoder_options: EncoderOptions, encoder_options: EncoderOptions,
pcm_buffer_capacity: usize, pcm_buffer_capacity: usize,
#[cfg(feature = "playlist")]
playlist_handle: Option<Arc<pmoplaylist::WriteHandle>>,
} }
impl FlacCacheSink { impl FlacCacheSink {
@@ -41,7 +46,7 @@ impl FlacCacheSink {
/// # Arguments /// # Arguments
/// ///
/// * `cache` - Arc vers le cache audio où stocker les fichiers FLAC encodés /// * `cache` - Arc vers le cache audio où stocker les fichiers FLAC encodés
pub fn new(cache: Arc<crate::Cache>) -> (Self, mpsc::Sender<Arc<AudioSegment>>) { pub fn new(cache: Arc<pmoaudiocache::Cache>) -> Self {
Self::with_channel_size(cache, DEFAULT_CHANNEL_SIZE) Self::with_channel_size(cache, DEFAULT_CHANNEL_SIZE)
} }
@@ -51,10 +56,7 @@ impl FlacCacheSink {
/// ///
/// * `cache` - Arc vers le cache audio /// * `cache` - Arc vers le cache audio
/// * `channel_size` - Taille du buffer MPSC (nombre de segments en attente avant backpressure) /// * `channel_size` - Taille du buffer MPSC (nombre de segments en attente avant backpressure)
pub fn with_channel_size( pub fn with_channel_size(cache: Arc<pmoaudiocache::Cache>, channel_size: usize) -> Self {
cache: Arc<crate::Cache>,
channel_size: usize,
) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_config(cache, channel_size, EncoderOptions::default(), None) Self::with_config(cache, channel_size, EncoderOptions::default(), None)
} }
@@ -67,33 +69,51 @@ impl FlacCacheSink {
/// * `encoder_options` - Options d'encodage FLAC (compression, etc.) /// * `encoder_options` - Options d'encodage FLAC (compression, etc.)
/// * `collection` - Collection optionnelle à laquelle appartiennent les fichiers /// * `collection` - Collection optionnelle à laquelle appartiennent les fichiers
pub fn with_config( pub fn with_config(
cache: Arc<crate::Cache>, cache: Arc<pmoaudiocache::Cache>,
channel_size: usize, channel_size: usize,
encoder_options: EncoderOptions, encoder_options: EncoderOptions,
collection: Option<String>, collection: Option<String>,
) -> (Self, mpsc::Sender<Arc<AudioSegment>>) { ) -> Self {
let (tx, rx) = mpsc::channel(channel_size); let (tx, rx) = mpsc::channel(channel_size);
let sink = Self { Self {
tx,
rx, rx,
cache, cache,
collection, collection,
encoder_options, encoder_options,
pcm_buffer_capacity: 8, pcm_buffer_capacity: 8,
}; #[cfg(feature = "playlist")]
(sink, tx) playlist_handle: None,
}
} }
/// Lance l'encodage et l'ingestion dans le cache. /// 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.playlist_handle = Some(Arc::new(handle));
}
/// Lance l'encodage et l'ingestion dans le cache (version interne).
/// ///
/// Cette méthode crée une nouvelle entrée de cache pour chaque TrackBoundary rencontré. /// Cette méthode crée une nouvelle entrée de cache pour chaque TrackBoundary rencontré.
/// Les métadonnées du TrackBoundary sont copiées dans le cache après l'ingestion. /// Les métadonnées du TrackBoundary sont copiées dans le cache après l'ingestion.
pub async fn run(self) -> Result<FlacCacheSinkStats, AudioError> { async fn run_internal(
self,
stop_token: CancellationToken,
) -> Result<FlacCacheSinkStats, AudioError> {
let FlacCacheSink { let FlacCacheSink {
tx: _,
mut rx, mut rx,
cache, cache,
collection, collection,
encoder_options, encoder_options,
pcm_buffer_capacity, pcm_buffer_capacity,
#[cfg(feature = "playlist")]
playlist_handle,
} = self; } = self;
let mut all_tracks = Vec::new(); let mut all_tracks = Vec::new();
@@ -102,7 +122,7 @@ impl FlacCacheSink {
loop { loop {
// Attendre le premier chunk audio pour cette track, en capturant les métadonnées du TrackBoundary // Attendre le premier chunk audio pour cette track, en capturant les métadonnées du TrackBoundary
let (first_segment, track_metadata) = let (first_segment, track_metadata) =
match wait_for_first_audio_chunk_with_metadata(&mut rx).await { match wait_for_first_audio_chunk_with_metadata(&mut rx, &stop_token).await {
Ok(result) => result, Ok(result) => result,
Err(_) => { Err(_) => {
// Plus d'audio disponible // Plus d'audio disponible
@@ -157,6 +177,7 @@ impl FlacCacheSink {
pcm_tx, pcm_tx,
bits_per_sample, bits_per_sample,
sample_rate, sample_rate,
&stop_token,
); );
let copy_future = async { let copy_future = async {
tokio::io::copy(&mut flac_stream, &mut flac_buffer) tokio::io::copy(&mut flac_stream, &mut flac_buffer)
@@ -200,6 +221,15 @@ impl FlacCacheSink {
})?; })?;
} }
// Ajouter à la playlist si enregistrée
#[cfg(feature = "playlist")]
if let Some(ref playlist_handle) = playlist_handle {
playlist_handle.push(pk.clone()).await
.map_err(|e| AudioError::ProcessingError(
format!("Failed to add to playlist: {}", e)
))?;
}
// Ajouter les stats de cette track // Ajouter les stats de cette track
all_tracks.push(TrackStats { all_tracks.push(TrackStats {
pk, pk,
@@ -238,6 +268,7 @@ enum StopReason {
/// Retourne une erreur si EndOfStream est reçu avant tout audio. /// Retourne une erreur si EndOfStream est reçu avant tout audio.
async fn wait_for_first_audio_chunk_with_metadata( async fn wait_for_first_audio_chunk_with_metadata(
rx: &mut mpsc::Receiver<Arc<AudioSegment>>, rx: &mut mpsc::Receiver<Arc<AudioSegment>>,
stop_token: &CancellationToken,
) -> Result< ) -> Result<
( (
Arc<AudioSegment>, Arc<AudioSegment>,
@@ -248,10 +279,14 @@ async fn wait_for_first_audio_chunk_with_metadata(
let mut track_metadata: Option<Arc<RwLock<dyn pmometadata::TrackMetadata>>> = None; let mut track_metadata: Option<Arc<RwLock<dyn pmometadata::TrackMetadata>>> = None;
loop { loop {
let segment = rx let segment = tokio::select! {
.recv() result = rx.recv() => {
.await result.ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))?
.ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))?; }
_ = stop_token.cancelled() => {
return Err(AudioError::ProcessingError("Cancelled".into()));
}
};
match &segment.segment { match &segment.segment {
_AudioSegment::Chunk(chunk) => { _AudioSegment::Chunk(chunk) => {
@@ -260,8 +295,8 @@ async fn wait_for_first_audio_chunk_with_metadata(
} }
return Ok((segment, track_metadata)); return Ok((segment, track_metadata));
} }
_AudioSegment::Sync(marker) => match **marker { _AudioSegment::Sync(marker) => match &**marker {
SyncMarker::TrackBoundary { ref metadata, .. } => { SyncMarker::TrackBoundary { metadata, .. } => {
// Capturer les métadonnées du TrackBoundary // Capturer les métadonnées du TrackBoundary
track_metadata = Some(metadata.clone()); track_metadata = Some(metadata.clone());
continue; continue;
@@ -287,6 +322,7 @@ async fn pump_track_segments(
pcm_tx: mpsc::Sender<Vec<u8>>, pcm_tx: mpsc::Sender<Vec<u8>>,
bits_per_sample: u8, bits_per_sample: u8,
expected_rate: u32, expected_rate: u32,
stop_token: &CancellationToken,
) -> Result<(u64, u64, f64, StopReason), AudioError> { ) -> Result<(u64, u64, f64, StopReason), AudioError> {
let mut chunks = 0u64; let mut chunks = 0u64;
let mut samples = 0u64; let mut samples = 0u64;
@@ -308,9 +344,17 @@ async fn pump_track_segments(
// Boucle sur les segments suivants // Boucle sur les segments suivants
loop { loop {
let segment = match rx.recv().await { let segment = tokio::select! {
Some(seg) => seg, result = rx.recv() => {
None => { match result {
Some(seg) => seg,
None => {
drop(pcm_tx); // Fermer le channel PCM
return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed));
}
}
}
_ = stop_token.cancelled() => {
drop(pcm_tx); // Fermer le channel PCM drop(pcm_tx); // Fermer le channel PCM
return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed)); return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed));
} }
@@ -327,7 +371,7 @@ async fn pump_track_segments(
))); )));
} }
let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?; let pcm_bytes = chunk_to_pcm_bytes(&chunk, bits_per_sample)?;
if pcm_bytes.is_empty() { if pcm_bytes.is_empty() {
continue; continue;
} }
@@ -546,6 +590,25 @@ pub struct FlacCacheSinkStats {
pub tracks: Vec<TrackStats>, pub tracks: Vec<TrackStats>,
} }
#[async_trait::async_trait]
impl AudioPipelineNode for FlacCacheSink {
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
Some(self.tx.clone())
}
fn register(&mut self, _child: Box<dyn AudioPipelineNode>) {
panic!("FlacCacheSink is a terminal sink and cannot have children");
}
async fn run(
self: Box<Self>,
stop_token: CancellationToken,
) -> Result<(), AudioError> {
self.run_internal(stop_token).await?;
Ok(())
}
}
impl TypedAudioNode for FlacCacheSink { impl TypedAudioNode for FlacCacheSink {
fn input_type(&self) -> Option<TypeRequirement> { fn input_type(&self) -> Option<TypeRequirement> {
// FlacCacheSink accepte n'importe quel type entier (I16, I24, I32) // FlacCacheSink accepte n'importe quel type entier (I16, I24, I32)

View File

@@ -0,0 +1,11 @@
//! Sinks d'extension pour pmoaudio
//!
//! Ce module contient des sinks qui dépendent de multiples crates
//! et ne peuvent pas être placés directement dans pmoaudio sans créer
//! de dépendances cycliques.
#[cfg(feature = "cache-sink")]
mod flac_cache_sink;
#[cfg(feature = "cache-sink")]
pub use flac_cache_sink::{FlacCacheSink, FlacCacheSinkStats, TrackStats};

View File

@@ -18,6 +18,7 @@ paste = "1"
soxr = "0.6.0" soxr = "0.6.0"
bytemuck = "1.24.0" bytemuck = "1.24.0"
reqwest = { version = "0.12", features = ["stream"] } reqwest = { version = "0.12", features = ["stream"] }
tracing = "0.1"
[dev-dependencies] [dev-dependencies]
tokio-test = "0.4" tokio-test = "0.4"

View File

@@ -1,14 +1,20 @@
//! Test d'intégration pour FileSource et FlacFileSink //! Test d'intégration pour FileSource et FlacFileSink avec la nouvelle architecture AudioPipelineNode
//! //!
//! Ce programme teste la chaîne complète : //! Ce programme teste la chaîne complète :
//! 1. Lecture d'un fichier audio avec FileSource //! 1. Lecture d'un fichier audio avec FileSource
//! 2. Écriture vers FLAC avec FlacFileSink //! 2. Écriture vers FLAC avec FlacFileSink
//! //!
//! La nouvelle architecture permet de :
//! - Construire le pipeline en enregistrant des enfants avec register()
//! - Lancer tout le pipeline avec un seul appel à run() sur la racine
//! - Arrêter proprement tout le pipeline avec un CancellationToken
//!
//! Usage: //! Usage:
//! cargo run --example file_nodes_test -- <input_file> <output_file> //! cargo run --example file_nodes_test -- <input_file> <output_file>
use pmoaudio::{FileSource, FlacFileSink}; use pmoaudio::{AudioPipelineNode, FileSource, FlacFileSink};
use std::env; use std::env;
use tokio_util::sync::CancellationToken;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -28,48 +34,35 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!(); println!();
// Créer le pipeline: FileSource → FlacFileSink // Créer le pipeline: FileSource → FlacFileSink
let mut source = FileSource::new(input_path); // Calcul automatique de la taille des chunks (~50ms) let mut source = FileSource::new(input_path);
let (sink, tx) = FlacFileSink::new(output_path); // Utilise le buffer par défaut (16 segments) let sink = FlacFileSink::new(output_path);
source.add_subscriber(tx); // Enregistrer le sink comme enfant de la source
source.register(Box::new(sink));
// Lancer le sink dans une tâche séparée // Créer un token d'arrêt pour contrôle manuel si besoin
let sink_handle = tokio::spawn(async move { let stop_token = CancellationToken::new();
println!("FlacFileSink started");
let result = sink.run().await;
println!("FlacFileSink finished");
result
});
// Lancer le source // Lancer tout le pipeline - run() spawne automatiquement tous les enfants
println!("FileSource started"); println!("Pipeline started");
let source_result = source.run().await; println!(" FileSource: reading from {}", input_path);
println!("FileSource finished"); println!(" FlacFileSink: writing to {}", output_path);
// Vérifier les résultats let result = Box::new(source).run(stop_token).await;
match source_result {
Ok(()) => println!("✓ FileSource completed successfully"), // Vérifier le résultat
match result {
Ok(()) => {
println!();
println!("✓ Pipeline completed successfully");
println!(" Output file: {}", output_path);
}
Err(e) => { Err(e) => {
eprintln!("✗ FileSource error: {}", e); eprintln!();
eprintln!("✗ Pipeline error: {}", e);
return Err(e.into()); return Err(e.into());
} }
} }
let stats = sink_handle.await??;
println!("✓ FlacFileSink completed successfully");
println!();
println!("Statistics:");
println!(" Tracks written: {}", stats.tracks.len());
for (i, track) in stats.tracks.iter().enumerate() {
println!(" Track {}:", i);
println!(" Output file: {:?}", track.path);
println!(" Chunks received: {}", track.chunks_received);
println!(" Total samples: {}", track.total_samples);
println!(
" Duration: {:.2} seconds",
track.total_duration_sec
);
}
Ok(()) Ok(())
} }

View File

@@ -85,6 +85,7 @@ mod audio_segment;
pub mod conversions; pub mod conversions;
pub mod events; pub mod events;
pub mod nodes; pub mod nodes;
pub mod pipeline;
mod sample_types; mod sample_types;
mod sync_marker; mod sync_marker;
pub mod type_constraints; pub mod type_constraints;
@@ -112,13 +113,16 @@ pub use events::{
VolumeChangeEvent, VolumeChangeEvent,
}; };
// Export du trait de pipeline
pub use pipeline::AudioPipelineNode;
// Exports publics des nodes // Exports publics des nodes
pub use nodes::{ pub use nodes::{
converter_nodes::{ToF32Node, ToF64Node, ToI16Node, ToI24Node, ToI32Node}, converter_nodes::{ToF32Node, ToF64Node, ToI16Node, ToI24Node, ToI32Node},
file_source::FileSource, file_source::FileSource,
flac_file_sink::{FlacFileSink, FlacFileSinkStats}, flac_file_sink::{FlacFileSink, FlacFileSinkStats},
http_source::HttpSource, http_source::HttpSource,
AudioError, AudioNode, MultiSubscriberNode, SingleSubscriberNode, TypedAudioNode, AudioError, AudioNode, TypedAudioNode,
}; };
// Nodes temporairement désactivés // Nodes temporairement désactivés

View File

@@ -8,376 +8,174 @@
//! les incompatibilités de type entre producers et consumers. //! les incompatibilités de type entre producers et consumers.
use crate::{ use crate::{
nodes::{AudioError, MultiSubscriberNode, TypedAudioNode}, nodes::{AudioError, TypedAudioNode},
type_constraints::{SampleType, TypeRequirement}, type_constraints::{SampleType, TypeRequirement},
AudioSegment, AudioPipelineNode, AudioSegment,
}; };
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
/// Node de conversion vers I16 // Macro pour générer les converter nodes avec la nouvelle architecture AudioPipelineNode
/// macro_rules! converter_node {
/// Convertit n'importe quel type de chunk audio vers I16 (16-bit signed integer). ($node_name:ident, $convert_method:ident, $output_type:expr, $doc:expr) => {
/// Utilise les conversions DSP SIMD optimisées. #[doc = $doc]
pub struct ToI16Node { pub struct $node_name {
rx: mpsc::Receiver<Arc<AudioSegment>>, tx: mpsc::Sender<Arc<AudioSegment>>,
subscribers: MultiSubscriberNode, rx: mpsc::Receiver<Arc<AudioSegment>>,
} child_txs: Vec<mpsc::Sender<Arc<AudioSegment>>>,
children: Vec<Box<dyn AudioPipelineNode>>,
impl ToI16Node {
/// Crée un nouveau node de conversion vers I16
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_channel_size(16)
}
/// Crée un nouveau node avec une taille de buffer spécifique
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
};
(node, tx)
}
/// Ajoute un abonné qui recevra les segments audio convertis
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le traitement de conversion
pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(segment) = self.rx.recv().await {
// Si c'est un syncmarker, passer directement
if !segment.is_audio_chunk() {
self.subscribers.push(segment).await?;
continue;
}
// Convertir le chunk audio vers I16
let converted_segment = if let Some(chunk) = segment.as_chunk() {
let converted_chunk = chunk.to_i16();
Arc::new(AudioSegment {
order: segment.order,
timestamp_sec: segment.timestamp_sec,
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
})
} else {
segment
};
self.subscribers.push(converted_segment).await?;
} }
Ok(()) impl $node_name {
} /// Crée un nouveau node de conversion
} pub fn new() -> Self {
Self::with_channel_size(16)
impl TypedAudioNode for ToI16Node {
fn input_type(&self) -> Option<TypeRequirement> {
// Accepte n'importe quel type
Some(TypeRequirement::any())
}
fn output_type(&self) -> Option<TypeRequirement> {
// Produit uniquement I16
Some(TypeRequirement::specific(SampleType::I16))
}
}
impl Default for ToI16Node {
fn default() -> Self {
Self::new().0
}
}
/// Node de conversion vers I24
///
/// Convertit n'importe quel type de chunk audio vers I24 (24-bit signed integer).
/// Utilise les conversions DSP SIMD optimisées.
pub struct ToI24Node {
rx: mpsc::Receiver<Arc<AudioSegment>>,
subscribers: MultiSubscriberNode,
}
impl ToI24Node {
/// Crée un nouveau node de conversion vers I24
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_channel_size(16)
}
/// Crée un nouveau node avec une taille de buffer spécifique
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
};
(node, tx)
}
/// Ajoute un abonné qui recevra les segments audio convertis
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le traitement de conversion
pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(segment) = self.rx.recv().await {
if !segment.is_audio_chunk() {
self.subscribers.push(segment).await?;
continue;
} }
let converted_segment = if let Some(chunk) = segment.as_chunk() { /// Crée un nouveau node avec une taille de buffer spécifique
let converted_chunk = chunk.to_i24(); pub fn with_channel_size(channel_size: usize) -> Self {
Arc::new(AudioSegment { let (tx, rx) = mpsc::channel(channel_size);
order: segment.order, Self {
timestamp_sec: segment.timestamp_sec, tx,
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)), rx,
}) child_txs: Vec::new(),
} else { children: Vec::new(),
segment }
}; }
self.subscribers.push(converted_segment).await?;
} }
Ok(()) #[async_trait::async_trait]
} impl AudioPipelineNode for $node_name {
} fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
Some(self.tx.clone())
impl TypedAudioNode for ToI24Node {
fn input_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::any())
}
fn output_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::specific(SampleType::I24))
}
}
impl Default for ToI24Node {
fn default() -> Self {
Self::new().0
}
}
/// Node de conversion vers I32
///
/// Convertit n'importe quel type de chunk audio vers I32 (32-bit signed integer).
/// Utilise les conversions DSP SIMD optimisées.
pub struct ToI32Node {
rx: mpsc::Receiver<Arc<AudioSegment>>,
subscribers: MultiSubscriberNode,
}
impl ToI32Node {
/// Crée un nouveau node de conversion vers I32
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_channel_size(16)
}
/// Crée un nouveau node avec une taille de buffer spécifique
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
};
(node, tx)
}
/// Ajoute un abonné qui recevra les segments audio convertis
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le traitement de conversion
pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(segment) = self.rx.recv().await {
if !segment.is_audio_chunk() {
self.subscribers.push(segment).await?;
continue;
} }
let converted_segment = if let Some(chunk) = segment.as_chunk() { fn register(&mut self, child: Box<dyn AudioPipelineNode>) {
let converted_chunk = chunk.to_i32(); if let Some(tx) = child.get_tx() {
Arc::new(AudioSegment { self.child_txs.push(tx);
order: segment.order, }
timestamp_sec: segment.timestamp_sec, self.children.push(child);
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
})
} else {
segment
};
self.subscribers.push(converted_segment).await?;
}
Ok(())
}
}
impl TypedAudioNode for ToI32Node {
fn input_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::any())
}
fn output_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::specific(SampleType::I32))
}
}
impl Default for ToI32Node {
fn default() -> Self {
Self::new().0
}
}
/// Node de conversion vers F32
///
/// Convertit n'importe quel type de chunk audio vers F32 (32-bit floating point).
/// Utilise les conversions DSP SIMD optimisées.
pub struct ToF32Node {
rx: mpsc::Receiver<Arc<AudioSegment>>,
subscribers: MultiSubscriberNode,
}
impl ToF32Node {
/// Crée un nouveau node de conversion vers F32
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_channel_size(16)
}
/// Crée un nouveau node avec une taille de buffer spécifique
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
};
(node, tx)
}
/// Ajoute un abonné qui recevra les segments audio convertis
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le traitement de conversion
pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(segment) = self.rx.recv().await {
if !segment.is_audio_chunk() {
self.subscribers.push(segment).await?;
continue;
} }
let converted_segment = if let Some(chunk) = segment.as_chunk() { async fn run(
let converted_chunk = chunk.to_f32(); mut self: Box<Self>,
Arc::new(AudioSegment { stop_token: CancellationToken,
order: segment.order, ) -> Result<(), AudioError> {
timestamp_sec: segment.timestamp_sec, // Spawner tous les enfants
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)), let mut child_handles = Vec::new();
}) for child in self.children {
} else { let child_token = stop_token.child_token();
segment let handle = tokio::spawn(async move { child.run(child_token).await });
}; child_handles.push(handle);
}
self.subscribers.push(converted_segment).await?; // Boucle de traitement
loop {
let segment = tokio::select! {
result = self.rx.recv() => {
match result {
Some(seg) => seg,
None => break,
}
}
_ = stop_token.cancelled() => {
break;
}
};
// Convertir si c'est un chunk audio, sinon passer tel quel
let output_segment = if segment.is_audio_chunk() {
if let Some(chunk) = segment.as_chunk() {
let converted_chunk = chunk.$convert_method();
Arc::new(AudioSegment {
order: segment.order,
timestamp_sec: segment.timestamp_sec,
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
})
} else {
segment
}
} else {
segment
};
// Envoyer à tous les enfants
for tx in &self.child_txs {
if tx.send(output_segment.clone()).await.is_err() {
// Un enfant est mort, arrêter
break;
}
}
}
// Attendre que tous les enfants se terminent
for handle in child_handles {
match handle.await {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(e),
Err(e) => {
return Err(AudioError::ProcessingError(format!(
"Child task panicked: {}",
e
)))
}
}
}
Ok(())
}
} }
Ok(()) impl TypedAudioNode for $node_name {
} fn input_type(&self) -> Option<TypeRequirement> {
} Some(TypeRequirement::any())
impl TypedAudioNode for ToF32Node {
fn input_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::any())
}
fn output_type(&self) -> Option<TypeRequirement> {
Some(TypeRequirement::specific(SampleType::F32))
}
}
impl Default for ToF32Node {
fn default() -> Self {
Self::new().0
}
}
/// Node de conversion vers F64
///
/// Convertit n'importe quel type de chunk audio vers F64 (64-bit floating point).
/// Utilise les conversions DSP SIMD optimisées.
pub struct ToF64Node {
rx: mpsc::Receiver<Arc<AudioSegment>>,
subscribers: MultiSubscriberNode,
}
impl ToF64Node {
/// Crée un nouveau node de conversion vers F64
pub fn new() -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
Self::with_channel_size(16)
}
/// Crée un nouveau node avec une taille de buffer spécifique
pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioSegment>>) {
let (tx, rx) = mpsc::channel(channel_size);
let node = Self {
rx,
subscribers: MultiSubscriberNode::new(),
};
(node, tx)
}
/// Ajoute un abonné qui recevra les segments audio convertis
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le traitement de conversion
pub async fn run(mut self) -> Result<(), AudioError> {
while let Some(segment) = self.rx.recv().await {
if !segment.is_audio_chunk() {
self.subscribers.push(segment).await?;
continue;
} }
let converted_segment = if let Some(chunk) = segment.as_chunk() { fn output_type(&self) -> Option<TypeRequirement> {
let converted_chunk = chunk.to_f64(); Some(TypeRequirement::specific($output_type))
Arc::new(AudioSegment { }
order: segment.order,
timestamp_sec: segment.timestamp_sec,
segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)),
})
} else {
segment
};
self.subscribers.push(converted_segment).await?;
} }
Ok(()) impl Default for $node_name {
} fn default() -> Self {
Self::new()
}
}
};
} }
impl TypedAudioNode for ToF64Node { // Générer les 5 converter nodes
fn input_type(&self) -> Option<TypeRequirement> { converter_node!(
Some(TypeRequirement::any()) ToI16Node,
} to_i16,
SampleType::I16,
fn output_type(&self) -> Option<TypeRequirement> { "Node de conversion vers I16 (16-bit signed integer)"
Some(TypeRequirement::specific(SampleType::F64)) );
} converter_node!(
} ToI24Node,
to_i24,
impl Default for ToF64Node { SampleType::I24,
fn default() -> Self { "Node de conversion vers I24 (24-bit signed integer)"
Self::new().0 );
} converter_node!(
} ToI32Node,
to_i32,
SampleType::I32,
"Node de conversion vers I32 (32-bit signed integer)"
);
converter_node!(
ToF32Node,
to_f32,
SampleType::F32,
"Node de conversion vers F32 (32-bit floating point)"
);
converter_node!(
ToF64Node,
to_f64,
SampleType::F64,
"Node de conversion vers F64 (64-bit floating point)"
);
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -386,7 +184,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_to_f32_node_type_requirements() { async fn test_to_f32_node_type_requirements() {
let (node, _tx) = ToF32Node::new(); let node = ToF32Node::new();
// Vérifier les types d'entrée/sortie // Vérifier les types d'entrée/sortie
assert_eq!( assert_eq!(
@@ -405,14 +203,60 @@ mod tests {
); );
} }
// Nœud de test simple qui collecte les segments
struct TestCollectorNode {
input_tx: mpsc::Sender<Arc<AudioSegment>>,
input_rx: mpsc::Receiver<Arc<AudioSegment>>,
output_tx: mpsc::Sender<Arc<AudioSegment>>,
}
impl TestCollectorNode {
fn new(output_tx: mpsc::Sender<Arc<AudioSegment>>) -> Self {
let (input_tx, input_rx) = mpsc::channel(16);
Self {
input_tx,
input_rx,
output_tx,
}
}
}
#[async_trait::async_trait]
impl AudioPipelineNode for TestCollectorNode {
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
Some(self.input_tx.clone())
}
fn register(&mut self, _child: Box<dyn AudioPipelineNode>) {
panic!("TestCollectorNode is a sink");
}
async fn run(
mut self: Box<Self>,
_stop_token: CancellationToken,
) -> Result<(), AudioError> {
while let Some(segment) = self.input_rx.recv().await {
if self.output_tx.send(segment).await.is_err() {
break;
}
}
Ok(())
}
}
#[tokio::test] #[tokio::test]
async fn test_to_i16_node_converts_from_i32() { async fn test_to_i16_node_converts_from_i32() {
let (mut node, tx) = ToI16Node::new(); let mut node = ToI16Node::new();
let (out_tx, mut out_rx) = mpsc::channel(16); let (out_tx, mut out_rx) = mpsc::channel(16);
node.add_subscriber(out_tx); let collector = TestCollectorNode::new(out_tx);
node.register(Box::new(collector));
// Récupérer le tx du node
let tx = node.get_tx().unwrap();
// Lancer le node dans une tâche // Lancer le node dans une tâche
let handle = tokio::spawn(async move { node.run().await }); let stop_token = CancellationToken::new();
let handle = tokio::spawn(async move { Box::new(node).run(stop_token).await });
// Créer et envoyer un chunk I32 // Créer et envoyer un chunk I32
let stereo = vec![[1_000_000i32 << 16, -500_000i32 << 16]; 100]; let stereo = vec![[1_000_000i32 << 16, -500_000i32 << 16]; 100];
@@ -451,12 +295,16 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_syncmarkers_passthrough() { async fn test_syncmarkers_passthrough() {
let (mut node, tx) = ToI16Node::new(); let mut node = ToI16Node::new();
let (out_tx, mut out_rx) = mpsc::channel(16); let (out_tx, mut out_rx) = mpsc::channel(16);
node.add_subscriber(out_tx); let collector = TestCollectorNode::new(out_tx);
node.register(Box::new(collector));
let tx = node.get_tx().unwrap();
let stop_token = CancellationToken::new();
tokio::spawn(async move { tokio::spawn(async move {
node.run().await.unwrap(); Box::new(node).run(stop_token).await.unwrap();
}); });
// Envoyer un syncmarker // Envoyer un syncmarker

View File

@@ -1,5 +1,6 @@
use crate::{ use crate::{
nodes::{AudioError, MultiSubscriberNode, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS}, nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
pipeline::AudioPipelineNode,
type_constraints::TypeRequirement, type_constraints::TypeRequirement,
AudioChunk, AudioChunkData, AudioSegment, I24, AudioChunk, AudioChunkData, AudioSegment, I24,
}; };
@@ -7,6 +8,8 @@ use pmoflac::{decode_audio_stream, AudioFileMetadata, StreamInfo};
use pmometadata::{MemoryTrackMetadata, TrackMetadata}; use pmometadata::{MemoryTrackMetadata, TrackMetadata};
use std::{path::PathBuf, sync::Arc, time::Duration}; use std::{path::PathBuf, sync::Arc, time::Duration};
use tokio::{fs::File, io::AsyncReadExt, sync::mpsc}; use tokio::{fs::File, io::AsyncReadExt, sync::mpsc};
use tokio_util::sync::CancellationToken;
use tracing;
/// FileSource - Lit un fichier audio et publie des `AudioSegment` /// FileSource - Lit un fichier audio et publie des `AudioSegment`
/// ///
@@ -21,7 +24,8 @@ use tokio::{fs::File, io::AsyncReadExt, sync::mpsc};
pub struct FileSource { pub struct FileSource {
path: PathBuf, path: PathBuf,
chunk_frames: usize, chunk_frames: usize,
subscribers: MultiSubscriberNode, child_txs: Vec<mpsc::Sender<Arc<AudioSegment>>>,
children: Vec<Box<dyn AudioPipelineNode>>,
} }
impl FileSource { impl FileSource {
@@ -43,156 +47,10 @@ impl FileSource {
Self { Self {
path: path.into(), path: path.into(),
chunk_frames, chunk_frames,
subscribers: MultiSubscriberNode::new(), child_txs: Vec::new(),
children: Vec::new(),
} }
} }
/// Ajoute un abonné qui recevra les segments audio.
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance la lecture du fichier et diffuse les segments audio.
pub async fn run(self) -> Result<(), AudioError> {
// Ouvrir le fichier
let file = File::open(&self.path).await.map_err(|e| {
AudioError::ProcessingError(format!("Failed to open {:?}: {}", self.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 self.chunk_frames == 0 {
// Calculer pour obtenir DEFAULT_CHUNK_DURATION_MS millisecondes
let frames =
(stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize;
// Arrondir à la puissance de 2 la plus proche pour optimiser les buffers
frames.next_power_of_two().max(256)
} else {
self.chunk_frames.max(1)
};
// Émettre TopZeroSync
let top_zero = AudioSegment::new_top_zero_sync();
self.subscribers.push(top_zero).await?;
// Extraire et émettre les métadonnées du fichier
match AudioFileMetadata::from_file(&self.path) {
Ok(file_metadata) => {
let mut metadata = MemoryTrackMetadata::new();
// Convertir AudioFileMetadata vers MemoryTrackMetadata
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;
}
// Émettre TrackBoundary
let track_boundary = AudioSegment::new_track_boundary(0, 0.0, Arc::new(tokio::sync::RwLock::new(metadata)));
self.subscribers.push(track_boundary).await?;
}
Err(e) => {
eprintln!(
"Warning: Failed to extract metadata from {:?}: {}",
self.path, e
);
// Continuer sans métadonnées
}
}
// 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 {
// Remplir le buffer
if pending.len() < chunk_byte_len {
let read = stream.read(&mut read_buf).await.map_err(|e| {
AudioError::ProcessingError(format!("I/O error while decoding: {}", e))
})?;
if read == 0 {
break;
}
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);
let take_bytes = frames_to_emit * frame_bytes;
let chunk_bytes = pending.drain(..take_bytes).collect::<Vec<u8>>();
// Calculer le timestamp
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
// Créer le segment audio
let segment = bytes_to_segment(
&chunk_bytes,
&stream_info,
frames_to_emit,
chunk_index,
timestamp_sec,
)?;
self.subscribers.push(segment).await?;
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)?;
self.subscribers.push(segment).await?;
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);
self.subscribers.push(eos).await?;
// Attendre la fin du décodage
stream
.wait()
.await
.map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?;
Ok(())
}
} }
fn validate_stream(info: &StreamInfo) -> Result<(), AudioError> { fn validate_stream(info: &StreamInfo) -> Result<(), AudioError> {
@@ -330,6 +188,230 @@ fn bytes_to_segment(
})) }))
} }
#[async_trait::async_trait]
impl AudioPipelineNode for FileSource {
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
// FileSource est une source, elle n'a pas d'input
None
}
fn register(&mut self, child: Box<dyn AudioPipelineNode>) {
// 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<Self>,
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::<Vec<u8>>();
// 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 { impl TypedAudioNode for FileSource {
fn input_type(&self) -> Option<TypeRequirement> { fn input_type(&self) -> Option<TypeRequirement> {
// FileSource est une source, elle ne consomme pas d'audio // FileSource est une source, elle ne consomme pas d'audio
@@ -384,12 +466,68 @@ mod tests {
file.flush().await.expect("flush file"); file.flush().await.expect("flush file");
flac_stream.wait().await.unwrap(); flac_stream.wait().await.unwrap();
// Créer un collecteur simple qui transmet les segments à un channel de test
struct TestCollectorNode {
tx: mpsc::Sender<Arc<AudioSegment>>,
rx: mpsc::Receiver<Arc<AudioSegment>>,
test_tx: mpsc::Sender<Arc<AudioSegment>>,
}
impl TestCollectorNode {
fn new(test_tx: mpsc::Sender<Arc<AudioSegment>>) -> 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<mpsc::Sender<Arc<AudioSegment>>> {
Some(self.tx.clone())
}
fn register(&mut self, _child: Box<dyn AudioPipelineNode>) {
panic!("TestCollectorNode is a terminal node");
}
async fn run(
mut self: Box<Self>,
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 mut source = FileSource::with_chunk_size(&flac_path, 64);
let (tx, mut rx) = mpsc::channel(16); let collector = TestCollectorNode::new(test_tx);
source.add_subscriber(tx); source.register(Box::new(collector));
tokio::spawn(async move { tokio::spawn(async move {
source.run().await.unwrap(); let token = CancellationToken::new();
Box::new(source).run(token).await.unwrap();
}); });
let mut received_frames = 0usize; let mut received_frames = 0usize;

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE}, nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE},
type_constraints::TypeRequirement, type_constraints::TypeRequirement,
AudioChunk, AudioSegment, SyncMarker, AudioChunk, AudioPipelineNode, AudioSegment, SyncMarker,
}; };
use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat}; use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
use std::{ use std::{
@@ -16,6 +16,7 @@ use tokio::{
io::{self, AsyncRead, AsyncWriteExt, ReadBuf}, io::{self, AsyncRead, AsyncWriteExt, ReadBuf},
sync::mpsc, sync::mpsc,
}; };
use tokio_util::sync::CancellationToken;
/// Sink qui encode les `AudioSegment` reçus au format FLAC. /// Sink qui encode les `AudioSegment` reçus au format FLAC.
/// ///
@@ -25,6 +26,7 @@ use tokio::{
/// - Adapte automatiquement l'encodage FLAC selon la profondeur de bit du chunk (8/16/24/32-bit) /// - Adapte automatiquement l'encodage FLAC selon la profondeur de bit du chunk (8/16/24/32-bit)
/// - Termine l'encodage proprement quand il reçoit EndOfStream /// - Termine l'encodage proprement quand il reçoit EndOfStream
pub struct FlacFileSink { pub struct FlacFileSink {
tx: mpsc::Sender<Arc<AudioSegment>>,
rx: mpsc::Receiver<Arc<AudioSegment>>, rx: mpsc::Receiver<Arc<AudioSegment>>,
base_path: PathBuf, base_path: PathBuf,
encoder_options: EncoderOptions, encoder_options: EncoderOptions,
@@ -38,7 +40,7 @@ impl FlacFileSink {
/// ///
/// * `base_path` - Chemin de base pour les fichiers FLAC. Si des TrackBoundary sont reçus, /// * `base_path` - Chemin de base pour les fichiers FLAC. Si des TrackBoundary sont reçus,
/// des fichiers seront créés avec des suffixes (_01, _02, etc.) /// des fichiers seront créés avec des suffixes (_01, _02, etc.)
pub fn new<P: Into<PathBuf>>(base_path: P) -> (Self, mpsc::Sender<Arc<AudioSegment>>) { pub fn new<P: Into<PathBuf>>(base_path: P) -> Self {
Self::with_channel_size(base_path, DEFAULT_CHANNEL_SIZE) Self::with_channel_size(base_path, DEFAULT_CHANNEL_SIZE)
} }
@@ -51,7 +53,7 @@ impl FlacFileSink {
pub fn with_channel_size<P: Into<PathBuf>>( pub fn with_channel_size<P: Into<PathBuf>>(
base_path: P, base_path: P,
channel_size: usize, channel_size: usize,
) -> (Self, mpsc::Sender<Arc<AudioSegment>>) { ) -> Self {
Self::with_config(base_path, channel_size, EncoderOptions::default()) Self::with_config(base_path, channel_size, EncoderOptions::default())
} }
@@ -66,15 +68,15 @@ impl FlacFileSink {
base_path: P, base_path: P,
channel_size: usize, channel_size: usize,
encoder_options: EncoderOptions, encoder_options: EncoderOptions,
) -> (Self, mpsc::Sender<Arc<AudioSegment>>) { ) -> Self {
let (tx, rx) = mpsc::channel(channel_size); let (tx, rx) = mpsc::channel(channel_size);
let sink = Self { Self {
tx,
rx, rx,
base_path: base_path.into(), base_path: base_path.into(),
encoder_options, encoder_options,
pcm_buffer_capacity: 8, pcm_buffer_capacity: 8,
}; }
(sink, tx)
} }
/// Lance l'encodage vers le(s) fichier(s) cible(s). /// Lance l'encodage vers le(s) fichier(s) cible(s).
@@ -84,27 +86,28 @@ impl FlacFileSink {
/// - Track 0 : base_path.flac /// - Track 0 : base_path.flac
/// - Track 1 : base_path_01.flac /// - Track 1 : base_path_01.flac
/// - Track 2 : base_path_02.flac, etc. /// - Track 2 : base_path_02.flac, etc.
pub async fn run(self) -> Result<FlacFileSinkStats, AudioError> { async fn run_internal(
let FlacFileSink { mut rx: mpsc::Receiver<Arc<AudioSegment>>,
mut rx, base_path: PathBuf,
base_path, encoder_options: EncoderOptions,
encoder_options, pcm_buffer_capacity: usize,
pcm_buffer_capacity, stop_token: CancellationToken,
} = self; ) -> Result<(), AudioError> {
let mut all_tracks = Vec::new();
let mut track_number = 0; let mut track_number = 0;
loop { loop {
// Vérifier si l'arrêt a été demandé
if stop_token.is_cancelled() {
return Ok(());
}
// Attendre le premier chunk audio pour cette track, en capturant les métadonnées du TrackBoundary // Attendre le premier chunk audio pour cette track, en capturant les métadonnées du TrackBoundary
let (first_segment, track_metadata) = match wait_for_first_audio_chunk_with_metadata(&mut rx).await { let (first_segment, track_metadata) = match wait_for_first_audio_chunk_with_metadata(&mut rx, &stop_token).await {
Ok(result) => result, Ok(result) => result,
Err(_) => { Err(_) => {
// Plus d'audio disponible // Plus d'audio disponible ou arrêt demandé
if all_tracks.is_empty() { return Ok(());
return Err(AudioError::ProcessingError("No audio data received".into()));
}
break;
} }
}; };
@@ -149,7 +152,7 @@ impl FlacFileSink {
// Exécuter pump et copy en parallèle avec tokio::select! en boucle // Exécuter pump et copy en parallèle avec tokio::select! en boucle
let pump_future = let pump_future =
pump_track_segments(first_segment, &mut rx, pcm_tx, bits_per_sample, sample_rate); pump_track_segments(first_segment, &mut rx, pcm_tx, bits_per_sample, sample_rate, &stop_token);
let copy_future = async { let copy_future = async {
let copy_result = tokio::io::copy(&mut flac_stream, &mut output).await; let copy_result = tokio::io::copy(&mut flac_stream, &mut output).await;
let flush_result = output.flush().await; let flush_result = output.flush().await;
@@ -168,16 +171,7 @@ impl FlacFileSink {
// Attendre les deux tâches en parallèle // Attendre les deux tâches en parallèle
let (copy_result, pump_result) = tokio::join!(copy_future, pump_future); let (copy_result, pump_result) = tokio::join!(copy_future, pump_future);
copy_result?; copy_result?;
let (chunks, samples, duration_sec, stop_reason) = pump_result?; let stop_reason = pump_result?;
// Ajouter les stats de cette track
all_tracks.push(TrackStats {
path: track_path,
track_number,
chunks_received: chunks,
total_samples: samples,
total_duration_sec: duration_sec,
});
// Vérifier le stop_reason pour savoir si on continue // Vérifier le stop_reason pour savoir si on continue
match stop_reason { match stop_reason {
@@ -186,14 +180,12 @@ impl FlacFileSink {
track_number += 1; track_number += 1;
continue; continue;
} }
StopReason::EndOfStream | StopReason::ChannelClosed => { StopReason::EndOfStream | StopReason::ChannelClosed | StopReason::Cancelled => {
// Fin de l'encodage // Fin de l'encodage
break; return Ok(());
} }
} }
} }
Ok(FlacFileSinkStats { tracks: all_tracks })
} }
} }
@@ -223,20 +215,26 @@ enum StopReason {
TrackBoundary(Arc<tokio::sync::RwLock<dyn pmometadata::TrackMetadata>>), TrackBoundary(Arc<tokio::sync::RwLock<dyn pmometadata::TrackMetadata>>),
EndOfStream, EndOfStream,
ChannelClosed, ChannelClosed,
Cancelled,
} }
/// Attend et retourne le premier chunk audio avec les métadonnées du TrackBoundary si présent. /// Attend et retourne le premier chunk audio avec les métadonnées du TrackBoundary si présent.
/// Retourne une erreur si EndOfStream est reçu avant tout audio. /// Retourne une erreur si EndOfStream est reçu avant tout audio ou si l'arrêt est demandé.
async fn wait_for_first_audio_chunk_with_metadata( async fn wait_for_first_audio_chunk_with_metadata(
rx: &mut mpsc::Receiver<Arc<AudioSegment>>, rx: &mut mpsc::Receiver<Arc<AudioSegment>>,
stop_token: &CancellationToken,
) -> Result<(Arc<AudioSegment>, Option<Arc<tokio::sync::RwLock<dyn pmometadata::TrackMetadata>>>), AudioError> { ) -> Result<(Arc<AudioSegment>, Option<Arc<tokio::sync::RwLock<dyn pmometadata::TrackMetadata>>>), AudioError> {
let mut track_metadata: Option<Arc<tokio::sync::RwLock<dyn pmometadata::TrackMetadata>>> = None; let mut track_metadata: Option<Arc<tokio::sync::RwLock<dyn pmometadata::TrackMetadata>>> = None;
loop { loop {
let segment = rx let segment = tokio::select! {
.recv() result = rx.recv() => {
.await result.ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))?
.ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))?; }
_ = stop_token.cancelled() => {
return Err(AudioError::ProcessingError("Cancelled".into()));
}
};
match &segment.segment { match &segment.segment {
crate::_AudioSegment::Chunk(chunk) => { crate::_AudioSegment::Chunk(chunk) => {
@@ -274,11 +272,8 @@ async fn pump_track_segments(
pcm_tx: mpsc::Sender<Vec<u8>>, pcm_tx: mpsc::Sender<Vec<u8>>,
bits_per_sample: u8, bits_per_sample: u8,
expected_rate: u32, expected_rate: u32,
) -> Result<(u64, u64, f64, StopReason), AudioError> { stop_token: &CancellationToken,
let mut chunks = 0u64; ) -> Result<StopReason, AudioError> {
let mut samples = 0u64;
let mut duration_sec = 0.0f64;
// Traiter le premier segment // Traiter le premier segment
if let Some(chunk) = first_segment.as_chunk() { if let Some(chunk) = first_segment.as_chunk() {
let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?; let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?;
@@ -287,19 +282,24 @@ async fn pump_track_segments(
.send(pcm_bytes) .send(pcm_bytes)
.await .await
.map_err(|_| AudioError::SendError)?; .map_err(|_| AudioError::SendError)?;
chunks += 1;
samples += chunk.len() as u64;
duration_sec += chunk.len() as f64 / expected_rate as f64;
} }
} }
// Boucle sur les segments suivants // Boucle sur les segments suivants
loop { loop {
let segment = match rx.recv().await { let segment = tokio::select! {
Some(seg) => seg, result = rx.recv() => {
None => { match result {
drop(pcm_tx); // Fermer le channel PCM Some(seg) => seg,
return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed)); None => {
drop(pcm_tx);
return Ok(StopReason::ChannelClosed);
}
}
}
_ = stop_token.cancelled() => {
drop(pcm_tx);
return Ok(StopReason::Cancelled);
} }
}; };
@@ -323,25 +323,16 @@ async fn pump_track_segments(
.send(pcm_bytes) .send(pcm_bytes)
.await .await
.map_err(|_| AudioError::SendError)?; .map_err(|_| AudioError::SendError)?;
chunks += 1;
samples += chunk.len() as u64;
duration_sec += chunk.len() as f64 / expected_rate as f64;
} }
crate::_AudioSegment::Sync(marker) => { crate::_AudioSegment::Sync(marker) => {
match &**marker { match &**marker {
SyncMarker::TrackBoundary { metadata, .. } => { SyncMarker::TrackBoundary { metadata, .. } => {
drop(pcm_tx); // Fermer le channel PCM drop(pcm_tx); // Fermer le channel PCM
return Ok(( return Ok(StopReason::TrackBoundary(metadata.clone()));
chunks,
samples,
duration_sec,
StopReason::TrackBoundary(metadata.clone()),
));
} }
SyncMarker::EndOfStream => { SyncMarker::EndOfStream => {
drop(pcm_tx); // Fermer le channel PCM drop(pcm_tx); // Fermer le channel PCM
return Ok((chunks, samples, duration_sec, StopReason::EndOfStream)); return Ok(StopReason::EndOfStream);
} }
_ => {} // Ignorer les autres syncmarkers _ => {} // Ignorer les autres syncmarkers
} }
@@ -535,6 +526,32 @@ pub struct FlacFileSinkStats {
pub tracks: Vec<TrackStats>, pub tracks: Vec<TrackStats>,
} }
#[async_trait::async_trait]
impl AudioPipelineNode for FlacFileSink {
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
Some(self.tx.clone())
}
fn register(&mut self, _child: Box<dyn AudioPipelineNode>) {
panic!("FlacFileSink is a terminal node and cannot have children");
}
async fn run(
self: Box<Self>,
stop_token: CancellationToken,
) -> Result<(), AudioError> {
let FlacFileSink {
tx: _tx,
rx,
base_path,
encoder_options,
pcm_buffer_capacity,
} = *self;
Self::run_internal(rx, base_path, encoder_options, pcm_buffer_capacity, stop_token).await
}
}
impl TypedAudioNode for FlacFileSink { impl TypedAudioNode for FlacFileSink {
fn input_type(&self) -> Option<TypeRequirement> { fn input_type(&self) -> Option<TypeRequirement> {
// FlacFileSink accepte n'importe quel type entier (I16, I24, I32) // FlacFileSink accepte n'importe quel type entier (I16, I24, I32)
@@ -565,8 +582,12 @@ mod tests {
let frames = 256; let frames = 256;
// Créer le sink // Créer le sink
let (sink, tx) = FlacFileSink::with_channel_size(&output_path, 16); let sink = FlacFileSink::with_channel_size(&output_path, 16);
let sink_handle = tokio::spawn(async move { sink.run().await.unwrap() }); let tx = sink.get_tx().unwrap();
let stop_token = CancellationToken::new();
let sink_handle = tokio::spawn(async move {
Box::new(sink).run(stop_token).await.unwrap()
});
// Envoyer des segments avec métadonnées // Envoyer des segments avec métadonnées
tokio::spawn(async move { tokio::spawn(async move {
@@ -682,8 +703,12 @@ mod tests {
flac_stream.wait().await.unwrap(); flac_stream.wait().await.unwrap();
// Maintenant utiliser FlacFileSink pour réécrire le fichier // Maintenant utiliser FlacFileSink pour réécrire le fichier
let (sink, tx) = FlacFileSink::with_channel_size(&output_path, 16); let sink = FlacFileSink::with_channel_size(&output_path, 16);
let sink_handle = tokio::spawn(async move { sink.run().await.unwrap() }); let tx = sink.get_tx().unwrap();
let stop_token = CancellationToken::new();
let sink_handle = tokio::spawn(async move {
Box::new(sink).run(stop_token).await.unwrap()
});
// Lire le fichier input et envoyer les segments au sink // Lire le fichier input et envoyer les segments au sink
tokio::spawn(async move { tokio::spawn(async move {
@@ -745,10 +770,7 @@ mod tests {
decode_stream.wait().await.unwrap(); decode_stream.wait().await.unwrap();
}); });
let stats = sink_handle.await.unwrap(); sink_handle.await.unwrap();
assert_eq!(stats.tracks.len(), 1);
assert!(stats.tracks[0].chunks_received > 0);
assert_eq!(stats.tracks[0].total_samples, frames as u64);
// Vérifier que le fichier de sortie est valide // Vérifier que le fichier de sortie est valide
let file = File::open(&output_path).await.unwrap(); let file = File::open(&output_path).await.unwrap();

View File

@@ -1,14 +1,14 @@
use crate::{ use crate::{
nodes::{AudioError, MultiSubscriberNode, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS}, nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
type_constraints::TypeRequirement, type_constraints::TypeRequirement,
AudioChunk, AudioChunkData, AudioSegment, I24, AudioChunk, AudioChunkData, AudioPipelineNode, AudioSegment, I24,
}; };
use futures_util::StreamExt; use futures_util::StreamExt;
use pmoflac::{decode_audio_stream, StreamInfo}; use pmoflac::{decode_audio_stream, StreamInfo};
use pmometadata::{MemoryTrackMetadata, TrackMetadata}; use pmometadata::{MemoryTrackMetadata, TrackMetadata};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_util::io::StreamReader; use tokio_util::{io::StreamReader, sync::CancellationToken};
/// HttpSource - Récupère un fichier audio via HTTP et publie des `AudioSegment` /// HttpSource - Récupère un fichier audio via HTTP et publie des `AudioSegment`
/// ///
@@ -93,7 +93,8 @@ use tokio_util::io::StreamReader;
pub struct HttpSource { pub struct HttpSource {
url: String, url: String,
chunk_frames: usize, chunk_frames: usize,
subscribers: MultiSubscriberNode, child_txs: Vec<mpsc::Sender<Arc<AudioSegment>>>,
children: Vec<Box<dyn AudioPipelineNode>>,
} }
impl HttpSource { impl HttpSource {
@@ -136,85 +137,22 @@ impl HttpSource {
Self { Self {
url: url.into(), url: url.into(),
chunk_frames, chunk_frames,
subscribers: MultiSubscriberNode::new(), child_txs: Vec::new(),
children: Vec::new(),
} }
} }
/// Ajoute un abonné qui recevra les segments audio. async fn run_internal(
/// url: String,
/// Chaque abonné recevra une copie (via `Arc`) de tous les segments audio chunk_frames: usize,
/// produits par cette source, y compris les syncmarkers. child_txs: Vec<mpsc::Sender<Arc<AudioSegment>>>,
/// stop_token: CancellationToken,
/// # Arguments ) -> Result<(), AudioError> {
///
/// * `tx` - Channel sender pour recevoir les `AudioSegment`
///
/// # Exemples
///
/// ```no_run
/// use pmoaudio::HttpSource;
/// use tokio::sync::mpsc;
///
/// let mut source = HttpSource::new("http://example.com/audio.flac");
/// let (tx, rx) = mpsc::channel(16);
/// source.add_subscriber(tx);
/// ```
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.add_subscriber(tx);
}
/// Lance le téléchargement et la lecture du flux audio.
///
/// Cette méthode consomme `self` et exécute le pipeline complet:
/// 1. Effectue la requête HTTP GET vers l'URL spécifiée
/// 2. Vérifie le status HTTP (doit être 2xx)
/// 3. Extrait les métadonnées des headers HTTP
/// 4. Décode le flux audio en streaming
/// 5. Émet les syncmarkers et chunks audio vers les abonnés
///
/// La méthode se termine quand le flux est complètement lu ou en cas d'erreur.
///
/// # Erreurs
///
/// Retourne `AudioError::ProcessingError` si:
/// - La requête HTTP échoue (réseau, DNS, etc.)
/// - Le serveur retourne un status code non-2xx
/// - Le format audio n'est pas supporté
/// - Le décodage échoue
/// - Le fichier a un nombre de canaux non supporté (doit être 1 ou 2)
/// - La profondeur de bit n'est pas supportée (doit être 8, 16, 24 ou 32 bits)
///
/// # Exemples
///
/// ```no_run
/// use pmoaudio::HttpSource;
/// use tokio::sync::mpsc;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let mut source = HttpSource::new("http://example.com/audio.flac");
/// let (tx, mut rx) = mpsc::channel(16);
/// source.add_subscriber(tx);
///
/// let handle = tokio::spawn(async move {
/// source.run().await
/// });
///
/// // Traiter les segments
/// while let Some(segment) = rx.recv().await {
/// println!("Segment reçu: order={}", segment.order);
/// }
///
/// handle.await??;
/// Ok(())
/// }
/// ```
pub async fn run(self) -> Result<(), AudioError> {
// Effectuer la requête HTTP // Effectuer la requête HTTP
let response = reqwest::get(&self.url) let response = reqwest::get(&url)
.await .await
.map_err(|e| { .map_err(|e| {
AudioError::ProcessingError(format!("HTTP request failed for {}: {}", self.url, e)) AudioError::ProcessingError(format!("HTTP request failed for {}: {}", url, e))
})?; })?;
// Vérifier le status // Vérifier le status
@@ -222,12 +160,12 @@ impl HttpSource {
return Err(AudioError::ProcessingError(format!( return Err(AudioError::ProcessingError(format!(
"HTTP request returned status {}: {}", "HTTP request returned status {}: {}",
response.status(), response.status(),
self.url url
))); )));
} }
// Extraire les métadonnées depuis les headers HTTP // Extraire les métadonnées depuis les headers HTTP
let metadata = extract_metadata_from_headers(&response, &self.url).await; let metadata = extract_metadata_from_headers(&response, &url).await;
// Convertir le stream de bytes en AsyncRead // Convertir le stream de bytes en AsyncRead
let bytes_stream = response.bytes_stream(); let bytes_stream = response.bytes_stream();
@@ -244,38 +182,54 @@ impl HttpSource {
validate_stream(&stream_info)?; validate_stream(&stream_info)?;
// Calculer la taille des chunks si non spécifiée (0 = auto) // Calculer la taille des chunks si non spécifiée (0 = auto)
let chunk_frames = if self.chunk_frames == 0 { let chunk_frames_final = if chunk_frames == 0 {
let frames = let frames =
(stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize; (stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize;
frames.next_power_of_two().max(256) frames.next_power_of_two().max(256)
} else { } else {
self.chunk_frames.max(1) chunk_frames.max(1)
}; };
// Émettre TopZeroSync // Émettre TopZeroSync
let top_zero = AudioSegment::new_top_zero_sync(); let top_zero = AudioSegment::new_top_zero_sync();
self.subscribers.push(top_zero).await?; for tx in &child_txs {
tx.send(top_zero.clone()).await.map_err(|_| AudioError::SendError)?;
}
// Émettre TrackBoundary avec les métadonnées HTTP // Émettre TrackBoundary avec les métadonnées HTTP
let track_boundary = AudioSegment::new_track_boundary(0, 0.0, Arc::new(tokio::sync::RwLock::new(metadata))); let track_boundary = AudioSegment::new_track_boundary(0, 0.0, Arc::new(tokio::sync::RwLock::new(metadata)));
self.subscribers.push(track_boundary).await?; for tx in &child_txs {
tx.send(track_boundary.clone()).await.map_err(|_| AudioError::SendError)?;
}
// Préparer la lecture des chunks audio // Préparer la lecture des chunks audio
let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize; let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize;
let chunk_byte_len = chunk_frames * frame_bytes; let chunk_byte_len = chunk_frames_final * frame_bytes;
let mut pending = Vec::new(); let mut pending = Vec::new();
let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames)]; let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames_final)];
let mut chunk_index = 0u64; let mut chunk_index = 0u64;
let mut total_frames = 0u64; let mut total_frames = 0u64;
// Lire et émettre les chunks audio // Lire et émettre les chunks audio
loop { loop {
// Vérifier l'arrêt
if stop_token.is_cancelled() {
return Ok(());
}
// Remplir le buffer // Remplir le buffer
if pending.len() < chunk_byte_len { if pending.len() < chunk_byte_len {
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
let read = stream.read(&mut read_buf).await.map_err(|e| { let read = tokio::select! {
AudioError::ProcessingError(format!("I/O error while decoding: {}", e)) result = stream.read(&mut read_buf) => {
})?; result.map_err(|e| {
AudioError::ProcessingError(format!("I/O error while decoding: {}", e))
})?
}
_ = stop_token.cancelled() => {
return Ok(());
}
};
if read == 0 { if read == 0 {
break; break;
} }
@@ -288,7 +242,7 @@ impl HttpSource {
// Extraire un chunk // Extraire un chunk
let frames_in_pending = pending.len() / frame_bytes; let frames_in_pending = pending.len() / frame_bytes;
let frames_to_emit = frames_in_pending.min(chunk_frames); let frames_to_emit = frames_in_pending.min(chunk_frames_final);
let take_bytes = frames_to_emit * frame_bytes; let take_bytes = frames_to_emit * frame_bytes;
let chunk_bytes = pending.drain(..take_bytes).collect::<Vec<u8>>(); let chunk_bytes = pending.drain(..take_bytes).collect::<Vec<u8>>();
@@ -303,7 +257,13 @@ impl HttpSource {
chunk_index, chunk_index,
timestamp_sec, timestamp_sec,
)?; )?;
self.subscribers.push(segment).await?;
for tx in &child_txs {
if tx.send(segment.clone()).await.is_err() {
// Un enfant est mort, arrêter
return Ok(());
}
}
chunk_index += 1; chunk_index += 1;
total_frames += frames_to_emit as u64; total_frames += frames_to_emit as u64;
@@ -316,7 +276,9 @@ impl HttpSource {
let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64; let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64;
let segment = let segment =
bytes_to_segment(&pending, &stream_info, frames, chunk_index, timestamp_sec)?; bytes_to_segment(&pending, &stream_info, frames, chunk_index, timestamp_sec)?;
self.subscribers.push(segment).await?; for tx in &child_txs {
let _ = tx.send(segment.clone()).await;
}
total_frames += frames as u64; total_frames += frames as u64;
chunk_index += 1; chunk_index += 1;
} }
@@ -325,7 +287,9 @@ impl HttpSource {
// Émettre EndOfStream // Émettre EndOfStream
let final_timestamp = total_frames as f64 / stream_info.sample_rate as f64; let final_timestamp = total_frames as f64 / stream_info.sample_rate as f64;
let eos = AudioSegment::new_end_of_stream(chunk_index, final_timestamp); let eos = AudioSegment::new_end_of_stream(chunk_index, final_timestamp);
self.subscribers.push(eos).await?; for tx in &child_txs {
let _ = tx.send(eos.clone()).await;
}
// Attendre la fin du décodage // Attendre la fin du décodage
stream stream
@@ -510,6 +474,61 @@ fn bytes_to_segment(
})) }))
} }
#[async_trait::async_trait]
impl AudioPipelineNode for HttpSource {
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
None // HttpSource est une source, pas d'input
}
fn register(&mut self, child: Box<dyn AudioPipelineNode>) {
if let Some(tx) = child.get_tx() {
self.child_txs.push(tx);
}
self.children.push(child);
}
async fn run(
self: Box<Self>,
stop_token: CancellationToken,
) -> Result<(), AudioError> {
let HttpSource {
url,
chunk_frames,
child_txs,
children,
} = *self;
// Spawner tous les enfants
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);
}
// Lancer la logique interne
let work_result = Self::run_internal(url, chunk_frames, child_txs, stop_token.clone()).await;
// Attendre que tous les enfants se terminent
for handle in child_handles {
match handle.await {
Ok(Ok(())) => {},
Ok(Err(e)) => return Err(e),
Err(e) => {
return Err(AudioError::ProcessingError(format!(
"Child task panicked: {}",
e
)))
}
}
}
work_result
}
}
impl TypedAudioNode for HttpSource { impl TypedAudioNode for HttpSource {
fn input_type(&self) -> Option<TypeRequirement> { fn input_type(&self) -> Option<TypeRequirement> {
// HttpSource est une source, elle ne consomme pas d'audio // HttpSource est une source, elle ne consomme pas d'audio
@@ -534,6 +553,47 @@ mod tests {
Mock, MockServer, ResponseTemplate, Mock, MockServer, ResponseTemplate,
}; };
/// Nœud de test qui collecte tous les segments et les envoie à un channel de test
struct TestCollectorNode {
input_tx: mpsc::Sender<Arc<AudioSegment>>,
input_rx: mpsc::Receiver<Arc<AudioSegment>>,
output_tx: mpsc::Sender<Arc<AudioSegment>>,
}
impl TestCollectorNode {
fn new(output_tx: mpsc::Sender<Arc<AudioSegment>>) -> Self {
let (input_tx, input_rx) = mpsc::channel(16);
Self {
input_tx,
input_rx,
output_tx,
}
}
}
#[async_trait::async_trait]
impl AudioPipelineNode for TestCollectorNode {
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
Some(self.input_tx.clone())
}
fn register(&mut self, _child: Box<dyn AudioPipelineNode>) {
panic!("TestCollectorNode is a sink and cannot have children");
}
async fn run(
mut self: Box<Self>,
_stop_token: CancellationToken,
) -> Result<(), AudioError> {
while let Some(segment) = self.input_rx.recv().await {
if self.output_tx.send(segment).await.is_err() {
break;
}
}
Ok(())
}
}
/// Test de création basique de HttpSource /// Test de création basique de HttpSource
#[test] #[test]
fn test_http_source_creation() { fn test_http_source_creation() {
@@ -599,12 +659,18 @@ mod tests {
// Créer la source HTTP pointant vers le mock // Créer la source HTTP pointant vers le mock
let url = format!("{}/test.flac", mock_server.uri()); let url = format!("{}/test.flac", mock_server.uri());
let mut source = HttpSource::with_chunk_size(&url, 64); let mut source = HttpSource::with_chunk_size(&url, 64);
let (tx, mut rx) = mpsc::channel(16);
source.add_subscriber(tx);
// Lancer le téléchargement et le décodage // Créer un noeud collecteur pour recevoir les segments
let (tx, mut rx) = mpsc::channel(16);
let collector = TestCollectorNode::new(tx);
// Construire le pipeline
source.register(Box::new(collector));
// Lancer le pipeline
let stop_token = CancellationToken::new();
tokio::spawn(async move { tokio::spawn(async move {
source.run().await.unwrap(); Box::new(source).run(stop_token).await.unwrap();
}); });
// Vérifier les segments reçus // Vérifier les segments reçus
@@ -683,10 +749,12 @@ mod tests {
let url = format!("{}/stream", mock_server.uri()); let url = format!("{}/stream", mock_server.uri());
let mut source = HttpSource::new(&url); let mut source = HttpSource::new(&url);
let (tx, mut rx) = mpsc::channel(16); let (tx, mut rx) = mpsc::channel(16);
source.add_subscriber(tx); let collector = TestCollectorNode::new(tx);
source.register(Box::new(collector));
let stop_token = CancellationToken::new();
tokio::spawn(async move { tokio::spawn(async move {
source.run().await.unwrap(); Box::new(source).run(stop_token).await.unwrap();
}); });
// Chercher le TrackBoundary pour vérifier les métadonnées // Chercher le TrackBoundary pour vérifier les métadonnées
@@ -720,9 +788,11 @@ mod tests {
let url = format!("{}/notfound.flac", mock_server.uri()); let url = format!("{}/notfound.flac", mock_server.uri());
let mut source = HttpSource::new(&url); let mut source = HttpSource::new(&url);
let (tx, _rx) = mpsc::channel(16); let (tx, _rx) = mpsc::channel(16);
source.add_subscriber(tx); let collector = TestCollectorNode::new(tx);
source.register(Box::new(collector));
let result = source.run().await; let stop_token = CancellationToken::new();
let result = Box::new(source).run(stop_token).await;
assert!(result.is_err(), "Doit retourner une erreur pour HTTP 404"); assert!(result.is_err(), "Doit retourner une erreur pour HTTP 404");
if let Err(AudioError::ProcessingError(msg)) = result { if let Err(AudioError::ProcessingError(msg)) = result {
@@ -751,9 +821,11 @@ mod tests {
let url = format!("{}/invalid.flac", mock_server.uri()); let url = format!("{}/invalid.flac", mock_server.uri());
let mut source = HttpSource::new(&url); let mut source = HttpSource::new(&url);
let (tx, _rx) = mpsc::channel(16); let (tx, _rx) = mpsc::channel(16);
source.add_subscriber(tx); let collector = TestCollectorNode::new(tx);
source.register(Box::new(collector));
let result = source.run().await; let stop_token = CancellationToken::new();
let result = Box::new(source).run(stop_token).await;
assert!( assert!(
result.is_err(), result.is_err(),
"Doit retourner une erreur pour un format invalide" "Doit retourner une erreur pour un format invalide"
@@ -804,10 +876,12 @@ mod tests {
let url = format!("{}/my-song.flac", mock_server.uri()); let url = format!("{}/my-song.flac", mock_server.uri());
let mut source = HttpSource::new(&url); let mut source = HttpSource::new(&url);
let (tx, mut rx) = mpsc::channel(16); let (tx, mut rx) = mpsc::channel(16);
source.add_subscriber(tx); let collector = TestCollectorNode::new(tx);
source.register(Box::new(collector));
let stop_token = CancellationToken::new();
tokio::spawn(async move { tokio::spawn(async move {
source.run().await.unwrap(); Box::new(source).run(stop_token).await.unwrap();
}); });
let mut found_title = false; let mut found_title = false;

View File

@@ -4,7 +4,6 @@
//! un pipeline audio, ainsi que les traits et structures de support. //! un pipeline audio, ainsi que les traits et structures de support.
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc;
use crate::type_constraints::{TypeMismatch, TypeRequirement}; use crate::type_constraints::{TypeMismatch, TypeRequirement};
use crate::AudioSegment; use crate::AudioSegment;
@@ -109,94 +108,6 @@ pub trait TypedAudioNode {
} }
} }
/// Node avec un seul abonné (pas de clone inutile)
///
/// Optimisé pour les cas où un node n'a qu'un seul destinataire.
/// Le Arc du chunk est simplement transféré sans clonage supplémentaire.
///
/// # Exemples
///
/// ```
/// use pmoaudio::SingleSubscriberNode;
/// use tokio::sync::mpsc;
///
/// let (tx, rx) = mpsc::channel(10);
/// let node = SingleSubscriberNode::new(tx);
/// ```
pub struct SingleSubscriberNode {
tx: mpsc::Sender<Arc<AudioSegment>>,
}
impl SingleSubscriberNode {
pub fn new(tx: mpsc::Sender<Arc<AudioSegment>>) -> Self {
Self { tx }
}
pub async fn push(&self, chunk: Arc<AudioSegment>) -> Result<(), AudioError> {
self.tx.send(chunk).await.map_err(|_| AudioError::SendError)
}
}
/// Node avec plusieurs abonnés (partage le même Arc)
///
/// Permet de broadcaster un chunk à plusieurs destinations.
/// Tous les abonnés reçoivent le même `Arc<AudioSegment>`, donc pas de copie
/// des données audio - seul le compteur de référence Arc est incrémenté.
///
/// # Exemples
///
/// ```
/// use pmoaudio::MultiSubscriberNode;
/// use tokio::sync::mpsc;
///
/// let mut node = MultiSubscriberNode::new();
/// let (tx1, rx1) = mpsc::channel(10);
/// let (tx2, rx2) = mpsc::channel(10);
///
/// node.add_subscriber(tx1);
/// node.add_subscriber(tx2);
/// // Les deux abonnés recevront les mêmes chunks
/// ```
pub struct MultiSubscriberNode {
subscribers: Vec<mpsc::Sender<Arc<AudioSegment>>>,
}
impl MultiSubscriberNode {
pub fn new() -> Self {
Self {
subscribers: Vec::new(),
}
}
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioSegment>>) {
self.subscribers.push(tx);
}
pub async fn push(&self, chunk: Arc<AudioSegment>) -> Result<(), AudioError> {
for tx in &self.subscribers {
// On partage le même Arc avec tous les abonnés
tx.send(chunk.clone())
.await
.map_err(|_| AudioError::SendError)?;
}
Ok(())
}
pub async fn try_push(&self, chunk: Arc<AudioSegment>) -> Result<(), AudioError> {
for tx in &self.subscribers {
// try_send non-bloquant, ignore si saturé
let _ = tx.try_send(chunk.clone());
}
Ok(())
}
}
impl Default for MultiSubscriberNode {
fn default() -> Self {
Self::new()
}
}
/// Erreurs possibles dans le pipeline audio /// Erreurs possibles dans le pipeline audio
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum AudioError { pub enum AudioError {

110
pmoaudio/src/pipeline.rs Normal file
View File

@@ -0,0 +1,110 @@
//! Architecture de pipeline audio avec propagation automatique du run et gestion d'arrêt
//!
//! Ce module définit le trait `AudioPipelineNode` qui permet de construire des arbres
//! de traitement audio avec :
//! - Démarrage automatique de tous les enfants lors du run de la tête
//! - Arrêt coordonné sur EOF ou erreur
//! - Propagation bidirectionnelle sans boucle infinie
//!
//! # Architecture
//!
//! Les pipelines forment des **arbres** (pas de DAG) où :
//! - Les sources n'ont pas d'input (get_tx retourne None)
//! - Les sinks n'ont pas d'enfants (register panic)
//! - Les convertisseurs ont à la fois un input et des enfants
//!
//! # Mécanisme d'arrêt
//!
//! - **Descendant** : `stop_token.cancel()` propage l'arrêt vers les fils
//! - **Montant** : Le retour de `run()` informe le parent
//! - **Détection** : Un enfant mort → parent voit `send().is_err()` ou `await handle`
//!
//! # Exemple
//!
//! ```no_run
//! use pmoaudio::{FileSource, AudioPipelineNode};
//! use pmoaudio::nodes::FlacFileSink;
//! use tokio_util::sync::CancellationToken;
//!
//! # async fn example() -> Result<(), pmoaudio::nodes::AudioError> {
//! // Construire le pipeline
//! let mut source = FileSource::new("input.flac");
//! let sink = FlacFileSink::new("output.flac");
//!
//! source.register(Box::new(sink));
//!
//! // Lancer avec contrôle d'arrêt
//! let stop_token = CancellationToken::new();
//! Box::new(source).run(stop_token).await?;
//! # Ok(())
//! # }
//! ```
use crate::{nodes::AudioError, AudioSegment};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
/// Trait pour les nœuds d'un pipeline audio
///
/// Permet la construction d'arbres de traitement avec:
/// - Démarrage automatique de tous les enfants
/// - Arrêt coordonné sur EOF ou erreur
/// - Propagation bidirectionnelle sans boucle
#[async_trait::async_trait]
pub trait AudioPipelineNode: Send + 'static {
/// Retourne un clone du sender pour recevoir des segments
///
/// # Retourne
///
/// - `Some(tx)` pour les nœuds qui ont un input (sinks, convertisseurs)
/// - `None` pour les sources qui génèrent des données
///
/// Le sender retourné est un clone, permettant au parent de l'extraire
/// avant de consommer le nœud dans `run()`.
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>>;
/// Enregistre un nœud enfant dans l'arbre
///
/// # Arguments
///
/// * `child` - Le nœud enfant à enregistrer
///
/// # Comportement
///
/// - Pour les sources et convertisseurs : enregistre l'enfant et clone son tx
/// - Pour les sinks : panic (nœuds terminaux)
///
/// Le parent extrait le tx via `child.get_tx()` avant de stocker le child.
fn register(&mut self, child: Box<dyn AudioPipelineNode>);
/// Lance le nœud et tous ses enfants
///
/// # Arguments
///
/// * `stop_token` - Token d'arrêt partagé pour coordination
///
/// # Comportement
///
/// 1. **Spawn enfants** : Tous les enfants sont spawned avant le traitement
/// 2. **Traitement** : Le nœud fait son travail (lecture, conversion, écriture)
/// 3. **Détection** : Si un enfant meurt, le parent le détecte via send().is_err()
/// 4. **Arrêt** : Sur EOF/erreur, appel de `stop_token.cancel()` pour les enfants
/// 5. **Attente** : Attend que tous les enfants se terminent
/// 6. **Retour** : Retourne pour informer le parent (propagation montante)
///
/// # Propagation d'erreur
///
/// - Erreur du nœud → propagée vers les enfants (cancel) puis vers le parent (return)
/// - Erreur d'un enfant → détectée à l'await du handle, propagée vers le parent
///
/// # Arrêt sans boucle
///
/// - Un seul `cancel()` par nœud (en sortant de la boucle de travail)
/// - L'enfant ne cancel JAMAIS le parent
/// - `cancel()` est idempotent (pas de problème si appelé plusieurs fois)
async fn run(
self: Box<Self>,
stop_token: CancellationToken,
) -> Result<(), AudioError>;
}

View File

@@ -13,7 +13,6 @@ pmodidl = { path = "../pmodidl" }
# Streaming FLAC asynchrone # Streaming FLAC asynchrone
pmoflac = { path = "../pmoflac" } pmoflac = { path = "../pmoflac" }
pmometadata = { path = "../pmometadata" } pmometadata = { path = "../pmometadata" }
pmoaudio = { path = "../pmoaudio" }
# Base de données # Base de données
rusqlite = { version = "0.37", features = ["bundled"] } rusqlite = { version = "0.37", features = ["bundled"] }

View File

@@ -79,7 +79,6 @@
pub mod cache; pub mod cache;
pub mod metadata; pub mod metadata;
pub mod metadata_ext; pub mod metadata_ext;
pub mod nodes;
pub mod streaming; pub mod streaming;
pub mod track_metadata; pub mod track_metadata;
@@ -93,7 +92,6 @@ pub mod config_ext;
pub use cache::{add_with_metadata_extraction, get_metadata, new_cache, AudioConfig, Cache}; pub use cache::{add_with_metadata_extraction, get_metadata, new_cache, AudioConfig, Cache};
pub use metadata::AudioMetadata; pub use metadata::AudioMetadata;
pub use metadata_ext::{AudioMetadataExt, AudioTrackMetadataExt}; pub use metadata_ext::{AudioMetadataExt, AudioTrackMetadataExt};
pub use nodes::{FlacCacheSink, FlacCacheSinkStats};
pub use track_metadata::AudioCacheTrackMetadata; pub use track_metadata::AudioCacheTrackMetadata;
#[cfg(feature = "pmoconfig")] #[cfg(feature = "pmoconfig")]

View File

@@ -1,8 +0,0 @@
//! Nodes audio pour pmoaudiocache
//!
//! Ce module fournit des nodes audio spécialisés qui étendent pmoaudio
//! pour intégrer le cache audio.
pub mod flac_cache_sink;
pub use flac_cache_sink::{FlacCacheSink, FlacCacheSinkStats};

View File

@@ -8,6 +8,9 @@ edition = "2021"
pmoaudiocache = { path = "../pmoaudiocache" } pmoaudiocache = { path = "../pmoaudiocache" }
pmocache = { path = "../pmocache" } pmocache = { path = "../pmocache" }
# Métadonnées
pmometadata = { path = "../pmometadata" }
# DIDL-Lite pour UPnP # DIDL-Lite pour UPnP
pmodidl = { path = "../pmodidl" } pmodidl = { path = "../pmodidl" }

View File

@@ -1,9 +1,11 @@
//! PlaylistTrack : résultat d'un pop() avec helpers pour accéder au cache //! PlaylistTrack : résultat d'un pop() avec helpers pour accéder au cache
use crate::Result; use crate::Result;
use pmoaudiocache::AudioMetadataExt; use pmoaudiocache::{AudioMetadataExt, AudioTrackMetadataExt};
use pmocache::cache_trait::FileCache; use pmocache::cache_trait::FileCache;
use std::path::PathBuf; use pmometadata::TrackMetadata;
use std::{path::PathBuf, sync::Arc};
use tokio::sync::RwLock;
/// Un morceau récupéré depuis une playlist /// Un morceau récupéré depuis une playlist
/// ///
@@ -87,6 +89,34 @@ impl PlaylistTrack {
.map_err(|e| crate::Error::CacheError(e.to_string())) .map_err(|e| crate::Error::CacheError(e.to_string()))
} }
/// Retourne une instance de TrackMetadata pour ce morceau
///
/// Cette méthode fournit un accès unifié aux métadonnées via le trait `TrackMetadata`.
/// L'instance retournée implémente toutes les méthodes du trait et permet un accès
/// asynchrone thread-safe aux métadonnées via RwLock.
///
/// # Exemples
///
/// ```no_run
/// # use pmoplaylist::*;
/// # async fn example(track: PlaylistTrack) -> Result<()> {
/// // Obtenir l'instance TrackMetadata
/// let metadata = track.track_metadata()?;
///
/// // Accéder aux métadonnées via le trait
/// let title = metadata.read().await.get_title().await?;
/// let artist = metadata.read().await.get_artist().await?;
///
/// println!("Titre: {:?}", title);
/// println!("Artiste: {:?}", artist);
/// # Ok(())
/// # }
/// ```
pub fn track_metadata(&self) -> Result<Arc<RwLock<dyn TrackMetadata>>> {
let cache = crate::manager::audio_cache()?;
Ok(cache.track_metadata(&self.cache_pk))
}
/// Récupère uniquement le titre du morceau (méthode légère) /// Récupère uniquement le titre du morceau (méthode légère)
/// ///
/// Utilise l'extension trait `AudioMetadataExt` pour récupérer qu'une seule valeur /// Utilise l'extension trait `AudioMetadataExt` pour récupérer qu'une seule valeur