427 lines
13 KiB
Markdown
427 lines
13 KiB
Markdown
|
|
# Résumé de l'implémentation - Extensions PMOAudio
|
|||
|
|
|
|||
|
|
## Objectif
|
|||
|
|
|
|||
|
|
Étendre le système de pipeline audio PMOAudio existant pour supporter :
|
|||
|
|
- Contrôle de volume dynamique avec synchronisation master/secondaire
|
|||
|
|
- Nouveaux types de sinks (Chromecast, MPD, Disk)
|
|||
|
|
- Système d'événements générique pour communication inter-nodes
|
|||
|
|
- Architecture multiroom avec flux dupliqués et volumes indépendants
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Modifications apportées
|
|||
|
|
|
|||
|
|
### 1. AudioChunk - Extension avec gain (src/audio_chunk.rs)
|
|||
|
|
|
|||
|
|
**Ajouts :**
|
|||
|
|
- Champ `gain: f32` (valeur par défaut : 1.0)
|
|||
|
|
- Méthode `with_gain()` : constructeur avec gain spécifique
|
|||
|
|
- Méthode `from_arc_with_gain()` : constructeur Arc avec gain
|
|||
|
|
- Méthode `apply_gain()` : matérialise le gain sur les samples
|
|||
|
|
- Méthode `with_modified_gain()` : modifie le gain sans copier les données
|
|||
|
|
|
|||
|
|
**Principe :** Le gain est stocké dans le chunk mais pas appliqué immédiatement (lazy evaluation). Cela permet de chaîner plusieurs transformations de volume sans copier les données audio.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. Système d'événements (src/events.rs) - NOUVEAU
|
|||
|
|
|
|||
|
|
**Composants créés :**
|
|||
|
|
|
|||
|
|
#### Traits et types de base
|
|||
|
|
- `NodeEvent` : trait pour tous les types d'événements
|
|||
|
|
- `NodeListener<E>` : trait pour écouter des événements
|
|||
|
|
- `EventPublisher<E>` : broadcaster d'événements type-safe
|
|||
|
|
- `EventReceiver<E>` : wrapper pour consommer des événements
|
|||
|
|
- `ClosureListener<E, F>` : listener basé sur une closure
|
|||
|
|
|
|||
|
|
#### Événements prédéfinis
|
|||
|
|
- `AudioDataEvent` : transport de chunks audio
|
|||
|
|
- `VolumeChangeEvent` : notification de changement de volume
|
|||
|
|
- `SourceNameUpdateEvent` : mise à jour du nom de la source
|
|||
|
|
|
|||
|
|
**Architecture :**
|
|||
|
|
```
|
|||
|
|
NodeA ──► EventPublisher<E> ──► mpsc::channel ──► EventReceiver<E> ──► NodeB
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Caractéristiques :**
|
|||
|
|
- Type-safe : chaque node ne reçoit que les événements qu'il attend
|
|||
|
|
- Non-bloquant : utilise `try_send` par défaut
|
|||
|
|
- Multi-subscriber : un événement peut être broadcasted à plusieurs nodes
|
|||
|
|
- Thread-safe : utilise les channels Tokio
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. VolumeNode (src/nodes/volume_node.rs) - NOUVEAU
|
|||
|
|
|
|||
|
|
**Fonctionnalités :**
|
|||
|
|
|
|||
|
|
#### Structure principale
|
|||
|
|
```rust
|
|||
|
|
pub struct VolumeNode {
|
|||
|
|
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
|||
|
|
subscribers: MultiSubscriberNode,
|
|||
|
|
volume: Arc<RwLock<f32>>,
|
|||
|
|
volume_publisher: EventPublisher<VolumeChangeEvent>,
|
|||
|
|
node_id: String,
|
|||
|
|
master_volume_rx: Option<mpsc::Receiver<VolumeChangeEvent>>,
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Modes d'utilisation
|
|||
|
|
|
|||
|
|
**Mode autonome :**
|
|||
|
|
```rust
|
|||
|
|
let (volume_node, tx) = VolumeNode::new("room1", 0.8, 10);
|
|||
|
|
let handle = volume_node.get_handle();
|
|||
|
|
handle.set_volume(0.5).await;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Mode master/slave :**
|
|||
|
|
```rust
|
|||
|
|
// Master
|
|||
|
|
let (mut master, master_tx) = VolumeNode::new("master", 1.0, 10);
|
|||
|
|
let (event_tx, event_rx) = mpsc::channel(10);
|
|||
|
|
master.subscribe_volume_events(event_tx);
|
|||
|
|
|
|||
|
|
// Slave
|
|||
|
|
let (mut slave, slave_tx) = VolumeNode::new("slave", 0.8, 10);
|
|||
|
|
slave.set_master_volume_source(event_rx);
|
|||
|
|
|
|||
|
|
// Le slave applique : gain = local_volume × master_volume
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### VolumeHandle
|
|||
|
|
- Permet le contrôle du volume depuis un contexte externe
|
|||
|
|
- Thread-safe via `Arc<RwLock<f32>>`
|
|||
|
|
- Méthodes : `set_volume()`, `get_volume()`, `adjust_volume()`
|
|||
|
|
|
|||
|
|
#### HardwareVolumeNode
|
|||
|
|
- Wrapper autour de VolumeNode
|
|||
|
|
- Prévu pour contrôle matériel (actuellement identique)
|
|||
|
|
- Extension future : intégration avec drivers système
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. DiskSink (src/nodes/disk_sink.rs) - NOUVEAU
|
|||
|
|
|
|||
|
|
**Fonctionnalités :**
|
|||
|
|
|
|||
|
|
#### Écriture sur disque
|
|||
|
|
- Formats supportés : WAV, FLAC (mock), PCM brut
|
|||
|
|
- Écriture asynchrone avec Tokio
|
|||
|
|
- Application automatique du gain avant écriture
|
|||
|
|
- Gestion d'en-têtes WAV avec mise à jour à la fermeture
|
|||
|
|
|
|||
|
|
#### Dérivation automatique du nom
|
|||
|
|
```rust
|
|||
|
|
let config = DiskSinkConfig {
|
|||
|
|
output_dir: PathBuf::from("/tmp/audio"),
|
|||
|
|
filename: None, // Sera dérivé du nom de source
|
|||
|
|
..Default::default()
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
disk_sink.set_source_name_source(source_name_rx);
|
|||
|
|
|
|||
|
|
// Quand un SourceNameUpdateEvent arrive :
|
|||
|
|
// "/tmp/audio/${source_name}.wav"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Structure
|
|||
|
|
```rust
|
|||
|
|
pub struct DiskSink {
|
|||
|
|
rx: mpsc::Receiver<Arc<AudioChunk>>,
|
|||
|
|
config: DiskSinkConfig,
|
|||
|
|
resolved_filename: Arc<RwLock<Option<PathBuf>>>,
|
|||
|
|
source_name_rx: Option<mpsc::Receiver<SourceNameUpdateEvent>>,
|
|||
|
|
writer: Option<AudioFileWriter>,
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Writer WAV
|
|||
|
|
- En-tête RIFF/WAVE standard
|
|||
|
|
- Format : 16-bit PCM stéréo little-endian
|
|||
|
|
- Mise à jour des tailles à la fermeture
|
|||
|
|
- Interleaving automatique des canaux
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5. ChromecastSink (src/nodes/chromecast_sink.rs) - NOUVEAU (mock)
|
|||
|
|
|
|||
|
|
**Configuration :**
|
|||
|
|
```rust
|
|||
|
|
pub struct ChromecastConfig {
|
|||
|
|
device_address: String, // IP du Chromecast
|
|||
|
|
device_name: String, // Nom amical
|
|||
|
|
port: u16, // Défaut: 8009
|
|||
|
|
buffer_size: usize,
|
|||
|
|
encoding: StreamEncoding, // Mp3, Aac, Opus, Pcm
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Implémentation actuelle :**
|
|||
|
|
- Mock qui simule la connexion et l'envoi
|
|||
|
|
- Prêt pour intégration avec `rust-cast` ou similaire
|
|||
|
|
|
|||
|
|
**Workflow prévu pour vraie implémentation :**
|
|||
|
|
1. Connexion TLS avec le device
|
|||
|
|
2. Lancement d'une application de récepteur
|
|||
|
|
3. Encodage de l'audio dans le format choisi
|
|||
|
|
4. Streaming via HTTP ou WebSocket
|
|||
|
|
5. Gestion des commandes (play, pause, stop)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 6. MpdSink (src/nodes/mpd_sink.rs) - NOUVEAU (mock)
|
|||
|
|
|
|||
|
|
**Configuration :**
|
|||
|
|
```rust
|
|||
|
|
pub struct MpdConfig {
|
|||
|
|
host: String, // Adresse du serveur
|
|||
|
|
port: u16, // Défaut: 6600
|
|||
|
|
password: Option<String>,
|
|||
|
|
output_name: Option<String>,
|
|||
|
|
format: MpdAudioFormat, // S16Le, S24Le, S32Le, F32
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**MpdHandle :**
|
|||
|
|
```rust
|
|||
|
|
let handle = mpd_sink.get_handle();
|
|||
|
|
handle.play().await;
|
|||
|
|
handle.pause().await;
|
|||
|
|
handle.set_volume(75).await; // 0-100
|
|||
|
|
handle.stop().await;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Implémentation actuelle :**
|
|||
|
|
- Mock qui simule la communication MPD
|
|||
|
|
- Prêt pour intégration avec protocole MPD complet
|
|||
|
|
|
|||
|
|
**Workflow prévu pour vraie implémentation :**
|
|||
|
|
1. Connexion TCP au serveur MPD
|
|||
|
|
2. Lecture de la bannière de version
|
|||
|
|
3. Authentification si nécessaire
|
|||
|
|
4. Configuration du format audio
|
|||
|
|
5. Streaming des données PCM
|
|||
|
|
6. Gestion des commandes via protocole texte MPD
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Architecture multiroom complète
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌──────────────┐
|
|||
|
|
│ SourceNode │
|
|||
|
|
│ (generate) │
|
|||
|
|
└──────┬───────┘
|
|||
|
|
│
|
|||
|
|
│ AudioChunk { gain: 1.0 }
|
|||
|
|
▼
|
|||
|
|
┌──────────────┐
|
|||
|
|
│ MasterVolume │
|
|||
|
|
│ (volume=1.0) │
|
|||
|
|
└──────┬───────┘
|
|||
|
|
│ ├─► VolumeChangeEvent
|
|||
|
|
│
|
|||
|
|
┌─────────────┴─────────────┐
|
|||
|
|
│ │
|
|||
|
|
▼ ▼
|
|||
|
|
┌─────────────────┐ ┌─────────────────┐
|
|||
|
|
│ChromecastVolume │ │ DiskVolume │
|
|||
|
|
│ local = 0.8 │ │ local = 0.9 │
|
|||
|
|
│ ◄─ Master evt │ │ ◄─ Master evt │
|
|||
|
|
└────────┬────────┘ └────────┬────────┘
|
|||
|
|
│ │
|
|||
|
|
│ gain = 1.0×0.8 │ gain = 1.0×0.9
|
|||
|
|
▼ ▼
|
|||
|
|
┌─────────────────┐ ┌─────────────────┐
|
|||
|
|
│ ChromecastSink │ │ DiskSink │
|
|||
|
|
│ 192.168.1.100 │ │ output.wav │
|
|||
|
|
│ apply_gain() │ │ apply_gain() │
|
|||
|
|
└─────────────────┘ └─────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Flux des données
|
|||
|
|
|
|||
|
|
1. **SourceNode** : génère chunks avec `gain = 1.0`
|
|||
|
|
2. **MasterVolume** :
|
|||
|
|
- Multiplie `chunk.gain *= master_volume`
|
|||
|
|
- Publie `VolumeChangeEvent` si changement
|
|||
|
|
3. **Volumes secondaires** :
|
|||
|
|
- Reçoivent les chunks du master
|
|||
|
|
- Écoutent les `VolumeChangeEvent` du master
|
|||
|
|
- Appliquent : `chunk.gain *= local_volume`
|
|||
|
|
4. **Sinks** :
|
|||
|
|
- Appellent `chunk.apply_gain()` pour matérialiser
|
|||
|
|
- Envoient/écrivent les données finales
|
|||
|
|
|
|||
|
|
### Avantages
|
|||
|
|
|
|||
|
|
- **Zero-copy** : les données audio ne sont pas copiées entre branches
|
|||
|
|
- **Lazy evaluation** : le gain n'est appliqué qu'au moment de l'output
|
|||
|
|
- **Synchronisation** : tous les volumes secondaires reçoivent les mises à jour master
|
|||
|
|
- **Indépendance** : chaque branche peut avoir son propre volume local
|
|||
|
|
- **Extensibilité** : facile d'ajouter de nouvelles branches
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Tests
|
|||
|
|
|
|||
|
|
### Tests unitaires ajoutés
|
|||
|
|
|
|||
|
|
**VolumeNode (5 tests) :**
|
|||
|
|
- `test_volume_node_basic` : modification de gain
|
|||
|
|
- `test_volume_handle` : contrôle via handle
|
|||
|
|
- `test_volume_events` : publication d'événements
|
|||
|
|
- `test_master_slave_volume` : synchronisation master/slave
|
|||
|
|
- (test dans volume_node.rs)
|
|||
|
|
|
|||
|
|
**DiskSink (1 test) :**
|
|||
|
|
- `test_disk_sink_basic` : écriture WAV complète
|
|||
|
|
- (test dans disk_sink.rs)
|
|||
|
|
|
|||
|
|
**ChromecastSink (1 test) :**
|
|||
|
|
- `test_chromecast_sink_basic` : mock de streaming
|
|||
|
|
- (test dans chromecast_sink.rs)
|
|||
|
|
|
|||
|
|
**MpdSink (2 tests) :**
|
|||
|
|
- `test_mpd_sink_basic` : mock de communication
|
|||
|
|
- `test_mpd_handle` : commandes de contrôle
|
|||
|
|
- (test dans mpd_sink.rs)
|
|||
|
|
|
|||
|
|
**Events (3 tests) :**
|
|||
|
|
- `test_event_publisher_basic` : publication simple
|
|||
|
|
- `test_multiple_subscribers` : broadcast multiple
|
|||
|
|
- `test_event_receiver` : réception
|
|||
|
|
- (test dans events.rs)
|
|||
|
|
|
|||
|
|
### Résultat
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
31 passed; 0 failed; 0 ignored
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Tous les tests existants continuent de passer + 12 nouveaux tests.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Exemples fournis
|
|||
|
|
|
|||
|
|
### 1. volume_control_demo.rs
|
|||
|
|
- Pipeline simple : Source → Volume → Sink
|
|||
|
|
- Changements dynamiques de volume pendant la lecture
|
|||
|
|
- Démonstration du VolumeHandle
|
|||
|
|
|
|||
|
|
### 2. multiroom_volume_demo.rs
|
|||
|
|
- Pipeline complet avec 2 branches
|
|||
|
|
- Volume master + 2 volumes secondaires
|
|||
|
|
- Chromecast + DiskSink en parallèle
|
|||
|
|
- Contrôle dynamique du master
|
|||
|
|
- Démonstration du système d'événements
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Contraintes respectées
|
|||
|
|
|
|||
|
|
### ✅ Pas de duplication
|
|||
|
|
- Utilisation des structures existantes (`MultiSubscriberNode`, `AudioError`)
|
|||
|
|
- Extension propre de `AudioChunk` sans casser l'API
|
|||
|
|
- Réutilisation du système de channels Tokio
|
|||
|
|
|
|||
|
|
### ✅ Zero-copy
|
|||
|
|
- `Arc<AudioChunk>` partagé entre branches
|
|||
|
|
- Modification du gain sans copie de données
|
|||
|
|
- Application lazy uniquement au sink
|
|||
|
|
|
|||
|
|
### ✅ Thread-safety
|
|||
|
|
- `Arc<RwLock<f32>>` pour le volume
|
|||
|
|
- Channels Tokio bounded
|
|||
|
|
- `EventPublisher` non-bloquant avec `try_send`
|
|||
|
|
|
|||
|
|
### ✅ Compatibilité
|
|||
|
|
- Toutes les signatures publiques existantes préservées
|
|||
|
|
- Pas de breaking changes
|
|||
|
|
- Extensions additives uniquement
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Statistiques du code
|
|||
|
|
|
|||
|
|
### Fichiers créés
|
|||
|
|
1. `src/events.rs` - 220 lignes
|
|||
|
|
2. `src/nodes/volume_node.rs` - 330 lignes
|
|||
|
|
3. `src/nodes/disk_sink.rs` - 480 lignes
|
|||
|
|
4. `src/nodes/chromecast_sink.rs` - 280 lignes
|
|||
|
|
5. `src/nodes/mpd_sink.rs` - 320 lignes
|
|||
|
|
6. `examples/volume_control_demo.rs` - 55 lignes
|
|||
|
|
7. `examples/multiroom_volume_demo.rs` - 150 lignes
|
|||
|
|
|
|||
|
|
### Fichiers modifiés
|
|||
|
|
1. `src/audio_chunk.rs` - ajout de ~50 lignes
|
|||
|
|
2. `src/lib.rs` - ajout d'exports
|
|||
|
|
3. `src/nodes/mod.rs` - ajout de modules
|
|||
|
|
|
|||
|
|
### Total
|
|||
|
|
- **~1900 lignes de code** ajoutées
|
|||
|
|
- **31 tests unitaires** (12 nouveaux)
|
|||
|
|
- **2 exemples complets**
|
|||
|
|
- **0 breaking changes**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Extensions futures possibles
|
|||
|
|
|
|||
|
|
### Court terme
|
|||
|
|
1. **Implémentation réelle des sinks :**
|
|||
|
|
- ChromecastSink avec `rust-cast`
|
|||
|
|
- MpdSink avec protocole MPD
|
|||
|
|
- DiskSink FLAC avec `claxon` ou `symphonia`
|
|||
|
|
|
|||
|
|
2. **Nouveaux sinks :**
|
|||
|
|
- AirPlaySink
|
|||
|
|
- PulseAudioSink / AlsaSink
|
|||
|
|
- HttpStreamSink (serveur Icecast)
|
|||
|
|
|
|||
|
|
### Moyen terme
|
|||
|
|
3. **Nodes DSP avancés :**
|
|||
|
|
- EqualizerNode (bandes paramétriques)
|
|||
|
|
- CompressorNode / LimiterNode
|
|||
|
|
- ReverbNode
|
|||
|
|
- CrossfadeNode
|
|||
|
|
|
|||
|
|
4. **Synchronisation multi-device :**
|
|||
|
|
- Timing précis avec NTP/PTP
|
|||
|
|
- Compensation de latence
|
|||
|
|
- Buffer adaptatif
|
|||
|
|
|
|||
|
|
### Long terme
|
|||
|
|
5. **Room correction :**
|
|||
|
|
- Mesure acoustique
|
|||
|
|
- FIR filters
|
|||
|
|
- Compensation de phase
|
|||
|
|
|
|||
|
|
6. **Interface de contrôle :**
|
|||
|
|
- API REST
|
|||
|
|
- WebSocket pour temps réel
|
|||
|
|
- Dashboard web
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Conclusion
|
|||
|
|
|
|||
|
|
L'implémentation est **complète, fonctionnelle et testée**. Elle respecte toutes les contraintes :
|
|||
|
|
- ✅ Architecture existante préservée
|
|||
|
|
- ✅ Zero-copy maintenu
|
|||
|
|
- ✅ Thread-safety garantie
|
|||
|
|
- ✅ Pas de breaking changes
|
|||
|
|
- ✅ Code documenté et testé
|
|||
|
|
- ✅ Exemples fournis
|
|||
|
|
|
|||
|
|
Le système est prêt pour :
|
|||
|
|
- Utilisation en production (avec implémentation des vrais sinks)
|
|||
|
|
- Extension avec de nouveaux types de nodes
|
|||
|
|
- Intégration dans un système complet multiroom
|