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)
PMOAudio
Pipeline audio stéréo async optimisé pour Rust, utilisant Tokio.
Caractéristiques
- Pipeline push-based async : Tous les nodes utilisent Tokio pour un traitement non-bloquant
- Zero-copy optimisé : Les données audio sont partagées via
Arc<Vec<f32>>pour éviter les clonages inutiles - Support multiroom : BufferNode avec buffer circulaire et offsets indépendants par abonné
- TimerNode : Calcul de position temporelle en temps réel
- Backpressure : Channels bounded avec
try_sendpour éviter les blocages
Architecture
AudioChunk
Structure de données pour un chunk audio stéréo :
pub struct AudioChunk {
pub order: u64, // Numéro d'ordre
pub left: Arc<Vec<f32>>, // Canal gauche (partagé)
pub right: Arc<Vec<f32>>, // Canal droit (partagé)
pub sample_rate: u32, // Taux d'échantillonnage
}
Les données sont wrappées dans Arc pour permettre le partage sans copie entre plusieurs abonnés.
Nodes
SingleSubscriberNode
- Un seul abonné
- Pas de clone inutile du Arc
MultiSubscriberNode
- Plusieurs abonnés
- Partage le même
Arc<AudioChunk>avec tous
SourceNode
- Génère ou lit des chunks audio
- Version mock avec génération de sinusoïdes pour tests
DecoderNode
- Décode les chunks audio
- Supporte le passthrough et le resampling (mock)
DspNode
- Applique des transformations DSP
- Clone les données uniquement si modification nécessaire
- Exemple : gain, filtrage
BufferNode
- Buffer circulaire (
VecDeque<Arc<AudioChunk>>) - Support multiroom avec offsets indépendants
try_sendnon-bloquant pour éviter de bloquer la source
TimerNode
- Node passthrough qui ne modifie pas les données
- Incrémente un compteur de samples
- Calcule la position :
position_sec = elapsed_samples / sample_rate - Fournit un
TimerHandlepour monitoring
SinkNode
- Node terminal qui consomme les chunks
- Versions : silent, logging, stats, mock file writer
Pipeline type
SourceNode → DecoderNode → DSPNode → BufferNode → TimerNode → SinkNode(s)
↓
Multiroom Sinks
(avec offsets)
Exemples
Pipeline simple
use pmoaudio::{SinkNode, SourceNode, TimerNode};
#[tokio::main]
async fn main() {
let (mut timer, timer_tx) = TimerNode::new(10);
let (sink, sink_tx) = SinkNode::new("Output".to_string(), 10);
timer.add_subscriber(sink_tx);
let timer_handle = timer.get_position_handle();
tokio::spawn(async move { timer.run().await.unwrap() });
let sink_handle = tokio::spawn(async move {
sink.run_with_stats().await.unwrap()
});
tokio::spawn(async move {
let mut source = SourceNode::new();
source.add_subscriber(timer_tx);
source.generate_chunks(30, 4800, 48000, 440.0).await.unwrap();
});
sink_handle.await.unwrap();
}
Multiroom
let (buffer, buffer_tx) = BufferNode::new(50, 10);
let (sink1, sink1_tx) = SinkNode::new("Room 1".to_string(), 10);
let (sink2, sink2_tx) = SinkNode::new("Room 2".to_string(), 10);
buffer.add_subscriber_with_offset(sink1_tx, 0).await; // Pas de délai
buffer.add_subscriber_with_offset(sink2_tx, 5).await; // 5 chunks de retard
Lancer les exemples
# Pipeline simple
cargo run --example simple_pipeline
# Pipeline complet avec tous les nodes
cargo run --example pipeline_demo
# Configuration multiroom
cargo run --example multiroom_demo
# Streaming avec timing réel
cargo run --example streaming_demo
Tests
cargo test
20 tests unitaires couvrant :
- Propagation des chunks
- Calcul de position par TimerNode
- BufferNode multi-abonné avec offsets
- Arc sharing et zero-copy
- DSP avec gain et filtrage
- Resampling
Optimisations
- Arc sharing : Les
AudioChunksont clonés viaArc::clone()qui ne clone que le pointeur - Copy-on-Write : Les DSP nodes clonent les données uniquement si modification nécessaire
- Bounded channels : Backpressure automatique
- try_send : Non-bloquant pour BufferNode, permet de sauter des chunks si un abonné est saturé
- RwLock : Pour partage concurrent du compteur TimerNode
Dépendances
tokio: Runtime async et channelsasync-trait: Traits async
License
CeCill-2.0