Files
pmomusic/pmoaudio/src/nodes/resampling_node.rs

578 lines
20 KiB
Rust
Raw Normal View History

feat: Add PlaylistSource and ResamplingNode for playlist playback This commit implements a new audio source that reads from pmoplaylist and streams tracks continuously, along with a resampling node to normalize sample rates. ## New Components ### PlaylistSource (pmoaudio-ext) - New source in pmoaudio-ext/src/sources/playlist_source.rs - Reads from pmoplaylist ReadHandle - Decodes tracks from audio cache (pmoaudiocache) - Emits PCM with heterogeneous sample_rate and bit_depth - Polls playlist when empty (configurable interval, default 100ms) - Emits TrackBoundary markers between tracks - Graceful shutdown with EndOfStream on stop - Gated behind 'playlist' feature flag **Design Philosophy:** - Keeps each node simple (single responsibility) - Emits raw PCM without format normalization - Pipeline designer chooses how to handle heterogeneity - Ideal for Radio Paradise (homogeneous streams) - Requires ResamplingNode + ToI24Node for mixed playlists ### ResamplingNode (pmoaudio) - Generic resampling node in pmoaudio/src/nodes/resampling_node.rs - Normalizes variable sample rates to a target rate - Uses libsoxr for high-quality resampling - Automatically detects sample rate changes - Recreates resampler as needed - Preserves chunk type (I16/I24/I32/F32/F64) - Quality adapts to bit depth (Medium/High/Very High) ## Architecture PlaylistSource is placed in pmoaudio-ext to avoid circular dependencies: - pmoaudio-ext depends on: pmoaudio, pmoplaylist, pmoaudiocache - No reverse dependencies = clean dependency graph ## Configuration ### pmoaudio-ext/Cargo.toml - Updated 'playlist' feature to include pmoaudiocache, pmocache, pmoflac - Added sources module export ### pmoaudio - Added resampling_node module - Public export: ResamplingNode ## System Requirements ⚠️ **IMPORTANT**: libsoxr-dev must be installed for compilation See INSTALL_NOTES.md for installation instructions per platform. ## Usage Example ```rust // Radio Paradise (homogeneous 44.1kHz/16bit) let mut source = PlaylistSource::new(playlist, cache); let to_i24 = ToI24Node::new(); source.register(Box::new(to_i24)); // Mixed playlist (needs normalization) let mut source = PlaylistSource::new(playlist, cache); let mut resampler = ResamplingNode::new(48000); // Force 48kHz let to_i24 = ToI24Node::new(); source.register(Box::new(resampler)); resampler.register(Box::new(to_i24)); ``` ## Files Changed - pmoaudio-ext/Cargo.toml: Update playlist feature - pmoaudio-ext/src/lib.rs: Add sources module - pmoaudio-ext/src/sources/mod.rs: New sources module - pmoaudio-ext/src/sources/playlist_source.rs: New PlaylistSource (580 lines) - pmoaudio/src/nodes/resampling_node.rs: New ResamplingNode (350 lines) - pmoaudio/src/nodes/mod.rs: Register resampling_node - pmoaudio/src/lib.rs: Export ResamplingNode - INSTALL_NOTES.md: System requirements documentation ## Future Work - GapInsertionNode (inserts silence between tracks) - CrossfadeNode (fade-in/fade-out mixing) - Examples (deferred until implementation validated)
2025-11-05 13:44:24 +00:00
//! ResamplingNode - Node de resampling pour normaliser le sample rate
//!
//! Ce node prend en entrée des chunks audio avec des sample rates variables
//! et les resample vers un sample rate cible fixe.
//!
//! # Usage
//!
//! ```rust,no_run
//! use pmoaudio::{ResamplingNode, FileSource};
//!
//! let mut source = FileSource::new("audio.flac");
//! let mut resampler = ResamplingNode::new(48000); // Force 48kHz
//! source.register(Box::new(resampler));
//! ```
//!
//! # Comportement
//!
//! - Détecte automatiquement les changements de sample rate
//! - Recrée le resampler quand nécessaire
//! - Passe les chunks directement si déjà au bon sample rate
//! - Préserve les sync markers (TrackBoundary, etc.)
//!
//! # Performance
//!
//! Le resampling est effectué via libsoxr (très haute qualité).
//! La qualité est adaptée selon la profondeur de bits :
//! - 8-bit : Medium quality
//! - 16-bit : High quality
//! - 24-bit/32-bit : Very high quality
use crate::{
dsp::resampling::{build_resampler, resampling, Resampler},
nodes::{AudioError, TypedAudioNode},
pipeline::{AudioPipelineNode, Node, NodeLogic},
type_constraints::TypeRequirement,
AudioChunk, AudioChunkData, AudioSegment, BitDepth, I24,
};
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tracing;
// ═══════════════════════════════════════════════════════════════════════════
// ResamplingLogic - Logique pure de resampling
// ═══════════════════════════════════════════════════════════════════════════
/// Logique pure de resampling
///
/// Maintient un resampler et le met à jour selon les changements de sample rate.
pub struct ResamplingLogic {
target_sample_rate: u32,
current_resampler: Option<ResamplerState>,
}
struct ResamplerState {
source_hz: u32,
resampler: Resampler,
}
impl ResamplingLogic {
pub fn new(target_sample_rate: u32) -> Self {
Self {
target_sample_rate,
current_resampler: None,
}
}
/// Resample un chunk audio vers le sample rate cible
fn resample_chunk(&mut self, chunk: &AudioChunk) -> Result<AudioChunk, AudioError> {
let source_sr = chunk.sample_rate();
let bit_depth = match chunk {
AudioChunk::I16(_) => BitDepth::B16,
AudioChunk::I24(_) => BitDepth::B24,
AudioChunk::I32(_) => BitDepth::B32,
AudioChunk::F32(_) => BitDepth::B32, // Traiter comme 32-bit
AudioChunk::F64(_) => BitDepth::B32, // Traiter comme 32-bit
};
feat: Add PlaylistSource and ResamplingNode for playlist playback This commit implements a new audio source that reads from pmoplaylist and streams tracks continuously, along with a resampling node to normalize sample rates. ## New Components ### PlaylistSource (pmoaudio-ext) - New source in pmoaudio-ext/src/sources/playlist_source.rs - Reads from pmoplaylist ReadHandle - Decodes tracks from audio cache (pmoaudiocache) - Emits PCM with heterogeneous sample_rate and bit_depth - Polls playlist when empty (configurable interval, default 100ms) - Emits TrackBoundary markers between tracks - Graceful shutdown with EndOfStream on stop - Gated behind 'playlist' feature flag **Design Philosophy:** - Keeps each node simple (single responsibility) - Emits raw PCM without format normalization - Pipeline designer chooses how to handle heterogeneity - Ideal for Radio Paradise (homogeneous streams) - Requires ResamplingNode + ToI24Node for mixed playlists ### ResamplingNode (pmoaudio) - Generic resampling node in pmoaudio/src/nodes/resampling_node.rs - Normalizes variable sample rates to a target rate - Uses libsoxr for high-quality resampling - Automatically detects sample rate changes - Recreates resampler as needed - Preserves chunk type (I16/I24/I32/F32/F64) - Quality adapts to bit depth (Medium/High/Very High) ## Architecture PlaylistSource is placed in pmoaudio-ext to avoid circular dependencies: - pmoaudio-ext depends on: pmoaudio, pmoplaylist, pmoaudiocache - No reverse dependencies = clean dependency graph ## Configuration ### pmoaudio-ext/Cargo.toml - Updated 'playlist' feature to include pmoaudiocache, pmocache, pmoflac - Added sources module export ### pmoaudio - Added resampling_node module - Public export: ResamplingNode ## System Requirements ⚠️ **IMPORTANT**: libsoxr-dev must be installed for compilation See INSTALL_NOTES.md for installation instructions per platform. ## Usage Example ```rust // Radio Paradise (homogeneous 44.1kHz/16bit) let mut source = PlaylistSource::new(playlist, cache); let to_i24 = ToI24Node::new(); source.register(Box::new(to_i24)); // Mixed playlist (needs normalization) let mut source = PlaylistSource::new(playlist, cache); let mut resampler = ResamplingNode::new(48000); // Force 48kHz let to_i24 = ToI24Node::new(); source.register(Box::new(resampler)); resampler.register(Box::new(to_i24)); ``` ## Files Changed - pmoaudio-ext/Cargo.toml: Update playlist feature - pmoaudio-ext/src/lib.rs: Add sources module - pmoaudio-ext/src/sources/mod.rs: New sources module - pmoaudio-ext/src/sources/playlist_source.rs: New PlaylistSource (580 lines) - pmoaudio/src/nodes/resampling_node.rs: New ResamplingNode (350 lines) - pmoaudio/src/nodes/mod.rs: Register resampling_node - pmoaudio/src/lib.rs: Export ResamplingNode - INSTALL_NOTES.md: System requirements documentation ## Future Work - GapInsertionNode (inserts silence between tracks) - CrossfadeNode (fade-in/fade-out mixing) - Examples (deferred until implementation validated)
2025-11-05 13:44:24 +00:00
// Si déjà au bon sample rate, retourner tel quel
if source_sr == self.target_sample_rate {
return Ok(chunk.clone());
}
// Vérifier si on doit recréer le resampler
let need_new_resampler = match &self.current_resampler {
None => true,
Some(state) => state.source_hz != source_sr,
};
if need_new_resampler {
tracing::debug!(
"ResamplingLogic: creating resampler {}Hz → {}Hz (bit_depth={:?})",
source_sr,
self.target_sample_rate,
bit_depth
);
let resampler = build_resampler(source_sr, self.target_sample_rate, bit_depth)
.map_err(|e| AudioError::ProcessingError(format!("Resampler init failed: {}", e)))?;
self.current_resampler = Some(ResamplerState {
source_hz: source_sr,
resampler,
});
}
let state = self.current_resampler.as_mut().unwrap();
// Extraire les canaux L/R en i32
let (left, right) = extract_channels_i32(chunk)?;
// Appliquer le resampling
let (resampled_left, resampled_right) = resampling(&left, &right, &mut state.resampler);
// Recréer le chunk avec le nouveau sample rate
reconstruct_chunk(chunk, resampled_left, resampled_right, self.target_sample_rate)
}
}
#[async_trait::async_trait]
impl NodeLogic for ResamplingLogic {
async fn process(
&mut self,
input: Option<mpsc::Receiver<Arc<AudioSegment>>>,
output: Vec<mpsc::Sender<Arc<AudioSegment>>>,
stop_token: CancellationToken,
) -> Result<(), AudioError> {
let mut rx = input.expect("ResamplingNode must have input");
tracing::debug!(
"ResamplingLogic::process started, target={}Hz, {} children",
self.target_sample_rate,
output.len()
);
loop {
let segment = tokio::select! {
_ = stop_token.cancelled() => {
tracing::debug!("ResamplingLogic cancelled");
break;
}
result = rx.recv() => {
match result {
Some(seg) => seg,
None => {
tracing::debug!("ResamplingLogic received EOF");
break;
}
}
}
};
// Resample 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 resampled_chunk = self.resample_chunk(chunk)?;
Arc::new(AudioSegment {
order: segment.order,
timestamp_sec: segment.timestamp_sec,
segment: crate::_AudioSegment::Chunk(Arc::new(resampled_chunk)),
})
} else {
segment
}
} else {
segment
};
// Envoyer à tous les enfants
for tx in &output {
tx.send(output_segment.clone())
.await
.map_err(|_| AudioError::ChildDied)?;
}
}
Ok(())
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Helper Functions
// ═══════════════════════════════════════════════════════════════════════════
/// Extrait les canaux L/R d'un AudioChunk en i32
fn extract_channels_i32(chunk: &AudioChunk) -> Result<(Vec<i32>, Vec<i32>), AudioError> {
match chunk {
AudioChunk::I16(data) => {
let frames = data.get_frames();
let left = frames.iter().map(|frame| frame[0] as i32).collect();
let right = frames.iter().map(|frame| frame[1] as i32).collect();
feat: Add PlaylistSource and ResamplingNode for playlist playback This commit implements a new audio source that reads from pmoplaylist and streams tracks continuously, along with a resampling node to normalize sample rates. ## New Components ### PlaylistSource (pmoaudio-ext) - New source in pmoaudio-ext/src/sources/playlist_source.rs - Reads from pmoplaylist ReadHandle - Decodes tracks from audio cache (pmoaudiocache) - Emits PCM with heterogeneous sample_rate and bit_depth - Polls playlist when empty (configurable interval, default 100ms) - Emits TrackBoundary markers between tracks - Graceful shutdown with EndOfStream on stop - Gated behind 'playlist' feature flag **Design Philosophy:** - Keeps each node simple (single responsibility) - Emits raw PCM without format normalization - Pipeline designer chooses how to handle heterogeneity - Ideal for Radio Paradise (homogeneous streams) - Requires ResamplingNode + ToI24Node for mixed playlists ### ResamplingNode (pmoaudio) - Generic resampling node in pmoaudio/src/nodes/resampling_node.rs - Normalizes variable sample rates to a target rate - Uses libsoxr for high-quality resampling - Automatically detects sample rate changes - Recreates resampler as needed - Preserves chunk type (I16/I24/I32/F32/F64) - Quality adapts to bit depth (Medium/High/Very High) ## Architecture PlaylistSource is placed in pmoaudio-ext to avoid circular dependencies: - pmoaudio-ext depends on: pmoaudio, pmoplaylist, pmoaudiocache - No reverse dependencies = clean dependency graph ## Configuration ### pmoaudio-ext/Cargo.toml - Updated 'playlist' feature to include pmoaudiocache, pmocache, pmoflac - Added sources module export ### pmoaudio - Added resampling_node module - Public export: ResamplingNode ## System Requirements ⚠️ **IMPORTANT**: libsoxr-dev must be installed for compilation See INSTALL_NOTES.md for installation instructions per platform. ## Usage Example ```rust // Radio Paradise (homogeneous 44.1kHz/16bit) let mut source = PlaylistSource::new(playlist, cache); let to_i24 = ToI24Node::new(); source.register(Box::new(to_i24)); // Mixed playlist (needs normalization) let mut source = PlaylistSource::new(playlist, cache); let mut resampler = ResamplingNode::new(48000); // Force 48kHz let to_i24 = ToI24Node::new(); source.register(Box::new(resampler)); resampler.register(Box::new(to_i24)); ``` ## Files Changed - pmoaudio-ext/Cargo.toml: Update playlist feature - pmoaudio-ext/src/lib.rs: Add sources module - pmoaudio-ext/src/sources/mod.rs: New sources module - pmoaudio-ext/src/sources/playlist_source.rs: New PlaylistSource (580 lines) - pmoaudio/src/nodes/resampling_node.rs: New ResamplingNode (350 lines) - pmoaudio/src/nodes/mod.rs: Register resampling_node - pmoaudio/src/lib.rs: Export ResamplingNode - INSTALL_NOTES.md: System requirements documentation ## Future Work - GapInsertionNode (inserts silence between tracks) - CrossfadeNode (fade-in/fade-out mixing) - Examples (deferred until implementation validated)
2025-11-05 13:44:24 +00:00
Ok((left, right))
}
AudioChunk::I24(data) => {
let frames = data.get_frames();
let left = frames.iter().map(|frame| frame[0].as_i32()).collect();
let right = frames.iter().map(|frame| frame[1].as_i32()).collect();
feat: Add PlaylistSource and ResamplingNode for playlist playback This commit implements a new audio source that reads from pmoplaylist and streams tracks continuously, along with a resampling node to normalize sample rates. ## New Components ### PlaylistSource (pmoaudio-ext) - New source in pmoaudio-ext/src/sources/playlist_source.rs - Reads from pmoplaylist ReadHandle - Decodes tracks from audio cache (pmoaudiocache) - Emits PCM with heterogeneous sample_rate and bit_depth - Polls playlist when empty (configurable interval, default 100ms) - Emits TrackBoundary markers between tracks - Graceful shutdown with EndOfStream on stop - Gated behind 'playlist' feature flag **Design Philosophy:** - Keeps each node simple (single responsibility) - Emits raw PCM without format normalization - Pipeline designer chooses how to handle heterogeneity - Ideal for Radio Paradise (homogeneous streams) - Requires ResamplingNode + ToI24Node for mixed playlists ### ResamplingNode (pmoaudio) - Generic resampling node in pmoaudio/src/nodes/resampling_node.rs - Normalizes variable sample rates to a target rate - Uses libsoxr for high-quality resampling - Automatically detects sample rate changes - Recreates resampler as needed - Preserves chunk type (I16/I24/I32/F32/F64) - Quality adapts to bit depth (Medium/High/Very High) ## Architecture PlaylistSource is placed in pmoaudio-ext to avoid circular dependencies: - pmoaudio-ext depends on: pmoaudio, pmoplaylist, pmoaudiocache - No reverse dependencies = clean dependency graph ## Configuration ### pmoaudio-ext/Cargo.toml - Updated 'playlist' feature to include pmoaudiocache, pmocache, pmoflac - Added sources module export ### pmoaudio - Added resampling_node module - Public export: ResamplingNode ## System Requirements ⚠️ **IMPORTANT**: libsoxr-dev must be installed for compilation See INSTALL_NOTES.md for installation instructions per platform. ## Usage Example ```rust // Radio Paradise (homogeneous 44.1kHz/16bit) let mut source = PlaylistSource::new(playlist, cache); let to_i24 = ToI24Node::new(); source.register(Box::new(to_i24)); // Mixed playlist (needs normalization) let mut source = PlaylistSource::new(playlist, cache); let mut resampler = ResamplingNode::new(48000); // Force 48kHz let to_i24 = ToI24Node::new(); source.register(Box::new(resampler)); resampler.register(Box::new(to_i24)); ``` ## Files Changed - pmoaudio-ext/Cargo.toml: Update playlist feature - pmoaudio-ext/src/lib.rs: Add sources module - pmoaudio-ext/src/sources/mod.rs: New sources module - pmoaudio-ext/src/sources/playlist_source.rs: New PlaylistSource (580 lines) - pmoaudio/src/nodes/resampling_node.rs: New ResamplingNode (350 lines) - pmoaudio/src/nodes/mod.rs: Register resampling_node - pmoaudio/src/lib.rs: Export ResamplingNode - INSTALL_NOTES.md: System requirements documentation ## Future Work - GapInsertionNode (inserts silence between tracks) - CrossfadeNode (fade-in/fade-out mixing) - Examples (deferred until implementation validated)
2025-11-05 13:44:24 +00:00
Ok((left, right))
}
AudioChunk::I32(data) => {
let frames = data.get_frames();
let left = frames.iter().map(|frame| frame[0]).collect();
let right = frames.iter().map(|frame| frame[1]).collect();
feat: Add PlaylistSource and ResamplingNode for playlist playback This commit implements a new audio source that reads from pmoplaylist and streams tracks continuously, along with a resampling node to normalize sample rates. ## New Components ### PlaylistSource (pmoaudio-ext) - New source in pmoaudio-ext/src/sources/playlist_source.rs - Reads from pmoplaylist ReadHandle - Decodes tracks from audio cache (pmoaudiocache) - Emits PCM with heterogeneous sample_rate and bit_depth - Polls playlist when empty (configurable interval, default 100ms) - Emits TrackBoundary markers between tracks - Graceful shutdown with EndOfStream on stop - Gated behind 'playlist' feature flag **Design Philosophy:** - Keeps each node simple (single responsibility) - Emits raw PCM without format normalization - Pipeline designer chooses how to handle heterogeneity - Ideal for Radio Paradise (homogeneous streams) - Requires ResamplingNode + ToI24Node for mixed playlists ### ResamplingNode (pmoaudio) - Generic resampling node in pmoaudio/src/nodes/resampling_node.rs - Normalizes variable sample rates to a target rate - Uses libsoxr for high-quality resampling - Automatically detects sample rate changes - Recreates resampler as needed - Preserves chunk type (I16/I24/I32/F32/F64) - Quality adapts to bit depth (Medium/High/Very High) ## Architecture PlaylistSource is placed in pmoaudio-ext to avoid circular dependencies: - pmoaudio-ext depends on: pmoaudio, pmoplaylist, pmoaudiocache - No reverse dependencies = clean dependency graph ## Configuration ### pmoaudio-ext/Cargo.toml - Updated 'playlist' feature to include pmoaudiocache, pmocache, pmoflac - Added sources module export ### pmoaudio - Added resampling_node module - Public export: ResamplingNode ## System Requirements ⚠️ **IMPORTANT**: libsoxr-dev must be installed for compilation See INSTALL_NOTES.md for installation instructions per platform. ## Usage Example ```rust // Radio Paradise (homogeneous 44.1kHz/16bit) let mut source = PlaylistSource::new(playlist, cache); let to_i24 = ToI24Node::new(); source.register(Box::new(to_i24)); // Mixed playlist (needs normalization) let mut source = PlaylistSource::new(playlist, cache); let mut resampler = ResamplingNode::new(48000); // Force 48kHz let to_i24 = ToI24Node::new(); source.register(Box::new(resampler)); resampler.register(Box::new(to_i24)); ``` ## Files Changed - pmoaudio-ext/Cargo.toml: Update playlist feature - pmoaudio-ext/src/lib.rs: Add sources module - pmoaudio-ext/src/sources/mod.rs: New sources module - pmoaudio-ext/src/sources/playlist_source.rs: New PlaylistSource (580 lines) - pmoaudio/src/nodes/resampling_node.rs: New ResamplingNode (350 lines) - pmoaudio/src/nodes/mod.rs: Register resampling_node - pmoaudio/src/lib.rs: Export ResamplingNode - INSTALL_NOTES.md: System requirements documentation ## Future Work - GapInsertionNode (inserts silence between tracks) - CrossfadeNode (fade-in/fade-out mixing) - Examples (deferred until implementation validated)
2025-11-05 13:44:24 +00:00
Ok((left, right))
}
AudioChunk::F32(data) => {
let frames = data.get_frames();
feat: Add PlaylistSource and ResamplingNode for playlist playback This commit implements a new audio source that reads from pmoplaylist and streams tracks continuously, along with a resampling node to normalize sample rates. ## New Components ### PlaylistSource (pmoaudio-ext) - New source in pmoaudio-ext/src/sources/playlist_source.rs - Reads from pmoplaylist ReadHandle - Decodes tracks from audio cache (pmoaudiocache) - Emits PCM with heterogeneous sample_rate and bit_depth - Polls playlist when empty (configurable interval, default 100ms) - Emits TrackBoundary markers between tracks - Graceful shutdown with EndOfStream on stop - Gated behind 'playlist' feature flag **Design Philosophy:** - Keeps each node simple (single responsibility) - Emits raw PCM without format normalization - Pipeline designer chooses how to handle heterogeneity - Ideal for Radio Paradise (homogeneous streams) - Requires ResamplingNode + ToI24Node for mixed playlists ### ResamplingNode (pmoaudio) - Generic resampling node in pmoaudio/src/nodes/resampling_node.rs - Normalizes variable sample rates to a target rate - Uses libsoxr for high-quality resampling - Automatically detects sample rate changes - Recreates resampler as needed - Preserves chunk type (I16/I24/I32/F32/F64) - Quality adapts to bit depth (Medium/High/Very High) ## Architecture PlaylistSource is placed in pmoaudio-ext to avoid circular dependencies: - pmoaudio-ext depends on: pmoaudio, pmoplaylist, pmoaudiocache - No reverse dependencies = clean dependency graph ## Configuration ### pmoaudio-ext/Cargo.toml - Updated 'playlist' feature to include pmoaudiocache, pmocache, pmoflac - Added sources module export ### pmoaudio - Added resampling_node module - Public export: ResamplingNode ## System Requirements ⚠️ **IMPORTANT**: libsoxr-dev must be installed for compilation See INSTALL_NOTES.md for installation instructions per platform. ## Usage Example ```rust // Radio Paradise (homogeneous 44.1kHz/16bit) let mut source = PlaylistSource::new(playlist, cache); let to_i24 = ToI24Node::new(); source.register(Box::new(to_i24)); // Mixed playlist (needs normalization) let mut source = PlaylistSource::new(playlist, cache); let mut resampler = ResamplingNode::new(48000); // Force 48kHz let to_i24 = ToI24Node::new(); source.register(Box::new(resampler)); resampler.register(Box::new(to_i24)); ``` ## Files Changed - pmoaudio-ext/Cargo.toml: Update playlist feature - pmoaudio-ext/src/lib.rs: Add sources module - pmoaudio-ext/src/sources/mod.rs: New sources module - pmoaudio-ext/src/sources/playlist_source.rs: New PlaylistSource (580 lines) - pmoaudio/src/nodes/resampling_node.rs: New ResamplingNode (350 lines) - pmoaudio/src/nodes/mod.rs: Register resampling_node - pmoaudio/src/lib.rs: Export ResamplingNode - INSTALL_NOTES.md: System requirements documentation ## Future Work - GapInsertionNode (inserts silence between tracks) - CrossfadeNode (fade-in/fade-out mixing) - Examples (deferred until implementation validated)
2025-11-05 13:44:24 +00:00
// Convertir f32 → i32 (dénormaliser)
let left = frames
feat: Add PlaylistSource and ResamplingNode for playlist playback This commit implements a new audio source that reads from pmoplaylist and streams tracks continuously, along with a resampling node to normalize sample rates. ## New Components ### PlaylistSource (pmoaudio-ext) - New source in pmoaudio-ext/src/sources/playlist_source.rs - Reads from pmoplaylist ReadHandle - Decodes tracks from audio cache (pmoaudiocache) - Emits PCM with heterogeneous sample_rate and bit_depth - Polls playlist when empty (configurable interval, default 100ms) - Emits TrackBoundary markers between tracks - Graceful shutdown with EndOfStream on stop - Gated behind 'playlist' feature flag **Design Philosophy:** - Keeps each node simple (single responsibility) - Emits raw PCM without format normalization - Pipeline designer chooses how to handle heterogeneity - Ideal for Radio Paradise (homogeneous streams) - Requires ResamplingNode + ToI24Node for mixed playlists ### ResamplingNode (pmoaudio) - Generic resampling node in pmoaudio/src/nodes/resampling_node.rs - Normalizes variable sample rates to a target rate - Uses libsoxr for high-quality resampling - Automatically detects sample rate changes - Recreates resampler as needed - Preserves chunk type (I16/I24/I32/F32/F64) - Quality adapts to bit depth (Medium/High/Very High) ## Architecture PlaylistSource is placed in pmoaudio-ext to avoid circular dependencies: - pmoaudio-ext depends on: pmoaudio, pmoplaylist, pmoaudiocache - No reverse dependencies = clean dependency graph ## Configuration ### pmoaudio-ext/Cargo.toml - Updated 'playlist' feature to include pmoaudiocache, pmocache, pmoflac - Added sources module export ### pmoaudio - Added resampling_node module - Public export: ResamplingNode ## System Requirements ⚠️ **IMPORTANT**: libsoxr-dev must be installed for compilation See INSTALL_NOTES.md for installation instructions per platform. ## Usage Example ```rust // Radio Paradise (homogeneous 44.1kHz/16bit) let mut source = PlaylistSource::new(playlist, cache); let to_i24 = ToI24Node::new(); source.register(Box::new(to_i24)); // Mixed playlist (needs normalization) let mut source = PlaylistSource::new(playlist, cache); let mut resampler = ResamplingNode::new(48000); // Force 48kHz let to_i24 = ToI24Node::new(); source.register(Box::new(resampler)); resampler.register(Box::new(to_i24)); ``` ## Files Changed - pmoaudio-ext/Cargo.toml: Update playlist feature - pmoaudio-ext/src/lib.rs: Add sources module - pmoaudio-ext/src/sources/mod.rs: New sources module - pmoaudio-ext/src/sources/playlist_source.rs: New PlaylistSource (580 lines) - pmoaudio/src/nodes/resampling_node.rs: New ResamplingNode (350 lines) - pmoaudio/src/nodes/mod.rs: Register resampling_node - pmoaudio/src/lib.rs: Export ResamplingNode - INSTALL_NOTES.md: System requirements documentation ## Future Work - GapInsertionNode (inserts silence between tracks) - CrossfadeNode (fade-in/fade-out mixing) - Examples (deferred until implementation validated)
2025-11-05 13:44:24 +00:00
.iter()
.map(|frame| (frame[0] * i32::MAX as f32) as i32)
.collect();
let right = frames
feat: Add PlaylistSource and ResamplingNode for playlist playback This commit implements a new audio source that reads from pmoplaylist and streams tracks continuously, along with a resampling node to normalize sample rates. ## New Components ### PlaylistSource (pmoaudio-ext) - New source in pmoaudio-ext/src/sources/playlist_source.rs - Reads from pmoplaylist ReadHandle - Decodes tracks from audio cache (pmoaudiocache) - Emits PCM with heterogeneous sample_rate and bit_depth - Polls playlist when empty (configurable interval, default 100ms) - Emits TrackBoundary markers between tracks - Graceful shutdown with EndOfStream on stop - Gated behind 'playlist' feature flag **Design Philosophy:** - Keeps each node simple (single responsibility) - Emits raw PCM without format normalization - Pipeline designer chooses how to handle heterogeneity - Ideal for Radio Paradise (homogeneous streams) - Requires ResamplingNode + ToI24Node for mixed playlists ### ResamplingNode (pmoaudio) - Generic resampling node in pmoaudio/src/nodes/resampling_node.rs - Normalizes variable sample rates to a target rate - Uses libsoxr for high-quality resampling - Automatically detects sample rate changes - Recreates resampler as needed - Preserves chunk type (I16/I24/I32/F32/F64) - Quality adapts to bit depth (Medium/High/Very High) ## Architecture PlaylistSource is placed in pmoaudio-ext to avoid circular dependencies: - pmoaudio-ext depends on: pmoaudio, pmoplaylist, pmoaudiocache - No reverse dependencies = clean dependency graph ## Configuration ### pmoaudio-ext/Cargo.toml - Updated 'playlist' feature to include pmoaudiocache, pmocache, pmoflac - Added sources module export ### pmoaudio - Added resampling_node module - Public export: ResamplingNode ## System Requirements ⚠️ **IMPORTANT**: libsoxr-dev must be installed for compilation See INSTALL_NOTES.md for installation instructions per platform. ## Usage Example ```rust // Radio Paradise (homogeneous 44.1kHz/16bit) let mut source = PlaylistSource::new(playlist, cache); let to_i24 = ToI24Node::new(); source.register(Box::new(to_i24)); // Mixed playlist (needs normalization) let mut source = PlaylistSource::new(playlist, cache); let mut resampler = ResamplingNode::new(48000); // Force 48kHz let to_i24 = ToI24Node::new(); source.register(Box::new(resampler)); resampler.register(Box::new(to_i24)); ``` ## Files Changed - pmoaudio-ext/Cargo.toml: Update playlist feature - pmoaudio-ext/src/lib.rs: Add sources module - pmoaudio-ext/src/sources/mod.rs: New sources module - pmoaudio-ext/src/sources/playlist_source.rs: New PlaylistSource (580 lines) - pmoaudio/src/nodes/resampling_node.rs: New ResamplingNode (350 lines) - pmoaudio/src/nodes/mod.rs: Register resampling_node - pmoaudio/src/lib.rs: Export ResamplingNode - INSTALL_NOTES.md: System requirements documentation ## Future Work - GapInsertionNode (inserts silence between tracks) - CrossfadeNode (fade-in/fade-out mixing) - Examples (deferred until implementation validated)
2025-11-05 13:44:24 +00:00
.iter()
.map(|frame| (frame[1] * i32::MAX as f32) as i32)
.collect();
Ok((left, right))
}
AudioChunk::F64(data) => {
let frames = data.get_frames();
feat: Add PlaylistSource and ResamplingNode for playlist playback This commit implements a new audio source that reads from pmoplaylist and streams tracks continuously, along with a resampling node to normalize sample rates. ## New Components ### PlaylistSource (pmoaudio-ext) - New source in pmoaudio-ext/src/sources/playlist_source.rs - Reads from pmoplaylist ReadHandle - Decodes tracks from audio cache (pmoaudiocache) - Emits PCM with heterogeneous sample_rate and bit_depth - Polls playlist when empty (configurable interval, default 100ms) - Emits TrackBoundary markers between tracks - Graceful shutdown with EndOfStream on stop - Gated behind 'playlist' feature flag **Design Philosophy:** - Keeps each node simple (single responsibility) - Emits raw PCM without format normalization - Pipeline designer chooses how to handle heterogeneity - Ideal for Radio Paradise (homogeneous streams) - Requires ResamplingNode + ToI24Node for mixed playlists ### ResamplingNode (pmoaudio) - Generic resampling node in pmoaudio/src/nodes/resampling_node.rs - Normalizes variable sample rates to a target rate - Uses libsoxr for high-quality resampling - Automatically detects sample rate changes - Recreates resampler as needed - Preserves chunk type (I16/I24/I32/F32/F64) - Quality adapts to bit depth (Medium/High/Very High) ## Architecture PlaylistSource is placed in pmoaudio-ext to avoid circular dependencies: - pmoaudio-ext depends on: pmoaudio, pmoplaylist, pmoaudiocache - No reverse dependencies = clean dependency graph ## Configuration ### pmoaudio-ext/Cargo.toml - Updated 'playlist' feature to include pmoaudiocache, pmocache, pmoflac - Added sources module export ### pmoaudio - Added resampling_node module - Public export: ResamplingNode ## System Requirements ⚠️ **IMPORTANT**: libsoxr-dev must be installed for compilation See INSTALL_NOTES.md for installation instructions per platform. ## Usage Example ```rust // Radio Paradise (homogeneous 44.1kHz/16bit) let mut source = PlaylistSource::new(playlist, cache); let to_i24 = ToI24Node::new(); source.register(Box::new(to_i24)); // Mixed playlist (needs normalization) let mut source = PlaylistSource::new(playlist, cache); let mut resampler = ResamplingNode::new(48000); // Force 48kHz let to_i24 = ToI24Node::new(); source.register(Box::new(resampler)); resampler.register(Box::new(to_i24)); ``` ## Files Changed - pmoaudio-ext/Cargo.toml: Update playlist feature - pmoaudio-ext/src/lib.rs: Add sources module - pmoaudio-ext/src/sources/mod.rs: New sources module - pmoaudio-ext/src/sources/playlist_source.rs: New PlaylistSource (580 lines) - pmoaudio/src/nodes/resampling_node.rs: New ResamplingNode (350 lines) - pmoaudio/src/nodes/mod.rs: Register resampling_node - pmoaudio/src/lib.rs: Export ResamplingNode - INSTALL_NOTES.md: System requirements documentation ## Future Work - GapInsertionNode (inserts silence between tracks) - CrossfadeNode (fade-in/fade-out mixing) - Examples (deferred until implementation validated)
2025-11-05 13:44:24 +00:00
// Convertir f64 → i32 (dénormaliser)
let left = frames
feat: Add PlaylistSource and ResamplingNode for playlist playback This commit implements a new audio source that reads from pmoplaylist and streams tracks continuously, along with a resampling node to normalize sample rates. ## New Components ### PlaylistSource (pmoaudio-ext) - New source in pmoaudio-ext/src/sources/playlist_source.rs - Reads from pmoplaylist ReadHandle - Decodes tracks from audio cache (pmoaudiocache) - Emits PCM with heterogeneous sample_rate and bit_depth - Polls playlist when empty (configurable interval, default 100ms) - Emits TrackBoundary markers between tracks - Graceful shutdown with EndOfStream on stop - Gated behind 'playlist' feature flag **Design Philosophy:** - Keeps each node simple (single responsibility) - Emits raw PCM without format normalization - Pipeline designer chooses how to handle heterogeneity - Ideal for Radio Paradise (homogeneous streams) - Requires ResamplingNode + ToI24Node for mixed playlists ### ResamplingNode (pmoaudio) - Generic resampling node in pmoaudio/src/nodes/resampling_node.rs - Normalizes variable sample rates to a target rate - Uses libsoxr for high-quality resampling - Automatically detects sample rate changes - Recreates resampler as needed - Preserves chunk type (I16/I24/I32/F32/F64) - Quality adapts to bit depth (Medium/High/Very High) ## Architecture PlaylistSource is placed in pmoaudio-ext to avoid circular dependencies: - pmoaudio-ext depends on: pmoaudio, pmoplaylist, pmoaudiocache - No reverse dependencies = clean dependency graph ## Configuration ### pmoaudio-ext/Cargo.toml - Updated 'playlist' feature to include pmoaudiocache, pmocache, pmoflac - Added sources module export ### pmoaudio - Added resampling_node module - Public export: ResamplingNode ## System Requirements ⚠️ **IMPORTANT**: libsoxr-dev must be installed for compilation See INSTALL_NOTES.md for installation instructions per platform. ## Usage Example ```rust // Radio Paradise (homogeneous 44.1kHz/16bit) let mut source = PlaylistSource::new(playlist, cache); let to_i24 = ToI24Node::new(); source.register(Box::new(to_i24)); // Mixed playlist (needs normalization) let mut source = PlaylistSource::new(playlist, cache); let mut resampler = ResamplingNode::new(48000); // Force 48kHz let to_i24 = ToI24Node::new(); source.register(Box::new(resampler)); resampler.register(Box::new(to_i24)); ``` ## Files Changed - pmoaudio-ext/Cargo.toml: Update playlist feature - pmoaudio-ext/src/lib.rs: Add sources module - pmoaudio-ext/src/sources/mod.rs: New sources module - pmoaudio-ext/src/sources/playlist_source.rs: New PlaylistSource (580 lines) - pmoaudio/src/nodes/resampling_node.rs: New ResamplingNode (350 lines) - pmoaudio/src/nodes/mod.rs: Register resampling_node - pmoaudio/src/lib.rs: Export ResamplingNode - INSTALL_NOTES.md: System requirements documentation ## Future Work - GapInsertionNode (inserts silence between tracks) - CrossfadeNode (fade-in/fade-out mixing) - Examples (deferred until implementation validated)
2025-11-05 13:44:24 +00:00
.iter()
.map(|frame| (frame[0] * i32::MAX as f64) as i32)
.collect();
let right = frames
feat: Add PlaylistSource and ResamplingNode for playlist playback This commit implements a new audio source that reads from pmoplaylist and streams tracks continuously, along with a resampling node to normalize sample rates. ## New Components ### PlaylistSource (pmoaudio-ext) - New source in pmoaudio-ext/src/sources/playlist_source.rs - Reads from pmoplaylist ReadHandle - Decodes tracks from audio cache (pmoaudiocache) - Emits PCM with heterogeneous sample_rate and bit_depth - Polls playlist when empty (configurable interval, default 100ms) - Emits TrackBoundary markers between tracks - Graceful shutdown with EndOfStream on stop - Gated behind 'playlist' feature flag **Design Philosophy:** - Keeps each node simple (single responsibility) - Emits raw PCM without format normalization - Pipeline designer chooses how to handle heterogeneity - Ideal for Radio Paradise (homogeneous streams) - Requires ResamplingNode + ToI24Node for mixed playlists ### ResamplingNode (pmoaudio) - Generic resampling node in pmoaudio/src/nodes/resampling_node.rs - Normalizes variable sample rates to a target rate - Uses libsoxr for high-quality resampling - Automatically detects sample rate changes - Recreates resampler as needed - Preserves chunk type (I16/I24/I32/F32/F64) - Quality adapts to bit depth (Medium/High/Very High) ## Architecture PlaylistSource is placed in pmoaudio-ext to avoid circular dependencies: - pmoaudio-ext depends on: pmoaudio, pmoplaylist, pmoaudiocache - No reverse dependencies = clean dependency graph ## Configuration ### pmoaudio-ext/Cargo.toml - Updated 'playlist' feature to include pmoaudiocache, pmocache, pmoflac - Added sources module export ### pmoaudio - Added resampling_node module - Public export: ResamplingNode ## System Requirements ⚠️ **IMPORTANT**: libsoxr-dev must be installed for compilation See INSTALL_NOTES.md for installation instructions per platform. ## Usage Example ```rust // Radio Paradise (homogeneous 44.1kHz/16bit) let mut source = PlaylistSource::new(playlist, cache); let to_i24 = ToI24Node::new(); source.register(Box::new(to_i24)); // Mixed playlist (needs normalization) let mut source = PlaylistSource::new(playlist, cache); let mut resampler = ResamplingNode::new(48000); // Force 48kHz let to_i24 = ToI24Node::new(); source.register(Box::new(resampler)); resampler.register(Box::new(to_i24)); ``` ## Files Changed - pmoaudio-ext/Cargo.toml: Update playlist feature - pmoaudio-ext/src/lib.rs: Add sources module - pmoaudio-ext/src/sources/mod.rs: New sources module - pmoaudio-ext/src/sources/playlist_source.rs: New PlaylistSource (580 lines) - pmoaudio/src/nodes/resampling_node.rs: New ResamplingNode (350 lines) - pmoaudio/src/nodes/mod.rs: Register resampling_node - pmoaudio/src/lib.rs: Export ResamplingNode - INSTALL_NOTES.md: System requirements documentation ## Future Work - GapInsertionNode (inserts silence between tracks) - CrossfadeNode (fade-in/fade-out mixing) - Examples (deferred until implementation validated)
2025-11-05 13:44:24 +00:00
.iter()
.map(|frame| (frame[1] * i32::MAX as f64) as i32)
.collect();
Ok((left, right))
}
}
}
/// Reconstruit un AudioChunk du même type avec les canaux resamplez
fn reconstruct_chunk(
original: &AudioChunk,
left: Vec<i32>,
right: Vec<i32>,
new_sample_rate: u32,
) -> Result<AudioChunk, AudioError> {
if left.len() != right.len() {
return Err(AudioError::ProcessingError(
"Left and right channel lengths differ after resampling".into(),
));
}
let gain_db = original.gain_db();
match original {
AudioChunk::I16(_) => {
let mut stereo = Vec::with_capacity(left.len());
for i in 0..left.len() {
stereo.push([left[i] as i16, right[i] as i16]);
}
Ok(AudioChunk::I16(AudioChunkData::new(
stereo,
new_sample_rate,
gain_db,
)))
}
AudioChunk::I24(_) => {
let mut stereo = Vec::with_capacity(left.len());
for i in 0..left.len() {
let l = I24::new(left[i])
.ok_or_else(|| AudioError::ProcessingError("Invalid I24 value".into()))?;
let r = I24::new(right[i])
.ok_or_else(|| AudioError::ProcessingError("Invalid I24 value".into()))?;
stereo.push([l, r]);
}
Ok(AudioChunk::I24(AudioChunkData::new(
stereo,
new_sample_rate,
gain_db,
)))
}
AudioChunk::I32(_) => {
let mut stereo = Vec::with_capacity(left.len());
for i in 0..left.len() {
stereo.push([left[i], right[i]]);
}
Ok(AudioChunk::I32(AudioChunkData::new(
stereo,
new_sample_rate,
gain_db,
)))
}
AudioChunk::F32(_) => {
let mut stereo = Vec::with_capacity(left.len());
for i in 0..left.len() {
stereo.push([
left[i] as f32 / i32::MAX as f32,
right[i] as f32 / i32::MAX as f32,
]);
}
Ok(AudioChunk::F32(AudioChunkData::new(
stereo,
new_sample_rate,
gain_db,
)))
}
AudioChunk::F64(_) => {
let mut stereo = Vec::with_capacity(left.len());
for i in 0..left.len() {
stereo.push([
left[i] as f64 / i32::MAX as f64,
right[i] as f64 / i32::MAX as f64,
]);
}
Ok(AudioChunk::F64(AudioChunkData::new(
stereo,
new_sample_rate,
gain_db,
)))
}
}
}
// ═══════════════════════════════════════════════════════════════════════════
// WRAPPER ResamplingNode - Délègue à Node<ResamplingLogic>
// ═══════════════════════════════════════════════════════════════════════════
/// ResamplingNode - Normalise le sample rate vers une valeur cible
///
/// Ce node prend en entrée des chunks audio avec des sample rates variables
/// et les resample vers un sample rate fixe.
pub struct ResamplingNode {
inner: Node<ResamplingLogic>,
}
impl ResamplingNode {
/// Crée un nouveau node de resampling
///
/// * `target_sample_rate` - Sample rate de sortie en Hz (ex: 48000)
pub fn new(target_sample_rate: u32) -> Box<dyn AudioPipelineNode> {
Self::with_channel_size(target_sample_rate, 16)
}
/// Crée un nouveau node de resampling avec taille de canal personnalisée
///
/// * `target_sample_rate` - Sample rate de sortie en Hz
/// * `channel_size` - Taille du canal de communication
pub fn with_channel_size(
target_sample_rate: u32,
channel_size: usize,
) -> Box<dyn AudioPipelineNode> {
let logic = ResamplingLogic::new(target_sample_rate);
Box::new(Self {
inner: Node::new_with_input(logic, channel_size),
})
}
}
#[async_trait::async_trait]
impl AudioPipelineNode for ResamplingNode {
fn get_tx(&self) -> Option<mpsc::Sender<Arc<AudioSegment>>> {
self.inner.get_tx()
}
fn register(&mut self, child: Box<dyn AudioPipelineNode>) {
self.inner.register(child)
}
async fn run(
self: Box<Self>,
stop_token: CancellationToken,
) -> Result<(), AudioError> {
Box::new(self.inner).run(stop_token).await
}
}
impl TypedAudioNode for ResamplingNode {
fn input_type(&self) -> Option<TypeRequirement> {
// Accepte n'importe quel type
Some(TypeRequirement::any())
}
fn output_type(&self) -> Option<TypeRequirement> {
// Produit le même type que l'entrée (mais sample rate changé)
Some(TypeRequirement::any())
}
}
test: Add comprehensive test coverage for PlaylistSource and ResamplingNode This commit adds extensive unit and integration tests for the audio pipeline components that were previously untested. ## ResamplingNode Tests (pmoaudio/src/nodes/resampling_node.rs) - Added 7 test functions covering: - Helper function tests: extract_channels_i16/i24 - Reconstruction tests: reconstruct_chunk_i16/i24 - Logic tests: passthrough when sample rate matches - Async tests: verify sync markers pass through unchanged - Integration test: actual 44.1kHz → 48kHz resampling ## PlaylistSource Tests (pmoaudio-ext/src/sources/playlist_source.rs) - Added 10 test functions covering: - Stream validation: valid/invalid channel counts and bit depths - PCM conversion: bytes_to_segment for I16/I24/I32 formats - Mono/stereo handling: verify channel duplication - Error handling: unsupported bit depth rejection - Type safety: compilation verification ## Bug Fixes - Fixed imports: Node and NodeLogic moved from nodes to pipeline module - Fixed AudioCache import: use pmoaudiocache::Cache with alias - Fixed API calls in ResamplingNode: - BitDepth::from_audio_chunk() → match pattern - .stereo() → .get_frames() - .to_i32() → .as_i32() for I24 - .sample_rate() → .get_sample_rate() ## Documentation - Added INSTALL_LIBSOXR.md with detailed installation instructions - Documents local libsoxr installation without sudo privileges - Provides troubleshooting guide for build and test environments All tests pass successfully (17 tests total: 7 ResamplingNode + 10 PlaylistSource).
2025-11-05 14:16:12 +00:00
#[cfg(test)]
mod tests {
use super::*;
use crate::{AudioChunk, AudioChunkData, SyncMarker};
#[test]
fn test_extract_channels_i16() {
let chunk = AudioChunk::I16(AudioChunkData::new(
vec![[100, 200], [300, 400]],
48000,
0.0,
));
let (left, right) = extract_channels_i32(&chunk).unwrap();
assert_eq!(left, vec![100i32, 300i32]);
assert_eq!(right, vec![200i32, 400i32]);
}
#[test]
fn test_extract_channels_i24() {
let chunk = AudioChunk::I24(AudioChunkData::new(
vec![
[I24::new(1_000_000).unwrap(), I24::new(2_000_000).unwrap()],
[I24::new(3_000_000).unwrap(), I24::new(4_000_000).unwrap()],
],
48000,
0.0,
));
let (left, right) = extract_channels_i32(&chunk).unwrap();
assert_eq!(left, vec![1_000_000i32, 3_000_000i32]);
assert_eq!(right, vec![2_000_000i32, 4_000_000i32]);
}
#[test]
fn test_reconstruct_chunk_i16() {
let original = AudioChunk::I16(AudioChunkData::new(
vec![[100, 200]],
44100,
0.0,
));
let left = vec![100i32, 300i32];
let right = vec![200i32, 400i32];
let result = reconstruct_chunk(&original, left, right, 48000).unwrap();
if let AudioChunk::I16(data) = result {
assert_eq!(data.get_sample_rate(), 48000);
let frames = data.get_frames();
assert_eq!(frames.len(), 2);
assert_eq!(frames[0], [100i16, 200i16]);
assert_eq!(frames[1], [300i16, 400i16]);
} else {
panic!("Expected I16 chunk");
}
}
#[test]
fn test_reconstruct_chunk_i24() {
let original = AudioChunk::I24(AudioChunkData::new(
vec![[I24::new(1_000_000).unwrap(), I24::new(2_000_000).unwrap()]],
44100,
0.0,
));
let left = vec![1_000_000i32, 3_000_000i32];
let right = vec![2_000_000i32, 4_000_000i32];
let result = reconstruct_chunk(&original, left, right, 48000).unwrap();
if let AudioChunk::I24(data) = result {
assert_eq!(data.get_sample_rate(), 48000);
let frames = data.get_frames();
assert_eq!(frames.len(), 2);
assert_eq!(frames[0][0].as_i32(), 1_000_000);
assert_eq!(frames[0][1].as_i32(), 2_000_000);
} else {
panic!("Expected I24 chunk");
}
}
#[test]
fn test_resample_chunk_no_change_if_same_rate() {
let mut logic = ResamplingLogic::new(48000);
let chunk = AudioChunk::I16(AudioChunkData::new(
vec![[100, 200], [300, 400]],
48000, // Déjà à 48kHz
0.0,
));
let result = logic.resample_chunk(&chunk).unwrap();
// Doit retourner le même chunk sans resampling
if let AudioChunk::I16(data) = result {
assert_eq!(data.get_sample_rate(), 48000);
assert_eq!(data.get_frames().len(), 2);
} else {
panic!("Expected I16 chunk");
}
}
#[tokio::test]
async fn test_resampling_logic_passes_sync_markers() {
let mut logic = ResamplingLogic::new(48000);
let (input_tx, input_rx) = mpsc::channel(10);
let (output_tx, mut output_rx) = mpsc::channel(10);
let stop_token = CancellationToken::new();
// Créer un TrackBoundary
let metadata = Arc::new(tokio::sync::RwLock::new(
pmometadata::MemoryTrackMetadata::new()
));
let boundary = AudioSegment::new_track_boundary(0, 0.0, metadata);
// Envoyer le boundary
input_tx.send(boundary.clone()).await.unwrap();
drop(input_tx);
// Lancer le traitement
tokio::spawn(async move {
logic
.process(Some(input_rx), vec![output_tx], stop_token)
.await
.unwrap();
});
// Vérifier que le boundary passe tel quel
let result = output_rx.recv().await.unwrap();
assert!(result.as_sync_marker().is_some());
if let Some(marker) = result.as_sync_marker() {
assert!(matches!(**marker, SyncMarker::TrackBoundary { .. }));
}
}
#[tokio::test]
async fn test_resampling_node_integration() {
// Test d'intégration complet avec ResamplingNode
let (input_tx, input_rx) = mpsc::channel(10);
let (output_tx, mut output_rx) = mpsc::channel(10);
let stop_token = CancellationToken::new();
let mut logic = ResamplingLogic::new(48000);
// Créer un chunk à 44.1kHz
let chunk_44k = AudioChunk::I16(AudioChunkData::new(
vec![[1000, 2000]; 100], // 100 frames
44100,
0.0,
));
let segment = Arc::new(AudioSegment {
order: 0,
timestamp_sec: 0.0,
segment: crate::_AudioSegment::Chunk(Arc::new(chunk_44k)),
});
input_tx.send(segment).await.unwrap();
drop(input_tx);
// Lancer le traitement
tokio::spawn(async move {
logic
.process(Some(input_rx), vec![output_tx], stop_token)
.await
.unwrap();
});
// Vérifier le résultat
let result = output_rx.recv().await.unwrap();
assert!(result.is_audio_chunk());
if let Some(chunk) = result.as_chunk() {
// Le chunk doit être I16 (même type)
assert!(matches!(chunk.as_ref(), AudioChunk::I16(_)));
// Le sample rate doit être 48000
assert_eq!(chunk.sample_rate(), 48000);
// Le nombre de frames doit avoir changé (ratio ~1.088)
// 100 frames @ 44.1kHz ≈ 109 frames @ 48kHz
if let AudioChunk::I16(data) = chunk.as_ref() {
let frames = data.get_frames().len();
assert!(frames >= 105 && frames <= 115, "Expected ~109 frames, got {}", frames);
}
} else {
panic!("Expected audio chunk");
}
}
}