Ajout d'un noeud puis vers le cache audio
This commit is contained in:
713
Cargo.lock
generated
713
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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"] }
|
||||||
|
|||||||
@@ -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
30
pmoaudio-ext/Cargo.toml
Normal 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
30
pmoaudio-ext/src/lib.rs
Normal 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::*;
|
||||||
@@ -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)
|
||||||
11
pmoaudio-ext/src/sinks/mod.rs
Normal file
11
pmoaudio-ext/src/sinks/mod.rs
Normal 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};
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
110
pmoaudio/src/pipeline.rs
Normal 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>;
|
||||||
|
}
|
||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
|||||||
@@ -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};
|
|
||||||
@@ -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" }
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user