Files
pmomusic/pmoaudio
Claude 6a7ba01102 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
..

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_send pour é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_send non-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 TimerHandle pour 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

  1. Arc sharing : Les AudioChunk sont clonés via Arc::clone() qui ne clone que le pointeur
  2. Copy-on-Write : Les DSP nodes clonent les données uniquement si modification nécessaire
  3. Bounded channels : Backpressure automatique
  4. try_send : Non-bloquant pour BufferNode, permet de sauter des chunks si un abonné est saturé
  5. RwLock : Pour partage concurrent du compteur TimerNode

Dépendances

  • tokio : Runtime async et channels
  • async-trait : Traits async

License

CeCill-2.0