Création du module pmoaudio
This commit is contained in:
11
pmoaudio/Cargo.toml
Normal file
11
pmoaudio/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "pmoaudio"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.42", features = ["full"] }
|
||||
async-trait = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
165
pmoaudio/README.md
Normal file
165
pmoaudio/README.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# 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 :
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
MIT
|
||||
77
pmoaudio/examples/multiroom_demo.rs
Normal file
77
pmoaudio/examples/multiroom_demo.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
//! Exemple de configuration multiroom avec BufferNode
|
||||
//!
|
||||
//! Démontre l'utilisation du buffer circulaire pour synchroniser
|
||||
//! plusieurs sorties avec des délais différents
|
||||
|
||||
use pmoaudio::{BufferNode, SinkNode, SourceNode};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Multiroom Demo ===\n");
|
||||
|
||||
// Buffer avec capacité pour gérer les délais
|
||||
let (buffer, buffer_tx) = BufferNode::new(50, 10);
|
||||
|
||||
// Créer 3 sorties avec délais différents
|
||||
let (sink1, sink1_tx) = SinkNode::new("Room 1 (no delay)".to_string(), 10);
|
||||
let (sink2, sink2_tx) = SinkNode::new("Room 2 (5 chunks delay)".to_string(), 10);
|
||||
let (sink3, sink3_tx) = SinkNode::new("Room 3 (10 chunks delay)".to_string(), 10);
|
||||
|
||||
buffer.add_subscriber_with_offset(sink1_tx, 0).await;
|
||||
buffer.add_subscriber_with_offset(sink2_tx, 5).await;
|
||||
buffer.add_subscriber_with_offset(sink3_tx, 10).await;
|
||||
|
||||
// Spawn buffer et sinks
|
||||
tokio::spawn(async move {
|
||||
buffer.run().await.unwrap();
|
||||
});
|
||||
|
||||
let sink1_handle = tokio::spawn(async move {
|
||||
let stats = sink1.run_with_stats().await.unwrap();
|
||||
stats.display();
|
||||
stats
|
||||
});
|
||||
|
||||
let sink2_handle = tokio::spawn(async move {
|
||||
let stats = sink2.run_with_stats().await.unwrap();
|
||||
stats.display();
|
||||
stats
|
||||
});
|
||||
|
||||
let sink3_handle = tokio::spawn(async move {
|
||||
let stats = sink3.run_with_stats().await.unwrap();
|
||||
stats.display();
|
||||
stats
|
||||
});
|
||||
|
||||
// Générer de l'audio dans une tâche séparée
|
||||
println!("Generating audio for multiroom playback...\n");
|
||||
tokio::spawn(async move {
|
||||
let mut source = SourceNode::new();
|
||||
source.add_subscriber(buffer_tx);
|
||||
source.generate_chunks(30, 4800, 48000, 440.0).await.unwrap();
|
||||
});
|
||||
|
||||
println!("Waiting for all rooms to finish...\n");
|
||||
|
||||
// Attendre toutes les sorties
|
||||
let stats1 = sink1_handle.await.unwrap();
|
||||
let stats2 = sink2_handle.await.unwrap();
|
||||
let stats3 = sink3_handle.await.unwrap();
|
||||
|
||||
println!("\n=== Multiroom Summary ===");
|
||||
println!(
|
||||
"{}: {} chunks received",
|
||||
stats1.name, stats1.chunks_received
|
||||
);
|
||||
println!(
|
||||
"{}: {} chunks received",
|
||||
stats2.name, stats2.chunks_received
|
||||
);
|
||||
println!(
|
||||
"{}: {} chunks received",
|
||||
stats3.name, stats3.chunks_received
|
||||
);
|
||||
|
||||
println!("\nNote: Delayed rooms receive fewer chunks due to the offset");
|
||||
}
|
||||
126
pmoaudio/examples/pipeline_demo.rs
Normal file
126
pmoaudio/examples/pipeline_demo.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
//! Exemple de pipeline audio stéréo complet avec tous les nodes
|
||||
//!
|
||||
//! Pipeline: SourceNode → DecoderNode → DspNode → BufferNode → TimerNode → SinkNode(s)
|
||||
|
||||
use pmoaudio::{BufferNode, DecoderNode, DspNode, SinkNode, SourceNode, TimerNode};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== PMOAudio Pipeline Demo ===\n");
|
||||
|
||||
// Créer le pipeline de nodes
|
||||
|
||||
// 2. DecoderNode - passthrough dans cet exemple
|
||||
let (mut decoder, decoder_tx) = DecoderNode::new(10);
|
||||
|
||||
// 3. DspNode - applique un gain de 0.5
|
||||
let (mut dsp, dsp_tx) = DspNode::new(10, 0.5);
|
||||
|
||||
// 4. BufferNode - buffer circulaire pour multiroom
|
||||
let (mut buffer, buffer_tx) = BufferNode::new(100, 10);
|
||||
|
||||
// 5. TimerNode - calcule la position temporelle
|
||||
let (mut timer, timer_tx) = TimerNode::new(10);
|
||||
|
||||
// 6. SinkNodes - deux destinations finales
|
||||
let (sink1, sink1_tx) = SinkNode::new("Main Output".to_string(), 10);
|
||||
let (sink2, sink2_tx) = SinkNode::new("Secondary Output".to_string(), 10);
|
||||
|
||||
// Ajouter un abonné au BufferNode avec offset (multiroom simulation)
|
||||
let (sink3, sink3_tx) = SinkNode::new("Delayed Output".to_string(), 10);
|
||||
buffer.add_subscriber_with_offset(sink3_tx, 5).await; // 5 chunks de retard
|
||||
|
||||
// Connecter le pipeline
|
||||
decoder.add_subscriber(dsp_tx);
|
||||
dsp.add_subscriber(buffer_tx);
|
||||
buffer.add_next_subscriber(timer_tx); // BufferNode -> TimerNode
|
||||
timer.add_subscriber(sink1_tx);
|
||||
timer.add_subscriber(sink2_tx);
|
||||
|
||||
// Obtenir un handle pour lire la position du TimerNode
|
||||
let timer_handle = timer.get_position_handle();
|
||||
|
||||
// Spawn tous les nodes
|
||||
let decoder_handle = tokio::spawn(async move {
|
||||
decoder.run_passthrough().await.unwrap();
|
||||
});
|
||||
|
||||
let dsp_handle = tokio::spawn(async move {
|
||||
dsp.run().await.unwrap();
|
||||
});
|
||||
|
||||
let buffer_handle = tokio::spawn(async move {
|
||||
buffer.run().await.unwrap();
|
||||
});
|
||||
|
||||
let timer_handle_task = tokio::spawn(async move {
|
||||
timer.run().await.unwrap();
|
||||
});
|
||||
|
||||
let sink1_handle = tokio::spawn(async move {
|
||||
let stats = sink1.run_with_stats().await.unwrap();
|
||||
stats.display();
|
||||
stats
|
||||
});
|
||||
|
||||
let sink2_handle = tokio::spawn(async move {
|
||||
sink2.run_silent().await.unwrap();
|
||||
});
|
||||
|
||||
let sink3_handle = tokio::spawn(async move {
|
||||
let stats = sink3.run_with_stats().await.unwrap();
|
||||
stats.display();
|
||||
stats
|
||||
});
|
||||
|
||||
// Spawn une tâche pour afficher la position périodiquement
|
||||
let position_monitor = tokio::spawn(async move {
|
||||
for _ in 0..10 {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||
let position = timer_handle.position_sec().await;
|
||||
let samples = timer_handle.elapsed_samples().await;
|
||||
println!("Position: {:.3} sec ({} samples)", position, samples);
|
||||
}
|
||||
});
|
||||
|
||||
// Générer des chunks audio
|
||||
println!("Generating audio chunks...\n");
|
||||
let chunk_size = 4800; // 100ms à 48kHz
|
||||
let sample_rate = 48000;
|
||||
let frequency = 440.0; // La 440Hz
|
||||
|
||||
// Source node dans une tâche séparée
|
||||
tokio::spawn(async move {
|
||||
let mut source = SourceNode::new();
|
||||
source.add_subscriber(decoder_tx);
|
||||
|
||||
// Générer 50 chunks (environ 5 secondes)
|
||||
source
|
||||
.generate_chunks(50, chunk_size, sample_rate, frequency)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("\nChunks sent. Processing...\n");
|
||||
});
|
||||
|
||||
// Attendre que tous les nodes terminent
|
||||
decoder_handle.await.unwrap();
|
||||
dsp_handle.await.unwrap();
|
||||
buffer_handle.await.unwrap();
|
||||
timer_handle_task.await.unwrap();
|
||||
|
||||
let stats1 = sink1_handle.await.unwrap();
|
||||
sink2_handle.await.unwrap();
|
||||
let stats3 = sink3_handle.await.unwrap();
|
||||
position_monitor.await.unwrap();
|
||||
|
||||
println!("\n=== Pipeline Demo Complete ===");
|
||||
println!(
|
||||
"Main output processed: {} chunks, {:.3} sec",
|
||||
stats1.chunks_received, stats1.total_duration_sec
|
||||
);
|
||||
println!(
|
||||
"Delayed output processed: {} chunks, {:.3} sec",
|
||||
stats3.chunks_received, stats3.total_duration_sec
|
||||
);
|
||||
}
|
||||
53
pmoaudio/examples/simple_pipeline.rs
Normal file
53
pmoaudio/examples/simple_pipeline.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
//! Exemple simple de pipeline audio : Source → Timer → Sink
|
||||
//!
|
||||
//! Démontre l'utilisation basique du pipeline avec calcul de position
|
||||
|
||||
use pmoaudio::{SinkNode, SourceNode, TimerNode};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Simple Pipeline Example ===\n");
|
||||
|
||||
// Créer les nodes
|
||||
let (mut timer, timer_tx) = TimerNode::new(10);
|
||||
let (sink, sink_tx) = SinkNode::new("Output".to_string(), 10);
|
||||
|
||||
// Connecter
|
||||
timer.add_subscriber(sink_tx);
|
||||
|
||||
// Handle pour monitorer la position
|
||||
let timer_handle = timer.get_position_handle();
|
||||
|
||||
// Spawn timer et sink
|
||||
tokio::spawn(async move {
|
||||
timer.run().await.unwrap();
|
||||
});
|
||||
|
||||
let sink_handle = tokio::spawn(async move {
|
||||
let stats = sink.run_with_stats().await.unwrap();
|
||||
stats.display();
|
||||
stats
|
||||
});
|
||||
|
||||
// Générer quelques secondes d'audio dans une tâche séparée
|
||||
println!("Generating 440Hz sine wave...\n");
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut source = SourceNode::new();
|
||||
source.add_subscriber(timer_tx);
|
||||
|
||||
source
|
||||
.generate_chunks(30, 4800, 48000, 440.0) // ~3 secondes
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// La source est drop ici, fermant le channel
|
||||
});
|
||||
|
||||
// Attendre la fin
|
||||
let stats = sink_handle.await.unwrap();
|
||||
|
||||
let final_position = timer_handle.position_sec().await;
|
||||
println!("\nFinal position: {:.3} seconds", final_position);
|
||||
println!("Total duration: {:.3} seconds", stats.total_duration_sec);
|
||||
}
|
||||
52
pmoaudio/examples/streaming_demo.rs
Normal file
52
pmoaudio/examples/streaming_demo.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Exemple de streaming audio en temps réel
|
||||
//!
|
||||
//! Démontre l'utilisation du pipeline avec génération de chunks
|
||||
//! en temps réel avec timing approprié
|
||||
|
||||
use pmoaudio::{SinkNode, SourceNode, TimerNode};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
println!("=== Streaming Demo ===\n");
|
||||
println!("Streaming audio in real-time for 3 seconds...\n");
|
||||
|
||||
let mut source = SourceNode::new();
|
||||
let (mut timer, timer_tx) = TimerNode::new(20);
|
||||
let (sink, sink_tx) = SinkNode::new("Streaming Output".to_string(), 20);
|
||||
|
||||
source.add_subscriber(timer_tx);
|
||||
timer.add_subscriber(sink_tx);
|
||||
|
||||
let timer_handle = timer.get_position_handle();
|
||||
|
||||
// Spawn le pipeline
|
||||
tokio::spawn(async move {
|
||||
timer.run().await.unwrap();
|
||||
});
|
||||
|
||||
let sink_handle = tokio::spawn(async move {
|
||||
sink.run_with_logging().await.unwrap();
|
||||
});
|
||||
|
||||
// Monitor la position
|
||||
let monitor_handle = tokio::spawn(async move {
|
||||
for _ in 0..15 {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||
let position = timer_handle.position_sec().await;
|
||||
println!("Playback position: {:.3} sec", position);
|
||||
}
|
||||
});
|
||||
|
||||
// Stream des chunks avec timing réel
|
||||
// 100ms par chunk à 48kHz = 4800 samples
|
||||
source
|
||||
.stream_chunks(4800, 48000, 440.0, 3000) // 3 secondes
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
println!("\nStreaming complete.");
|
||||
|
||||
// Attendre la fin
|
||||
sink_handle.await.unwrap();
|
||||
monitor_handle.await.unwrap();
|
||||
}
|
||||
173
pmoaudio/src/audio_chunk.rs
Normal file
173
pmoaudio/src/audio_chunk.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Représente un chunk audio stéréo avec données partagées via Arc
|
||||
///
|
||||
/// Cette structure encapsule des données audio stéréo (canaux gauche et droit)
|
||||
/// en utilisant `Arc<Vec<f32>>` pour permettre le partage efficace entre plusieurs
|
||||
/// consumers sans copier les données audio.
|
||||
///
|
||||
/// # Optimisation zero-copy
|
||||
///
|
||||
/// Les données audio sont wrappées dans `Arc`, ce qui signifie que:
|
||||
/// - Le clonage d'un `AudioChunk` ne clone que les pointeurs Arc (très rapide)
|
||||
/// - Les données audio réelles ne sont copiées que si nécessaire (Copy-on-Write)
|
||||
/// - Plusieurs nodes peuvent partager le même chunk simultanément
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoaudio::AudioChunk;
|
||||
///
|
||||
/// // Créer un chunk avec des données générées
|
||||
/// let left = vec![0.0, 0.1, 0.2, 0.3];
|
||||
/// let right = vec![0.0, 0.1, 0.2, 0.3];
|
||||
/// let chunk = AudioChunk::new(0, left, right, 48000);
|
||||
///
|
||||
/// assert_eq!(chunk.len(), 4);
|
||||
/// assert_eq!(chunk.sample_rate, 48000);
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AudioChunk {
|
||||
/// Numéro d'ordre du chunk dans le flux
|
||||
///
|
||||
/// Permet de suivre l'ordre des chunks et détecter les pertes éventuelles
|
||||
pub order: u64,
|
||||
|
||||
/// Canal gauche (partagé via Arc pour éviter les clonages)
|
||||
///
|
||||
/// Les samples sont en format float 32-bit, normalement entre -1.0 et 1.0
|
||||
pub left: Arc<Vec<f32>>,
|
||||
|
||||
/// Canal droit (partagé via Arc pour éviter les clonages)
|
||||
///
|
||||
/// Les samples sont en format float 32-bit, normalement entre -1.0 et 1.0
|
||||
pub right: Arc<Vec<f32>>,
|
||||
|
||||
/// Taux d'échantillonnage en Hz
|
||||
///
|
||||
/// Valeurs typiques: 44100, 48000, 96000, 192000
|
||||
pub sample_rate: u32,
|
||||
}
|
||||
|
||||
impl AudioChunk {
|
||||
/// Crée un nouveau chunk audio
|
||||
///
|
||||
/// Les vecteurs sont automatiquement wrappés dans `Arc`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `order` - Numéro d'ordre du chunk dans le flux
|
||||
/// * `left` - Samples du canal gauche
|
||||
/// * `right` - Samples du canal droit
|
||||
/// * `sample_rate` - Taux d'échantillonnage en Hz
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoaudio::AudioChunk;
|
||||
///
|
||||
/// let chunk = AudioChunk::new(
|
||||
/// 0,
|
||||
/// vec![0.0, 0.5, 1.0],
|
||||
/// vec![0.0, 0.5, 1.0],
|
||||
/// 48000
|
||||
/// );
|
||||
/// ```
|
||||
pub fn new(order: u64, left: Vec<f32>, right: Vec<f32>, sample_rate: u32) -> Self {
|
||||
Self {
|
||||
order,
|
||||
left: Arc::new(left),
|
||||
right: Arc::new(right),
|
||||
sample_rate,
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un chunk à partir de données déjà wrappées dans Arc
|
||||
///
|
||||
/// Utile pour éviter un double wrapping si les données sont déjà dans Arc.
|
||||
pub fn from_arc(
|
||||
order: u64,
|
||||
left: Arc<Vec<f32>>,
|
||||
right: Arc<Vec<f32>>,
|
||||
sample_rate: u32,
|
||||
) -> Self {
|
||||
Self {
|
||||
order,
|
||||
left,
|
||||
right,
|
||||
sample_rate,
|
||||
}
|
||||
}
|
||||
|
||||
/// Retourne le nombre d'échantillons par canal
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoaudio::AudioChunk;
|
||||
///
|
||||
/// let chunk = AudioChunk::new(0, vec![0.0; 1000], vec![0.0; 1000], 48000);
|
||||
/// assert_eq!(chunk.len(), 1000);
|
||||
/// ```
|
||||
pub fn len(&self) -> usize {
|
||||
self.left.len()
|
||||
}
|
||||
|
||||
/// Vérifie si le chunk est vide
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.left.is_empty()
|
||||
}
|
||||
|
||||
/// Clone les données pour permettre une modification (Copy-on-Write)
|
||||
///
|
||||
/// Cette méthode doit être appelée uniquement si vous avez besoin de modifier
|
||||
/// les données audio. Pour une simple lecture, utilisez directement les champs
|
||||
/// `left` et `right`.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoaudio::AudioChunk;
|
||||
///
|
||||
/// let chunk = AudioChunk::new(0, vec![1.0, 2.0], vec![3.0, 4.0], 48000);
|
||||
/// let (mut left, mut right) = chunk.clone_data();
|
||||
///
|
||||
/// // Modifier les données
|
||||
/// for sample in &mut left {
|
||||
/// *sample *= 0.5;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn clone_data(&self) -> (Vec<f32>, Vec<f32>) {
|
||||
((*self.left).clone(), (*self.right).clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_audio_chunk_creation() {
|
||||
let left = vec![0.0, 0.1, 0.2];
|
||||
let right = vec![0.0, 0.1, 0.2];
|
||||
let chunk = AudioChunk::new(0, left, right, 48000);
|
||||
|
||||
assert_eq!(chunk.order, 0);
|
||||
assert_eq!(chunk.len(), 3);
|
||||
assert_eq!(chunk.sample_rate, 48000);
|
||||
assert!(!chunk.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audio_chunk_arc_sharing() {
|
||||
let left = Arc::new(vec![0.0, 0.1, 0.2]);
|
||||
let right = Arc::new(vec![0.0, 0.1, 0.2]);
|
||||
|
||||
let chunk1 = AudioChunk::from_arc(0, left.clone(), right.clone(), 48000);
|
||||
let chunk2 = chunk1.clone();
|
||||
|
||||
// Vérifier que les Arc pointent vers les mêmes données
|
||||
assert!(Arc::ptr_eq(&chunk1.left, &chunk2.left));
|
||||
assert!(Arc::ptr_eq(&chunk1.right, &chunk2.right));
|
||||
}
|
||||
}
|
||||
90
pmoaudio/src/lib.rs
Normal file
90
pmoaudio/src/lib.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
//! PMOAudio - Pipeline audio stéréo async optimisé
|
||||
//!
|
||||
//! Cette crate fournit un pipeline audio push-based async utilisant Tokio,
|
||||
//! optimisé pour minimiser les clonages de données via `Arc<Vec<f32>>`.
|
||||
//!
|
||||
//! # Architecture
|
||||
//!
|
||||
//! Le pipeline est composé de nodes asynchrones qui communiquent via des channels Tokio.
|
||||
//! Les données audio sont encapsulées dans des [`AudioChunk`] et partagées via `Arc` pour
|
||||
//! éviter les copies inutiles.
|
||||
//!
|
||||
//! ## Pipeline type
|
||||
//!
|
||||
//! ```text
|
||||
//! SourceNode → DecoderNode → DSPNode → BufferNode → TimerNode → SinkNode(s)
|
||||
//! ↓
|
||||
//! Multiroom Sinks
|
||||
//! (avec offsets)
|
||||
//! ```
|
||||
//!
|
||||
//! # Exemples
|
||||
//!
|
||||
//! ## Pipeline simple
|
||||
//!
|
||||
//! ```no_run
|
||||
//! 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);
|
||||
//!
|
||||
//! 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();
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Configuration multiroom
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use pmoaudio::{BufferNode, SinkNode};
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() {
|
||||
//! 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);
|
||||
//!
|
||||
//! // Room 1 sans délai, Room 2 avec 5 chunks de retard
|
||||
//! buffer.add_subscriber_with_offset(sink1_tx, 0).await;
|
||||
//! buffer.add_subscriber_with_offset(sink2_tx, 5).await;
|
||||
//!
|
||||
//! tokio::spawn(async move { buffer.run().await.unwrap() });
|
||||
//! // ... spawn sinks et source
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! # Optimisations
|
||||
//!
|
||||
//! - **Zero-copy** : Les [`AudioChunk`] sont partagés via `Arc`, seul le pointeur est cloné
|
||||
//! - **Copy-on-Write** : Les nodes DSP clonent les données uniquement si modification nécessaire
|
||||
//! - **Backpressure** : Channels bounded avec `try_send` pour éviter les blocages
|
||||
//! - **RwLock** : Pour partage concurrent du compteur [`TimerNode`]
|
||||
|
||||
mod audio_chunk;
|
||||
mod nodes;
|
||||
|
||||
pub use audio_chunk::AudioChunk;
|
||||
pub use nodes::{
|
||||
buffer_node::BufferNode,
|
||||
decoder_node::DecoderNode,
|
||||
dsp_node::DspNode,
|
||||
sink_node::{SinkNode, SinkStats},
|
||||
source_node::SourceNode,
|
||||
timer_node::{TimerHandle, TimerNode},
|
||||
AudioError, AudioNode, MultiSubscriberNode, SingleSubscriberNode,
|
||||
};
|
||||
241
pmoaudio/src/nodes/buffer_node.rs
Normal file
241
pmoaudio/src/nodes/buffer_node.rs
Normal file
@@ -0,0 +1,241 @@
|
||||
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
|
||||
/// Subscriber avec son propre offset dans le buffer
|
||||
struct BufferSubscriber {
|
||||
tx: mpsc::Sender<Arc<AudioChunk>>,
|
||||
offset: usize, // Position dans le buffer circulaire
|
||||
}
|
||||
|
||||
/// BufferNode avec buffer circulaire pour support multiroom
|
||||
///
|
||||
/// Ce node maintient un buffer circulaire de chunks et permet à plusieurs
|
||||
/// abonnés de lire avec des offsets différents, ce qui est idéal pour des
|
||||
/// configurations multiroom où différentes pièces peuvent avoir un léger
|
||||
/// délai de synchronisation.
|
||||
///
|
||||
/// # Fonctionnement
|
||||
///
|
||||
/// - Le buffer est implémenté avec un `VecDeque` de taille fixe
|
||||
/// - Chaque abonné peut avoir un offset indépendant (en nombre de chunks)
|
||||
/// - Utilise `try_send` pour éviter de bloquer si un abonné est saturé
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::{BufferNode, SinkNode};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// 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);
|
||||
///
|
||||
/// // Room 1 sans délai
|
||||
/// buffer.add_subscriber_with_offset(sink1_tx, 0).await;
|
||||
///
|
||||
/// // Room 2 avec 5 chunks de retard
|
||||
/// buffer.add_subscriber_with_offset(sink2_tx, 5).await;
|
||||
///
|
||||
/// tokio::spawn(async move { buffer.run().await.unwrap() });
|
||||
/// // ... spawn sinks et source
|
||||
/// }
|
||||
/// ```
|
||||
pub struct BufferNode {
|
||||
buffer: Arc<RwLock<VecDeque<Arc<AudioChunk>>>>,
|
||||
subscribers: Arc<RwLock<Vec<BufferSubscriber>>>,
|
||||
buffer_size: usize,
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
next_subscribers: MultiSubscriberNode, // Pour passer au node suivant
|
||||
}
|
||||
|
||||
impl BufferNode {
|
||||
/// Crée un nouveau BufferNode
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `buffer_size` - Taille maximale du buffer circulaire
|
||||
/// * `channel_size` - Taille du channel bounded pour backpressure
|
||||
pub fn new(buffer_size: usize, channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let node = Self {
|
||||
buffer: Arc::new(RwLock::new(VecDeque::with_capacity(buffer_size))),
|
||||
subscribers: Arc::new(RwLock::new(Vec::new())),
|
||||
buffer_size,
|
||||
rx,
|
||||
next_subscribers: MultiSubscriberNode::new(),
|
||||
};
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
/// Ajoute un abonné avec un offset spécifique (pour multiroom)
|
||||
pub async fn add_subscriber_with_offset(
|
||||
&self,
|
||||
tx: mpsc::Sender<Arc<AudioChunk>>,
|
||||
offset: usize,
|
||||
) {
|
||||
let mut subs = self.subscribers.write().await;
|
||||
subs.push(BufferSubscriber { tx, offset });
|
||||
}
|
||||
|
||||
/// Ajoute un abonné sans offset (commence au chunk courant)
|
||||
pub async fn add_subscriber(&self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.add_subscriber_with_offset(tx, 0).await;
|
||||
}
|
||||
|
||||
/// Ajoute un abonné pour le node suivant (sans buffer)
|
||||
pub fn add_next_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.next_subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Démarre la boucle de traitement du BufferNode
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
let mut chunk_index = 0usize;
|
||||
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
// Ajouter au buffer circulaire
|
||||
{
|
||||
let mut buffer = self.buffer.write().await;
|
||||
if buffer.len() >= self.buffer_size {
|
||||
buffer.pop_front();
|
||||
}
|
||||
buffer.push_back(chunk.clone());
|
||||
}
|
||||
|
||||
// Envoyer aux abonnés avec offset
|
||||
{
|
||||
let buffer = self.buffer.read().await;
|
||||
let mut subs = self.subscribers.write().await;
|
||||
|
||||
for sub in subs.iter_mut() {
|
||||
// Calculer l'index dans le buffer en fonction de l'offset
|
||||
let target_index = if chunk_index >= sub.offset {
|
||||
chunk_index - sub.offset
|
||||
} else {
|
||||
continue; // Pas encore assez de données
|
||||
};
|
||||
|
||||
// Vérifier si le chunk est disponible dans le buffer
|
||||
let buffer_age = chunk_index - target_index;
|
||||
if buffer_age < buffer.len() {
|
||||
let chunk_to_send = &buffer[buffer.len() - buffer_age - 1];
|
||||
// try_send non-bloquant pour éviter de bloquer la source
|
||||
let _ = sub.tx.try_send(chunk_to_send.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push vers les nodes suivants sans buffer
|
||||
self.next_subscribers.try_push(chunk).await?;
|
||||
|
||||
chunk_index += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Version avec push synchrone au lieu de try_push
|
||||
pub async fn run_blocking(mut self) -> Result<(), AudioError> {
|
||||
let mut chunk_index = 0usize;
|
||||
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
// Ajouter au buffer circulaire
|
||||
{
|
||||
let mut buffer = self.buffer.write().await;
|
||||
if buffer.len() >= self.buffer_size {
|
||||
buffer.pop_front();
|
||||
}
|
||||
buffer.push_back(chunk.clone());
|
||||
}
|
||||
|
||||
// Envoyer aux abonnés avec offset
|
||||
{
|
||||
let buffer = self.buffer.read().await;
|
||||
let subs = self.subscribers.read().await;
|
||||
|
||||
for sub in subs.iter() {
|
||||
let target_index = if chunk_index >= sub.offset {
|
||||
chunk_index - sub.offset
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let buffer_age = chunk_index - target_index;
|
||||
if buffer_age < buffer.len() {
|
||||
let chunk_to_send = &buffer[buffer.len() - buffer_age - 1];
|
||||
let _ = sub.tx.send(chunk_to_send.clone()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Push vers les nodes suivants
|
||||
for _ in 0..self.next_subscribers.subscribers.len() {
|
||||
self.next_subscribers.push(chunk.clone()).await?;
|
||||
}
|
||||
|
||||
chunk_index += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_buffer_node_basic() {
|
||||
let (mut node, tx) = BufferNode::new(10, 5);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(5);
|
||||
|
||||
node.add_next_subscriber(out_tx);
|
||||
|
||||
// Spawn le node
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer des chunks
|
||||
for i in 0..3 {
|
||||
let chunk = AudioChunk::new(i, vec![0.0; 100], vec![0.0; 100], 48000);
|
||||
tx.send(Arc::new(chunk)).await.unwrap();
|
||||
}
|
||||
|
||||
// Recevoir les chunks
|
||||
for i in 0..3 {
|
||||
let chunk = out_rx.recv().await.unwrap();
|
||||
assert_eq!(chunk.order, i);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_buffer_node_with_offset() {
|
||||
let (node, tx) = BufferNode::new(10, 10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
// Ajouter un abonné avec offset de 2 chunks
|
||||
node.add_subscriber_with_offset(out_tx, 2).await;
|
||||
|
||||
// Spawn le node
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer 5 chunks
|
||||
for i in 0..5 {
|
||||
let chunk = AudioChunk::new(i, vec![0.0; 100], vec![0.0; 100], 48000);
|
||||
tx.send(Arc::new(chunk)).await.unwrap();
|
||||
}
|
||||
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||
|
||||
// L'abonné devrait recevoir les chunks 0, 1, 2 (avec 2 chunks de retard)
|
||||
let chunk = out_rx.try_recv().unwrap();
|
||||
assert_eq!(chunk.order, 0);
|
||||
}
|
||||
}
|
||||
147
pmoaudio/src/nodes/decoder_node.rs
Normal file
147
pmoaudio/src/nodes/decoder_node.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// DecoderNode - Décode des chunks audio
|
||||
///
|
||||
/// Version mock qui passe simplement les chunks (ou simule un décodage simple)
|
||||
pub struct DecoderNode {
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
impl DecoderNode {
|
||||
pub fn new(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
};
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Mode passthrough - passe les chunks sans modification
|
||||
pub async fn run_passthrough(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
self.subscribers.push(chunk).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mode mock décodage - simule un changement de sample rate
|
||||
pub async fn run_with_resampling(mut self, target_sample_rate: u32) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
if chunk.sample_rate == target_sample_rate {
|
||||
// Pas besoin de resampling
|
||||
self.subscribers.push(chunk).await?;
|
||||
} else {
|
||||
// Simuler un resampling (mock simple)
|
||||
let ratio = target_sample_rate as f64 / chunk.sample_rate as f64;
|
||||
let new_len = (chunk.len() as f64 * ratio) as usize;
|
||||
|
||||
let (left_data, right_data) = chunk.clone_data();
|
||||
let mut new_left = Vec::with_capacity(new_len);
|
||||
let mut new_right = Vec::with_capacity(new_len);
|
||||
|
||||
// Resampling linéaire simple (mock)
|
||||
for i in 0..new_len {
|
||||
let src_pos = i as f64 / ratio;
|
||||
let src_idx = src_pos as usize;
|
||||
|
||||
if src_idx < left_data.len() - 1 {
|
||||
let frac = src_pos - src_idx as f64;
|
||||
let left_sample =
|
||||
left_data[src_idx] * (1.0 - frac as f32) + left_data[src_idx + 1] * frac as f32;
|
||||
let right_sample =
|
||||
right_data[src_idx] * (1.0 - frac as f32) + right_data[src_idx + 1] * frac as f32;
|
||||
|
||||
new_left.push(left_sample);
|
||||
new_right.push(right_sample);
|
||||
} else if src_idx < left_data.len() {
|
||||
new_left.push(left_data[src_idx]);
|
||||
new_right.push(right_data[src_idx]);
|
||||
}
|
||||
}
|
||||
|
||||
let new_chunk = AudioChunk::new(chunk.order, new_left, new_right, target_sample_rate);
|
||||
self.subscribers.push(Arc::new(new_chunk)).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_decoder_passthrough() {
|
||||
let (mut node, tx) = DecoderNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run_passthrough().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk
|
||||
let chunk = AudioChunk::new(0, vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0], 48000);
|
||||
let chunk_arc = Arc::new(chunk);
|
||||
tx.send(chunk_arc.clone()).await.unwrap();
|
||||
|
||||
// Recevoir le chunk
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
assert!(Arc::ptr_eq(&chunk_arc, &received));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_decoder_resampling() {
|
||||
let (mut node, tx) = DecoderNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run_with_resampling(96000).await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk à 48000 Hz
|
||||
let chunk = AudioChunk::new(0, vec![1.0; 100], vec![1.0; 100], 48000);
|
||||
tx.send(Arc::new(chunk)).await.unwrap();
|
||||
|
||||
// Recevoir le chunk resampleé
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
assert_eq!(received.sample_rate, 96000);
|
||||
// Le chunk devrait être environ 2x plus grand
|
||||
assert!(received.len() > 150 && received.len() < 250);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_decoder_no_resampling_needed() {
|
||||
let (mut node, tx) = DecoderNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run_with_resampling(48000).await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk déjà au bon sample rate
|
||||
let chunk = AudioChunk::new(0, vec![1.0; 100], vec![1.0; 100], 48000);
|
||||
let chunk_arc = Arc::new(chunk);
|
||||
tx.send(chunk_arc.clone()).await.unwrap();
|
||||
|
||||
// Le chunk devrait être passé sans modification
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
assert!(Arc::ptr_eq(&chunk_arc, &received));
|
||||
}
|
||||
}
|
||||
236
pmoaudio/src/nodes/dsp_node.rs
Normal file
236
pmoaudio/src/nodes/dsp_node.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// DspNode - Applique des transformations DSP aux chunks audio
|
||||
///
|
||||
/// Clone les données uniquement si elles doivent être modifiées
|
||||
pub struct DspNode {
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
gain: f32,
|
||||
}
|
||||
|
||||
impl DspNode {
|
||||
pub fn new(channel_size: usize, gain: f32) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
gain,
|
||||
};
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Applique le gain aux chunks
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
if (self.gain - 1.0).abs() < f32::EPSILON {
|
||||
// Gain = 1.0, pas de transformation nécessaire
|
||||
self.subscribers.push(chunk).await?;
|
||||
} else {
|
||||
// Clone les données pour les modifier
|
||||
let (mut left_data, mut right_data) = chunk.clone_data();
|
||||
|
||||
// Appliquer le gain
|
||||
for sample in &mut left_data {
|
||||
*sample *= self.gain;
|
||||
}
|
||||
for sample in &mut right_data {
|
||||
*sample *= self.gain;
|
||||
}
|
||||
|
||||
let new_chunk = AudioChunk::new(
|
||||
chunk.order,
|
||||
left_data,
|
||||
right_data,
|
||||
chunk.sample_rate,
|
||||
);
|
||||
|
||||
self.subscribers.push(Arc::new(new_chunk)).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Met à jour le gain dynamiquement (nécessite un `Arc<RwLock<f32>>` dans une version réelle)
|
||||
pub fn set_gain(&mut self, gain: f32) {
|
||||
self.gain = gain;
|
||||
}
|
||||
}
|
||||
|
||||
/// DspNode avec filtre passe-bas simple (mock)
|
||||
#[allow(dead_code)]
|
||||
pub struct LowPassDspNode {
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
alpha: f32, // Coefficient du filtre
|
||||
prev_left: f32,
|
||||
prev_right: f32,
|
||||
}
|
||||
|
||||
impl LowPassDspNode {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(channel_size: usize, cutoff_ratio: f32) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
// Filtre RC simple: alpha = dt / (RC + dt)
|
||||
// cutoff_ratio entre 0 (tout couper) et 1 (tout passer)
|
||||
let alpha = cutoff_ratio.clamp(0.0, 1.0);
|
||||
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
alpha,
|
||||
prev_left: 0.0,
|
||||
prev_right: 0.0,
|
||||
};
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
let (left_data, right_data) = chunk.clone_data();
|
||||
let mut new_left = Vec::with_capacity(left_data.len());
|
||||
let mut new_right = Vec::with_capacity(right_data.len());
|
||||
|
||||
// Appliquer le filtre
|
||||
for &sample in &left_data {
|
||||
self.prev_left = self.prev_left + self.alpha * (sample - self.prev_left);
|
||||
new_left.push(self.prev_left);
|
||||
}
|
||||
|
||||
for &sample in &right_data {
|
||||
self.prev_right = self.prev_right + self.alpha * (sample - self.prev_right);
|
||||
new_right.push(self.prev_right);
|
||||
}
|
||||
|
||||
let new_chunk = AudioChunk::new(
|
||||
chunk.order,
|
||||
new_left,
|
||||
new_right,
|
||||
chunk.sample_rate,
|
||||
);
|
||||
|
||||
self.subscribers.push(Arc::new(new_chunk)).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dsp_node_unity_gain() {
|
||||
let (mut node, tx) = DspNode::new(10, 1.0);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk
|
||||
let chunk = AudioChunk::new(0, vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0], 48000);
|
||||
let chunk_arc = Arc::new(chunk);
|
||||
tx.send(chunk_arc.clone()).await.unwrap();
|
||||
|
||||
// Avec gain = 1.0, le chunk ne devrait pas être cloné
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
assert!(Arc::ptr_eq(&chunk_arc, &received));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dsp_node_gain() {
|
||||
let (mut node, tx) = DspNode::new(10, 2.0);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk
|
||||
let chunk = AudioChunk::new(0, vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0], 48000);
|
||||
tx.send(Arc::new(chunk)).await.unwrap();
|
||||
|
||||
// Vérifier que le gain a été appliqué
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
assert_eq!(received.left[0], 2.0);
|
||||
assert_eq!(received.left[1], 4.0);
|
||||
assert_eq!(received.left[2], 6.0);
|
||||
assert_eq!(received.right[0], 8.0);
|
||||
assert_eq!(received.right[1], 10.0);
|
||||
assert_eq!(received.right[2], 12.0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_lowpass_dsp_node() {
|
||||
let (mut node, tx) = LowPassDspNode::new(10, 0.5);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk avec un signal carré
|
||||
let chunk = AudioChunk::new(
|
||||
0,
|
||||
vec![1.0, 1.0, 1.0, -1.0, -1.0, -1.0],
|
||||
vec![1.0, 1.0, 1.0, -1.0, -1.0, -1.0],
|
||||
48000,
|
||||
);
|
||||
tx.send(Arc::new(chunk)).await.unwrap();
|
||||
|
||||
// Le filtre devrait lisser le signal
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
|
||||
// Vérifier que le signal est lissé (valeurs intermédiaires)
|
||||
assert!(received.left[0].abs() < 1.0); // Premier échantillon lissé
|
||||
assert!(received.left[2].abs() < 1.0); // Signal ne devrait pas atteindre 1.0 immédiatement
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dsp_node_multiple_subscribers() {
|
||||
let (mut node, tx) = DspNode::new(10, 0.5);
|
||||
let (out_tx1, mut out_rx1) = mpsc::channel(10);
|
||||
let (out_tx2, mut out_rx2) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx1);
|
||||
node.add_subscriber(out_tx2);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
let chunk = AudioChunk::new(0, vec![2.0, 4.0], vec![2.0, 4.0], 48000);
|
||||
tx.send(Arc::new(chunk)).await.unwrap();
|
||||
|
||||
// Les deux abonnés devraient recevoir le même Arc
|
||||
let received1 = out_rx1.recv().await.unwrap();
|
||||
let received2 = out_rx2.recv().await.unwrap();
|
||||
|
||||
assert!(Arc::ptr_eq(&received1, &received2));
|
||||
assert_eq!(received1.left[0], 1.0); // 2.0 * 0.5
|
||||
assert_eq!(received1.left[1], 2.0); // 4.0 * 0.5
|
||||
}
|
||||
}
|
||||
146
pmoaudio/src/nodes/mod.rs
Normal file
146
pmoaudio/src/nodes/mod.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
//! Nodes du pipeline audio
|
||||
//!
|
||||
//! Ce module contient tous les types de nodes disponibles pour construire
|
||||
//! un pipeline audio, ainsi que les traits et structures de support.
|
||||
|
||||
use crate::AudioChunk;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
pub mod buffer_node;
|
||||
pub mod decoder_node;
|
||||
pub mod dsp_node;
|
||||
pub mod sink_node;
|
||||
pub mod source_node;
|
||||
pub mod timer_node;
|
||||
|
||||
/// Trait de base pour tous les nodes audio
|
||||
///
|
||||
/// Tous les nodes du pipeline implémentent ce trait pour permettre
|
||||
/// une interface uniforme de traitement des chunks audio.
|
||||
#[async_trait::async_trait]
|
||||
pub trait AudioNode: Send + Sync {
|
||||
/// Push un chunk vers ce node
|
||||
///
|
||||
/// # Erreurs
|
||||
///
|
||||
/// Retourne `AudioError::SendError` si l'envoi échoue
|
||||
async fn push(&mut self, chunk: Arc<AudioChunk>) -> Result<(), AudioError>;
|
||||
|
||||
/// Ferme le node proprement
|
||||
async fn close(&mut self);
|
||||
}
|
||||
|
||||
/// Node avec un seul abonné (pas de clone inutile)
|
||||
///
|
||||
/// Optimisé pour les cas où un node n'a qu'un seul destinataire.
|
||||
/// Le Arc du chunk est simplement transféré sans clonage supplémentaire.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoaudio::SingleSubscriberNode;
|
||||
/// use tokio::sync::mpsc;
|
||||
///
|
||||
/// let (tx, rx) = mpsc::channel(10);
|
||||
/// let node = SingleSubscriberNode::new(tx);
|
||||
/// ```
|
||||
pub struct SingleSubscriberNode {
|
||||
tx: mpsc::Sender<Arc<AudioChunk>>,
|
||||
}
|
||||
|
||||
impl SingleSubscriberNode {
|
||||
pub fn new(tx: mpsc::Sender<Arc<AudioChunk>>) -> Self {
|
||||
Self { tx }
|
||||
}
|
||||
|
||||
pub async fn push(&self, chunk: Arc<AudioChunk>) -> Result<(), AudioError> {
|
||||
self.tx
|
||||
.send(chunk)
|
||||
.await
|
||||
.map_err(|_| AudioError::SendError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Node avec plusieurs abonnés (partage le même Arc)
|
||||
///
|
||||
/// Permet de broadcaster un chunk à plusieurs destinations.
|
||||
/// Tous les abonnés reçoivent le même `Arc<AudioChunk>`, donc pas de copie
|
||||
/// des données audio - seul le compteur de référence Arc est incrémenté.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```
|
||||
/// use pmoaudio::MultiSubscriberNode;
|
||||
/// use tokio::sync::mpsc;
|
||||
///
|
||||
/// let mut node = MultiSubscriberNode::new();
|
||||
/// let (tx1, rx1) = mpsc::channel(10);
|
||||
/// let (tx2, rx2) = mpsc::channel(10);
|
||||
///
|
||||
/// node.add_subscriber(tx1);
|
||||
/// node.add_subscriber(tx2);
|
||||
/// // Les deux abonnés recevront les mêmes chunks
|
||||
/// ```
|
||||
pub struct MultiSubscriberNode {
|
||||
subscribers: Vec<mpsc::Sender<Arc<AudioChunk>>>,
|
||||
}
|
||||
|
||||
impl MultiSubscriberNode {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.push(tx);
|
||||
}
|
||||
|
||||
pub async fn push(&self, chunk: Arc<AudioChunk>) -> Result<(), AudioError> {
|
||||
for tx in &self.subscribers {
|
||||
// On partage le même Arc avec tous les abonnés
|
||||
tx.send(chunk.clone())
|
||||
.await
|
||||
.map_err(|_| AudioError::SendError)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn try_push(&self, chunk: Arc<AudioChunk>) -> Result<(), AudioError> {
|
||||
for tx in &self.subscribers {
|
||||
// try_send non-bloquant, ignore si saturé
|
||||
let _ = tx.try_send(chunk.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MultiSubscriberNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Erreurs possibles dans le pipeline audio
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AudioError {
|
||||
/// Échec d'envoi d'un chunk à travers un channel
|
||||
SendError,
|
||||
/// Échec de réception d'un chunk depuis un channel
|
||||
ReceiveError,
|
||||
/// Erreur de traitement avec message descriptif
|
||||
ProcessingError(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AudioError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AudioError::SendError => write!(f, "Failed to send audio chunk"),
|
||||
AudioError::ReceiveError => write!(f, "Failed to receive audio chunk"),
|
||||
AudioError::ProcessingError(msg) => write!(f, "Processing error: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AudioError {}
|
||||
199
pmoaudio/src/nodes/sink_node.rs
Normal file
199
pmoaudio/src/nodes/sink_node.rs
Normal file
@@ -0,0 +1,199 @@
|
||||
use crate::{AudioChunk, nodes::AudioError};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// SinkNode - Node terminal qui consomme les chunks audio
|
||||
///
|
||||
/// Version mock pour tests et logging
|
||||
pub struct SinkNode {
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl SinkNode {
|
||||
pub fn new(name: String, channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let node = Self { rx, name };
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
/// Version silencieuse - consomme les chunks sans action
|
||||
pub async fn run_silent(mut self) -> Result<(), AudioError> {
|
||||
while let Some(_chunk) = self.rx.recv().await {
|
||||
// Ne rien faire, juste consommer
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Version avec logging
|
||||
pub async fn run_with_logging(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
println!(
|
||||
"[{}] Received chunk #{} - {} samples @ {} Hz",
|
||||
self.name,
|
||||
chunk.order,
|
||||
chunk.len(),
|
||||
chunk.sample_rate
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Version avec statistiques
|
||||
pub async fn run_with_stats(mut self) -> Result<SinkStats, AudioError> {
|
||||
let mut stats = SinkStats::new(self.name.clone());
|
||||
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
stats.process_chunk(&chunk);
|
||||
}
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
/// Version mock pour écriture dans un fichier (simule l'écriture)
|
||||
pub async fn run_mock_file_writer(mut self) -> Result<usize, AudioError> {
|
||||
let mut total_samples = 0;
|
||||
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
total_samples += chunk.len();
|
||||
// Simuler l'écriture avec un petit délai
|
||||
tokio::time::sleep(tokio::time::Duration::from_micros(10)).await;
|
||||
}
|
||||
|
||||
Ok(total_samples)
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistiques collectées par un SinkNode
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SinkStats {
|
||||
pub name: String,
|
||||
pub chunks_received: u64,
|
||||
pub total_samples: u64,
|
||||
pub total_duration_sec: f64,
|
||||
pub peak_left: f32,
|
||||
pub peak_right: f32,
|
||||
pub rms_left: f64,
|
||||
pub rms_right: f64,
|
||||
}
|
||||
|
||||
impl SinkStats {
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
chunks_received: 0,
|
||||
total_samples: 0,
|
||||
total_duration_sec: 0.0,
|
||||
peak_left: 0.0,
|
||||
peak_right: 0.0,
|
||||
rms_left: 0.0,
|
||||
rms_right: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_chunk(&mut self, chunk: &AudioChunk) {
|
||||
self.chunks_received += 1;
|
||||
self.total_samples += chunk.len() as u64;
|
||||
self.total_duration_sec += chunk.len() as f64 / chunk.sample_rate as f64;
|
||||
|
||||
// Calculer les peaks
|
||||
for &sample in chunk.left.iter() {
|
||||
if sample.abs() > self.peak_left {
|
||||
self.peak_left = sample.abs();
|
||||
}
|
||||
}
|
||||
|
||||
for &sample in chunk.right.iter() {
|
||||
if sample.abs() > self.peak_right {
|
||||
self.peak_right = sample.abs();
|
||||
}
|
||||
}
|
||||
|
||||
// Calculer RMS (moyenne des carrés)
|
||||
let sum_squares_left: f64 = chunk.left.iter().map(|&x| (x * x) as f64).sum();
|
||||
let sum_squares_right: f64 = chunk.right.iter().map(|&x| (x * x) as f64).sum();
|
||||
|
||||
self.rms_left = ((self.rms_left.powi(2) * (self.total_samples - chunk.len() as u64) as f64
|
||||
+ sum_squares_left)
|
||||
/ self.total_samples as f64)
|
||||
.sqrt();
|
||||
self.rms_right = ((self.rms_right.powi(2) * (self.total_samples - chunk.len() as u64) as f64
|
||||
+ sum_squares_right)
|
||||
/ self.total_samples as f64)
|
||||
.sqrt();
|
||||
}
|
||||
|
||||
pub fn display(&self) {
|
||||
println!("\n=== Sink Statistics: {} ===", self.name);
|
||||
println!("Chunks received: {}", self.chunks_received);
|
||||
println!("Total samples: {}", self.total_samples);
|
||||
println!("Total duration: {:.3} sec", self.total_duration_sec);
|
||||
println!("Peak L/R: {:.3} / {:.3}", self.peak_left, self.peak_right);
|
||||
println!("RMS L/R: {:.3} / {:.3}", self.rms_left, self.rms_right);
|
||||
println!("========================\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sink_node_silent() {
|
||||
let (node, tx) = SinkNode::new("test".to_string(), 10);
|
||||
|
||||
let handle = tokio::spawn(async move { node.run_silent().await });
|
||||
|
||||
// Envoyer quelques chunks
|
||||
for i in 0..3 {
|
||||
let chunk = AudioChunk::new(i, vec![0.0; 100], vec![0.0; 100], 48000);
|
||||
tx.send(Arc::new(chunk)).await.unwrap();
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
handle.await.unwrap().unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sink_node_stats() {
|
||||
let (node, tx) = SinkNode::new("test".to_string(), 10);
|
||||
|
||||
let handle = tokio::spawn(async move { node.run_with_stats().await });
|
||||
|
||||
// Envoyer des chunks avec signal connu
|
||||
for i in 0..3 {
|
||||
let chunk = AudioChunk::new(i, vec![1.0; 1000], vec![0.5; 1000], 48000);
|
||||
tx.send(Arc::new(chunk)).await.unwrap();
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
let stats = handle.await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(stats.chunks_received, 3);
|
||||
assert_eq!(stats.total_samples, 3000);
|
||||
assert_eq!(stats.peak_left, 1.0);
|
||||
assert_eq!(stats.peak_right, 0.5);
|
||||
assert!((stats.rms_left - 1.0).abs() < 0.001);
|
||||
assert!((stats.rms_right - 0.5).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sink_node_file_writer() {
|
||||
let (node, tx) = SinkNode::new("writer".to_string(), 10);
|
||||
|
||||
let handle = tokio::spawn(async move { node.run_mock_file_writer().await });
|
||||
|
||||
// Envoyer des chunks
|
||||
for i in 0..5 {
|
||||
let chunk = AudioChunk::new(i, vec![0.0; 100], vec![0.0; 100], 48000);
|
||||
tx.send(Arc::new(chunk)).await.unwrap();
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
let total_samples = handle.await.unwrap().unwrap();
|
||||
|
||||
assert_eq!(total_samples, 500);
|
||||
}
|
||||
}
|
||||
169
pmoaudio/src/nodes/source_node.rs
Normal file
169
pmoaudio/src/nodes/source_node.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// SourceNode - Génère ou lit des chunks audio depuis une source
|
||||
///
|
||||
/// Ce node est la source du pipeline. Version mock pour tests.
|
||||
pub struct SourceNode {
|
||||
subscribers: MultiSubscriberNode,
|
||||
}
|
||||
|
||||
impl SourceNode {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Génère un chunk de test avec une forme d'onde sinusoïdale
|
||||
pub fn generate_test_chunk(
|
||||
order: u64,
|
||||
size: usize,
|
||||
sample_rate: u32,
|
||||
frequency: f32,
|
||||
) -> AudioChunk {
|
||||
let mut left = Vec::with_capacity(size);
|
||||
let mut right = Vec::with_capacity(size);
|
||||
|
||||
for i in 0..size {
|
||||
let t = (order * size as u64 + i as u64) as f32 / sample_rate as f32;
|
||||
let sample = (2.0 * std::f32::consts::PI * frequency * t).sin();
|
||||
left.push(sample);
|
||||
right.push(sample * 0.8); // Légèrement différent pour la stéréo
|
||||
}
|
||||
|
||||
AudioChunk::new(order, left, right, sample_rate)
|
||||
}
|
||||
|
||||
/// Génère et envoie des chunks de test
|
||||
pub async fn generate_chunks(
|
||||
&self,
|
||||
count: u64,
|
||||
chunk_size: usize,
|
||||
sample_rate: u32,
|
||||
frequency: f32,
|
||||
) -> Result<(), AudioError> {
|
||||
for i in 0..count {
|
||||
let chunk = Self::generate_test_chunk(i, chunk_size, sample_rate, frequency);
|
||||
self.subscribers.push(Arc::new(chunk)).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Génère des chunks silencieux
|
||||
pub async fn generate_silence(
|
||||
&self,
|
||||
count: u64,
|
||||
chunk_size: usize,
|
||||
sample_rate: u32,
|
||||
) -> Result<(), AudioError> {
|
||||
for i in 0..count {
|
||||
let chunk = AudioChunk::new(
|
||||
i,
|
||||
vec![0.0; chunk_size],
|
||||
vec![0.0; chunk_size],
|
||||
sample_rate,
|
||||
);
|
||||
self.subscribers.push(Arc::new(chunk)).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Version streaming : génère des chunks continuellement avec délai
|
||||
pub async fn stream_chunks(
|
||||
&self,
|
||||
chunk_size: usize,
|
||||
sample_rate: u32,
|
||||
frequency: f32,
|
||||
duration_ms: u64,
|
||||
) -> Result<(), AudioError> {
|
||||
let chunk_duration_ms = (chunk_size as f64 / sample_rate as f64 * 1000.0) as u64;
|
||||
let mut order = 0u64;
|
||||
|
||||
let start = tokio::time::Instant::now();
|
||||
let duration = tokio::time::Duration::from_millis(duration_ms);
|
||||
|
||||
while start.elapsed() < duration {
|
||||
let chunk = Self::generate_test_chunk(order, chunk_size, sample_rate, frequency);
|
||||
self.subscribers.push(Arc::new(chunk)).await?;
|
||||
|
||||
order += 1;
|
||||
|
||||
// Attendre pour simuler le timing réel
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(chunk_duration_ms)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SourceNode {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_source_node_generation() {
|
||||
let mut source = SourceNode::new();
|
||||
let (tx, mut rx) = mpsc::channel(10);
|
||||
|
||||
source.add_subscriber(tx);
|
||||
|
||||
// Générer 3 chunks
|
||||
source.generate_chunks(3, 100, 48000, 440.0).await.unwrap();
|
||||
|
||||
// Vérifier la réception
|
||||
for i in 0..3 {
|
||||
let chunk = rx.recv().await.unwrap();
|
||||
assert_eq!(chunk.order, i);
|
||||
assert_eq!(chunk.len(), 100);
|
||||
assert_eq!(chunk.sample_rate, 48000);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sine_wave_generation() {
|
||||
let chunk = SourceNode::generate_test_chunk(0, 48000, 48000, 440.0);
|
||||
|
||||
// Vérifier qu'on a bien une sinusoïde
|
||||
// À 440 Hz avec 48000 samples/s, on devrait avoir 440 cycles
|
||||
let left = &*chunk.left;
|
||||
|
||||
// Trouver les passages par zéro
|
||||
let mut zero_crossings = 0;
|
||||
for i in 1..left.len() {
|
||||
if (left[i - 1] < 0.0 && left[i] >= 0.0) || (left[i - 1] >= 0.0 && left[i] < 0.0) {
|
||||
zero_crossings += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 440 cycles = 880 passages par zéro (approximativement)
|
||||
assert!(zero_crossings > 850 && zero_crossings < 910);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_source_node_silence() {
|
||||
let mut source = SourceNode::new();
|
||||
let (tx, mut rx) = mpsc::channel(10);
|
||||
|
||||
source.add_subscriber(tx);
|
||||
|
||||
source.generate_silence(2, 100, 48000).await.unwrap();
|
||||
|
||||
for _ in 0..2 {
|
||||
let chunk = rx.recv().await.unwrap();
|
||||
assert!(chunk.left.iter().all(|&x| x == 0.0));
|
||||
assert!(chunk.right.iter().all(|&x| x == 0.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
281
pmoaudio/src/nodes/timer_node.rs
Normal file
281
pmoaudio/src/nodes/timer_node.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use crate::{AudioChunk, nodes::{AudioError, MultiSubscriberNode}};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
|
||||
/// TimerNode - Node passthrough qui calcule la position temporelle
|
||||
///
|
||||
/// Ce node ne modifie pas les données audio, il les passe directement
|
||||
/// aux abonnés tout en maintenant un compteur de samples pour calculer
|
||||
/// la position en secondes.
|
||||
///
|
||||
/// # Fonctionnement
|
||||
///
|
||||
/// Pour chaque chunk reçu:
|
||||
/// 1. Incrémente `elapsed_samples += chunk.len()`
|
||||
/// 2. Calcule `position_sec = elapsed_samples / sample_rate`
|
||||
/// 3. Push le chunk (sans modification) vers les abonnés
|
||||
///
|
||||
/// # Utilisation
|
||||
///
|
||||
/// Le TimerNode fournit un [`TimerHandle`] qui permet de lire la position
|
||||
/// depuis d'autres threads/tasks sans bloquer le pipeline.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::TimerNode;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let (mut timer, timer_tx) = TimerNode::new(10);
|
||||
/// let handle = timer.get_position_handle();
|
||||
///
|
||||
/// tokio::spawn(async move {
|
||||
/// timer.run().await.unwrap();
|
||||
/// });
|
||||
///
|
||||
/// // Lire la position depuis un autre thread
|
||||
/// let position = handle.position_sec().await;
|
||||
/// println!("Position: {:.2} sec", position);
|
||||
/// }
|
||||
/// ```
|
||||
pub struct TimerNode {
|
||||
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
||||
subscribers: MultiSubscriberNode,
|
||||
elapsed_samples: Arc<RwLock<u64>>,
|
||||
current_sample_rate: Arc<RwLock<u32>>,
|
||||
}
|
||||
|
||||
impl TimerNode {
|
||||
/// Crée un nouveau TimerNode
|
||||
pub fn new(channel_size: usize) -> (Self, mpsc::Sender<Arc<AudioChunk>>) {
|
||||
let (tx, rx) = mpsc::channel(channel_size);
|
||||
|
||||
let node = Self {
|
||||
rx,
|
||||
subscribers: MultiSubscriberNode::new(),
|
||||
elapsed_samples: Arc::new(RwLock::new(0)),
|
||||
current_sample_rate: Arc::new(RwLock::new(48000)), // Default
|
||||
};
|
||||
|
||||
(node, tx)
|
||||
}
|
||||
|
||||
/// Ajoute un abonné
|
||||
pub fn add_subscriber(&mut self, tx: mpsc::Sender<Arc<AudioChunk>>) {
|
||||
self.subscribers.add_subscriber(tx);
|
||||
}
|
||||
|
||||
/// Retourne la position actuelle en secondes
|
||||
pub async fn position_sec(&self) -> f64 {
|
||||
let elapsed = *self.elapsed_samples.read().await;
|
||||
let sample_rate = *self.current_sample_rate.read().await;
|
||||
elapsed as f64 / sample_rate as f64
|
||||
}
|
||||
|
||||
/// Retourne le nombre total d'échantillons écoulés
|
||||
pub async fn elapsed_samples(&self) -> u64 {
|
||||
*self.elapsed_samples.read().await
|
||||
}
|
||||
|
||||
/// Reset le compteur
|
||||
pub async fn reset(&self) {
|
||||
let mut elapsed = self.elapsed_samples.write().await;
|
||||
*elapsed = 0;
|
||||
}
|
||||
|
||||
/// Démarre la boucle de traitement du TimerNode
|
||||
pub async fn run(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
// Mettre à jour le sample rate si nécessaire
|
||||
{
|
||||
let mut sr = self.current_sample_rate.write().await;
|
||||
if *sr != chunk.sample_rate {
|
||||
*sr = chunk.sample_rate;
|
||||
}
|
||||
}
|
||||
|
||||
// Incrémenter le compteur d'échantillons
|
||||
{
|
||||
let mut elapsed = self.elapsed_samples.write().await;
|
||||
*elapsed += chunk.len() as u64;
|
||||
}
|
||||
|
||||
// Push immédiatement le même chunk vers les abonnés (passthrough)
|
||||
self.subscribers.push(chunk).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Version non-bloquante avec try_push
|
||||
pub async fn run_nonblocking(mut self) -> Result<(), AudioError> {
|
||||
while let Some(chunk) = self.rx.recv().await {
|
||||
{
|
||||
let mut sr = self.current_sample_rate.write().await;
|
||||
if *sr != chunk.sample_rate {
|
||||
*sr = chunk.sample_rate;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut elapsed = self.elapsed_samples.write().await;
|
||||
*elapsed += chunk.len() as u64;
|
||||
}
|
||||
|
||||
self.subscribers.try_push(chunk).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retourne un handle pour lire la position depuis d'autres threads
|
||||
pub fn get_position_handle(&self) -> TimerHandle {
|
||||
TimerHandle {
|
||||
elapsed_samples: self.elapsed_samples.clone(),
|
||||
current_sample_rate: self.current_sample_rate.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle pour lire la position du TimerNode depuis d'autres threads
|
||||
///
|
||||
/// Ce handle peut être cloné et utilisé depuis plusieurs threads/tasks
|
||||
/// pour monitorer la position de lecture sans bloquer le pipeline.
|
||||
///
|
||||
/// # Exemples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use pmoaudio::TimerNode;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let (mut timer, _tx) = TimerNode::new(10);
|
||||
/// let handle = timer.get_position_handle();
|
||||
/// let handle_clone = handle.clone();
|
||||
///
|
||||
/// // Utiliser depuis plusieurs tasks
|
||||
/// tokio::spawn(async move {
|
||||
/// loop {
|
||||
/// let pos = handle_clone.position_sec().await;
|
||||
/// println!("Position: {:.2}s", pos);
|
||||
/// tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
/// }
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct TimerHandle {
|
||||
elapsed_samples: Arc<RwLock<u64>>,
|
||||
current_sample_rate: Arc<RwLock<u32>>,
|
||||
}
|
||||
|
||||
impl TimerHandle {
|
||||
/// Retourne la position actuelle en secondes
|
||||
pub async fn position_sec(&self) -> f64 {
|
||||
let elapsed = *self.elapsed_samples.read().await;
|
||||
let sample_rate = *self.current_sample_rate.read().await;
|
||||
elapsed as f64 / sample_rate as f64
|
||||
}
|
||||
|
||||
/// Retourne le nombre total d'échantillons écoulés
|
||||
pub async fn elapsed_samples(&self) -> u64 {
|
||||
*self.elapsed_samples.read().await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_timer_node_position_calculation() {
|
||||
let (mut node, tx) = TimerNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
let handle = node.get_position_handle();
|
||||
|
||||
// Spawn le node
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer 3 chunks de 1000 samples à 48000 Hz
|
||||
for i in 0..3 {
|
||||
let chunk = AudioChunk::new(i, vec![0.0; 1000], vec![0.0; 1000], 48000);
|
||||
tx.send(Arc::new(chunk)).await.unwrap();
|
||||
}
|
||||
|
||||
// Attendre que les chunks soient traités
|
||||
for _ in 0..3 {
|
||||
out_rx.recv().await.unwrap();
|
||||
}
|
||||
|
||||
// Vérifier la position
|
||||
let position = handle.position_sec().await;
|
||||
let expected = 3000.0 / 48000.0; // 3 chunks * 1000 samples / 48000 Hz
|
||||
assert!((position - expected).abs() < 0.0001);
|
||||
|
||||
let elapsed = handle.elapsed_samples().await;
|
||||
assert_eq!(elapsed, 3000);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_timer_node_passthrough() {
|
||||
let (mut node, tx) = TimerNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Envoyer un chunk
|
||||
let chunk = AudioChunk::new(42, vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0], 48000);
|
||||
let chunk_arc = Arc::new(chunk);
|
||||
tx.send(chunk_arc.clone()).await.unwrap();
|
||||
|
||||
// Recevoir le chunk
|
||||
let received = out_rx.recv().await.unwrap();
|
||||
|
||||
// Vérifier que c'est le même Arc (pas de clone des données)
|
||||
assert!(Arc::ptr_eq(&chunk_arc, &received));
|
||||
assert_eq!(received.order, 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_timer_node_sample_rate_change() {
|
||||
let (mut node, tx) = TimerNode::new(10);
|
||||
let (out_tx, mut out_rx) = mpsc::channel(10);
|
||||
|
||||
node.add_subscriber(out_tx);
|
||||
|
||||
let handle = node.get_position_handle();
|
||||
|
||||
tokio::spawn(async move {
|
||||
node.run().await.unwrap();
|
||||
});
|
||||
|
||||
// Chunk à 48000 Hz
|
||||
let chunk1 = AudioChunk::new(0, vec![0.0; 48000], vec![0.0; 48000], 48000);
|
||||
tx.send(Arc::new(chunk1)).await.unwrap();
|
||||
out_rx.recv().await.unwrap();
|
||||
|
||||
// Après 48000 samples à 48000 Hz = 1 seconde
|
||||
let pos1 = handle.position_sec().await;
|
||||
assert!((pos1 - 1.0).abs() < 0.0001);
|
||||
|
||||
// Chunk à 96000 Hz
|
||||
let chunk2 = AudioChunk::new(1, vec![0.0; 96000], vec![0.0; 96000], 96000);
|
||||
tx.send(Arc::new(chunk2)).await.unwrap();
|
||||
out_rx.recv().await.unwrap();
|
||||
|
||||
// Position calculée avec le nouveau sample rate
|
||||
let pos2 = handle.position_sec().await;
|
||||
let expected = (48000.0 + 96000.0) / 96000.0;
|
||||
assert!((pos2 - expected).abs() < 0.0001);
|
||||
}
|
||||
}
|
||||
152
pmoaudio/tests/integration_test.rs
Normal file
152
pmoaudio/tests/integration_test.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
//! Tests d'intégration pour le pipeline audio complet
|
||||
|
||||
use pmoaudio::{BufferNode, DecoderNode, DspNode, SinkNode, SourceNode, TimerNode};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_complete_pipeline() {
|
||||
// Créer un pipeline complet : Source → Decoder → DSP → Buffer → Timer → Sink
|
||||
|
||||
let (mut decoder, decoder_tx) = DecoderNode::new(10);
|
||||
let (mut dsp, dsp_tx) = DspNode::new(10, 0.5); // Gain de 0.5
|
||||
let (mut buffer, buffer_tx) = BufferNode::new(50, 10);
|
||||
let (mut timer, timer_tx) = TimerNode::new(10);
|
||||
let (sink, sink_tx) = SinkNode::new("Integration Test".to_string(), 10);
|
||||
|
||||
// Connecter le pipeline
|
||||
decoder.add_subscriber(dsp_tx);
|
||||
dsp.add_subscriber(buffer_tx);
|
||||
buffer.add_next_subscriber(timer_tx);
|
||||
timer.add_subscriber(sink_tx);
|
||||
|
||||
let timer_handle = timer.get_position_handle();
|
||||
|
||||
// Spawn tous les nodes
|
||||
tokio::spawn(async move { decoder.run_passthrough().await.unwrap() });
|
||||
tokio::spawn(async move { dsp.run().await.unwrap() });
|
||||
tokio::spawn(async move { buffer.run().await.unwrap() });
|
||||
tokio::spawn(async move { timer.run().await.unwrap() });
|
||||
|
||||
let sink_handle = tokio::spawn(async move { sink.run_with_stats().await.unwrap() });
|
||||
|
||||
// Générer des chunks
|
||||
tokio::spawn(async move {
|
||||
let mut source = SourceNode::new();
|
||||
source.add_subscriber(decoder_tx);
|
||||
source.generate_chunks(10, 4800, 48000, 440.0).await.unwrap();
|
||||
});
|
||||
|
||||
// Attendre la fin
|
||||
let stats = sink_handle.await.unwrap();
|
||||
|
||||
// Vérifier les résultats
|
||||
assert_eq!(stats.chunks_received, 10);
|
||||
assert_eq!(stats.total_samples, 48000);
|
||||
|
||||
// Vérifier que le gain a été appliqué (peak devrait être ~0.5)
|
||||
assert!(stats.peak_left < 0.51 && stats.peak_left > 0.49);
|
||||
|
||||
// Vérifier la position
|
||||
let position = timer_handle.position_sec().await;
|
||||
assert!((position - 1.0).abs() < 0.01); // ~1 seconde
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiroom_buffering() {
|
||||
// Tester le BufferNode avec plusieurs abonnés avec offsets
|
||||
|
||||
let (buffer, buffer_tx) = BufferNode::new(50, 20);
|
||||
|
||||
let (sink1, sink1_tx) = SinkNode::new("Room 1".to_string(), 20);
|
||||
let (sink2, sink2_tx) = SinkNode::new("Room 2".to_string(), 20);
|
||||
let (sink3, sink3_tx) = SinkNode::new("Room 3".to_string(), 20);
|
||||
|
||||
buffer.add_subscriber_with_offset(sink1_tx, 0).await;
|
||||
buffer.add_subscriber_with_offset(sink2_tx, 3).await;
|
||||
buffer.add_subscriber_with_offset(sink3_tx, 6).await;
|
||||
|
||||
tokio::spawn(async move { buffer.run().await.unwrap() });
|
||||
|
||||
let sink1_handle = tokio::spawn(async move { sink1.run_with_stats().await.unwrap() });
|
||||
let sink2_handle = tokio::spawn(async move { sink2.run_with_stats().await.unwrap() });
|
||||
let sink3_handle = tokio::spawn(async move { sink3.run_with_stats().await.unwrap() });
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut source = SourceNode::new();
|
||||
source.add_subscriber(buffer_tx);
|
||||
source.generate_chunks(20, 1000, 48000, 440.0).await.unwrap();
|
||||
});
|
||||
|
||||
let stats1 = sink1_handle.await.unwrap();
|
||||
let stats2 = sink2_handle.await.unwrap();
|
||||
let stats3 = sink3_handle.await.unwrap();
|
||||
|
||||
// Room 1 devrait avoir tous les chunks
|
||||
assert_eq!(stats1.chunks_received, 20);
|
||||
|
||||
// Room 2 devrait avoir 3 chunks de moins
|
||||
assert_eq!(stats2.chunks_received, 17);
|
||||
|
||||
// Room 3 devrait avoir 6 chunks de moins
|
||||
assert_eq!(stats3.chunks_received, 14);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_timer_accuracy() {
|
||||
// Tester la précision du TimerNode
|
||||
|
||||
let (mut timer, timer_tx) = TimerNode::new(10);
|
||||
let (sink, sink_tx) = SinkNode::new("Timer Test".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_silent().await.unwrap() });
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut source = SourceNode::new();
|
||||
source.add_subscriber(timer_tx);
|
||||
|
||||
// 48000 samples à 48kHz = 1 seconde
|
||||
source.generate_chunks(1, 48000, 48000, 440.0).await.unwrap();
|
||||
});
|
||||
|
||||
sink_handle.await.unwrap();
|
||||
|
||||
let position = timer_handle.position_sec().await;
|
||||
let samples = timer_handle.elapsed_samples().await;
|
||||
|
||||
assert_eq!(samples, 48000);
|
||||
assert!((position - 1.0).abs() < 0.0001);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_arc_sharing() {
|
||||
// Vérifier que les chunks sont bien partagés via Arc sans copie
|
||||
|
||||
let (mut timer, timer_tx) = TimerNode::new(10);
|
||||
let (sink1, sink1_tx) = SinkNode::new("Sink1".to_string(), 10);
|
||||
let (sink2, sink2_tx) = SinkNode::new("Sink2".to_string(), 10);
|
||||
|
||||
timer.add_subscriber(sink1_tx);
|
||||
timer.add_subscriber(sink2_tx);
|
||||
|
||||
tokio::spawn(async move { timer.run().await.unwrap() });
|
||||
|
||||
let sink1_handle = tokio::spawn(async move { sink1.run_with_stats().await.unwrap() });
|
||||
let sink2_handle = tokio::spawn(async move { sink2.run_with_stats().await.unwrap() });
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut source = SourceNode::new();
|
||||
source.add_subscriber(timer_tx);
|
||||
source.generate_silence(5, 1000, 48000).await.unwrap();
|
||||
});
|
||||
|
||||
let stats1 = sink1_handle.await.unwrap();
|
||||
let stats2 = sink2_handle.await.unwrap();
|
||||
|
||||
// Les deux sinks devraient avoir reçu les mêmes chunks
|
||||
assert_eq!(stats1.chunks_received, 5);
|
||||
assert_eq!(stats2.chunks_received, 5);
|
||||
assert_eq!(stats1.total_samples, stats2.total_samples);
|
||||
}
|
||||
Reference in New Issue
Block a user