Ou encore un peu de factorisation dans les nœuds.
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3035,6 +3035,7 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cpal",
|
"cpal",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"once_cell",
|
||||||
"paste",
|
"paste",
|
||||||
"pmoflac",
|
"pmoflac",
|
||||||
"pmometadata",
|
"pmometadata",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use pmoaudio::{
|
use pmoaudio::{
|
||||||
nodes::{AudioError, DEFAULT_CHANNEL_SIZE},
|
nodes::{AudioError, DEFAULT_CHANNEL_SIZE},
|
||||||
pipeline::{AudioPipelineNode, Node, NodeLogic, PipelineHandle},
|
pipeline::{send_to_children, AudioPipelineNode, Node, NodeLogic, PipelineHandle},
|
||||||
AudioSegment, TypeRequirement, TypedAudioNode,
|
AudioSegment, TypeRequirement, TypedAudioNode,
|
||||||
};
|
};
|
||||||
use pmocovers::Cache as CoverCache;
|
use pmocovers::Cache as CoverCache;
|
||||||
@@ -140,6 +140,7 @@ impl NodeLogic for TrackBoundaryCoverLogic {
|
|||||||
"TrackBoundaryCoverNode requires an upstream input channel".into(),
|
"TrackBoundaryCoverNode requires an upstream input channel".into(),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
let node_name = std::any::type_name::<Self>();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let segment = select! {
|
let segment = select! {
|
||||||
@@ -159,11 +160,7 @@ impl NodeLogic for TrackBoundaryCoverLogic {
|
|||||||
self.ensure_cover_pk(Arc::clone(metadata)).await;
|
self.ensure_cover_pk(Arc::clone(metadata)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
for tx in &output {
|
send_to_children(node_name, &output, segment).await?;
|
||||||
if tx.send(segment.clone()).await.is_err() {
|
|
||||||
return Err(AudioError::ChildDied);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
use pmoaudio::{
|
use pmoaudio::{
|
||||||
nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
|
nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
|
||||||
pipeline::{AudioPipelineNode, Node, NodeLogic},
|
pipeline::{send_to_children, AudioPipelineNode, Node, NodeLogic},
|
||||||
type_constraints::TypeRequirement,
|
type_constraints::TypeRequirement,
|
||||||
AudioChunk, AudioChunkData, AudioSegment, I24,
|
AudioChunk, AudioChunkData, AudioSegment, I24,
|
||||||
};
|
};
|
||||||
@@ -175,16 +175,7 @@ impl NodeLogic for PlaylistSourceLogic {
|
|||||||
output.len()
|
output.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Macro helper pour envoyer à tous les enfants
|
let node_name = std::any::type_name::<Self>();
|
||||||
macro_rules! send_to_children {
|
|
||||||
($segment:expr) => {
|
|
||||||
for tx in &output {
|
|
||||||
tx.send($segment.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut first_track = true;
|
let mut first_track = true;
|
||||||
|
|
||||||
@@ -193,7 +184,7 @@ impl NodeLogic for PlaylistSourceLogic {
|
|||||||
if stop_token.is_cancelled() {
|
if stop_token.is_cancelled() {
|
||||||
tracing::info!("PlaylistSourceLogic: stop requested, emitting EndOfStream");
|
tracing::info!("PlaylistSourceLogic: stop requested, emitting EndOfStream");
|
||||||
let eos = AudioSegment::new_end_of_stream(0, 0.0);
|
let eos = AudioSegment::new_end_of_stream(0, 0.0);
|
||||||
send_to_children!(eos);
|
send_to_children(node_name, &output, eos).await?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +193,7 @@ impl NodeLogic for PlaylistSourceLogic {
|
|||||||
_ = stop_token.cancelled() => {
|
_ = stop_token.cancelled() => {
|
||||||
tracing::info!("PlaylistSourceLogic: stop cancelled during pop");
|
tracing::info!("PlaylistSourceLogic: stop cancelled during pop");
|
||||||
let eos = AudioSegment::new_end_of_stream(0, 0.0);
|
let eos = AudioSegment::new_end_of_stream(0, 0.0);
|
||||||
send_to_children!(eos);
|
send_to_children(node_name, &output, eos).await?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
result = self.playlist_handle.pop() => {
|
result = self.playlist_handle.pop() => {
|
||||||
@@ -236,7 +227,7 @@ impl NodeLogic for PlaylistSourceLogic {
|
|||||||
0.0,
|
0.0,
|
||||||
format!("Playlist error: {}", e)
|
format!("Playlist error: {}", e)
|
||||||
);
|
);
|
||||||
send_to_children!(error_marker);
|
send_to_children(node_name, &output, error_marker).await?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,7 +241,7 @@ impl NodeLogic for PlaylistSourceLogic {
|
|||||||
tracing::warn!("PlaylistSourceLogic: failed to get metadata: {}", e);
|
tracing::warn!("PlaylistSourceLogic: failed to get metadata: {}", e);
|
||||||
let error_marker =
|
let error_marker =
|
||||||
AudioSegment::new_error(0, 0.0, format!("Failed to get metadata: {}", e));
|
AudioSegment::new_error(0, 0.0, format!("Failed to get metadata: {}", e));
|
||||||
send_to_children!(error_marker);
|
send_to_children(node_name, &output, error_marker).await?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -279,7 +270,7 @@ impl NodeLogic for PlaylistSourceLogic {
|
|||||||
|
|
||||||
tracing::debug!("PlaylistSourceLogic: emitting TrackBoundary");
|
tracing::debug!("PlaylistSourceLogic: emitting TrackBoundary");
|
||||||
let boundary = AudioSegment::new_track_boundary(0, 0.0, metadata);
|
let boundary = AudioSegment::new_track_boundary(0, 0.0, metadata);
|
||||||
send_to_children!(boundary);
|
send_to_children(node_name, &output, boundary).await?;
|
||||||
|
|
||||||
// Obtenir le chemin du fichier
|
// Obtenir le chemin du fichier
|
||||||
let file_path = match track.file_path() {
|
let file_path = match track.file_path() {
|
||||||
@@ -288,7 +279,7 @@ impl NodeLogic for PlaylistSourceLogic {
|
|||||||
tracing::warn!("PlaylistSourceLogic: failed to get file path: {}", e);
|
tracing::warn!("PlaylistSourceLogic: failed to get file path: {}", e);
|
||||||
let error_marker =
|
let error_marker =
|
||||||
AudioSegment::new_error(0, 0.0, format!("Failed to get file path: {}", e));
|
AudioSegment::new_error(0, 0.0, format!("Failed to get file path: {}", e));
|
||||||
send_to_children!(error_marker);
|
send_to_children(node_name, &output, error_marker).await?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -303,6 +294,7 @@ impl NodeLogic for PlaylistSourceLogic {
|
|||||||
first_track = false;
|
first_track = false;
|
||||||
|
|
||||||
match decode_and_emit_track(
|
match decode_and_emit_track(
|
||||||
|
node_name,
|
||||||
&file_path,
|
&file_path,
|
||||||
self.chunk_frames,
|
self.chunk_frames,
|
||||||
&output,
|
&output,
|
||||||
@@ -334,7 +326,7 @@ impl NodeLogic for PlaylistSourceLogic {
|
|||||||
tracing::error!("PlaylistSourceLogic: error decoding track: {}", e);
|
tracing::error!("PlaylistSourceLogic: error decoding track: {}", e);
|
||||||
let error_marker =
|
let error_marker =
|
||||||
AudioSegment::new_error(0, 0.0, format!("Decode error: {}", e));
|
AudioSegment::new_error(0, 0.0, format!("Decode error: {}", e));
|
||||||
send_to_children!(error_marker);
|
send_to_children(node_name, &output, error_marker).await?;
|
||||||
// Continue vers la piste suivante
|
// Continue vers la piste suivante
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,6 +348,7 @@ impl NodeLogic for PlaylistSourceLogic {
|
|||||||
/// Gère le cache progressif : si EOF est atteint et que le download est toujours en cours,
|
/// Gère le cache progressif : si EOF est atteint et que le download est toujours en cours,
|
||||||
/// attend et réessaie au lieu de terminer immédiatement.
|
/// attend et réessaie au lieu de terminer immédiatement.
|
||||||
async fn decode_and_emit_track(
|
async fn decode_and_emit_track(
|
||||||
|
node_name: &'static str,
|
||||||
path: &PathBuf,
|
path: &PathBuf,
|
||||||
chunk_frames: usize,
|
chunk_frames: usize,
|
||||||
output: &[mpsc::Sender<Arc<AudioSegment>>],
|
output: &[mpsc::Sender<Arc<AudioSegment>>],
|
||||||
@@ -491,18 +484,10 @@ async fn decode_and_emit_track(
|
|||||||
if emit_top_zero && total_frames == 0 {
|
if emit_top_zero && total_frames == 0 {
|
||||||
tracing::debug!("decode_and_emit_track: emitting TopZeroSync (first chunk)");
|
tracing::debug!("decode_and_emit_track: emitting TopZeroSync (first chunk)");
|
||||||
let top_zero = AudioSegment::new_top_zero_sync();
|
let top_zero = AudioSegment::new_top_zero_sync();
|
||||||
for tx in output {
|
send_to_children(node_name, output, top_zero).await?;
|
||||||
tx.send(top_zero.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for tx in output {
|
send_to_children(node_name, output, segment).await?;
|
||||||
tx.send(segment.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
chunk_index += 1;
|
chunk_index += 1;
|
||||||
total_frames += frames_to_emit as u64;
|
total_frames += frames_to_emit as u64;
|
||||||
@@ -517,11 +502,7 @@ async fn decode_and_emit_track(
|
|||||||
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)?;
|
||||||
for tx in output {
|
send_to_children(node_name, output, segment).await?;
|
||||||
tx.send(segment.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ bytemuck = "1.24.0"
|
|||||||
reqwest = { version = "0.12", features = ["stream"] }
|
reqwest = { version = "0.12", features = ["stream"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
cpal = "0.15"
|
cpal = "0.15"
|
||||||
|
once_cell = "1.20"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = "0.4"
|
tokio-test = "0.4"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
nodes::AudioError,
|
nodes::AudioError,
|
||||||
pipeline::{Node, NodeLogic},
|
pipeline::{send_to_children, Node, NodeLogic},
|
||||||
AudioChunk, AudioPipelineNode, AudioSegment,
|
AudioChunk, AudioPipelineNode, AudioSegment,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -100,12 +100,7 @@ where
|
|||||||
segment
|
segment
|
||||||
};
|
};
|
||||||
|
|
||||||
// Envoyer à tous les enfants
|
send_to_children(std::any::type_name::<Self>(), &output, output_segment).await?;
|
||||||
for tx in &output {
|
|
||||||
tx.send(output_segment.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
|
nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
|
||||||
pipeline::{AudioPipelineNode, Node, NodeLogic},
|
pipeline::{send_to_children, AudioPipelineNode, Node, NodeLogic},
|
||||||
type_constraints::TypeRequirement,
|
type_constraints::TypeRequirement,
|
||||||
AudioChunk, AudioChunkData, AudioSegment, I24,
|
AudioChunk, AudioChunkData, AudioSegment, I24,
|
||||||
};
|
};
|
||||||
@@ -47,17 +47,6 @@ impl NodeLogic for FileSourceLogic {
|
|||||||
output.len()
|
output.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Macro helper pour envoyer à tous les enfants
|
|
||||||
macro_rules! send_to_children {
|
|
||||||
($segment:expr) => {
|
|
||||||
for tx in &output {
|
|
||||||
tx.send($segment.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ouvrir le fichier
|
// Ouvrir le fichier
|
||||||
let file = File::open(&self.path)
|
let file = File::open(&self.path)
|
||||||
.await
|
.await
|
||||||
@@ -82,7 +71,7 @@ impl NodeLogic for FileSourceLogic {
|
|||||||
|
|
||||||
// Émettre TopZeroSync
|
// Émettre TopZeroSync
|
||||||
let top_zero = AudioSegment::new_top_zero_sync();
|
let top_zero = AudioSegment::new_top_zero_sync();
|
||||||
send_to_children!(top_zero);
|
send_to_children(std::any::type_name::<Self>(), &output, top_zero).await?;
|
||||||
|
|
||||||
// Extraire et émettre les métadonnées du fichier
|
// Extraire et émettre les métadonnées du fichier
|
||||||
if let Ok(file_metadata) = AudioFileMetadata::from_file(&self.path) {
|
if let Ok(file_metadata) = AudioFileMetadata::from_file(&self.path) {
|
||||||
@@ -110,7 +99,7 @@ impl NodeLogic for FileSourceLogic {
|
|||||||
0.0,
|
0.0,
|
||||||
Arc::new(tokio::sync::RwLock::new(metadata)),
|
Arc::new(tokio::sync::RwLock::new(metadata)),
|
||||||
);
|
);
|
||||||
send_to_children!(track_boundary);
|
send_to_children(std::any::type_name::<Self>(), &output, track_boundary).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Préparer la lecture des chunks audio
|
// Préparer la lecture des chunks audio
|
||||||
@@ -168,7 +157,7 @@ impl NodeLogic for FileSourceLogic {
|
|||||||
timestamp_sec,
|
timestamp_sec,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_to_children!(segment);
|
send_to_children(std::any::type_name::<Self>(), &output, segment).await?;
|
||||||
|
|
||||||
chunk_index += 1;
|
chunk_index += 1;
|
||||||
total_frames += frames_to_emit as u64;
|
total_frames += frames_to_emit as u64;
|
||||||
@@ -183,7 +172,7 @@ impl NodeLogic for FileSourceLogic {
|
|||||||
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)?;
|
||||||
send_to_children!(segment);
|
send_to_children(std::any::type_name::<Self>(), &output, segment).await?;
|
||||||
total_frames += frames as u64;
|
total_frames += frames as u64;
|
||||||
chunk_index += 1;
|
chunk_index += 1;
|
||||||
}
|
}
|
||||||
@@ -192,7 +181,7 @@ impl NodeLogic for FileSourceLogic {
|
|||||||
// É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);
|
||||||
send_to_children!(eos);
|
send_to_children(std::any::type_name::<Self>(), &output, eos).await?;
|
||||||
|
|
||||||
// Attendre la fin du décodage
|
// Attendre la fin du décodage
|
||||||
stream
|
stream
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
|
nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
|
||||||
pipeline::{Node, NodeLogic},
|
pipeline::{send_to_children, Node, NodeLogic},
|
||||||
type_constraints::TypeRequirement,
|
type_constraints::TypeRequirement,
|
||||||
AudioChunk, AudioChunkData, AudioPipelineNode, AudioSegment, I24,
|
AudioChunk, AudioChunkData, AudioPipelineNode, AudioSegment, I24,
|
||||||
};
|
};
|
||||||
@@ -127,16 +127,6 @@ impl NodeLogic for HttpSourceLogic {
|
|||||||
output: Vec<mpsc::Sender<Arc<AudioSegment>>>,
|
output: Vec<mpsc::Sender<Arc<AudioSegment>>>,
|
||||||
stop_token: CancellationToken,
|
stop_token: CancellationToken,
|
||||||
) -> Result<(), AudioError> {
|
) -> Result<(), AudioError> {
|
||||||
macro_rules! send_to_children {
|
|
||||||
($segment:expr) => {
|
|
||||||
for tx in &output {
|
|
||||||
tx.send($segment.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Effectuer la requête HTTP
|
// Effectuer la requête HTTP
|
||||||
let response = reqwest::get(&self.url).await.map_err(|e| {
|
let response = reqwest::get(&self.url).await.map_err(|e| {
|
||||||
AudioError::ProcessingError(format!("HTTP request failed for {}: {}", self.url, e))
|
AudioError::ProcessingError(format!("HTTP request failed for {}: {}", self.url, e))
|
||||||
@@ -179,12 +169,17 @@ impl NodeLogic for HttpSourceLogic {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Émettre TopZeroSync
|
// Émettre TopZeroSync
|
||||||
send_to_children!(AudioSegment::new_top_zero_sync());
|
send_to_children(
|
||||||
|
std::any::type_name::<Self>(),
|
||||||
|
&output,
|
||||||
|
AudioSegment::new_top_zero_sync(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Émettre TrackBoundary avec les métadonnées HTTP
|
// Émettre TrackBoundary avec les métadonnées HTTP
|
||||||
let track_boundary =
|
let track_boundary =
|
||||||
AudioSegment::new_track_boundary(0, 0.0, Arc::new(tokio::sync::RwLock::new(metadata)));
|
AudioSegment::new_track_boundary(0, 0.0, Arc::new(tokio::sync::RwLock::new(metadata)));
|
||||||
send_to_children!(track_boundary);
|
send_to_children(std::any::type_name::<Self>(), &output, track_boundary).await?;
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -242,7 +237,7 @@ impl NodeLogic for HttpSourceLogic {
|
|||||||
timestamp_sec,
|
timestamp_sec,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
send_to_children!(segment);
|
send_to_children(std::any::type_name::<Self>(), &output, segment).await?;
|
||||||
|
|
||||||
chunk_index += 1;
|
chunk_index += 1;
|
||||||
total_frames += frames_to_emit as u64;
|
total_frames += frames_to_emit as u64;
|
||||||
@@ -255,7 +250,7 @@ impl NodeLogic for HttpSourceLogic {
|
|||||||
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)?;
|
||||||
send_to_children!(segment);
|
send_to_children(std::any::type_name::<Self>(), &output, segment).await?;
|
||||||
total_frames += frames as u64;
|
total_frames += frames as u64;
|
||||||
chunk_index += 1;
|
chunk_index += 1;
|
||||||
}
|
}
|
||||||
@@ -264,7 +259,7 @@ impl NodeLogic for HttpSourceLogic {
|
|||||||
// É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);
|
||||||
send_to_children!(eos);
|
send_to_children(std::any::type_name::<Self>(), &output, eos).await?;
|
||||||
|
|
||||||
// Attendre la fin du décodage
|
// Attendre la fin du décodage
|
||||||
stream
|
stream
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
dsp::resampling::{build_resampler, resampling, Resampler},
|
dsp::resampling::{build_resampler, resampling, Resampler},
|
||||||
nodes::{AudioError, TypedAudioNode},
|
nodes::{AudioError, TypedAudioNode},
|
||||||
pipeline::{AudioPipelineNode, Node, NodeLogic},
|
pipeline::{send_to_children, AudioPipelineNode, Node, NodeLogic},
|
||||||
type_constraints::TypeRequirement,
|
type_constraints::TypeRequirement,
|
||||||
AudioChunk, AudioChunkData, AudioSegment, BitDepth, I24,
|
AudioChunk, AudioChunkData, AudioSegment, BitDepth, I24,
|
||||||
};
|
};
|
||||||
@@ -172,12 +172,7 @@ impl NodeLogic for ResamplingLogic {
|
|||||||
segment
|
segment
|
||||||
};
|
};
|
||||||
|
|
||||||
// Envoyer à tous les enfants
|
send_to_children(std::any::type_name::<Self>(), &output, output_segment).await?;
|
||||||
for tx in &output {
|
|
||||||
tx.send(output_segment.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE, DEFAULT_CHUNK_DURATION_MS},
|
nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE, DEFAULT_CHUNK_DURATION_MS},
|
||||||
pipeline::{AudioPipelineNode, Node, NodeLogic},
|
pipeline::{send_to_children, AudioPipelineNode, Node, NodeLogic},
|
||||||
type_constraints::TypeRequirement,
|
type_constraints::TypeRequirement,
|
||||||
AudioSegment, SyncMarker, _AudioSegment,
|
AudioSegment, SyncMarker, _AudioSegment,
|
||||||
};
|
};
|
||||||
@@ -135,11 +135,7 @@ impl TimerBufferNodeLogic {
|
|||||||
self.buffer.len()
|
self.buffer.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
for tx in output {
|
send_to_children(std::any::type_name::<Self>(), output, segment).await?;
|
||||||
tx.send(segment.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -228,11 +224,12 @@ impl NodeLogic for TimerBufferNodeLogic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Propager le marker immédiatement
|
// Propager le marker immédiatement
|
||||||
for tx in &output {
|
send_to_children(
|
||||||
tx.send(segment.clone())
|
std::any::type_name::<Self>(),
|
||||||
.await
|
&output,
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
segment.clone(),
|
||||||
}
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
_AudioSegment::Chunk(chunk) => {
|
_AudioSegment::Chunk(chunk) => {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE},
|
nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE},
|
||||||
pipeline::{AudioPipelineNode, Node, NodeLogic},
|
pipeline::{send_to_children_with_timing, AudioPipelineNode, Node, NodeLogic},
|
||||||
type_constraints::TypeRequirement,
|
type_constraints::TypeRequirement,
|
||||||
AudioSegment, SyncMarker, _AudioSegment,
|
AudioSegment, SyncMarker, _AudioSegment,
|
||||||
};
|
};
|
||||||
@@ -132,27 +132,6 @@ impl NodeLogic for TimerNodeLogic {
|
|||||||
output.len()
|
output.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Macro helper pour envoyer à tous les enfants
|
|
||||||
macro_rules! send_to_children {
|
|
||||||
($segment:expr) => {
|
|
||||||
for (idx, tx) in output.iter().enumerate() {
|
|
||||||
let send_start = Instant::now();
|
|
||||||
tx.send($segment.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
|
||||||
let send_duration = send_start.elapsed();
|
|
||||||
if send_duration.as_millis() >= 50 {
|
|
||||||
tracing::debug!(
|
|
||||||
"TimerNode: send to child {} blocked for {:.3}s (segment ts={:.3}s)",
|
|
||||||
idx,
|
|
||||||
send_duration.as_secs_f64(),
|
|
||||||
$segment.timestamp_sec
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let segment = tokio::select! {
|
let segment = tokio::select! {
|
||||||
_ = stop_token.cancelled() => {
|
_ = stop_token.cancelled() => {
|
||||||
@@ -184,7 +163,23 @@ impl NodeLogic for TimerNodeLogic {
|
|||||||
// Autres markers: passthrough transparent
|
// Autres markers: passthrough transparent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
send_to_children!(segment);
|
let segment_ts = segment.timestamp_sec;
|
||||||
|
send_to_children_with_timing(
|
||||||
|
std::any::type_name::<Self>(),
|
||||||
|
&output,
|
||||||
|
segment.clone(),
|
||||||
|
|idx, send_duration, _capacity_before| {
|
||||||
|
if send_duration.as_millis() >= 50 {
|
||||||
|
tracing::debug!(
|
||||||
|
"TimerNode: send to child {} blocked for {:.3}s (segment ts={:.3}s)",
|
||||||
|
idx,
|
||||||
|
send_duration.as_secs_f64(),
|
||||||
|
segment_ts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
_AudioSegment::Chunk(_) => {
|
_AudioSegment::Chunk(_) => {
|
||||||
@@ -263,7 +258,23 @@ impl NodeLogic for TimerNodeLogic {
|
|||||||
tracing::warn!("TimerNodeLogic: NO TIMER SET - passthrough without pacing! (ts={:.3}s)", segment.timestamp_sec);
|
tracing::warn!("TimerNodeLogic: NO TIMER SET - passthrough without pacing! (ts={:.3}s)", segment.timestamp_sec);
|
||||||
}
|
}
|
||||||
|
|
||||||
send_to_children!(segment);
|
let segment_ts = segment.timestamp_sec;
|
||||||
|
send_to_children_with_timing(
|
||||||
|
std::any::type_name::<Self>(),
|
||||||
|
&output,
|
||||||
|
segment.clone(),
|
||||||
|
|idx, send_duration, _capacity_before| {
|
||||||
|
if send_duration.as_millis() >= 50 {
|
||||||
|
tracing::debug!(
|
||||||
|
"TimerNode: send to child {} blocked for {:.3}s (segment ts={:.3}s)",
|
||||||
|
idx,
|
||||||
|
send_duration.as_secs_f64(),
|
||||||
|
segment_ts
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,10 @@
|
|||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use crate::{nodes::AudioError, AudioSegment};
|
use crate::{nodes::AudioError, AudioSegment};
|
||||||
use std::sync::Arc;
|
use once_cell::sync::Lazy;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
@@ -304,6 +307,83 @@ pub trait NodeLogic: Send + 'static {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Envoie un segment à l'ensemble des enfants d'un nœud.
|
||||||
|
///
|
||||||
|
/// Cette fonction gère la logique de clonage d'`Arc<AudioSegment>` et la
|
||||||
|
/// conversion de l'erreur `mpsc::error::SendError` en `AudioError::ChildDied`.
|
||||||
|
static FIRST_AUDIO_CHUNK_TRACKER: Lazy<Mutex<HashSet<usize>>> =
|
||||||
|
Lazy::new(|| Mutex::new(HashSet::new()));
|
||||||
|
|
||||||
|
const FIRST_CHUNK_EPSILON: f64 = 1e-6;
|
||||||
|
|
||||||
|
fn record_first_audio_chunk_timestamp(
|
||||||
|
node_name: &'static str,
|
||||||
|
outputs: &[mpsc::Sender<Arc<AudioSegment>>],
|
||||||
|
segment: &Arc<AudioSegment>,
|
||||||
|
) {
|
||||||
|
if outputs.is_empty() || !segment.is_audio_chunk() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tracker = FIRST_AUDIO_CHUNK_TRACKER
|
||||||
|
.lock()
|
||||||
|
.expect("invariant tracker mutex poisoned");
|
||||||
|
let key = outputs.as_ptr() as usize;
|
||||||
|
if tracker.contains(&key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if segment.timestamp_sec.abs() > FIRST_CHUNK_EPSILON {
|
||||||
|
tracing::warn!(
|
||||||
|
"First audio chunk emitted by {node_name} started at {:.6}s (order={}), expected 0s",
|
||||||
|
segment.timestamp_sec,
|
||||||
|
segment.order,
|
||||||
|
node_name = node_name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tracker.insert(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_to_children(
|
||||||
|
node_name: &'static str,
|
||||||
|
outputs: &[mpsc::Sender<Arc<AudioSegment>>],
|
||||||
|
segment: Arc<AudioSegment>,
|
||||||
|
) -> Result<(), AudioError> {
|
||||||
|
record_first_audio_chunk_timestamp(node_name, outputs, &segment);
|
||||||
|
for tx in outputs {
|
||||||
|
tx.send(segment.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|_| AudioError::ChildDied)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Variante de [`send_to_children`] qui expose le temps passé à envoyer à chaque enfant.
|
||||||
|
///
|
||||||
|
/// Utile pour les nœuds qui souhaitent instrumenter les blocages éventuels lors
|
||||||
|
/// de l'envoi (ex: TimerNode).
|
||||||
|
pub async fn send_to_children_with_timing<F>(
|
||||||
|
node_name: &'static str,
|
||||||
|
outputs: &[mpsc::Sender<Arc<AudioSegment>>],
|
||||||
|
segment: Arc<AudioSegment>,
|
||||||
|
mut inspector: F,
|
||||||
|
) -> Result<(), AudioError>
|
||||||
|
where
|
||||||
|
F: FnMut(usize, Duration, usize),
|
||||||
|
{
|
||||||
|
record_first_audio_chunk_timestamp(node_name, outputs, &segment);
|
||||||
|
for (idx, tx) in outputs.iter().enumerate() {
|
||||||
|
let capacity_before = tx.capacity();
|
||||||
|
let send_start = Instant::now();
|
||||||
|
tx.send(segment.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|_| AudioError::ChildDied)?;
|
||||||
|
inspector(idx, send_start.elapsed(), capacity_before);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle pour contrôler un pipeline en cours d'exécution
|
/// Handle pour contrôler un pipeline en cours d'exécution
|
||||||
///
|
///
|
||||||
/// Retourné par la méthode `start()`, ce handle permet de :
|
/// Retourné par la méthode `start()`, ce handle permet de :
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use pmoaudio::{
|
use pmoaudio::{
|
||||||
nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
|
nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS},
|
||||||
pipeline::{Node, NodeLogic},
|
pipeline::{send_to_children, send_to_children_with_timing, Node, NodeLogic},
|
||||||
type_constraints::TypeRequirement,
|
type_constraints::TypeRequirement,
|
||||||
AudioPipelineNode, AudioSegment, SyncMarker, I24,
|
AudioPipelineNode, AudioSegment, SyncMarker, I24,
|
||||||
};
|
};
|
||||||
@@ -395,45 +395,42 @@ impl RadioParadiseStreamSourceLogic {
|
|||||||
output: &[mpsc::Sender<Arc<AudioSegment>>],
|
output: &[mpsc::Sender<Arc<AudioSegment>>],
|
||||||
segment: Arc<AudioSegment>,
|
segment: Arc<AudioSegment>,
|
||||||
) -> Result<(), AudioError> {
|
) -> Result<(), AudioError> {
|
||||||
self.stats.record_segment_received(segment.timestamp_sec);
|
let segment_ts = segment.timestamp_sec;
|
||||||
|
self.stats.record_segment_received(segment_ts);
|
||||||
|
|
||||||
for (i, tx) in output.iter().enumerate() {
|
let segment_bytes = match &segment.segment {
|
||||||
let capacity_before = tx.capacity();
|
pmoaudio::_AudioSegment::Chunk(chunk) => chunk.len() * 2 * 4,
|
||||||
tracing::trace!(
|
_ => 0,
|
||||||
"send_to_children: Sending to child {} (channel capacity={}, timestamp={:.3}s)",
|
};
|
||||||
i,
|
|
||||||
capacity_before,
|
|
||||||
segment.timestamp_sec
|
|
||||||
);
|
|
||||||
|
|
||||||
let send_start = std::time::Instant::now();
|
send_to_children_with_timing(
|
||||||
tx.send(segment.clone())
|
std::any::type_name::<Self>(),
|
||||||
.await
|
output,
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
segment,
|
||||||
let send_duration = send_start.elapsed();
|
|i, send_duration, capacity_before| {
|
||||||
|
|
||||||
if send_duration.as_millis() > 10 {
|
|
||||||
let duration_ms = send_duration.as_millis() as u64;
|
|
||||||
self.stats.record_backpressure(duration_ms);
|
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"send_to_children: Send to child {} BLOCKED for {:.3}s (channel capacity before send={}, timestamp={:.3}s)",
|
"send_to_children: Sending to child {} (channel capacity={}, timestamp={:.3}s)",
|
||||||
i,
|
i,
|
||||||
send_duration.as_secs_f64(),
|
|
||||||
capacity_before,
|
capacity_before,
|
||||||
segment.timestamp_sec
|
segment_ts
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// Estimer la taille du segment pour les stats (frames * 2 channels * bytes_per_sample)
|
if send_duration.as_millis() > 10 {
|
||||||
let segment_bytes = match &segment.segment {
|
let duration_ms = send_duration.as_millis() as u64;
|
||||||
pmoaudio::_AudioSegment::Chunk(chunk) => {
|
self.stats.record_backpressure(duration_ms);
|
||||||
// Approximation: frames * 2 (stereo) * 4 bytes (i32/f32)
|
tracing::trace!(
|
||||||
chunk.len() * 2 * 4
|
"send_to_children: Send to child {} BLOCKED for {:.3}s (channel capacity before send={}, timestamp={:.3}s)",
|
||||||
|
i,
|
||||||
|
send_duration.as_secs_f64(),
|
||||||
|
capacity_before,
|
||||||
|
segment_ts
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_ => 0,
|
|
||||||
};
|
self.stats.record_segment_sent(segment_bytes);
|
||||||
self.stats.record_segment_sent(segment_bytes);
|
},
|
||||||
}
|
)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -705,11 +702,7 @@ impl NodeLogic for RadioParadiseStreamSourceLogic {
|
|||||||
output.len()
|
output.len()
|
||||||
);
|
);
|
||||||
let eos = AudioSegment::new_end_of_stream(order, last_timestamp);
|
let eos = AudioSegment::new_end_of_stream(order, last_timestamp);
|
||||||
for tx in &output {
|
send_to_children(std::any::type_name::<Self>(), &output, eos).await?;
|
||||||
tx.send(eos.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|_| AudioError::ChildDied)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMPORTANT: Attendre que tous les channels soient fermés par les enfants
|
// IMPORTANT: Attendre que tous les channels soient fermés par les enfants
|
||||||
// Cela garantit que tous les chunks (y compris ceux en attente dans les buffers MPSC)
|
// Cela garantit que tous les chunks (y compris ceux en attente dans les buffers MPSC)
|
||||||
|
|||||||
Reference in New Issue
Block a user